前置知识
什么是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多键报告发送失败";}
}