上位机与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.mzph.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 …