USB HID学习:MFC检测USB拔插事件

MFC具备检测设备的消息,但需要手动添加。针对USB设备,需要注册对应的GUID方可。本文对此进行简单记录。
本省略对MFC机制的描述,仅描述主要的模块代码。

一、步骤

Dbt.h头文件引用

在stdafx.h(或有关的头文件)添加Dbt.h头文件的引用:

1
#include <Dbt.h>

注册USB设备GUID

在对话框初始化函数中注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BOOL CFooDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 将“关于...”菜单项添加到系统菜单中。

// ...

// 注册HID事件
DEV_BROADCAST_DEVICEINTERFACE DevBroadcastDeviceInterface;

memset(&DevBroadcastDeviceInterface, 0, sizeof(DEV_BROADCAST_DEVICEINTERFACE));
DevBroadcastDeviceInterface.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
DevBroadcastDeviceInterface.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
// HID设备的GUID,可在设备管理器中查询,经查结果如下:
// {745a17a0-74d3-11d0-b6fe-00a0c90f57da}
// 注:使用真实的HID的GUID,反正检测不出来,如果是其它的GUID,所有USB事件都能检测出
const GUID GUID_DEVINTERFACE_LIST[] = {
{ 0xA5DCBF10, 0x6530, 0x11D2,{ 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } }, // USB设备
{ 0x53f56307, 0xb6bf, 0x11d0,{ 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } }, // 磁盘(U盘)
{ 0x4D1E55B2, 0xF16F, 0x11CF,{ 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }, // HID
{ 0x745A17A0, 0x74D3, 0x11D0,{ 0xB6, 0xFE, 0x00, 0xA0, 0xC9, 0x0F, 0x57, 0xDA } }, // 另一个HID
{ 0xad498944, 0x762f, 0x11d0,{ 0x8d, 0xcb, 0x00, 0xc0, 0x4f, 0xc3, 0x35, 0x8c } } }; // 网卡
// 可以循环注册所有列出的GUID,此处只使用一种
DevBroadcastDeviceInterface.dbcc_classguid = GUID_DEVINTERFACE_LIST[2];

RegisterDeviceNotification(this->GetSafeHwnd(), &DevBroadcastDeviceInterface, DEVICE_NOTIFY_WINDOW_HANDLE);

}

说明1:不同的USB设备使用不同的GUID表示。在注册时需要指定要检测哪一类,本文针对HID,有兴趣者可使用其它来测试。
说明2:笔者使用的键盘有多个USB设备,其一为HID设备,在设备管理器中查询其类GUID为745a17a0-74d3-11d0-b6fe-00a0c90f57da
说明3:查询到的GUID与代码GUID结构体本质一样,形式不同。具体参考定义。

消息函数声明

在对话框头文件声明消息函数:

1
afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD dwData);

消息声明

在对话框实现文件中添加ON_WM_DEVICECHANGE消息:

1
2
3
4
5
6
7
8
9
10
BEGIN_MESSAGE_MAP(CFooDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_MESSAGE(WM_SHOWTASK, OnSystemtray)
// ...
ON_WM_SIZE()
ON_WM_DESTROY()
ON_WM_DEVICECHANGE() // USB HID设备检测消息
END_MESSAGE_MAP()

消息响应函数实现

下面实现OnDeviceChange函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
BOOL CFooDlg::OnDeviceChange(UINT nEventType, DWORD dwData)
{
DEV_BROADCAST_DEVICEINTERFACE* dbd = (DEV_BROADCAST_DEVICEINTERFACE*)dwData;

wchar_t vid[32] = { 0 };
int offset = 4 * 3 + sizeof(GUID)+10;
CString szInfo;
int sendtype = 0;
switch (nEventType)
{
case DBT_DEVICEARRIVAL:
{
memcpy(vid, (char*)dwData + offset, 32);
wchar_t* rr = wcsstr(vid, L"VID_AA55"); // !! 可过滤特定设备ID,下同
if (rr == NULL)
{
return FALSE;
}

szInfo.Format(L"提示信息: 设备已插入.\n");

this->GetDlgItem(IDC_STC_DEVINFO)->SetWindowText(szInfo);

}
break;
case DBT_DEVICEREMOVECOMPLETE:
{
// 注:dbd->dbcc_name只有1个字节,不能直接用其来做源地址拷贝,直接使用偏移,上同
//wmemcpy(vid, (wchar_t*)dwData + offset, 32);
memcpy(vid, (char*)dwData + offset, 32);
wchar_t* rr = wcsstr(vid, L"VID_AA55");
if (rr == NULL)
{
return FALSE;
}

szInfo.Format(L"提示信息: 设备已移除.\n");

this->GetDlgItem(IDC_STC_DEVINFO)->SetWindowText(szInfo);

}
break;

default:
{
//szInfo.Format(L"[%d]got event: %d\n", cnt, nEventType);
//this->GetDlgItem(IDC_STC_DEVINFO)->SetWindowText(szInfo);
}
break;
}

return TRUE;
}

注1:只有注册的设备,nEventType才有DBT_DEVICEARRIVAL、DBT_DEVICEREMOVECOMPLETE(当然也有其它值,按下不提),如果不注册,nEventType的值为7。
注2:查了些资料,说nEventType值不同,dwData亦不同。但本文没有深入研究。
注3:如果要针对某一种设备,如所属为HID,但厂家不同,则可以通过查找VID关键字来过滤。文中代码使用偏移量外加字符串搜索来实现,仅作示例,有些绕,但能实现功能。

后在 Windows 7 系统上用 VS 2015 编译,发现 ON_WM_DEVICECHANGE 出错,经排查,定位到

1
afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD dwData);

声明不兼容。修改为:

1
afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD_PTR dwData);

可解决编译问题,并且在 Win10 也能正常编译。

二、测试

使用

1
0xA5DCBF10, 0x6530, 0x11D2,{ 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED }

可以检测出所有的USB设备事件。包括U盘、键盘等。
使用

1
0x53f56307, 0xb6bf, 0x11d0,{ 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b }

只能检测出U盘事件。
使用

1
0x745A17A0, 0x74D3, 0x11D0,{ 0xB6, 0xFE, 0x00, 0xA0, 0xC9, 0x0F, 0x57, 0xDA }

检测不出HID事件(此处原因未知)。
但是,使用

1
0x4D1E55B2, 0xF16F, 0x11CF,{ 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }

可以检测出HID事件。

三、小结

本文不过多涉及检测原理,代码测试通过。
需要指出的是,在 Windows 上使用 Qt 编程检测 USB 事件,也是使用本文所提到的技术,包括注册、响应事件。毕竟,无论 MFC 还是 Qt 程序,都是在 Windows 上运行的,可谓殊途同归。当然,如 Linux 或 MacOS 系统,机制已然不同,不在此列。