wechat_ai/RPA.md

8.6 KiB
Raw Permalink Blame History

Desktop MarkCtrl+1 后的标注逻辑技术说明

本文只总结当前项目中 按下 Ctrl+1 之后 的窗口选择与标注逻辑,不包含主页面其它功能。 下面的所有代码E:\workspace\wechat_rap

1. 入口:全局快捷键 Ctrl+1

代码位置:src-tauri/src/window_capture.rssrc-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_CONTROLCtrl
  • b'1':数字键 1
  • id = 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_HOTKEYwParam == 1 时,说明用户按下了 Ctrl+1


2. 快捷键触发后:进入窗口选择模式

代码位置:src-tauri/src/window_capture.rs

按下 Ctrl+1Rust 会调用:

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.tsoverlay.htmlsrc/overlay.css

3.1 Overlay 的数据来源

前端通过轮询:

invoke<OverlayFrame>('get_overlay_frame')

从 Rust 获取当前帧状态。

OverlayFrame 关键字段:

  • mode: 'annotation' | 'window_select'
  • virtual_screen_rect
  • target_window
  • hover_window
  • cursor
  • annotations
  • node_tree

Ctrl+1 后:

mode = window_select

3.2 鼠标移到哪个窗口,哪个窗口高亮

Rust 侧通过:

hover_target()

获取鼠标所在真实窗口。

逻辑要点:

  1. 先用 GetCursorPos 取屏幕坐标
  2. 再通过 WindowFromPoint + GetAncestor(..., GA_ROOT) 拿到鼠标下的顶层窗口
  3. 再向下过滤,跳过:
    • Desktop Mark 自己
    • 工具窗口
    • 输入法 / 任务栏 / 桌面 / 阴影等系统窗口
  4. 最终得到一个可选的真实桌面应用窗口 hover_window

3.3 前端高亮绘制

overlay.ts 里:

  • 整屏先绘制半透明遮罩
  • 当前 hover_window 对应区域被镂空
  • 再绘制高亮边框与提示标签

窗口选择模式下,标签文本类似:

点击选择:窗口标题 · x=... , y=... · DPI ...%

因此用户看到的体验是:

Ctrl+1 → 整屏暗化 → 鼠标移到哪个应用窗口,哪个窗口被高亮

4. 点击窗口:确认目标窗口

代码位置:src/overlay.tssrc-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.tssrc-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 会做这些校验:

  1. 目标窗口是否仍存在
  2. 标注框尺寸是否太小
  3. 标注框是否完整位于目标窗口内部

通过后,生成:

  • annotation.id
  • label
  • action_kind
  • description
  • screen_rect
  • normalized_rect
  • feature_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_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 后逻辑的完整时序

用户按下 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