前置知識
什么是HID?
HID(Human Interface Device)是?直接與人交互的電子設備?,通過標準化協議實現用戶與計算機或其他設備的通信,典型代表包括鍵盤、鼠標、游戲手柄等。?
為什么HID要與qt進行通信?
我這里的應用場景是數位板與我自己寫的上位機進行通信,用戶可以在上位機軟件中手動設置數位板上按鍵代表的快捷鍵。
如何知道當前HID設備的VID,PID?
- 打開?設備管理器,找到您的設備(通常在?人體學輸入設備?或?通用串行總線設備?類別下)。
- 右鍵點擊設備 ->?屬性?-> 事件選項卡
如何通信?
1. 引入?hidapi
?庫
hidapi庫是一個第三方庫需要下載。下載完編譯之后把 hidapi.h,hidapi.dll,hidapi.lib放到項目根目錄下。
hid設備初始化有以下幾個步驟:
2. 初始化?hidapi
hid_init()
3. 枚舉和選擇設備
void enumerateDevices() {struct hid_device_info *devs = hid_enumerate(0x0, 0x0); // 參數為VID和PID,0x0表示匹配所有struct hid_device_info *cur_dev = devs;while (cur_dev) {printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls",cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number);printf("\n");printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string);printf(" Product: %ls\n", cur_dev->product_string);printf(" Release: %hx\n", cur_dev->release_number);printf(" Interface Number: %d\n\n", cur_dev->interface_number);cur_dev = cur_dev->next;}hid_free_enumeration(devs);
}
4. 打開設備
hid_device *handle;
handle = hid_open(0x1234, 0x5678, NULL); // 替換為設備的VID和PID
5.?設置非阻塞模式(可選)
int res = hid_set_nonblocking(handle, 1); // 參數為1表示非阻塞模式
if (res < 0) {// 處理設置失敗的情況
}
?6. 讀取和寫入數據
// 讀取數據
unsigned char buf[256];
int res = hid_read(handle, buf, sizeof(buf));
if (res < 0) {// 處理讀取錯誤
} else {// 處理讀取到的數據
}// 寫入數據
unsigned char data[] = {0x00, 0x01, 0x02}; // 示例數據
res = hid_write(handle, data, sizeof(data));
if (res < 0) {// 處理寫入錯誤
}
?7. 關閉設備和釋放資源
hid_close(handle);
hid_exit();
示例代碼整合
void MainWindow::HidInit()
{// 1. 初始化HIDAPIif (hid_init() != 0) {qDebug() << "[錯誤] HIDAPI初始化失敗";return;}else{qDebug() << "[正確] HIDAPI初始化成功";}// 2. 枚舉設備qDebug() << "[調試] 開始枚舉HID設備...";hid_device_info *devs = hid_enumerate(0x0, 0x0);if (!devs) {qDebug() << "[錯誤] 無法枚舉HID設備,可能沒有HID設備連接";hid_exit();return;}hid_device_info *cur_dev = devs;char* devicePath = nullptr;bool deviceFound = false;int deviceCount = 0; // 用于統計發現的HID設備數量qDebug() << "[調試] 開始遍歷HID設備列表...";while (cur_dev) {deviceCount++;// 打印所有HID設備信息,用于調試qDebug().nospace() << "[調試] 設備 #" << deviceCount<< ": VID=0x" << QString::number(cur_dev->vendor_id, 16).toUpper()<< ", PID=0x" << QString::number(cur_dev->product_id, 16).toUpper();// << ", 路徑=" << QString::fromWCharArray(cur_dev->path);if (cur_dev->vendor_id == TARGET_VID && cur_dev->product_id == TARGET_PID) {devicePath = _strdup(cur_dev->path);deviceFound = true;qDebug() << "\n[信息] 找到目標設備:";qDebug() << "路徑:" << devicePath;qDebug() << "制造商:" << (cur_dev->manufacturer_string ? QString::fromWCharArray(cur_dev->manufacturer_string) : "N/A");qDebug() << "產品名:" << (cur_dev->product_string ? QString::fromWCharArray(cur_dev->product_string) : "N/A");qDebug() << "接口號:" << cur_dev->interface_number;break;}cur_dev = cur_dev->next;}qDebug() << "[調試] 遍歷完成,共發現" << deviceCount << "個HID設備";hid_free_enumeration(devs);if (!deviceFound) {qDebug() << "[錯誤] 未找到目標設備 (VID: 0x" << QString::number(TARGET_VID, 16).toUpper()<< ", PID: 0x" << QString::number(TARGET_PID, 16).toUpper() << ")";hid_exit();return;}// 3. 打開設備qDebug() << "[調試] 嘗試打開目標設備...";hid_device* handle = hid_open_path(devicePath);if (!handle) {qDebug() << "[錯誤] 無法打開設備:" << QString::fromWCharArray(hid_error(nullptr));free(devicePath);hid_exit();return;}// 3. 打開設備handle = hid_open_path(devicePath);if (!handle) {qDebug() << "[錯誤] 無法打開設備:" << QString::fromWCharArray(hid_error(nullptr));free(devicePath);hid_exit();return;}// 設置非阻塞模式hid_set_nonblocking(handle, 1);qDebug() << "\n[信息] 設備已成功打開";// 4. 嘗試通信const int REPORT_SIZE = 65; // 64字節數據 + 1字節報告IDunsigned char buf[REPORT_SIZE] = {0};// 嘗試不同報告ID (0x00-0xFF)for (int report_id = 0x00; report_id <= 0xFF; report_id++) {// 4.1 嘗試特性報告buf[0] = report_id;buf[1] = 0x01; // 示例命令qDebug() << "\n[調試] 嘗試報告ID: 0x" << QString::number(report_id, 16).toUpper();int res = hid_send_feature_report(handle, buf, REPORT_SIZE);if (res > 0) {qDebug() << "[成功] 特性報告發送成功 (ID: 0x"<< QString::number(report_id, 16).toUpper() << ")";break;} else if (report_id == 0xFF) {qDebug() << "[警告] 所有特性報告嘗試失敗";}// 4.2 嘗試輸出報告res = hid_write(handle, buf, REPORT_SIZE);if (res > 0) {qDebug() << "[成功] 輸出報告發送成功 (ID: 0x"<< QString::number(report_id, 16).toUpper() << ")";break;} else if (report_id == 0xFF) {qDebug() << "[警告] 所有輸出報告嘗試失敗";}}// 5. 讀取響應 (5秒超時)qDebug() << "\n[信息] 等待設備響應...";int timeout_ms = 5000;QElapsedTimer timer;timer.start();while (timer.elapsed() < timeout_ms) {int res = hid_read(handle, buf, REPORT_SIZE);if (res > 0) {qDebug() << "[成功] 收到" << res << "字節數據:";// 打印接收到的數據 (十六進制格式)QString hexData;for (int i = 0; i < res; i++) {hexData += "0x" + QString::number(buf[i], 16).toUpper().rightJustified(2, '0') + " ";if ((i+1) % 8 == 0) hexData += "\n";}qDebug() << hexData;break;} else if (res == 0) {QThread::msleep(100); // 避免CPU占用過高} else {qDebug() << "[錯誤] 讀取失敗:" << QString::fromWCharArray(hid_error(handle));break;}}if (timer.elapsed() >= timeout_ms) {qDebug() << "[警告] 讀取超時,未收到響應";}// 6. 清理資源hid_close(handle);free(devicePath);hid_exit();qDebug() << "\n[信息] HID通信結束";
}
?運行效果演示(我接入的是wacom數位板):
?全是0xFF為未激活狀態(初始狀態)。
總結操作流程
- 確認設備功能與協議:明確設備是輸入型(主動上報)還是命令型(需指令觸發)。
- 發送測試指令:若無文檔,通過簡單指令試探設備響應模式。
- 解析數據結構:根據響應數據的變化規律,逆向推導字節含義(如坐標、狀態、校驗等)。
- 編寫業務邏輯:基于解析結果,實現數據處理或控制功能(如鼠標模擬、設備配置等)。
解析報告數據
如何解析
用以下結構來存儲報告:
struct TabletData {quint8 reportId; // 報告IDquint16 x; // X坐標(0-最大值)quint16 y; // Y坐標(0-最大值)quint16 pressure; // 壓力值(0-最大值)QList<int> buttons; // 按下的按鈕列表(按鈕編號從1開始)
};
?創建一個TabletData 類型的函數:
該函數對報告進行解析,第0字節是報告ID,第1字節是按鈕位置........
TabletData HidManager::parseTabletData(const QByteArray& data) {TabletData result;if (data.isEmpty()) return result;result.reportId = data[0];switch (result.reportId) {case 0x11: // 按鈕報告(假設按鈕在字節1-2)for (int byteIdx = 1; byteIdx < 3; byteIdx++) {if (byteIdx >= data.size()) break;unsigned char byte = data[byteIdx];for (int bitIdx = 0; bitIdx < 8; bitIdx++) {if ((byte & (1 << bitIdx)) != 0) { // 1表示按下result.buttons.append((byteIdx - 1) * 8 + bitIdx + 1);}}}break;case 0x10: // **關鍵修改**:坐標/壓力報告ID改為0x10// 解析坐標和壓力(假設坐標在字節1-4,壓力在字節5-6)if (data.size() >= 5) {// 小端序解析:低字節在前,高字節在后result.x = static_cast<quint16>(data[1]) | (static_cast<quint16>(data[2]) << 8);result.y = static_cast<quint16>(data[3]) | (static_cast<quint16>(data[4]) << 8);}if (data.size() >= 7) {result.pressure = static_cast<quint16>(data[5]) | (static_cast<quint16>(data[6]) << 8);}result.buttons.clear(); // 坐標報告不含按鈕,清空列表break;default:qWarning() << "未知報告ID:" << QString::number(result.reportId, 16);break;}return result;
}
?寫一個打印輸出函數
void HidManager::handleHidData(const QByteArray& data)
{// 數據為空或與上次完全相同則直接返回static QByteArray lastDataFrame;if (data.isEmpty() || data == lastDataFrame) return;lastDataFrame = data;// 解析數據到結構體TabletData currentData = parseTabletData(data);// 打印原始數據和解析結果(調試用)if (debugMode) { // 可添加調試開關QString hexData;for (int i = 0; i < data.size(); i++) {hexData += "0x" + QString::number((unsigned char)data[i], 16).toUpper().rightJustified(2, '0') + " ";if ((i+1) % 8 == 0) hexData += "\n";}qDebug() << "收到新數據:" << hexData;qDebug() << "解析后數據:"<< "報告ID:" << QString::number(currentData.reportId, 16)<< "坐標: (" << currentData.x << ", " << currentData.y << ")"<< "壓力:" << currentData.pressure<< "按鈕:" << currentData.buttons;}// 靜態變量存儲上次數據,用于檢測變化static TabletData lastData;// 檢查關鍵數據是否變化(按鈕、坐標、壓力)bool isButtonChanged = (currentData.buttons != lastData.buttons);bool isPositionChanged = (currentData.x != lastData.x || currentData.y != lastData.y);bool isPressureChanged = (currentData.pressure != lastData.pressure);// 根據變化類型發送不同信號if (isButtonChanged) {emit buttonStateChanged(currentData.buttons);}if (isPositionChanged || isPressureChanged) {emit tabletMoved(currentData.x, currentData.y, currentData.pressure);}// 更新上次數據緩存lastData = currentData;
}
打印輸出:
拿wacom數位板舉例。以下是連接wacom數位板之后,數位筆滑動之后wacom數位板發送過來的報告:
解析內容
報告第一個字節為報告ID,用來區分用戶進行的是什么操作。
當報告ID為0X10時代表坐標移動
當報告ID為0X11時代表按鍵按下
例:
按下第一個按鍵,此時報告ID為0x11,表示按鍵事件發生。此時第2個字節發生了變化,也就是第一個字節被按下了:
當用數位筆在數位板上滑動之后收到如下報告:
報告ID為0x10,表示坐標發生變化。坐標在字節1-4,壓力在字節5-6:
上位機向HID設備發送報告
// ================== **發送HID報告(核心功能)** ==================
void HidManager::sendReportInThread(const QByteArray &reportData, bool useFeatureReport) {// 添加設備狀態檢查if(!hidHandle || !hidRunning) {qDebug() << "設備未就緒";return;}// 確保報告長度正確(多數HID需要64字節)QByteArray paddedData = reportData;if(paddedData.size() < 64) {paddedData.resize(64, 0x00);qDebug() << "自動填充報告至64字節";}// 嘗試兩種發送方式int result = -1;if(useFeatureReport) {result = hid_send_feature_report(hidHandle,(uchar*)paddedData.constData(), paddedData.size());} else {// 先嘗試Output報告result = hid_write(hidHandle,(uchar*)paddedData.constData(), paddedData.size());// 失敗后嘗試Feature報告if(result < 0) {qDebug() << "嘗試改用特性報告發送";result = hid_send_feature_report(hidHandle,(uchar*)paddedData.constData(), paddedData.size());}}// 錯誤處理if(result != paddedData.size()) {qDebug() << "發送失敗詳情:";qDebug() << " 請求長度:" << paddedData.size();qDebug() << " 實際發送:" << result;qDebug() << " 最后錯誤:" << QString::fromWCharArray(hid_error(hidHandle));}
}
bool HidManager::sendReport(const QByteArray &reportData, bool useFeatureReport) {qDebug() << "[準備發送] 數據大小:" << reportData.size()<< "使用特性報告:" << useFeatureReport<< "線程狀態:" << (hidThread ? hidThread->isRunning() : false);if (!hidThread || !hidThread->isRunning()) {qDebug() << "[錯誤] HID線程未運行";return false;}// 打印要發送的數據內容QString hexData;for (int i = 0; i < reportData.size(); ++i) {hexData += QString("0x%1 ").arg((uchar)reportData.at(i), 2, 16, QChar('0'));}qDebug() << "[發送數據] " << hexData;QMetaObject::invokeMethod(this, "sendReportInThread", Qt::QueuedConnection,Q_ARG(QByteArray, reportData),Q_ARG(bool, useFeatureReport));return true;
}
主函數調用寫一個測試報告發送:
void MainWindow::InitHid()
{HID = new HidManager(this);HID->hidInit(TARGET_VID, TARGET_PID);// 定義測試報告數據(在lambda外部)auto createTestReport = []() {QByteArray cmd;cmd.append(0x01); // 報告IDcmd.append(0x02); // 保留字節cmd.append(0x55); // 測試模式標識cmd.append(0xAA); // 驗證碼cmd.resize(64, 0x00); // 填充至標準長度return cmd;};// 使用 [this, createTestReport] 捕獲必要的變量QTimer::singleShot(1000, this, [this, createTestReport]() {QByteArray report = createTestReport();qDebug() << "準備發送測試報告,長度:" << report.size();qDebug() << "報告內容:" << report.toHex(' ').toUpper();// 先嘗試Output報告//作用:Output 報告由主機(如電腦)發送到 HID 設備,用于向設備發送命令或數據。例如,向鍵盤發送背光控制命令、向游戲手柄發送振動指令等。if(HID->sendReport(report, false)) {qDebug() << "Output報告發送成功\n";} else {qDebug() << "Output報告發送失敗\n";}QThread::msleep(50);// 再嘗試Feature報告// 作用:Feature 報告用于在主機和設備之間傳輸配置信息或特殊命令。與 Output 報告不同,Feature 報告通常用于獲取或設置設備的持久化配置(如保存設備的校準數據)。if(HID->sendReport(report, true)) {qDebug() << "Feature報告發送成功\n";} else {qDebug() << "Feature報告發送失敗\n";}});
}
這個時候下位機寫一個接受報告,然后再給接收到的數據做一個取反再發回給上位機,上位機如果接收到取反發回的數據則通信成功。
結果如下:?
上位機發送:
?下位機獲取數據進行取反發回,上位機接受取反之后的數據:
至此,上位機與HID設備已經完成了一次通信。?
業務通信
業務需求:
上位機上四個UI按鈕與HID設備4個按鈕映射。
當用戶點擊UI界面某個按鈕時,會彈出該按鈕的對話框,用戶可以在該對話框內設置該按鈕所投射的快捷鍵,同時該映射關系也會被同步到HID設備對應的按鈕上。
場景:
按下按鈕1之后在彈出的快捷鍵按鈕輸入框里按下快捷鍵后,會有一個報告發送給HID設備。
報告如下:
實現:
快捷鍵捕獲:
新建一個類,該類中重寫了eventFilter事件,用來捕獲用戶按下的快捷鍵。
還有在UI顯示用戶按下的多組合快捷鍵這一操作。
#include "shortcutcapturedialog.h"
#include <QLabel>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QKeyEvent>
#include <QDebug>/*** @brief 構造函數實現* @param parent 父窗口指針* @param buttonId 關聯的按鈕ID*/
ShortcutCaptureDialog::ShortcutCaptureDialog(int buttonId,QWidget *parent): QDialog(parent), m_buttonId(buttonId)
{// 設置窗口屬性setWindowTitle(tr("設置快捷鍵")); // 使用tr()支持國際化setFixedSize(300, 180); // 固定對話框大小setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); // 移除幫助按鈕// 創建界面布局QVBoxLayout *layout = new QVBoxLayout(this);layout->setContentsMargins(15, 15, 15, 15); // 設置邊距// 創建提示標簽m_label = new QLabel(tr("為按鈕 %1 設置快捷鍵:").arg(m_buttonId), this);// 創建快捷鍵顯示輸入框m_lineEdit = new QLineEdit(this);m_lineEdit->setReadOnly(true); // 設置為只讀m_lineEdit->setAlignment(Qt::AlignCenter); // 文本居中m_lineEdit->setPlaceholderText(tr("請按下快捷鍵...")); // 提示文本m_lineEdit->installEventFilter(this); // 安裝事件過濾器// 創建按鈕盒(確定/取消)QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);// 將控件添加到布局layout->addWidget(m_label);layout->addWidget(m_lineEdit);layout->addWidget(buttonBox);// 設置對話框布局setLayout(layout);
}
/*** @brief 獲取當前設置的快捷鍵*/
QKeySequence ShortcutCaptureDialog::getShortcut() const
{if (m_keySequence.size() == 1) {return QKeySequence(m_keySequence.first());}else if (m_keySequence.size() > 1) {// 構造多鍵組合的 QKeySequenceQList<int> keys;for (const auto& combo : m_keySequence) {keys.append(combo.toCombined());}return QKeySequence(keys[0], keys.size() > 1 ? keys[1] : 0,keys.size() > 2 ? keys[2] : 0, keys.size() > 3 ? keys[3] : 0);}return QKeySequence();
}/*** @brief 獲取關聯的按鈕ID*/
int ShortcutCaptureDialog::getButtonId() const
{return m_buttonId;
}// QKeySequence ShortcutCaptureDialog::getShortcut() const
// {
// return m_currentShortcut;
// }/*** @brief 事件過濾器實現* @param obj 事件目標對象* @param event 事件對象* @return bool 是否處理該事件*/bool ShortcutCaptureDialog::eventFilter(QObject *obj, QEvent *event) {if (obj == m_lineEdit) {if (event->type() == QEvent::KeyPress) {QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);Qt::Key key = static_cast<Qt::Key>(keyEvent->key());// 更新修飾鍵狀態if (key == Qt::Key_Control || key == Qt::Key_Shift ||key == Qt::Key_Alt || key == Qt::Key_Meta) {m_currentModifiers |= keyEvent->modifiers();return true;}// 開始捕獲if (!m_isCapturing) {m_keySequence.clear();m_isCapturing = true;}// 限制最多3鍵組合if (m_keySequence.size() >= 3) {return true;}// 對于第一個按鍵,記錄完整組合(修飾鍵+按鍵)// 對于后續按鍵,只記錄按鍵本身(不帶修飾鍵)if (m_keySequence.isEmpty()) {m_keySequence.append(QKeyCombination(m_currentModifiers, key));} else {m_keySequence.append(QKeyCombination(Qt::NoModifier, key));}updateShortcutText();return true;}else if (event->type() == QEvent::KeyRelease) {QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);Qt::Key key = static_cast<Qt::Key>(keyEvent->key());// 更新修飾鍵狀態if (key == Qt::Key_Control || key == Qt::Key_Shift ||key == Qt::Key_Alt || key == Qt::Key_Meta) {m_currentModifiers &= ~keyEvent->modifiers();}return true;}}return QDialog::eventFilter(obj, event);
}
// 輔助函數:判斷是否為修飾鍵
bool ShortcutCaptureDialog::isModifierKey(Qt::Key key) {return key == Qt::Key_Shift || key == Qt::Key_Control ||key == Qt::Key_Alt || key == Qt::Key_Meta;
}//快捷鍵文本顯示
void ShortcutCaptureDialog::updateShortcutText() {if (m_keySequence.isEmpty()) {m_lineEdit->setText(tr("請按下快捷鍵..."));return;}QStringList parts;// 處理第一個鍵(帶修飾鍵)if (!m_keySequence.isEmpty()) {parts.append(QKeySequence(m_keySequence.first()).toString(QKeySequence::NativeText));}// 處理后續按鍵(不帶修飾鍵)for (int i = 1; i < m_keySequence.size(); ++i) {parts.append(QKeySequence(m_keySequence[i].key()).toString(QKeySequence::NativeText));}m_lineEdit->setText(parts.join("+"));
}
給四個按鈕綁定槽函數,把自己映射的對應按鈕的按鈕ID發送過去:
connect(btn10, &QPushButton::clicked, this, [this]() {BingShortcutKey(0x01);});connect(btn11, &QPushButton::clicked, this, [this]() {BingShortcutKey(0x02);});connect(btn12, &QPushButton::clicked, this, [this]() {BingShortcutKey(0x04);});connect(btn13, &QPushButton::clicked, this, [this]() {BingShortcutKey(0x08);});
構建映射表
namespace HidKeyCodes {
const quint8 A = 0x04;
.......
const quint8 Z = 0x1D;
// 方向鍵
const quint8 UpArrow = 0x52;
const quint8 DownArrow = 0x53;
const quint8 LeftArrow = 0x50;
const quint8 RightArrow= 0x51;// 常用組合鍵的控制字節(示例)
// Ctrl = LeftCtrl (0x01), Shift = LeftShift (0x02), Alt = LeftAlt (0x04)
const quint8 CtrlMask = LeftCtrl; // Ctrl鍵控制字節
const quint8 ShiftMask = LeftShift; // Shift鍵控制字節
const quint8 AltMask = LeftAlt; // Alt鍵控制字節
const quint8 CtrlShiftMask = CtrlMask | ShiftMask; // Ctrl+Shift組合......
}
?映射
quint8 MainWindow::mapQtKeyToHidKey(Qt::Key key) {// ------------------------- 字母鍵映射(連續范圍) -------------------------// Qt::Key_A到Qt::Key_Z是連續的枚舉值(0x41-0x5A)// HID鍵碼中字母A到Z也是連續的(0x04-0x1D)if (key >= Qt::Key_A && key <= Qt::Key_Z) {return HidKeyCodes::A + (key - Qt::Key_A); // 例如:Qt::Key_B -> 0x04 + (0x42-0x41) = 0x05}// ------------------------- 功能鍵映射(連續范圍) -------------------------// Qt::Key_F1到Qt::Key_F12是連續的枚舉值(0x01000030-0x0100003B)// HID鍵碼中F1到F12也是連續的(0x3A-0x45)else if (key >= Qt::Key_F1 && key <= Qt::Key_F12) {return HidKeyCodes::F1 + (key - Qt::Key_F1); // 例如:Qt::Key_F2 -> 0x3A + (2-1) = 0x3B}// ------------------------- 特殊鍵映射(離散值) -------------------------else {switch (key) {// 常用字母鍵(未包含在連續范圍中的)case Qt::Key_J: return HidKeyCodes::J; // HID: 0x0D (對應USB HID標準中的Key J)case Qt::Key_K: return HidKeyCodes::K; // HID: 0x0E// 特殊功能鍵case Qt::Key_Space: return HidKeyCodes::Space; // HID: 0x2C (空格)// 方向鍵case Qt::Key_Left: return HidKeyCodes::LeftArrow; // HID: 0x50case Qt::Key_Right: return HidKeyCodes::RightArrow; // HID: 0x4Fcase Qt::Key_Up: return HidKeyCodes::UpArrow; // HID: 0x52case Qt::Key_Down: return HidKeyCodes::DownArrow; // HID: 0x51// 其他常用鍵case Qt::Key_Control: return HidKeyCodes::LeftCtrl; // HID: 0xE0 (左Ctrl)case Qt::Key_Shift: return HidKeyCodes::LeftShift; // HID: 0xE1 (左Shift)case Qt::Key_Alt: return HidKeyCodes::LeftAlt; // HID: 0xE2 (左Alt)default:qDebug() << "Unmapped Qt key:" << key; // 調試未映射的鍵return 0x00; // 未知鍵返回0x00(HID協議中表示無按鍵)}}
}
?解析多組合快捷鍵
HidMultiKeyData MainWindow::parseMultiKeyShortcut(const QKeySequence &shortcut) {HidMultiKeyData data; // 用于存儲HID多鍵數據的結構體// 校驗快捷鍵有效性:空序列或超過6個按鍵(HID規范最多支持6個按鍵)if (shortcut.isEmpty() || shortcut.count() > 6) {qWarning() << "Invalid shortcut length:" << shortcut.count(); // 輸出警告信息return data; // 返回空數據}// 遍歷快捷鍵中的每個按鍵組合(最多處理6個)for (int i = 0; i < shortcut.count(); ++i) {int keyCombination = shortcut[i]; // 獲取第i個按鍵的組合值(包含鍵值和修飾鍵)// 拆分按鍵組合:高字節為修飾鍵,低字節為主按鍵// 使用按位與操作分離鍵值和修飾鍵(Qt::KeyboardModifierMask為0xFF000000)Qt::Key key = static_cast<Qt::Key>(keyCombination & ~Qt::KeyboardModifierMask); // 提取主按鍵(清除修飾鍵位)Qt::KeyboardModifiers mods = static_cast<Qt::KeyboardModifiers>(keyCombination & Qt::KeyboardModifierMask); // 提取修飾鍵(僅保留高位修飾鍵位)// ------------------------- 修飾鍵轉換 -------------------------// 將Qt修飾鍵映射到HID鍵碼(僅保留左部修飾鍵,忽略重復類型)// HID規范中每個修飾鍵僅需一個(如LeftCtrl和RightCtrl不同,但此處統一用Left)if (mods & Qt::ControlModifier) data.modifiers |= HidKeyCodes::LeftCtrl; // Ctrl鍵映射為HID左Ctrlif (mods & Qt::ShiftModifier) data.modifiers |= HidKeyCodes::LeftShift; // Shift鍵映射為HID左Shiftif (mods & Qt::AltModifier) data.modifiers |= HidKeyCodes::LeftAlt; // Alt鍵映射為HID左Altif (mods & Qt::MetaModifier) data.modifiers |= HidKeyCodes::LeftMeta; // Meta/Win鍵映射為HID左Meta// ------------------------- 主按鍵轉換 -------------------------quint8 keyCode = mapQtKeyToHidKey(key); // 調用自定義函數將Qt鍵轉換為HID鍵碼// 校驗轉換結果并添加到數組(HID最多支持6個主按鍵)if (keyCode != 0x00 && data.count < 6) { // 0x00表示無效鍵碼data.keyCodes[data.count++] = keyCode; // 存入鍵碼數組,并遞增計數} else {qWarning() << "Unsupported key in multi-key:" << key; // 輸出不支持的鍵警告}}return data; // 返回填充后的HID多鍵數據
}
?構建報告,發送報告
void MainWindow::BingShortcutKey(quint8 buttonId) {if (dialog.exec() != QDialog::Accepted) return;// 解析組合鍵為多鍵數據(支持同時按下Ctrl+J+K)QKeySequence shortcut = dialog.getShortcut();HidMultiKeyData multiKey = parseMultiKeyShortcut(shortcut);if (multiKey.count == 0) {qWarning() << "Invalid multi-key shortcut";return;}// 構建HID報告(一次性發送所有鍵)QByteArray report(64, 0x00);report[0] = 0x01; // 報告IDreport[1] = buttonId; // 按鈕掩碼report[2] = multiKey.modifiers; // 修飾鍵(如Ctrl=0x01)// 填充非修飾鍵(J和K分別在第3、4字節)for (int i = 0; i < multiKey.count; ++i) {report[3 + i] = multiKey.keyCodes[i];}qDebug() << "發送多鍵HID報告:"<< "按鈕:" << Qt::hex << static_cast<int>(buttonId)<< "修飾鍵:" << Qt::hex << static_cast<int>(multiKey.modifiers)<< "鍵值:" << QString::asprintf("0x%02X, 0x%02X",multiKey.keyCodes[0], multiKey.keyCodes[1]);// 發送報告(僅需一次發送,無需延時)if (!HID || !HID->sendReport(report, false)) {qWarning() << "HID多鍵報告發送失敗";}
}