[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"
}

使用方法

  1. 确保您已安装 动作构建 (Action Builder) 动作。
  2. 将本文提供的 .cs.json 文件下载到同一目录
  3. 选中 .json 文件,运行「动作构建」即可生成并安装动作。
posted @ 2025-12-27 22:17  luoluoluo22  阅读(16)  评论(0)    收藏  举报