云南省网站建设_网站建设公司_论坛网站_seo优化
2025/12/27 22:19:50 网站建设 项目流程

动作:窗口便利贴

款为软件界面量身定制的“虚拟贴纸”工具。它可以精准捕捉任何软件窗口内的控件或区域,并为其添加个性化的文字标签或图片标记。标记会智能随窗口移动、隐藏或显示,帮助您快速识别功能区域、记录操作提醒或美化工作流界面。

更新时间:2025-12-27 22:17
原始文件:WindowMarker.cs

核心代码

// ref: WindowsBaseusing 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, Ratiopublic 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 Constantspublic 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_ROOTinfo.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 Modepublic 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 Modepublic 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 = 0if (_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: // TopLeftcurrentLeftOffset = _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: // TopLeftnewRelX = 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}");elseMessageBox.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+VSendKeys.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: // TopLeftpos = 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"
}

使用方法

  1. 确保您已安装 动作构建 (Action Builder) 动作。
  2. 将本文提供的 .cs.json 文件下载到同一目录
  3. 选中 .json 文件,运行「动作构建」即可生成并安装动作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询