细节调整及优化(26.8.3)

This commit is contained in:
zhongluofeng
2026-08-21 17:25:47 +08:00
parent 0a19b4b38a
commit 4dd60f42a1
72 changed files with 4779 additions and 1395 deletions
+17 -24
View File
@@ -3,7 +3,8 @@
//! 实现:
//! - 全屏(虚拟屏)捕获:BitBlt 从屏幕 DC 拷贝到兼容位图,GetDIBits 取像素
//! - 窗口捕获:PrintWindow(PW_RENDERFULLCONTENT) 捕获 DWM 内容(覆盖硬件加速窗口)
//! - 窗口拾取:EnumWindows Z 序命中测试(排除本进程窗口,避免命中覆盖层自身)
//! - 窗口拾取:pick_windows 枚举 Z 序窗口列表(排除本进程,避免命中覆盖层自身)
//! 前端缓存列表后本地命中测试
//! - 顶层窗口枚举:EnumWindows
//! - 像素 → PNG / CF_DIB 转换
//!
@@ -276,18 +277,16 @@ pub fn capture_window(hwnd: isize) -> Result<CapturedImage, String> {
}
}
/// 获取指定屏幕坐标下的顶层窗口(窗口拾取
/// 枚举可拾取的顶层窗口(Z 序顶→底
///
/// 入参 x/y 为物理屏幕坐标(前端需按显示器 scaleFactor 从逻辑坐标换算)
///
/// 不能直接用 WindowFromPoint:覆盖层是 alwaysOnTop 全屏窗口,会命中覆盖层自身。
/// 改为 EnumWindows 按 Z 序(顶→底)枚举顶层窗口做命中测试,并排除本进程
/// (覆盖层/主窗口/编辑器)的窗口,从而取到覆盖层下面的目标窗口。
pub fn window_from_point(x: i32, y: i32) -> Option<WindowInfo> {
/// 与逐点拾取同语义:排除本进程窗口(覆盖层/主窗口/编辑器)、不可见窗口、工具窗口
/// 前端在截图开始时缓存该列表,鼠标移动时在 JS 侧做命中测试(rect 包含点,取 Z 序
/// 最顶的第一个命中),消除逐帧 window_from_point 的 IPC 往返;且列表与冻结底图
/// 同一时刻生成,命中结果与画面严格一致。
pub fn pick_windows() -> Vec<WindowInfo> {
struct PickContext {
my_pid: u32,
pt: POINT,
found: Option<WindowInfo>,
out: Vec<WindowInfo>,
}
extern "system" fn enum_proc(hwnd: HWND, lparam: isize) -> i32 {
@@ -311,30 +310,24 @@ pub fn window_from_point(x: i32, y: i32) -> Option<WindowInfo> {
if GetWindowRect(hwnd, &mut rect) == 0 {
return 1;
}
// 命中测试(物理坐标),Z 序最顶层的第一个命中即为目标
let pt = ctx.pt;
if pt.x >= rect.left && pt.x < rect.right && pt.y >= rect.top && pt.y < rect.bottom {
ctx.found = Some(WindowInfo {
hwnd,
title: get_window_title(hwnd),
rect: ScreenRect::from(rect),
visual_rect: extended_frame_bounds(hwnd),
});
return 0; // 停止枚举
}
ctx.out.push(WindowInfo {
hwnd,
title: get_window_title(hwnd),
rect: ScreenRect::from(rect),
visual_rect: extended_frame_bounds(hwnd),
});
}
1
}
let mut ctx = PickContext {
my_pid: std::process::id(),
pt: POINT { x, y },
found: None,
out: Vec::new(),
};
unsafe {
EnumWindows(Some(enum_proc), &mut ctx as *mut _ as isize);
}
ctx.found
ctx.out
}
/// 获取当前鼠标物理屏幕坐标(覆盖层打开时定位初始悬停窗口)
+49 -19
View File
@@ -1,12 +1,13 @@
//! Tauri 命令:截图模块
//!
//! 命令清单:
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态(不做 PNG 编码)
//! - screenshot_capture_fullscreen:捕获虚拟屏并存入静态(不做 PNG 编码),返回捕获时刻光标坐标
//! - screenshot_get_fullscreen_bmp:取出全屏捕获的 BMP 原始字节(raw IPC,覆盖层显示用,不移除)
//! - screenshot_fullscreen_png:全屏捕获编码 PNG base64 并清除(全屏截图进编辑器用)
//! - screenshot_clear_fullscreen:清除静态全屏捕获(覆盖层关闭时)
//! - screenshot_crop_stored:按物理像素裁剪已存储的全屏捕获
//! - screenshot_window_from_point:拾取指定屏幕坐标下的顶层窗口
//! - screenshot_pick_list:枚举可拾取顶层窗口(Z 序,前端缓存后本地命中测试)
//! - screenshot_show_overlay:一次 IPC 完成覆盖层 show + focus(关键路径减少往返)
//! - screenshot_enum_windows:枚举可见顶层窗口
//! - screenshot_capture_window:按 hwnd 捕获指定窗口
//! - screenshot_set_editor_image / screenshot_get_editor_image:编辑器图片传递
@@ -14,7 +15,7 @@
//! - screenshot_save_png:写入文件
//! - screenshot_disable_transitions:禁用窗口显示/隐藏过渡动画(消除覆盖层缩放动画)
use super::{CaptureData, WindowInfo};
use super::{CaptureData, CaptureStart, WindowInfo};
use tauri::{Emitter, Manager};
/// 禁用指定窗口(按 label 查找)的显示/隐藏过渡动画,消除覆盖层出现/消失时的缩放动画
@@ -88,16 +89,19 @@ pub async fn screenshot_unregister_pin_shortcut(app: tauri::AppHandle) -> Result
Ok(())
}
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
/// 同时返回捕获时刻的光标物理坐标(覆盖层据此做初始窗口拾取,省一次 IPC 往返)。
#[tauri::command]
#[specta::specta]
pub async fn screenshot_capture_fullscreen() -> Result<(), String> {
pub async fn screenshot_capture_fullscreen() -> Result<CaptureStart, String> {
#[cfg(windows)]
{
// 屏幕捕获涉及 GDI 调用,放线程池避免阻塞 async 调度
tauri::async_runtime::spawn_blocking(|| {
let img = super::capture::capture_virtual_screen()?;
super::capture::store_fullscreen(img)
super::capture::store_fullscreen(img)?;
let (cursor_x, cursor_y) = super::capture::cursor_pos().unwrap_or((0, 0));
Ok(CaptureStart { cursor_x, cursor_y })
})
.await
.map_err(|e| format!("捕获任务失败: {}", e))?
@@ -205,29 +209,39 @@ pub async fn screenshot_crop_copy_stored(
}
}
/// 拾取指定物理屏幕坐标下的顶层窗口
/// 枚举可拾取的顶层窗口(Z 序顶→底,排除本进程/不可见/工具窗口)。
/// 前端在截图开始时缓存列表,鼠标移动时在 JS 侧本地命中测试,消除逐帧 IPC 往返。
#[tauri::command]
#[specta::specta]
pub async fn screenshot_window_from_point(
x: i32,
y: i32,
) -> Result<Option<WindowInfo>, String> {
pub async fn screenshot_pick_list() -> Result<Vec<WindowInfo>, String> {
#[cfg(windows)]
{
tauri::async_runtime::spawn_blocking(move || {
Ok(super::capture::window_from_point(x, y))
})
.await
.map_err(|e| format!("查询失败: {}", e))?
tauri::async_runtime::spawn_blocking(super::capture::pick_windows)
.await
.map_err(|e| format!("枚举失败: {}", e))
}
#[cfg(not(windows))]
{
let _ = (x, y);
Ok(None)
Ok(vec![])
}
}
/// 获取当前鼠标物理屏幕坐标(覆盖层打开时定位初始悬停窗口
/// 一次 IPC 完成指定窗口的显示与聚焦(截图覆盖层显示关键路径,减少串行往返
#[tauri::command]
#[specta::specta]
pub async fn screenshot_show_overlay(
app: tauri::AppHandle,
label: String,
) -> Result<(), String> {
let win = app
.get_webview_window(&label)
.ok_or_else(|| format!("窗口不存在: {}", label))?;
win.show().map_err(|e| format!("显示窗口失败: {}", e))?;
win.set_focus().map_err(|e| format!("聚焦窗口失败: {}", e))?;
Ok(())
}
/// 获取当前鼠标物理屏幕坐标(贴图窗口拖动跟随等场景使用)
#[tauri::command]
#[specta::specta]
pub async fn screenshot_cursor_pos() -> Result<(i32, i32), String> {
@@ -433,6 +447,22 @@ pub async fn screenshot_load_cache(
.map_err(|e| format!("读取缓存任务失败: {}", e))?
}
/// 读取历史缓存 PNG 原始字节(raw IPC → 前端 ArrayBuffer,贴图窗口显示用:
/// 跳过 base64 编码,IPC 传输与前端内存占用均省 ~33%;同 get_fullscreen_bmp 豁免 specta
#[tauri::command]
pub async fn screenshot_load_cache_raw(
app: tauri::AppHandle,
path: String,
) -> Result<tauri::ipc::Response, String> {
tauri::async_runtime::spawn_blocking(move || {
let p = ensure_in_history_dir(&app, &path)?;
let bytes = std::fs::read(&p).map_err(|e| format!("读取缓存失败: {}", e))?;
Ok(tauri::ipc::Response::new(bytes))
})
.await
.map_err(|e| format!("读取缓存任务失败: {}", e))?
}
/// 删除历史缓存文件(历史项移除/清空时调用,静默忽略不存在文件)
#[tauri::command]
#[specta::specta]
+8
View File
@@ -20,6 +20,14 @@ pub struct CaptureData {
pub height: i32,
}
/// 截图启动信息:捕获时刻的光标物理坐标(覆盖层据此做初始窗口拾取,省一次 IPC 往返)
#[derive(serde::Serialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct CaptureStart {
pub cursor_x: i32,
pub cursor_y: i32,
}
/// 窗口信息(窗口拾取 / 枚举)
#[derive(serde::Serialize, Type)]
#[serde(rename_all = "camelCase")]