☰
Qt5 USB设备检测实战:QStorageInfo与libusb热插拔方案
2026/10/15 18:46:34 网站建设 项目流程

简介:本资源面向需要在 Qt5 环境下实现 USB 设备热插拔检测的开发者,尤其适合从事硬件交互、设备管理类桌面应用的中级 Qt 使用者。资源核心围绕 QDeviceWatcher 类展开,演示如何监听 USB 设备的插入与移除事件,并通过 deviceAdded、deviceRemoved 信号触发自定义处理逻辑,同时兼顾 Windows、Linux、macOS 多平台兼容性。压缩包共 72 个文件,约 1.18MB,包含 12 个 cpp 源文件、7 个 h 头文件、4 个 pro 工程文件及多个 obj、o 编译产物,另有 so、dll 动态库与 vcproj、sln 工程配置,覆盖从源码到可执行文件的完整链路。该工程已在 wang-bin-qdevicewatcher-2018.1.30-2052 版本上编译成功,可直接参考其目录组织与配置方式。目前已有 1383 人学习下载,读者可借此掌握设备监控的接入方式、信号槽连接写法及跨平台适配思路,快速将 USB 检测能力集成到自己的 Qt5 项目中。

1. 为什么“QT5检测USB设备”值得你花一个下午跑通

如果你正在用 Qt 做上位机、工控面板或者测试工具,迟早会遇到一个需求:设备插上了要自动认出来,拔掉了要立刻知道,而不是让用户点一下“刷新”按钮。这就是“QT5检测USB设备”要解决的事。它听起来简单,但真正落地时会发现,Qt 本身并没有一个跨平台的 USB 热插拔统一接口,Windows、Linux 各有各的脾气。我见过太多项目在这上面翻车:要么轮询把 CPU 吃满,要么拔掉设备后句柄没释放导致下次插上认不出来。这篇文章不讲空泛概念,只讲我实际用过的两条路——一条是 Qt 自带的QStorageInfo加QFileSystemWatcher做轻量级检测,另一条是接libusb做真正的设备级识别。前者适合 U 盘、存储类设备,后者适合自定义 HID、串口转 USB 的工控设备。读完你能自己判断该走哪条路,并且能直接抄走可运行的代码。

2. 先搞清楚 Qt5 能拿到哪些 USB 信息:三条技术路线的选型对比

在动手写代码之前,得先明白 Qt5 对 USB 设备的“可见度”到底在哪一层。很多新手一上来就搜“Qt USB 检测”,结果找到的是QSerialPort或者QUsbDevice这种第三方库,方向就偏了。Qt5 官方模块里,跟 USB 沾边的其实只有存储设备枚举和文件系统监控,真正的 USB 设备描述符、VID/PID 这些,官方是不提供的。所以选型本质上是在问:你要检测的是“一个盘”还是“一个设备”。

2.1 存储类设备:QStorageInfo 能拿到什么、拿不到什么

QStorageInfo是 Qt5 里最容易被低估的类。它不需要任何额外依赖,跨平台可用,能枚举出当前挂载的所有卷。对于 U 盘、移动硬盘这类设备,它给出的信息已经够用了:挂载点、文件系统类型、总容量、可用容量、设备名。但它拿不到 USB 的 VID/PID,也拿不到设备序列号。如果你的需求只是“U 盘插上后自动把里面的日志拷出来”,那QStorageInfo就是最省事的方案。

我一般会这样写一个枚举函数,先看看当前系统认出了哪些卷:

#include <QStorageInfo> #include <QDebug> void listStorageVolumes() { // 获取所有已挂载的存储卷 const QList<QStorageInfo> volumes = QStorageInfo::mountedVolumes(); for (const QStorageInfo &vol : volumes) { // 跳过根分区和只读系统分区,只关注可移动设备 if (vol.isRoot() || vol.isReadOnly()) continue; qDebug() << "挂载点:" << vol.rootPath() << "设备名:" << vol.device() << "文件系统:" << vol.fileSystemType() << "总大小(GB):" << vol.bytesTotal() / 1024.0 / 1024.0 / 1024.0 << "可用(GB):" << vol.bytesAvailable() / 1024.0 / 1024.0 / 1024.0; } }

这段代码的逻辑很直白:遍历所有挂载卷,过滤掉根分区和只读分区,剩下的基本就是外接存储。参数上唯一需要注意的是bytesTotal()返回的是字节数,除以三次 1024 才是 GB。如果你在 Windows 上跑,会发现 U 盘的device()返回的是\\.\E:这种形式;在 Linux 上则是/dev/sdb1。这个差异后面做热插拔判断时会用到。

