Files
FancyInput-Qt/MIGRATION_PLAN.md
T
2026-09-03 17:08:55 +08:00

520 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FancyInput WPF → Qt 6 (QML + C++) 迁移方案
> 版本:v2.1 · 日期:2026-08-20 · 状态:待评审
> 源项目:`WPF\FancyInput`(.NET 8.0 WPF,约 132 文件 / 24,000 行)
> 目标框架:**Qt 6.8.3(MinGW 13.1.0 x64 + CMake + QML)**
> 目标:**UI 布局完全一致、逻辑相同**,代码用 QML + C++ 重写
> 构建约束:**CMake + MinGW(mingw32-make 生成器,无 Ninja)**,每阶段结束即可就地编译调试
> 许可约束:**只使用可免费商用的组件,不使用 GPL/AGPL 强传染许可的开源模块**
---
## 新会话快速上手(★ 新开会话先读这里)
> 本工程已通过 **阶段 0(Hello World)+ 构建基础设施**,全部验证通过。以下信息足以让一个新会话立刻上手,详细踩坑记录见仓库记忆 `/memories/repo/qt-build-notes.md`(新会话会自动加载)。
### 当前工程结构(已验证可构建可运行)
```
Qt\
├── CMakeLists.txt # 顶层工程(两个 QML 模块 + 部署裁剪)
├── CMakePresets.json # MinGW_64_Debug / MinGW_64_Release 预设
├── MIGRATION_PLAN.md # 本文档
├── .vscode\settings.json # qmlls import paths + cmake preset
├── src\
│ ├── main.cpp # QGuiApplication + addImportPath("qrc:/") + loadFromModule
│ └── qml\
│ ├── Main.qml # 主窗口(ApplicationWindow 600×480,阶段 0 骨架)
│ └── controls\ # ★ 用户自定义控件目录(URI: FancyInput.Controls)
│ └── Theme.qml # 全局主题单例(pragma Singleton)
├── bin\{Debug,Release}\ # exe + windeployqt 部署的 Qt 运行库(最终产物)
└── build\MinGW_64_{Debug,Release}\ # 中间产物 + QML 模块目录(qmlls 用)
```
### QML 模块架构(关键!)
| 模块 | 类型 | URI | 内容 | 说明 |
|---|---|---|---|---|
| `FancyInputControls` | **静态库**(`STATIC` 关键字写在 `qt_add_qml_module` 内) | `FancyInput.Controls` | `src/qml/controls/*.qml`(GLOB 自动收集) | 以后**新增控件只需放 .qml 文件,无需改 CMakeLists** |
| `FancyInputQt` | 可执行文件主模块 | `FancyInputQt` | `src/qml/Main.qml` | `DEPENDENCIES FancyInputControls` |
必须记住的三条铁律(踩过坑):
1. **静态 QML 模块必须链接插件 target**:`target_link_libraries(FancyInputQt PRIVATE FancyInputControls FancyInputControlsplugin ...)`,否则运行时报 "plugin FancyInputControlsplugin not found";
2. **`pragma Singleton` 不会被自动识别**:必须 `set_source_files_properties(Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)` **且放在 `qt_add_qml_module` 调用之前**;
3. **`QT_QML_OUTPUT_DIRECTORY` 必须设置**:在**所有** `qt_add_qml_module` 之前写 `set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")`——否则库模块输出到 build 根目录(Qt6QmlMacros.cmake:1430 警告)且 qmlls 找不到模块。
### 构建与验证命令(终端)
```powershell
# 终端 PATH(每次新终端都要先执行)
$env:Path = "D:/Qt/6.8.3/mingw_64/bin;D:/Qt/Tools/mingw1310_64/bin;C:\Windows\System32;C:\Windows"
Set-Location "d:\Projects\FancyInput\Qt"
# 配置(仅首次或 CMakeLists 变更时)
& "C:\Program Files\CMake\bin\cmake.exe" --preset MinGW_64_Debug
# 编译(每次改完代码;干净重建加 --clean-first)
& "C:\Program Files\CMake\bin\cmake.exe" --build --preset MinGW_64_Debug
# 运行验证(GUI 程序无控制台输出,用窗口标题确认)
# 先切干净 PATH 再运行,验证独立运行(不带 Qt bin 目录):
$env:Path = "C:\Windows\System32;C:\Windows"
Start-Process "bin\Debug\FancyInputQt.exe" -PassThru | Select-Object MainWindowTitle
# 期望标题:"FancyInput · Qt 迁移版(阶段 0)"
# QML 模块解析验证(无输出 = 成功)
& "D:\Qt\6.8.3\mingw_64\bin\qmllint.exe" -I build/MinGW_64_Debug -I D:/Qt/6.8.3/mingw_64/qml src/qml/Main.qml src/qml/controls/Theme.qml
```
### 许可状态(2026-08-20 已核对,全部 LGPL 安全 ✅)
- 部署的 Qt DLL 全部为 LGPL v3 官方模块(SPDX 核对 qtbase/qtdeclarative/qtsvg:`Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only`,开源版按 LGPL v3 使用);
- MinGW 运行时 DLL(libgcc_s_seh-1/libstdc++-6/libwinpthread-1):GPLv3 + **GCC Runtime Library Exception**(`D:\Qt\Tools\mingw1310_64\licenses\gcc\COPYING.RUNTIME` 已确认),允许闭源商用;
- **Qt6VirtualKeyboard(GPL v3)已被 CMakeLists 的 `GPL_COMPONENTS` 块强制裁剪**(windeployqt 会隐式部署,App 实际不用)——这是防御性安全网,不是已部署组件;
- bin/Release 实测 24 个 DLL(21 Qt6 + 3 MinGW 运行时)+ qml 模块仅 {QML, QtQml, QtQuick},无任何 GPL 文件。
### 下一步
**进入阶段 1(通用函数与数据结构)**,从 `src\core\common\Enums.h` 开始。详见下文第四、五节的分阶段计划。
---
## 〇、许可合规策略(重要)
**核心原则:全部组件要么是 LGPL 动态链接(可免费闭源商用),要么是宽松许可(MIT/zlib/BSD),一律避开 GPL/AGPL。**
### Qt 模块选型(Qt 6.8.3 开源版,LGPL v3)
| Qt 模块 | 许可 | 商用方式 | 本项目用途 | 采用 |
|---|---|---|---|---|
| Qt6Core / Gui / Qml / Quick | LGPL v3 | **动态链接 DLL** + 附 LGPL 声明 → 可闭源免费商用 | 基础/界面 | ✅ |
| Qt6QuickControls2 | LGPL v3 | 同上 | 控件库 | ✅ |
| Qt6Network | LGPL v3 | 同上 | 更新检查 | ✅ |
| Qt6Multimedia | LGPL v3 | 同上 | GIF/音效(可延后) | ✅ |
| Qt6Svg | LGPL v3 | 同上 | 图标渲染(可选) | ✅ |
| Qt6QuickShapes | LGPL v3 | 同上 | 矢量图标(替代 PathData) | ✅ |
| Qt6Charts | **GPL v3** | 需商业授权 | 不需要 | ❌ |
| Qt6DataVisualization | **GPL v3** | 需商业授权 | 不需要 | ❌ |
| QtGamepad | **仅商业版** | 开源版没有该模块 | 手柄(改用自写 XInput/SDL3 封装) | ❌ |
> **LGPL 合规要点(务必遵守)**:
> 1. **动态链接** Qt 共享库(默认方式),**禁止** `-static` 静态链接 Qt;
> 2. 发布时附带 `LGPLv3` 许可文本 + 使用的 Qt 版本/模块清单;
> 3. 随包分发 Qt DLL(`windeployqt` 自动拷贝),不修改 Qt 库源码;
> 4. 应用自身代码可完全闭源。
### 第三方依赖选型
| 库 | 许可 | 商用 | 用途 | 采用 |
|---|---|---|---|---|
| SDL3(C API) | **zlib** | ✅ 免费商用 | 手柄读取后端 | ✅ |
| XInput(xinput1_4.dll) | 系统组件 | ✅ | 手柄读取后端(备选) | ✅ |
| ViGEmClient | **MIT** | ✅ 免费商用 | 虚拟 Xbox 手柄(延后阶段) | ✅ |
| HidHideCLI.exe | **GPL v3** | ❌ 需授权/分发限制 | 隐藏真实手柄(WPF 中已注释) | ❌ 不迁移 |
| MouseKeyHook | BSD-2 | ✅ | WPF 用;Qt 侧改用 Raw Input,**不需要** | ❌ |
| WpfAnimatedGif | MIT | ✅ | WPF 用;Qt 侧用 QML AnimatedImage/QMovie | ❌ |
**结论**:所有用到的组件均可免费商用;HidHide 因 GPL 放弃迁移(WPF 中本就是注释代码)。
---
## 一、现状盘点
### 1.1 WPF 项目结构(迁移源)
| 层 | 文件数 | 行数 | 说明 |
|---|---|---|---|
| `Models/` | 13 | ~4,569 | InputParser / RawInputParser / InputHandler / InputGenerator / XImage / OverlayParser / Macro 引擎 |
| `ViewModels/` | 14 | ~5,435 | MainWindow / ElementTree / ElementViewModel×3 / Macro / ImageEditor 等 |
| `Views/Controls/` | 22 | ~2,050 | IconButton / AnimatedImage / 五面板 / Terminal / FormRow 等 |
| `Views/Elements/` | 22 | ~1,610 | 10 种叠层元素编辑器 + ElementTest |
| `Views/Macro/` | 12 | ~1,440 | 宏窗口 / 轨道 / 动作按钮 / 属性面板 |
| `Views/`(根+窗口) | 26 | ~3,800 | 主窗 / 叠层窗 / 元素树窗 / 图像编辑器 / 辅助窗口 |
| `Common/` | 5 | ~340 | 枚举 / PathData / 转换器 |
| 根目录 | 7 | ~1,900 | App / MainWindow |
| **XAML 样式** | 10 | ~2,888 | 全局主题(紫色 #FF651F65 + Cascadia Mono) |
### 1.2 功能模块清单
1. **输入捕获**:键盘(RawInput + 全局钩子双路径)、鼠标按键/移动/滚轮、XInput 手柄、SDL3 手柄(可切换)、Guide 键
2. **输入合成(反向链路)**:SendInput 键鼠合成、ViGEm 虚拟 Xbox360 手柄、HidHide 手柄伪装
3. **叠层系统**:透明置顶穿透窗口、多窗口管理、10 种元素(键盘/鼠标/手柄按钮/滚轮/摇杆/扳机/玩家灯/DPad/鼠标移动/贴图)、帧切换驱动
4. **元素树编辑器**:列表 + 画布 + 属性面板、拖拽/框选/对齐/移动绑定、图集生成、PNG+JSON / .fip 项目文件导出
5. **图像编辑器**:裁剪/缩放/通道分离合并/圆形擦除/四方向合并/图集打包/拼接/GIF
6. **宏系统**:三轨道(Pre/On/Post)、6 种动作(命令/键盘/鼠标按键/鼠标移动/滚轮/延迟)、Once/Hold/Toggle 触发、录制、.fipm 文件
7. **工坊**:宏管理入口 + 默认路径
8. **主窗口五面板**:读取 / 创建 / 管理 / 工坊 / 设置
9. **辅助窗口**:关于 / 更新提示 / 版本时间线 / 二维码 / 控制台 / 开源声明 / 帮助 / 自定义消息框
10. **系统级**:单实例(Mutex + 命名管道)、托盘图标、管理员自提权、在线更新检查、config.json 持久化
### 1.3 本机 Qt 环境(已确认可用)
```
D:\Qt\6.8.3\mingw_64 ← Qt 6.8.3 MinGW 套件(qmake 已确认)
D:\Qt\Tools\mingw1310_64 ← MinGW 13.1.0(g++ 13.1.0 / mingw32-make)
cmake 4.3.2(C:\Program Files\CMake)
QML 模块:QtQuick / QuickControls2 / Network / Multimedia / Svg 全部可用(LGPL)
注意:
- QtGamepad 模块开源版不存在 → 手柄后端用 SDL3(zlib)/ XInput(系统组件)C++ 封装
- 无 Ninja → 用 "MinGW Makefiles" 生成器 + mingw32-make
- QtCharts / DataVisualization 为 GPL → 不用
```
---
## 二、目标架构(Qt 6 + QML + C++)
### 2.1 目录结构(在 `Qt\` 下新建)
```
Qt\
├── CMakeLists.txt # 顶层工程(Qt6 + QML 模块)
├── MIGRATION_PLAN.md # 本文档
├── resources\
│ ├── icons\ # Icon.ico / Icon.png(拷贝自 WPF)
│ ├── qrcodes\ # bilibili.jpg / wechat.jpg / xlworkspace.jpg
│ ├── styles\ # 主题 qmldir + QML 样式组件
│ └── Update.txt # 本地更新说明
├── src\
│ ├── core\ # 纯 C++ 逻辑层(不依赖 QML)
│ │ ├── input\ # InputParser / RawInputFilter / InputHandler / InputGenerator
│ │ │ ├── enums.h/.cpp # 全部枚举(FipKeys/FipRawKeys/InputDevice/…)
│ │ │ ├── inputevent.h # InputEvent 统一事件结构(Q_GADGET)
│ │ │ ├── win32rawinputfilter.h/.cpp # QAbstractNativeEventFilter + RawInput
│ │ │ ├── inputparser.h/.cpp # 统一采集器(QTimer 心跳 + 双手柄后端)
│ │ │ ├── xinputbackend.h/.cpp # XInput LoadLibrary 封装
│ │ │ ├── sdl3backend.h/.cpp # SDL3 手柄封装(链接 SDL3 C API)
│ │ │ ├── inputhandler.h/.cpp # 事件→元素状态分发
│ │ │ ├── inputgenerator.h/.cpp # SendInput / ViGEm / HidHide
│ │ │ └── vigemmanager.h/.cpp # ViGEmClient C++ SDK 封装
│ │ ├── image\ # XImage 算法层(QImage 重写)
│ │ │ ├── ximage.h/.cpp # Crop/ReSize/EraseCircle/Merge/PackImages/ConcatImages
│ │ │ └── gifplayer.h/.cpp # QImageReader/QMovie 帧延迟封装
│ │ ├── overlay\ # 叠层数据模型 + 序列化
│ │ │ ├── overlayparser.h/.cpp # OverlayRoot/OverlayElement JSON 读写
│ │ │ └── overlaytypes.h # OverlayElement struct
│ │ ├── macro\ # 宏引擎
│ │ │ ├── macroaction.h/.cpp # 6 种动作 + DTO 序列化
│ │ │ ├── macroplayer.h/.cpp # 播放引擎(QTimer 状态机 + QProcess)
│ │ │ ├── macrotrack.h/.cpp # 三轨道 + 录制
│ │ │ └── macro.h/.cpp # MacroViewModel 等价(触发/循环/保存)
│ │ └── app\ # 应用服务
│ │ ├── appconfig.h/.cpp # config.json / .fips / 单实例 / 托盘
│ │ ├── updatechecker.h/.cpp # QNetworkAccessManager 更新检查
│ │ └── singleinstance.h/.cpp # QLocalServer/QLocalSocket 单实例 + 管道消息
│ ├── models\ # QObject 模型层(QML 可绑定)
│ │ ├── elementviewmodel.h/.cpp # ElementViewModel(Q_PROPERTY + Q_ENUM)
│ │ ├── elementtreeviewmodel.h/.cpp# 元素树模型(QAbstractListModel + DTO)
│ │ ├── overlaywindowviewmodel.h/.cpp
│ │ ├── mainwindowviewmodel.h/.cpp # 主窗 VM(配置/面板/叠层集合)
│ │ ├── macrowindowviewmodel.h/.cpp
│ │ └── imageeditorviewmodel.h/.cpp
│ └── qml\ # QML 视图层
│ ├── Main.qml # 主窗口(五面板 + 标题 + 状态栏)
│ ├── pages\ # ReadPage / CreatePage / ManagePage / WorkshopPanel / SettingsPage
│ ├── overlay\ # OverlayWindow.qml + 元素渲染组件(Repeater + Image 换帧)
│ ├── editor\ # ElementTreeWindow.qml + ElementEditor*.qml
│ ├── macro\ # MacroWindow.qml / MacroTrack.qml / MacroActionPanel.qml
│ ├── imageeditor\ # ImageEditor.qml
│ ├── dialogs\ # About / UpdatePrompt / RgbInput / AppMessageBox / …
│ └── controls\ # IconButton / AnimatedImage / FormRow / ToggleSwitch / Slider / …
```
### 2.2 架构原则
1. **分层**:`core/`(纯 C++ 逻辑,可单测)→ `models/`(QObject,暴露给 QML)→ `qml/`(纯视图,不做业务逻辑)
2. **与 WPF 的 MVVM 同构**:WPF ViewModel → Qt `QObject`(Q_PROPERTY + notify 信号);WPF XAML → QML;WPF IValueConverter → QML 绑定或 C++ 转换属性;WPF Command → QML 信号处理器调用 C++ 槽
3. **UI 一致**:主题色 #FF651F65 / #953895、Cascadia Mono 字体、圆角 8/10、阴影、滑动开关、现代滑杆、圆角 ComboBox——全部用 QML 自定义组件复刻
4. **行为一致**:所有阈值(200ms 滚轮停止、100ms 移动停止、XInput 20 / SDL 2000 / SDL 扳机 5、10 帧扳机缓存、12 样本滑动平均去极值、DPad 1~8 帧映射等)原样保留
5. **数据兼容**:`.fip` / `.fips` / `.fipm` / config.json / Overlay JSON 与 WPF 完全互通
---
## 三、逐子系统迁移方案
### 3.1 输入子系统(最高技术风险)
| WPF 组件 | Qt 6 对应 | 关键点 |
|---|---|---|
| `RawInputParser`(WM_INPUT) | `Win32RawInputFilter : QAbstractNativeEventFilter` | `qApp->installNativeEventFilter()`;`MSG.message == WM_INPUT` 分支;`RegisterRawInputDevices`(usage 0x06 键盘 / 0x02 鼠标 + RIDEV_INPUTSINK);绝对/相对坐标归一化;`_pressedKeys` 去重;修饰键/Lock 检测(GetKeyState);`WM_ENTERSIZEMOVE/EXITSIZEMOVE` 拖拽时注销/注册 |
| `Gma.System.MouseKeyHook` 全局钩子 | ① RAWMOUSE `usButtonFlags` 补齐按钮/滚轮(推荐,去掉钩子依赖)② 或 `SetWindowsHookEx(WH_MOUSE_LL)` 封装 | 二选一,推荐① |
| `InputParser`(DispatcherTimer 16.67ms) | `QTimer(Qt::PreciseTimer)` + `QElapsedTimer`×2 | 滚轮停止 >200ms / 移动停止 >100ms;停止后虚拟坐标回屏幕中心;TimerInterval 钳制 [5,1000] |
| XInput 手柄 | `LoadLibrary("xinput1_4.dll")` + `GetProcAddress`(无第三方依赖) | 4 个 pad、`IsConnected` 过滤、按钮按位边沿、摇杆阈值 20、扳机无阈值 |
| SDL3 手柄 | SDL3 C API 静态链接(仅手柄子系统,不与 Qt 冲突) | `SDL_UpdateGamepads()` 热插拔差分;阈值 2000/5;Y 轴反转;扳机 ×255/32767;按钮映射表(South→A…);Guide 独立设备 |
| `InputHandler` 分发 | C++ `InputHandler`(QObject) | 响应者字典 / 键盘双模式字典 / DPadShift / 鼠标滑动平均 / Arrow/Dot / 摇杆换算 / 扳机帧索引 / 25ms 回弹定时器——全部照搬 |
| `InputGenerator`(SendInput) | user32 直调 + 结构体照搬(注意 64 位 sizeof) | 扫描码合成(KEYEVENTF_SCANCODE);NumLock 小键盘表;绝对坐标 65535 归一化 |
| `VirtualControllerManager`(ViGEm 反射) | **ViGEmClient C++ SDK 静态链接**(MIT) | `vigem_alloc/connect/target_xbox360_alloc/add`;SetButton/Trigger/Stick + `submit_report`;Select→Back 映射 |
| `HidHideManager` | `QProcess` 调 HidHideCLI.exe(改异步防卡 UI) | 步骤照搬;JSON 解析 QJsonDocument;虚拟设备关键字 VIGEM/NEFARIUS/VIRTUAL/ROOT\ |
| `Screen` / `Cursor` / DPI | `QScreen` / `QCursor` / `devicePixelRatio` | 高 DPI 自动处理,叠层物理像素对齐时用 devicePixelRatio |
### 3.2 叠层与元素系统
| WPF | Qt 6 对应 |
|---|---|
| `OverlayWindow`(透明置顶穿透) | QML `Window { flags: Qt.FramelessWindowHint \| Qt.WindowStaysOnTopHint \| Qt.Tool; color: "transparent" }`;穿透用 `Qt.WindowTransparentForInput` 运行时切换(替代 WS_EX_TRANSPARENT P/Invoke) |
| 黄色边框/去边框模式 | `margin`/`x`/`y`/`width`/`height` 联动 + 切换时 `opacity` 过渡(UpdateWindowSize 逻辑照搬) |
| 缩放 `2^Scale` | C++ 属性 `currentWidth = initialWidth * pow(2, scale)` + QML 绑定 |
| 元素渲染(Canvas+ItemsControl) | `Repeater` + `Item { x: model.posX; y: model.posY }` + `Image { source: model.currentFrame }`——帧切换驱动换图,架构不变 |
| 多窗口 / 索引 / 置顶顺序 | C++ `MainWindowViewModel` 持有 `QList<OverlayWindowViewModel*>`;`raise()`/`lower()`;GamepadUserIndex 自动分配逻辑照搬 |
| 拖拽窗口 | `MouseArea` + C++ 算坐标(或 `QWindow::startSystemMove()`) |
| 11 种元素 | `ElementViewModel`(QObject)复用同一模型;每种元素渲染组件 + 编辑器组件 |
| 元素树编辑器 | `ElementTreeWindow.qml`:左 ListView + 中 Flickable 画布(scale 缩放、锚点缩放公式照搬)+ 右属性面板 + `SplitView` 分栏 |
| 图集生成 | `QImage` + `QPainter` 重写 `PackImages`/`ConcatImages`(含 SPACE_BETWEEN_IMAGES=3;PackImages 按高度降序逻辑照搬) |
| DTO 序列化 | `QJsonDocument` 重写 `.fip`(含 base64 内嵌 PNG/GIF 字节);Overlay JSON 字段名不变 |
### 3.3 图像编辑器
| WPF(XImage.cs 662 行) | Qt 6 对应 |
|---|---|
| BGRA32 像素数组 | `QImage::Format_ARGB32`(小端同为 BGRA 字节序,可逐字节对照) |
| `Crop`(越界透明填充) | `QImage::copy` 不透明填充 → 需 `canvas.fill(Qt::transparent)` + `QPainter::drawImage` 保持语义 |
| `ReSize` / `Scale` | `QImage::scaled(..., Qt::SmoothTransformation)` |
| `EraseCircle` | `QPainter` + `QPainterPath::addEllipse` + `CompositionMode_Clear`(含包围盒优化) |
| `ExtractChannels` / `MergeChannels` | `QImage::bits()` 逐像素循环,逻辑 1:1(注意参数顺序 r,g,b,a,h,w 保留) |
| 四方向 `Merge` | 像素级照搬(percentage 语义表:Up/Down/Left/Right 的 splitPos 公式一字不差) |
| `PackImages` | QPainter drawImage 照搬(保持 WPF 的"按高度降序单行/行打包"输出一致) |
| `ConcatImages` | QPainter drawImage + spacing |
| GIF 帧/延迟 | `QImageReader::imageCount()/jumpToNextImage()/nextImageDelay()`(已处理 1/100s→ms 与 0→100ms) |
| GIF 播放 | QML `AnimatedImage`(内部 QMovie)替代 WpfAnimatedGif |
| 编辑器 UI | `ImageEditor.qml`:Flickable + scale 锚点缩放(公式照搬)+ 滑杆面板;通道偏移 4 分支逻辑照搬(含透明保护、alpha 下限 1、蒙版) |
### 3.4 宏系统
| WPF | Qt 6 对应 |
|---|---|
| 宏模型(三轨道 + 6 动作) | C++ `Macro`/`MacroTrack`/`MacroAction`(QObject),动作枚举 Command/Keyboard/MouseButton/MouseMove/MouseWheel/Delay |
| 触发(Once/Hold/Toggle + Press/Release) | C++ 槽接 InputParser 信号;Channel 命令队列 → `QQueue<TriggerCommand>` + 互斥;校验逻辑(Hold 禁止 Release、宏包含触发动作禁止启用、总时长 <200ms 禁止)照搬 |
| 播放引擎 | C++ 状态机:`QTimer::singleShot` 链式执行各动作 + 取消令牌;Hold/Toggle 循环 10ms 间隔 |
| 命令动作 | `QProcess`(cmd /C 与 powershell -Command 参数照搬;stdout/stderr → 日志;waitUntilExit) |
| 键盘/鼠标/滚轮/延迟动作 | 调 `InputGenerator`(SendInput / 动画插值函数照搬,含 MouseMoveAnimation 等) |
| 录制 | 输入事件流 → 动作追加;5000ms 阈值拆分、滚轮合并、距离/10 步数——录制状态机照搬 |
| 取消安全语义 | 取消时补发 Release、防 Start/Stop 竞态(`_isMacroRunning` 由 Play 收尾复位) |
| .fipm 保存 | `QJsonDocument` + 反射 DTO 改为手写字段序列化(字段名与 WPF 一致保证互通) |
| 宏窗口 UI | `MacroWindow.qml`:三栏(宏列表 / 轨道+终端 / 属性面板),WrapPanel → `Flow`,模板切换 → `Loader`,录制红点/停止按钮 |
### 3.5 主窗口与 UI 主题
| WPF | Qt 6 对应 |
|---|---|
| 标题(Fancy/Input 双色 80px Cascadia Mono) | QML `Text` ×2(#FF651F65 + 默认黑) |
| 五面板切换按钮(读取/创建/管理/工坊/设置) | QML `Button` 自定义样式(选中 FancySelectedButton / 未选 FancyButton) |
| `TabControl` | QML `StackLayout` + 按钮切换(与 WPF 面板切换行为一致) |
| 状态栏(调试圆点 + 文字) | QML `Row` + `Rectangle`(绿/红)+ `Text` |
| 托盘 | `QSystemTrayIcon`(C++)+ QML 信号桥;左键显示窗口 |
| 单实例 | `QLocalServer`/`QLocalSocket`(对应 Mutex + NamedPipe);`--restart-as-admin` 参数处理照搬 |
| 管理员自提权 | `QProcess::startDetached` 重启 + `--restart-as-admin` |
| 更新检查 | `QNetworkAccessManager` + `QJsonDocument`(API 地址、版本比较、忽略版本持久化照搬) |
| config.json | `QJsonDocument` 读写 `%APPDATA%/FancyInput/config.json`(字段一致) |
### 3.6 主题样式(QML 复刻)
| WPF 样式 | QML 复刻 |
|---|---|
| FancyButton / RoundCornerButton | 自定义 `FancyButton.qml`(radius 8、purple bg、hover #FF953895、按下 #AA953895、DropShadow) |
| FancyTextBox / RoundCornerTextBox | `FancyTextBox.qml`(radius 8/4、purple border 2px、focus #FF805C8F) |
| SimpleToggleSwitch(滑动开关) | `ToggleSwitch.qml`(80×36 容器、紫色 #9C27B0 滑入动画 0.2s、按下缩放 0.9) |
| ModernSlider | `FancySlider.qml`(8px 轨道、紫色渐变进度、28px 圆形滑块 + 阴影) |
| RoundCornerComboBox | `FancyComboBox.qml`(85×25、radius 5、箭头 Path、紫色高亮项) |
| SimpleScrollViewer | QML `ScrollView` + 自定义 `ScrollBar`(8px 圆角 thumb #A8B3C2) |
| RoundedLabel / InfoTipBorder | `RoundedLabel.qml` / `InfoTip.qml` |
| IconButton(PathData) | `IconButton.qml`:`Shape`/`Path` 解析 SVG PathData 字符串(Qt 支持 path 子集),属性 PathFill/PathStroke/CornerRadius/IsFlipped 照搬 |
| AnimatedImage | QML `AnimatedImage`(GIF)+ 静态兜底逻辑 |
---
## 四、实施阶段划分(渐进式,每步可编译可调试)
> **核心原则**:按依赖顺序分 8 个阶段推进,每个阶段结束都有可运行、可验证的产物(编译通过 + 可运行 + 可见结果)。**宏系统、虚拟手柄、GIF 等锦上添花功能整体后移**,不污染前期代码。
**每阶段通用验收流程**(适用于所有阶段):
1. 编写/调整代码 → 2. `cmake --build` 就地编译 → 3. 运行程序验证 → 4. 有变化才进入下一步
**每阶段通用的构建命令**(`Qt\` 目录下,已改用 CMakePresets):
```powershell
# 终端 PATH
$env:Path = "D:/Qt/6.8.3/mingw_64/bin;D:/Qt/Tools/mingw1310_64/bin;C:\Windows\System32;C:\Windows"
# 配置(仅首次或 CMakeLists 变更时)
& "C:\Program Files\CMake\bin\cmake.exe" --preset MinGW_64_Debug
# 编译(每次改完代码)
& "C:\Program Files\CMake\bin\cmake.exe" --build --preset MinGW_64_Debug
# 运行(exe 在 bin\Debug\ 下,非 build 目录)
.\bin\Debug\FancyInputQt.exe
```
---
### 阶段 0:Hello World 工程骨架(可运行窗口)✅ 已完成
**目标**:CMake + MinGW 工程跑通,QML 窗口显示,主题色验证。**之后每一步都在此骨架上增量开发。**
- [x] `Qt\CMakeLists.txt`:`find_package(Qt6 COMPONENTS Core Gui Qml Quick QuickControls2)`;`qt_add_executable(FancyInputQt ...)` + `qt_add_qml_module`
- [x] `src\main.cpp`:`QGuiApplication` + `QQmlApplicationEngine` 加载 QML(`objectCreationFailed` 兜底退出)
- [x] `src\qml\Main.qml`:`ApplicationWindow` 600×480,标题 "FancyInput · Qt 迁移版(阶段 0)"
- [x] `src\qml\controls\Theme.qml`:全局主题单例(主色 `#651F65`、背景 `#1E1E24`、字体 `Cascadia Mono`)——**注意在 `controls\` 目录**(属 `FancyInput.Controls` 模块),不在 qml 根目录;用 `Theme.xxx` 引用
- [x] 双 QML 模块架构:`FancyInput.Controls`(静态库,GLOB 自动收集 controls/ 下所有 .qml)+ `FancyInputQt`(主模块)——以后加控件零改 CMakeLists
- [x] 构建基础设施:CMakePresets(Debug/Release)、windeployqt 部署 + 体积裁剪、qmlls/qmllint 无警告
- [ ] 资源目录 `resources\icons\`(拷贝 Icon.png/ico,阶段 4 主界面时补)
**产出**:`bin\Debug\FancyInputQt.exe` 可运行,窗口显示、主题色正确(已验证,2026-08-19 至 08-20)。
### 阶段 1:通用函数与数据结构(纯 C++,不依赖 UI)
**目标**:先把所有"不依赖其他代码"的通用模块落地并单测,为输入识别打基础。
- [x] `src\core\common\Enums.h`:**全部枚举逐字迁移**(`FipKeys`/`FipRawKeys`/`KeyBoardCodeType`/`InputDevice`/`ElementType`/`FIPGamepadButtonflags`/`MoveState`/`TriggerMode` 等,数值一致)
- [ ] `src\core\common\InputEvent.h`:`InputEvent` 结构(对应 WPF `InputArgs`),Q_GADGET 便于 QML 消费
- [ ] `src\core\image\XImage.h/.cpp`:QImage 重写(Crop/ReSize/Scale/EraseCircle/Merge/PackImages/ConcatImages/通道分离合并/GIF 帧),**与 WPF XImage.cs API 一一对应**
- [ ] `src\core\overlay\OverlayParser.h/.cpp`:overlay JSON 读写(QJsonDocument),**字段名与 WPF 完全一致**(snake_case)
- [ ] `src\core\common\Utility.h/.cpp`:剪贴板 Win32 封装(带重试)
- [ ] 单元测试:枚举值对拍、XImage 像素操作对拍(用 WPF 生成的样本图)、Overlay JSON 往返一致
**产出**:核心算法库可用,单测全绿;不涉及任何 UI。
### 阶段 2:输入识别核心(技术风险最高,优先做)
**目标**:全局键盘/鼠标/手柄捕获 → 统一 `InputEvent` 事件流;先做**识别**(Read),合成(Write)下一阶段。
- [ ] `src\core\input\Win32RawInputFilter.h/.cpp`:`QAbstractNativeEventFilter` + `RegisterRawInputDevices`(键盘 usage 0x06 / 鼠标 0x02 + RIDEV_INPUTSINK);`WM_INPUT` 解析;扫描码→`FipRawKeys` 映射表(60+ 分支照搬);`_pressedKeys` 去重;修饰键/Lock 检测
- [ ] `src\core\input\InputParser.h/.cpp`:QTimer(PreciseTimer) 16.67ms 心跳;滚轮停止 >200ms / 移动停止 >100ms 判定;`MoveState` 语义;TimerInterval 钳制 [5,1000]
- [ ] 鼠标按键/滚轮:RAWMOUSE `usButtonFlags` 补齐(**不依赖 MouseKeyHook**)
- [ ] `src\core\input\XInputBackend.h/.cpp`:`LoadLibrary("xinput1_4.dll")` + `GetProcAddress`(系统组件,无第三方依赖);4 pad 轮询、按钮边沿、摇杆阈值 20
- [ ] `src\core\input\Sdl3Backend.h/.cpp`:SDL3 C API(zlib 许可);热插拔差分、按钮映射表(South→A…)、摇杆 Y 反转、扳机 ×255/32767(**已确认:双后端一开始都做,与 WPF 一致**)
- [ ] 调试界面:`src\qml\pages\InputDebugPage.qml`——实时显示键盘/鼠标/手柄事件(替换 WPF 的 `DebubTest` 状态栏)
- [ ] 单元测试:键码映射对照、阈值行为
**产出**:程序运行时能实时看到全局输入事件流(键盘/鼠标/手柄),验证识别正确性。
### 阶段 3:输入合成(SendInput + 虚拟手柄,延后项前置部分)
**目标**:识别之外,把"模拟输出"打通(先键盘/鼠标,虚拟手柄后置)。
- [ ] `src\core\input\InputGenerator.h/.cpp`:`SendInput`(KEYBDINPUT 扫描码模式 + MOUSEINPUT + 绝对坐标 65535 归一化),结构体照搬(注意 64 位 sizeof)
- [ ] `src\core\input\InputMacro.h/.cpp`:渐变动画辅助(MouseMoveAnimation/MouseWheelAnimation/GamepadTriggerAnimation/KeyPress——先实现键鼠部分)
- [ ] ~~虚拟手柄(ViGEm,MIT)~~:**已确认不做**(与 HidHide 一起砍掉,保持代码纯净)
**产出**:能向系统发送模拟键鼠输入(用记事本验证);虚拟手柄留到后期。
### 阶段 4:主界面(五面板 + 主题控件库)
**目标**:WPF 主窗口布局完全一致的 QML 实现。
- [ ] `src\qml\controls\` 主题控件库:FancyButton / FancyTextBox / ToggleSwitch / FancySlider / FancyComboBox / ScrollViewer / IconButton(PathData→QML Path)——对应 WPF Styles/*.xaml
- [ ] `src\qml\Main.qml` 完整化:顶部 "Fancy/Input" 标题(80px Cascadia Mono 双色)+ 5 面板按钮(读取/创建/管理/工坊/设置)+ `StackLayout` 切换 + 底部状态栏(调试圆点 + 文字)
- [ ] `src\models\MainWindowViewModel.h/.cpp`:`PanelType`/`SelectedPanelIndex`/5 按钮状态/配置加载保存(`%APPDATA%/FancyInput/config.json`,QJsonDocument)
- [ ] 五个占位面板:ReadPage / CreatePage / ManagePage / WorkshopPanel / SettingsPage(先静态布局,业务后续填)
- [ ] 系统托盘 `QSystemTrayIcon`(退出菜单 + 双击恢复)
**产出**:主窗口与 WPF 布局逐像素一致,面板可切换,托盘可用。
### 阶段 5:各个分窗口(元素编辑窗 + 辅助对话框)
**目标**:10 种元素编辑窗 + 通用对话框,UI 布局与 WPF 一致。
- [ ] `src\models\ElementViewModel.h/.cpp`:10 种元素模型(QObject + Q_PROPERTY;图片 9 段;触发枚举列表;OffsetAnchor 链)
- [ ] `src\qml\elements\`:10 个元素编辑窗(KeyBoardButton / MouseButton / MouseWheel / MouseMovement / GamepadButton / GamepadDpad / GamepadTrigger / GamepadPlayerId / AnalogStick / Texture),统一布局:左侧 1-2 个贴图面板 + 右侧配置列(背景/名称/触发/检测按钮)
- [ ] `src\qml\controls\ElementViewPanel.qml`:贴图面板(加载/编辑/复制/粘贴)
- [ ] `src\qml\dialogs\`:AboutWindow / QRCodeWindow / RgbInputDialog / UpdatePromptWindow / FullScreenMask / AppMessageBox / ElementTreeHelp / OpenSourceStatement(布局照搬)
- [ ] `src\models\ImageEditorViewModel.h/.cpp` + `ImageEditor.qml`:裁切/通道偏移/蒙版(**可延后**,先放按钮占位)
**产出**:元素编辑窗可打开、可配置,对话框可用。
### 阶段 6:Overlay 叠层窗口 + 元素树编辑器(核心业务,最后攻坚)
**目标**:透明置顶覆盖窗口 + 元素树编辑器,实现"输入驱动叠层动画"的核心闭环。
- [ ] `src\qml\overlay\OverlayWindow.qml`:FramelessWindowHint + WindowStaysOnTopHint + 透明背景;元素渲染(Repeater + Image 换帧);拖拽/缩放/黄框模式
- [ ] `src\models\OverlayWindowViewModel.h/.cpp`:位置/尺寸/置顶/隐藏/去黄边逻辑(照搬 WPF)
- [ ] `src\models\ElementTreeViewModel.h/.cpp`:元素集合(QAbstractListModel)、选择矩形、锁定/隐藏、图集生成(XImage.PackImages)、导出 PNG+JSON/.fip
- [ ] `src\core\input\InputHandler.h/.cpp`:输入事件 → 元素状态分发(响应者字典/扳机帧索引/DPadShift/鼠标滑动平均——全部照搬)
- [ ] `src\qml\editor\ElementTreeWindow.qml`:三栏(列表/画布/属性)
- [ ] `src\qml\pages\ReadPage.qml` 完成:加载 PNG+JSON / .fip;`ManagePage.qml` 完成:多窗口管理列表
- [ ] `FullScreenMask`(坐标拾取)
**产出**:加载 WPF 生成的 PNG+JSON 工程,覆盖窗口显示一致,真实输入驱动元素动画。
### 阶段 7:宏系统 + 工坊(延后功能,单独成块)
**目标**:宏录制/编辑/播放,不影响前面任何代码。
- [ ] `src\core\macro\`:MacroAction 体系(6 动作 + DTO 序列化)、MacroTrack 录制状态机、Macro 触发引擎(Once/Hold/Toggle)、MacroActionPlayer(QProcess 命令动作 + SendInput 键鼠动作)
- [ ] `src\qml\macro\MacroWindow.qml`:三栏(宏列表 / 轨道+动作 / 属性面板)+ MacroTrack / MacroButton / FormRow / MacroActionPropertyPanel
- [ ] 工坊面板(WorkshopPanel):宏管理入口 + 默认路径
- [ ] `.fipm` 文件与 WPF 互通验证
### 阶段 8:锦上添花 + 收尾(GIF / 更新 / 发布)
- [ ] GIF 支持(QML AnimatedImage / QMovie,替代 WpfAnimatedGif)
- [ ] 在线更新检查(QNetworkAccessManager,API 地址照搬)
- [ ] 虚拟手柄完善(ViGEm 若未在前置做)+ 终端页(QProcess cmd/PowerShell)
- [ ] 管理员自提权、单实例、命令行参数(.fip/.fips 打开)
- [ ] `windeployqt` 发布打包 + LGPL 合规文件(LGPLv3 文本 + Qt 版本声明)——**许可已核对(2026-08-20)**:全部部署组件 LGPL 安全(见文首快速上手章节),打包时需附 LGPLv3 文本 + 保留 DLL 可替换性
- [ ] UI 逐屏对照验收 + 行为对照验收
---
## 五、风险清单与对策
| # | 风险 | 等级 | 对策 |
|---|---|---|---|
| 1 | RawInput + 全局钩子是 Win32 深度集成 | 高 | QAbstractNativeEventFilter 是官方支持路径;键盘/鼠标全部收敛到 nativeEventFilter;保留 RAWMOUSE 按钮字段可去掉全局钩子 |
| 2 | 像素级图像算法(XImage)格式语义差异 | 高 | QImage ARGB32 与 BGRA32 字节序同为 BGRA(小端);用 WPF 样本图做像素级对拍单测;Crop 透明填充必须手工补 |
| 3 | ViGEm 从反射改 C++ SDK | 中 | 官方 C++ 客户端 MIT 许可,API 一一对应;预留 QLibrary 动态加载兜底 |
| 4 | SDL3-CS → SDL3 C API | 中 | 同名枚举(SDL_GAMEPAD_BUTTON_SOUTH…),映射表照搬 |
| 5 | GIF 动画(WpfAnimatedGif → QMovie) | 中 | QML AnimatedImage 内建 QMovie;静态兜底逻辑保留 |
| 6 | QtGamepad 模块缺失 | 中 | 不用;手柄走 SDL3/XInput C++ 封装,QML 只消费结果 |
| 7 | 宏播放时序(Task 协程 → QTimer 状态机) | 中 | 顺序执行语义用 singleShot 链实现;取消令牌 + 补发 Release 照搬;防竞态锁照搬 |
| 8 | DPI/多屏(WPF 逻辑像素 vs Qt 高 DPI) | 中 | Qt 6 自动高 DPI;叠层/遮罩坐标用 devicePixelRatio 归一化 |
| 9 | 图标 PathData(SVG path 字符串) | 低 | Qt `Path` 支持 path 子集;少数不兼容的几何用 Canvas 兜底 |
| 10 | 反射式 DTO 序列化 | 低 | 改手写字段序列化,字段名/结构保持一致即可互通 |
---
## 六、工作量与里程碑估算
| 阶段 | 内容 | 估算 |
|---|---|---|
| 0 | Hello World 工程骨架 | 0.5 天 |
| 1 | 通用函数与数据结构(枚举/XImage/OverlayParser) | 2 天 |
| 2 | 输入识别核心(RawInput/InputParser/XInput) | 3 天 |
| 3 | 输入合成(SendInput 键鼠) | 1 天 |
| 4 | 主界面五面板 + 主题控件库 | 3 天 |
| 5 | 分窗口(元素编辑窗 + 对话框) | 3 天 |
| 6 | Overlay 叠层 + 元素树编辑器 | 4 天 |
| 7 | 宏系统 + 工坊 | 3 天 |
| 8 | 锦上添花 + 发布 | 2 天 |
| **合计** | | **约 21.5 人日**(单人全职约 5 周) |
> 说明:阶段 1/2/3 为"核心功能前置",阶段 4/5/6 为"UI 主界面→分窗口"推进,阶段 7/8 为延后功能,**互不污染**,每阶段独立可编译调试。
**里程碑**:
- M0(阶段 0 完成):Qt/MinGW 工程骨架跑通,QML 窗口显示 → 后续所有开发的基座
- M1(阶段 2 完成):**输入识别核心**可用,实时看到全局输入事件流 → 技术风险解除
- M2(阶段 4 完成):主窗口五面板 + 主题控件库,UI 与 WPF 一致
- M3(阶段 6 完成):叠层窗口 + 元素树编辑器闭环,.fip/PNG+JSON 与 WPF 互通
- M4(阶段 8 完成):宏/手柄/GIF/更新全功能对齐,验收通过
---
## 七、验收标准
1. **UI 一致**:主窗口/叠层/编辑器/宏窗口/辅助窗口逐屏截图对比,布局、配色(#FF651F65 主题)、字体(Cascadia Mono)、控件样式(圆角/阴影/开关/滑杆)一致
2. **逻辑一致**:
- 输入:同一套按键/手柄输入在两版下产生相同元素帧切换(对拍测试)
- 图像:同一 PNG 经 Crop/Merge/PackImages 等处理后像素输出一致(对拍测试)
- 宏:同一 .fipm 在两版下播放行为一致(含触发模式/时序/取消)
- 配置:config.json / .fip / .fips 双向读写互通
3. **性能**:输入延迟、叠层渲染帧率、宏触发响应不低于 WPF 版
---
## 八、待用户确认事项
1. **工程形式**:纯 CMake 工程(已定,用 MinGW Makefiles 生成器)
2. **构建工具**:MinGW 13.1.0(已定)
3. **手柄后端**:SDL3(zlib)+ XInput(系统组件)双后端**一开始都保留**(已定,阶段 2 实现)
4. **GIF 支持**:延后到阶段 8(已定,QML AnimatedImage 原生支持)
5. **虚拟手柄(ViGEm)**:**已确认不做**;HidHide(GPL)同样不迁移
6. **范围**:按上表全功能迁移,宏系统/图像编辑器/在线更新延后(已定)
7. **代码中已注释掉的逻辑**(HidHide、MirrorGamepad、ConsoleWindow 等)是否一并迁移?建议不迁(保持与 WPF 行为一致)
8. **第三方依赖 DLL 存放方案**:待确认是否按 `bin/<config>/thirdparty/` 子目录存放第三方库(SDL3/ViGEmClient 等),与 Qt DLL 分离,便于许可管理(尚未实施,等 SDL3 接入时确认)