8.6 KiB
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注册:
RegisterHotKey(null_mut(), 1, MOD_CONTROL as u32, b'1' as u32)
含义:
MOD_CONTROL:Ctrlb'1':数字键 1id = 1:该快捷键内部 ID yu
1.2 消息循环监听
快捷键不是前端监听,而是 Rust 后台线程通过 Win32 消息循环监听:
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 会调用:
enter_window_select_mode_internal(&app, &state)
2.1 切换内部状态
该函数会更新 OverlayStore:
store.target_window_id = None;
store.mode = OverlayMode::WindowSelect;
当前状态含义:
target_window_id = None:还没有确认选中哪个真实应用窗口mode = WindowSelect:前端 overlay 进入“窗口选择”模式,而不是“标注框绘制”模式
2.2 打开 Overlay 窗口
然后调用:
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 的直接结果是:
全屏透明置顶 Overlay 被显示出来,进入窗口选择模式
3. Overlay 前端:窗口选择模式如何工作
代码位置:src/overlay.ts、overlay.html、src/overlay.css
3.1 Overlay 的数据来源
前端通过轮询:
invoke<OverlayFrame>('get_overlay_frame')
从 Rust 获取当前帧状态。
OverlayFrame 关键字段:
mode:'annotation' | 'window_select'virtual_screen_recttarget_windowhover_windowcursorannotationsnode_tree
在 Ctrl+1 后:
mode = window_select
3.2 鼠标移到哪个窗口,哪个窗口高亮
Rust 侧通过:
hover_target()
获取鼠标所在真实窗口。
逻辑要点:
- 先用
GetCursorPos取屏幕坐标 - 再通过
WindowFromPoint+GetAncestor(..., GA_ROOT)拿到鼠标下的顶层窗口 - 再向下过滤,跳过:
- Desktop Mark 自己
- 工具窗口
- 输入法 / 任务栏 / 桌面 / 阴影等系统窗口
- 最终得到一个可选的真实桌面应用窗口
hover_window
3.3 前端高亮绘制
在 overlay.ts 里:
- 整屏先绘制半透明遮罩
- 当前
hover_window对应区域被镂空 - 再绘制高亮边框与提示标签
窗口选择模式下,标签文本类似:
点击选择:窗口标题 · x=... , y=... · DPI ...%
因此用户看到的体验是:
Ctrl+1 → 整屏暗化 → 鼠标移到哪个应用窗口,哪个窗口被高亮
4. 点击窗口:确认目标窗口
代码位置:src/overlay.ts、src-tauri/src/window_capture.rs
4.1 前端点击逻辑
在 overlay.ts 中,pointerup 时如果当前模式是 window_select:
await invoke('select_target_window', { windowId: frame.hover_window.window_id });
frame = await invoke<OverlayFrame>('get_overlay_frame');
也就是说:
- 用户点击当前高亮窗口
- 把这个窗口的
window_id发送给 Rust
4.2 Rust 侧确认目标窗口
对应命令:
select_target_window(window_id, app, state)
它会做三件事:
a. 校验窗口存在
find_window_by_id(&window_id)
b. 把目标窗口提到前台
bring_window_to_front(window.hwnd)
内部会:
ShowWindow(hwnd, SW_RESTORE)恢复窗口SetWindowPos(... HWND_TOPMOST ...)暂时提到最上层SetForegroundWindow(hwnd)设为前台
c. 切换 Overlay 模式
store.target_window_id = Some(window_id);
store.mode = OverlayMode::Annotation;
此时进入:
标注模式(Annotation)
d. 再次显示并聚焦 Overlay
show_overlay_window(&app)
这样可以避免用户选完窗口后还要额外点一下别处,才能开始画标注框。
5. 标注模式:只显示当前目标窗口的标注框
代码位置:src-tauri/src/window_capture.rs
用户确认窗口后,后续 get_overlay_frame() 不会再返回所有标注,而是只返回:
当前 target_window_id 对应的 annotations
实现逻辑:
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
然后调用:
invoke('add_annotation', {
input: {
window_id: frame.target_window.window_id,
screen_rect: rect,
label: null,
action_kind: 'click',
description: null,
}
})
6.2 Rust 保存标注
命令:
add_annotation(input, state)
Rust 会做这些校验:
- 目标窗口是否仍存在
- 标注框尺寸是否太小
- 标注框是否完整位于目标窗口内部
通过后,生成:
annotation.idlabelaction_kinddescriptionscreen_rectnormalized_rectfeature_image_path
6.3 特征图生成
保存标注时还会生成一个本地 BMP 特征图:
capture_feature_image(&format!("ann-{index}"), input.screen_rect)
特征图保存目录:
%TEMP%/desktop_mark/feature_images/
7. 标注数据如何持久化
代码位置:src-tauri/src/window_capture.rs
当前标注逻辑不是只存在内存里,而是会持久化到本地:
7.1 状态文件
./data/workflow-state.json
7.2 持久化内容
PersistedWorkflowState 包含:
app_nameapp_descriptionannotationsnext_annotation_index
7.3 触发时机
以下操作后都会持久化:
add_annotationupdate_annotationupdate_annotation_geometrydelete_annotationclear_annotationsupdate_app_metadata
8. Ctrl+1 后逻辑的完整时序
用户按下 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.rssrc-tauri/src/window_capture.rs
Overlay 前端
overlay.htmlsrc/overlay.tssrc/overlay.css
类型定义
src/types.ts