添加跨平台全局热键支持PTT按住说话功能

This commit is contained in:
2026-06-23 21:11:05 +08:00
parent 3437329193
commit 5141e524dd
3 changed files with 280 additions and 3 deletions
+11 -1
View File
@@ -6,7 +6,7 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
set(CMAKE_PREFIX_PATH "/home/misaki/Qt6.3/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写) set(CMAKE_PREFIX_PATH "/home/misaki/Qt/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写)
# 设置是否为debug模式 默认为Debug # 设置是否为debug模式 默认为Debug
if(NOT CMAKE_BUILD_TYPE) if(NOT CMAKE_BUILD_TYPE)
@@ -183,6 +183,16 @@ target_link_libraries(${PROJECT_NAME}
Qt::Concurrent Qt::Concurrent
) )
# 全局热键 (PTT) 所需平台库
if(APPLE)
find_library(COREGRAPHICS_LIB CoreGraphics REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE ${COREGRAPHICS_LIB})
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(X11 REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE X11::X11)
endif()
# 添加头文件 # 添加头文件
target_include_directories(${PROJECT_NAME} target_include_directories(${PROJECT_NAME}
PRIVATE PRIVATE
+42 -2
View File
@@ -12,12 +12,22 @@
#include <QMutex> #include <QMutex>
#include <QObject> #include <QObject>
#include <QHash> #include <QHash>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
#ifdef Q_OS_LINUX
#include <QAbstractNativeEventFilter>
#endif
#include "serialportmanager.h" #include "serialportmanager.h"
class DeviceTcpServer; class DeviceTcpServer;
class DeviceWebSocketServer; class DeviceWebSocketServer;
class AppCore final : public QObject { class AppCore final : public QObject
#ifdef Q_OS_LINUX
, public QAbstractNativeEventFilter
#endif
{
Q_OBJECT Q_OBJECT
Q_DISABLE_COPY(AppCore) Q_DISABLE_COPY(AppCore)
@@ -44,5 +54,35 @@ public:
public: public:
void SingleExchange(); void SingleExchange();
void tryToInit() { return; }; void tryToInit() { return; }
// PTT 按住说话
void startPttRecording();
void stopPttRecording();
void setupGlobalHotkey();
void cleanupGlobalHotkey();
#if defined(Q_OS_WIN)
private:
static LRESULT CALLBACK lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam);
HHOOK m_keyboardHook = nullptr;
bool m_isPttDown = false;
UINT m_hotkeyVKey = VK_OEM_3;
#elif defined(Q_OS_LINUX)
private:
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override;
void onDebounceStop();
bool m_isPttDown = false;
int m_hotkeyCode = 0;
class QTimer *m_pttDebounce = nullptr;
#elif defined(Q_OS_MACOS)
private:
void *m_eventTap = nullptr;
void *m_runLoopSource = nullptr;
bool m_isPttDown = false;
int m_hotkeyCode = 50;
#endif
}; };
+227
View File
@@ -4,6 +4,8 @@
#include "AppCore.h" #include "AppCore.h"
#include <QDebug> #include <QDebug>
#include <QApplication>
#include <QTimer>
#include "AudioDataHandle.h" #include "AudioDataHandle.h"
#include "AutoAgentHandle.h" #include "AutoAgentHandle.h"
@@ -15,10 +17,26 @@
#include "websocketmanager.h" #include "websocketmanager.h"
#include "DeviceTcpServer.h" #include "DeviceTcpServer.h"
#include "DeviceWebSocketServer.h" #include "DeviceWebSocketServer.h"
#ifdef Q_OS_LINUX
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#endif
#ifdef Q_OS_MACOS
#include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#endif
// 初始化静态成员 // 初始化静态成员
QScopedPointer<AppCore> AppCore::m_instance; QScopedPointer<AppCore> AppCore::m_instance;
QMutex AppCore::m_mutex; QMutex AppCore::m_mutex;
#ifdef Q_OS_WIN
static AppCore *g_hotkeyInstance = nullptr;
#endif
// 单例实现 (QScopedPointer + Mutex) // 单例实现 (QScopedPointer + Mutex)
AppCore* AppCore::getInstance() AppCore* AppCore::getInstance()
{ {
@@ -81,10 +99,13 @@ AppCore::AppCore(QObject *parent) : QObject(parent)
// 连接必要的信号 // 连接必要的信号
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte,
this, &AppCore::onRecordingFinished_Byte); this, &AppCore::onRecordingFinished_Byte);
setupGlobalHotkey();
} }
AppCore::~AppCore() AppCore::~AppCore()
{ {
cleanupGlobalHotkey();
if (m_deviceTcpServer) m_deviceTcpServer->stop(); if (m_deviceTcpServer) m_deviceTcpServer->stop();
if (m_deviceWsServer) m_deviceWsServer->stop(); if (m_deviceWsServer) m_deviceWsServer->stop();
@@ -118,3 +139,209 @@ void AppCore::onRecordingFinished_Byte(const QByteArray &wavData) {
NetworkDO::getInstance()->sendPacket(packet); NetworkDO::getInstance()->sendPacket(packet);
} }
void AppCore::startPttRecording() {
qDebug() << "PTT recording started";
AudioInput::getInstance()->startAudio();
}
void AppCore::stopPttRecording() {
qDebug() << "PTT recording stopped";
AudioInput::getInstance()->stopAudio();
}
// ===================== Windows =====================
#ifdef Q_OS_WIN
void AppCore::setupGlobalHotkey() {
g_hotkeyInstance = this;
HMODULE hMod = GetModuleHandle(nullptr);
m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, lowLevelKeyboardHook, hMod, 0);
if (m_keyboardHook) {
qDebug() << "[AppCore] 全局 PTT 热键钩子已安装 (键码:" << m_hotkeyVKey << ")";
} else {
qWarning() << "[AppCore] 全局 PTT 热键钩子安装失败:" << GetLastError();
}
}
void AppCore::cleanupGlobalHotkey() {
if (m_keyboardHook) {
UnhookWindowsHookEx(m_keyboardHook);
m_keyboardHook = nullptr;
qDebug() << "[AppCore] 全局 PTT 热键钩子已卸载";
}
g_hotkeyInstance = nullptr;
}
LRESULT CALLBACK AppCore::lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_hotkeyInstance) {
KBDLLHOOKSTRUCT *pKb = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if (pKb->vkCode == g_hotkeyInstance->m_hotkeyVKey) {
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
if (!g_hotkeyInstance->m_isPttDown) {
g_hotkeyInstance->m_isPttDown = true;
QMetaObject::invokeMethod(qApp, []() {
AppCore::getInstance()->startPttRecording();
}, Qt::QueuedConnection);
}
return 1;
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
if (g_hotkeyInstance->m_isPttDown) {
g_hotkeyInstance->m_isPttDown = false;
QMetaObject::invokeMethod(qApp, []() {
AppCore::getInstance()->stopPttRecording();
}, Qt::QueuedConnection);
}
return 1;
}
}
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
// ===================== Linux (X11) =====================
#elif defined(Q_OS_LINUX)
void AppCore::setupGlobalHotkey() {
Display *display = XOpenDisplay(nullptr);
if (!display) {
qWarning() << "[AppCore] 无法打开 X11 Display,全局热键不可用";
return;
}
// 尝试开启 detectable auto-repeat(不一定持久影响其他连接,不影响 debounce 方案)
int orig = 0;
XkbSetDetectableAutoRepeat(display, True, &orig);
KeyCode keycode = XKeysymToKeycode(display, XK_grave);
if (!keycode) {
qWarning() << "[AppCore] 未找到波浪号键的键码";
XCloseDisplay(display);
return;
}
m_hotkeyCode = keycode;
XGrabKey(display, keycode, AnyModifier, DefaultRootWindow(display),
True, GrabModeAsync, GrabModeAsync);
XCloseDisplay(display);
// debounce 定时器:KeyRelease 后等待 50ms,若没有新的 KeyPress 再停
m_pttDebounce = new QTimer(this);
m_pttDebounce->setSingleShot(true);
connect(m_pttDebounce, &QTimer::timeout, this, &AppCore::onDebounceStop);
qApp->installNativeEventFilter(this);
qDebug() << "[AppCore] Linux 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
}
void AppCore::cleanupGlobalHotkey() {
qApp->removeNativeEventFilter(this);
if (m_pttDebounce) {
m_pttDebounce->stop();
delete m_pttDebounce;
m_pttDebounce = nullptr;
}
Display *display = XOpenDisplay(nullptr);
if (display) {
XUngrabKey(display, m_hotkeyCode, AnyModifier, DefaultRootWindow(display));
XCloseDisplay(display);
}
m_hotkeyCode = 0;
qDebug() << "[AppCore] Linux 全局 PTT 热键已卸载";
}
void AppCore::onDebounceStop() {
if (m_isPttDown) {
m_isPttDown = false;
stopPttRecording();
}
}
bool AppCore::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) {
if (eventType != "xcb_generic_event_t")
return false;
auto *data = static_cast<const uint8_t *>(message);
uint8_t type = data[0] & 0x7f;
if ((type == 2 || type == 3) && data[1] == static_cast<uint8_t>(m_hotkeyCode)) {
if (type == 2) { // KeyPress
m_pttDebounce->stop(); // 取消待决的停止
if (!m_isPttDown) {
m_isPttDown = true;
startPttRecording();
}
} else { // KeyRelease
// 不立即停,等待 50ms 确认没有连发 KeyPress
if (m_isPttDown)
m_pttDebounce->start(50);
}
if (result) *result = 1;
return true;
}
return false;
}
// ===================== macOS (CGEventTap) =====================
#elif defined(Q_OS_MACOS)
static CGEventRef pttEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
auto *core = static_cast<AppCore *>(refcon);
if (type == kCGEventTapDisabledByTimeout) {
if (core && core->m_eventTap)
CGEventTapEnable(static_cast<CFMachPortRef>(core->m_eventTap), true);
return event;
}
if (type != kCGEventKeyDown && type != kCGEventKeyUp)
return event;
CGKeyCode kc = static_cast<CGKeyCode>(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
if (kc != core->m_hotkeyCode)
return event;
if (type == kCGEventKeyDown && !core->m_isPttDown) {
core->m_isPttDown = true;
core->startPttRecording();
} else if (type == kCGEventKeyUp && core->m_isPttDown) {
core->m_isPttDown = false;
core->stopPttRecording();
}
return nullptr;
}
void AppCore::setupGlobalHotkey() {
CFMachPortRef tap = CGEventTapCreate(
kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp),
&pttEventTapCallback,
this);
if (!tap) {
qWarning() << "[AppCore] macOS PTT 热键创建失败 (需要辅助功能权限)";
return;
}
m_eventTap = tap;
CFRunLoopSourceRef src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
m_runLoopSource = src;
CFRunLoopAddSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
CGEventTapEnable(tap, true);
qDebug() << "[AppCore] macOS 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
}
void AppCore::cleanupGlobalHotkey() {
if (m_eventTap) {
auto *tap = static_cast<CFMachPortRef>(m_eventTap);
CGEventTapEnable(tap, false);
if (m_runLoopSource) {
auto *src = static_cast<CFRunLoopSourceRef>(m_runLoopSource);
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
CFRelease(src);
}
CFRelease(tap);
m_eventTap = nullptr;
m_runLoopSource = nullptr;
}
qDebug() << "[AppCore] macOS 全局 PTT 热键已卸载";
}
#endif