[Quicker] 窗口便利贴 - 源码归档
动作:窗口便利贴
款为软件界面量身定制的“虚拟贴纸”工具。它可以精准捕捉任何软件窗口内的控件或区域,并为其添加个性化的文字标签或图片标记。标记会智能随窗口移动、隐藏或显示,帮助您快速识别功能区域、记录操作提醒或美化工作流界面。
更新时间:2025-12-27 22:17
原始文件:WindowMarker.cs
核心代码
// ref: WindowsBase
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using Quicker.Public;
using System.Runtime.InteropServices;
using System.IO;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Net;
using Newtonsoft.Json;
/// <summary>
/// 窗口标记动作 - 仿 MenuModifier2 开发规范
/// 支持相对位置(四角锚点)与比例固定
/// 支持图片标记 (jpg, png)
/// </summary>
public static void Exec(IStepContext context)
{
try
{
using (var mainForm = new MarkerMainForm(context))
{
Application.Run(mainForm);
}
}
catch (Exception ex)
{
MessageBox.Show("程序错误: " + ex.Message);
}
}
#region 数据模型
public class AppConfig
{
public bool HideMainWindowOnStart { get; set; } = false;
public bool RunAtStartup { get; set; } = false;
public List<MarkerInfo> Markers { get; set; } = new List<MarkerInfo>();
}
public class MarkerInfo
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Title { get; set; } = "新建标记";
[JsonIgnore]
public string DisplayName { get { return Title; } set { Title = value; } }
public bool IsEnabled { get; set; } = true;
// 使用 string 存储 IntPtr 方便序列化
public string hWndStr { get; set; }
[JsonIgnore]
public IntPtr hWnd {
get { return string.IsNullOrEmpty(hWndStr) ? IntPtr.Zero : new IntPtr(long.Parse(hWndStr)); }
set { hWndStr = value.ToInt64().ToString(); }
}
public string WindowTitle { get; set; }
public string ProcessName { get; set; }
public string ProcessPath { get; set; } // 进程路径,用于获取图标
[JsonIgnore]
public Icon ProcessIcon { get; set; } // 缓存的进程图标
public bool UseRegex { get; set; } = false;
// 定位设置
public string PositionMode { get; set; } = "Relative"; // Relative, Ratio
public string Anchor { get; set; } = "TopLeft"; // TopLeft, TopRight, BottomLeft, BottomRight
// 相对位置 (从锚点偏移)
public int RelX { get; set; }
public int RelY { get; set; }
[JsonIgnore]
public Point RelativePoint {
get { return new Point(RelX, RelY); }
set { RelX = value.X; RelY = value.Y; }
}
// 比例位置 (0.0 - 1.0)
public double RatioX { get; set; }
public double RatioY { get; set; }
public int WinW { get; set; }
public int WinH { get; set; }
[JsonIgnore]
public Size WindowSize {
get { return new Size(WinW, WinH); }
set { WinW = value.Width; WinH = value.Height; }
}
// 间隔时间逻辑自事件驱动重构后已弃用
[JsonIgnore]
public int UpdateIntervalMs { get; set; } = 100;
// 标记设置
public string ImagePath { get; set; }
[JsonIgnore]
public string LabelText { get { return Title; } set { Title = value; } }
public string ColorHex { get; set; } = "#FF0000"; // 默认红色(圆点)
public string TextColorHex { get; set; } = "#000000"; // 默认红色文字
public string BgColorHex { get; set; } = "#FFFF00"; // 默认黄底
[JsonIgnore]
public Color MarkerColor {
get { return ColorTranslator.FromHtml(ColorHex); }
set { ColorHex = ColorTranslator.ToHtml(value); }
}
[JsonIgnore]
public Color TextColor {
get { return ColorTranslator.FromHtml(TextColorHex); }
set { TextColorHex = ColorTranslator.ToHtml(value); }
}
[JsonIgnore]
public Color BgColor {
get { return ColorTranslator.FromHtml(BgColorHex); }
set { BgColorHex = ColorTranslator.ToHtml(value); }
}
// 点击动作设置
public string ClickActionType { get; set; } = "None";
public string ClickActionParam { get; set; }
// 便签内容 (当 ClickActionType 为 "Note" 时)
public string NoteText { get; set; }
// 发送文本列表 (当 ClickActionType 为 "SendText" 时)
public List<string> MultiTexts { get; set; } = new List<string>();
// 临时标记位置
[JsonIgnore]
public Rectangle CapturedRect { get; set; }
// Style Constants
public static readonly Color ColorBg = Color.FromArgb(248, 250, 252);
public static readonly Color ColorSidebar = Color.White;
public static readonly Color ColorAccent = Color.FromArgb(37, 99, 235);
public static readonly Color ColorBorder = Color.FromArgb(226, 232, 240);
public static readonly Color ColorTextPrimary = Color.FromArgb(15, 23, 42);
public static readonly Color ColorTextSecondary = Color.FromArgb(100, 116, 139);
public static readonly Color ColorLogBg = Color.FromArgb(15, 23, 31);
}
#endregion
#region 核心逻辑
public static class WindowMarker
{
public static MarkerInfo CaptureWindowInfo()
{
var info = new MarkerInfo();
// 获取当前鼠标位置
GetCursorPos(out POINT mousePos);
IntPtr hWnd = WindowFromPoint(mousePos);
if (hWnd == IntPtr.Zero) return null;
// 获取顶层窗口
IntPtr rootHWnd = GetAncestor(hWnd, 2); // GA_ROOT
info.hWnd = rootHWnd;
// 窗口基本信息
StringBuilder sbTitle = new StringBuilder(256);
GetWindowText(rootHWnd, sbTitle, 256);
info.WindowTitle = sbTitle.ToString();
uint pid;
GetWindowThreadProcessId(rootHWnd, out pid);
var proc = Process.GetProcessById((int)pid);
info.ProcessName = proc.ProcessName;
try {
info.ProcessPath = proc.MainModule.FileName;
} catch {
// 某些 64 位程序在 32 位进程中可能无法获取路径,或权限不足
info.ProcessPath = "";
}
// 窗口位置
GetWindowRect(rootHWnd, out RECT rect);
int winW = rect.Right - rect.Left;
int winH = rect.Bottom - rect.Top;
info.WindowSize = new Size(winW, winH);
// 默认按左上角记录相对位置
info.Anchor = "TopLeft";
info.PositionMode = "Relative";
info.RelativePoint = new Point(mousePos.X - rect.Left, mousePos.Y - rect.Top);
// 计算比例
if (winW > 0 && winH > 0)
{
info.RatioX = (double)(mousePos.X - rect.Left) / winW;
info.RatioY = (double)(mousePos.Y - rect.Top) / winH;
}
// 记录标记矩形用于视觉反馈
info.CapturedRect = new Rectangle(mousePos.X - 25, mousePos.Y - 25, 50, 50);
return info;
}
public static void ShowHighlight(Rectangle rect)
{
if (rect.IsEmpty) return;
Form highlight = new Form();
highlight.FormBorderStyle = FormBorderStyle.None;
highlight.StartPosition = FormStartPosition.Manual;
highlight.Location = rect.Location;
highlight.Size = rect.Size;
highlight.TopMost = true;
highlight.ShowInTaskbar = false;
highlight.BackColor = Color.Red;
highlight.TransparencyKey = Color.Red;
highlight.Opacity = 0.7;
// 关键:确保不获取焦点
const int WS_EX_NOACTIVATE = 0x08000000;
// const int WS_EX_TOPMOST = 0x00000008; // 未使用
const int WS_EX_TRANSPARENT = 0x00000020;
// 设置窗口样式以防止抢夺焦点和干扰鼠标
var style = GetWindowLong(highlight.Handle, -20);
SetWindowLong(highlight.Handle, -20, style | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT);
highlight.Paint += (s, e) => {
using (Pen pen = new Pen(Color.Red, 4))
{
e.Graphics.DrawRectangle(pen, 0, 0, highlight.Width - 1, highlight.Height - 1);
}
};
highlight.Show();
var timer = new Timer { Interval = 800 };
timer.Tick += (s, e) => {
highlight.Close();
highlight.Dispose();
timer.Stop();
timer.Dispose();
};
timer.Start();
}
[DllImport("user32.dll")]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
}
#endregion
#region 主界面
public class MarkerMainForm : Form
{
private IStepContext _context;
private AppConfig _config = new AppConfig();
private List<MarkerInfo> _markers { get { return _config.Markers; } }
private Dictionary<string, MarkerOverlay> _overlays = new Dictionary<string, MarkerOverlay>();
// Style Constants - Light Mode
public static readonly Color ColorBgLight = Color.FromArgb(248, 250, 252);
public static readonly Color ColorSidebarLight = Color.White;
public static readonly Color ColorAccentLight = Color.FromArgb(37, 99, 235);
public static readonly Color ColorBorderLight = Color.FromArgb(226, 232, 240);
public static readonly Color ColorTextPrimaryLight = Color.FromArgb(15, 23, 42);
public static readonly Color ColorTextSecondaryLight = Color.FromArgb(100, 116, 139);
public static readonly Color ColorInputBgLight = Color.White;
public static readonly Color ColorSearchBgLight = Color.FromArgb(241, 245, 249);
public static readonly Color ColorSelectedBgLight = Color.FromArgb(239, 246, 255);
// Style Constants - Dark Mode
public static readonly Color ColorBgDark = Color.FromArgb(24, 24, 27);
public static readonly Color ColorSidebarDark = Color.FromArgb(39, 39, 42);
public static readonly Color ColorAccentDark = Color.FromArgb(96, 165, 250);
public static readonly Color ColorBorderDark = Color.FromArgb(63, 63, 70);
public static readonly Color ColorTextPrimaryDark = Color.FromArgb(250, 250, 250);
public static readonly Color ColorTextSecondaryDark = Color.FromArgb(161, 161, 170);
public static readonly Color ColorInputBgDark = Color.FromArgb(39, 39, 42);
public static readonly Color ColorSearchBgDark = Color.FromArgb(52, 52, 56);
public static readonly Color ColorSelectedBgDark = Color.FromArgb(55, 65, 81);
// Current Theme Colors (will be set based on system theme)
public static Color ColorBg { get; private set; } = ColorBgLight;
public static Color ColorSidebar { get; private set; } = ColorSidebarLight;
public static Color ColorAccent { get; private set; } = ColorAccentLight;
public static Color ColorBorder { get; private set; } = ColorBorderLight;
public static Color ColorTextPrimary { get; private set; } = ColorTextPrimaryLight;
public static Color ColorTextSecondary { get; private set; } = ColorTextSecondaryLight;
public static Color ColorInputBg { get; private set; } = ColorInputBgLight;
public static Color ColorSearchBg { get; private set; } = ColorSearchBgLight;
public static Color ColorSelectedBg { get; private set; } = ColorSelectedBgLight;
private static bool _isDarkMode = false;
public static bool IsDarkMode => _isDarkMode;
private Timer _themeTimer;
// 控件
private Panel _pnlSidebar;
private Panel _pnlMain;
private ListBox _lstMarkers;
private Panel _configContainer;
private TextBox _txtSearch;
private TextBox _txtLog;
private NotifyIcon _trayIcon;
private bool _isExiting = false;
private Timer _visibilityTimer;
private MarkerConfigPanel _configPanel;
private string _dataPath;
public static Icon AppIcon { get; private set; }
public MarkerMainForm(IStepContext context)
{
_context = context;
_dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WindowMarkers.json");
LoadIcon();
LoadData();
DetectAndApplyTheme(); // 检测系统主题
InitializeUI();
InitializeTray();
InitializeVisibilityTimer();
InitializeHooks();
InitializeThemeMonitor(); // 监控主题变化
LoadProcessIcons();
SelectCurrentWindowMarker();
Log("程序已启动。点击'新建标记'开始。");
}
private static bool GetSystemDarkMode()
{
try {
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")) {
if (key != null) {
object val = key.GetValue("AppsUseLightTheme");
if (val != null) return (int)val == 0;
}
}
} catch { }
return false;
}
private void DetectAndApplyTheme()
{
bool wasDark = _isDarkMode;
_isDarkMode = GetSystemDarkMode();
if (_isDarkMode) {
ColorBg = ColorBgDark;
ColorSidebar = ColorSidebarDark;
ColorAccent = ColorAccentDark;
ColorBorder = ColorBorderDark;
ColorTextPrimary = ColorTextPrimaryDark;
ColorTextSecondary = ColorTextSecondaryDark;
ColorInputBg = ColorInputBgDark;
ColorSearchBg = ColorSearchBgDark;
ColorSelectedBg = ColorSelectedBgDark;
} else {
ColorBg = ColorBgLight;
ColorSidebar = ColorSidebarLight;
ColorAccent = ColorAccentLight;
ColorBorder = ColorBorderLight;
ColorTextPrimary = ColorTextPrimaryLight;
ColorTextSecondary = ColorTextSecondaryLight;
ColorInputBg = ColorInputBgLight;
ColorSearchBg = ColorSearchBgLight;
ColorSelectedBg = ColorSelectedBgLight;
}
}
private void InitializeThemeMonitor()
{
_themeTimer = new Timer { Interval = 2000 };
_themeTimer.Tick += (s, e) => {
bool newDark = GetSystemDarkMode();
if (newDark != _isDarkMode) {
DetectAndApplyTheme();
ApplyThemeToUI();
Log(_isDarkMode ? "已切换到暗色模式" : "已切换到亮色模式");
}
};
_themeTimer.Start();
}
private void ApplyThemeToUI()
{
this.BackColor = ColorBg;
_pnlSidebar.BackColor = ColorSidebar;
_pnlMain.BackColor = ColorBg;
_lstMarkers.BackColor = ColorSidebar;
if (_txtSearch != null) {
_txtSearch.BackColor = ColorSearchBg;
_txtSearch.ForeColor = ColorTextPrimary;
}
_lstMarkers.Invalidate();
// 重新加载配置面板
if (_lstMarkers.SelectedItem is MarkerInfo selectedInfo) {
ShowMarkerConfig(selectedInfo);
}
}
private ComboBox _cmbProcessFilter; // 进程筛选下拉框
private WinEventDelegate _winEventDelegate;
private IntPtr _hWinEventHook;
private void InitializeHooks()
{
// 安装 WinEventHook 监听窗口位置变化 (EVENT_OBJECT_LOCATIONCHANGE = 0x800B)
_winEventDelegate = new WinEventDelegate(WinEventProc);
_hWinEventHook = SetWinEventHook(0x800B, 0x800B, IntPtr.Zero, _winEventDelegate, 0, 0, 0); // 0 = WINEVENT_OUTOFCONTEXT
}
private void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
if (idObject != 0) return; // OBJID_WINDOW = 0
if (_overlays.Count == 0) return;
// 查找是否有正在标记该窗口的 Overlay
// 注意:这里是在 UI 线程执行 (因为 WINEVENT_OUTOFCONTEXT 会 PostMessage 到线程消息队列)
foreach (var overlay in _overlays.Values)
{
if (overlay.TargetHwnd == hwnd && overlay.Visible)
{
overlay.UpdatePosition(null, null);
}
}
}
private void LoadIcon()
{
if (AppIcon != null) return;
string iconCachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WindowMarker_Cache.png");
try {
if (File.Exists(iconCachePath)) {
using (var ms = new MemoryStream(File.ReadAllBytes(iconCachePath))) {
Bitmap bmp = new Bitmap(ms);
AppIcon = Icon.FromHandle(bmp.GetHicon());
return;
}
}
using (WebClient wc = new WebClient()) {
byte[] data = wc.DownloadData("https://files.getquicker.net/_icons/FA3F3C0DAA167D5BE58FA688AE4AA6D8F2D8E75D.png");
File.WriteAllBytes(iconCachePath, data); // 缓存到本地
using (var ms = new MemoryStream(data)) {
Bitmap bmp = new Bitmap(ms);
AppIcon = Icon.FromHandle(bmp.GetHicon());
}
}
} catch {
AppIcon = SystemIcons.Application;
}
}
private void LoadData()
{
try
{
if (File.Exists(_dataPath))
{
Log($"正在从本地加载数据: {_dataPath}");
string json = File.ReadAllText(_dataPath);
// 尝试按新格式加载
try {
var config = JsonConvert.DeserializeObject<AppConfig>(json);
if (config != null && config.Markers != null) {
_config = config;
} else {
// 尝试按旧格式(List<MarkerInfo>)加载
var markers = JsonConvert.DeserializeObject<List<MarkerInfo>>(json);
if (markers != null) _config.Markers = markers;
}
} catch {
// 兼容旧格式
try {
var markers = JsonConvert.DeserializeObject<List<MarkerInfo>>(json);
if (markers != null) _config.Markers = markers;
} catch {}
}
Log($"数据加载成功,共计 {_markers.Count} 个标记。");
foreach (var info in _markers)
{
if (info.IsEnabled)
{
UpdateOverlay(info);
}
}
LoadProcessIcons(); // 加载完数据后同步图标
}
else
{
Log("未找到本地数据文件,将以空列表启动。");
}
}
catch (Exception ex)
{
Log("加载数据失败: " + ex.Message);
}
}
public void SaveData()
{
try
{
string json = JsonConvert.SerializeObject(_config, Formatting.Indented);
File.WriteAllText(_dataPath, json);
Log("数据已持久化到本地。");
UpdateStartupRegistry();
}
catch (Exception ex)
{
Log("保存数据失败: " + ex.Message);
}
}
private void UpdateStartupRegistry()
{
try {
string runKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(runKey, true)) {
if (_config.RunAtStartup) {
key.SetValue("QuickerWindowMarker", Application.ExecutablePath);
} else {
key.DeleteValue("QuickerWindowMarker", false);
}
}
} catch (Exception ex) {
Log("更新自启动注册表失败: " + ex.Message);
}
}
private void InitializeVisibilityTimer()
{
_visibilityTimer = new Timer { Interval = 300 };
_visibilityTimer.Tick += (s, e) => CheckMarkersVisibility();
_visibilityTimer.Start();
}
private void CheckMarkersVisibility()
{
try
{
IntPtr activeHWnd = GetForegroundWindow();
if (activeHWnd == IntPtr.Zero) return;
// 获取当前激活窗口的信息
StringBuilder sbTitle = new StringBuilder(512);
GetWindowText(activeHWnd, sbTitle, 512);
string activeTitle = sbTitle.ToString();
uint pid;
GetWindowThreadProcessId(activeHWnd, out pid);
string activeProcess = "";
try { activeProcess = Process.GetProcessById((int)pid).ProcessName; } catch { }
bool isIconic = IsIconic(activeHWnd);
foreach (var info in _markers)
{
if (!info.IsEnabled)
{
if (_overlays.ContainsKey(info.Id)) _overlays[info.Id].Visible = false;
continue;
}
// 匹配逻辑
bool isMatch = false;
// 1. 优先检查进程名(如果设置了)
if (!string.IsNullOrEmpty(info.ProcessName) && !string.Equals(info.ProcessName, activeProcess, StringComparison.OrdinalIgnoreCase))
{
isMatch = false;
}
else
{
// 2. 检查标题
if (string.IsNullOrEmpty(info.WindowTitle))
{
isMatch = true; // 如果没设标题,只靠进程匹配
}
else if (info.UseRegex)
{
try { isMatch = System.Text.RegularExpressions.Regex.IsMatch(activeTitle, info.WindowTitle, System.Text.RegularExpressions.RegexOptions.IgnoreCase); } catch { isMatch = false; }
}
else
{
isMatch = activeTitle.Contains(info.WindowTitle);
}
}
if (isMatch && !isIconic)
{
// 如果匹配成功且未最小化
// 如果 hWnd 变了,更新它
if (info.hWnd != activeHWnd)
{
info.hWnd = activeHWnd;
}
// 确保 Overlay 存在且未被销毁
if (!_overlays.ContainsKey(info.Id) || _overlays[info.Id].IsDisposed)
{
UpdateOverlay(info);
}
if (_overlays.ContainsKey(info.Id))
{
var overlay = _overlays[info.Id];
if (!overlay.Visible)
{
// 第一次显示时,先设置位置再显示,防止在原点闪烁
overlay.UpdatePosition(null, null);
overlay.Show();
}
overlay.Visible = true;
}
}
else
{
// 匹配失败或最小化,隐藏
if (_overlays.ContainsKey(info.Id))
{
_overlays[info.Id].Visible = false;
}
}
}
}
catch { }
}
public void LoadProcessIcons()
{
Log($"检查图标中... 当前标记数: {_markers.Count}");
foreach (var info in _markers)
{
if (info.ProcessIcon != null) continue;
if (string.IsNullOrEmpty(info.ProcessPath)) {
Log($"[{info.Title}] 路径为空,跳过图标加载");
continue;
}
if (File.Exists(info.ProcessPath))
{
try {
info.ProcessIcon = Icon.ExtractAssociatedIcon(info.ProcessPath);
Log($"[{info.Title}] 图标加载成功: {info.ProcessPath}");
} catch (Exception ex) {
Log($"[{info.Title}] 提取图标失败: {ex.Message}");
}
}
else
{
Log($"[{info.Title}] 文件不存在: {info.ProcessPath}");
}
}
_lstMarkers?.Invalidate(); // 强制重绘列表以更新图标
}
private void SelectCurrentWindowMarker()
{
try
{
IntPtr activeHWnd = GetForegroundWindow();
if (activeHWnd == IntPtr.Zero) return;
uint pid;
GetWindowThreadProcessId(activeHWnd, out pid);
string activeProcess = "";
try { activeProcess = Process.GetProcessById((int)pid).ProcessName; } catch { return; }
// 找到第一个匹配当前进程的标记
var match = _markers.FirstOrDefault(m =>
string.Equals(m.ProcessName, activeProcess, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
_lstMarkers.SelectedItem = match;
}
}
catch { }
}
private Label _lblBrand;
private Button _btnAdd;
private Font MainFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular);
private Font BoldFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
private Font TitleFont = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold);
private void InitializeUI()
{
Log("正在加载 Pro 核心组件...");
this.Text = "WindowMarker Pro";
this.Icon = AppIcon;
this.Size = new Size(1000, 900);
this.StartPosition = FormStartPosition.CenterScreen;
this.Font = MainFont;
this.BackColor = ColorBg;
// 总体布局:左侧侧边栏,右侧主内容区
_pnlSidebar = new Panel {
Dock = DockStyle.Left,
Width = 280,
BackColor = ColorSidebar,
Padding = new Padding(0)
};
// 侧边栏右边界线
_pnlSidebar.Paint += (s, e) => {
e.Graphics.DrawLine(new Pen(ColorBorder), _pnlSidebar.Width - 1, 0, _pnlSidebar.Width - 1, _pnlSidebar.Height);
};
_pnlMain = new Panel { Dock = DockStyle.Fill, BackColor = ColorBg };
// --- 侧边栏组件 ---
Panel pnlBrand = new Panel { Dock = DockStyle.Top, Height = 70, Padding = new Padding(15, 20, 15, 0) };
_lblBrand = new Label {
Text = "WindowMarker",
Font = new Font("Microsoft YaHei UI", 14F, FontStyle.Bold),
ForeColor = ColorTextPrimary,
Location = new Point(15, 20),
AutoSize = true
};
_btnAdd = new Button {
Text = "+",
Font = new Font("Consolas", 14F, FontStyle.Bold),
ForeColor = ColorAccent,
FlatStyle = FlatStyle.Flat,
Size = new Size(32, 32),
Location = new Point(230, 20),
Cursor = Cursors.Hand
};
_btnAdd.FlatAppearance.BorderSize = 0;
_btnAdd.Click += (s, e) => AddNewMarker();
pnlBrand.Controls.Add(_lblBrand);
pnlBrand.Controls.Add(_btnAdd);
Panel pnlSearch = new Panel { Dock = DockStyle.Top, Height = 60, Padding = new Padding(15, 5, 15, 15) };
_txtSearch = new TextBox {
Dock = DockStyle.Fill,
Text = "搜索配置...",
ForeColor = ColorTextSecondary,
BackColor = ColorSearchBg,
BorderStyle = BorderStyle.None,
Font = new Font(MainFont.FontFamily, 10F)
};
// 伪装 SearchBox 背景
Panel pnlSearchInner = new Panel {
Dock = DockStyle.Fill,
BackColor = ColorSearchBg,
Padding = new Padding(10, 10, 10, 10)
};
pnlSearchInner.Controls.Add(_txtSearch);
pnlSearch.Controls.Add(pnlSearchInner);
_txtSearch.Enter += (s, e) => { if(_txtSearch.Text == "搜索配置...") { _txtSearch.Text = ""; _txtSearch.ForeColor = ColorTextPrimary; } };
_txtSearch.Leave += (s, e) => { if(string.IsNullOrWhiteSpace(_txtSearch.Text)) { _txtSearch.Text = "搜索配置..."; _txtSearch.ForeColor = ColorTextSecondary; } };
_txtSearch.TextChanged += (s, e) => UpdateMarkerList();
_lstMarkers = new ListBox
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.None,
DrawMode = DrawMode.OwnerDrawFixed,
ItemHeight = 60,
IntegralHeight = false,
BackColor = ColorSidebar
};
_lstMarkers.DrawItem += LstMarkers_DrawItem;
_lstMarkers.SelectedIndexChanged += (s, e) => ShowMarkerConfig(_lstMarkers.SelectedItem as MarkerInfo);
_pnlSidebar.Controls.Add(_lstMarkers);
_pnlSidebar.Controls.Add(pnlSearch);
_pnlSidebar.Controls.Add(pnlBrand);
// --- 主内容区组件 ---
_configContainer = new Panel { Dock = DockStyle.Fill, Padding = new Padding(0) };
_pnlMain.Controls.Add(_configContainer);
this.Controls.Add(_pnlMain);
this.Controls.Add(_pnlSidebar);
Log("界面布局加载完成。版本: Pro v2.1.0");
UpdateMarkerList();
ShowEmptyState();
}
private void LstMarkers_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
var info = _lstMarkers.Items[e.Index] as MarkerInfo;
if (info == null) return;
bool isSelected = (e.State & DrawItemState.Selected) == DrawItemState.Selected;
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// 背景
Color bg = isSelected ? ColorSelectedBg : ColorSidebar;
using (var brush = new SolidBrush(bg)) g.FillRectangle(brush, e.Bounds);
// 选中指示条
if (isSelected)
{
using (var p = new SolidBrush(ColorAccent))
{
g.FillRectangle(p, e.Bounds.X, e.Bounds.Y + 12, 4, e.Bounds.Height - 24);
}
}
int x = e.Bounds.X + 20;
int y = e.Bounds.Y + 14;
// 图标
Rectangle iconRect = new Rectangle(x, y, 32, 32);
if (info.ProcessIcon != null)
{
g.DrawIcon(info.ProcessIcon, iconRect);
}
else
{
using (var brush = new SolidBrush(info.BgColor))
{
g.FillEllipse(brush, iconRect);
}
}
// 标题
string title = string.IsNullOrEmpty(info.Title) ? "未命名配置" : info.Title;
Color textColor = isSelected ? ColorAccent : ColorTextPrimary;
using (var font = new Font(MainFont.FontFamily, 9F, isSelected ? FontStyle.Bold : FontStyle.Regular))
{
g.DrawString(title, font, new SolidBrush(textColor), x + 42, y);
}
// 进程名
string subText = string.IsNullOrEmpty(info.ProcessName) ? "全局显示" : info.ProcessName;
using (var font = new Font(MainFont.FontFamily, 7.5F))
{
g.DrawString(subText, font, new SolidBrush(ColorTextSecondary), x + 42, y + 18);
}
}
private void InitializeTray()
{
_trayIcon = new NotifyIcon();
_trayIcon.Text = "窗口标记管理器";
_trayIcon.Icon = AppIcon;
var menu = new ContextMenuStrip();
menu.Items.Add("显示管理器", null, (s, e) => { this.Show(); this.WindowState = FormWindowState.Normal; this.BringToFront(); });
menu.Items.Add(new ToolStripSeparator());
var itemStartup = new ToolStripMenuItem("开机自启动");
itemStartup.CheckOnClick = true;
itemStartup.Checked = _config.RunAtStartup;
itemStartup.CheckedChanged += (s, e) => { _config.RunAtStartup = itemStartup.Checked; SaveData(); };
menu.Items.Add(itemStartup);
var itemHideOnStart = new ToolStripMenuItem("启动时不显示主窗口");
itemHideOnStart.CheckOnClick = true;
itemHideOnStart.Checked = _config.HideMainWindowOnStart;
itemHideOnStart.CheckedChanged += (s, e) => { _config.HideMainWindowOnStart = itemHideOnStart.Checked; SaveData(); };
menu.Items.Add(itemHideOnStart);
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("显示所有标记", null, (s, e) => ToggleAllMarkers(true));
menu.Items.Add("隐藏所有标记", null, (s, e) => ToggleAllMarkers(false));
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("退出程序", null, (s, e) => ExitApp());
_trayIcon.ContextMenuStrip = menu;
_trayIcon.Visible = true;
_trayIcon.DoubleClick += (s, e) => { this.Show(); this.WindowState = FormWindowState.Normal; this.BringToFront(); };
this.FormClosing += (s, e) => {
if (!_isExiting && e.CloseReason == CloseReason.UserClosing) {
e.Cancel = true;
this.Hide();
}
};
// 处理启动隐藏逻辑
if (_config.HideMainWindowOnStart)
{
this.WindowState = FormWindowState.Minimized;
this.ShowInTaskbar = false;
// 在 Load 之后立即隐藏
this.Load += (s, e) => {
this.BeginInvoke(new Action(() => {
this.Hide();
this.ShowInTaskbar = true; // 恢复,以便下次显示时正常
}));
};
}
}
public void UpdateMarkerList()
{
_lstMarkers.BeginUpdate();
_lstMarkers.Items.Clear();
string filterText = _txtSearch?.Text == "搜索配置..." ? "" : _txtSearch?.Text?.Trim() ?? "";
foreach (var m in _markers)
{
if (string.IsNullOrEmpty(filterText) ||
(m.Title != null && m.Title.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0) ||
(m.ProcessName != null && m.ProcessName.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0))
{
_lstMarkers.Items.Add(m);
}
}
_lstMarkers.EndUpdate();
}
private void AddNewMarker()
{
var info = new MarkerInfo { Title = $"新配置 #{_markers.Count + 1}" };
_markers.Add(info);
UpdateMarkerList();
_lstMarkers.SelectedItem = info;
Log($"已创建新标记资源: {info.Title}");
}
private void ShowMarkerConfig(MarkerInfo info)
{
_configContainer.Controls.Clear();
if (info == null) {
ShowEmptyState();
return;
}
_configPanel = new MarkerConfigPanel(info, this);
_configPanel.Dock = DockStyle.Fill;
_configContainer.Controls.Add(_configPanel);
}
private void ShowEmptyState()
{
var lbl = new Label {
Text = "请从左侧选择一个标记,或点击'新建标记'开始。",
TextAlign = ContentAlignment.MiddleCenter,
Dock = DockStyle.Fill,
ForeColor = Color.Gray
};
_configContainer.Controls.Add(lbl);
}
public void Log(string msg)
{
if (_txtLog == null) return;
string text = $"[{DateTime.Now:HH:mm:ss}] {msg}\r\n";
if (_txtLog.IsHandleCreated)
_txtLog.BeginInvoke(new Action(() => _txtLog.AppendText(text)));
else
_txtLog.AppendText(text);
}
public void UpdateOverlay(MarkerInfo info)
{
if (_overlays.ContainsKey(info.Id)) {
if (!_overlays[info.Id].IsDisposed)
_overlays[info.Id].Close();
_overlays.Remove(info.Id);
}
if (info.IsEnabled) {
var overlay = new MarkerOverlay(info, this);
_overlays[info.Id] = overlay;
}
// Log($"更新标记 [{info.Title}],路径: {info.ProcessPath}");
// LoadProcessIcons(); // 频率太高,改为按需调用
UpdateMarkerList();
// 同步配置面板状态
if (_configPanel != null && _configPanel.CurrentInfo == info)
{
_configPanel.RefreshState();
}
}
private void ToggleAllMarkers(bool show)
{
foreach (var info in _markers) {
info.IsEnabled = show;
UpdateOverlay(info);
}
}
private void ExitApp()
{
_isExiting = true;
// 1. 隐藏托盘图标,防止残留
if (_trayIcon != null) {
_trayIcon.Visible = false;
_trayIcon.Dispose();
}
// 2. 停止检测定时器
if (_visibilityTimer != null) {
_visibilityTimer.Stop();
_visibilityTimer.Dispose();
}
// 3. 关闭所有标记层
foreach (var overlay in _overlays.Values.ToList()) {
try {
overlay.Close();
overlay.Dispose();
} catch { }
}
_overlays.Clear();
// 4. 关闭主窗体并结束 Application 循环
this.Close();
if (_hWinEventHook != IntPtr.Zero)
{
UnhookWinEvent(_hWinEventHook);
_hWinEventHook = IntPtr.Zero;
}
Application.ExitThread(); // 使用 ExitThread 而不是 Exit 更加安全
}
public void RemoveMarker(MarkerInfo info)
{
if (_overlays.ContainsKey(info.Id)) {
_overlays[info.Id].Close();
_overlays.Remove(info.Id);
}
_markers.Remove(info);
UpdateMarkerList();
ShowEmptyState();
SaveData();
}
public void ShowEdit(MarkerInfo info)
{
this.Show();
this.WindowState = FormWindowState.Normal;
this.BringToFront();
// 选中列表中的对应项
_lstMarkers.SelectedItem = info;
}
}
#endregion
#region 配置面板控件
public class MarkerConfigPanel : UserControl
{
private MarkerInfo _info;
private MarkerMainForm _main;
public MarkerInfo CurrentInfo => _info;
public void RefreshState()
{
if (_chkEnabled != null)
{
_chkEnabled.Checked = _info.IsEnabled;
}
}
private TextBox _txtLabel;
private TextBox _txtWindowTitle;
private TextBox _txtProcessName;
private CheckBox _chkUseRegex;
private TextBox _txtImagePath;
private PictureBox _imgPreview;
private Label _lblPreview;
private Button _btnCapture;
private CheckBox _chkEnabled;
private ComboBox _cmbPositionMode;
private ComboBox _cmbAnchor;
private Button _btnTextColor;
private Button _btnBgColor;
// 动作配置
private ComboBox _cmbActionType;
private TextBox _txtActionParam;
private Label _lblActionTip;
private TextBox _txtMultiTexts;
private Font MainFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular);
private Font BoldFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
private Font HeaderFont = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
public MarkerConfigPanel(MarkerInfo info, MarkerMainForm main)
{
_info = info;
_main = main;
this.BackColor = MarkerMainForm.ColorBg;
InitializeUI();
}
private void InitializeUI()
{
this.Padding = new Padding(0); // 整体容器不留边距,确保底部栏贴合
// 1. 底部保存栏 (最先添加,确保 Dock 在底部)
Panel pnlBottom = new Panel { Dock = DockStyle.Bottom, Height = 60, BackColor = MarkerMainForm.ColorSidebar, Padding = new Padding(30, 10, 30, 10) };
pnlBottom.Paint += (s, e) => e.Graphics.DrawLine(new Pen(MarkerMainForm.ColorBorder), 0, 0, pnlBottom.Width, 0);
this.Controls.Add(pnlBottom);
Button btnSave = new Button {
Text = "保存配置并应用",
Dock = DockStyle.Left,
Width = 200,
BackColor = MarkerMainForm.ColorAccent,
ForeColor = Color.White,
FlatStyle = FlatStyle.Flat,
Font = BoldFont,
Cursor = Cursors.Hand
};
btnSave.FlatAppearance.BorderSize = 0;
btnSave.Click += (s, e) => SaveChanges();
Button btnDel = new Button {
Text = "删除此标记",
Dock = DockStyle.Right,
Width = 120,
ForeColor = Color.FromArgb(239, 68, 68),
FlatStyle = FlatStyle.Flat,
Cursor = Cursors.Hand
};
btnDel.FlatAppearance.BorderSize = 0;
btnDel.Click += (s, e) => {
if (MessageBox.Show("确定要永久删除此标记配置吗?", "危险操作", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) {
_main.RemoveMarker(_info);
}
};
pnlBottom.Controls.Add(btnSave);
pnlBottom.Controls.Add(btnDel);
// 2. 主滚动容器 (Fill 剩余空间)
Panel pnlContainer = new Panel { Dock = DockStyle.Fill, AutoScroll = true, Padding = new Padding(40, 30, 40, 0) }; // 增加左右边距,防止贴边
this.Controls.Add(pnlContainer);
int y = 0;
// --- 基础信息区 ---
y = AddHeader(pnlContainer, "基础信息", y);
_chkEnabled = new CheckBox { Text = "启用此标记", Checked = _info.IsEnabled, Font = MainFont, ForeColor = MarkerMainForm.ColorTextPrimary, AutoSize = true, Location = new Point(5, y) };
pnlContainer.Controls.Add(_chkEnabled);
y += 35;
y = AddLabel(pnlContainer, "标记显示标题", y);
_txtLabel = new TextBox {
Text = _info.Title,
Width = 500,
Location = new Point(5, y),
BorderStyle = BorderStyle.None,
BackColor = MarkerMainForm.ColorInputBg,
ForeColor = MarkerMainForm.ColorTextPrimary,
Font = new Font(MainFont.FontFamily, 11F)
};
y = WrapInModernBox(pnlContainer, _txtLabel, y) + 20;
_txtLabel.TextChanged += (s, e) => { _info.Title = _txtLabel.Text; UpdatePreview(); };
// --- 定位引擎 ---
y = AddHeader(pnlContainer, "定位引擎", y);
_btnCapture = new Button {
Text = " 捕获目标窗口位置",
Width = 180,
Height = 35,
Location = new Point(5, y),
FlatStyle = FlatStyle.Flat,
BackColor = MarkerMainForm.ColorInputBg,
ForeColor = MarkerMainForm.ColorAccent,
Cursor = Cursors.Hand
};
_btnCapture.FlatAppearance.BorderColor = MarkerMainForm.ColorAccent;
_btnCapture.Click += (s, e) => StartCaptureWithDelay();
pnlContainer.Controls.Add(_btnCapture);
y += 45;
y = AddLabel(pnlContainer, "定位模式与锚点", y);
_cmbPositionMode = new ComboBox { Width = 150, DropDownStyle = ComboBoxStyle.DropDownList, Font = MainFont, Location = new Point(5, y) };
_cmbPositionMode.Items.AddRange(new string[] { "相对锚点 (Offset)", "比例定位 (Ratio)" });
_cmbPositionMode.SelectedIndex = _info.PositionMode == "Relative" ? 0 : 1;
_cmbAnchor = new ComboBox { Width = 120, DropDownStyle = ComboBoxStyle.DropDownList, Font = MainFont, Location = new Point(170, y) };
_cmbAnchor.Items.AddRange(new string[] { "左上", "右上", "左下", "右下" });
_cmbAnchor.SelectedIndex = GetAnchorIndex(_info.Anchor);
_cmbAnchor.Enabled = (_info.PositionMode == "Relative");
pnlContainer.Controls.Add(_cmbPositionMode);
pnlContainer.Controls.Add(_cmbAnchor);
y += 40;
_cmbPositionMode.SelectedIndexChanged += (s, e) => { _cmbAnchor.Enabled = _cmbPositionMode.SelectedIndex == 0; };
_cmbAnchor.SelectedIndexChanged += (s, e) => { if(_cmbPositionMode.SelectedIndex == 0) RecalculateRelativeOffsets(); };
// --- 窗口匹配 ---
y = AddHeader(pnlContainer, "匹配规则", y);
y = AddLabel(pnlContainer, "目标进程名", y);
_txtProcessName = new TextBox { Text = _info.ProcessName, Width = 500, Location = new Point(5, y), BorderStyle = BorderStyle.None, BackColor = MarkerMainForm.ColorInputBg, ForeColor = MarkerMainForm.ColorTextPrimary };
y = WrapInModernBox(pnlContainer, _txtProcessName, y) + 20;
y = AddLabel(pnlContainer, "窗口标题匹配", y);
_txtWindowTitle = new TextBox { Text = _info.WindowTitle, Width = 400, Location = new Point(5, y), BorderStyle = BorderStyle.None, BackColor = MarkerMainForm.ColorInputBg, ForeColor = MarkerMainForm.ColorTextPrimary };
_chkUseRegex = new CheckBox { Text = "正则表达式", Checked = _info.UseRegex, AutoSize = true, ForeColor = MarkerMainForm.ColorTextPrimary, Location = new Point(440, y + 8), Font = MainFont };
pnlContainer.Controls.Add(_chkUseRegex);
y = WrapInModernBox(pnlContainer, _txtWindowTitle, y) + 20;
// --- 视觉样式 ---
y = AddHeader(pnlContainer, "视觉样式", y);
y = AddLabel(pnlContainer, "实时预览", y);
_lblPreview = new Label {
Text = string.IsNullOrEmpty(_info.Title) ? "预览文本" : _info.Title,
Location = new Point(10, y + 5),
AutoSize = true,
ForeColor = _info.TextColor,
BackColor = _info.BgColor,
Padding = new Padding(10, 5, 10, 5),
Font = new Font(MainFont.FontFamily, 10F, FontStyle.Bold)
};
pnlContainer.Controls.Add(_lblPreview);
_btnTextColor = CreateIconButton("文字", y, (s, e) => {
using (var cd = new ColorDialog { Color = _info.TextColor }) if (cd.ShowDialog() == DialogResult.OK) { _info.TextColor = cd.Color; UpdatePreview(); }
});
_btnBgColor = CreateIconButton("背景", y, (s, e) => {
using (var cd = new ColorDialog { Color = _info.BgColor }) if (cd.ShowDialog() == DialogResult.OK) { _info.BgColor = cd.Color; UpdatePreview(); }
});
_btnTextColor.Location = new Point(200, y + 5);
_btnBgColor.Location = new Point(280, y + 5);
pnlContainer.Controls.Add(_btnTextColor);
pnlContainer.Controls.Add(_btnBgColor);
y += 50;
// --- 响应行为 ---
y = AddHeader(pnlContainer, "响应行为", y);
y = AddLabel(pnlContainer, "点击触发动作", y);
_cmbActionType = new ComboBox { Width = 180, DropDownStyle = ComboBoxStyle.DropDownList, Font = MainFont, Location = new Point(5, y) };
_cmbActionType.Items.AddRange(new string[] { "无", "打开网址", "打开文件", "执行命令", "Quicker动作", "简易便签", "发送文本" });
_cmbActionType.SelectedIndex = GetActionIndex(_info.ClickActionType);
pnlContainer.Controls.Add(_cmbActionType);
y += 35;
_txtActionParam = new TextBox { Text = _info.ClickActionParam, Width = 500, Location = new Point(5, y), BorderStyle = BorderStyle.None, BackColor = MarkerMainForm.ColorInputBg, ForeColor = MarkerMainForm.ColorTextPrimary };
int paramY = y;
y = WrapInModernBox(pnlContainer, _txtActionParam, y) + 10;
_txtMultiTexts = new TextBox {
Multiline = true,
Text = string.Join("\r\n", _info.MultiTexts ?? new List<string>()),
Width = 500,
Height = 120,
Location = new Point(5, paramY),
BorderStyle = BorderStyle.None,
BackColor = MarkerMainForm.ColorInputBg,
ForeColor = MarkerMainForm.ColorTextPrimary
};
int multiY = paramY;
y = WrapInModernBox(pnlContainer, _txtMultiTexts, multiY) + 10;
_lblActionTip = new Label { Text = "...", AutoSize = true, Location = new Point(10, y), ForeColor = MarkerMainForm.ColorTextSecondary, Font = new Font(MainFont.FontFamily, 8.5F) };
pnlContainer.Controls.Add(_lblActionTip);
y += 100; // 增加底部边距,留出充足空间以免被置底按钮遮挡内容
_cmbActionType.SelectedIndexChanged += (s, e) => UpdateActionUI();
UpdateActionUI();
}
private int AddHeader(Panel p, string text, int y)
{
p.Controls.Add(new Label { Text = text, Font = HeaderFont, ForeColor = MarkerMainForm.ColorTextPrimary, Location = new Point(0, y), AutoSize = true });
return y + 30;
}
private int AddLabel(Panel p, string text, int y)
{
p.Controls.Add(new Label { Text = text, Font = new Font(MainFont.FontFamily, 8.5F), ForeColor = MarkerMainForm.ColorTextSecondary, Location = new Point(5, y), AutoSize = true });
return y + 22;
}
private int WrapInModernBox(Panel p, Control ctrl, int y)
{
Panel wrap = new Panel {
Location = new Point(0, y),
Size = new Size(ctrl.Width + 20, ctrl.Height + 16),
BackColor = MarkerMainForm.ColorInputBg,
Padding = new Padding(10, 8, 10, 8)
};
wrap.Paint += (s, e) => {
e.Graphics.DrawRectangle(new Pen(MarkerMainForm.ColorBorder), 0, 0, wrap.Width - 1, wrap.Height - 1);
};
ctrl.Dock = DockStyle.Fill;
wrap.Controls.Add(ctrl);
p.Controls.Add(wrap);
return y + wrap.Height;
}
private Button CreateIconButton(string text, int y, EventHandler click)
{
Button btn = new Button { Text = text, Width = 70, Height = 30, FlatStyle = FlatStyle.Flat, BackColor = MarkerMainForm.ColorInputBg, ForeColor = MarkerMainForm.ColorTextPrimary, Font = new Font(MainFont.FontFamily, 8.5F), Cursor = Cursors.Hand };
btn.FlatAppearance.BorderColor = MarkerMainForm.ColorBorder;
btn.Click += click;
return btn;
}
private void UpdateActionUI()
{
int idx = _cmbActionType.SelectedIndex;
_txtActionParam.Parent.Visible = (idx >= 1 && idx <= 4);
_txtMultiTexts.Parent.Visible = (idx == 6);
switch (idx)
{
case 1: _lblActionTip.Text = "ℹ️ 请输入完整网址,如 https://www.google.com"; break;
case 2: _lblActionTip.Text = "ℹ️ 请输入本地文件或程序路径"; break;
case 3: _lblActionTip.Text = "ℹ️ 请输入要执行的命令行指令"; break;
case 4: _lblActionTip.Text = "ℹ️ 请输入 Quicker 动作 ID 或指令"; break;
case 5: _lblActionTip.Text = "ℹ️ 点击标记可弹出实时保存的便签窗口"; break;
case 6: _lblActionTip.Text = "ℹ️ 每行一个文本,点击标记后弹出列表供选择粘贴"; break;
default: _lblActionTip.Text = "ℹ️ 点击标记时不执行任何动作"; break;
}
if (idx == 6) _lblActionTip.Location = new Point(10, _txtMultiTexts.Parent.Bottom + 5);
else if (idx >= 1 && idx <= 4) _lblActionTip.Location = new Point(10, _txtActionParam.Parent.Bottom + 5);
else _lblActionTip.Location = new Point(10, _cmbActionType.Bottom + 5);
}
private void UpdatePreview()
{
if (_lblPreview != null)
{
_lblPreview.Text = string.IsNullOrEmpty(_txtLabel.Text) ? "预览文本" : _txtLabel.Text;
_lblPreview.ForeColor = _info.TextColor;
_lblPreview.BackColor = _info.BgColor;
}
}
private async void StartCaptureWithDelay()
{
_main.Hide();
using (Form tipForm = new Form())
{
// 设置提示窗口
tipForm.FormBorderStyle = FormBorderStyle.None;
tipForm.ShowInTaskbar = false;
tipForm.TopMost = true;
tipForm.Size = new Size(200, 30);
tipForm.BackColor = Color.Black;
tipForm.Opacity = 0.7;
Label lblTip = new Label {
Text = "️ 移动选择,左键标记",
ForeColor = Color.White,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleCenter
};
tipForm.Controls.Add(lblTip);
tipForm.Show();
MarkerInfo captured = null;
while (true)
{
GetCursorPos(out POINT p);
tipForm.Location = new Point(p.X + 20, p.Y + 20);
// 检测左键按下 (VK_LBUTTON = 0x01)
if ((GetAsyncKeyState(0x01) & 0x8000) != 0)
{
captured = WindowMarker.CaptureWindowInfo();
if (captured != null) WindowMarker.ShowHighlight(captured.CapturedRect);
break;
}
// 检测 ESC 退出 (VK_ESCAPE = 0x1B)
if ((GetAsyncKeyState(0x1B) & 0x8000) != 0) break;
await System.Threading.Tasks.Task.Delay(50);
}
if (captured != null)
{
_info.hWnd = captured.hWnd;
_info.WindowTitle = captured.WindowTitle;
_info.ProcessName = captured.ProcessName;
_info.ProcessPath = captured.ProcessPath; // 关键:补全路径属性
_info.RelativePoint = captured.RelativePoint;
_info.RatioX = captured.RatioX;
_info.RatioY = captured.RatioY;
_info.WindowSize = captured.WindowSize;
_info.CapturedRect = captured.CapturedRect;
_info.ProcessIcon = null; // 重置图标,强制在接下来的单次更新中重新获取
_main.LoadProcessIcons(); // 立即加载新图标
_main.Log($"成功标记: [{_info.ProcessName}] {_info.WindowTitle}");
UpdateTargetInfoDisplay();
_main.UpdateOverlay(_info);
}
}
_main.Show();
_main.BringToFront();
}
private void SaveChanges()
{
_info.IsEnabled = _chkEnabled.Checked;
_info.Title = _txtLabel.Text;
_info.WindowTitle = _txtWindowTitle.Text;
_info.ProcessName = _txtProcessName.Text;
_info.UseRegex = _chkUseRegex.Checked;
_info.ClickActionType = GetActionType(_cmbActionType.SelectedIndex);
_info.ClickActionParam = _txtActionParam.Text;
_info.MultiTexts = _txtMultiTexts.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries).ToList();
_info.PositionMode = _cmbPositionMode.SelectedIndex == 0 ? "Relative" : "Ratio";
_info.Anchor = GetAnchorString(_cmbAnchor.SelectedIndex);
_main.Log($"已同步配置: {_info.Title}");
_main.UpdateOverlay(_info);
_main.SaveData();
_main.UpdateMarkerList();
}
private string GetActionType(int index)
{
switch (index)
{
case 1: return "Url";
case 2: return "File";
case 3: return "Command";
case 4: return "Quicker";
case 5: return "Note";
case 6: return "SendText";
default: return "None";
}
}
private int GetAnchorIndex(string anchor)
{
switch (anchor)
{
case "TopRight": return 1;
case "BottomLeft": return 2;
case "BottomRight": return 3;
default: return 0;
}
}
private string GetAnchorString(int index)
{
switch (index)
{
case 1: return "TopRight";
case 2: return "BottomLeft";
case 3: return "BottomRight";
default: return "TopLeft";
}
}
private void RecalculateRelativeOffsets()
{
int currentLeftOffset = 0;
int currentTopOffset = 0;
switch (_info.Anchor)
{
case "TopRight":
currentLeftOffset = _info.WinW + _info.RelX;
currentTopOffset = _info.RelY;
break;
case "BottomLeft":
currentLeftOffset = _info.RelX;
currentTopOffset = _info.WinH + _info.RelY;
break;
case "BottomRight":
currentLeftOffset = _info.WinW + _info.RelX;
currentTopOffset = _info.WinH + _info.RelY;
break;
default: // TopLeft
currentLeftOffset = _info.RelX;
currentTopOffset = _info.RelY;
break;
}
string newAnchor = GetAnchorString(_cmbAnchor.SelectedIndex);
int newRelX = 0, newRelY = 0;
switch (newAnchor)
{
case "TopRight":
newRelX = currentLeftOffset - _info.WinW;
newRelY = currentTopOffset;
break;
case "BottomLeft":
newRelX = currentLeftOffset;
newRelY = currentTopOffset - _info.WinH;
break;
case "BottomRight":
newRelX = currentLeftOffset - _info.WinW;
newRelY = currentTopOffset - _info.WinH;
break;
default: // TopLeft
newRelX = currentLeftOffset;
newRelY = currentTopOffset;
break;
}
_info.RelX = newRelX;
_info.RelY = newRelY;
_info.Anchor = newAnchor;
}
private int GetActionIndex(string type)
{
if (type == "Url") return 1;
if (type == "File") return 2;
if (type == "Command") return 3;
if (type == "Quicker") return 4;
if (type == "Note") return 5;
if (type == "SendText") return 6;
return 0;
}
private void UpdateTargetInfoDisplay()
{
_txtWindowTitle.Text = _info.WindowTitle;
_txtProcessName.Text = _info.ProcessName;
}
private void SelectImage()
{
using (var ofd = new OpenFileDialog { Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp" }) {
if (ofd.ShowDialog() == DialogResult.OK) {
_info.ImagePath = ofd.FileName;
_txtImagePath.Text = ofd.FileName;
if (_imgPreview.Image != null) _imgPreview.Image.Dispose();
_imgPreview.Image = Image.FromFile(ofd.FileName);
_main.UpdateOverlay(_info);
}
}
}
}
#endregion
#region 标记显示层
public class MarkerOverlay : Form
{
private MarkerInfo _info;
private MarkerMainForm _main;
// private Timer _tracker;
public IntPtr TargetHwnd => _info.hWnd;
public MarkerOverlay(MarkerInfo info, MarkerMainForm main)
{
_info = info;
_main = main;
this.Text = "QuickerMarker";
this.FormBorderStyle = FormBorderStyle.None;
this.ShowInTaskbar = false;
this.TopMost = true;
this.BackColor = Color.Magenta;
this.TransparencyKey = Color.Magenta;
// 设置 WS_EX_NOACTIVATE 样式
int exStyle = GetWindowLong(this.Handle, -20);
SetWindowLong(this.Handle, -20, exStyle | 0x08000000); // WS_EX_NOACTIVATE
// 右键菜单
var menu = new ContextMenuStrip();
menu.Items.Add("编辑标记", null, (s, e) => _main.ShowEdit(_info));
menu.Items.Add("隐藏标记", null, (s, e) => {
_info.IsEnabled = false;
_main.UpdateOverlay(_info);
_main.SaveData();
});
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add("删除标记", null, (s, e) => {
if (MessageBox.Show("确定要删除此标记吗?", "确认删除", MessageBoxButtons.YesNo) == DialogResult.Yes) {
_main.RemoveMarker(_info);
}
});
this.ContextMenuStrip = menu;
if (!string.IsNullOrEmpty(_info.ImagePath) && File.Exists(_info.ImagePath))
{
var img = Image.FromFile(_info.ImagePath);
this.BackgroundImage = img;
this.BackgroundImageLayout = ImageLayout.Zoom;
this.Size = new Size(Math.Min(img.Width, 100), Math.Min(img.Height, 100));
}
else
{
this.Size = new Size(200, 30);
this.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
this.Paint += (s, e) => {
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
string text = string.IsNullOrEmpty(_info.Title) ? "未命名标记" : _info.Title;
Size size = TextRenderer.MeasureText(text, this.Font);
int targetW = size.Width + 24;
int targetH = size.Height + 12;
if (this.Width != targetW || this.Height != targetH)
{
this.Size = new Size(targetW, targetH);
return;
}
using (var brush = new SolidBrush(_info.BgColor))
{
g.FillPath(brush, GetRoundedRect(new Rectangle(0, 0, this.Width - 1, this.Height - 1), 6));
}
TextRenderer.DrawText(g, text, this.Font, new Rectangle(0, 0, this.Width, this.Height), _info.TextColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
};
}
// 跟踪逻辑
// _tracker = new Timer { Interval = _info.UpdateIntervalMs };
// _tracker.Tick += UpdatePosition;
// _tracker.Start();
this.Click += HandleClick;
this.Cursor = Cursors.Hand;
}
private void HandleClick(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_info.ClickActionType) || _info.ClickActionType == "None") return;
try
{
switch (_info.ClickActionType)
{
case "Url":
case "File":
Process.Start(_info.ClickActionParam);
break;
case "Command":
Process.Start("cmd.exe", "/c " + _info.ClickActionParam);
break;
case "Quicker":
string quickerPath = @"C:\Program Files\Quicker\Quickerstarter.exe";
if (File.Exists(quickerPath))
Process.Start(quickerPath, $"runaction:{_info.ClickActionParam}");
else
MessageBox.Show("未找到 Quickerstarter.exe,请检查安装路径。");
break;
case "Note":
ShowNoteWindow();
break;
case "SendText":
HandleSendTextAction();
break;
}
}
catch (Exception ex)
{
MessageBox.Show("执行动作失败: " + ex.Message);
}
}
private void HandleSendTextAction()
{
if (_info.MultiTexts == null || _info.MultiTexts.Count == 0) return;
if (_info.MultiTexts.Count == 1)
{
PasteText(_info.MultiTexts[0]);
}
else
{
var menu = new ContextMenuStrip();
foreach (var t in _info.MultiTexts)
{
if (string.IsNullOrWhiteSpace(t)) continue;
string displayText = t.Length > 30 ? t.Substring(0, 30) + "..." : t;
var itemText = t;
menu.Items.Add(displayText, null, (s, e) => PasteText(itemText));
}
menu.Show(Cursor.Position);
}
}
private void PasteText(string text)
{
if (string.IsNullOrEmpty(text)) return;
try {
Clipboard.SetText(text);
// 关键修复:确保目标窗口在前台
if (TargetHwnd != IntPtr.Zero)
{
SetForegroundWindow(TargetHwnd);
System.Threading.Thread.Sleep(50); // 给系统一点反应时间
}
// 发送 Ctrl+V
SendKeys.SendWait("^v");
} catch (Exception ex) {
_main.Log("粘贴失败: " + ex.Message);
}
}
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private void ShowNoteWindow()
{
Form noteForm = new Form();
noteForm.Text = "简易便签 - " + _info.DisplayName;
noteForm.Size = new Size(300, 250);
noteForm.StartPosition = FormStartPosition.Manual;
noteForm.Location = this.Location;
noteForm.TopMost = true;
noteForm.FormBorderStyle = FormBorderStyle.SizableToolWindow;
TextBox txtNote = new TextBox();
txtNote.Multiline = true;
txtNote.Dock = DockStyle.Fill;
txtNote.ScrollBars = ScrollBars.Vertical;
txtNote.Text = _info.NoteText;
txtNote.Font = new Font("Microsoft YaHei", 10F);
txtNote.TextChanged += (s, e) => {
_info.NoteText = txtNote.Text;
_main.SaveData(); // 自动保存
};
noteForm.Controls.Add(txtNote);
noteForm.Show();
}
private int _invalidHandleCount = 0; // 连续无效句柄计数
public void UpdatePosition(object sender, EventArgs e)
{
if (!IsWindow(_info.hWnd))
{
_invalidHandleCount++;
if (_invalidHandleCount == 1)
{
_main.Log($"[{_info.DisplayName}] 窗口句柄无效: {_info.hWnd},暂停跟踪");
}
if (_invalidHandleCount > 100)
{
// _tracker.Stop();
// 既然是事件驱动,这里不需要停止 Timer,但可以记录日志
if (_invalidHandleCount == 101)
_main.Log($"[{_info.DisplayName}] 窗口已关闭(连续检测无效),停止跟踪");
}
this.Visible = false;
return;
}
_invalidHandleCount = 0; // 重置计数
GetWindowRect(_info.hWnd, out RECT rect);
int winW = rect.Right - rect.Left;
int winH = rect.Bottom - rect.Top;
Point pos = Point.Empty;
if (_info.PositionMode == "Ratio")
{
pos = new Point(rect.Left + (int)(winW * _info.RatioX),
rect.Top + (int)(winH * _info.RatioY));
}
else // Relative
{
switch (_info.Anchor)
{
case "TopRight":
pos = new Point(rect.Right + _info.RelX, rect.Top + _info.RelY);
break;
case "BottomLeft":
pos = new Point(rect.Left + _info.RelX, rect.Bottom + _info.RelY);
break;
case "BottomRight":
pos = new Point(rect.Right + _info.RelX, rect.Bottom + _info.RelY);
break;
default: // TopLeft
pos = new Point(rect.Left + _info.RelX, rect.Top + _info.RelY);
break;
}
}
this.Location = pos;
}
private System.Drawing.Drawing2D.GraphicsPath GetRoundedRect(Rectangle bounds, int radius)
{
int diameter = radius * 2;
Size size = new Size(diameter, diameter);
Rectangle arc = new Rectangle(bounds.Location, size);
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
if (radius == 0)
{
path.AddRectangle(bounds);
return path;
}
path.AddArc(arc, 180, 90);
arc.X = bounds.Right - diameter;
path.AddArc(arc, 270, 90);
arc.Y = bounds.Bottom - diameter;
path.AddArc(arc, 0, 90);
arc.X = bounds.Left;
path.AddArc(arc, 90, 90);
path.CloseFigure();
return path;
}
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr hWnd);
}
#endregion
#region Win32 API
[StructLayout(LayoutKind.Sequential)]
public struct POINT { public int X; public int Y; }
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
[DllImport("user32.dll")] static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")] static extern IntPtr WindowFromPoint(POINT Point);
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] static extern IntPtr GetAncestor(IntPtr hWnd, uint gaFlags);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam);
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")] static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
const int EM_SETCUEBANNER = 0x1501;
private static void SetPlaceholder(TextBox textBox, string placeholder)
{
SendMessage(textBox.Handle, EM_SETCUEBANNER, (IntPtr)1, placeholder);
}
delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
[DllImport("user32.dll")] static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
[DllImport("user32.dll")] static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
动作配置 (JSON)
{
"ActionId": "65c487ab-3b0b-4831-a7c8-a793b0e449d7",
"Title": "窗口便利贴",
"Description": "款为软件界面量身定制的“虚拟贴纸”工具。它可以精准捕捉任何软件窗口内的控件或区域,并为其添加个性化的文字标签或图片标记。标记会智能随窗口移动、隐藏或显示,帮助您快速识别功能区域、记录操作提醒或美化工作流界面。",
"Icon": "https://files.getquicker.net/_icons/FA3F3C0DAA167D5BE58FA688AE4AA6D8F2D8E75D.png"
}
使用方法
- 确保您已安装 动作构建 (Action Builder) 动作。
- 将本文提供的
.cs和.json文件下载到同一目录。 - 选中
.json文件,运行「动作构建」即可生成并安装动作。
浙公网安备 33010602011771号