但QStorageInfo有个硬伤:它只能告诉你“现在有什么”,不能告诉你“什么时候变了”。所以必须配合QFileSystemWatcher或者定时器来做变化感知。

2.2 非存储类设备:为什么必须引入 libusb

如果你要检测的是一个自定义的 USB 工控板、一个 HID 扫码枪、或者一个 USB 转串口模块,QStorageInfo就完全无能为力了,因为它根本不会挂载成文件系统。这时候唯一靠谱的路子是走 libusb。libusb 是一个跨平台的用户态 USB 库,Windows 上可以用 WinUSB 或 libusbK 驱动,Linux 上直接走 usbfs。Qt5 本身不封装 libusb,但你可以把 libusb 的头文件和库链接进 Qt 工程,然后在 Qt 的事件循环里开一个低频率的轮询线程。

选 libusb 的理由有三个:第一,它能拿到设备的 VID、PID、序列号、制造商字符串,这些是识别“是不是我的设备”的唯一可靠依据;第二,它支持热插拔回调(Linux 下是 netlink,Windows 下是窗口消息),不用自己写轮询;第三,它的 API 是纯 C 的,跟 Qt 的 C++ 混编没有障碍。代价是你得自己处理驱动安装问题,Windows 上如果设备没装 WinUSB 驱动,libusb 是打不开的。

2.3 选型决策表:按你的设备类型对号入座

检测目标推荐方案能否拿到 VID/PID热插拔实时性额外依赖
U 盘 / 移动硬盘QStorageInfo + QFileSystemWatcher否秒级无
USB 转串口QSerialPort + 轮询部分(需解析)秒级无
自定义 HID / 工控板libusb + 热插拔回调是毫秒级libusb 库
复合设备(存储+串口)libusb 为主,QStorageInfo 为辅是毫秒级libusb 库

这张表是我踩过几次坑之后总结的。最典型的翻车场景是:有人用QStorageInfo去检测一个 USB 转串口设备,结果发现设备插上了但没挂载,程序一直没反应。所以选型第一步永远是确认你的设备在系统里到底以什么形态出现。

3. 用 QStorageInfo 加 QFileSystemWatcher 做 U 盘热插拔检测

这一章讲的是最轻量的方案,适合不想引入任何第三方库、只做存储设备监控的场景。核心思路是:用QFileSystemWatcher监控几个关键挂载目录的变化,一旦目录内容变了,就重新枚举QStorageInfo,跟上一次的快照做对比,从而判断是插入还是拔出。

3.1 监控哪些目录:Windows 和 Linux 的差异

QFileSystemWatcher只能监控存在的目录,不能监控一个还不存在的挂载点。所以监控策略要分平台。Windows 上,U 盘会挂载成盘符,比如E:\、F:\,你没法提前知道会分到哪个字母,所以通常的做法是监控QDir::drives()返回的列表,或者干脆用一个 500 毫秒的定时器轮询。Linux 上就友好很多,/media和/mnt是常见的挂载父目录,你可以直接监控这两个目录,当 U 盘插入时,系统会在/media/用户名/下创建一个新目录,QFileSystemWatcher会立刻收到directoryChanged信号。

我一般会这样初始化监控:

#include <QFileSystemWatcher> #include <QStorageInfo> #include <QTimer> class UsbStorageMonitor : public QObject { Q_OBJECT public: explicit UsbStorageMonitor(QObject *parent = nullptr) : QObject(parent) { watcher = new QFileSystemWatcher(this); // Linux 下监控常见挂载父目录 const QStringList watchDirs = {"/media", "/mnt", "/run/media"}; for (const QString &dir : watchDirs) { if (QDir(dir).exists()) watcher->addPath(dir); } // Windows 下没有固定父目录,用定时器兜底 timer = new QTimer(this); timer->setInterval(800); // 800ms 轮询一次,兼顾响应和 CPU connect(timer, &QTimer::timeout, this, &UsbStorageMonitor::refresh); timer->start(); connect(watcher, &QFileSystemWatcher::directoryChanged, this, &UsbStorageMonitor::refresh); refresh(); // 启动时先拍一次快照 } private slots: void refresh(); private: QFileSystemWatcher *watcher; QTimer *timer; QSet<QString> lastDevices; // 上一次枚举到的设备集合 };

