Compare commits

..
2 Commits
Author SHA1 Message Date
RL-Xiang 22ca924e3e 基本输入检测 2026-09-03 17:09:23 +08:00
RL-Xiang 856baa697e 基本输入检测 2026-09-03 17:08:55 +08:00
9 changed files with 534 additions and 566 deletions
+3
View File
@@ -93,3 +93,6 @@ Thumbs.db
*.ppt
*.pptx
*.pdf
# ===== 其他 =====
doc/
+2
View File
@@ -29,6 +29,8 @@ set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
# ============ 主程序 ============
qt_add_executable(FancyInputQt
src/main.cpp
src/core/input/RawInputDetector.cpp
src/core/input/RawInputDetector.h
)
# ============ QML 模块 1:FancyInput.Controls(用户自定义控件,≈ WPF UserControl) ============
-519
View File
@@ -1,519 +0,0 @@
# 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)
**目标**:先把所有"不依赖其他代码"的通用模块落地并单测,为输入识别打基础。
- [ ] `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 接入时确认)
+121
View File
@@ -0,0 +1,121 @@
#pragma once
#include <cstdint>
namespace FancyInput {
enum class GamepadBackend : std::uint8_t { SDL = 0, XInput = 1 };
enum class MousMoveMode : std::uint8_t { Absolute = 0, Relative = 1 };
enum class MouseWheelDirection : std::uint8_t { Up = 0, Down = 1 };
enum class MacroAddDirection : std::uint8_t { Right = 0, Left = 1 };
enum class InputDevice : std::uint8_t {
Keyboard = 0, MouseButton, MouseWheel, MouseMove,
XInputButton, XInputTrigger, XInputStick,
SDLButton, SDLTrigger, SDLStick, SDLGuide
};
enum class MoveState : std::uint8_t { StartMove = 0, Moving = 1, StopMove = 2 };
enum class FIPGamepadButtonflags : std::uint16_t {
None = 0, Up = 1, Down = 2, Left = 4, Right = 8,
Start = 0x10, Select = 0x20, LS = 0x40, RS = 0x80,
LB = 0x100, RB = 0x200, A = 0x1000, B = 0x2000,
X = 0x4000, Y = 0x8000
};
enum class ElementType : std::uint8_t {
Texture = 0, KeyboardButton, GamepadButton, MouseButton, MouseWheel,
AnalogStick, GamepadTrigger, GamepadPlayerId, DPad, MouseMovement
};
enum class GamepadCodeType : std::uint16_t {
A = 0, B = 1, X = 2, Y = 3, Select = 4, Start = 6,
LB = 9, RB = 10, Up = 11, Down = 12, Left = 13, Right = 14,
HitBox_B = 60416, HitBox_A = 60417, HitBox_Y = 60418, HitBox_X = 60419,
HitBox_LB = 60420, HitBox_RB = 60421, HitBox_Select = 60422,
HitBox_Start = 60423, HitBox_LS = 60425, HitBox_RS = 60426,
HitBox_Left = 60427, HitBox_Right = 60428, HitBox_Up = 60429,
HitBox_Down = 60430
};
enum class GamepadTriggerType : std::uint8_t { Left = 0, Right = 1 };
enum class GamepadThumbType : std::uint8_t { LeftThumb = 0, RightThumb = 1 };
enum class GamepadUserIdx : std::uint8_t { One = 0, Two = 1, Three = 2, Four = 3 };
enum class MouseCodeType : std::uint8_t { None = 0, Left = 1, Right = 2, Middle = 3, XButton1 = 4, XButton2 = 5 };
enum class Direction : std::uint8_t { Up = 1, Down = 2, Left = 3, Right = 4 };
enum class Side : std::uint8_t { Left = 0, Right = 1 };
enum class ExportType : std::uint8_t { PngAndJson = 0, ProjectFile = 1 };
enum class MainWindowPanelType : std::uint8_t { Read = 0, Create = 1, Manage = 2, Workshop = 3, Settings = 4 };
enum class MouseMoveType : std::uint8_t { Dot = 0, Arrow = 1 };
enum class WindowCloseType : std::uint8_t { Save = 0, Discard = 1 };
enum class ButtonState : std::uint8_t { Released = 0, Pressed = 1 };
enum class KeyBoardMappingType : std::uint8_t { Windows = 0, BIOS = 1 };
enum class KeyBoardCodeType : std::uint16_t {
None = 0, Escape = 1, D1 = 2, D2 = 3, D3 = 4, D4 = 5, D5 = 6, D6 = 7, D7 = 8, D8 = 9, D9 = 10, D0 = 11,
OemMinus = 12, Oemplus = 13, Back = 14, Tab = 15, Q = 16, W = 17, E = 18, R = 19, T = 20, Y = 21, U = 22, I = 23, O = 24, P = 25,
OemOpenBrackets = 26, OemCloseBrackets = 27, Enter = 28, LControlKey = 29, A = 30, S = 31, D = 32, F = 33, G = 34, H = 35,
J = 36, K = 37, L = 38, Oem1 = 39, Oem7 = 40, Oem3 = 41, LShiftKey = 42, Oem5 = 43, Z = 44, X = 45, C = 46, V = 47, B = 48,
N = 49, M = 50, Oemcomma = 51, OemPeriod = 52, Oem2 = 53, RShiftKey = 54, Divide = 3637, Multiply = 55, LMenu = 56, Space = 57, CapsLock = 58,
F1 = 59, F2 = 60, F3 = 61, F4 = 62, F5 = 63, F6 = 64, F7 = 65, F8 = 66, F9 = 67, F10 = 68, NumLock = 69, ScrollLock = 70,
NumPad7 = 71, NumPad8 = 72, NumPad9 = 73, Subtract = 74, NumPad4 = 75, NumPad5 = 76, NumPad6 = 77, Add = 78, NumPad1 = 79, NumPad2 = 80,
NumPad3 = 81, NumPad0 = 82, Decimal = 83, NumpadEquals = 3597, F11 = 87, F12 = 88, F13 = 91, F14 = 92, F15 = 93,
F16 = 99, F17 = 100, F18 = 101, F19 = 102, F20 = 103, F21 = 104, F22 = 105, F23 = 106, F24 = 107,
PrintScreen = 3639, Oem102 = 3654, RMenu = 3640, NumpadEnter = 3612, RControlKey = 3613,
Pause = 3653, Home = 3655, PageUp = 3657, End = 3663, PageDown = 3665, Insert = 3666, Delete = 3667,
Up = 57416, Left = 57419, Right = 57421, Down = 57424, LWin = 3675, RWin = 3676, Apps = 3677,
Power = 57438, Sleep = 57439, Wake = 57443, MediaPlay = 57378, MediaStop = 57380,
MediaPrevious = 57360, MediaNext = 57369, MediaSelect = 57453, MediaEject = 57388,
VolumeMute = 57376, VolumeUp = 57392, VolumeDown = 57390, AppMail = 57452,
AppCalculator = 57377, AppMusic = 57404, AppPictures = 57444, BrowserSearch = 57445,
BrowserHome = 57394, BrowserBack = 57450, BrowserForward = 57449, BrowserStop = 57448,
BrowserRefresh = 57447, BrowserFavorites = 57446, Katakana = 112, Underscore = 115,
Furigana = 119, Kanji = 121, Hiragana = 123, Yen = 125, NumpadComma = 126,
SunHelp = 65397, SunStop = 65400, SunProps = 65398, SunFront = 65399, SunOpen = 65396,
SunFind = 65406, SunAgain = 65401, SunUndo = 65402, SunCopy = 65404, SunInsert = 65405, SunCut = 65403
};
enum class TriggerMode : std::uint8_t { Once = 0, Hold = 1, Toggle = 2 };
enum class TriggerTiming : std::uint8_t { Press = 0, Release = 1 };
enum class FipKeys : std::uint16_t {
None = 0x00, Escape = 0x1B, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34, D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39, D0 = 0x30,
OemMinus = 0xBD, Oemplus = 0xBB, Back = 0x08, Tab = 0x09, Q = 0x51, W = 0x57, E = 0x45, R = 0x52, T = 0x54, Y = 0x59, U = 0x55, I = 0x49, O = 0x4F, P = 0x50,
OemOpenBrackets = 0xDB, OemCloseBrackets = 0xDD, Enter = 0x1000, LControlKey = 0xA2, A = 0x41, S = 0x53, D = 0x44, F = 0x46, G = 0x47, H = 0x48,
J = 0x4A, K = 0x4B, L = 0x4C, Oem1 = 0xBA, Oem7 = 0xDE, Oem3 = 0xC0, Oem102 = 0xE2, LShiftKey = 0xA0, Oem5 = 0xDC, Z = 0x5A, X = 0x58, C = 0x43, V = 0x56, B = 0x42,
N = 0x4E, M = 0x4D, Oemcomma = 0xBC, OemPeriod = 0xBE, Oem2 = 0xBF, RShiftKey = 0xA1, Divide = 0x6F, Multiply = 0x6A, LMenu = 0xA4, Space = 0x20, CapsLock = 0x14,
F1 = 0x70, F2 = 0x71, F3 = 0x72, F4 = 0x73, F5 = 0x74, F6 = 0x75, F7 = 0x76, F8 = 0x77, F9 = 0x78, F10 = 0x79, NumLock = 0x90, ScrollLock = 0x91,
NumPad7 = 0x67, NumPad8 = 0x68, NumPad9 = 0x69, Subtract = 0x6D, NumPad4 = 0x64, NumPad5 = 0x65, NumPad6 = 0x66, Add = 0x6B, NumPad1 = 0x61, NumPad2 = 0x62,
NumPad3 = 0x63, NumPad0 = 0x60, Decimal = 0x6E, NumpadEquals = 0x92, F11 = 0x7A, F12 = 0x7B, F13 = 0x7C, F14 = 0x7D, F15 = 0x7E,
F16 = 0x7F, F17 = 0x80, F18 = 0x81, F19 = 0x82, F20 = 0x83, F21 = 0x84, F22 = 0x85, F23 = 0x86, F24 = 0x87,
PrintScreen = 0x2C, RMenu = 0xA5, NumpadEnter = 0x1001, RControlKey = 0xA3, Pause = 0x13,
Home = 0x24, PageUp = 0x21, End = 0x23, PageDown = 0x22, Insert = 0x2D, Delete = 0x2E, Up = 0x26, Left = 0x25, Right = 0x27, Down = 0x28,
LWin = 0x5B, RWin = 0x5C, Apps = 0x5D, Sleep = 0x5F, Wake = 0xE3, MediaNext = 0xB0, MediaPrevious = 0xB1,
MediaStop = 0xB2, MediaPlay = 0xB3, MediaSelect = 0xB5, VolumeMute = 0xAD, VolumeUp = 0xAF, VolumeDown = 0xAE,
AppMail = 0xB4, AppMusic = 0xB6, AppPictures = 0xB7, AppCalculator = 0xB7, BrowserBack = 0xA6, BrowserForward = 0xA7,
BrowserRefresh = 0xA8, BrowserStop = 0xA9, BrowserSearch = 0xAA, BrowserFavorites = 0xAB, BrowserHome = 0xAC
};
enum class FipRawKeys : std::uint32_t {
None = 0x0000, Escape = 0x01, D1 = 0x02, D2 = 0x03, D3 = 0x04, D4 = 0x05, D5 = 0x06, D6 = 0x07, D7 = 0x08, D8 = 0x09, D9 = 0x0A, D0 = 0x0B,
OemMinus = 0x0C, Oemplus = 0x0D, Back = 0x0E, Tab = 0x0F, Q = 0x10, W = 0x11, E = 0x12, R = 0x13, T = 0x14, Y = 0x15, U = 0x16, I = 0x17, O = 0x18, P = 0x19,
OemOpenBrackets = 0x1A, OemCloseBrackets = 0x1B, Enter = 0x1C, LControlKey = 0x1D, A = 0x1E, S = 0x1F, D = 0x20, F = 0x21, G = 0x22, H = 0x23,
J = 0x24, K = 0x25, L = 0x26, Oem1 = 0x27, Oem7 = 0x28, Oem3 = 0x29, Oem102 = 0x56, LShiftKey = 0x2A, Oem5 = 0x2B, Z = 0x2C, X = 0x2D, C = 0x2E, V = 0x2F, B = 0x30,
N = 0x31, M = 0x32, Oemcomma = 0x33, OemPeriod = 0x34, Oem2 = 0x35, RShiftKey = 0x36, Divide = 0xE035, Multiply = 0x37, LMenu = 0x38, Space = 0x39, CapsLock = 0x3A,
F1 = 0x3B, F2 = 0x3C, F3 = 0x3D, F4 = 0x3E, F5 = 0x3F, F6 = 0x40, F7 = 0x41, F8 = 0x42, F9 = 0x43, F10 = 0x44, NumLock = 0x45, ScrollLock = 0x46,
NumPad7 = 0x47, NumPad8 = 0x48, NumPad9 = 0x49, Subtract = 0x4A, NumPad4 = 0x4B, NumPad5 = 0x4C, NumPad6 = 0x4D, Add = 0x4E, NumPad1 = 0x4F, NumPad2 = 0x50,
NumPad3 = 0x51, NumPad0 = 0x52, Decimal = 0x53, NumpadEquals = 0xE00D, F11 = 0x57, F12 = 0x58, F13 = 0x5B, F14 = 0x5C, F15 = 0x5D,
F16 = 0x63, F17 = 0x64, F18 = 0x65, F19 = 0x66, F20 = 0x67, F21 = 0x68, F22 = 0x69, F23 = 0x6A, F24 = 0x6B,
PrintScreen = 0xE037, RMenu = 0xE038, NumpadEnter = 0xE01C, RControlKey = 0xE01D, Pause = 0xE11D45,
Home = 0xE047, PageUp = 0xE049, End = 0xE04F, PageDown = 0xE051, Insert = 0xE052, Delete = 0xE053, Up = 0xE048, Left = 0xE04B, Right = 0xE04D, Down = 0xE050,
LWin = 0xE05B, RWin = 0xE05C, Apps = 0xE05D, Power = 0xE05E, Sleep = 0xE05F, Wake = 0xE063,
MediaPlay = 0xE022, MediaStop = 0xE024, MediaPrevious = 0xE010, MediaNext = 0xE019, MediaSelect = 0xE06D, MediaEject = 0xE02C,
VolumeMute = 0xE020, VolumeUp = 0xE030, VolumeDown = 0xE02E, AppMail = 0xE06C, AppCalculator = 0xE021, AppMusic = 0xE03C, AppPictures = 0xE064,
BrowserSearch = 0xE065, BrowserHome = 0xE032, BrowserBack = 0xE06A, BrowserForward = 0xE069, BrowserStop = 0xE068, BrowserRefresh = 0xE067, BrowserFavorites = 0xE066,
Katakana = 0x70, Underscore = 0x73, Furigana = 0x77, Kanji = 0x79, Hiragana = 0x7B, Yen = 0x7D, NumpadComma = 0x7E,
SunHelp = 0xFF75, SunStop = 0xFF78, SunProps = 0xFF76, SunFront = 0xFF77, SunOpen = 0xFF74, SunFind = 0xFF7E,
SunAgain = 0xFF79, SunUndo = 0xFF7A, SunCopy = 0xFF7C, SunInsert = 0xFF7D, SunCut = 0xFF7B
};
} // namespace FancyInput
+242
View File
@@ -0,0 +1,242 @@
#include "RawInputDetector.h"
#include <QCoreApplication>
#include <QDebug>
#include <QGuiApplication>
#ifdef _WIN32
#include <windows.h>
#include <hidusage.h>
namespace {
constexpr UINT kRidInput = 0x10000003;
constexpr USHORT kE0Flag = 0x02;
constexpr USHORT kMouseLeftDown = 0x0001;
constexpr USHORT kMouseLeftUp = 0x0002;
constexpr USHORT kMouseRightDown = 0x0004;
constexpr USHORT kMouseRightUp = 0x0008;
constexpr USHORT kMouseMiddleDown = 0x0010;
constexpr USHORT kMouseMiddleUp = 0x0020;
constexpr USHORT kMouseX1Down = 0x0040;
constexpr USHORT kMouseX1Up = 0x0080;
constexpr USHORT kMouseX2Down = 0x0100;
constexpr USHORT kMouseX2Up = 0x0200;
constexpr USHORT kMouseWheel = 0x0400;
}
#endif
namespace FancyInput {
RawInputDetector::RawInputDetector(QObject *parent)
: QObject(parent)
{
m_rawBuffer.resize(64);
}
RawInputDetector::~RawInputDetector()
{
stop();
}
bool RawInputDetector::start(QWindow *targetWindow)
{
#ifdef _WIN32
if (m_running || targetWindow == nullptr || targetWindow->winId() == 0)
return false;
if (!registerDevices(reinterpret_cast<HWND>(targetWindow->winId())))
return false;
m_targetWindow = targetWindow;
m_pressedKeys.clear();
QCoreApplication::instance()->installNativeEventFilter(this);
m_running = true;
return true;
#else
Q_UNUSED(targetWindow)
return false;
#endif
}
void RawInputDetector::stop()
{
#ifdef _WIN32
if (!m_running)
return;
QCoreApplication::instance()->removeNativeEventFilter(this);
unregisterDevices();
m_pressedKeys.clear();
m_targetWindow = nullptr;
m_running = false;
#endif
}
bool RawInputDetector::nativeEventFilter(const QByteArray &eventType,
void *message,
qintptr *result)
{
Q_UNUSED(result)
#ifdef _WIN32
if (!m_running || eventType != "windows_generic_MSG" || message == nullptr)
return false;
const MSG *msg = static_cast<const MSG *>(message);
if (msg->message == WM_INPUT)
handleRawInput(reinterpret_cast<HRAWINPUT>(msg->lParam));
#else
Q_UNUSED(eventType)
Q_UNUSED(message)
#endif
return false;
}
#ifdef _WIN32
bool RawInputDetector::registerDevices(HWND targetHandle)
{
RAWINPUTDEVICE devices[2] = {};
devices[0].usUsagePage = HID_USAGE_PAGE_GENERIC;
devices[0].usUsage = HID_USAGE_GENERIC_KEYBOARD;
devices[0].dwFlags = RIDEV_INPUTSINK;
devices[0].hwndTarget = targetHandle;
devices[1].usUsagePage = HID_USAGE_PAGE_GENERIC;
devices[1].usUsage = HID_USAGE_GENERIC_MOUSE;
devices[1].dwFlags = RIDEV_INPUTSINK;
devices[1].hwndTarget = targetHandle;
return RegisterRawInputDevices(devices, 2, sizeof(RAWINPUTDEVICE)) != FALSE;
}
void RawInputDetector::unregisterDevices()
{
RAWINPUTDEVICE devices[2] = {};
devices[0].usUsagePage = HID_USAGE_PAGE_GENERIC;
devices[0].usUsage = HID_USAGE_GENERIC_KEYBOARD;
devices[0].dwFlags = RIDEV_REMOVE;
devices[1].usUsagePage = HID_USAGE_PAGE_GENERIC;
devices[1].usUsage = HID_USAGE_GENERIC_MOUSE;
devices[1].dwFlags = RIDEV_REMOVE;
RegisterRawInputDevices(devices, 2, sizeof(RAWINPUTDEVICE));
}
void RawInputDetector::handleRawInput(HRAWINPUT rawInput)
{
UINT size = 0;
if (GetRawInputData(rawInput, RID_INPUT, nullptr, &size,
sizeof(RAWINPUTHEADER)) == static_cast<UINT>(-1) || size == 0)
return;
if (m_rawBuffer.size() < static_cast<qsizetype>(size))
m_rawBuffer.resize(static_cast<qsizetype>(size));
if (GetRawInputData(rawInput, RID_INPUT, m_rawBuffer.data(), &size,
sizeof(RAWINPUTHEADER)) == static_cast<UINT>(-1))
return;
const auto *input = reinterpret_cast<const RAWINPUT *>(m_rawBuffer.constData());
if (input->header.dwType == RIM_TYPEKEYBOARD)
handleKeyboard(input->data.keyboard);
else if (input->header.dwType == RIM_TYPEMOUSE)
handleMouse(input->data.mouse);
}
void RawInputDetector::handleKeyboard(const RAWKEYBOARD &keyboard)
{
const bool keyDown = keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN;
const bool keyUp = keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP;
if (!keyDown && !keyUp)
return;
const bool extended = (keyboard.Flags & kE0Flag) != 0;
const quint16 virtualKey = keyboard.VKey;
const FipKeys key = mapKey(virtualKey, keyboard.MakeCode, extended);
const FipRawKeys rawKey = mapRawKey(virtualKey, keyboard.MakeCode, extended);
if (keyDown) {
if (m_pressedKeys.contains(virtualKey))
return;
m_pressedKeys.insert(virtualKey);
}
if (keyUp)
m_pressedKeys.remove(virtualKey);
const bool ctrl = (GetAsyncKeyState(VK_LCONTROL) & 0x8000) || (GetAsyncKeyState(VK_RCONTROL) & 0x8000);
const bool shift = (GetAsyncKeyState(VK_LSHIFT) & 0x8000) || (GetAsyncKeyState(VK_RSHIFT) & 0x8000);
const bool alt = (GetAsyncKeyState(VK_LMENU) & 0x8000) || (GetAsyncKeyState(VK_RMENU) & 0x8000);
const bool win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
const bool numLock = (GetKeyState(VK_NUMLOCK) & 1) != 0;
const bool capsLock = (GetKeyState(VK_CAPITAL) & 1) != 0;
const bool scrollLock = (GetKeyState(VK_SCROLL) & 1) != 0;
if (keyDown)
emit keyPressed(key, rawKey, virtualKey, keyboard.MakeCode, extended, ctrl, shift, alt, win, numLock, capsLock, scrollLock);
else
emit keyReleased(key, rawKey, virtualKey, keyboard.MakeCode, extended, ctrl, shift, alt, win, numLock, capsLock, scrollLock);
}
void RawInputDetector::handleMouse(const RAWMOUSE &mouse)
{
const USHORT flags = mouse.usButtonFlags;
if (flags & kMouseLeftDown) emit mouseButtonChanged(MouseCodeType::Left, true);
if (flags & kMouseLeftUp) emit mouseButtonChanged(MouseCodeType::Left, false);
if (flags & kMouseRightDown) emit mouseButtonChanged(MouseCodeType::Right, true);
if (flags & kMouseRightUp) emit mouseButtonChanged(MouseCodeType::Right, false);
if (flags & kMouseMiddleDown) emit mouseButtonChanged(MouseCodeType::Middle, true);
if (flags & kMouseMiddleUp) emit mouseButtonChanged(MouseCodeType::Middle, false);
if (flags & kMouseX1Down) emit mouseButtonChanged(MouseCodeType::XButton1, true);
if (flags & kMouseX1Up) emit mouseButtonChanged(MouseCodeType::XButton1, false);
if (flags & kMouseX2Down) emit mouseButtonChanged(MouseCodeType::XButton2, true);
if (flags & kMouseX2Up) emit mouseButtonChanged(MouseCodeType::XButton2, false);
if (flags & kMouseWheel)
emit mouseWheelMoved(static_cast<qint16>(mouse.usButtonData));
if (mouse.lLastX != 0 || mouse.lLastY != 0)
emit mouseMoved(mouse.lLastX, mouse.lLastY);
}
#endif
FipKeys RawInputDetector::mapKey(quint16 virtualKey, quint16 scanCode, bool extended)
{
if (virtualKey == VK_PAUSE) return FipKeys::Pause;
if (virtualKey == VK_SNAPSHOT) return FipKeys::PrintScreen;
if (virtualKey == VK_RETURN && scanCode == 0x1C) return extended ? FipKeys::NumpadEnter : FipKeys::Enter;
if (virtualKey == VK_CONTROL && scanCode == 0x1D) return extended ? FipKeys::RControlKey : FipKeys::LControlKey;
if (virtualKey == VK_MENU && scanCode == 0x38) return extended ? FipKeys::RMenu : FipKeys::LMenu;
if (virtualKey == VK_SHIFT) {
if (scanCode == 0x2A) return FipKeys::LShiftKey;
if (scanCode == 0x36) return FipKeys::RShiftKey;
}
if (virtualKey == VK_LWIN) return FipKeys::LWin;
if (virtualKey == VK_RWIN) return FipKeys::RWin;
if (virtualKey == VK_APPS) return FipKeys::Apps;
if (extended) {
switch (scanCode) {
case 0x48: return FipKeys::Up;
case 0x4B: return FipKeys::Left;
case 0x4D: return FipKeys::Right;
case 0x50: return FipKeys::Down;
case 0x47: return FipKeys::Home;
case 0x4F: return FipKeys::End;
case 0x49: return FipKeys::PageUp;
case 0x51: return FipKeys::PageDown;
case 0x52: return FipKeys::Insert;
case 0x53: return FipKeys::Delete;
case 0x35: return FipKeys::Divide;
default: break;
}
}
return static_cast<FipKeys>(virtualKey);
}
FipRawKeys RawInputDetector::mapRawKey(quint16 virtualKey, quint16 scanCode, bool extended)
{
if (virtualKey == VK_PAUSE) return FipRawKeys::Pause;
if (virtualKey == VK_SNAPSHOT) return FipRawKeys::PrintScreen;
if (virtualKey == VK_CONTROL && scanCode == 0x1D) return extended ? FipRawKeys::RControlKey : FipRawKeys::LControlKey;
if (virtualKey == VK_MENU && scanCode == 0x38) return extended ? FipRawKeys::RMenu : FipRawKeys::LMenu;
if (virtualKey == VK_RETURN && scanCode == 0x1C) return extended ? FipRawKeys::NumpadEnter : FipRawKeys::Enter;
if (virtualKey == VK_LWIN) return FipRawKeys::LWin;
if (virtualKey == VK_RWIN) return FipRawKeys::RWin;
if (virtualKey == VK_APPS) return FipRawKeys::Apps;
const quint32 code = extended ? (0xE000u | scanCode) : scanCode;
return static_cast<FipRawKeys>(code);
}
} // namespace FancyInput
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include "../common/Enums.h"
#include <QObject>
#include <QAbstractNativeEventFilter>
#include <QByteArray>
#include <QSet>
#include <QWindow>
#ifdef _WIN32
#include <windows.h>
#endif
namespace FancyInput {
class RawInputDetector final : public QObject, public QAbstractNativeEventFilter
{
Q_OBJECT
public:
explicit RawInputDetector(QObject *parent = nullptr);
~RawInputDetector() override;
bool start(QWindow *targetWindow);
void stop();
bool isRunning() const { return m_running; }
bool nativeEventFilter(const QByteArray &eventType, void *message,
qintptr *result) override;
signals:
void keyPressed(FipKeys key, FipRawKeys rawKey, quint16 virtualKey,
quint16 scanCode, bool extended, bool ctrlDown,
bool shiftDown, bool altDown, bool winDown,
bool numLockOn, bool capsLockOn, bool scrollLockOn);
void keyReleased(FipKeys key, FipRawKeys rawKey, quint16 virtualKey,
quint16 scanCode, bool extended, bool ctrlDown,
bool shiftDown, bool altDown, bool winDown,
bool numLockOn, bool capsLockOn, bool scrollLockOn);
void mouseButtonChanged(MouseCodeType button, bool pressed);
void mouseWheelMoved(int delta);
void mouseMoved(int deltaX, int deltaY);
private:
bool registerDevices(HWND targetHandle);
void unregisterDevices();
void handleRawInput(HRAWINPUT rawInput);
void handleKeyboard(const RAWKEYBOARD &keyboard);
void handleMouse(const RAWMOUSE &mouse);
static FipKeys mapKey(quint16 virtualKey, quint16 scanCode, bool extended);
static FipRawKeys mapRawKey(quint16 virtualKey, quint16 scanCode, bool extended);
QWindow *m_targetWindow = nullptr;
QSet<quint16> m_pressedKeys;
bool m_running = false;
QByteArray m_rawBuffer;
};
} // namespace FancyInput
+48
View File
@@ -1,6 +1,13 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>
#include <QWindow>
#include "core/common/Enums.h"
#include "core/input/RawInputDetector.h"
static_assert(static_cast<std::uint16_t>(FancyInput::FipKeys::Enter) == 0x1000);
static_assert(static_cast<std::uint32_t>(FancyInput::FipRawKeys::NumpadEnter) == 0xE01C);
static_assert(static_cast<std::uint16_t>(FancyInput::FIPGamepadButtonflags::Y) == 0x8000);
int main(int argc, char *argv[])
{
@@ -16,5 +23,46 @@ int main(int argc, char *argv[])
Qt::QueuedConnection);
engine.loadFromModule("FancyInputQt", "MainWindow");
FancyInput::RawInputDetector rawInputDetector(&app);
if (!engine.rootObjects().isEmpty()) {
if (auto *window = qobject_cast<QWindow *>(engine.rootObjects().constFirst())) {
rawInputDetector.start(window);
QObject::connect(&rawInputDetector, &FancyInput::RawInputDetector::keyPressed,
[](FancyInput::FipKeys key, FancyInput::FipRawKeys rawKey,
quint16 virtualKey, quint16 scanCode, bool extended,
bool ctrl, bool shift, bool alt, bool win,
bool numLock, bool capsLock, bool scrollLock) {
qInfo().nospace() << "KEY DOWN key=" << static_cast<quint16>(key)
<< " raw=" << static_cast<quint32>(rawKey)
<< " vk=0x" << Qt::hex << virtualKey
<< " scan=0x" << scanCode << Qt::dec
<< " e0=" << extended << " modifiers(ctrl=" << ctrl
<< ",shift=" << shift << ",alt=" << alt << ",win=" << win
<< ") locks(num=" << numLock << ",caps=" << capsLock
<< ",scroll=" << scrollLock << ')';
});
QObject::connect(&rawInputDetector, &FancyInput::RawInputDetector::keyReleased,
[](FancyInput::FipKeys key, FancyInput::FipRawKeys rawKey,
quint16 virtualKey, quint16 scanCode, bool extended,
bool, bool, bool, bool, bool, bool, bool) {
qInfo().nospace() << "KEY UP key=" << static_cast<quint16>(key)
<< " raw=" << static_cast<quint32>(rawKey)
<< " vk=0x" << Qt::hex << virtualKey
<< " scan=0x" << scanCode << Qt::dec
<< " e0=" << extended;
});
QObject::connect(&rawInputDetector, &FancyInput::RawInputDetector::mouseButtonChanged,
[](FancyInput::MouseCodeType button, bool pressed) {
qInfo() << (pressed ? "MOUSE DOWN" : "MOUSE UP")
<< static_cast<int>(button);
});
QObject::connect(&rawInputDetector, &FancyInput::RawInputDetector::mouseWheelMoved,
[](int delta) { qInfo() << "MOUSE WHEEL" << delta; });
QObject::connect(&rawInputDetector, &FancyInput::RawInputDetector::mouseMoved,
[](int deltaX, int deltaY) { qInfo() << "MOUSE MOVE" << deltaX << deltaY; });
qInfo() << "Raw Input detection enabled on the main window.";
}
}
return app.exec();
}
+8 -47
View File
@@ -1,58 +1,19 @@
import QtQuick
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import FancyInput.Controls
import FancyInput.Windows
// 主界面(根元素为 Rectangle —— Qt Creator 设计模式可可视化编辑)
// 由 main.cpp 用 QQuickWindow 包装成主窗口。
Rectangle {
id: root
width: 600
height: 480
color: Theme.background
property int clickCount: 0
property Window settingsWindow: null
property Window overlayWindow: null
function openSettingsWindow(modal) {
settingsWindow = settingsWindowComp.createObject(modal ? root : null, { modal: modal })
if (settingsWindow) settingsWindow.show()
}
function openOverlayWindow() {
overlayWindow = overlayWindowComp.createObject(null, {})
if (overlayWindow) overlayWindow.show()
}
// 设置窗口
Component {
id: settingsWindowComp
FancyWindow {
width: 480
height: 360
windowTitle: qsTr("设置 · FancyInput")
SettingsWindow {
anchors.fill: parent
onCloseRequested: hostWindow.close()
}
}
}
// 悬浮窗
Component {
id: overlayWindowComp
FancyWindow {
width: 280
height: 180
frameless: true
transparent: true
alwaysOnTop: true
toolWindow: true
OverlayWindow {
anchors.fill: parent
onCloseRequested: hostWindow.close()
}
}
PopupWindows {
id: popupWindows
owner: root
}
ColumnLayout {
@@ -110,7 +71,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
text: qsTr("打开设置窗口")
font.family: Theme.fontFamily
onClicked: root.openSettingsWindow(false)
onClicked: popupWindows.openSettingsWindow(false)
}
// 打开模态窗口(WPF Window.ShowDialog 等价)
@@ -118,7 +79,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
text: qsTr("打开模态窗口")
font.family: Theme.fontFamily
onClicked: root.openSettingsWindow(true)
onClicked: popupWindows.openSettingsWindow(true)
}
// 打开无边框透明悬浮窗(WPF 悬浮窗等价)
@@ -126,7 +87,7 @@ Rectangle {
Layout.alignment: Qt.AlignHCenter
text: qsTr("打开悬浮窗")
font.family: Theme.fontFamily
onClicked: root.openOverlayWindow()
onClicked: popupWindows.openOverlayWindow()
}
}
}
+50
View File
@@ -0,0 +1,50 @@
import QtQuick
QtObject {
id: manager
property Item owner
property Window settingsWindow: null
property Window overlayWindow: null
function openSettingsWindow(modal) {
settingsWindow = settingsWindowComp.createObject(modal ? owner : null, { modal: modal })
if (settingsWindow) settingsWindow.show()
}
function openOverlayWindow() {
overlayWindow = overlayWindowComp.createObject(null, {})
if (overlayWindow) overlayWindow.show()
}
property Component settingsWindowComp: Component {
FancyWindow {
id: settingsHostWindow
width: 480
height: 360
windowTitle: qsTr("设置 · FancyInput")
SettingsWindow {
anchors.fill: parent
onCloseRequested: settingsHostWindow.hostWindow.close()
}
}
}
property Component overlayWindowComp: Component {
FancyWindow {
id: overlayHostWindow
width: 280
height: 180
frameless: true
transparent: true
alwaysOnTop: true
toolWindow: true
OverlayWindow {
anchors.fill: parent
onCloseRequested: overlayHostWindow.hostWindow.close()
}
}
}
}