fix: 框选坐标改由主进程采样光标绝对位置\n\n- 混合 DPI 下窗口 clientX/clientY 与屏幕位置映射不可靠, 导致选区偏离鼠标实际位置\n- 渲染层只上报按下/松开状态, 主进程轮询 getCursorScreenPoint 取屏幕绝对 DIP 坐标计算选区\n- 选区正确性不再依赖覆盖层窗口几何, 裁剪与返回值均基于绝对坐标
This commit is contained in:
parent
0641c5b91b
commit
5e6b7cd878
@ -706,10 +706,10 @@ function regionOverlayHtml(): string {
|
||||
"<div id=sel></div><div id=tip>拖拽框选区域,松开鼠标完成;Esc 取消</div>",
|
||||
"<script>",
|
||||
"var sel = document.getElementById('sel'); var start = null;",
|
||||
"document.addEventListener('mousedown', function(e) { start = [e.clientX, e.clientY]; sel.style.display = 'block'; sel.style.left = e.clientX + 'px'; sel.style.top = e.clientY + 'px'; sel.style.width = '0px'; sel.style.height = '0px'; });",
|
||||
"document.addEventListener('mousedown', function(e) { start = [e.clientX, e.clientY]; window.__down = true; window.__wasDown = true; sel.style.display = 'block'; sel.style.left = e.clientX + 'px'; sel.style.top = e.clientY + 'px'; sel.style.width = '0px'; sel.style.height = '0px'; });",
|
||||
"document.addEventListener('mousemove', function(e) { if (!start) return; sel.style.left = Math.min(start[0], e.clientX) + 'px'; sel.style.top = Math.min(start[1], e.clientY) + 'px'; sel.style.width = Math.abs(e.clientX - start[0]) + 'px'; sel.style.height = Math.abs(e.clientY - start[1]) + 'px'; });",
|
||||
"document.addEventListener('mouseup', function(e) { if (!start) return; window.__region = { x: Math.min(start[0], e.clientX), y: Math.min(start[1], e.clientY), width: Math.abs(e.clientX - start[0]), height: Math.abs(e.clientY - start[1]) }; window.__done = true; });",
|
||||
"document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { window.__cancelled = true; window.__done = true; } });",
|
||||
"document.addEventListener('mouseup', function() { if (!start) return; start = null; window.__down = false; });",
|
||||
"document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { window.__cancelled = true; } });",
|
||||
"</script></body></html>"
|
||||
].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
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user