fix(windows): 修复热键单键支持和隐藏窗口闪烁

- setHotkeyCombo 支持无修饰键的单键(CapsLock/F1 等)
- 新增 CapsLock 及特殊按键到 Win32 VK 映射
- 热键窗口改为纯 Qt 方式隐藏,不再闪烁(winId+move 屏幕外)
- 解析失败时 emit error 信号并记录日志,不再静默忽略
- 添加详细调试日志用于问题定位

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
impressionyang 2026-06-15 16:43:31 +08:00
parent 20bc24a028
commit 2c8b2a8ad9
2 changed files with 234 additions and 122 deletions

View File

@ -7,6 +7,7 @@
#include <QGuiApplication> #include <QGuiApplication>
#include <QThread> #include <QThread>
#include <QWidget> #include <QWidget>
#include <QTimer>
#endif #endif
static const char* const kTag = "CapsLockVoiceHotkey"; static const char* const kTag = "CapsLockVoiceHotkey";
@ -16,14 +17,11 @@ namespace impress {
struct CapsLockVoiceHotkey::Impl { struct CapsLockVoiceHotkey::Impl {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
int hotkeyId = 0; int hotkeyId = 0;
QTimer* longPressTimer = nullptr; QTimer* keyUpTimer = nullptr; // 检测松开的轮询定时器
QTimer* keyUpDebounce = nullptr;
bool isHolding = false;
bool longPressFired = false;
bool pollThreadRunning = false;
void* nativeEventFilter = nullptr;
QWidget* hiddenWindow = nullptr; QWidget* hiddenWindow = nullptr;
static constexpr int kLongPressMs = 1000; void* nativeEventFilter = nullptr;
bool isHolding = false;
bool pollThreadRunning = false;
#endif #endif
}; };
@ -53,78 +51,132 @@ private:
}; };
#endif #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) CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent)
: QObject(parent) : QObject(parent)
, impl_(std::make_unique<Impl>()) , impl_(std::make_unique<Impl>())
{} {
hotkeyCombo_ = "Ctrl+Alt+C";
}
CapsLockVoiceHotkey::~CapsLockVoiceHotkey() { CapsLockVoiceHotkey::~CapsLockVoiceHotkey() {
stop(); 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() { bool CapsLockVoiceHotkey::start() {
if (active_) return true; if (active_) return true;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
// 创建隐藏窗口用于接收 WM_HOTKEY 消息 registerHotkey();
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<HWND>(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;
});
active_ = true; active_ = true;
emit ready(); emit ready();
LOG_INFO(kTag, "CapsLock 快捷键已注册"); LOG_INFO(kTag, QString("快捷键已注册(%1").arg(hotkeyCombo_));
return true; return true;
#else #else
emit error("CapsLockVoiceHotkey 仅支持 Windows 平台"); emit error("CapsLockVoiceHotkey 仅支持 Windows 平台");
@ -137,15 +189,61 @@ void CapsLockVoiceHotkey::stop() {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
impl_->pollThreadRunning = false; impl_->pollThreadRunning = false;
unregisterHotkey();
if (impl_->longPressTimer) { active_ = false;
impl_->longPressTimer->stop(); recording_ = false;
} impl_->isHolding = false;
if (impl_->keyUpDebounce) { LOG_INFO(kTag, "快捷键已停止");
impl_->keyUpDebounce->stop(); #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<HWND>(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) { if (impl_->nativeEventFilter) {
auto* filter = static_cast<HotkeyNativeEventFilter*>(impl_->nativeEventFilter); auto* filter = static_cast<HotkeyNativeEventFilter*>(impl_->nativeEventFilter);
QGuiApplication::instance()->removeNativeEventFilter(filter); QGuiApplication::instance()->removeNativeEventFilter(filter);
@ -153,9 +251,9 @@ void CapsLockVoiceHotkey::stop() {
impl_->nativeEventFilter = nullptr; impl_->nativeEventFilter = nullptr;
} }
// 注销快捷键
if (impl_->hiddenWindow) { if (impl_->hiddenWindow) {
HWND hwnd = reinterpret_cast<HWND>(impl_->hiddenWindow->winId()); WId wid = impl_->hiddenWindow->winId();
HWND hwnd = reinterpret_cast<HWND>(wid);
if (hwnd && impl_->hotkeyId) { if (hwnd && impl_->hotkeyId) {
UnregisterHotKey(hwnd, impl_->hotkeyId); UnregisterHotKey(hwnd, impl_->hotkeyId);
GlobalDeleteAtom(impl_->hotkeyId); GlobalDeleteAtom(impl_->hotkeyId);
@ -163,60 +261,72 @@ void CapsLockVoiceHotkey::stop() {
} }
impl_->hiddenWindow->hide(); impl_->hiddenWindow->hide();
} }
}
active_ = false;
recording_ = false;
impl_->isHolding = false;
impl_->longPressFired = false;
LOG_INFO(kTag, "CapsLock 快捷键已停止");
#endif #endif
void CapsLockVoiceHotkey::toggleRecording() {
if (!active_ || ignoreEvents_) return;
if (recording_) {
recording_ = false;
emit recordingStopped();
} else {
recording_ = true;
emit recordingStarted();
}
} }
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) { void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) {
if (!active_) return; if (!active_ || impl_->isHolding) return;
// Windows 只在按键按下时触发 WM_HOTKEY LOG_DEBUG(kTag, "快捷键按下,开始录音");
// 我们通过 GetAsyncKeyState 轮询检测松开 recording_ = true;
impl_->isHolding = true; impl_->isHolding = true;
impl_->longPressFired = false; emit recordingStarted();
// 启动长按定时器 // 启动轮询线程检测松开(任何修饰键或主键松开即停止)
if (impl_->longPressTimer) {
impl_->longPressTimer->start();
}
// 启动轮询线程检测松开
if (impl_->pollThreadRunning) return; if (impl_->pollThreadRunning) return;
impl_->pollThreadRunning = true; 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) { 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; impl_->isHolding = false;
if (impl_->longPressTimer) {
impl_->longPressTimer->stop();
}
QMetaObject::invokeMethod(this, [this]() { QMetaObject::invokeMethod(this, [this]() {
if (impl_->longPressFired) { if (recording_) {
// 长按 → 停止录音 LOG_DEBUG(kTag, "快捷键松开,停止录音");
recording_ = false; recording_ = false;
emit recordingStopped(); 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); }, Qt::QueuedConnection);
break; break;
} }
QThread::msleep(50); QThread::msleep(30);
} }
impl_->pollThreadRunning = false; impl_->pollThreadRunning = false;
})->start(); })->start();

