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 };