ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt调用SetupAPI实现Windows设备管理器枚举与驱动信息查询

Qt调用SetupAPI实现Windows设备管理器枚举与驱动信息查询 简介这套Qt示例项目源码面向Windows平台的应用开发者演示了如何调用setupapi.h库中的SetupDiGetClassDevs和SetupDiEnumDeviceInfo接口完整遍历设备管理器中的设备列表并获取设备描述、图标、类名、GUID、实例路径、硬件ID、驱动INF名称、驱动版本、供应商等详细属性。工程共12个文件包含4个cpp源文件、3个头文件、2个ui界面文件以及pro工程配置和说明文档压缩包大小仅21KBcpp实现核心逻辑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_DATAHDEVINFO 是“设备信息集”的句柄你可以把它理解成一份内存里的设备列表快照。枚举第一件事就是通过SetupDiGetClassDevs拿到这个句柄之后所有查询都围绕它进行用完必须SetupDiDestroyDeviceInfoList释放这个对称性要记牢漏了就内存泄漏。SP_DEVINFO_DATA 是单个设备在这个信息集中的身份标识里面装着设备在列表里的索引、类 GUID 等元数据。它所有成员都可以不用管唯独开头的cbSize必须手动填成sizeof(SP_DEVINFO_DATA)这个字段是告诉 API“我传进来的结构体是多大”的填错或者不填枚举接口直接给你返回ERROR_INVALID_USER_BUFFER。我在后面踩坑章节还会单独说它。2.2 一次完整枚举需要认识的四个 APIAPI 函数作用典型调用位置SetupDiGetClassDevs获取设备信息集句柄可按类 GUID、枚举器名过滤枚举最开始SetupDiEnumDeviceInfo按索引枚举信息集中的设备成功返回 TRUE循环每次迭代SetupDiGetDeviceRegistryProperty读取设备属性如描述、制造商、硬件 ID拿到设备后SetupDiGetDeviceInstanceId读取设备实例 ID 完整字符串拿到设备后SetupDiDestroyDeviceInfoList释放设备信息集句柄所有查询结束这五个是保底配置。SetupDiGetDeviceRegistryProperty是主力它能拿到的属性有一大堆从设备描述到驱动键路径都走它。SetupDiGetDeviceInstanceId则是单独函数它的返回串形如USB\VID_1234PID_5678\SN123456后面做 VID/PID 解析就是拿这个字符串做文章。2.3 Qt 工程怎么把 setupapi.h 接进来在 Qt 的 .pro 文件里把setupapi.lib链上win32条件里加一行即可MSVC 和 MinGW 都能识别-lsetupapiQT core gui widgets TARGET DeviceInfoViewer TEMPLATE app CONFIG c17 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. 跑通最小 DemoDeviceQuery 类与 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_1234PID_5678\SN001 QStringList hardwareIds; // 硬件 ID 多字符串列表 QString className; // 安装类名如 USB、Display }; class DeviceQuery { public: QListDeviceInfo 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(); } QVectorwchar_t buf(bytes / sizeof(wchar_t) 1, 0); if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, type, reinterpret_castBYTE *(buf.data()), static_castDWORD(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; } QVectorwchar_t buf(bytes / sizeof(wchar_t) 2, 0); if (!SetupDiGetDeviceRegistryPropertyW(set, info, prop, type, reinterpret_castBYTE *(buf.data()), static_castDWORD(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_castint(wcslen(p)); if (len 0) break; result QString::fromWCharArray(p, len); p len 1; } return result; } } // namespace QListDeviceInfo DeviceQuery::enumAllDevices() { QListDeviceInfo 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 QListDeviceInfo 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_DEVICEDESCREG_SZ设备描述设备管理器主显示名SPDRP_FRIENDLYNAMEREG_SZ友好名称可能为空SPDRP_MFGREG_SZ制造商名称SPDRP_HARDWAREIDREG_MULTI_SZ硬件 ID 列表SPDRP_COMPATIBLEIDSREG_MULTI_SZ兼容的设备 ID 列表SPDRP_CLASSREG_SZ安装类名如 USB、DisplaySPDRP_CLASSGUIDREG_SZ安装类 GUID 字符串SPDRP_DRIVERREG_SZ驱动键路径如{4d36e96c-e325-11ce-bfc1-08002be10318}\0000SPDRP_LOCATION_INFORMATIONREG_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_1234PID_5678REV_0100、USB\VID_1234PID_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_8086DEV_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 循环跑了几次之后突然返回 FALSEGetLastError 是 1784ERROR_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(); QVectorwchar_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_castMSG *(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_DUMP1时把结果写进 CSV。客户现场报“设备认不到”你把这份 dump 拿回来两分钟就能看出来是他的设备没插稳还是驱动没有 INSTALL还是枚举代码漏了设备类。这个习惯帮我省过不少远程排查的来回。希望这篇示例项目能让你第一次跑 SetupAPI 的时候少走弯路也希望你和我一样在写完一个能看清整台机器设备全貌的程序时感觉到 Windows 底层那层硬核的秩序感。本文还有配套的精品资源点击获取
返回列表