View File

@ -2,20 +2,15 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QTimer>
#include <memory> #include <memory>
namespace impress { namespace impress {
/** /**
* @brief CapsLock Windows * @brief Windows
* *
* 使 Windows RegisterHotKey API * 使 Windows RegisterHotKey API
* *
* 1. CapsLock 1
* 2.
* 3. CapsLock
* 4. < 1s CapsLock
*/ */
class CapsLockVoiceHotkey : public QObject { class CapsLockVoiceHotkey : public QObject {
Q_OBJECT Q_OBJECT
@ -32,20 +27,19 @@ public:
/** @brief 是否已激活 */ /** @brief 是否已激活 */
bool isActive() const { return active_; } bool isActive() const { return active_; }
/** @brief 当前是否正在录音CapsLock 长按超过 1s 后) */ /** @brief 当前是否正在录音 */
bool isRecording() const { return recording_; } bool isRecording() const { return recording_; }
/** @brief 临时忽略信号 */
void setIgnoreEvents(bool ignore) { ignoreEvents_ = ignore; }
/** @brief 设置快捷键组合(如 "Ctrl+Alt+C" */
void setHotkeyCombo(const QString& combo);
signals: signals:
/** @brief 开始录音(长按超过 1 秒后) */
void recordingStarted(); void recordingStarted();
/** @brief 停止录音(松开快捷键后) */
void recordingStopped(); void recordingStopped();
/** @brief 快捷键已注册 */
void ready(); void ready();
/** @brief 初始化失败 */
void error(const QString& message); void error(const QString& message);
/** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */ /** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */
@ -57,6 +51,14 @@ private:
std::unique_ptr<Impl> impl_; std::unique_ptr<Impl> impl_;
bool active_ = false; bool active_ = false;
bool recording_ = 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 } // namespace impress