由于工作需要,前段时间一直在找关于在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来查找更好的程序。