设备信息






4.91/5 (10投票s)
使用两个 C++ 类封装各种 setup API 调用,以获取、过滤和显示设备名称和信息。

引言
在实验室自动化领域工作时,控制软件的开发和使用经常需要建立通信通道,以便与各种设备进行对话,例如机器人手臂或自动化细胞培养箱。硬件连接可以通过 USB、Firewire、CANBus、I2C 和串行端口进行,并且可能使用转换器,例如 USB 到 4 路串行端口盒或串行到 I2C 转换器。与大多数设备制造商的说明相反,将应用程序设置为通过错误的通道以错误的设置与设备进行对话是相当容易的,将所述设备插入错误的端口也是如此。各种法律似乎还要求此类连接器位于狭窄空间中,且布线具有强烈的意大利面条状趋势。
最初的实验室自动化开发人员在阅读有关所涉及的各种设备时,通常会找出在他们特定的开发笔记本电脑上连接什么以及连接到哪里。这些开发人员通常专注于让设备启动并运行。忽略记录通信通道设置可能是硬编码的或记录在某个 obscure 设置文件中,这已成为常见现象。为了解决这个问题,编写了两个 C++ 类,供第一个开发人员使用,以造福后来的开发人员和最终用户。这些类允许以有助于快速、清晰地启动和运行的方式显示通信通道选项。这些类通常用于在列表控件中快速简单地显示过滤后的设备信息(和设备图标)。它们是对一些 Windows setup-API 函数的薄封装,优点是由于它们是相对薄的封装类,因此除了这些方法的原始 API 文档之外,几乎不需要其他内容即可使用。
例如,下面显示的数据是通过两个 C++ 类获得的,屏幕截图向用户显示,如果在此特定 PC 上选择了 COM13,则相关设备应插入 4 路 USB-to-Serial 转换器盒上标有“端口 3”的连接器。这听起来像是一个微不足道的练习,但是当某些设备只是停在那里,也许是因为某个 PC 的某些通信通道的默认值与最初使用的不同,或者正在使用连接不正确的非标准通信线缆,能够在几分钟而不是几小时或几天内证明所有设备都连接正确(或不正确)是一个很大的优势。该示例使用串行端口,但同样适用于其他类型的设备和通信通道。

