简介:这套Qt示例项目源码面向Windows平台的应用开发者,演示了如何调用setupapi.h库中的SetupDiGetClassDevs和SetupDiEnumDeviceInfo接口,完整遍历设备管理器中的设备列表,并获取设备描述、图标、类名、GUID、实例路径、硬件ID、驱动INF名称、驱动版本、供应商等详细属性。工程共12个文件,包含4个cpp源文件、3个头文件、2个ui界面文件,以及pro工程配置和说明文档,压缩包大小仅21KB;cpp实现核心逻辑,h声明接口,ui完成界面布局,结构清晰,便于快速定位与二次开发。项目将设备信息提取逻辑独立封装,界面与后台分离,主窗口和设备管理器页面可直观展示枚举结果,方便观察不同属性值的对应关系。已有141人学习下载,适合需要在Qt环境中操作Windows设备管理API、编写系统信息查询工具或驱动相关应用的开发者参考,能够有效缩短功能原型的搭建时间。
1. 在 Qt 里复现设备管理器:绕不开的 SETUPAPI.H
很多做设备工具的开发者,是被“设备管理器里那行设备描述”逼着来查的:想在 Qt 程序里复现设备管理器上的详细信息,却发现读注册表拿不全,用 WMI 又慢又像黑匣子。设备管理器背后是 WINDOWS API 中的 SETUPAPI.H 库,Qt 工程直接调用这套接口,才是拿到完整设备信息的正确姿势。这篇文章就是一个跑得通的示例项目源码,从枚举设备到提取描述、硬件 ID、制造商和驱动版本,全部摆出来讲。适合做产测工具、设备管理、驱动状态查看这类方向的 Qt 开发者;新手能按步骤搭起来,熟手可以直接把枚举和缓冲处理那段代码拿走改。
2. SetupAPI 枚举链路:从 HDEVINFO 到设备实例 ID
在写代码之前,先把链路讲清楚。设备管理器里每一个条目,背后都对应设备树上的一个设备节点。Windows 把这些节点组织成树,但对外提供查询能力的是 SetupAPI,而不是让你直接去翻注册表。注册表里HKLM\SYSTEM\CurrentControlSet\Enum下面确实有设备键,但键的 ACL、值格式和系统版本差异都很折腾,WMI 的Win32_PnPEntity又慢,还缺驱动版本这类细节。SetupAPI 是性能和完整度的平衡点,设备管理器自己就是它的图形壳。
2.1 两个必须先理解的概念:HDEVINFO 和 SP_DEVINFO_DATA
HDEVINFO 是“设备信息集”的句柄,你可以把它理解成一份内存里的设备列表快照。枚举第一件事就是通过SetupDiGetClassDevs拿到这个句柄,之后所有查询都围绕它进行,用完必须SetupDiDestroyDeviceInfoList释放,这个对称性要记牢,漏了就内存泄漏。
SP_DEVINFO_DATA 是单个设备在这个信息集中的身份标识,里面装着设备在列表里的索引、类 GUID 等元数据。它所有成员都可以不用管,唯独开头的cbSize必须手动填成sizeof(SP_DEVINFO_DATA),这个字段是告诉 API“我传进来的结构体是多大”的,填错或者不填,枚举接口直接给你返回ERROR_INVALID_USER_BUFFER。我在后面踩坑章节还会单独说它。
2.2 一次完整枚举需要认识的四个 API
| API 函数 | 作用 | 典型调用位置 |
|---|---|---|
SetupDiGetClassDevs | 获取设备信息集句柄,可按类 GUID、枚举器名过滤 | 枚举最开始 |
SetupDiEnumDeviceInfo | 按索引枚举信息集中的设备,成功返回 TRUE | 循环每次迭代 |
SetupDiGetDeviceRegistryProperty | 读取设备属性,如描述、制造商、硬件 ID | 拿到设备后 |
SetupDiGetDeviceInstanceId | 读取设备实例 ID 完整字符串 | 拿到设备后 |
SetupDiDestroyDeviceInfoList | 释放设备信息集句柄 | 所有查询结束 |
这五个是保底配置。SetupDiGetDeviceRegistryProperty是主力,它能拿到的属性有一大堆,从设备描述到驱动键路径都走它。SetupDiGetDeviceInstanceId则是单独函数,它的返回串形如USB\VID_1234&PID_5678\SN123456,后面做 VID/PID 解析就是拿这个字符串做文章。
2.3 Qt 工程怎么把 setupapi.h 接进来
在 Qt 的 .pro 文件里把setupapi.lib链上,win32条件里加一行即可,MSVC 和 MinGW 都能识别-lsetupapi:
QT += core gui widgets TARGET = DeviceInfoViewer TEMPLATE = app CONFIG += c++17 win32 { LIBS += -lsetupapi } SOURCES += main.cpp mainwindow.cpp devicequery.cpp HEADERS += mainwindow.h devicequery.h头文件包含有个坑:windows.h会定义min和max宏,Qt 的全局头文件一旦在它之后加载,模板代码里出现min就会被宏替换,编译直接炸掉一片。常见做法是在包含 Windows 头之前定义NOMINMAX:
#ifndef NOMINMAX #define NOMINMAX #endif #include <windows.h> #include <setupapi.h>如果你用的是 MSVC 编译套件,还建议把_CRT_SECURE_NO_WARNINGS加上,省得 SetupAPI 相关代码触发一堆 C4996 警告。MinGW 下则不用管这个,两边行为不一样。
2.4 调用时序:Get 之后必须 Destroy
整个枚举过程的组织方式是这样的:先SetupDiGetClassDevs拿句柄,然后for循环里SetupDiEnumDeviceInfo逐个取设备,每个设备再去查想要的属性,最后SetupDiDestroyDeviceInfoList收尾。中间任何一步失败,都不能跳过后面的释放。我一般用 RAII 的思路在 Qt 里包一层:构造函数不做事,枚举函数内部开句柄,函数末尾统一释放,这样即使中间continue也不会漏。
3. 跑通最小 Demo:DeviceQuery 类与 QTreeWidget 设备表
这一章直接上能编译的最小工程代码,你要在十分钟内跑出第一屏设备列表。工程结构不用复杂,四个文件足够:main.cpp、mainwindow.h/.cpp、devicequery.h/.cpp。DeviceQuery只干一件事——枚举,不掺界面逻辑,方便你以后把它拷到其他项目里用。
3.1 DeviceQuery 的数据结构与对外接口
头文件里不暴露任何 Windows 类型,调用方只跟QString、QList打交道,这样界面层不用#include <windows.h>,解耦干净:
// devicequery.h #ifndef DEVICEQUERY_H #define DEVICEQUERY_H #include <QList> #include <QString> #include <QStringList> struct DeviceInfo { QString description; // 设备管理器显示的名称 QString friendlyName; // 友好名称,可能为空 QString manufacturer; // 制造商 QString instanceId; // 实例 ID,如 USB\VID_1234&PID_5678\SN001 QStringList hardwareIds; // 硬件 ID 多字符串列表 QString className; // 安装类名,如 USB、Display }; class DeviceQuery { public: QList<DeviceInfo> enumAllDevices(); }; #endif // DEVICEQUERY_H这里故意把hardwareIds做成QStringList,因为硬件 ID 本身是 REG_MULTI_SZ 多字符串,一个设备可以有好几个候选 ID,后面解析 VID/PID 时要用到完整的列表。className字段用来对应设备管理器左侧的分组。
3.2 枚举全部存在的设备:核心实现
enumAllDevices的实现是整个示例项目的主干。里面的辅助函数queryStringProp和queryMultiStringProp先不做内部展开,这一节先看懂枚举主流程:
// devicequery.cpp #include "devicequery.h" #ifndef NOMINMAX #define NOMINMAX #endif #include <windows.h> #include <setupapi.h> namespace { // 读取单字符串类型的设备属性,内部处理了缓冲区长度的两次调用 QString queryStringProp(HDEVINFO set, PSP_DEVINFO_DATA info, DWORD prop) { DWORD type = 0; DWORD bytes = 0; if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, &type, nullptr, 0, &bytes)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return QString(); } QVector<wchar_t> buf(bytes / sizeof(wchar_t) + 1, 0); if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, &type, reinterpret_cast<BYTE *>(buf.data()), static_cast<DWORD>(buf.size() * sizeof(wchar_t)), &bytes)) { return QString(); } return QString::fromWCharArray(buf.data()); } // 读取 REG_MULTI_SZ 多字符串属性,用双 null 结尾分割 QStringList queryMultiStringProp(HDEVINFO set, PSP_DEVINFO_DATA info, DWORD prop) { QStringList result; DWORD type = 0; DWORD bytes = 0; if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, &type, nullptr, 0, &bytes)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return result; } QVector<wchar_t> buf(bytes / sizeof(wchar_t) + 2, 0); if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, &type, reinterpret_cast<BYTE *>(buf.data()), static_cast<DWORD>(buf.size() * sizeof(wchar_t)), &bytes)) { return result; } const wchar_t *p = buf.data(); const wchar_t *end = buf.data() + bytes / sizeof(wchar_t) + 1; while (p < end) { int len = static_cast<int>(wcslen(p)); if (len == 0) break; result << QString::fromWCharArray(p, len); p += len + 1; } return result; } } // namespace QList<DeviceInfo> DeviceQuery::enumAllDevices() { QList<DeviceInfo> devices; // 枚举所有类别的设备,且只枚举当前真实存在的设备 HDEVINFO set = SetupDiGetClassDevsW(nullptr, nullptr, nullptr, DIGCF_ALLCLASSES | DIGCF_PRESENT); if (set == INVALID_HANDLE_VALUE) return devices; SP_DEVINFO_DATA info; info.cbSize = sizeof(SP_DEVINFO_DATA); for (DWORD index = 0; SetupDiEnumDeviceInfo(set, index, &info); ++index) { DeviceInfo dev; dev.description = queryStringProp(set, &info, SPDRP_DEVICEDESC); dev.friendlyName = queryStringProp(set, &info, SPDRP_FRIENDLYNAME); dev.manufacturer = queryStringProp(set, &info, SPDRP_MFG); dev.hardwareIds = queryMultiStringProp(set, &info, SPDRP_HARDWAREID); dev.className = queryStringProp(set, &info, SPDRP_CLASS); devices.append(dev); } SetupDiDestroyDeviceInfoList(set); return devices; }SetupDiGetClassDevsW的W后缀表示使用宽字符版本,对应后面所有属性查询都用W版本,这样拿回来可以直接转QString,避免编码转换。第一个参数传nullptr并且配合DIGCF_ALLCLASSES,意思是不限定设备类,列出全部类别;DIGCF_PRESENT则过滤掉了“曾经装过但现在没连接”的幽灵设备,这个不加的话,枚举结果会比设备管理器多出一大堆灰色条目。
循环体中每次拿到SP_DEVINFO_DATA之后,queryStringProp内部会重新读注册表属性,这个过程是内存操作,速度很快。完整枚举一次大概几百毫秒,体感上点一下按钮就能出结果。如果你以后要监控设备状态,这个函数就是刷新的核心,直接重复调用即可。
3.3 主窗口展示:QTreeWidget 四列表格
界面用QTreeWidget就够了,不用上QTableView加 Model,因为设备数量通常是几十到上百,没有性能压力:
// mainwindow.cpp 关键部分 #include "mainwindow.h" #include "devicequery.h" #include <QTreeWidget> #include <QHeaderView> void MainWindow::refreshDevices() { ui->treeWidget->clear(); DeviceQuery query; const QList<DeviceInfo> devices = query.enumAllDevices(); for (const DeviceInfo &dev : devices) { QStringList columns; columns << dev.description << dev.manufacturer << (dev.hardwareIds.isEmpty() ? QString() : dev.hardwareIds.first()) << dev.instanceId; QTreeWidgetItem *item = new QTreeWidgetItem(ui->treeWidget, columns); item->setText(0, dev.description); item->setText(1, dev.manufacturer); item->setText(2, dev.hardwareIds.isEmpty() ? QString() : dev.hardwareIds.first()); item->setText(3, dev.instanceId); } ui->statusBar->showMessage(QStringLiteral("共枚举到 %1 个设备").arg(devices.size())); }表头在构造函数里设置:第一列“设备描述”,第二列“制造商”,第三列“硬件 ID”,第四列“实例 ID”。setSortingEnabled(true)开起来,用户点击表头就能按列排序,产测场景下按制造商排序是高频操作。
第一次跑起来时,你会看到设备数量比设备管理器“设备管理器”页显示的多一些,这是正常的。设备管理器默认隐藏了打印机、蓝牙适配器等某些类别的设备,而DIGCF_ALLCLASSES是全部吐出来,两者数量天然不一致。想看差异,可以把设备管理器的“查看→显示隐藏的设备”打开对比,这样两边就大体重合了。
4. 深入设备详细信息:SPDRP 属性、驱动版本与回退策略
基础枚举跑通之后,你会发现光有描述、制造商和硬件 ID 还不够。很多场景要的是驱动版本号、设备状态、安装类名这些“详细信息”页里的内容。这一章把 SPDRP 属性族讲透,并给出按需查询驱动的做法。
4.1 SPDRP 属性族:一个函数通吃所有注册表值
SetupDiGetDeviceRegistryProperty的第三个参数传不同的SPDRP_*常量,就能取到设备节点下的不同注册表值。最常用的是这张表:
| SPDRP 常量 | 数据类型 | 对应信息 |
|---|---|---|
SPDRP_DEVICEDESC | REG_SZ | 设备描述,设备管理器主显示名 |
SPDRP_FRIENDLYNAME | REG_SZ | 友好名称,可能为空 |
SPDRP_MFG | REG_SZ | 制造商名称 |
SPDRP_HARDWAREID | REG_MULTI_SZ | 硬件 ID 列表 |
SPDRP_COMPATIBLEIDS | REG_MULTI_SZ | 兼容的设备 ID 列表 |
SPDRP_CLASS | REG_SZ | 安装类名,如 USB、Display |
SPDRP_CLASSGUID | REG_SZ | 安装类 GUID 字符串 |
SPDRP_DRIVER | REG_SZ | 驱动键路径,如{4d36e96c-e325-11ce-bfc1-08002be10318}\0000 |
SPDRP_LOCATION_INFORMATION | REG_SZ | 位置信息,如“Port_#0001.Hub_#0002” |
注意SPDRP_DRIVER拿到的不是驱动文件路径,而是设备节点驱动键在服务目录下的索引。想拿真正的 INF 路径和驱动文件版本,得走 4.4 那套驱动信息接口。
4.2 设备描述选谁:FriendlyName 与 DeviceDesc 的回退
设备管理器第一列显示的名称,优先取友好名称,没有友好名称时才用设备描述。但实际设备中,有 FriendlyName 的反而少,大部分 USB 设备只有 DeviceDesc。所以读描述的正确顺序是:先读SPDRP_FRIENDLYNAME,为空再读SPDRP_DEVICEDESC,两个都为空就用硬件 ID 兜底:
QString resolveDisplayName(HDEVINFO set, PSP_DEVINFO_DATA info, const QStringList &hardwareIds) { QString name = queryStringProp(set, info, SPDRP_FRIENDLYNAME); if (name.isEmpty()) name = queryStringProp(set, info, SPDRP_DEVICEDESC); if (name.isEmpty() && !hardwareIds.isEmpty()) name = hardwareIds.first(); return name; }这个函数在界面上显示时直接调用,能减少不少“设备描述为空”的白行。你可能会遇到某些设备显示成“未知设备”或直接拿硬件 ID 充当名字,比如没装驱动的黄叹号设备,原因就是它的 DeviceDesc 键缺失。这种情况下把硬件 ID 显示出来,反而对排查更有帮助——用户一眼就能看到USB\VID_1234这种字符串,知道你机器上插了个什么设备。
4.3 硬件 ID 解析:从 REG_MULTI_SZ 里榨出 VID/PID
硬件 ID 是一组多字符串,同一个设备可能同时有USB\VID_1234&PID_5678&REV_0100、USB\VID_1234&PID_5678两条。解析 VID/PID 是 USB 设备工具刚需,我一般会加一个简单函数:
QString extractVidPid(const QString &hardwareId) { const int vidPos = hardwareId.indexOf(QStringLiteral("VID_"), 0, Qt::CaseInsensitive); const int pidPos = hardwareId.indexOf(QStringLiteral("PID_"), 0, Qt::CaseInsensitive); if (vidPos < 0 || pidPos < 0) return QString(); const QString vid = hardwareId.mid(vidPos + 4, 4); const QString pid = hardwareId.mid(pidPos + 4, 4); return QStringLiteral("VID_%1 PID_%2").arg(vid.toUpper(), pid.toUpper()); }为什么不建议用正则?因为这个字符串格式非常规整,用indexOf加mid就够了,跑起来没有正则的编译开销。注意有些设备的硬件 ID 是PCI\VEN_8086&DEV_9A49这种,VID/PID 关键字不一样,这个函数返回空就行,不影响主流程。
4.4 驱动版本与驱动日期:按数据查询,别在批量循环里做
驱动版本在SP_DRVINFO_DATAW结构体里,需要先调用SetupDiBuildDriverInfoList构建设备驱动列表,再用SetupDiEnumDriverInfo逐个取。这两个接口是“重量级”操作,会扫描驱动存储,几百个设备全部跑一遍,界面能卡住十几秒。所以正确姿势是:只对用户选中或指定实例 ID 的设备查询驱动信息。
bool queryDriverInfo(HDEVINFO set, PSP_DEVINFO_DATA info, QString *versionOut, QDate *dateOut) { if (!SetupDiBuildDriverInfoListW(set, info, SPDIT_COMPATDRIVER)) return false; SP_DRVINFO_DATAW drv; drv.cbSize = sizeof(SP_DRVINFO_DATAW); bool ok = false; for (DWORD i = 0; SetupDiEnumDriverInfoW(set, info, SPDIT_COMPATDRIVER, i, &drv); ++i) { SYSTEMTIME st; if (FileTimeToSystemTime(&drv.DriverDate, &st)) { *dateOut = QDate(st.wYear, st.wMonth, st.wDay); } *versionOut = QStringLiteral("%1.%2.%3.%4") .arg((drv.DriverVersion >> 48) & 0xFFFF) .arg((drv.DriverVersion >> 32) & 0xFFFF) .arg((drv.DriverVersion >> 16) & 0xFFFF) .arg(drv.DriverVersion & 0xFFFF); ok = true; break; } SetupDiDestroyDriverInfoList(set, info, SPDIT_COMPATDRIVER); return ok; }DriverVersion是DWORDLONG,四个 16 位段分别对应主版本、次版本、构建号和修订号,直接除以 10 是不对的,要用移位再取低 16 位。DriverDate是FILETIME,转成SYSTEMTIME之后读年月日即可。这套接口在 Win10、Win11 上仍然稳定工作。
5. 设备信息抓取的 5 个典型踩坑:从死循环到乱码
这块内容是我自己反复翻过车的集合。每一条都按“现象→原因→解决”的顺序写,你在现场遇到同类问题可以直接对号入座。
5.1 枚举到一半就停:SP_DEVINFO_DATA.cbSize 没有正确初始化
现象:SetupDiEnumDeviceInfo 循环跑了几次之后突然返回 FALSE,GetLastError 是 1784(ERROR_INVALID_USER_BUFFER),设备列表活活少了一半。
原因:SP_DEVINFO_DATA 在使用前必须把 cbSize 设为 sizeof(SP_DEVINFO_DATA)。如果用了栈上变量没初始化,或者初始化成了别的值,SetupAPI 在枚举到第二个、第三个设备时缓冲区校验失败。更阴的地方在于,如果你在循环里复用同一个 SP_DEVINFO_DATA 变量,而中间有别的代码往这个结构体里写过数据,cbSize 可能被覆盖。
解决:在每次调用 SetupDiEnumDeviceInfo 之前重新赋值一次 cbSize,不要只在循环外做一次初始化。也就是把info.cbSize = sizeof(SP_DEVINFO_DATA);写进 for 循环体第一行。这个习惯能帮你挡掉不少环境导致的玄学问题。
5.2 实例 ID 读到一半:缓冲区长度参数的单位是字符不是字节
现象:调用 SetupDiGetDeviceInstanceIdW 时,第二次传入的缓冲区长度沿用了 SetupDiGetDeviceRegistryPropertyW 的习惯,结果返回的字符串被截断,或者干脆返回 ERROR_INSUFFICIENT_BUFFER。
原因:两个 API 的缓冲区长度单位不一样。SetupDiGetDeviceRegistryPropertyW 的第 5 个参数是字节数,而 SetupDiGetDeviceInstanceIdW 的最后一个参数实传递是字符数。这个差异在文档角落里,不踩一次很难意识到。
解决:封装实例 ID 查询时单独注释清楚:
QString queryInstanceId(HDEVINFO set, PSP_DEVINFO_DATA info) { DWORD chars = 0; // 第一次调用,拿到的是字符数,不是字节数 SetupDiGetDeviceInstanceIdW(set, info, nullptr, 0, &chars); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) return QString(); QVector<wchar_t> buf(chars + 1, 0); if (!SetupDiGetDeviceInstanceIdW(set, info, buf.data(), chars, &chars)) return QString(); return QString::fromWCharArray(buf.data()); }这类“字节 vs 字符”的坑在 Windows API 里很常见,我的习惯是每个相关函数都写注释标注单位。
5.3 Windows 头与 Qt 头互相打架:min/max 宏冲突
现象:编译报一堆 C2589 语法错误,错误位置全在 Qt 容器的qMin、qMax或者 STL 的std::min附近,怎么看都不像是自己代码的问题。
原因:windows.h里定义了min和max宏,预处理阶段把std::min展开成了std::(a) (b)这种语法垃圾,导致后面所有模板代码全部崩掉。
解决:在任何#include <windows.h>之前定义NOMINMAX,推荐写在devicequery.cpp的最顶部,最好在预编译头里也加上。如果项目里某些第三方头文件偷偷包含了 windows.h,那就在 .pro 的 DEFINES 里加DEFINES += NOMINMAX,这招保得更彻底。
5.4 设备管理器没有的东西,列表里全冒出来了
现象:列表里出现大量重复设备,比如同一个 U 盘品牌出现十几次,还有一堆名字带“!”的设备,系统重启之后它们依然在。
原因:枚举标志只传了DIGCF_ALLCLASSES,没有传DIGCF_PRESENT。这样会把系统中曾经登记过、但当前没连接的历史设备全部列出来,也就是设备管理器里“显示隐藏的设备”会出现的那些幽灵项。
解决:SetupDiGetClassDevsW的最后一个参数统一写成DIGCF_ALLCLASSES | DIGCF_PRESENT。如果你确实要排查历史驱动残留,可以单独写一个带DIGCF_PRESENT参数开关的枚举函数,不要把它做成默认行为。
5.5 驱动信息查询卡死 UI:批量查询驱动列表是性能陷阱
现象:在循环里对每个设备调用SetupDiBuildDriverInfoListW,界面直接无响应十几秒,任务管理器看 CPU 没爆,但窗口就是拖不动。
原因:这个 API 会扫描驱动存储,并且对每个设备构建完整的驱动节点树,成本根本不是读注册表属性那个量级。在几百个设备上跑全量驱动查询,耗时按秒计算,Qt 的 UI 线程没有让出事件循环,自然卡死。
解决:改成按需查询——只有用户选中某一行、或双击某一行时才触发驱动信息查询。如果你确实需要批量导出驱动版本,那就放到子线程里去跑,跑完通过信号槽回抛结果。我在示例项目里默认用的是“点击行查询”的方案。
6. 让设备列表随插拔即时刷新:热插拔监听与验证技巧
设备工具只枚举一次是不够的,产测场景里用户会反复插拔。常见方案有两种:一个是 QTimer 每 2 秒全量枚举一次,简单但浪费,列表还会闪;另一个是监听WM_DEVICECHANGE消息,设备变化时被动刷新,这个在 Qt 里用QAbstractNativeEventFilter就能实现:
class DeviceEventFilter : public QAbstractNativeEventFilter { public: bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override { Q_UNUSED(eventType); Q_UNUSED(result); MSG *msg = static_cast<MSG *>(message); if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED) { emit deviceListChanged(); } return false; } };DBT_DEVNODES_CHANGED是设备节点整体变化的事件,USB 插拔、网卡禁用、驱动更新都会触发它。比起一个个监听DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE,这个事件覆盖面最广,也最简单。在MainWindow里把它连到refreshDevices(),插拔后列表自动刷新。
最后说验证技巧。我每次写完设备枚举,第一件事是开着设备管理器手工对:按“显示隐藏的设备”,然后逐个类型展开对比数量。尤其注意“通用串行总线控制器”里是不是比设备管理器多出设备——多出来说明 DIGCF_PRESENT 丢了,少出来说明你的过滤条件写多了。驱动版本则单独对 INF 文件属性里的版本确认一遍。
还有一个小习惯:在枚举函数里加一个环境变量开关,比如DEVICE_DUMP=1时把结果写进 CSV。客户现场报“设备认不到”,你把这份 dump 拿回来,两分钟就能看出来是他的设备没插稳,还是驱动没有 INSTALL,还是枚举代码漏了设备类。这个习惯帮我省过不少远程排查的来回。希望这篇示例项目能让你第一次跑 SetupAPI 的时候少走弯路,也希望你和我一样,在写完一个能看清整台机器设备全貌的程序时,感觉到 Windows 底层那层硬核的秩序感。
本文还有配套的精品资源,点击获取