这段代码的关键点在于:Linux 下QFileSystemWatcher能直接感知到/media下新建的目录,触发directoryChanged;Windows 下因为没有统一的挂载父目录,所以用 800 毫秒的定时器兜底。800 这个数字不是随便定的,低于 500 毫秒在低配工控机上会有可感知的 CPU 占用,高于 1000 毫秒用户会觉得“插了半天没反应”。

3.2 快照对比法:怎么判断是插入还是拔出

refresh()函数是整个方案的核心。它的逻辑是:每次触发时重新枚举所有可移动存储设备,生成一个设备标识集合,跟lastDevices做差集。新增的就是插入,消失的就是拔出。

void UsbStorageMonitor::refresh() { QSet<QString> currentDevices; const QList<QStorageInfo> volumes = QStorageInfo::mountedVolumes(); for (const QStorageInfo &vol : volumes) { if (vol.isRoot() || vol.isReadOnly()) continue; // 用设备名+大小作为唯一标识,避免盘符变化导致误判 QString id = vol.device() + "|" + QString::number(vol.bytesTotal()); currentDevices.insert(id); } // 新增的设备 = 当前有但上次没有 QSet<QString> inserted = currentDevices - lastDevices; // 移除的设备 = 上次有但当前没有 QSet<QString> removed = lastDevices - currentDevices; for (const QString &id : inserted) { emit deviceInserted(id); qDebug() << "设备插入:" << id; } for (const QString &id : removed) { emit deviceRemoved(id); qDebug() << "设备拔出:" << id; } lastDevices = currentDevices; }

这里有个血泪经验:设备标识不要只用盘符。Windows 上同一个 U 盘这次插是E:,下次插可能变成F:,如果你用盘符做标识,会误判成“拔出了 E 盘,插入了 F 盘”。用device() + bytesTotal()组合就稳很多,因为同一个设备的容量不会变。但如果你有两个同型号同容量的 U 盘,这个方案会混淆,那就需要进一步读卷序列号,不过那就超出QStorageInfo的能力范围了。

3.3 信号槽设计:把检测结果干净地传给业务层

检测模块不应该直接操作 UI 或者业务逻辑,它只负责发信号。我习惯定义两个信号:deviceInserted(QString id)和deviceRemoved(QString id)。业务层拿到信号后,自己去查这个 id 对应的挂载点,然后决定是拷贝文件、弹出提示还是启动上传。

// 业务层连接示例 connect(monitor, &UsbStorageMonitor::deviceInserted, this, [](const QString &id){ // 从 id 里解析出挂载点,这里简化处理 QString mountPoint = id.split("|").first(); qDebug() << "准备处理新设备,挂载点:" << mountPoint; // 实际业务:扫描文件、校验、上传 });

这种设计的另一个好处是,你可以把UsbStorageMonitor放到单独的线程里,通过moveToThread让它不影响主界面响应。不过对于 800 毫秒一次的轮询来说,主线程完全扛得住,没必要过度设计。

4. 接 libusb 做设备级识别:VID/PID 过滤与热插拔回调

当你需要精确识别“是不是我的设备”时,QStorageInfo那套就不够用了。这一章讲怎么把 libusb 集成进 Qt5 工程,用 VID/PID 做过滤,并且用 libusb 的热插拔回调实现毫秒级响应。

4.1 工程配置:.pro 文件里怎么链接 libusb

在 Qt5 的.pro文件里链接 libusb 需要分平台写。Linux 下通常直接-lusb-1.0,Windows 下需要指定库文件路径和libusb-1.0.lib。

# 在 .pro 文件中添加 unix:!macx { LIBS += -lusb-1.0 INCLUDEPATH += /usr/include/libusb-1.0 } win32 { LIBS += -L$$PWD/third_party/libusb/lib -llibusb-1.0 INCLUDEPATH += $$PWD/third_party/libusb/include # Windows 下还需要链接 SetupAPI LIBS += -lsetupapi }

这里要注意,Windows 下 libusb 依赖setupapi.lib,不链接的话会在编译期报一堆未定义符号。另外,libusb 的 Windows 二进制包通常分 MSVC 和 MinGW 两个版本,如果你用 Qt 的 MinGW 套件,必须下对应的 MinGW 版,混用会链接失败。

4.2 枚举设备并按 VID/PID 过滤的最小代码

初始化 libusb 并枚举设备是第一步。下面这段代码会遍历所有 USB 设备,打印出 VID 和 PID,你可以先跑一遍拿到自己设备的数值,再写死到过滤条件里。

#include <libusb-1.0/libusb.h> #include <QDebug> // 假设你的设备 VID 是 0x1234,PID 是 0x5678 #define MY_VID 0x1234 #define MY_PID 0x5678 void enumerateUsbDevices() { libusb_device **devs = nullptr; libusb_context *ctx = nullptr; int r = libusb_init(&ctx); if (r < 0) { qDebug() << "libusb 初始化失败,错误码:" << r; return; } ssize_t cnt = libusb_get_device_list(ctx, &devs); if (cnt < 0) { qDebug() << "获取设备列表失败"; libusb_exit(ctx); return; } for (ssize_t i = 0; i < cnt; i++) { libusb_device *dev = devs[i]; libusb_device_descriptor desc; r = libusb_get_device_descriptor(dev, &desc); if (r < 0) continue; // 按 VID/PID 过滤 if (desc.idVendor == MY_VID && desc.idProduct == MY_PID) { qDebug() << "找到目标设备,VID:" << QString::number(desc.idVendor, 16) << "PID:" << QString::number(desc.idProduct, 16) << "总线号:" << libusb_get_bus_number(dev) << "设备地址:" << libusb_get_device_address(dev); } } libusb_free_device_list(devs, 1); libusb_exit(ctx); }

这段代码里,libusb_get_device_descriptor拿到的是设备描述符,idVendor和idProduct就是 VID/PID。libusb_get_bus_number和libusb_get_device_address组合起来可以唯一标识一个物理端口,这在多设备场景下很有用。注意libusb_free_device_list的第二个参数传 1,表示同时解除对设备的引用,不传的话会内存泄漏。

4.3 热插拔回调:不用轮询也能知道设备来了

libusb 从 1.0.16 开始支持热插拔回调,Linux 下走 netlink,Windows 下走窗口消息。用法是先注册一个回调函数,然后在一个循环里调用libusb_handle_events。

#include <libusb-1.0/libusb.h> #include <QDebug> #include <QThread> // 热插拔回调,注意这个函数在 libusb 的内部线程里执行 int hotplugCallback(libusb_context *ctx, libusb_device *dev, libusb_hotplug_event event, void *user_data) { Q_UNUSED(ctx); Q_UNUSED(user_data); struct libusb_device_descriptor desc; libusb_get_device_descriptor(dev, &desc); if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) { qDebug() << "设备插入,VID:" << QString::number(desc.idVendor, 16) << "PID:" << QString::number(desc.idProduct, 16); } else if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT) { qDebug() << "设备拔出,VID:" << QString::number(desc.idVendor, 16) << "PID:" << QString::number(desc.idProduct, 16); } return 0; } // 在独立线程里跑事件循环 void startHotplugMonitor() { libusb_context *ctx = nullptr; libusb_init(&ctx); libusb_hotplug_callback_handle handle; int r = libusb_hotplug_register_callback( ctx, (libusb_hotplug_event)(LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT), LIBUSB_HOTPLUG_ENUMERATE, // 注册时先枚举已存在的设备 MY_VID, MY_PID, LIBUSB_HOTPLUG_MATCH_ANY, // 匹配任意接口 hotplugCallback, nullptr, &handle); if (r != LIBUSB_SUCCESS) { qDebug() << "热插拔回调注册失败:" << r; return; } // 事件循环,必须持续调用才能收到回调 while (true) { r = libusb_handle_events(ctx); if (r < 0) { qDebug() << "事件处理出错:" << r; break; } } }

这里有几个关键参数:LIBUSB_HOTPLUG_ENUMERATE表示注册时先把当前已经插着的设备也回调一遍,这样程序启动时不会漏掉已插入的设备。LIBUSB_HOTPLUG_MATCH_ANY表示不限制接口号,因为有些设备有多个接口。回调函数里不要做耗时操作,因为它在 libusb 的内部线程执行,阻塞久了会丢事件。我一般是在回调里发一个 Qt 信号到主线程,让主线程去处理业务。

5. 避坑与排查:USB 检测最容易翻车的五个地方

这一章是我自己踩过的坑,每一个都真实发生过,按“现象 → 原因 → 解决”写清楚。

5.1 现象:设备拔掉后重新插上,程序认不出来

原因:QStorageInfo的快照里还留着旧设备的缓存,或者 libusb 的设备句柄没有释放。Windows 上尤其常见,因为盘符可能被系统保留了一段时间。

解决:在deviceRemoved信号里强制清空lastDevices中对应的条目,并且调用QStorageInfo::refresh()强制刷新缓存。libusb 那边要确保libusb_close被调用,不要只libusb_unref_device。

5.2 现象:Linux 下监控 /media 没反应,但手动 ls 能看到目录

原因:QFileSystemWatcher在 Linux 下用的是 inotify,而/media下的子目录是系统挂载时动态创建的,inotify 默认不监控子目录的创建事件,只监控目录本身的修改。

解决:不要只监控/media,要把/media/用户名这一层也加进去。或者改用QTimer轮询,500 到 800 毫秒一次,虽然土但稳定。我现在的做法是两者都用,QFileSystemWatcher做快速响应,定时器做兜底。

5.3 现象:Windows 下 libusb 打开设备返回 LIBUSB_ERROR_ACCESS

原因:设备没有安装 WinUSB 或 libusbK 驱动,系统默认用的是厂商驱动,libusb 没有权限访问。

解决:用 Zadig 工具给设备替换驱动,或者在你的安装包里集成一个 inf 文件。注意替换驱动后,厂商自带的软件可能就用不了这个设备了,这是个取舍。如果设备是复合设备(比如既有存储又有自定义接口),可以只给自定义接口替换驱动,存储接口保留原样。

5.4 现象:热插拔回调触发了两次

原因:libusb 在 Linux 下,一个物理设备如果有多个配置或接口,可能会触发多次回调。另外,如果注册回调时用了LIBUSB_HOTPLUG_ENUMERATE,程序启动时会先回调一次已存在的设备,然后你手动枚举时又处理了一次。

解决:在回调里用libusb_get_bus_number和libusb_get_device_address组合成唯一 key,用一个QSet去重。或者在业务层做幂等处理,同一个设备短时间内重复插入信号只响应第一次。

5.5 现象:程序退出时崩溃,报段错误

原因:libusb 的事件循环线程还在跑,但libusb_context已经被libusb_exit释放了。或者QFileSystemWatcher还在监控一个已经被卸载的目录。

解决:在程序退出前,先给事件循环线程发一个退出标志,等线程真正结束后再调用libusb_exit。Qt 这边在aboutToQuit信号里移除所有监控路径。顺序反了必崩,这是血泪教训。

6. 进阶技巧:用 udev 规则和 Windows 消息做零轮询检测

如果你追求极致的响应速度和零 CPU 占用,轮询方案终究是不够优雅的。这一章讲两个平台原生的零轮询方案,算是给已经跑通基础版本的读者一个进阶方向。

Linux 下,udev 是设备管理的核心。你可以写一条 udev 规则,当特定 VID/PID 的设备插入时,让内核执行一个脚本或者发一个信号给你的程序。具体做法是在/etc/udev/rules.d/下建一个99-myusb.rules文件,内容类似:

# 当 VID=1234 PID=5678 的设备插入时,创建一个标记文件 SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", ACTION=="add", RUN+="/bin/touch /tmp/myusb_inserted" SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", ACTION=="remove", RUN+="/bin/rm -f /tmp/myusb_inserted"

然后你的 Qt 程序只需要用QFileSystemWatcher监控/tmp/myusb_inserted这个文件的存在性,就能做到零轮询。这个方案的响应速度是毫秒级的,而且 CPU 占用为零。缺点是需要在目标机器上预置 udev 规则,对部署环境有要求。

Windows 下,你可以注册一个隐藏窗口来接收WM_DEVICECHANGE消息。Qt 里可以通过重写QWidget::nativeEvent来实现:

bool MyWidget::nativeEvent(const QByteArray &eventType, void *message, long *result) { MSG *msg = static_cast<MSG *>(message); if (msg->message == WM_DEVICECHANGE) { switch (msg->wParam) { case DBT_DEVICEARRIVAL: qDebug() << "有设备插入"; // 这里可以进一步解析 DEV_BROADCAST_DEVICEINTERFACE 拿到 VID/PID break; case DBT_DEVICEREMOVECOMPLETE: qDebug() << "有设备拔出"; break; } } return QWidget::nativeEvent(eventType, message, result); }

这个方案的好处是完全事件驱动,没有任何轮询开销。但要注意,WM_DEVICECHANGE是广播消息,系统里任何设备变化都会触发,你需要在处理函数里用RegisterDeviceNotification注册特定的设备接口 GUID,才能只收到你关心的设备。不注册的话,U 盘插拔、打印机上线都会让你的回调被调用,虽然不会出错,但日志会很难看。

我现在的习惯是:Linux 下优先用 udev 规则加文件监控,Windows 下优先用WM_DEVICECHANGE加接口 GUID 过滤。只有在跨平台一致性要求极高、不想维护两套逻辑时,才退回 libusb 热插拔回调。这个选择没有绝对的对错,取决于你的部署环境和团队维护成本。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询