借着学习USB的风,闲暇之余写了个小程序——枚举系统设备,用vs2013 Qt5.6.0来实现设备管理器。
外观上来说,设备管理器提供计算机上所安装硬件的图形视图。所以本节要设计的设备管理器只是实现:显示计算机上所安装硬件的视图软件。
☆ START ☆
开发环境和UI设计
开发环境:vs2013 Qt5.6.0
UI设计:
UI很简单,主要由QTreeView和QTextEdit两个控件组成:
- QTreeView:以树形列表的形式按照设备类型枚举出计算机上所连接的各类设备
- QTextEdit:当鼠标点击到设备列表上的某个设备时,该控件打印出该设备的具体信息,比如GUID、PID、VID等。
主要API
Windows API
Windows API主要用于获取指定设备类的指定属性。
SetupDiGetClassDevs
SetupDiGetClassDevs函数返回一个包含本机上所有被请求的设备信息的设备信息集合句柄。
代码语言:javascript复制HDEVINFO SetupDiGetClassDevs(
_In_opt_ const GUID *ClassGuid, // 一个指向GUID的指针,此GUID可标识一个设备安装类或一个设备接口类, 可以为NULL
_In_opt_ PCTSTR Enumerator, // 一个指向以空字符结束的字符串的指针
_In_opt_ HWND hwndParent, // 用于与在设备信息集中安装设备实例相关联的用户界面的顶级窗口句柄
_In_ DWORD Flags // 通过此参数来过滤指定的设备信息集中的设备, DIGCF_PRESENT表示只返回当前系统中存在的(已连接)设备。
)
该函数的第一个入口参数GUID指定了我们想要检索什么类型的设备,它的取值可以在devguid文件中查找,这里不一一列举。本例中会只用到以下GUID,它们的含义如下:
代码语言:javascript复制GUID_DEVCLASS_SYSTEM // 系统设备GUID
GUID_DEVCLASS_USB // USB设备GUID
GUID_DEVCLASS_MOUSE // 鼠标设备GUID
GUID_DEVCLASS_NET // 网络设备GUID
GUID_DEVCLASS_KEYBOARD // 键盘设备GUID
当调用完此函数并处理完相应数据后,必须调用SetupDiDestroyDeviceInfoList函数,否则内存溢出。
SetupDiEnumDeviceInfo
SetupDiEnumDeviceInfo函数返回一个SP_DEVINFO_DATA结构,它指定该设备的信息集的设备的信息元素。
代码语言:javascript复制BOOL SetupDiEnumDeviceInfo(
_In_ HDEVINFO DeviceInfoSet, // 设备信息集的句柄,即SetupDiGetClassDevs返回的句柄
_In_ DWORD MemberIndex, // 要检索的设备信息元素的从零开始的索引
_Out_ PSP_DEVINFO_DATA DeviceInfoData // 指向SP_DEVINFO_DATA结构的指针,以接收有关枚举设备信息元素的信息
);
SetupDiGetDeviceRegistryProperty
SetupDiGetDeviceRegistryProperty检索指定的即插即用设备属性.
代码语言:javascript复制BOOL
SetupDiGetDeviceRegistryPropertyW(
_In_ HDEVINFO DeviceInfoSet, // 设备信息集的句柄,即SetupDiGetClassDevs返回的句柄
_In_ PSP_DEVINFO_DATA DeviceInfoData, // 指向SP_DEVINFO_DATA结构的指针,该结构指定DeviceInfoSet中的设备信息元素
_In_ DWORD Property, // 指定要检索的属性
_Out_opt_ PDWORD PropertyRegDataType, // 指向一个变量的指针,该变量接收要检索的属性的数据类型。
_Out_writes_bytes_to_opt_(PropertyBufferSize, *RequiredSize) PBYTE PropertyBuffer, // 指向缓冲区的指针,该缓冲区接收正在检索的属性
_In_ DWORD PropertyBufferSize, // PropertyBuffer缓冲区的大小(单位:字节)
_Out_opt_ PDWORD RequiredSize // 指向DWORD类型的变量的指针,该变量接收所需的PropertyBuffer缓冲区的大小(单位:字节)
);
该函数的第三个入口参数Property决定了我们想要检索设备的什么属性,它的取值可以在SetupAPI.h文件里查找,这里不一一列举。比如本例中会用到下述Property,它们的含义如下:
代码语言:javascript复制#define SPDRP_DEVICEDESC (0x00000000) // DeviceDesc (R/W)
#define SPDRP_HARDWAREID (0x00000001) // HardwareID (R/W)
#define SPDRP_COMPATIBLEIDS (0x00000002) // CompatibleIDs (R/W)
#define SPDRP_CLASS (0x00000007) // Class (R--tied to ClassGUID)
#define SPDRP_CLASSGUID (0x00000008) // ClassGUID (R/W)
Qt相关控件
QTreeView
QTreeView类提供树视图的默认模型/视图实现。QTreeView实现了模型中项目的树形表示。关于这个控件的使用方式,比如添加条目(Item)、设置条目图片等在代码里会体现,也可以自己查询Qt Assistant。
比较重要的是本例中用到的QTreeView的一个信号槽函数。当鼠标点击到设备树上的某个设备时,我们需要知道鼠标点击的设备属于什么类型(USB设备?键盘类?鼠标类?),即要知道被点击的节点的父节点是谁。另一方面,也需要知道被点击的节点在该类设备中的索引。所以在本例中我们有以下的信号槽连接:
代码语言:javascript复制connect(ui.treeView, SIGNAL(clicked(const QModelIndex)), this, SLOT(getTreeClicked(const QModelIndex)));
其中:
- ui.treeView:即UI上的QTreeView控件
- SIGNAL:信号,当设备树上某个节点被鼠标点击时,会自动emit信号
- SLOT:槽函数,自己实现的处理函数,根据QModelIndex获取父节点和该节点索引
QTimer
定时器,定时刷新设备树。计算机上连接的设备可能会动态改变,比如插拔USB设备。关于QTimer的使用在此也不详述。本例中有以下信号槽连接:
代码语言:javascript复制connect(timer, SIGNAL(timeout()), this, SLOT(refreshTree()));
其中,timer是全局的QTimer对象。信号timeout表示定时器溢出时自动发出的信号,溢出频率可以通过QTimer的setInterval函数设定。槽函数refreshTree()是Jungle自己定义实现的,从函数名字可以知道,每次定时器溢出时,将会刷新设备树。
程序结构
本例的程序结构图如下:
UsbViewerQt
UsbViewerQt是主要的框架类,处理UI事务和功能事务。这里的UI事务是指用户与软件界面的交互,比如鼠标点击的活动;功能事务是指调用对应接口检索设备信息。UsbViewerQt的作用即是衔接UI事务和功能事务。
从上面的类图中可以看到,类UsbViewerQt有几个QStringList对象,分别用于保存各类设备下子设备的描述信息。LOG类对象log用于为整个程序提供日志功能,关于这部分,详见4.3。接口initTreeModel()完成初始化工作;refreshDeviceList()用于周期刷新设备列表;getHostName()用于获取主机名称,显示在设备树根节点上。
UsbInterface
UsbInterface并不是一个类,而是用纯C语言实现的检索指定设备类的各类属性的接口,为框架类对象UsbViewerQt服务。为了使用方便,Jungle定义了与Windows API适配的一个枚举:
代码语言:javascript复制enum DeviceClass{
DeviceClass_NONE = -1,
DeviceClass_MOUSE,
DeviceClass_SYSTEM,
DeviceClass_USB,
DeviceClass_NET,
DeviceClass_KEYBOARD
};
LOG
这是Jungle自行设计实现的一个日志系统,方便调试跟踪程序运行状况,设计语言为C 。这部分内容可以参见:C 实现日志系统。源码可以在Github上获取:https://github.com/FengJungle/Log
效果
在工程路径的Log文件夹下可以看到日志文件UsbViewerQt.log:
源码获取
源码地址:https://github.com/FengJungle/UsbViewer_Qt
欢迎和Jungle交流。