由于工作需要,前段时间一直在找关于在C#中如何检测USB设备的资料,其实C#中使用的函数也是引用的操作系统提供的API函数,还不如在C++中写方便,于是自己简单的写了一个检测USB设备插入和拔出的程序。
程序写完之后,将USB光驱、移动硬盘和优盘插入和拔出都是可以检测到的,就是有些内存卡插入的时候检测不到,于是又接着找资料,发现检测读卡器需要另外的方法。在本文中将分为两部分来解释这些是怎么实现的。其实和网上的其他代码示例是差不多的,调用的函数也都是一样的。本文的目的只是将学到的东西记录下来。
1)检测USB光驱、移动硬盘和优盘插入和拔出
在C++窗口类中主要用到了两个函数和处理两个主要消息。
两个函数是RegisterDeviceNotification和UnregisterDeviceNotification。一个消息是指WM_DEVICECHANGE和 DBT_DEVICEREMOVECOMPLETE 。
在MSDN中对WM_DEVICECHANGE的描述是:Notifies an application of a change to the hardware configuration of a device or the computer,翻译过来就是当设备和计算机有硬件配置变化时通知一个应用程序。并不是每个程序天生就可以收到这个消息,程序如果想获得这个消息必须在程序启动的时候调用RegisterDeviceNotification来进行注册,如果函数执行成功,则有硬件变化时操作系统就会发消息通知该程序。程序结束时需要调用UnregisterDeviceNotification函数撤销注册。
RegisterDeviceNotification的函数原型如下:
HDEVNOTIFY WINAPI RegisterDeviceNotification(
_In_ HANDLE hRecipient,
_In_ LPVOID NotificationFilter,
_In_ DWORD Flags
);
其中hRecipient是当前窗口的窗口句柄(如果是服务的话则是服务句柄);
NotificationFilter是通用的数据结构指针,这些数据结构的开头三项都是下面的结构的成员:
typedef struct _DEV_BROADCAST_HDR {
DWORD dbch_size; // 当前结构的总的大小,按字节算
DWORD dbch_devicetype; // 指定哪些设备有变动时需要通知程序,具体类型可以查MSDN,每种类型的
// 数据结构都不一样
DWORD dbch_reserved; // 保留位,没什么用
} DEV_BROADCAST_HDR, *PDEV_BROADCAST_HDR;
Flags是个标志,用于指定第一个参数的类型,它的取值有DEVICE_NOTIFY_WINDOW_HANDLE(句柄是窗口句柄)、DEVICE_NOTIFY_SERVICE_HANDLE(句柄是服务句柄),如果第二个参数中的dbch_devicetype类型指定成DBT_DEVTYP_DEVICEINTERFACE,则Flags还可以取DEVICE_NOTIFY_ALL_INTERFACE_CLASSES,这个标志的意思是不需要过滤消息,所有的设备变动都通知程序。
我们在例子中设置 dbch_devicetype的值为 DBT_DEVTYP_DEVICEINTERFACE。
如果调用 RegisterDeviceNotification函数成功,则函数返回一个设备通知句柄,当程序结束时将该句柄传递给 UnregisterDeviceNotification函数用于撤销通知。
接下来就是处理 WM_DEVICECHANGE消息,在C++中已经有封装好的映射函数用于处理这个消息,只需要在 BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间添加一个ON_WM_DEVICECHANGE()就可以了,剩下的就是在窗口类的头文件中添加afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD_PTR dwData),并实现这个函数就可以了。
这个函数的参数意义如下:
DN,我们的程序中主
要使用DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE,前一个事件发生在设备插入后,后一个事件发生在设备拔出后。
第二个参数dwData是一个通用数据结构指针,这些结构都是以 DEV_BROADCAST_HDR结构开始的,在这个结构的第二项中 dbch_devicetype 指定是什么类型的设备,每种类型的数据结构都不一样,只有确定了具体的设备才能确定 dwData指向的 具体的数据类型。本程序中我们只针对 DBT_DEVTYP_VOLUME(设备插入后会才操作系统中出现盘符)类型,该类型设备对应的数据结构如下:
typedef struct _DEV_BROADCAST_VOLUME {
DWORD dbcv_size;
DWORD dbcv_devicetype;
DWORD dbcv_reserved;
DWORD dbcv_unitmask;
WORD dbcv_flags;
} DEV_BROADCAST_VOLUME, *PDEV_BROADCAST_VOLUME;
这个结构中可以从
dbcv_unitmask中获取设备对应的逻辑盘符,从
dbcv_flags中获取设备的大致类型,比如U盘插入的时候
dbcv_flags为0,光盘为1,
dbcv_flags值为2时则是与网络卷标相关。
DBT_DEVICEREMOVECOMPLETE与DBT_DEVICEARRIVAL类似,就不介绍了,MSDN中有示例程序。
2)检测存储卡插入和拔出
最初的程序写完之后,用优盘、移动硬盘做试验都没有问题,但是当试验读卡器和存储卡时出现了问题,这里我用的读卡器是插入读卡器后即使不插入存储卡,操作系统中也会出现逻辑盘符,只是不可用,当读卡器的某一个接口中插入存储卡后,这个接口对应的盘符变得可用,但是这时候程序也收不到DBT_DEVICEARRIVAL消息了,也就不知道存储卡是否插进来(拔出好像可以检测到)。
为此又是到网上检索资料,最后找到的方法是也是两个函数和两个消息:
两个函数是SHChangeNotifyRegister和SHChangeNotifyDeregister,两个消息是SHCNE_MEDIAINSERTED和SHCNE_MEDIAREMOVED。
首先来看 SHChangeNotifyRegister,它的函数原型如下:
ULONG SHChangeNotifyRegister(
_In_ HWND hwnd,
int fSources,
LONG fEvents,
UINT wMsg,
int cEntries,
_In_ const SHChangeNotifyEntry *pshcne
);
其中hwnd是当前窗口程序句柄;
fSources指定当前窗口需要获得哪些类型的消息通知,有很多种消息类型,我们需要的是SHCNE_DISKEVENTS;
fEvents指定需要获取的具体的消息,上一个参数仅是一个大类,这个参数就是来指定子类消息;
wMsg是一个自定义消息,当操作系统有我们需要的消息发生时,操作系统会发送wMsg指定的消息来通知我们的程序,这里定义我们的消息为WM_USER_MEDIACHANGED;
cEntries是说明下一个参数,也就是一个数组的元素个数,这个值MSDN中说必须要设置成1;
pshcne这个结构我也不太清楚,直接是抄的网上的代码。
这个函数调用成功后会返回一个句柄,调用函数 SHChangeNotifyDeregister会用到 该句柄。
接下来就是处理 WM_USER_MEDIACHANGED消息,需要在 BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间添加一个
ON_MESSAGE(WM_USER_MEDIACHANGED,&CGetDeviceInfoDlg::OnMediaChanged) 就可以了,剩下的就是在窗口类的头文件中添加afx_msg LRESULT OnMediaChanged(WPARAM,LPARAM),并实现这个函数就可以了。
OnMediaChanged函数的第二个参数指定了Windows消息类型,也就是对应的 SHChangeNotifyRegister函数中的fEvents指定的消息类型。由于我们仅仅是需要知道是否有设备插入或者拔出,所以只对 LPARAM进行判断就可以了,(第一个参数 WPARAM 是SHNOTIFYSTRUCT指针,具体意义可以查询MSDN ):
LRESULT CGetDeviceInfoDlg::OnMediaChanged(WPARAM wParam, LPARAM lParam)
{
SHNOTIFYSTRUCT *shns=(SHNOTIFYSTRUCT*)wParam;
CString strPath,strMsg;
switch (lParam)
{
case SHCNE_MEDIAINSERTED:
this->SetDlgItemText(IDC_EDIT_SOURCE,"SHCNE_MEDIAINSERTED");
strPath =GetPathFromPIDL(shns->dwItem1);
if (!strPath.IsEmpty())
{
strMsg.Format("Media inserted into %s",strPath);
this->SetDlgItemText(IDC_EDIT_MSG,strMsg);
}
break;
case SHCNE_MEDIAREMOVED:
this->SetDlgItemText(IDC_EDIT_SOURCE,"SHCNE_MEDIAREMOVED");
strPath=GetPathFromPIDL(shns->dwItem1);
if (!strPath.IsEmpty())
{
strMsg.Format("Media removed from %s",strPath);
this->SetDlgItemText(IDC_EDIT_MSG,strMsg);
}
break;
}
return NULL;
}
3)一些说明
上述例子都是在窗口程序中实现的,除了在窗口程序中获取设备插入、拔出信息,还可以在windows服务中获取这些信息,但是在windows服务中并不能获取到设备对应的逻辑盘符,查询资料知道原来逻辑盘符是用户相关的,一个设备插入后对应的逻辑盘符因用户不同而可能不同,而服务程序与用户无关,所以不能再服务中获取设备的逻辑盘符。
提供的程序是一个比较简陋的C++程序,仅仅是显示设备是插入还是拔出,但是示意效果已经达到了,如果想获取更好的程序,可以到codeproject网站上按照 RegisterDeviceNotification或者 SHChangeNotifyRegister来查找更好的程序。