633 lines
16 KiB
Markdown
633 lines
16 KiB
Markdown
# 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 窗口识别
|
||
|
||
当前算法:
|
||
|
||
1. 不直接使用 `WindowFromPoint`,因为全屏 overlay 会遮住底层窗口。
|
||
2. 从 `GetTopWindow(None)` 开始,按 Z 序循环 `GetWindow(hwnd, GW_HWNDNEXT)`。
|
||
3. 跳过:
|
||
- 已排除窗口句柄。
|
||
- 不可见窗口。
|
||
- 最小化窗口。
|
||
- `WS_EX_TOOLWINDOW` 工具窗口。
|
||
- 宽高小于等于 0 的窗口。
|
||
4. 用 `GetWindowRect(hwnd)` 得到窗口矩形。
|
||
5. 返回第一个包含鼠标点的合格顶层窗口。
|
||
6. 标题来自 `GetWindowText(hwnd)`。
|
||
7. 进程名来自 `GetWindowThreadProcessId` + `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + 模块路径文件名。
|
||
8. 没有命中任何窗口时,返回虚拟屏幕桌面窗口:
|
||
- `hwnd = null`
|
||
- `title = "Desktop"`
|
||
- `process_name = "explorer.exe"`
|
||
- `rect = virtual_screen_rect`
|
||
- `is_desktop = true`
|
||
|
||
虚拟屏幕使用:
|
||
|
||
- `SM_XVIRTUALSCREEN`
|
||
- `SM_YVIRTUALSCREEN`
|
||
- `SM_CXVIRTUALSCREEN`
|
||
- `SM_CYVIRTUALSCREEN`
|
||
|
||
### 2.6 Overlay 状态机
|
||
|
||
状态枚举:
|
||
|
||
```rust
|
||
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`,或 Canvas `globalCompositeOperation = "destination-out"`。
|
||
|
||
### 2.8 标注交互
|
||
|
||
创建标注:
|
||
|
||
1. `Ctrl+R` 进入 `DrawingAnnotation`。
|
||
2. 鼠标左键按下记录起点。
|
||
3. 拖动时显示临时矩形。
|
||
4. 鼠标释放:
|
||
- 对矩形做 `normalized()`。
|
||
- 裁剪到锁定窗口 `clamped_to(locked_window.rect)`。
|
||
- 小于 `6×6` 像素视为误触,不创建。
|
||
- 合格则添加到 `project.annotations`。
|
||
- 进入 `EditingAnnotation`。
|
||
- 在标注框右下附近显示编辑表单。
|
||
|
||
命中测试顺序:
|
||
|
||
1. resize handle。
|
||
2. 标注框内部移动。
|
||
3. 空白区域新建。
|
||
|
||
多个标注重叠时,最后创建的标注优先命中。
|
||
|
||
移动规则:
|
||
|
||
- 移动时保持标注框宽高不变。
|
||
- 移动结果必须完整留在锁定窗口内。
|
||
- 不允许因简单裁剪导致宽高变小。
|
||
|
||
缩放规则:
|
||
|
||
- 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` 类型如下:
|
||
|
||
```rust
|
||
#[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 + width`
|
||
- `bottom() -> i32`:`y + height`
|
||
- `contains(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 保存格式
|
||
|
||
保存路径必须兼容当前版本:
|
||
|
||
```text
|
||
%USERPROFILE%\.desktop_mark\last_project.json
|
||
```
|
||
|
||
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:
|
||
|
||
1. `floating`
|
||
- 80×80。
|
||
- always-on-top。
|
||
- decorations false。
|
||
- transparent true。
|
||
- resizable false。
|
||
- skip taskbar true。
|
||
- 显示 `RPA` / `Ctrl+1`。
|
||
|
||
2. `overlay`
|
||
- 覆盖虚拟屏幕。
|
||
- always-on-top。
|
||
- decorations false。
|
||
- transparent true。
|
||
- skip taskbar true。
|
||
- 初始隐藏。
|
||
- 用于窗口选择、遮罩、标注绘制。
|
||
|
||
3. `panel`
|
||
- 宽 300。
|
||
- 高度为虚拟屏幕高度。
|
||
- 位于虚拟屏幕左侧。
|
||
- always-on-top。
|
||
- 初始隐藏。
|
||
- 显示窗口信息和标注列表。
|
||
|
||
4. `annotation_form`
|
||
- 约 320×420。
|
||
- always-on-top。
|
||
- 初始隐藏。
|
||
- 位于标注框右下附近,越界则左/上偏移。
|
||
|
||
也可以把 `panel` 和 `annotation_form` 做成 overlay 内部 DOM,但独立窗口更接近当前 PySide6 行为。
|
||
|
||
### 5.2 Rust 模块建议
|
||
|
||
```text
|
||
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 前端模块建议
|
||
|
||
```text
|
||
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
|
||
|
||
建议命令:
|
||
|
||
```rust
|
||
#[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>;
|
||
```
|
||
|
||
前端事件:
|
||
|
||
```text
|
||
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_TOOLWINDOW`
|
||
- `GetWindowRect(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 标注类型
|
||
|
||
固定值:
|
||
|
||
```text
|
||
button
|
||
input
|
||
text
|
||
image
|
||
other
|
||
```
|
||
|
||
### 8.2 动作枚举
|
||
|
||
固定值:
|
||
|
||
```text
|
||
click
|
||
double_click
|
||
right_click
|
||
input_text
|
||
hover
|
||
scroll
|
||
```
|
||
|
||
### 8.3 左侧面板字段
|
||
|
||
- 窗口标题。
|
||
- 进程名。
|
||
- bbox:`x, y, width, height`。
|
||
- 窗口名称:可编辑。
|
||
- 窗口简介:可编辑。
|
||
- 标注列表:`序号. label (type)`。
|
||
|
||
`label` 优先级:
|
||
|
||
1. `control_name`
|
||
2. `description`
|
||
3. `标注 {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. 推荐开发顺序
|
||
|
||
1. 建 Tauri 项目和四个窗口。
|
||
2. 实现 Rust `Rect`、模型、JSON 保存/加载和单元测试。
|
||
3. 实现 Win32 虚拟屏幕与窗口识别。
|
||
4. 实现全局快捷键。
|
||
5. 实现 `floating` 窗口。
|
||
6. 实现 `overlay` 状态机和窗口高亮。
|
||
7. 实现标注框创建、移动、缩放。
|
||
8. 实现 `panel` 和 `annotation_form`。
|
||
9. 串联自动保存和启动加载。
|
||
10. 按第 9 节逐项手工验收。
|
||
|
||
## 12. 当前 Python 版本验证基线
|
||
|
||
当前版本已验证:
|
||
|
||
```text
|
||
uv venv --python 3.11 --seed --clear
|
||
.\.venv\Scripts\python -m pip install -e .[dev]
|
||
.\.venv\Scripts\python -m pytest
|
||
```
|
||
|
||
结果:
|
||
|
||
```text
|
||
6 passed
|
||
```
|
||
|
||
启动烟测:
|
||
|
||
```text
|
||
.\.venv\Scripts\python -m desktop_mark
|
||
```
|
||
|
||
进程保持运行 3 秒,无 stdout/stderr 异常;测试后主动终止。
|