# Desktop Mark:Ctrl+1 后的标注逻辑技术说明 本文只总结当前项目中 **按下 `Ctrl+1` 之后** 的窗口选择与标注逻辑,不包含主页面其它功能。 下面的所有代码:E:\workspace\wechat_rap --- ## 1. 入口:全局快捷键 `Ctrl+1` 代码位置:`src-tauri/src/window_capture.rs`、`src-tauri/src/lib.rs` ### 1.1 启动时注册快捷键 应用启动时: - 在 `lib.rs` 中调用 `window_capture::spawn_shortcut_listener(app.handle().clone())` - 该函数在后台线程中通过 Win32 `RegisterHotKey` 注册: ```rust RegisterHotKey(null_mut(), 1, MOD_CONTROL as u32, b'1' as u32) ``` 含义: - `MOD_CONTROL`:Ctrl - `b'1'`:数字键 1 - `id = 1`:该快捷键内部 ID yu ### 1.2 消息循环监听 快捷键不是前端监听,而是 Rust 后台线程通过 Win32 消息循环监听: ```rust while GetMessageW(&mut message, null_mut(), 0, 0) > 0 { if message.message == WM_HOTKEY && message.wParam == 1 { ... } } ``` 当收到 `WM_HOTKEY` 且 `wParam == 1` 时,说明用户按下了 `Ctrl+1`。 --- ## 2. 快捷键触发后:进入窗口选择模式 代码位置:`src-tauri/src/window_capture.rs` 按下 `Ctrl+1` 后,Rust 会调用: ```rust enter_window_select_mode_internal(&app, &state) ``` ### 2.1 切换内部状态 该函数会更新 `OverlayStore`: ```rust store.target_window_id = None; store.mode = OverlayMode::WindowSelect; ``` 当前状态含义: - `target_window_id = None`:还没有确认选中哪个真实应用窗口 - `mode = WindowSelect`:前端 overlay 进入“窗口选择”模式,而不是“标注框绘制”模式 ### 2.2 打开 Overlay 窗口 然后调用: ```rust show_overlay_window(app) ``` 作用: - 根据虚拟屏幕范围设置 overlay 位置与尺寸 - 显示 overlay 窗口 - 重新聚焦 overlay Overlay 特征(见 `lib.rs`): - `transparent(true)`:透明 - `decorations(false)`:无边框 - `always_on_top(true)`:置顶 - `visible(false)`:默认隐藏,按需显示 - `skip_taskbar(true)`:不进任务栏 因此,`Ctrl+1` 的直接结果是: ```text 全屏透明置顶 Overlay 被显示出来,进入窗口选择模式 ``` --- ## 3. Overlay 前端:窗口选择模式如何工作 代码位置:`src/overlay.ts`、`overlay.html`、`src/overlay.css` ### 3.1 Overlay 的数据来源 前端通过轮询: ```ts invoke('get_overlay_frame') ``` 从 Rust 获取当前帧状态。 `OverlayFrame` 关键字段: - `mode`: `'annotation' | 'window_select'` - `virtual_screen_rect` - `target_window` - `hover_window` - `cursor` - `annotations` - `node_tree` 在 `Ctrl+1` 后: ```text mode = window_select ``` ### 3.2 鼠标移到哪个窗口,哪个窗口高亮 Rust 侧通过: ```rust hover_target() ``` 获取鼠标所在真实窗口。 逻辑要点: 1. 先用 `GetCursorPos` 取屏幕坐标 2. 再通过 `WindowFromPoint` + `GetAncestor(..., GA_ROOT)` 拿到鼠标下的顶层窗口 3. 再向下过滤,跳过: - Desktop Mark 自己 - 工具窗口 - 输入法 / 任务栏 / 桌面 / 阴影等系统窗口 4. 最终得到一个可选的真实桌面应用窗口 `hover_window` ### 3.3 前端高亮绘制 在 `overlay.ts` 里: - 整屏先绘制半透明遮罩 - 当前 `hover_window` 对应区域被镂空 - 再绘制高亮边框与提示标签 窗口选择模式下,标签文本类似: ```text 点击选择:窗口标题 · x=... , y=... · DPI ...% ``` 因此用户看到的体验是: ```text Ctrl+1 → 整屏暗化 → 鼠标移到哪个应用窗口,哪个窗口被高亮 ``` --- ## 4. 点击窗口:确认目标窗口 代码位置:`src/overlay.ts`、`src-tauri/src/window_capture.rs` ### 4.1 前端点击逻辑 在 `overlay.ts` 中,`pointerup` 时如果当前模式是 `window_select`: ```ts await invoke('select_target_window', { windowId: frame.hover_window.window_id }); frame = await invoke('get_overlay_frame'); ``` 也就是说: - 用户点击当前高亮窗口 - 把这个窗口的 `window_id` 发送给 Rust ### 4.2 Rust 侧确认目标窗口 对应命令: ```rust select_target_window(window_id, app, state) ``` 它会做三件事: #### a. 校验窗口存在 ```rust find_window_by_id(&window_id) ``` #### b. 把目标窗口提到前台 ```rust bring_window_to_front(window.hwnd) ``` 内部会: - `ShowWindow(hwnd, SW_RESTORE)` 恢复窗口 - `SetWindowPos(... HWND_TOPMOST ...)` 暂时提到最上层 - `SetForegroundWindow(hwnd)` 设为前台 #### c. 切换 Overlay 模式 ```rust store.target_window_id = Some(window_id); store.mode = OverlayMode::Annotation; ``` 此时进入: ```text 标注模式(Annotation) ``` #### d. 再次显示并聚焦 Overlay ```rust show_overlay_window(&app) ``` 这样可以避免用户选完窗口后还要额外点一下别处,才能开始画标注框。 --- ## 5. 标注模式:只显示当前目标窗口的标注框 代码位置:`src-tauri/src/window_capture.rs` 用户确认窗口后,后续 `get_overlay_frame()` 不会再返回所有标注,而是只返回: ```rust 当前 target_window_id 对应的 annotations ``` 实现逻辑: ```rust let visible_annotations = if store.mode == OverlayMode::Annotation { store .target_window_id .as_ref() .map(|window_id| { store .annotations .iter() .filter(|annotation| &annotation.window_id == window_id) .cloned() .collect() }) .unwrap_or_default() } else { Vec::new() }; ``` 效果: - 切到窗口 A 标注时,只显示窗口 A 的标注框 - 切到窗口 B 标注时,窗口 A 的标注框会隐藏,但数据不会丢 --- ## 6. 在目标窗口内拖拽创建标注框 代码位置:`src/overlay.ts`、`src-tauri/src/window_capture.rs` ### 6.1 前端拖拽 在 `annotation` 模式下: - `pointerdown` 记录起点 - `pointermove` 更新终点 - `pointerup` 生成屏幕坐标矩形 `RectInfo` 然后调用: ```ts invoke('add_annotation', { input: { window_id: frame.target_window.window_id, screen_rect: rect, label: null, action_kind: 'click', description: null, } }) ``` ### 6.2 Rust 保存标注 命令: ```rust add_annotation(input, state) ``` Rust 会做这些校验: 1. 目标窗口是否仍存在 2. 标注框尺寸是否太小 3. 标注框是否完整位于目标窗口内部 通过后,生成: - `annotation.id` - `label` - `action_kind` - `description` - `screen_rect` - `normalized_rect` - `feature_image_path` ### 6.3 特征图生成 保存标注时还会生成一个本地 BMP 特征图: ```rust capture_feature_image(&format!("ann-{index}"), input.screen_rect) ``` 特征图保存目录: ```text %TEMP%/desktop_mark/feature_images/ ``` --- ## 7. 标注数据如何持久化 代码位置:`src-tauri/src/window_capture.rs` 当前标注逻辑不是只存在内存里,而是会持久化到本地: ### 7.1 状态文件 ```text ./data/workflow-state.json ``` ### 7.2 持久化内容 `PersistedWorkflowState` 包含: - `app_name` - `app_description` - `annotations` - `next_annotation_index` ### 7.3 触发时机 以下操作后都会持久化: - `add_annotation` - `update_annotation` - `update_annotation_geometry` - `delete_annotation` - `clear_annotations` - `update_app_metadata` --- ## 8. Ctrl+1 后逻辑的完整时序 ```text 用户按下 Ctrl+1 → Rust Win32 热键线程收到 WM_HOTKEY → 进入 WindowSelect 模式 → 显示全屏透明 Overlay → 鼠标移动,Rust 持续计算 hover_window → 前端绘制高亮窗口 → 用户点击高亮窗口 → Rust 记录 target_window_id,并切到 Annotation 模式 → Overlay 再次聚焦 → 用户直接在目标窗口内拖拽 → 前端把矩形发给 add_annotation → Rust 校验并保存 annotation + feature image → 当前窗口的标注框持续显示,其它窗口标注框隐藏 ``` --- ## 9. 当前 Ctrl+1 方案的核心特点 ### 优点 - 全局快捷键直接进入窗口选择,不依赖前端焦点 - Overlay 与真实应用窗口分离,不污染目标应用 - 只显示当前目标窗口的标注框,切窗不混乱 - 标注数据与特征图会落本地 - DPI、虚拟屏、多窗口过滤都在 Rust/Win32 层处理 ### 当前限制 - 目标窗口关系、节点关系仍主要依赖本地状态和后续页面逻辑 - Ctrl+1 只负责“选窗 + 进入标注模式”,不负责主页面流程编排 - feature image 当前保存为 BMP,不是 PNG --- ## 10. 涉及文件清单 ### Rust 后端 - `src-tauri/src/lib.rs` - `src-tauri/src/window_capture.rs` ### Overlay 前端 - `overlay.html` - `src/overlay.ts` - `src/overlay.css` ### 类型定义 - `src/types.ts`