上位機與Hid設備通信

前置知識

什么是HID?

HID(Human Interface Device)是?直接與人交互的電子設備?,通過標準化協議實現用戶與計算機或其他設備的通信,典型代表包括鍵盤、鼠標、游戲手柄等。?

為什么HID要與qt進行通信?

我這里的應用場景是數位板與我自己寫的上位機進行通信,用戶可以在上位機軟件中手動設置數位板上按鍵代表的快捷鍵。

如何知道當前HID設備的VID,PID?

  1. 打開?設備管理器,找到您的設備(通常在?人體學輸入設備?或?通用串行總線設備?類別下)。
  2. 右鍵點擊設備 ->?屬性?-> 事件選項卡

如何通信?

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為未激活狀態(初始狀態)。

    總結操作流程

    1. 確認設備功能與協議:明確設備是輸入型(主動上報)還是命令型(需指令觸發)。
    2. 發送測試指令:若無文檔,通過簡單指令試探設備響應模式。
    3. 解析數據結構:根據響應數據的變化規律,逆向推導字節含義(如坐標、狀態、校驗等)。
    4. 編寫業務邏輯:基于解析結果,實現數據處理或控制功能(如鼠標模擬、設備配置等)。

    解析報告數據

    如何解析

    用以下結構來存儲報告:

    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多鍵報告發送失敗";}
    }
    

    HID設備按鍵與Qt界面UI按鍵的快捷鍵綁定實現

    流程圖

    本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
    如若轉載,請注明出處:http://www.pswp.cn/web/80678.shtml
    繁體地址,請注明出處:http://hk.pswp.cn/web/80678.shtml
    英文地址,請注明出處:http://en.pswp.cn/web/80678.shtml

    如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

    相關文章

    JVM 工具實戰指南(jmap / jstack / Arthas / MAT)

    &#x1f50d; 從診斷到定位&#xff1a;掌握生產級 JVM 排查工具鏈 &#x1f4d6; 前言&#xff1a;系統故障時&#xff0c;如何快速定位&#xff1f; 無論 JVM 理論多么扎實&#xff0c;當線上服務出現 CPU 飆高、響應超時、內存泄漏或頻繁 Full GC 時&#xff0c;僅靠猜測…

    mac上安裝 Rust 開發環境

    1.你可以按照提示在終端中執行以下命令&#xff08;安全、官方支持&#xff09;&#xff1a; curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh然后按提示繼續安裝即可。 注意&#xff1a;安裝過程中建議選擇默認配置&#xff08;按 1 即可&#xff09;。 如果遇…

    C++(5)switch語句 循環while

    這是一個電影評分的程序 default 就是 如果上述的都沒有執行 就統一的執行default的內容。 然后記得break ___________________________________ 循環 &#xff08;while&#xff09; while的使用方式 輸出 0-9的while循環

    [Linux] Linux線程信號的原理與應用

    Linux線程信號的原理與應用 文章目錄 Linux線程信號的原理與應用**關鍵詞****第一章 理論綜述****第二章 研究方法**1. **實驗設計**1.1 構建多線程測試環境1.2 信號掩碼策略對比實驗 2. **數據來源**2.1 內核源碼分析2.2 用戶態API調用日志與性能監控 **第三章 Linux信號的用法…

    25.5.20學習總結

    做題思路 數列分段 Section IIhttps://www.luogu.com.cn/problem/P1182正如題目所說&#xff0c;我們需要得到一個最小的最大段的值&#xff0c;可能有人將注意力放在分段上&#xff0c;事實上&#xff0c;我們更多的應該關注結果。這是一道二分答案的題&#xff0c;你可以先確…

    Python爬蟲-爬取百度指數之人群興趣分布數據,進行數據分析

    前言 本文是該專欄的第56篇,后面會持續分享python爬蟲干貨知識,記得關注。 在本專欄之前的文章《Python爬蟲-爬取百度指數之需求圖譜近一年數據》中,筆者有詳細介紹過爬取需求圖譜的數據教程。 而本文,筆者將再以百度指數為例子,基于Python爬蟲獲取指定關鍵詞的人群“興…

    【工具使用】STM32CubeMX-USB配置-實現U盤功能

    一、概述 無論是新手還是大佬&#xff0c;基于STM32單片機的開發&#xff0c;使用STM32CubeMX都是可以極大提升開發效率的&#xff0c;并且其界面化的開發&#xff0c;也大大降低了新手對STM32單片機的開發門檻。 ????本文主要講述STM32芯片USB功能的配置及其相關知識。 二…

    從ISO17025合規到信創適配 解密質檢lims系統實驗室的 AI 質檢全鏈路實踐

    在北京某國家級質檢中心的 CMA 復評審現場&#xff0c;審核專家通過系統后臺調取近半年的檢測記錄&#xff0c;從樣品登記時的電子簽名到報告簽發的 CA 簽章&#xff0c;178 項合規指標全部自動校驗通過 —— 這是白碼質檢 LIMS 系統創造的合規奇跡。 一、智能合規引擎&#xf…

    【操作系統】進程同步問題——生產者-消費者問題

    問題描述 生產者進程負責生產產品&#xff0c;并將產品存入緩沖池&#xff0c;消費者進程則從緩沖池中取出產品進行消費。為實現生產者和消費者的并發執行&#xff0c;系統在兩者之間設置了一個包含n個緩沖區的緩沖池。生產者將產品放入緩沖區&#xff0c;消費者則從緩沖區中取…

    SpringBoot-6-在IDEA中配置SpringBoot的Web開發測試環境

    文章目錄 1 環境配置1.1 JDK1.2 Maven安裝配置1.2.1 安裝1.2.2 配置1.3 Tomcat1.4 IDEA項目配置1.4.1 配置maven1.4.2 配置File Encodings1.4.3 配置Java Compiler1.4.4 配置Tomcat插件2 Web開發環境2.1 項目的POM文件2.2 項目的主啟動類2.3 打包為jar或war2.4 訪問測試3 附錄3…

    Vue3 父子組件傳值, 跨組件傳值,傳函數

    目錄 1.父組件向子組件傳值 1.1 步驟 1.2 格式 2. 子組件向父組件傳值 1.1 步驟 1.2 格式 3. 跨組件傳值 運行 4. 跨組件傳函數 ?5. 總結 1. 父傳子 2. 子傳父 3. 跨組件傳值(函數) 1.父組件向子組件傳值 1.1 步驟 在父組件中引入子組件 在子組件標簽中自定義屬…

    嵌入式學習筆記 - STM32 U(S)ART 模塊HAL 庫函數總結

    一 串口發送方式&#xff1a; ①輪訓方式發送&#xff0c;也就是主動發送&#xff0c;這個容易理解&#xff0c;使用如下函數&#xff1a; HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout); ②中斷方式發送&#xff…

    AI無法解決的Bug系列(一)跨時區日期過濾問題

    跨時區開發中&#xff0c;React Native如何處理新西蘭的日期過濾問題 有些Bug&#xff0c;不是你寫錯代碼&#xff0c;而是現實太魔幻。 比如我最近給新西蘭客戶開發一個React Native應用&#xff0c;功能非常樸素&#xff1a;用戶選一個日期范圍&#xff0c;系統返回該范圍內…

    基于天貓 API 的高效商品詳情頁實時數據接入方法解析

    一、引言 在電商大數據分析、競品監控及智能選品等場景中&#xff0c;實時獲取天貓商品詳情頁數據是關鍵需求。本文將詳細解析通過天貓開放平臺 API 高效接入商品詳情數據的技術方案&#xff0c;涵蓋接口申請、數據獲取邏輯及代碼實現&#xff0c;幫助開發者快速構建實時數據采…

    系分論文《論遺產系統演化》

    系統分析師論文范文系列 摘要 2022年6月,某金融機構啟動核心業務系統的技術升級項目,旨在對其運行超過十年的遺留系統進行演化改造。該系統承擔著賬戶管理、支付結算等關鍵業務功能,但其技術架構陳舊、擴展性不足,難以適應數字化轉型與業務快速增長的需求。作為系統分析師,…

    Spark Core基礎與源碼剖析全景手冊

    Spark Core基礎與源碼剖析全景手冊 Spark作為大數據領域的明星計算引擎&#xff0c;其核心原理、源碼實現與調優方法一直是面試和實戰中的高頻考點。本文將系統梳理Spark Core與Hadoop生態的關系、經典案例、聚合與分區優化、算子底層原理、集群架構和源碼剖析&#xff0c;結合…

    人工智能賦能產業升級:AI在智能制造、智慧城市等領域的應用實踐

    人工智能賦能產業升級&#xff1a;AI在智能制造、智慧城市等領域的應用實踐 近年來&#xff0c;人工智能&#xff08;AI&#xff09;技術的快速發展為各行各業帶來了深刻的變革。無論是制造業、城市管理&#xff0c;還是交通、醫療等領域&#xff0c;AI技術都展現出了強大的應用…

    React Native打包報錯: Task :react-native-picker:verifyReleaseResources FAILE

    RN打包報錯&#xff1a; Task :react-native-picker:verifyReleaseResources FAILED Execution failed for task :react-native-picker:verifyReleaseResources. 解決方法&#xff1a; 修改文件react-native-picker中的版本信息。 路徑&#xff1a;node_modules/react-native-p…

    虛擬網絡編輯器

    vmnet1 僅主機模式 hostonly 功能&#xff1a;虛擬機只能和宿主機通過vmnet1通信&#xff0c;不可連接其他網絡&#xff08;包括互聯網&#xff09; vmnet8 地址轉換模式 NAT 功能&#xff1a;虛擬機可以和宿主通過vmnet8通信&#xff0c;并且可以連接其他網絡&#xff0c;但是…

    docker環境和dockerfile制作

    docker 一、環境和安裝 1、 docker安裝 使用 root 權限登錄 CentOS。確保 yum 包更新到最新sudo yum update卸載舊版本yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-selinux …