From 2c8b2a8ad97bd92bd3979cc8c2ce7c896fee9a04 Mon Sep 17 00:00:00 2001 From: impressionyang Date: Mon, 15 Jun 2026 16:43:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(windows):=20=E4=BF=AE=E5=A4=8D=E7=83=AD?= =?UTF-8?q?=E9=94=AE=E5=8D=95=E9=94=AE=E6=94=AF=E6=8C=81=E5=92=8C=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E7=AA=97=E5=8F=A3=E9=97=AA=E7=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setHotkeyCombo 支持无修饰键的单键(CapsLock/F1 等) - 新增 CapsLock 及特殊按键到 Win32 VK 映射 - 热键窗口改为纯 Qt 方式隐藏,不再闪烁(winId+move 屏幕外) - 解析失败时 emit error 信号并记录日志,不再静默忽略 - 添加详细调试日志用于问题定位 Co-Authored-By: Claude Opus 4.6 --- src/core/win_hotkey.cpp | 324 +++++++++++++++++++++++++++------------- src/core/win_hotkey.h | 32 ++-- 2 files changed, 234 insertions(+), 122 deletions(-) diff --git a/src/core/win_hotkey.cpp b/src/core/win_hotkey.cpp index 6296902..e2b1988 100644 --- a/src/core/win_hotkey.cpp +++ b/src/core/win_hotkey.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #endif static const char* const kTag = "CapsLockVoiceHotkey"; @@ -16,14 +17,11 @@ namespace impress { struct CapsLockVoiceHotkey::Impl { #ifdef Q_OS_WIN int hotkeyId = 0; - QTimer* longPressTimer = nullptr; - QTimer* keyUpDebounce = nullptr; - bool isHolding = false; - bool longPressFired = false; - bool pollThreadRunning = false; - void* nativeEventFilter = nullptr; + QTimer* keyUpTimer = nullptr; // 检测松开的轮询定时器 QWidget* hiddenWindow = nullptr; - static constexpr int kLongPressMs = 1000; + void* nativeEventFilter = nullptr; + bool isHolding = false; + bool pollThreadRunning = false; #endif }; @@ -53,78 +51,132 @@ private: }; #endif +/** Qt::KeyboardModifiers → Win32 MOD_* */ +static int qtModsToWin32(int qtMods) { + int mods = 0; + if (qtMods & Qt::ControlModifier) mods |= MOD_CONTROL; + if (qtMods & Qt::AltModifier) mods |= MOD_ALT; + if (qtMods & Qt::ShiftModifier) mods |= MOD_SHIFT; + if (qtMods & Qt::MetaModifier) mods |= MOD_WIN; + return mods; +} + +/** Qt::Key → Win32 virtual key code */ +static int qtKeyToWin32(int key) { + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + return 'A' + (key - Qt::Key_A); + } + if (key >= Qt::Key_0 && key <= Qt::Key_9) { + return '0' + (key - Qt::Key_0); + } + if (key >= Qt::Key_F1 && key <= Qt::Key_F12) { + return VK_F1 + (key - Qt::Key_F1); + } + switch (key) { + case Qt::Key_Space: return VK_SPACE; + case Qt::Key_Return: + case Qt::Key_Enter: return VK_RETURN; + case Qt::Key_Escape: return VK_ESCAPE; + case Qt::Key_Tab: return VK_TAB; + case Qt::Key_Backspace: return VK_BACK; + case Qt::Key_Delete: return VK_DELETE; + case Qt::Key_Insert: return VK_INSERT; + case Qt::Key_Home: return VK_HOME; + case Qt::Key_End: return VK_END; + case Qt::Key_PageUp: return VK_PRIOR; + case Qt::Key_PageDown: return VK_NEXT; + case Qt::Key_Up: return VK_UP; + case Qt::Key_Down: return VK_DOWN; + case Qt::Key_Left: return VK_LEFT; + case Qt::Key_Right: return VK_RIGHT; + case Qt::Key_CapsLock: return VK_CAPITAL; + default: return 0; + } +} + CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent) : QObject(parent) , impl_(std::make_unique()) -{} +{ + hotkeyCombo_ = "Ctrl+Alt+C"; +} CapsLockVoiceHotkey::~CapsLockVoiceHotkey() { stop(); } +void CapsLockVoiceHotkey::setHotkeyCombo(const QString& combo) { + hotkeyCombo_ = combo; + + int mods = 0, key = 0; + QStringList parts = combo.split('+', Qt::SkipEmptyParts); + + // 解析修饰键(除了最后一个部分) + for (int i = 0; i < parts.size() - 1; i++) { + QString part = parts[i].trimmed().toLower(); + if (part == "ctrl" || part == "control") mods |= Qt::ControlModifier; + else if (part == "alt") mods |= Qt::AltModifier; + else if (part == "shift") mods |= Qt::ShiftModifier; + else if (part == "meta" || part == "super" || part == "win") mods |= Qt::MetaModifier; + } + + // 解析主键(最后一个部分) + QString mainKey = parts.last().trimmed(); + if (mainKey.length() == 1 && mainKey[0].isLetter()) { + key = Qt::Key_A + (mainKey[0].toUpper().unicode() - 'A'); + } else if (mainKey.length() == 1 && mainKey[0].isDigit()) { + key = Qt::Key_0 + (mainKey[0].unicode() - '0'); + } else { + // 特殊按键映射 + QString mk = mainKey.toLower(); + if (mk == "capslock") key = Qt::Key_CapsLock; + else if (mk == "space") key = Qt::Key_Space; + else if (mk == "enter" || mk == "return") key = Qt::Key_Return; + else if (mk == "esc") key = Qt::Key_Escape; + else if (mk == "tab") key = Qt::Key_Tab; + else if (mk == "backspace") key = Qt::Key_Backspace; + else if (mk == "delete") key = Qt::Key_Delete; + else if (mk == "insert") key = Qt::Key_Insert; + else if (mk == "home") key = Qt::Key_Home; + else if (mk == "end") key = Qt::Key_End; + else if (mk == "pageup") key = Qt::Key_PageUp; + else if (mk == "pagedown") key = Qt::Key_PageDown; + else if (mk == "up") key = Qt::Key_Up; + else if (mk == "down") key = Qt::Key_Down; + else if (mk == "left") key = Qt::Key_Left; + else if (mk == "right") key = Qt::Key_Right; + else if (mk.startsWith('f') && mk.length() >= 2) { + int num = mk.mid(1).toInt(); + if (num >= 1 && num <= 12) key = Qt::Key_F1 + (num - 1); + } + } + + if (key == 0) { + LOG_ERROR(kTag, QString("无法解析快捷键组合: %1").arg(combo)); + emit error(QString("无效的快捷键: %1").arg(combo)); + return; + } + + modifiers_ = qtModsToWin32(mods); + vkKey_ = qtKeyToWin32(key); + + LOG_INFO(kTag, QString("快捷键已更新: %1 (mods=%2, vk=0x%3)").arg(combo).arg(modifiers_).arg(vkKey_, 0, 16)); + + if (active_) { + unregisterHotkey(); + registerHotkey(); + } +} + bool CapsLockVoiceHotkey::start() { if (active_) return true; #ifdef Q_OS_WIN - // 创建隐藏窗口用于接收 WM_HOTKEY 消息 - if (!impl_->hiddenWindow) { - impl_->hiddenWindow = new QWidget(); - impl_->hiddenWindow->setObjectName("HotkeyReceiver"); - impl_->hiddenWindow->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); - impl_->hiddenWindow->resize(0, 0); - } - // 确保窗口已创建(show 会创建原生句柄) - impl_->hiddenWindow->show(); - - HWND hwnd = reinterpret_cast(impl_->hiddenWindow->winId()); - if (!hwnd) { - emit error("无法创建窗口句柄"); - return false; - } - - // 注册 CapsLock (VK_CAPITAL = 0x14) 全局快捷键 - // MOD_NOREPEAT 防止按住时重复触发 - const int vkCapsLock = 0x14; - impl_->hotkeyId = GlobalAddAtom(L"ImpressVoiceHotkey"); - - BOOL ok = RegisterHotKey(hwnd, impl_->hotkeyId, MOD_NOREPEAT, vkCapsLock); - if (!ok) { - DWORD err = GetLastError(); - emit error(QString("注册 CapsLock 快捷键失败 (错误码: %1)").arg(err)); - LOG_ERROR(kTag, QString("RegisterHotKey failed: %1").arg(err)); - return false; - } - - // 安装原生事件过滤器 - auto* filter = new HotkeyNativeEventFilter(this); - QGuiApplication::instance()->installNativeEventFilter(filter); - impl_->nativeEventFilter = filter; - - // 长按定时器 - impl_->longPressTimer = new QTimer(this); - impl_->longPressTimer->setSingleShot(true); - impl_->longPressTimer->setInterval(Impl::kLongPressMs); - connect(impl_->longPressTimer, &QTimer::timeout, this, [this]() { - if (impl_->isHolding && !impl_->longPressFired) { - impl_->longPressFired = true; - recording_ = true; - emit recordingStarted(); - LOG_DEBUG(kTag, "长按触发,开始录音"); - } - }); - - // 松开后延迟重置,避免 CapsLock 状态闪烁 - impl_->keyUpDebounce = new QTimer(this); - impl_->keyUpDebounce->setSingleShot(true); - impl_->keyUpDebounce->setInterval(200); - connect(impl_->keyUpDebounce, &QTimer::timeout, this, [this]() { - impl_->isHolding = false; - impl_->longPressFired = false; - }); + registerHotkey(); active_ = true; emit ready(); - LOG_INFO(kTag, "CapsLock 快捷键已注册"); + LOG_INFO(kTag, QString("快捷键已注册(%1)").arg(hotkeyCombo_)); return true; #else emit error("CapsLockVoiceHotkey 仅支持 Windows 平台"); @@ -137,15 +189,61 @@ void CapsLockVoiceHotkey::stop() { #ifdef Q_OS_WIN impl_->pollThreadRunning = false; + unregisterHotkey(); - if (impl_->longPressTimer) { - impl_->longPressTimer->stop(); - } - if (impl_->keyUpDebounce) { - impl_->keyUpDebounce->stop(); + active_ = false; + recording_ = false; + impl_->isHolding = false; + LOG_INFO(kTag, "快捷键已停止"); +#endif +} + +#ifdef Q_OS_WIN +void CapsLockVoiceHotkey::registerHotkey() { + if (!impl_->hiddenWindow) { + impl_->hiddenWindow = new QWidget(); + impl_->hiddenWindow->setObjectName("HotkeyReceiver"); + impl_->hiddenWindow->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); + impl_->hiddenWindow->resize(1, 1); + impl_->hiddenWindow->move(-10000, -10000); // 移到屏幕外 + } + + // 通过 winId() 创建原生窗口(不显示) + WId wid = impl_->hiddenWindow->winId(); + HWND hwnd = reinterpret_cast(wid); + if (!hwnd) { + emit error("无法创建窗口句柄"); + return; + } + + impl_->hotkeyId = GlobalAddAtom(L"ImpressVoiceHotkey"); + + // MOD_NOREPEAT 防止按住时重复触发 WM_HOTKEY + BOOL ok = RegisterHotKey(hwnd, impl_->hotkeyId, modifiers_ | MOD_NOREPEAT, vkKey_); + if (!ok) { + DWORD err = GetLastError(); + emit error(QString("注册快捷键 %1 失败 (错误码: %2)").arg(hotkeyCombo_).arg(err)); + LOG_ERROR(kTag, QString("RegisterHotKey failed: %1").arg(err)); + return; + } + + LOG_DEBUG(kTag, QString("热键窗口已注册 (HWND=%1)").arg((qulonglong)hwnd)); + + auto* filter = new HotkeyNativeEventFilter(this); + QGuiApplication::instance()->installNativeEventFilter(filter); + impl_->nativeEventFilter = filter; + + // keyUpTimer 用于检测松开 + impl_->keyUpTimer = new QTimer(this); + impl_->keyUpTimer->setSingleShot(true); + impl_->keyUpTimer->setInterval(500); // 最长等待 500ms +} + +void CapsLockVoiceHotkey::unregisterHotkey() { + if (impl_->keyUpTimer) { + impl_->keyUpTimer->stop(); } - // 移除原生事件过滤器 if (impl_->nativeEventFilter) { auto* filter = static_cast(impl_->nativeEventFilter); QGuiApplication::instance()->removeNativeEventFilter(filter); @@ -153,9 +251,9 @@ void CapsLockVoiceHotkey::stop() { impl_->nativeEventFilter = nullptr; } - // 注销快捷键 if (impl_->hiddenWindow) { - HWND hwnd = reinterpret_cast(impl_->hiddenWindow->winId()); + WId wid = impl_->hiddenWindow->winId(); + HWND hwnd = reinterpret_cast(wid); if (hwnd && impl_->hotkeyId) { UnregisterHotKey(hwnd, impl_->hotkeyId); GlobalDeleteAtom(impl_->hotkeyId); @@ -163,60 +261,72 @@ void CapsLockVoiceHotkey::stop() { } impl_->hiddenWindow->hide(); } - - active_ = false; - recording_ = false; - impl_->isHolding = false; - impl_->longPressFired = false; - LOG_INFO(kTag, "CapsLock 快捷键已停止"); +} #endif + +void CapsLockVoiceHotkey::toggleRecording() { + if (!active_ || ignoreEvents_) return; + + if (recording_) { + recording_ = false; + emit recordingStopped(); + } else { + recording_ = true; + emit recordingStarted(); + } } #ifdef Q_OS_WIN void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) { - if (!active_) return; + if (!active_ || impl_->isHolding) return; - // Windows 只在按键按下时触发 WM_HOTKEY - // 我们通过 GetAsyncKeyState 轮询检测松开 + LOG_DEBUG(kTag, "快捷键按下,开始录音"); + recording_ = true; impl_->isHolding = true; - impl_->longPressFired = false; + emit recordingStarted(); - // 启动长按定时器 - if (impl_->longPressTimer) { - impl_->longPressTimer->start(); - } - - // 启动轮询线程检测松开 + // 启动轮询线程检测松开(任何修饰键或主键松开即停止) if (impl_->pollThreadRunning) return; impl_->pollThreadRunning = true; - QThread::create([this]() { - const int vkCapsLock = 0x14; + + const int vk = vkKey_; + const int mods = modifiers_; + + QThread::create([this, vk, mods]() { + // 等待所有相关键松开 while (impl_->pollThreadRunning && impl_->isHolding) { - SHORT state = GetAsyncKeyState(vkCapsLock); - if (!(state & 0x8000)) { - // 按键松开 + // 检查主键 + SHORT mainState = GetAsyncKeyState(vk); + bool mainHeld = (mainState & 0x8000) != 0; + + // 检查修饰键 + bool ctrlHeld = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + bool altHeld = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; + bool shiftHeld = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; + bool winHeld = (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0 || + (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0; + + bool modsHeld = true; + if (mods & MOD_CONTROL && !ctrlHeld) modsHeld = false; + if (mods & MOD_ALT && !altHeld) modsHeld = false; + if (mods & MOD_SHIFT && !shiftHeld) modsHeld = false; + if (mods & MOD_WIN && !winHeld) modsHeld = false; + + if (!mainHeld || !modsHeld) { + // 任一组合键松开 impl_->isHolding = false; - if (impl_->longPressTimer) { - impl_->longPressTimer->stop(); - } + QMetaObject::invokeMethod(this, [this]() { - if (impl_->longPressFired) { - // 长按 → 停止录音 + if (recording_) { + LOG_DEBUG(kTag, "快捷键松开,停止录音"); recording_ = false; emit recordingStopped(); - LOG_DEBUG(kTag, "长按结束,停止录音"); - } else { - // 短按 → 不处理(让系统处理 CapsLock) - LOG_DEBUG(kTag, "短按,不拦截 CapsLock"); - } - impl_->longPressFired = false; - if (impl_->keyUpDebounce) { - impl_->keyUpDebounce->start(); } + impl_->isHolding = false; }, Qt::QueuedConnection); break; } - QThread::msleep(50); + QThread::msleep(30); } impl_->pollThreadRunning = false; })->start(); diff --git a/src/core/win_hotkey.h b/src/core/win_hotkey.h index 9ed10ce..8fe0865 100644 --- a/src/core/win_hotkey.h +++ b/src/core/win_hotkey.h @@ -2,20 +2,15 @@ #include #include -#include #include namespace impress { /** - * @brief CapsLock 长按语音输入快捷键管理器(Windows) + * @brief 全局语音输入快捷键管理器(Windows) * * 使用 Windows RegisterHotKey API 实现全局快捷键。 - * 工作流程: - * 1. 用户长按 CapsLock 1 秒后触发录音 - * 2. 长按期间持续录音 - * 3. 松开 CapsLock 后停止录音并触发转写 - * 4. 短按(< 1s)直接传递 CapsLock 事件(切换大小写锁定) + * 切换模式:按一次开始录音,再按一次停止录音并转写。 */ class CapsLockVoiceHotkey : public QObject { Q_OBJECT @@ -32,20 +27,19 @@ public: /** @brief 是否已激活 */ bool isActive() const { return active_; } - /** @brief 当前是否正在录音(CapsLock 长按超过 1s 后) */ + /** @brief 当前是否正在录音 */ bool isRecording() const { return recording_; } + /** @brief 临时忽略信号 */ + void setIgnoreEvents(bool ignore) { ignoreEvents_ = ignore; } + + /** @brief 设置快捷键组合(如 "Ctrl+Alt+C") */ + void setHotkeyCombo(const QString& combo); + signals: - /** @brief 开始录音(长按超过 1 秒后) */ void recordingStarted(); - - /** @brief 停止录音(松开快捷键后) */ void recordingStopped(); - - /** @brief 快捷键已注册 */ void ready(); - - /** @brief 初始化失败 */ void error(const QString& message); /** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */ @@ -57,6 +51,14 @@ private: std::unique_ptr impl_; bool active_ = false; bool recording_ = false; + bool ignoreEvents_ = false; + QString hotkeyCombo_; + int modifiers_ = 0x0002 | 0x0001; // MOD_CONTROL | MOD_ALT + int vkKey_ = 'C'; + + void registerHotkey(); + void unregisterHotkey(); + void toggleRecording(); }; } // namespace impress