[Quicker] 智能窗口布局 - 源码归档

动作:智能窗口布局

自动整理桌面窗口布局,支持1-N个窗口的智能排列。采用Z型优先级填充策略,左侧VIP席位+右侧网格布局。
20260103230619848
PixPin_2026-01-03_11-34-55_2

前往quicker安装

更新时间:2026-01-03 22:28
原始文件:WindowLayoutManager.cs

核心代码

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using Quicker.Public;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Windows.Automation;

public static void Exec(IStepContext context)
{
    bool isDev = File.Exists(@"f:\桌面\开发\WindowLayoutManager.cs");

    if (isDev)
    {
        Logger.Init(@"f:\桌面\开发\WindowLayout.log");
        Logger.Log("==================================================");
        Logger.Log(">>> 开发者模式已识别:日志模块启动");
        Logger.Log($">>> 算法进化:响应式动态填坑算法 V4 (True Gap-Filler)");
    }

    try
    {
        bool instantMode = context.GetVarValue("instantMode")?.ToString().ToLower() == "true";
        int.TryParse(context.GetVarValue("autoExitSeconds")?.ToString(), out int autoExitSeconds);
        
        BlacklistManager.Reload();

        if (OverlayForm.Instance != null && !OverlayForm.Instance.IsDisposed && OverlayForm.Instance.Visible)
        {
            Logger.Log(">>> 触发已有实例热刷新");
            OverlayForm.Instance.Invoke((MethodInvoker)delegate {
                OverlayForm.Instance.TriggerManualRefresh();
                if (instantMode) OverlayForm.Instance.Close(); 
                else OverlayForm.Instance.Activate();
            });
            return;
        }

        var wins = WindowHelper.GetFilterWindows();
        Logger.Log($"[Filter-Done] 识别到参与排版的独立窗口数量: {wins.Count}");

        if (wins.Count == 0) return;

        if (instantMode)
        {
            Rectangle wa = Screen.PrimaryScreen.WorkingArea;
            var locks = LockManager.Load();
            var lockedItemMatches = new Dictionary<IntPtr, Rectangle>();
            var normalItems = new List<IntPtr>();
            var usedLocks = new HashSet<LockInfo>();

            foreach (var hwnd in wins)
            {
                var exactMatch = locks.Where(l => !usedLocks.Contains(l)).FirstOrDefault(l => LockManager.IsMatchStrict(hwnd, l));
                if (exactMatch != null) { lockedItemMatches[hwnd] = exactMatch.Rect; usedLocks.Add(exactMatch); }
                else normalItems.Add(hwnd);
            }

            if (normalItems.Count > 0) { Random rng = new Random(); normalItems = normalItems.OrderBy(x => rng.Next()).ToList(); }

            var assignment = LayoutAlgorithm.AssignSmartGaps(wa, lockedItemMatches, normalItems);
            foreach (var kvp in assignment) { if (WindowHelper.IsWindow(kvp.Key)) WindowHelper.MoveWindowCompensated(kvp.Key, kvp.Value); }
            return;
        }

        Rectangle workArea = Screen.PrimaryScreen.WorkingArea;
        var overlay = new OverlayForm(wins, workArea, autoExitSeconds);
        Application.Run(overlay);
    }
    catch (Exception ex)
    {
        Logger.Log($"[Critical] {ex.ToString()}");
        context.SetVarValue("errMessage", ex.Message);
    }
}

public class OverlayForm : Form
{
    private FlowLayoutPanel _sceneGallery;
    private Panel _galleryOverlay;

    public static OverlayForm Instance { get; private set; }
    private List<IntPtr> _windows;
    public List<LockInfo> _locks;
    private Rectangle _workArea;
    private Button _refreshBtn;
    private Button _confirmBtn;
    private Panel _iconContainer;
    private ToolTip _toolTip;
    private int _shuffleOffset = 0;
    private bool _locksDirty = false;
    private System.Windows.Forms.Timer _saveTimer;
    private System.Windows.Forms.Timer _autoExitTimer;
    private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
    private WinEventDelegate _winEventDelegate;
    private IntPtr _hHook;

