一般 Qt 在 Windows 上做设备热插拔需要两个步骤:
1.使用 win32 的 RegisterDeviceNotification 函数注册要监听的设备类别,需要绑定一个窗口 id;
2.重写 QWidget 的 nativeEvent 虚函数或者 QAbstractNativeEventFilter 的 nativeEventFilter 虚函数,处理热插拔相关的回调。
后来参考别人的代码,可以创建一个 win32 的隐藏窗口来接收消息,这样就不用耦合到 Qt 界面上的窗口了。
(2023-03-21 补充)
如果用通用的 USB GUID GUID_DEVINTERFACE_USB_DEVICE 来注册,插入 UVC 相机并触发插入消息后,去枚举相机列表,此时可能还枚举不到这个设备,如果注册的 KSCATEGORY_CAPTURE 就能检测到插入的 UVC 相机。
GUID_DEVINTERFACE_USB_DEVICE -- usbiodef.h
GUID{ 0xA5DCBF10L, 0x6530, 0x11D2, { 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } }
KSCATEGORY_CAPTURE -- Ks.h
GUID{ 0x65E8773DL, 0x8F56, 0x11D0, { 0xA3, 0xB9, 0x00, 0xA0, 0xC9, 0x22, 0x31, 0x96 } }
代码链接:
https://github.com/gongjianbo/MyTestCode/tree/master/Qt/DeviceHotplug_Win
主要代码:
main.cpp
#include <QApplication>
#include <QMainWindow>
#include <QDebug>
#include "DeviceHotplug.h"
#include <Windows.h>
#include <Dbt.h>
#include <devguid.h>
// 具体的设备 GUID 需要 initguid, 如 usbiodef
#include <initguid.h>
// USB 设备
// GUID_DEVINTERFACE_USB_DEVICE
#include <usbiodef.h>
// HID 人机交互设备-鼠标键盘等
#include <hidclass.h>
// 键盘 GUID_DEVINTERFACE_KEYBOARD
#include <ntddkbd.h>
// 鼠标 GUID_DEVINTERFACE_MOUSE
#include <ntddmou.h>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
DeviceHotplug filter;
QVector<QUuid> uuids;
// 监测 USB 插拔
uuids << GUID_DEVINTERFACE_USB_DEVICE;
// 监测鼠标键盘插拔,有些有扩展功能的鼠标会同时触发键盘和鼠标的插拔事件
// uuids << GUID_DEVINTERFACE_KEYBOARD << GUID_DEVINTERFACE_MOUSE;
filter.init(uuids);
QObject::connect(&filter, &DeviceHotplug::deviceAttached,
&app, [](quint16 vid, quint16 pid){
qDebug()<<"attached"<<QString::number(vid, 16)<<QString::number(pid, 16);
});
QObject::connect(&filter, &DeviceHotplug::deviceDetached,
&app, [](quint16 vid, quint16 pid){
qDebug()<<"detached"<<QString::number(vid, 16)<<QString::number(pid, 16);
});
QMainWindow w;
w.resize(600, 500);
w.setWindowTitle("DeviceHotplug (by GongJianBo 1992)");
w.show();
return app.exec();
}
DeviceEventFilter.h
扫描二维码关注公众号,回复:
14846581 查看本文章
#pragma once
#include <QObject>
#include <QUuid>
#include <QSharedPointer>
#include <Windows.h>
class DeviceHotplugPrivate;
/**
* @brief 设备插拔事件监听
* @author 龚建波
* @date 2022-12-24
* @details
* 设备类文档,可以在设备管理器找自己的设备 GUID
* https://learn.microsoft.com/zh-cn/windows-hardware/drivers/install/overview-of-device-setup-classes
* @history
* 2023-03-20
* 参考 https://github.com/wang-bin/qdevicewatcher
* 由 QAbstractNativeEventFilter 过滤事件改为了创建一个 win32 窗口来接收事件
*/
class DeviceHotplug : public QObject
{
Q_OBJECT
public:
explicit DeviceHotplug(QObject *parent = nullptr);
~DeviceHotplug();
// RegisterDeviceNotification 注册对应的 GUID 消息通知
// 暂未考虑重复注册和注册失败的处理
void init(const QVector<QUuid> &uuids);
// UnregisterDeviceNotification
// 会在析构中自动调用一次
void free();
signals:
// 设备插入
void deviceAttached(quint16 vid, quint16 pid);
// 设备拔出
void deviceDetached(quint16 vid, quint16 pid);
private:
QSharedPointer<DeviceHotplugPrivate> dptr;
};
DeviceEventFilter.cpp
#include "DeviceHotplug.h"
#include <QMetaObject>
#include <QHash>
#include <QDebug>
#include <Dbt.h>
#pragma comment(lib, "user32.lib")
// 内部结构,存储对应平台需要的数据
class DeviceHotplugPrivate
{
public:
void deviceAttached(quint16 vid, quint16 pid) {
QMetaObject::invokeMethod(ptr, "deviceAttached", Qt::QueuedConnection,
Q_ARG(quint16, vid),
Q_ARG(quint16, pid));
}
void deviceDetached(quint16 vid, quint16 pid) {
QMetaObject::invokeMethod(ptr, "deviceDetached", Qt::QueuedConnection,
Q_ARG(quint16, vid),
Q_ARG(quint16, pid));
}
// 处理窗口消息
static LRESULT CALLBACK windowMessageProcess(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
// win32 窗口类名
static QString windowClassName();
// 创建一个用于接收消息的窗口,注册消息回调
bool createMessageWindow(const QVector<QUuid> &uuids);
// 释放
void destroyMessageWindow();
// 关联的对象
DeviceHotplug *ptr{nullptr};
// 关联的窗口
HWND hwnd{nullptr};
// 设备通知句柄和 UUID/GUID
QHash<QUuid, HDEVNOTIFY> devNotifys;
};
// 处理窗口消息
LRESULT CALLBACK DeviceHotplugPrivate::windowMessageProcess(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_DEVICECHANGE) {
do {
// 设备可用事件
const bool is_add = wParam == DBT_DEVICEARRIVAL;
// 设备移除事件
const bool is_remove = wParam == DBT_DEVICEREMOVECOMPLETE;
if (!is_add && !is_remove)
break;
// 过滤 device interface class 以外类型的消息
DEV_BROADCAST_HDR *broadcast = reinterpret_cast<DEV_BROADCAST_HDR *>(lParam);
if (!broadcast || broadcast->dbch_devicetype != DBT_DEVTYP_DEVICEINTERFACE)
break;
// 获取 SetWindowLongPtrW 设置的对象
DeviceHotplugPrivate *data = reinterpret_cast<DeviceHotplugPrivate *>(::GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (!data)
break;
// 过滤不监听的设备类型
DEV_BROADCAST_DEVICEINTERFACE *device_interface = reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE *>(broadcast);
QUuid uid(device_interface->dbcc_classguid);
if (!data->devNotifys.contains(uid))
break;
QString device_name;
if (device_interface->dbcc_name) {
#ifdef UNICODE
device_name = QString::fromWCharArray(device_interface->dbcc_name);
#else
device_name = QString(device_interface->dbcc_name);
#endif
}
// 从设备描述中获取 vid 和 pid
int offset = -1;
quint16 vid = 0;
quint16 pid = 0;
offset = device_name.indexOf("VID_");
if (offset > 0 && offset + 8 <= device_name.size()) {
vid = device_name.mid(offset + 4, 4).toUShort(nullptr, 16);
}
offset = device_name.indexOf("PID_");
if (offset > 0 && offset + 8 <= device_name.size()) {
pid = device_name.mid(offset + 4, 4).toUShort(nullptr, 16);
}
if (is_add) {
fprintf(stderr, "device attached: vid 0x%04x, pid 0x%04x.\n", vid, pid);
data->deviceAttached(vid, pid);
} else if (is_remove) {
fprintf(stderr, "device detached: vid 0x%04x, pid 0x%04x.\n", vid, pid);
data->deviceDetached(vid, pid);
}
} while(false);
}
return ::DefWindowProcW(hwnd, message, wParam, lParam);
}
// 窗口类名
QString DeviceHotplugPrivate::windowClassName()
{
return QLatin1String("Qt_DeviceHotplug_Window_") + QString::number(quintptr(windowMessageProcess));
}
// 创建一个用于接收消息的窗口,注册消息回调
bool DeviceHotplugPrivate::createMessageWindow(const QVector<QUuid> &uuids)
{
QString class_name = windowClassName();
HINSTANCE hi = ::GetModuleHandleW(nullptr);
WNDCLASSW wc;
memset(&wc, 0, sizeof(WNDCLASSW));
wc.lpfnWndProc = windowMessageProcess;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hi;
wc.lpszClassName = reinterpret_cast<const wchar_t *>(class_name.utf16());
::RegisterClassW(&wc);
hwnd = ::CreateWindowW(wc.lpszClassName, // classname
wc.lpszClassName, // window name
0, // style
0, // x
0, // y
0, // width
0, // height
0, // parent
0, // menu handle
hi, // application
0); // windows creation data.
if (!hwnd) {
qDebug()<<"createMessageWindow error"<<(int)GetLastError();
} else {
// 初始化 DEV_BROADCAST_DEVICEINTERFACE 数据结构
DEV_BROADCAST_DEVICEINTERFACE_W filter_data;
memset(&filter_data, 0, sizeof(DEV_BROADCAST_DEVICEINTERFACE_W));
filter_data.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE_W);
filter_data.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
for (auto &&uuid : uuids)
{
filter_data.dbcc_classguid = uuid;
HDEVNOTIFY handle = ::RegisterDeviceNotificationW(hwnd, &filter_data, DEVICE_NOTIFY_WINDOW_HANDLE);
if (handle) {
devNotifys.insert(uuid, handle);
} else {
qDebug()<<"RegisterDeviceNotification error"<<uuid;
}
}
::SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)this);
}
return !!hwnd;
}
// 释放
void DeviceHotplugPrivate::destroyMessageWindow()
{
if (hwnd) {
::DestroyWindow(hwnd);
hwnd = nullptr;
for (HDEVNOTIFY handle : qAsConst(devNotifys))
{
::UnregisterDeviceNotification(handle);
}
devNotifys.clear();
}
::UnregisterClassW(reinterpret_cast<const wchar_t *>(windowClassName().utf16()), ::GetModuleHandleW(nullptr));
}
DeviceHotplug::DeviceHotplug(QObject *parent)
: QObject{parent}
, dptr{new DeviceHotplugPrivate}
{
dptr->ptr = this;
}
DeviceHotplug::~DeviceHotplug()
{
free();
}
void DeviceHotplug::init(const QVector<QUuid> &uuids)
{
const bool ret = dptr->createMessageWindow( uuids);
if (!ret) {
dptr->destroyMessageWindow();
}
}
void DeviceHotplug::free()
{
dptr->destroyMessageWindow();
}