From 0960be88e54ab6d27f21a5369b325a5f59cc8ef6 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 3 Nov 2026 10:35:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A1=86=E9=80=89=E8=BD=AE=E8=AF=A2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=85=8D=20System.Drawing=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8,=20=E9=87=87=E6=A0=B7=E9=98=9F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E5=B9=B6=E5=8F=96=E6=B6=88=E9=98=BB=E5=A1=9E=E7=AD=89=E5=BE=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/desktop-pick.ts | 105 +++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/src/main/desktop-pick.ts b/src/main/desktop-pick.ts index 59b7993..fc0c0c7 100644 --- a/src/main/desktop-pick.ts +++ b/src/main/desktop-pick.ts @@ -722,40 +722,41 @@ interface MouseSample { } interface MouseButtonPoller { - /** 至少读到一次采样后为 true;超时/启动失败为 false,调用方回退到渲染层事件 */ - ready: Promise; - /** 最近一次采样;轮询进程退出后返回 null,调用方回退渲染层事件 */ - sample: () => MouseSample | null; + /** live=已产出采样; dead=进程退出(脚本编译失败/被杀); starting=启动中 */ + status: () => "live" | "dead" | "starting"; + /** 取走自上次调用以来的全部采样流(40ms 一条), 按下→松开的转移不会被主循环轮询间隔漏掉 */ + drain: () => MouseSample[]; stop: () => void; } // 覆盖层窗口可能被其他窗口盖住/失去焦点, 渲染层 mouseup/keydown 都可能收不到, // 导致"画得出选区却永远完不成"。改用主进程轮询物理左键状态+光标位置 // (GetAsyncKeyState/GetCursorPos), 与任何窗口的焦点、层级、事件路由完全解耦。 -// 按键状态与坐标来自同一次采样, 按下点不会因轮询间隔/PowerShell 冷启动延迟而漂移。 +// 采样进队列: 即使主循环在忙别的(或 PowerShell 冷启动后才就绪), +// 拖拽全程的按下点/松开点也都精确保留在采样流里。 +// 注意: GetCursorPos 参数用 int[] 而非 System.Drawing.Point—— +// Add-Type -MemberDefinition 编译期默认不引用 System.Drawing.dll, 用 Point 会导致编译失败、脚本直接退出 function spawnMouseButtonPoller(): MouseButtonPoller { const script = [ + "Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport(\"user32.dll\")] public static extern short GetAsyncKeyState(int vKey); [DllImport(\"user32.dll\")] public static extern bool GetCursorPos(int[] pt);' -Name K -Namespace WMouseButton | Out-Null", // 必须 DPI 感知, 否则 GetCursorPos 返回的是被系统虚拟化(96dpi)后的坐标 - "Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport(\"user32.dll\")] public static extern short GetAsyncKeyState(int vKey); [DllImport(\"user32.dll\")] public static extern bool GetCursorPos(out System.Drawing.Point p);' -Name K -Namespace WMouseButton | Out-Null", - "Add-Type -AssemblyName System.Drawing | Out-Null", "try { [void][WMouseButton.K]::SetProcessDpiAwarenessContext([IntPtr](-4)) } catch {}", "while ($true) {", " $down = ([WMouseButton.K]::GetAsyncKeyState(1) -band 0x8000) -ne 0", - " $p = New-Object System.Drawing.Point", - " [void][WMouseButton.K]::GetCursorPos([ref]$p)", - " [Console]::Out.Write([string]([int]$down) + ' ' + $p.X + ' ' + $p.Y + [char]10)", + " [int[]]$p = @(0,0)", + " [void][WMouseButton.K]::GetCursorPos($p)", + " [Console]::Out.Write([string]([int]$down) + ' ' + $p[0] + ' ' + $p[1] + [char]10)", " Start-Sleep -Milliseconds 40", "}" ].join("\n"); const child = spawn( "powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], - { stdio: ["ignore", "pipe", "ignore"], windowsHide: true } + { stdio: ["ignore", "pipe", "pipe"], windowsHide: true } ); - let sample: MouseSample | null = null; + const queue: MouseSample[] = []; + let live = false; let dead = false; - let readyResolve: (value: boolean) => void = () => {}; - const ready = new Promise(resolve => { readyResolve = resolve; }); let buffer = ""; child.stdout?.on("data", (chunk: Buffer) => { buffer += chunk.toString(); @@ -768,17 +769,30 @@ function spawnMouseButtonPoller(): MouseButtonPoller { const x = Number(parts[1]); const y = Number(parts[2]); if (Number.isFinite(x) && Number.isFinite(y)) { - sample = { down: parts[0] === "1", x, y }; - readyResolve(true); + queue.push({ down: parts[0] === "1", x, y }); + if (!live) { + live = true; + console.log("[region-pick] poller live"); + } } } } }); - child.on("error", () => { dead = true; readyResolve(false); }); - child.on("exit", () => { dead = true; }); + child.on("error", error => { + dead = true; + console.warn("[region-pick] poller spawn failed", error); + }); + child.on("exit", code => { + dead = true; + console.warn("[region-pick] poller exited, code=" + code); + }); + child.stderr?.on("data", (chunk: Buffer) => { + const text = chunk.toString().trim(); + if (text) console.warn("[region-pick] poller stderr:", text.slice(0, 500)); + }); return { - ready, - sample: () => (dead ? null : sample), + status: () => (dead ? "dead" : live ? "live" : "starting"), + drain: () => queue.splice(0, queue.length), stop: () => { try { child.kill(); } catch {} } @@ -817,12 +831,8 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: if (!win.isDestroyed() && display.id === cursorDisplay.id) win.focus(); } - const pollerReady = await Promise.race([ - poller.ready, - new Promise(resolve => setTimeout(() => resolve(false), 2500)) - ]); - if (!pollerReady) console.warn("[region-pick] 按键轮询未就绪, 回退渲染层事件"); - + // 不再阻塞等待轮询就绪: 采样进队列, 冷启动期间发生的拖拽也不会丢; + // 轮询死亡(starting/dead)时按 tick 回退渲染层事件 + getCursorScreenPoint const deadline = Date.now() + 120000; let cancelled = false; // Esc 全局兜底: 覆盖层失去键盘焦点时也能取消 @@ -836,14 +846,15 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: } // 选区坐标不取自窗口 clientX/clientY(混合 DPI 下窗口坐标映射不可靠); - // 优先用轮询采样(物理坐标经 screenToDipPoint 换算), 轮询不可用时回退渲染层事件 + getCursorScreenPoint + // 轮询 live: 按采样流识别按下→松开(40ms 粒度, 精确到帧); + // 轮询 starting/dead: 回退渲染层事件 + getCursorScreenPoint let downPoint: { x: number; y: number } | null = null; let sawDown = false; let picked: { rect: { x: number; y: number; width: number; height: number }; display: (typeof displays)[number] } | null = null; try { try { while (Date.now() < deadline) { - await new Promise(r => setTimeout(r, 80)); + await new Promise(r => setTimeout(r, 60)); if (cancelled) break; // 聚合所有覆盖层状态: 任一屏按 Esc 取消; 按下/松开仅作轮询失败时的回退 let rendererDown = false; @@ -859,17 +870,39 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: } catch {} } if (cancelled) break; + + if (poller.status() === "live") { + // 主链路: 处理采样队列里的按下→松开转移 + const samples = poller.drain(); + for (const s of samples) { + const dip = screen.screenToDipPoint({ x: s.x, y: s.y }); + if (!downPoint && s.down) { + downPoint = { x: dip.x, y: dip.y }; + sawDown = true; + console.log("[region-pick] down", downPoint, "(polled)"); + } else if (downPoint && !s.down) { + const display = displayForPoint(downPoint.x, downPoint.y); + const rx0 = Math.min(downPoint.x, dip.x); + const ry0 = Math.min(downPoint.y, dip.y); + const rx1 = Math.max(downPoint.x, dip.x); + const ry1 = Math.max(downPoint.y, dip.y); + picked = { rect: { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 }, display }; + console.log("[region-pick] up", picked.rect, "(polled)"); + break; + } + } + if (picked) break; + continue; + } + + // 回退链路: 轮询不可用, 用渲染层事件 + getCursorScreenPoint if (!anyAlive) continue; - const mouse = pollerReady ? poller.sample() : null; - // 按键与坐标同源采样; 轮询不可用时回退: 渲染层事件 + getCursorScreenPoint - const cursor = mouse - ? screen.screenToDipPoint({ x: mouse.x, y: mouse.y }) - : screen.getCursorScreenPoint(); - const down = mouse ? mouse.down : rendererDown; + const cursor = screen.getCursorScreenPoint(); + const down = rendererDown; if (down && !downPoint) { downPoint = { x: cursor.x, y: cursor.y }; sawDown = true; - console.log("[region-pick] down", downPoint, mouse ? "(polled)" : "(renderer)"); + console.log("[region-pick] down", downPoint, "(renderer)"); } if (!down && sawDown && downPoint) { const display = displayForPoint(downPoint.x, downPoint.y); @@ -878,7 +911,7 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: const rx1 = Math.max(downPoint.x, cursor.x); const ry1 = Math.max(downPoint.y, cursor.y); picked = { rect: { x: rx0, y: ry0, width: rx1 - rx0, height: ry1 - ry0 }, display }; - console.log("[region-pick] up", picked.rect); + console.log("[region-pick] up", picked.rect, "(renderer)"); break; } }