    [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);
    [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

    static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
    const uint SWP_NOMOVE = 0x0002;
    const uint SWP_NOSIZE = 0x0001;
    const uint SWP_SHOWWINDOW = 0x0040;

    private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
    private const uint WINEVENT_OUTOFCONTEXT = 0;

    public OverlayForm(List<IntPtr> windows, Rectangle workArea, int autoExitSeconds)
    {
        Instance = this;
        this.Text = "Quicker_WindowLayoutOverlay_Host"; 
        this.AllowTransparency = true;
        this.SetStyle(ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
        this.UpdateStyles();
        this.Opacity = 0; // 初始隐藏,防止闪烁

        _windows = windows;
        _workArea = workArea;
        _locks = LockManager.Load();
        _toolTip = new ToolTip { InitialDelay = 200, AutoPopDelay = 10000 };
        
        this.FormBorderStyle = FormBorderStyle.None;
        this.Bounds = workArea; // 直接指定边界,不使用 Maximized 以减少黑屏概率
        this.BackColor = Color.Magenta;
        this.TransparencyKey = Color.Magenta; 
        this.TopMost = true; 
        this.ShowInTaskbar = false;
        
        // 强制置顶
        SetWindowPos(this.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
        
        this.Deactivate += (s, e) => { if (!this.ContainsFocus && !this.Capture) this.Close(); };

        this.KeyPreview = true;
        this.KeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) this.Close(); };

        if (autoExitSeconds > 0)
        {
            _autoExitTimer = new System.Windows.Forms.Timer { Interval = autoExitSeconds * 1000 };
            _autoExitTimer.Tick += (s, e) => this.Close();
            _autoExitTimer.Start();
        }

        _iconContainer = new Panel { Dock = DockStyle.Fill, BackColor = Color.Transparent };
        this.Controls.Add(_iconContainer);

        this.Shown += (s, e) => { this.Opacity = 1; }; // 准备好了再亮相
        
        CreateControlButtons(); 
        PerformReLayout();

        _winEventDelegate = new WinEventDelegate(WinEventProc);
        _hHook = SetWinEventHook(EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, IntPtr.Zero, _winEventDelegate, 0, 0, WINEVENT_OUTOFCONTEXT);

        _saveTimer = new System.Windows.Forms.Timer { Interval = 500 };
        _saveTimer.Tick += (s, e) => { if (_locksDirty) { LockManager.Save(_locks); _locksDirty = false; } };
        _saveTimer.Start();
    }
    
    public async void TriggerManualRefresh()
    {
        BlacklistManager.Reload();
        var latestWindows = WindowHelper.GetFilterWindows();
        _windows = latestWindows;
        this.Opacity = 0;
        await Task.Delay(20); 
        _shuffleOffset++; 
        PerformReLayout(); 
        this.Opacity = 1;
        SetWindowPos(this.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
        _refreshBtn.BringToFront(); _confirmBtn.BringToFront();
        if (_autoExitTimer != null) { _autoExitTimer.Stop(); _autoExitTimer.Start(); }
    }

    private void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        // 增加对当前窗口标题的实时过滤,防止扫描到自己
        if (idObject == 0 && _windows.Contains(hwnd) && this.Opacity > 0.1) 
        {
            this.Invoke((MethodInvoker)delegate { UpdateLockBtnPositionByHandle(hwnd); });
        }
    }

    private void UpdateLockBtnPositionByHandle(IntPtr hwnd)
    {
        foreach (Control c in _iconContainer.Controls)
        {
            if (c is LockButton btn && btn.Hwnd == hwnd)
            {
                Rectangle oldRect = btn.Bounds;
                Rectangle r = WindowHelper.GetVisibleRect(hwnd);
                if (oldRect.X == r.Left + 5 && oldRect.Y == r.Top + 5) return; // 位置没变,不处理

                btn.SetPositionByWindow(r);
                
                // 【核心:消除残影】通知窗体重画旧区域和新区域
                this.Invalidate(oldRect);
                this.Invalidate(btn.Bounds);
                this.Update(); // 立即执行重绘消息
                
                if (btn.IsLocked)
                {
                    var lockInfo = _locks.FirstOrDefault(l => LockManager.IsMatchStrict(hwnd, l));
                    if (lockInfo != null && lockInfo.Rect != r) { lockInfo.Rect = r; _locksDirty = true; }
                }
                break;
            }
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        this.FormClosed += (s, args) => { 
            if (_hHook != IntPtr.Zero) UnhookWinEvent(_hHook); 
            if (OverlayForm.Instance == this) OverlayForm.Instance = null;
        };
        _toolTip.Dispose();
        if (_locksDirty) LockManager.Save(_locks);
        base.OnFormClosing(e);
    }

    private void CreateControlButtons()
    {
        int btnSize = 40; int spacing = 20;
        int centerX = _workArea.X + (_workArea.Width / 2);
        int centerY = _workArea.Y + (_workArea.Height - btnSize) / 2;

        // 刷新按钮 (左)
        _refreshBtn = new Button {
            Size = new Size(btnSize, btnSize),
            Location = new Point(centerX - btnSize - btnSize/2 - spacing, centerY),
            FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(180, 40, 40, 40), ForeColor = Color.White,
            Text = "↻", Font = new Font("Segoe UI Symbol", 18, FontStyle.Bold), Cursor = Cursors.Hand
        };
        _refreshBtn.FlatAppearance.BorderSize = 0;
        GraphicsPath p1 = new GraphicsPath(); p1.AddEllipse(0, 0, btnSize, btnSize); _refreshBtn.Region = new Region(p1);
        _refreshBtn.Click += (s, e) => { TriggerManualRefresh(); this.Activate(); };
        this.Controls.Add(_refreshBtn);

        // 相机按钮 (中)
        var _sceneSaveBtn = new Button {
            Size = new Size(btnSize, btnSize),
            Location = new Point(centerX - btnSize/2, centerY),
            FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(180, 20, 20, 20), ForeColor = Color.FromArgb(52, 152, 219),
            Text = "", Font = new Font("Segoe UI Symbol", 18, FontStyle.Bold), Cursor = Cursors.Hand, Name = "SceneBtn"
        };
        _sceneSaveBtn.FlatAppearance.BorderSize = 0;
        GraphicsPath p3 = new GraphicsPath(); p3.AddEllipse(0, 0, btnSize, btnSize); _sceneSaveBtn.Region = new Region(p3);
        _sceneSaveBtn.Click += (s, e) => { 
            string name = DateTime.Now.ToString("MM-dd HHmmss") + " 布局";
            Logger.Log($"[SaveScene] >>> 开始执行快照保存: {name}");
            
            // 重要:不再只保存 _locks,而是保存当前所有 _windows 句柄的状态
            var snapshot = new List<LockInfo>();
            foreach (var h in _windows) {
                if (!WindowHelper.IsWindow(h)) continue;
                var info = new LockInfo {
                    ProcessName = LockManager.GetProcessName(h),
                    Title = WindowHelper.GetWindowTitle(h),
                    MatchKey = WindowHelper.GetSmartMatchKey(h),
                    Rect = WindowHelper.GetVisibleRect(h),
                    ExecPath = WindowHelper.GetExecutablePath(h)
                };
                if (info.ProcessName == "msedge" || info.ProcessName == "chrome") {
                    info.Url = WindowHelper.GetBrowserUrl(h);
                    Logger.Log($"[SaveScene-Debug] 浏览器 URL 抓取: {info.Title} -> {(string.IsNullOrEmpty(info.Url) ? "失败" : info.Url)}");
                }
                else if (info.ProcessName == "explorer") {
                    info.Url = WindowHelper.GetExplorerPath(h);
                    Logger.Log($"[SaveScene-Debug] 资源管理器路径抓取: {info.Title} -> {(string.IsNullOrEmpty(info.Url) ? "失败" : info.Url)}");
                }
                snapshot.Add(info);
            }

            SceneManager.SaveScene(name, snapshot);
            MessageBox.Show($"场景 [{name}] 已保存!(包含 {snapshot.Count} 个窗口)");
            
            this.Activate(); 
        };
        _sceneSaveBtn.MouseDown += (s, e) => {
            if (e.Button == MouseButtons.Right) {
                ShowSceneGallery();
                this.Activate();
            }
        };

        this.Controls.Add(_sceneSaveBtn);
        this._toolTip.SetToolTip(_sceneSaveBtn, "左键:保存当前场景\r\n右键:加载已有场景");

        // 确认按钮 (右)
        _confirmBtn = new Button {
            Size = new Size(btnSize, btnSize),
            Location = new Point(centerX + btnSize/2 + spacing, centerY),
            FlatStyle = FlatStyle.Flat, BackColor = Color.FromArgb(200, 20, 20, 20), ForeColor = Color.FromArgb(46, 204, 113),
            Text = "✔", Font = new Font("Segoe UI Symbol", 18, FontStyle.Bold), Cursor = Cursors.Hand
        };
        _confirmBtn.FlatAppearance.BorderSize = 0;
        GraphicsPath p2 = new GraphicsPath(); p2.AddEllipse(0, 0, btnSize, btnSize); _confirmBtn.Region = new Region(p2);
        _confirmBtn.Click += (s, e) => this.Close();
        this.Controls.Add(_confirmBtn);
    }

    private void ShowSceneGallery() {
        if (_galleryOverlay == null) {
            // 不再使用全屏遮罩,直接创建悬浮画廊容器
            _galleryOverlay = new Panel {
                Size = new Size(820, 500),
                BackColor = Color.FromArgb(255, 25, 25, 25),
                Location = new Point((this.Width - 820) / 2, (this.Height - 500) / 2),
                Visible = false
            };
            GraphicsPath path = new GraphicsPath(); 
            int r = 20; path.AddArc(0, 0, r, r, 180, 90); path.AddArc(_galleryOverlay.Width - r, 0, r, r, 270, 90);
            path.AddArc(_galleryOverlay.Width - r, _galleryOverlay.Height - r, r, r, 0, 90); path.AddArc(0, _galleryOverlay.Height - r, r, r, 90, 90);
            _galleryOverlay.Region = new Region(path);
            
            var title = new Label {
                Text = " 我的布局场境 (右键管理)", ForeColor = Color.White, 
                Font = new Font("微软雅黑", 14, FontStyle.Bold),
                Location = new Point(30, 20), AutoSize = true
            };
            _galleryOverlay.Controls.Add(title);

            // 关闭按钮
            var closeBtn = new Label {
                Text = "✕", ForeColor = Color.Gray, Font = new Font("Segoe UI", 14, FontStyle.Bold),
                Location = new Point(780, 10), AutoSize = true, Cursor = Cursors.Hand
            };
            closeBtn.Click += (s, e) => { _galleryOverlay.Visible = false; };
            closeBtn.MouseEnter += (s, e) => { closeBtn.ForeColor = Color.White; };
            closeBtn.MouseLeave += (s, e) => { closeBtn.ForeColor = Color.Gray; };
            _galleryOverlay.Controls.Add(closeBtn);

            _sceneGallery = new FlowLayoutPanel {
                Location = new Point(20, 60), Size = new Size(780, 420),
                AutoScroll = true, BackColor = Color.Transparent
            };
            _galleryOverlay.Controls.Add(_sceneGallery);
            this.Controls.Add(_galleryOverlay);
        }

        _sceneGallery.Controls.Clear();
        var all = SceneManager.LoadAll();
        if (all.Count == 0) {
            _sceneGallery.Controls.Add(new Label { Text = "空空如也,快去点击  保存一个吧!", ForeColor = Color.Gray, AutoSize = true, Margin = new Padding(30) });
        }

        foreach (var s in all) {
            var card = new Panel { Size = new Size(240, 180), Margin = new Padding(10), BackColor = Color.FromArgb(40, 40, 40), Cursor = Cursors.Hand };
            
            PictureBox pic = new PictureBox {
                Size = new Size(220, 130), Location = new Point(10, 10),
                SizeMode = PictureBoxSizeMode.Zoom, BackColor = Color.Black
            };
            if (File.Exists(s.ThumbnailPath)) pic.Image = Image.FromFile(s.ThumbnailPath);

            Label name = new Label {
                Text = s.Name, ForeColor = Color.White, TextAlign = ContentAlignment.MiddleCenter,
                Size = new Size(240, 30), Location = new Point(0, 145), Font = new Font("微软雅黑", 9)
            };

            card.Controls.Add(pic); card.Controls.Add(name);
            
            // 交互设置
            pic.Click += async (send, args) => { _galleryOverlay.Visible = false; await SceneManager.ApplyScene(s); };
            name.Click += async (send, args) => { _galleryOverlay.Visible = false; await SceneManager.ApplyScene(s); };
            card.Click += async (send, args) => { _galleryOverlay.Visible = false; await SceneManager.ApplyScene(s); };
            
            // Hover 变色
            card.MouseEnter += (send, args) => { card.BackColor = Color.FromArgb(70, 70, 70); };
            card.MouseLeave += (send, args) => { card.BackColor = Color.FromArgb(40, 40, 40); };

            // 右键菜单管理
            ContextMenuStrip cm = new ContextMenuStrip();
            var currentCard = card; // 捕获当前卡片引用
            var currentScene = s;   // 捕获当前场景引用
            cm.Items.Add("️ 删除此场景", null, (send, args) => { 
                var list = SceneManager.LoadAll(); 
                list.RemoveAll(x => x.Name == currentScene.Name);
                if (File.Exists(currentScene.ThumbnailPath)) try { File.Delete(currentScene.ThumbnailPath); } catch{}
                string scenesFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Quicker", "LayoutScenes", "WindowLayoutScenes.json");
                File.WriteAllText(scenesFile, Newtonsoft.Json.JsonConvert.SerializeObject(list));
                // 直接移除卡片
                _sceneGallery.Controls.Remove(currentCard);
                currentCard.Dispose();
                // 如果没有场景了,显示提示
                if (_sceneGallery.Controls.Count == 0) {
                    _sceneGallery.Controls.Add(new Label { Text = "空空如也,快去点击  保存一个吧!", ForeColor = Color.Gray, AutoSize = true, Margin = new Padding(30) });
                }
            });
            cm.Items.Add(" 全部清空", null, (send, args) => { 
                SceneManager.Clear(); 
                _sceneGallery.Controls.Clear();
                _sceneGallery.Controls.Add(new Label { Text = "空空如也,快去点击  保存一个吧!", ForeColor = Color.Gray, AutoSize = true, Margin = new Padding(30) });
            });
            card.ContextMenuStrip = cm;

            _sceneGallery.Controls.Add(card);
        }

        _galleryOverlay.Visible = true;
        _galleryOverlay.BringToFront();
    }

    private void PerformReLayout()
    {
        var lockedItemMatches = new Dictionary<IntPtr, Rectangle>();
        var normalItems = new List<IntPtr>();
        var usedLocks = new HashSet<LockInfo>();

        foreach (var hwnd in _windows)
        {
            var exactMatch = _locks.Where(l => !usedLocks.Contains(l)).FirstOrDefault(l => LockManager.IsMatchStrict(hwnd, l));
            if (exactMatch != null) { lockedItemMatches[hwnd] = exactMatch.Rect; usedLocks.Add(exactMatch); }
            else normalItems.Add(hwnd);
        }

        if (normalItems.Count > 0)
        {
            int offset = _shuffleOffset % normalItems.Count;
            normalItems = normalItems.Skip(offset).Concat(normalItems.Take(offset)).ToList();
        }

        var assignment = LayoutAlgorithm.AssignSmartGaps(_workArea, lockedItemMatches, normalItems);
        foreach (var kvp in assignment) { if (WindowHelper.IsWindow(kvp.Key)) WindowHelper.MoveWindowCompensated(kvp.Key, kvp.Value); }
        RebuildUI(assignment, lockedItemMatches.Keys.ToHashSet());
    }

    private void RebuildUI(Dictionary<IntPtr, Rectangle> assignment, HashSet<IntPtr> actuallyLockedHwnds)
    {
        _iconContainer.SuspendLayout();
        _iconContainer.Controls.Clear();
        foreach (var kvp in assignment)
        {
            IntPtr hwnd = kvp.Key;
            if (!WindowHelper.IsWindow(hwnd)) continue;
            var btn = new LockButton(hwnd, kvp.Value);
            bool isLocked = actuallyLockedHwnds.Contains(hwnd);
            btn.UpdateIcon(isLocked);
            btn.Click += (s, e) => { ToggleLock(btn); this.Activate(); if (_autoExitTimer != null) { _autoExitTimer.Stop(); _autoExitTimer.Start(); } };
            
            string title = WindowHelper.GetWindowTitle(hwnd);
            string proc = LockManager.GetProcessName(hwnd);
            _toolTip.SetToolTip(btn, $"[{proc}]\r\n{title}");

            ContextMenuStrip cm = new ContextMenuStrip();
            cm.Items.Add($" 永久忽略 [{proc}]", null, (sender, args) => {
               if(MessageBox.Show($"确定要忽略 [{proc}] 吗?", "加入黑名单", MessageBoxButtons.YesNo) == DialogResult.Yes) { BlacklistManager.Add(proc); this.TriggerManualRefresh(); }
               this.Activate();
            });
            cm.Items.Add(new ToolStripSeparator());
            cm.Items.Add(" 管理黑名单", null, (sender, args) => { BlacklistManager.OpenEditor(); this.Activate(); });
            btn.ContextMenuStrip = cm;
            _iconContainer.Controls.Add(btn);
            
            // 添加拖拽手柄按钮(在锁按钮右侧)
            var dragHandle = new DragHandle(hwnd, kvp.Value, btn);
            _toolTip.SetToolTip(dragHandle, "拖拽到另一个手柄可交换窗口位置");
            _iconContainer.Controls.Add(dragHandle);
        }
        _iconContainer.ResumeLayout();
        
        // 关键修复:确保所有控制按钮永远在图标容器之上
        foreach(Control c in this.Controls) {
            if (c != _iconContainer) c.BringToFront();
        }
    }

    private void ToggleLock(LockButton btn)
    {
        string pName = LockManager.GetProcessName(btn.Hwnd);
        string title = WindowHelper.GetWindowTitle(btn.Hwnd);
        string matchKey = WindowHelper.GetSmartMatchKey(btn.Hwnd);
        
        var existing = _locks.FirstOrDefault(l => LockManager.IsMatchStrict(btn.Hwnd, l));
        if (existing == null) {
            Rectangle realRect = WindowHelper.GetVisibleRect(btn.Hwnd);
            _locks.Add(new LockInfo { ProcessName = pName, Title = title, MatchKey = matchKey, Rect = realRect });
            btn.UpdateIcon(true);
        }
        else { _locks.Remove(existing); btn.UpdateIcon(false); }
        LockManager.Save(_locks);
    }
}

public class LockButton : Label {
    public IntPtr Hwnd; public Rectangle CurrentWindowRect; public bool IsLocked;
    
    public LockButton(IntPtr h, Rectangle r) {
        Hwnd = h; Size = new Size(32,32); Cursor = Cursors.Hand; TextAlign = ContentAlignment.MiddleCenter;
        Font = new Font("Segoe UI Emoji", 12); SetPositionByWindow(r);
    }
    
    public void SetPositionByWindow(Rectangle r) { CurrentWindowRect = r; this.Location = new Point(r.Left + 5, r.Top + 5); }
    
    public void UpdateIcon(bool locked) { 
        IsLocked = locked; 
        if (locked) {
            Text = ""; 
            ForeColor = Color.White; 
            BackColor = Color.FromArgb(220, 46, 204, 113); // 锁定:翡翠绿背景
        } else {
            Text = "";
            ForeColor = Color.Black; 
            BackColor = Color.FromArgb(220, 220, 220, 220); // 未锁:浅灰色背景
        }
    }
}

public class DragHandle : Label {
    public IntPtr Hwnd;
    public LockButton LinkedLock;
    
    private bool _isDragging = false;
    private Point _dragStartPoint;
    private Point _originalLocation;
    private static DragHandle _dragSource = null;
    
    public DragHandle(IntPtr h, Rectangle r, LockButton lockBtn) {
        Hwnd = h;
        LinkedLock = lockBtn;
        Size = new Size(20, 32);
        Cursor = Cursors.SizeAll;
        TextAlign = ContentAlignment.MiddleCenter;
        Font = new Font("Consolas", 10, FontStyle.Bold);
        Text = "⋮";
        ForeColor = Color.White;
        BackColor = Color.FromArgb(180, 149, 165, 166); // 银灰色
        Location = new Point(r.Left + 40, r.Top + 5); // 在锁按钮右侧
        
        this.MouseDown += DragHandle_MouseDown;
        this.MouseMove += DragHandle_MouseMove;
        this.MouseUp += DragHandle_MouseUp;
    }
    
    public void SetPositionByWindow(Rectangle r) {
        this.Location = new Point(r.Left + 40, r.Top + 5);
    }
    
    private void DragHandle_MouseDown(object sender, MouseEventArgs e) {
        if (e.Button == MouseButtons.Left) {
            _dragStartPoint = e.Location;
            _originalLocation = this.Location;
        }
    }
    
    private void DragHandle_MouseMove(object sender, MouseEventArgs e) {
        if (e.Button == MouseButtons.Left) {
            // 判断是否开始拖拽(移动超过 3 像素)
            if (!_isDragging && (Math.Abs(e.X - _dragStartPoint.X) > 3 || Math.Abs(e.Y - _dragStartPoint.Y) > 3)) {
                _isDragging = true;
                _dragSource = this;
                this.BackColor = Color.FromArgb(220, 52, 152, 219); // 拖拽时:蓝色
                this.BringToFront();
            }
            
            if (_isDragging) {
                this.Left += e.X - _dragStartPoint.X;
                this.Top += e.Y - _dragStartPoint.Y;
            }
        }
    }
    
    private void DragHandle_MouseUp(object sender, MouseEventArgs e) {
        if (_isDragging && _dragSource == this) {
            _isDragging = false;
            _dragSource = null;
            
            // 查找目标(鼠标下方的其他拖拽手柄或锁按钮)
            Point screenPos = this.PointToScreen(e.Location);
            DragHandle target = null;
            
            foreach (Control c in this.Parent.Controls) {
                if (c is DragHandle dh && dh != this) {
                    Rectangle dhScreen = dh.RectangleToScreen(dh.ClientRectangle);
                    if (dhScreen.Contains(screenPos)) {
                        target = dh;
                        break;
                    }
                }
                // 也检测锁按钮
                if (c is LockButton lb && lb != this.LinkedLock) {
                    Rectangle lbScreen = lb.RectangleToScreen(lb.ClientRectangle);
                    if (lbScreen.Contains(screenPos)) {
                        // 找到对应的 DragHandle
                        foreach (Control cc in this.Parent.Controls) {
                            if (cc is DragHandle dh2 && dh2.LinkedLock == lb) {
                                target = dh2;
                                break;
                            }
                        }
                        break;
                    }
                }
            }
            
            if (target != null) {
                SwapWindowPositions(this, target);
            } else {
                this.Location = _originalLocation;
            }
            
            this.BackColor = Color.FromArgb(180, 149, 165, 166);
        }
    }
    
    private void SwapWindowPositions(DragHandle src, DragHandle dst) {
        Rectangle srcRect = WindowHelper.GetVisibleRect(src.Hwnd);
        Rectangle dstRect = WindowHelper.GetVisibleRect(dst.Hwnd);
        
        Logger.Log($"[Swap] 开始交换位置: {LockManager.GetProcessName(src.Hwnd)} <-> {LockManager.GetProcessName(dst.Hwnd)}");
        Logger.Log($"  - 源窗口: {srcRect}");
        Logger.Log($"  - 目标窗口: {dstRect}");
        
        // 交换窗口位置
        WindowHelper.MoveWindowCompensated(src.Hwnd, dstRect);
        WindowHelper.MoveWindowCompensated(dst.Hwnd, srcRect);
        
        // 更新锁按钮和拖拽手柄的位置
        src.LinkedLock.SetPositionByWindow(dstRect);
        dst.LinkedLock.SetPositionByWindow(srcRect);
        src.SetPositionByWindow(dstRect);
        dst.SetPositionByWindow(srcRect);
        
        // 交换内部记录
        src.LinkedLock.CurrentWindowRect = dstRect;
        dst.LinkedLock.CurrentWindowRect = srcRect;
        
        // 更新锁定信息
        if (OverlayForm.Instance != null) {
            var locks = OverlayForm.Instance._locks;
            var srcLock = locks.FirstOrDefault(l => LockManager.IsMatchStrict(src.Hwnd, l));
            var dstLock = locks.FirstOrDefault(l => LockManager.IsMatchStrict(dst.Hwnd, l));
            
            if (srcLock != null) srcLock.Rect = dstRect;
            if (dstLock != null) dstLock.Rect = srcRect;
            
            LockManager.Save(locks);
        }
        
        Logger.Log($"[Swap] 交换完成");
    }
}

public static class LayoutAlgorithm {
    private static string _lastLayoutLog = "";

    // 【算法升级 V4】全动态填补:彻底解决重叠
    public static Dictionary<IntPtr, Rectangle> AssignSmartGaps(Rectangle workArea, Dictionary<IntPtr, Rectangle> lockedWins, List<IntPtr> normals) {
        var res = new Dictionary<IntPtr, Rectangle>();
        List<Rectangle> gaps = new List<Rectangle> { workArea };

        // 1. 将所有锁定窗口物理占用的空间从总画布中剔除
        foreach (var kvp in lockedWins) {
            res[kvp.Key] = kvp.Value;
            var tempGaps = new List<Rectangle>();
            foreach (var g in gaps) tempGaps.AddRange(SubtractRectangle(g, kvp.Value));
            gaps = tempGaps.Where(r => r.Width > 50 && r.Height > 50).ToList();
        }

        // 减少此处的冗余日志
        string currentLog = $"Normals:{normals.Count}|Gaps:{gaps.Count}";
        if (currentLog != _lastLayoutLog) {
            _lastLayoutLog = currentLog;
            Logger.Log($"[Layout-V4] 锁定剔除完成,可用坑位: {gaps.Count} | 待填入: {normals.Count}");
        }

        if (normals.Count == 0) return res;

        // 2. 如果坑位不够,把最大的坑位切分
        while (gaps.Count < normals.Count && gaps.Count > 0) {
            gaps = gaps.OrderByDescending(r => r.Width * r.Height).ToList();
            Rectangle big = gaps[0]; gaps.RemoveAt(0);
            if (big.Width > big.Height) {
                gaps.Add(new Rectangle(big.X, big.Y, big.Width / 2, big.Height));
                gaps.Add(new Rectangle(big.X + big.Width / 2, big.Y, big.Width - big.Width / 2, big.Height));
            } else {
                gaps.Add(new Rectangle(big.X, big.Y, big.Width, big.Height / 2));
                gaps.Add(new Rectangle(big.X, big.Y + big.Height / 2, big.Width, big.Height - big.Height / 2));
            }
        }

        // 3. 按照面积从大到小排列坑位,依次填入普通窗口
        gaps = gaps.OrderByDescending(r => r.Width * r.Height).ToList();
        for (int i = 0; i < normals.Count; i++) {
            if (i < gaps.Count) res[normals[i]] = gaps[i];
            else if (gaps.Count > 0) res[normals[i]] = gaps[i % gaps.Count]; // 极端兜底:重叠
        }
        
        return res;
    }

    // 经典矩形剔除逻辑 (CSG)
    private static List<Rectangle> SubtractRectangle(Rectangle baseRect, Rectangle sub) {
        var results = new List<Rectangle>();
        Rectangle inter = Rectangle.Intersect(baseRect, sub);
        if (inter.IsEmpty) { results.Add(baseRect); return results; }

        if (inter.Top > baseRect.Top) results.Add(new Rectangle(baseRect.X, baseRect.Y, baseRect.Width, inter.Y - baseRect.Y));
        if (inter.Bottom < baseRect.Bottom) results.Add(new Rectangle(baseRect.X, inter.Bottom, baseRect.Width, baseRect.Bottom - inter.Bottom));
        if (inter.Left > baseRect.Left) results.Add(new Rectangle(baseRect.X, inter.Y, inter.X - baseRect.X, inter.Height));
        if (inter.Right < baseRect.Right) results.Add(new Rectangle(inter.Right, inter.Y, baseRect.Right - inter.Right, inter.Height));

        return results;
    }
}

public static class BlacklistManager {
    static string F = Environment.ExpandEnvironmentVariables(@"%AppData%\Quicker\WindowLayoutBlacklist.txt");
    private static HashSet<string> _cache;
    public static void Reload() {
        if (!File.Exists(F)) _cache = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        else _cache = new HashSet<string>(File.ReadAllLines(F).Where(x => !string.IsNullOrWhiteSpace(x)), StringComparer.OrdinalIgnoreCase);
    }
    public static bool Contains(string processName) { if (_cache == null) Reload(); return _cache.Contains(processName); }
    public static void Add(string processName) { if (_cache == null) Reload(); if (_cache.Contains(processName)) return; _cache.Add(processName); File.AppendAllLines(F, new[] { processName }); }
    public static void OpenEditor() { if (!File.Exists(F)) File.WriteAllText(F, ""); System.Diagnostics.Process.Start("notepad.exe", F); }
}

public static class WindowHelper {
    [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr h);
    [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
    [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
    [DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int h2, bool b);
    [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out Rect r);
    [DllImport("dwmapi.dll")] public static extern int DwmGetWindowAttribute(IntPtr h, int a, out Rect r, int s);
    [DllImport("dwmapi.dll", EntryPoint = "DwmGetWindowAttribute")] public static extern int DwmGetCloaked(IntPtr h, int a, out int v, int s);
    [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc p, IntPtr l); public delegate bool EnumProc(IntPtr h, IntPtr l);
    [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int m);
    [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern int GetClassName(IntPtr h, StringBuilder s, int m);
    [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int n);
    [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr h);

    [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; }
    public static string GetWindowTitle(IntPtr hwnd) { StringBuilder sb = new StringBuilder(256); GetWindowText(hwnd, sb, 256); return sb.ToString(); }
    public static void MoveWindowCompensated(IntPtr hwnd, Rectangle targetVisible) {
        Rect logical, visible; GetWindowRect(hwnd, out logical);
        if (DwmGetWindowAttribute(hwnd, 9, out visible, Marshal.SizeOf(typeof(Rect))) != 0) { MoveWindow(hwnd, targetVisible.X, targetVisible.Y, targetVisible.Width, targetVisible.Height, true); return; }
        int dL = visible.Left - logical.Left, dT = visible.Top - logical.Top, dR = logical.Right - visible.Right, dB = logical.Bottom - visible.Bottom;
        MoveWindow(hwnd, targetVisible.X - dL, targetVisible.Y - dT, targetVisible.Width + dL + dR, targetVisible.Height + dT + dB, true);
    }
    public static Rectangle GetVisibleRect(IntPtr hwnd) {
        Rect v; if (DwmGetWindowAttribute(hwnd, 9, out v, Marshal.SizeOf(typeof(Rect))) == 0) return new Rectangle(v.Left, v.Top, v.Right - v.Left, v.Bottom - v.Top);
        Rect r; if (GetWindowRect(hwnd, out r)) return new Rectangle(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
        return Rectangle.Empty;
    }
    [DllImport("oleacc.dll")] static extern int AccessibleObjectFromWindow(IntPtr hwnd, uint dwId, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject);

    public static string GetExecutablePath(IntPtr hwnd) {
        try {
            GetWindowThreadProcessId(hwnd, out int pid);
            return Process.GetProcessById(pid).MainModule.FileName;
        } catch { return ""; }
    }
    [DllImport("user32.dll")] static extern int GetWindowThreadProcessId(IntPtr h, out int p);

    // 获取资源管理器窗口当前浏览的文件夹路径
    public static string GetExplorerPath(IntPtr hwnd) {
        try {
            dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));
            foreach (dynamic win in shell.Windows()) {
                if ((IntPtr)(long)win.HWND == hwnd) {
                    string path = win.Document.Folder.Self.Path;
                    return path ?? "";
                }
            }
        } catch { }
        return "";
    }

    public static string GetSmartMatchKey(IntPtr hwnd) {
        string pn = LockManager.GetProcessName(hwnd);
        string title = GetWindowTitle(hwnd);
        
        // 1. 针对浏览器,优先获取域名
        if (pn == "msedge" || pn == "chrome") {
            string url = GetBrowserUrl(hwnd);
            if (!string.IsNullOrEmpty(url)) {
                try { 
                    var match = Regex.Match(url, @"[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+");
                    if (match.Success) return "domain:" + match.Value;
                } catch {}
            }
        }
        
        // 2. 否则进行标题清洗
        string cleanTitle = Regex.Replace(title, @"( 和另外 \d+ 个页面)? - .*$", "");
        cleanTitle = Regex.Replace(cleanTitle, @" - (Microsoft Edge|Google Chrome)$", "");
        return "title:" + cleanTitle.Trim();
    }

    public static string GetBrowserUrl(IntPtr hwnd) {
        // 使用托管 UI Automation API (参考 EqualFill.cs)
        try {
            AutomationElement root = AutomationElement.FromHandle(hwnd);
            if (root == null) return null;
            
            // 查找所有 Edit 控件
            var edits = root.FindAll(TreeScope.Descendants, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit));
            
            int count = 0;
            foreach (AutomationElement edit in edits) {
                if (count++ > 5) break; // 只检查前5个
                string val = null;
                
                // 尝试 ValuePattern
                if (edit.TryGetCurrentPattern(ValuePattern.Pattern, out object vPtrn)) {
                    val = ((ValuePattern)vPtrn).Current.Value;
                } else {
                    val = edit.Current.Name;
                }
                
                if (!string.IsNullOrEmpty(val)) {
                    // 简单验证是否像 URL
                    if (val.StartsWith("http") || val.StartsWith("file:") || (val.Contains(".") && !val.Contains(" "))) {
                        return val.StartsWith("http") ? val : (val.StartsWith("file:") ? val : "https://" + val);
                    }
                }
            }
        } catch { }
        
        // 回退:传统 IAccessible
        try {
            Guid guid = new Guid("{61873046-5153-11ce-9a88-00aa006f1a39}");
            object obj;
            if (AccessibleObjectFromWindow(hwnd, 0xFFFFFFFC, ref guid, out obj) == 0) {
                var acc = (IAccessible)obj;
                return FindUrlInAcc(acc);
            }
        } catch {}
        return null;
    }

    private static string FindUrlInAcc(IAccessible acc) {
        try {
            if (acc.accValue != null && acc.accValue.ToString().StartsWith("http")) return acc.accValue.ToString();
            for (int i = 1; i <= acc.accChildCount; i++) {
                if (acc.get_accChild(i) is IAccessible child) {
                    string res = FindUrlInAcc(child);
                    if (res != null) return res;
                }
            }
        } catch {}
        return null;
    }

    public static List<IntPtr> GetFilterWindows() {
        var rawList = new List<IntPtr>();
        EnumWindows((h, p) => {
            if (!IsWindow(h) || !IsWindowVisible(h) || IsIconic(h)) return true;
            int style = GetWindowLong(h, -16); 
            if ((style & 0x10000000) == 0) return true; 
            if (GetParent(h) != IntPtr.Zero) return true;
            
            int cl; if (DwmGetCloaked(h, 14, out cl, 4) == 0 && cl != 0) return true;
            Rectangle r = GetVisibleRect(h); if (r.Width < 100 || r.Height < 100) return true;
            
            StringBuilder sbT = new StringBuilder(256); GetWindowText(h, sbT, 256); string t = sbT.ToString();
            StringBuilder sbC = new StringBuilder(256); GetClassName(h, sbC, 256); string cn = sbC.ToString();
            string pn = LockManager.GetProcessName(h);
            
            // 只有在调试模式下且窗口列表有变动时才由外部控制是否记录(此处先扫描)
            if (string.IsNullOrEmpty(t) || t == "Program Manager" || t == "Quicker_WindowLayoutOverlay_Host" || cn.Contains("WindowsForms10.Window.8.app")) return true;
            // 采用更安全的实例判定,防止访问已销毁的实例句柄
            var inst = OverlayForm.Instance;
            if (inst != null && !inst.IsDisposed) {
                try { if (h == inst.Handle) return true; } catch { /* 忽略可能存在的瞬间销毁异常 */ }
            }
            
            if ((pn == "Quicker" || pn == "Typeless" || pn == "Antigravity") && (t == "Status" || t == "Quicker" || t == "NotificationsWindow" || t == "QuickerStarter")) return true;
            if (t == "Status" && r.X == 0 && r.Y == 0) return true;
            if (pn.StartsWith("PixPin", StringComparison.OrdinalIgnoreCase)) return true;
            if (cn == "Shell_TrayWnd" || cn == "WorkerW" || cn == "Progman") return true; 

            rawList.Add(h); return true;
        }, IntPtr.Zero);

        var finalMap = new Dictionary<string, IntPtr>();
        foreach (var h in rawList) {
            string key = LockManager.GetProcessName(h) + "|" + GetWindowTitle(h);
            if (finalMap.ContainsKey(key)) { if ((long)h > (long)finalMap[key]) finalMap[key] = h; } 
            else finalMap[key] = h;
        }
        var result = finalMap.Values.ToList();
        
        // 日志指纹逻辑:防止重复记录
        string currentFingerprint = string.Join(",", result.Select(h => h.ToString() + GetWindowTitle(h)));
        if (currentFingerprint != _lastFingerprint) {
            _lastFingerprint = currentFingerprint;
            Logger.Log($"[Filter-Done] 窗口列表变动,识别到参与排版的独立窗口数量: {result.Count}");
            foreach(var h in result) Logger.Log($"  - HWND:{h} | Proc:{LockManager.GetProcessName(h)} | Title:\"{GetWindowTitle(h)}\"");
        }
        
        return result;
    }
    private static string _lastFingerprint = "";
}

public class LockInfo { 
    public string ProcessName; public string Title; public string MatchKey; public Rectangle Rect; 
    public string ExecPath; public string Url;
    public override string ToString() => $"{ProcessName}|{Title}|{Rect.X},{Rect.Y},{Rect.Width},{Rect.Height}|{MatchKey}|{ExecPath}|{Url}"; 
}

public class SceneProfile {
    public string Name;
    public List<LockInfo> Items;
    public string ThumbnailPath;
}

public static class SceneManager {
    static string Root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Quicker", "LayoutScenes");
    static string F = Path.Combine(Root, "WindowLayoutScenes.json");
    
    public static void SaveScene(string name, List<LockInfo> currentSnapshot) {
        if (!Directory.Exists(Root)) Directory.CreateDirectory(Root);
        var all = LoadAll();
        all.RemoveAll(x => x.Name == name);

        string thumbName = $"thumb_{DateTime.Now.Ticks}.jpg";
        string thumbPath = Path.Combine(Root, thumbName);
        CaptureScreenshot(thumbPath);

        all.Add(new SceneProfile { Name = name, Items = currentSnapshot, ThumbnailPath = thumbPath });
        File.WriteAllText(F, Newtonsoft.Json.JsonConvert.SerializeObject(all, Newtonsoft.Json.Formatting.Indented));
        
        Logger.Log($"[SaveScene] 场景文件已保存至磁盘: {all.Count} 个历史场景");
        foreach(var item in currentSnapshot) Logger.Log($"  - 记录: {item.ProcessName} | T:\"{item.Title}\" | P:\"{item.ExecPath}\" | Url:\"{item.Url}\"");
    }

    private static void CaptureScreenshot(string path) {
        try {
            var bounds = Screen.PrimaryScreen.Bounds;
            using (var bmp = new Bitmap(bounds.Width, bounds.Height)) {
                using (var g = Graphics.FromImage(bmp)) {
                    g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
                }
                // 缩小为 160x90 的图标级别缩略图,节省空间
                using (var thumb = bmp.GetThumbnailImage(160, 90, null, IntPtr.Zero)) {
                    thumb.Save(path, System.Drawing.Imaging.ImageFormat.Jpeg);
                }
            }
        } catch {}
    }

    public static void Clear() { if(File.Exists(F)) File.Delete(F); if(Directory.Exists(Root)) { try { Directory.Delete(Root, true); } catch {} } }
    public static List<SceneProfile> LoadAll() {
        if (!File.Exists(F)) return new List<SceneProfile>();
        try { return Newtonsoft.Json.JsonConvert.DeserializeObject<List<SceneProfile>>(File.ReadAllText(F)); } catch { return new List<SceneProfile>(); }
    }

    public static Task ApplyScene(SceneProfile scene) {
        Logger.Log($"[ApplyScene] >>> 开始恢复场景: {scene.Name} (包含记录: {scene.Items.Count} 个)");
        var hwnds = WindowHelper.GetFilterWindows(); 
        
        // 打印当前布局实况
        Logger.Log($"[ApplyScene] --- 当前布局实况 (恢复前) ---");
        foreach(var h in hwnds) {
            var r = WindowHelper.GetVisibleRect(h);
            Logger.Log($"  - {LockManager.GetProcessName(h)} | Pos: {r.X},{r.Y},{r.Width}x{r.Height} | Title: \"{WindowHelper.GetWindowTitle(h)}\"");
        }
        
        Logger.Log($"[ApplyScene] 当前系统范围内待匹配窗口总数: {hwnds.Count}");

        int successCount = 0;
        foreach (var item in scene.Items) {
            var target = hwnds.FirstOrDefault(h => LockManager.IsMatchStrict(h, item));
            
            if (target != IntPtr.Zero) {
                Logger.Log($"[ApplyScene] [√] 匹配成功: {item.ProcessName} | \"{item.Title}\" -> 移动至 {item.Rect}");
                WindowHelper.MoveWindowCompensated(target, item.Rect);
                successCount++;
            } else if (!string.IsNullOrEmpty(item.ExecPath)) {
                Logger.Log($"[ApplyScene] [?] 窗口未找到,尝试自动化拉起: {item.ProcessName} | 路径: {item.ExecPath}");
                try {
                    string args = "";
                    if (!string.IsNullOrEmpty(item.Url)) {
                        if (item.ProcessName == "msedge" || item.ProcessName == "chrome") {
                            args = $"--new-window \"{item.Url}\"";
                        } else if (item.ProcessName == "explorer") {
                            args = $"\"{item.Url}\""; // 资源管理器直接用路径作为参数
                        }
                    }
                    Process.Start(item.ExecPath, args);
                    _ = Task.Run(async () => {
                        for (int i = 0; i < 20; i++) {
                            await Task.Delay(500);
                            var currentHwnds = WindowHelper.GetFilterWindows();
                            var h = currentHwnds.FirstOrDefault(wh => LockManager.IsMatchStrict(wh, item));
                            if (h != IntPtr.Zero) {
                                Logger.Log($"[ApplyScene-Async] [√] 延迟归位成功: {item.ProcessName}");
                                WindowHelper.MoveWindowCompensated(h, item.Rect);
                                break;
                            }
                        }
                    });
                } catch (Exception ex) {
                    Logger.Log($"[ApplyScene] [!] 拉起失败: {ex.Message}");
                }
            } else {
                 Logger.Log($"[ApplyScene] [X] 匹配失败且无路径可拉起: {item.ProcessName} | {item.Title}");
            }
        }
        Logger.Log($"[ApplyScene] <<< 恢复任务发送完毕,直接命中数: {successCount}");
        return Task.CompletedTask;
    }
}

[ComImport, Guid("61873046-5153-11ce-9a88-00aa006f1a39"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IAccessible {
    [DispId(-5000)] object accParent { get; }
    [DispId(-5001)] int accChildCount { get; }
    [DispId(-5003)] string accName { get; }
    [DispId(-5004)] string accValue { get; }
    [DispId(-5012)] object get_accChild(object childID);
}

public static class LockManager {
    static string F = Environment.ExpandEnvironmentVariables(@"%AppData%\Quicker\WindowLayoutLocks.txt");
    public static List<LockInfo> Load() => !File.Exists(F) ? new List<LockInfo>() : File.ReadAllLines(F).Select(l => {
        try { var p=l.Split('|'); if(p.Length < 3) return null; var r=p[2].Split(','); 
            var info = new LockInfo{ProcessName=p[0], Title=p[1], Rect=new Rectangle(int.Parse(r[0]),int.Parse(r[1]),int.Parse(r[2]),int.Parse(r[3]))};
            if (p.Length >= 4) info.MatchKey = p[3];
            return info;
        } catch{return null;}
    }).Where(x=>x!=null).ToList();
    public static void Save(List<LockInfo> l) { try { string tmp = F + ".tmp"; File.WriteAllLines(tmp, l.Select(x=>x.ToString())); if(File.Exists(F)) File.Delete(F); File.Move(tmp, F); } catch { } }
    
    public static bool IsMatchStrict(IntPtr h, LockInfo i) { 
        if (!string.Equals(GetProcessName(h), i.ProcessName, StringComparison.OrdinalIgnoreCase)) return false;
        
        // 资源管理器特殊处理:使用文件夹路径匹配
        if (i.ProcessName == "explorer" && !string.IsNullOrEmpty(i.Url)) {
            string currentPath = WindowHelper.GetExplorerPath(h);
            if (!string.IsNullOrEmpty(currentPath) && string.Equals(currentPath, i.Url, StringComparison.OrdinalIgnoreCase)) return true;
            return false; // 路径不匹配则直接失败
        }
        
        // 浏览器特殊处理:需要匹配 URL 或标题
        if (i.ProcessName == "msedge" || i.ProcessName == "chrome") {
            // 走后续的标题/MatchKey 匹配逻辑
        }
        // 其他单窗口应用(代码编辑器、普通软件等):进程名相同即可匹配
        else {
            return true; // 直接匹配成功
        }
        
        string currentKey = WindowHelper.GetSmartMatchKey(h);
        string currentTitle = WindowHelper.GetWindowTitle(h);
        
        // 核心优化:清洗标题(剔除浏览器动态增加的后缀)
        string cleanCurrent = CleanTitle(currentTitle);
        string cleanSave = CleanTitle(i.Title);

        // 1. 尝试 MatchKey (精确域名匹配)
        if (!string.IsNullOrEmpty(i.MatchKey) && currentKey == i.MatchKey) return true;
        
        // 2. 尝试清洗后的标题完全匹配
        if (string.Equals(cleanCurrent, cleanSave, StringComparison.OrdinalIgnoreCase)) return true;

        // 3. 模糊包含(针对极端情况)
        if (!string.IsNullOrEmpty(cleanSave) && cleanCurrent.Contains(cleanSave)) return true;
        
        return false;
    }

    private static string CleanTitle(string t) {
        if (string.IsNullOrEmpty(t)) return "";
        // 剔除 "和另外 X 个页面"
        string res = System.Text.RegularExpressions.Regex.Replace(t, @" 和另外 \d+ 个页面.*$", "");
        // 剔除 " - Microsoft​ Edge" 或 " - Google Chrome"
        res = res.Replace(" - Microsoft​ Edge", "").Replace(" - Google Chrome", "");
        return res.Trim();
    }
    public static string GetProcessName(IntPtr h) { GetWindowThreadProcessId(h, out int pid); try { return System.Diagnostics.Process.GetProcessById(pid).ProcessName; } catch { return "Unknown"; } }
    [DllImport("user32.dll")] static extern int GetWindowThreadProcessId(IntPtr h, out int p);
}

public static class Logger {
    static string _p; public static void Init(string p) => _p=p;
    public static void Log(string m) { if (_p == null) return; try { File.AppendAllText(_p, $"[{DateTime.Now:HH:mm:ss}] {m}\r\n"); } catch {} }
}

动作配置 (JSON)

{
  "ActionId": "06b092f2-9672-4ca5-820e-36512d29c437",
  "Title": "智能窗口布局",
  "Description": "自动整理桌面窗口布局,支持1-N个窗口的智能排列。采用Z型优先级填充策略,左侧VIP席位+右侧网格布局。",
  "Icon": "fa:Solid_ThLarge:#4A90D9",
  "Variables": [
    {
      "Type": 0,
      "Desc": "结果文本(自动)",
      "IsOutput": false,
      "Key": "text"
    },
    {
      "Type": 0,
      "Desc": "返回值(自动)",
      "IsOutput": false,
      "Key": "rtn"
    },
    {
      "Type": 0,
      "Desc": "错误信息(自动)",
      "IsOutput": false,
      "Key": "errMessage"
    },
    {
      "Type": 2,
      "Desc": "静默启动",
      "DefaultValue": "true",
      "IsInput": false,
      "Key": "silent"
    },
    {
      "Type": 2,
      "Desc": "刷新后退出",
      "DefaultValue": "false",
      "IsInput": true,
      "SaveState": true,
      "Key": "instantMode"
    },
    {
      "Type": 1,
      "Desc": "自动退出时长(秒)",
      "DefaultValue": "0",
      "IsInput": true,
      "SaveState": true,
      "Key": "autoExitSeconds"
    }
  ],
  "References": []
}

使用方法

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