背景
研究如何列出(最初只是有关可用串行端口的信息,后来扩展到涵盖所有(软件)设备),首先导致使用了注册表的代码。一个随附的 4 路 USB 到串行端口盒的实用程序显示,还可以获取其他有用的信息。寻找一种与该实用程序类似的方法,最终导致使用 setupapi.h 及其提供的函数,例如 SetupDiGetClassDevs。
微软文章 259695:如何使用 SetupDi 调用枚举硬件设备给出了访问设备的一般思路。Chuan-Liang Teng 的文章 枚举 Windows 设备也探讨了这一领域。
类
编写了两个类:`CDevInfo` 和 `CDeviceImageList`,用于封装各种结构和 API 调用。`CDeviceImageList` 用于获取设备图标的图像列表,`CDevInfo` 用于访问和枚举设备信息。
CDevInfo
封装了一个 HDEVINFO
(一个设备接口集的句柄)和一个 SP_DEVINFO_DATA 结构,该结构定义了该设备接口集中的一个设备。CDevInfo
封装的 Setupapi 调用包括:
- SetupDiGetClassDevs
- SetupDiDestroyDeviceInfoList
- SetupDiEnumDeviceInfo
- SetupDiGetDeviceRegistryProperty
- SetupDiGetClassImageIndex
- SetupDiGetClassDescription
`CDeviceImageList` 封装了 SP_CLASSIMAGELIST_DATA 结构,并调用 setupapi 函数 SetupDiGetClassImageList 和 SetupDiDestroyClassImageList,以生成设备管理器中显示的设备类图标。
这两个类与 ATL 的 `CListViewCtrl` 一起,允许使用 Setup Device Registry Property (SPDRP) 代码选择设备列表及相关信息。每个条目旁边都会显示一个相应的设备类图标。显示此类信息通过以下形式的代码实现:
// m_wndListView is a CListViewCtrl in report mode...
m_wndListView.Attach(GetDlgItem(IDC_LIST1));
m_wndListView.InsertColumn(0, _T("Name"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(1, _T("Friendly-Name"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(2, _T("Driver"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(3, _T("Mfg"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(4, _T("Physical Device"), LVCFMT_LEFT, 200, 0);
m_wndListView.SetImageList(m_DevImageList, 1);
CDevInfo cDevInfo(m_hWnd);
int a = 0;
while(cDevInfo.EnumDeviceInfo())
{
wchar_t szBuf[MAX_PATH] = {0};
if(cDevInfo.GetDeviceRegistryProperty(SPDRP_CLASS, (PBYTE)szBuf))
{
wchar_t szFriendlyName[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_FRIENDLYNAME,
(PBYTE)szFriendlyName);
wchar_t szDriver[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_DRIVER, (PBYTE)szDriver);
wchar_t szMfg[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_MFG, (PBYTE)szMfg);
wchar_t szPhysical[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty
(SPDRP_PHYSICAL_DEVICE_OBJECT_NAME, (PBYTE)szPhysical);
int ImageIndex = 0;
if(cDevInfo.GetClassImageIndex(m_DevImageList, &ImageIndex))
{
wchar_t szDesc[MAX_PATH] = {0};
if(cDevInfo.GetClassDescription(szDesc))
{
ATLTRACE(szDesc);
ATLTRACE(_T("\n"));
}
m_wndListView.InsertItem(a,szDesc,ImageIndex);
m_wndListView.SetItemText(a,1,szFriendlyName);
m_wndListView.SetItemText(a,2,szDriver);
m_wndListView.SetItemText(a,3,szMfg);
m_wndListView.SetItemText(a,4,szPhysical);
}
}
}
为了能够过滤显示哪些设备,添加了一个备用的 `CDevInfo` 构造函数,该构造函数公开了 API 对 GUID 的使用,以指定感兴趣的设备。现在可以创建一个对象,传递设备类型的 GUID 和所需的任何标志的 GUID。在下面的例子中,这些标志确保只显示实际连接到 PC 的串行设备和 USB-串行设备。
const GUID Guid = GUID_DEVCLASS_PORTS;
CDevInfo cDevInfo(m_hWnd, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE , &Guid);
while(cDevInfo.EnumDeviceInfo())
{
wchar_t szFriendlyName[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_FRIENDLYNAME, (PBYTE)szFriendlyName);
...
}
如果没有 `DIGCF_PRESENT` 标志,任何使用 COM 端口的已安装设备都会显示出来;无论它是否物理连接到 PC;但是当使用 `DIGCF_PRESENT` 标志时,只会显示实际插入 PC 的 USB 转串行设备。由于所使用的转换器盒制造商已在“信息”字段中包含了盒子上连接器的编号,用户现在可以通过选择,例如,某个串行设备的 COM11,从而知道该设备的串行线缆需要插入转换器盒上标有“端口 1”的连接器。
关注点
当将 `CDeviceImageList` 封装的图像列表传递给 `CListViewCtrl` 的方法之一时,出现了图像列表所有权的问题。为了解决这个问题,编写了一个非标准的复制构造函数和 `CDeviceImageList & operator= (CDeviceImageList & pSource)`,以像智能指针一样传递列表的所有权,而不是复制它。`CDevInfo` 类也以相同的方式处理,以便传递而不是复制封装的 `HDEVINFO`。Visual Studio 项目 `DevInfoTester` 通过创建几个 `CDevInfo` 对象并像下面显示的那样分配它们来演示此功能。
CDevInfo foo(CDevInfo & _DevInfo)
{
return _DevInfo;
}
int _tmain(int /*argc*/, _TCHAR* /*argv[]*/)
{
// Exercise constructor, auto_ptr type copy constructor and assignment operator
CDevInfo DevInfo1(NULL);
CDevInfo DevInfo2 = DevInfo1;
CDevInfo DevInfo3(NULL);
DevInfo3 = foo(DevInfo2);// Generates C4239 for use of non-standard assignment
DevInfo1 = DevInfo3;
...
为了使 COM 端口以合理的顺序显示,必须为 `CListViewCtrl` 中的行添加一些非默认排序,因为如果没有此功能,端口会以 COM1、COM11、COM2 而不是 COM1、COM2、COM11 的顺序显示。这在 Visual Studio 项目 `PortInfo` 中有所演示。
通过 `CDeviceImageList` 和 `CListViewCtrl` 对象使用 setupapi,可以在一个对话框中用 40 行易于维护的代码替换超过 100 行直接调用注册表的灵活性较差的代码。
Using the Code
随附的 VS2005 解决方案包含三个项目,其中一个为了简单起见,可以在没有 WTL 的情况下编译,另外两个则需要安装 WTL 才能编译(有关下载,请参阅 Windows Template Library (WTL))。`DevInfoWin32` 在命令窗口中运行,在操作 `CDevInfo` 对象后,会显示检测到的端口列表。这个项目是最简单的。
ListViewTest
VS2005 项目使用基于对话框的项目来显示所有找到的设备列表;此项目未实现排序,以说明使用这些类所需的代码。
第三个 VS2005 项目 `PortInfo`,通过菜单上的“端口 | 选择”访问对话框,并显示串行端口列表。此项目实现了排序。
要更改显示的设备类,请使用不同的设备 GUID。例如,在当前显示所有设备的 `ListViewTest` 项目中,将基本的 `CDevInfo` 构造函数行更改为...
CDevInfo cDevInfo(m_hWnd);
...到...
const GUID Guid = GUID_DEVCLASS_USB;
CDevInfo cDevInfo(m_hWnd, DIGCF_PRESENT , &Guid);
...将显示限制为所有 USB 设备。
参考文献
Code Project 文章 如何在 CTreeCtrl 中使用 32 位图标 展示了如何将图标加载到控件中,而 另一个串行端口枚举器 是我开始的地方。在查找对话框调整大小代码的提醒时,我还偶然发现了文章 使用 WTL 内置对话框调整大小类。谢谢。
对于资源转移而非复制的自动指针类型操作,请参阅“C++ in action 10.6-10.8, B.Milewski, ISBN 0-201-69948-6, Addison-Wesley 2001”。
历史
- 2008年12月12日:初始版本