16 KiB
Rust + Tauri 重构开发文档
1. 目标
用 Rust + Tauri 重构当前 Python + PySide6 版本,功能行为保持一致:
- 启动后显示 80×80 置顶悬浮窗。
Ctrl+1进入窗口识别模式。- 全屏半透明遮罩覆盖虚拟屏幕。
- 鼠标移动时识别底层应用窗口,蓝色边框高亮;窗口内部无遮罩。
- 鼠标在桌面或无合格窗口时,默认选择整个虚拟屏幕。
- 鼠标点击锁定当前窗口;锁定后鼠标移动不再切换目标。
Esc或回车退出当前选择/标注流程,不删除已保存标注。- 锁定窗口后
Ctrl+R进入矩形标注模式。 - 可创建多个标注框;标注框可选中、移动、缩放。
- 左侧显示 300px 标注信息面板。
- 标注框附近显示编辑表单,可编辑控件名、描述、类型、动作、特征图占位。
- 自动保存到用户目录 JSON 文件。
首版仍只支持 Windows 10/11。
2. 当前 Python 版本功能清单
2.1 应用启动
当前入口为 python -m desktop_mark。
启动时创建:
WindowProvider:Win32 窗口识别。FloatingButton:80×80 悬浮窗。OverlayWindow:窗口选择与标注 overlay。AnnotationPanel:左侧标注信息面板。AnnotationFormPopup:标注编辑表单。HotkeyWindow:全局快捷键桥接。
启动后只显示悬浮窗;overlay、信息面板、表单默认隐藏。
2.2 DPI 策略
当前 Python 版启动时调用:
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)- Qt 高 DPI rounding policy:
PassThrough
Tauri 版应在 Rust 入口尽早设置 Win32 DPI awareness,避免 Win32 坐标和 WebView 坐标偏移。
2.3 悬浮窗
当前行为:
- 固定尺寸:80×80。
- 置顶、无边框、工具窗口。
- 深色半透明圆角背景。
- 主文本:
RPA。 - 副文本:
Ctrl+1。 - 点击等价于触发窗口选择。
- 支持鼠标拖动移动。
- 自身窗口句柄必须加入窗口识别排除列表,避免选中工具自身。
2.4 全局快捷键
当前快捷键:
Ctrl+1:进入窗口选择模式。Ctrl+R:进入标注模式;如果没有锁定窗口,则先进入窗口选择模式。
当前实现使用 Win32 RegisterHotKey,不是键盘 hook。Tauri 版也应使用 Rust 原生层注册系统热键,避免管理员权限和杀软误报。
2.5 窗口识别
当前算法:
- 不直接使用
WindowFromPoint,因为全屏 overlay 会遮住底层窗口。 - 从
GetTopWindow(None)开始,按 Z 序循环GetWindow(hwnd, GW_HWNDNEXT)。 - 跳过:
- 已排除窗口句柄。
- 不可见窗口。
- 最小化窗口。
WS_EX_TOOLWINDOW工具窗口。- 宽高小于等于 0 的窗口。
- 用
GetWindowRect(hwnd)得到窗口矩形。 - 返回第一个包含鼠标点的合格顶层窗口。
- 标题来自
GetWindowText(hwnd)。 - 进程名来自
GetWindowThreadProcessId+OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)+ 模块路径文件名。 - 没有命中任何窗口时,返回虚拟屏幕桌面窗口:
hwnd = nulltitle = "Desktop"process_name = "explorer.exe"rect = virtual_screen_rectis_desktop = true
虚拟屏幕使用:
SM_XVIRTUALSCREENSM_YVIRTUALSCREENSM_CXVIRTUALSCREENSM_CYVIRTUALSCREEN
2.6 Overlay 状态机
状态枚举:
enum OverlayMode {
Idle,
PickingWindow,
WindowLocked,
DrawingAnnotation,
EditingAnnotation,
MovingAnnotation,
ResizingAnnotation,
}
行为:
Idle:overlay 隐藏。PickingWindow:overlay 显示,每 50ms 读取鼠标位置并识别窗口。WindowLocked:点击窗口后进入;蓝框保持锁定,不再跟随鼠标。DrawingAnnotation:锁定窗口内拖拽新建矩形。EditingAnnotation:显示表单,选中标注。MovingAnnotation:拖动已有标注框。ResizingAnnotation:拖动 8 个 resize handles。
2.7 Overlay 绘制规则
当前视觉规则:
- 全屏半透明黑色遮罩,透明度约 120/255。
- 当前目标窗口区域清除遮罩。
- 目标窗口边缘绘制 2px 蓝色边框。
- 右上角显示窗口标题和 bbox。
- 已有标注框为橙色 2px 边框。
- 标注序号显示在框左上角。
- 选中标注显示 8 个白底蓝边 resize handles。
- 新建中的临时框为蓝色 2px 边框。
Tauri 版建议:
- 用独立透明 always-on-top overlay window。
- 前端用 SVG 或 Canvas 绘制遮罩、挖空、边框、标注框。
- 遮罩挖空可用 SVG
mask/clipPath,或 CanvasglobalCompositeOperation = "destination-out"。
2.8 标注交互
创建标注:
Ctrl+R进入DrawingAnnotation。- 鼠标左键按下记录起点。
- 拖动时显示临时矩形。
- 鼠标释放:
- 对矩形做
normalized()。 - 裁剪到锁定窗口
clamped_to(locked_window.rect)。 - 小于
6×6像素视为误触,不创建。 - 合格则添加到
project.annotations。 - 进入
EditingAnnotation。 - 在标注框右下附近显示编辑表单。
- 对矩形做
命中测试顺序:
- resize handle。
- 标注框内部移动。
- 空白区域新建。
多个标注重叠时,最后创建的标注优先命中。
移动规则:
- 移动时保持标注框宽高不变。
- 移动结果必须完整留在锁定窗口内。
- 不允许因简单裁剪导致宽高变小。
缩放规则:
- 8 个 handles:
nw、n、ne、e、se、s、sw、w。 - 最小尺寸:
6×6。 - 缩放结果裁剪到锁定窗口内。
取消规则:
Esc或回车:退出当前 overlay 流程。- 清空当前 hover/locked/temp 状态。
- 隐藏 overlay、左侧面板、表单。
- 不删除内存中的项目数据和已保存数据。
3. 数据模型契约
Tauri 版 Rust 与前端必须使用同一份 JSON schema。建议 Rust serde 类型如下:
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowInfo {
pub hwnd: Option<isize>,
pub title: String,
pub process_name: String,
pub rect: Rect,
pub is_desktop: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AnnotationType {
Button,
Input,
Text,
Image,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Annotation {
pub id: String,
pub rect: Rect,
pub control_name: String,
pub description: String,
#[serde(rename = "type")]
pub annotation_type: AnnotationType,
pub actions: Vec<String>,
pub feature_preview_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnnotationProject {
pub window: WindowInfo,
pub custom_window_name: String,
pub window_description: String,
pub annotations: Vec<Annotation>,
}
3.1 Rect 方法
必须实现并测试:
right() -> i32:x + widthbottom() -> i32:y + heightcontains(x, y) -> bool:使用 normalized 后半开区间[x, right)、[y, bottom)。normalized() -> Rect:支持负向拖拽。clamped_to(bounds) -> Rect:矩形裁剪到边界内。move_clamped(dx, dy, bounds) -> Rect:保持原宽高移动,并完整限制在边界内。
3.2 坐标约定
所有坐标都使用屏幕绝对物理像素:
- 窗口 bbox:屏幕绝对物理像素。
- 标注框:屏幕绝对物理像素。
- JSON 保存:屏幕绝对物理像素。
首版不实现窗口移动后的标注自动重定位。
4. JSON 保存格式
保存路径必须兼容当前版本:
%USERPROFILE%\.desktop_mark\last_project.json
JSON 字段固定:
{
"window": {
"hwnd": 123456,
"title": "Untitled - Notepad",
"process_name": "notepad.exe",
"rect": { "x": 100, "y": 100, "width": 800, "height": 600 },
"is_desktop": false
},
"custom_window_name": "登录页",
"window_description": "用户登录窗口",
"annotations": [
{
"id": "uuid-string",
"rect": { "x": 150, "y": 160, "width": 120, "height": 32 },
"control_name": "用户名输入框",
"description": "输入登录用户名",
"type": "input",
"actions": ["input_text"],
"feature_preview_path": null
}
]
}
保存时机:
- 标注表单保存后。
- 窗口名称或窗口简介变化后。
加载规则:
- 启动时如果文件存在则加载到内存。
- 不自动显示 overlay。
- 加载失败时弹出错误,继续空项目。
- 不覆盖坏文件。
5. Tauri 推荐架构
5.1 窗口划分
建议使用 4 个 Tauri windows:
-
floating- 80×80。
- always-on-top。
- decorations false。
- transparent true。
- resizable false。
- skip taskbar true。
- 显示
RPA/Ctrl+1。
-
overlay- 覆盖虚拟屏幕。
- always-on-top。
- decorations false。
- transparent true。
- skip taskbar true。
- 初始隐藏。
- 用于窗口选择、遮罩、标注绘制。
-
panel- 宽 300。
- 高度为虚拟屏幕高度。
- 位于虚拟屏幕左侧。
- always-on-top。
- 初始隐藏。
- 显示窗口信息和标注列表。
-
annotation_form- 约 320×420。
- always-on-top。
- 初始隐藏。
- 位于标注框右下附近,越界则左/上偏移。
也可以把 panel 和 annotation_form 做成 overlay 内部 DOM,但独立窗口更接近当前 PySide6 行为。
5.2 Rust 模块建议
src-tauri/src/
main.rs
models.rs
rect.rs
win32_window.rs
hotkeys.rs
storage.rs
window_manager.rs
commands.rs
职责:
models.rs:WindowInfo、Annotation、AnnotationProject、AnnotationType。rect.rs:Rect几何方法和单元测试。win32_window.rs:窗口枚举、进程名、虚拟屏幕。hotkeys.rs:RegisterHotKey或 Tauri global shortcut 适配。storage.rs:JSON 保存/加载。window_manager.rs:创建/定位/显示隐藏 Tauri windows。commands.rs:暴露给前端的 Tauri commands。
5.3 前端模块建议
src/
main.tsx
state/projectStore.ts
types.ts
windows/Floating.tsx
windows/Overlay.tsx
windows/Panel.tsx
windows/AnnotationForm.tsx
geometry/rect.ts
职责:
types.ts:与 Rust serde 对齐的类型。projectStore.ts:当前项目、锁定窗口、hover 窗口、选中标注、overlay mode。Overlay.tsx:遮罩、窗口高亮、标注框、鼠标交互。Panel.tsx:窗口信息、窗口名称/简介、标注列表。AnnotationForm.tsx:控件名、描述、类型、动作、预览占位。rect.ts:前端几何算法,必须与 Rust 测试一致。
6. Rust Native API / Tauri Commands
建议命令:
#[tauri::command]
fn get_virtual_screen_rect() -> Rect;
#[tauri::command]
fn window_at(x: i32, y: i32, excluded_hwnds: Vec<isize>) -> WindowInfo;
#[tauri::command]
fn foreground_window(excluded_hwnds: Vec<isize>) -> Option<WindowInfo>;
#[tauri::command]
fn save_project(project: AnnotationProject) -> Result<(), String>;
#[tauri::command]
fn load_project() -> Result<Option<AnnotationProject>, String>;
#[tauri::command]
fn set_tool_window_exclusions(labels: Vec<String>) -> Result<Vec<isize>, String>;
前端事件:
hotkey:start-window-picking
hotkey:start-annotation
project:changed
annotation:edit-requested
annotation:selected
flow:cancelled
7. Win32 实现要点
Rust 可使用 windows crate。
必须调用或等价实现:
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)GetTopWindow(HWND(0))GetWindow(hwnd, GW_HWNDNEXT)IsWindowVisible(hwnd)IsIconic(hwnd)GetWindowLongPtrW(hwnd, GWL_EXSTYLE),跳过WS_EX_TOOLWINDOWGetWindowRect(hwnd)GetWindowTextW(hwnd)GetWindowThreadProcessId(hwnd, &mut pid)OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)QueryFullProcessImageNameW或等价 API 获取进程路径GetSystemMetrics(SM_XVIRTUALSCREEN)等虚拟屏幕指标
不要把 overlay window 作为候选窗口。所有 Tauri 工具窗口 hwnd 必须加入排除集合。
8. UI 字段与枚举
8.1 标注类型
固定值:
button
input
text
image
other
8.2 动作枚举
固定值:
click
double_click
right_click
input_text
hover
scroll
8.3 左侧面板字段
- 窗口标题。
- 进程名。
- bbox:
x, y, width, height。 - 窗口名称:可编辑。
- 窗口简介:可编辑。
- 标注列表:
序号. label (type)。
label 优先级:
control_namedescription标注 {index}
8.4 标注表单字段
- 控件名:文本输入。
- 描述:多行文本。
- 类型:下拉选择。
- 动作:多选列表。
- 特征图:占位,显示
预览待生成。 - 保存按钮。
- 取消按钮。
保存行为:
- 更新当前 annotation。
feature_preview_path保持null。- 刷新左侧列表。
- 自动保存 JSON。
- 隐藏表单。
取消行为:
- 只关闭表单。
- 不删除 annotation。
9. 关键行为验收
9.1 启动
命令启动后:
- 桌面出现 80×80
RPA悬浮窗。 - overlay、panel、form 不显示。
- 无控制台异常。
9.2 窗口选择
准备:打开记事本和浏览器。
操作:按 Ctrl+1。
预期:
- 全屏半透明遮罩出现。
- 鼠标移动到记事本/浏览器时,对应窗口显示蓝色边框。
- 被高亮窗口内部无遮罩。
- 鼠标移动到桌面空白时,蓝框覆盖虚拟屏幕。
9.3 锁定与取消
操作:在浏览器窗口上点击。
预期:
- 蓝框锁定浏览器。
- 鼠标移动到其他窗口不切换蓝框。
- 按
Esc或回车后 overlay 隐藏。 - 悬浮窗仍存在。
9.4 标注
操作:重新 Ctrl+1 锁定记事本,按 Ctrl+R,拖拽两个矩形。
预期:
- 两个橙色标注框出现。
- 标注框不能拖出记事本窗口。
- 选中框显示 8 个调整手柄。
- 左侧出现 300px 信息面板并列出两个标注。
9.5 表单与保存
操作:完成标注后,在表单输入:
- 控件名:
用户名输入框 - 描述:
输入登录用户名 - 类型:
input - 动作:
input_text
点击保存。
预期:
- 左侧列表同步显示该标注。
%USERPROFILE%\.desktop_mark\last_project.json存在。- JSON 包含对应
control_name、description、type、actions。
9.6 边界
操作:在锁定窗口边缘向窗口外拖拽创建标注。
预期:
- 最终矩形被裁剪到锁定窗口内。
- 小于
6×6的点击/拖拽不会新增标注。
10. 与当前 Python 版本保持一致的边界决定
- 只支持 Windows 10/11。
- 坐标保存为屏幕绝对物理像素。
- 不实现窗口移动后的标注自动重定位。
- 不实现特征图真实截图,只显示占位。
- 快捷键冲突时显示错误,不自动改用其他快捷键。
- 工具自身窗口必须被窗口识别排除。
- 没有命中应用窗口时选择整个虚拟屏幕。
11. 推荐开发顺序
- 建 Tauri 项目和四个窗口。
- 实现 Rust
Rect、模型、JSON 保存/加载和单元测试。 - 实现 Win32 虚拟屏幕与窗口识别。
- 实现全局快捷键。
- 实现
floating窗口。 - 实现
overlay状态机和窗口高亮。 - 实现标注框创建、移动、缩放。
- 实现
panel和annotation_form。 - 串联自动保存和启动加载。
- 按第 9 节逐项手工验收。
12. 当前 Python 版本验证基线
当前版本已验证:
uv venv --python 3.11 --seed --clear
.\.venv\Scripts\python -m pip install -e .[dev]
.\.venv\Scripts\python -m pytest
结果:
6 passed
启动烟测:
.\.venv\Scripts\python -m desktop_mark
进程保持运行 3 秒,无 stdout/stderr 异常;测试后主动终止。