From 5e6b7cd87819a2c3bb200b4c10e0897ed2ad33b9 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 3 Nov 2026 09:46:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A1=86=E9=80=89=E5=9D=90=E6=A0=87?= =?UTF-8?q?=E6=94=B9=E7=94=B1=E4=B8=BB=E8=BF=9B=E7=A8=8B=E9=87=87=E6=A0=B7?= =?UTF-8?q?=E5=85=89=E6=A0=87=E7=BB=9D=E5=AF=B9=E4=BD=8D=E7=BD=AE\n\n-=20?= =?UTF-8?q?=E6=B7=B7=E5=90=88=20DPI=20=E4=B8=8B=E7=AA=97=E5=8F=A3=20client?= =?UTF-8?q?X/clientY=20=E4=B8=8E=E5=B1=8F=E5=B9=95=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E6=98=A0=E5=B0=84=E4=B8=8D=E5=8F=AF=E9=9D=A0,=20=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E9=80=89=E5=8C=BA=E5=81=8F=E7=A6=BB=E9=BC=A0=E6=A0=87?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E4=BD=8D=E7=BD=AE\n-=20=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=B1=82=E5=8F=AA=E4=B8=8A=E6=8A=A5=E6=8C=89=E4=B8=8B/?= =?UTF-8?q?=E6=9D=BE=E5=BC=80=E7=8A=B6=E6=80=81,=20=E4=B8=BB=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E8=BD=AE=E8=AF=A2=20getCursorScreenPoint=20=E5=8F=96?= =?UTF-8?q?=E5=B1=8F=E5=B9=95=E7=BB=9D=E5=AF=B9=20DIP=20=E5=9D=90=E6=A0=87?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=89=E5=8C=BA\n-=20=E9=80=89=E5=8C=BA?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E6=80=A7=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=B1=82=E7=AA=97=E5=8F=A3=E5=87=A0=E4=BD=95?= =?UTF-8?q?,=20=E8=A3=81=E5=89=AA=E4=B8=8E=E8=BF=94=E5=9B=9E=E5=80=BC?= =?UTF-8?q?=E5=9D=87=E5=9F=BA=E4=BA=8E=E7=BB=9D=E5=AF=B9=E5=9D=90=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/desktop-pick.ts | 65 ++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/main/desktop-pick.ts b/src/main/desktop-pick.ts index 769bfb6..0637e82 100644 --- a/src/main/desktop-pick.ts +++ b/src/main/desktop-pick.ts @@ -706,10 +706,10 @@ function regionOverlayHtml(): string { "
拖拽框选区域,松开鼠标完成;Esc 取消
", "" ].join(""); } @@ -735,34 +735,47 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: }); const deadline = Date.now() + 60000; let cancelled = false; - let picked: { region: { x: number; y: number; width: number; height: number }; display: (typeof displays)[number] } | null = null; + // 选区坐标不取自窗口 clientX/clientY(混合 DPI 下窗口坐标映射不可靠), + // 渲染层只上报按下/松开状态, 主进程用 getCursorScreenPoint 采样鼠标屏幕绝对 DIP 坐标 + 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; while (Date.now() < deadline) { - await new Promise(r => setTimeout(r, 250)); - for (const { win, display } of overlays) { + await new Promise(r => setTimeout(r, 100)); + let state: { down: boolean; wasDown: boolean; cancelled: boolean } | null = null; + for (const { win } of overlays) { if (win.isDestroyed()) continue; try { - const state = await win.webContents.executeJavaScript("JSON.stringify({ done: window.__done === true, cancelled: window.__cancelled === true, region: window.__region || null })", true); - const parsed = JSON.parse(state) as { done: boolean; cancelled: boolean; region: { x: number; y: number; width: number; height: number } | null }; - if (parsed.cancelled) { cancelled = true; break; } - if (parsed.done && parsed.region && parsed.region.width > 2 && parsed.region.height > 2) { - picked = { region: parsed.region, display }; - break; - } + const raw = await win.webContents.executeJavaScript("JSON.stringify({ down: !!window.__down, wasDown: !!window.__wasDown, cancelled: !!window.__cancelled })", true); + state = JSON.parse(raw) as { down: boolean; wasDown: boolean; cancelled: boolean }; + break; } catch {} } - if (cancelled || picked) break; + if (!state) continue; + if (state.cancelled) { cancelled = true; break; } + const cursor = screen.getCursorScreenPoint(); + if (state.down && !downPoint) { downPoint = { x: cursor.x, y: cursor.y }; sawDown = true; } + if (!state.down && sawDown && downPoint) { + const display = displayForPoint(downPoint.x, downPoint.y); + const rx0 = Math.min(downPoint.x, cursor.x); + const ry0 = Math.min(downPoint.y, cursor.y); + 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 }; + break; + } } for (const w of appWindows) { if (!w.isDestroyed()) { w.show(); w.restore(); w.focus(); } } for (const { win } of overlays) if (!win.isDestroyed()) win.destroy(); if (cancelled) throw new RuntimeError("pick_cancelled", "已取消框选"); - if (!picked) throw new RuntimeError("pick_cancelled", "未完成框选"); - const { region, display } = picked; + if (!picked || picked.rect.width <= 2 || picked.rect.height <= 2) throw new RuntimeError("pick_cancelled", "未完成框选"); + const { rect, display } = picked; await new Promise(r => setTimeout(r, 300)); const outDir = path.join(app.getPath("userData"), "region-picks"); fs.mkdirSync(outDir, { recursive: true }); const imagePath = path.join(outDir, `pick-region-${Date.now()}.png`); // GDI CopyFromScreen 截不到 GPU 合成窗口(Electron 自身界面), 会截出全黑; - // 改用 desktopCapturer 走系统合成器截屏。覆盖层坐标是本屏 DIP, 裁剪时按 scaleFactor 换算物理像素 + // 改用 desktopCapturer 走系统合成器截屏。rect 是屏幕绝对 DIP, 裁剪时换算到该屏相对坐标再乘 scaleFactor const scale = display.scaleFactor || 1; const sources = await desktopCapturer.getSources({ types: ["screen"], @@ -776,17 +789,19 @@ export async function pickScreenRegion(): Promise<{ x: number; y: number; width: } const thumbnail = source.thumbnail; const actualScale = thumbnail.getSize().width / Math.max(1, display.bounds.width); - const cropX = Math.max(0, Math.min(thumbnail.getSize().width - 1, Math.round(region.x * actualScale))); - const cropY = Math.max(0, Math.min(thumbnail.getSize().height - 1, Math.round(region.y * actualScale))); - const cropW = Math.max(1, Math.min(thumbnail.getSize().width - cropX, Math.round(region.width * actualScale))); - const cropH = Math.max(1, Math.min(thumbnail.getSize().height - cropY, Math.round(region.height * actualScale))); + const relX = rect.x - display.bounds.x; + const relY = rect.y - display.bounds.y; + const cropX = Math.max(0, Math.min(thumbnail.getSize().width - 1, Math.round(relX * actualScale))); + const cropY = Math.max(0, Math.min(thumbnail.getSize().height - 1, Math.round(relY * actualScale))); + const cropW = Math.max(1, Math.min(thumbnail.getSize().width - cropX, Math.round(rect.width * actualScale))); + const cropH = Math.max(1, Math.min(thumbnail.getSize().height - cropY, Math.round(rect.height * actualScale))); fs.writeFileSync(imagePath, thumbnail.crop({ x: cropX, y: cropY, width: cropW, height: cropH }).toPNG()); // 返回屏幕绝对 DIP 坐标(含负值/副屏偏移)+该屏缩放比, 运行时由此换算点击位置 return { - x: display.bounds.x + region.x, - y: display.bounds.y + region.y, - width: region.width, - height: region.height, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, scale, imagePath };