Qt 调 SetupAPI 读取设备管理器硬件信息:驱动版本与硬件 ID 实战
简介本资源为Qt调用Windows API中setupapi.h库获取设备管理器设备详细信息的示例项目源代码面向具备一定Qt与Windows开发基础的桌面应用开发者。项目通过SetupDiGetClassDevs与SetupDiEnumDeviceInfo等函数枚举设备列表并读取设备描述、图标、类名、GUID、设备实例路径、硬件ID、驱动INF名称、驱动版本、显示名称、供应商等属性值可用于设备管理工具开发、硬件信息采集或系统信息类项目参考。资源包共12个文件包含4个cpp源文件、3个h头文件、2个ui界面文件及pro工程文件、user配置与md说明文档压缩包约21KB结构精简便于直接编译运行与二次修改。目前已有141人学习下载。读者可从中掌握setupapi设备枚举流程、属性提取方法与Qt界面集成思路快速搭建自己的设备信息查看工具。1. 从设备管理器里把硬件信息“抠”出来Qt 调 SetupAPI 到底能拿到什么设备管理器里那一棵硬件树右键属性里能看到厂商、驱动版本、硬件 ID、占用资源这些信息在排查现场问题时比任何日志都直接。但很多做 Qt 上位机的同行第一反应是去读注册表或者调 WMI结果要么字段残缺要么在 64 位系统上被重定向搞得一头雾水。标题里说的这条路——用 Qt 调用 Windows API 中的SETUPAPI.H库——是更底层、更完整的一种做法它直接跟系统的设备安装服务对话拿到的是设备管理器同一份数据源。这套方案适合谁做工业上位机、测试工装、设备巡检工具的 Qt 开发者尤其是需要在软件里列出本机串口、USB 设备、PCI 设备清单并且要读到驱动版本和硬件 ID 的场景。它不依赖 COM 组件注册不需要管理员权限就能枚举大部分设备节点编译出来是纯 Win32 调用跟 Qt 的事件循环不冲突。下面从原理到代码把这条路走通。2. SetupAPI 与设备树先搞清楚你在枚举什么2.1 设备管理器背后的三层结构Windows 把设备信息组织成三层最上面是设备实例Device Instance每个实例有一个唯一标识叫设备实例 ID形如USB\VID_1A86PID_7523\52B3C4D5E01中间是设备类Device Class比如端口类、人机接口类、网络适配器类最下面是驱动节点Driver Node记录驱动文件、版本、日期。SetupAPI提供两组核心函数SetupDiGetClassDevs用来获取某个设备类或全部设备的句柄集合SetupDiEnumDeviceInfo用来遍历这个集合里的每一个设备信息元素。拿到元素后用SetupDiGetDeviceRegistryProperty读取属性用SetupDiGetDeviceInstanceId读实例 ID。设备管理器界面就是把这些属性按树形渲染出来的你调 API 拿到的字段跟界面上看到的是同一份。这里有个容易混淆的点SetupDiGetClassDevs的第一个参数是GUID传NULL配合DIGCF_ALLCLASSES可以枚举所有类如果只想看串口就传GUID_DEVCLASS_PORTS。第二个参数是枚举器通常传NULL表示当前机器。第三个参数是顶层窗口句柄一般传NULL。第四个参数是标志位常用组合是DIGCF_PRESENT | DIGCF_ALLCLASSES表示只要当前存在的设备、不限类别。2.2 为什么不用 WMI 和注册表WMI 查Win32_PnPEntity确实方便几行 WQL 就能出结果但它依赖 WMI 服务健康状态在一些精简版系统或服务被禁用的工控机上直接罢工。注册表路径HKLM\SYSTEM\CurrentControlSet\Enum虽然数据全但 32 位 Qt 程序在 64 位系统上访问会被重定向到Wow6432Node读出来的东西不全还得处理权限。SetupAPI是微软给设备安装程序用的官方接口不经过 WMI 服务不受注册表重定向影响稳定性高一个档次。另一个实际原因是速度。WMI 查询一次通常要几百毫秒起步设备多的时候更慢SetupAPI枚举几百个设备节点通常在几十毫秒内完成做实时刷新列表的工装软件时差距很明显。2.3 最小可跑通的枚举框架先看一段能直接编译的骨架代码作用是枚举当前所有存在的设备打印实例 ID 和描述。在.pro文件里需要加LIBS -lsetupapi头文件包含windows.h、setupapi.h、devguid.h。#include windows.h #include setupapi.h #include devguid.h #include QDebug void enumAllDevices() { // 获取所有类别的设备信息集合只要当前存在的 HDEVINFO devInfo SetupDiGetClassDevs( NULL, // 不限定 GUID NULL, // 不限定枚举器 NULL, // 无父窗口 DIGCF_PRESENT | DIGCF_ALLCLASSES ); if (devInfo INVALID_HANDLE_VALUE) { qDebug() SetupDiGetClassDevs failed: GetLastError(); return; } SP_DEVINFO_DATA devData; devData.cbSize sizeof(SP_DEVINFO_DATA); for (DWORD i 0; SetupDiEnumDeviceInfo(devInfo, i, devData); i) { // 读设备实例 ID char instanceId[512] {0}; if (SetupDiGetDeviceInstanceIdA(devInfo, devData, instanceId, sizeof(instanceId), NULL)) { qDebug() InstanceId: instanceId; } // 读设备描述 char desc[512] {0}; DWORD regType 0; DWORD needed 0; if (SetupDiGetDeviceRegistryPropertyA(devInfo, devData, SPDRP_DEVICEDESC, regType, (PBYTE)desc, sizeof(desc), needed)) { qDebug() Desc: desc; } } SetupDiDestroyDeviceInfoList(devInfo); }逻辑说明SetupDiGetClassDevs返回的是一个信息集合句柄不是单个设备。SetupDiEnumDeviceInfo按索引遍历索引从 0 开始返回FALSE且GetLastError()为ERROR_NO_MORE_ITEMS时表示遍历结束。SP_DEVINFO_DATA结构体必须先设置cbSize否则函数直接失败这是最常见的翻车点。参数说明SPDRP_DEVICEDESC是设备描述属性对应设备管理器里显示的名称。SetupDiGetDeviceRegistryPropertyA的needed参数返回实际需要的字节数如果缓冲区不够会返回FALSE并设置ERROR_INSUFFICIENT_BUFFER此时可以按needed重新分配再调一次。缓冲区建议至少 512 字节某些长设备名会超过 256。3. 把设备管理器里的字段逐个读出来属性、类名、驱动版本3.1 常用属性对照表SetupDiGetDeviceRegistryProperty的第二个参数决定读哪个字段下面这张表是我在实际项目里最常用的几个对应设备管理器界面上的位置也标了出来。属性常量含义设备管理器对应位置SPDRP_DEVICEDESC设备描述设备名称SPDRP_FRIENDLYNAME友好名称带编号的显示名SPDRP_HARDWAREID硬件 ID 列表详细信息→硬件 IDSPDRP_MFG制造商详细信息→制造商SPDRP_DRIVER驱动注册表键详细信息→驱动程序键SPDRP_CLASS设备类名详细信息→类SPDRP_CLASSGUID设备类 GUID详细信息→类 GUID硬件 ID 是REG_MULTI_SZ类型返回的是多个以\0分隔的字符串最后以双\0结束。读的时候不能当普通字符串处理要按多字符串遍历。驱动版本不在SPDRP_DRIVER里那个只是注册表路径版本号需要用SetupDiBuildDriverInfoList配合SetupDiEnumDriverInfo才能拿到或者读SPDRP_DRIVER指向的注册表键下的DriverVersion值。3.2 读硬件 ID 多字符串的正确姿势下面这段代码演示怎么把SPDRP_HARDWAREID读出来并拆成多个字符串。很多示例代码直接当单字符串打印结果只看到第一个 ID后面的全丢了。QStringList readHardwareIds(HDEVINFO devInfo, SP_DEVINFO_DATA *devData) { QStringList ids; DWORD regType 0; DWORD needed 0; // 先探大小 SetupDiGetDeviceRegistryPropertyA(devInfo, devData, SPDRP_HARDWAREID, regType, NULL, 0, needed); if (needed 0) return ids; QByteArray buf(needed, 0); if (!SetupDiGetDeviceRegistryPropertyA(devInfo, devData, SPDRP_HARDWAREID, regType, (PBYTE)buf.data(), needed, needed)) { return ids; } // REG_MULTI_SZ按 \0 切分遇到空串结束 const char *p buf.constData(); while (*p) { ids QString::fromLatin1(p); p strlen(p) 1; } return ids; }逻辑说明第一次调用传NULL缓冲区和 0 大小函数返回FALSE但needed会被填上实际需要的字节数这是标准的“探大小”模式。拿到大小后分配缓冲区再调一次。多字符串的遍历用strlen步进每次跳过字符串本身加一个终止符直到遇到空字符串。参数说明regType会返回REG_MULTI_SZ可以据此判断是否是多字符串。如果设备没有硬件 ID 属性第一次调用needed就是 0直接返回空列表不要继续分配。3.3 获取驱动版本和驱动日期驱动版本需要另一条路径。先用SetupDiGetDeviceRegistryProperty读SPDRP_DRIVER拿到驱动键名然后拼出完整注册表路径HKLM\SYSTEM\CurrentControlSet\Control\Class\{类GUID}\驱动键名再读DriverVersion和DriverDate两个值。下面用 Qt 的QSettings来读比直接调RegOpenKeyEx简洁。QString readDriverVersion(HDEVINFO devInfo, SP_DEVINFO_DATA *devData) { char driverKey[512] {0}; DWORD regType 0, needed 0; if (!SetupDiGetDeviceRegistryPropertyA(devInfo, devData, SPDRP_DRIVER, regType, (PBYTE)driverKey, sizeof(driverKey), needed)) { return QString(); } // 读类 GUID char classGuid[64] {0}; SetupDiGetDeviceRegistryPropertyA(devInfo, devData, SPDRP_CLASSGUID, regType, (PBYTE)classGuid, sizeof(classGuid), needed); QString path QString(HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet \\Control\\Class\\%1) .arg(QString::fromLatin1(classGuid)); QSettings reg(path, QSettings::NativeFormat); return reg.value(DriverVersion).toString(); }逻辑说明SPDRP_DRIVER返回的是驱动键的相对路径必须跟类 GUID 拼起来才是完整路径。QSettings用NativeFormat直接读注册表键名不带反斜杠前缀。驱动日期字段是DriverDate格式是yyyy-MM-dd读出来直接能用。参数说明类 GUID 带花括号拼路径时保留花括号。如果SPDRP_DRIVER读不到说明设备没有关联驱动直接返回空。QSettings在 32 位程序里读 64 位注册表视图需要加QSettings::Registry64Format否则可能读不到这是 64 位系统上的一个坑。4. 避坑与排查SetupAPI 调用中最容易翻车的五个地方4.1 现象SetupDiEnumDeviceInfo一直返回 FALSE一个设备都枚举不到原因SP_DEVINFO_DATA的cbSize没赋值或者赋成了sizeof(SP_DEVINFO_DATA*)这种指针大小。函数靠cbSize判断结构体版本值不对直接拒绝。解决每次声明SP_DEVINFO_DATA后立刻写devData.cbSize sizeof(SP_DEVINFO_DATA);不要用memset清零代替清零后cbSize是 0 同样失败。4.2 现象读出来的中文设备名是乱码原因调用了SetupDiGetDeviceRegistryPropertyA的 ANSI 版本但系统返回的是 Unicode 字符串或者反过来。Windows 内部统一用 UnicodeANSI 版本会做转换转换依赖当前代码页跨语言系统上容易出问题。解决统一用SetupDiGetDeviceRegistryPropertyW宽字符版本配合QString::fromWCharArray转成 Qt 字符串。如果坚持用 A 版本确保项目字符集设置一致但我不推荐宽字符版本更省心。4.3 现象硬件 ID 只读到第一个后面的丢了原因把REG_MULTI_SZ当普通字符串处理strlen只算到第一个\0就停了。解决按 3.2 节的遍历方式用strlen步进并跳过终止符直到遇到空字符串。判断类型时检查regType REG_MULTI_SZ。4.4 现象程序在 64 位系统上读不到某些设备的驱动信息原因32 位 Qt 程序默认访问Wow6432Node重定向后的注册表视图而设备驱动的类信息在 64 位视图下。解决用QSettings时指定QSettings::Registry64Format或者直接调RegOpenKeyEx时加KEY_WOW64_64KEY标志。如果程序本身就是 64 位编译这个问题不存在。4.5 现象枚举完设备后程序内存持续增长原因SetupDiGetClassDevs返回的HDEVINFO句柄没有释放每次刷新都泄漏一个信息集合。解决每次枚举结束后必须调SetupDiDestroyDeviceInfoList(devInfo)放在函数退出前的所有路径上。用 RAII 封装一个守卫类更稳妥避免提前 return 时漏掉。5. 封装成可复用的设备信息读取类与刷新策略5.1 用 Qt 风格封装 SetupAPI直接把 Win32 调用散落在业务代码里维护起来很痛苦。我一般封装一个DeviceInfoProvider类对外只暴露QListDeviceInfo内部处理句柄生命周期和字符串转换。DeviceInfo结构体包含实例 ID、描述、类名、硬件 ID 列表、驱动版本五个字段够覆盖大部分工装需求。struct DeviceInfo { QString instanceId; QString description; QString className; QStringList hardwareIds; QString driverVersion; }; class DeviceInfoProvider { public: static QListDeviceInfo enumerate(const GUID *classGuid nullptr) { QListDeviceInfo result; HDEVINFO devInfo SetupDiGetClassDevs( classGuid, NULL, NULL, DIGCF_PRESENT | (classGuid ? 0 : DIGCF_ALLCLASSES)); if (devInfo INVALID_HANDLE_VALUE) return result; SP_DEVINFO_DATA devData; devData.cbSize sizeof(SP_DEVINFO_DATA); for (DWORD i 0; SetupDiEnumDeviceInfo(devInfo, i, devData); i) { DeviceInfo info; info.instanceId readString(devInfo, devData, SPDRP_DEVICEDESC); info.description readString(devInfo, devData, SPDRP_FRIENDLYNAME); info.className readString(devInfo, devData, SPDRP_CLASS); info.hardwareIds readMultiString(devInfo, devData, SPDRP_HARDWAREID); info.driverVersion readDriverVersion(devInfo, devData); result.append(info); } SetupDiDestroyDeviceInfoList(devInfo); return result; } };逻辑说明enumerate接受一个可选的类 GUID传nullptr时枚举全部设备传GUID_DEVCLASS_PORTS时只枚举端口类。readString和readMultiString是内部辅助函数分别处理单字符串和多字符串属性。句柄在函数退出前统一释放不会泄漏。参数说明DIGCF_PRESENT保证只返回当前插着的设备拔掉的设备不会出现。如果要做设备插拔监控需要配合WM_DEVICECHANGE消息在消息里重新调enumerate刷新列表而不是轮询。5.2 刷新策略与性能取舍工装软件里设备列表通常需要实时刷新。我的做法是监听WM_DEVICECHANGE在DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE两个事件里触发重新枚举而不是开定时器每秒轮询。轮询在设备多的时候会持续占用 CPU而且枚举过程中如果有设备正在安装驱动可能读到半成品状态。如果必须轮询间隔不要低于 2 秒并且把枚举放在独立线程里避免阻塞 UI。枚举本身虽然快但读驱动版本要访问注册表设备多的时候累计延迟不可忽略。实测 200 个设备节点全量读取大约 80 到 120 毫秒放在主线程会感觉到列表刷新时的卡顿。5.3 按类过滤的实用场景实际项目里很少需要列出全部设备通常是找串口、找 USB 设备、找特定厂商的 PCI 卡。按类过滤用GUID_DEVCLASS_PORTS拿串口用GUID_DEVCLASS_USB拿 USB 控制器用GUID_DEVCLASS_NET拿网卡。如果要按厂商过滤枚举出来后用硬件 ID 里的VID_前缀匹配比在枚举阶段过滤更灵活。// 只枚举串口设备 QListDeviceInfo ports DeviceInfoProvider::enumerate(GUID_DEVCLASS_PORTS); for (const auto p : ports) { qDebug() p.description p.hardwareIds; }逻辑说明GUID_DEVCLASS_PORTS定义在devguid.h里直接取地址传入。返回的列表里每个元素都带完整硬件 ID可以用VID_和PID_进一步筛选特定芯片的串口比如 CH340 的VID_1A86。参数说明类 GUID 是常量不要动态构造。如果项目里需要多个类把 GUID 做成配置项运行时传入避免硬编码散落各处。5.4 验证读取结果是否与设备管理器一致写完代码后怎么确认读对了打开设备管理器随便找一个设备右键属性→详细信息把“硬件 ID”和“驱动程序版本”两个字段跟程序输出对比。硬件 ID 应该完全一致包括顺序。驱动版本如果对不上检查是不是读到了DriverVersion以外的字段或者注册表视图不对。另一个验证方法是拔掉一个 USB 设备重新枚举看列表里是否少了一项插上后再枚举看是否多出来。如果拔掉后设备还在列表里说明DIGCF_PRESENT没生效检查标志位是否被覆盖。这套方案我用了几年从串口工具到整机巡检软件都靠它拿设备信息比 WMI 稳、比注册表全。唯一需要留心的是宽字符和注册表视图这两个点踩过一次之后基本不会再犯。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →