恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Qt5 USB设备检测实战:QStorageInfo与libusb热插拔方案
首页
资讯中心
/
Qt5 USB设备检测实战:QStorageInfo与libusb热插拔方案
Qt5 USB设备检测实战:QStorageInfo与libusb热插拔方案
发布时间:2026/10/15 18:46:36
简介本资源面向需要在 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 QListQStorageInfo 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 下是 netlinkWindows 下是窗口消息不用自己写轮询第三它的 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; QSetQString lastDevices; // 上一次枚举到的设备集合 };这段代码的关键点在于Linux 下QFileSystemWatcher能直接感知到/media下新建的目录触发directoryChangedWindows 下因为没有统一的挂载父目录所以用 800 毫秒的定时器兜底。800 这个数字不是随便定的低于 500 毫秒在低配工控机上会有可感知的 CPU 占用高于 1000 毫秒用户会觉得“插了半天没反应”。3.2 快照对比法怎么判断是插入还是拔出refresh()函数是整个方案的核心。它的逻辑是每次触发时重新枚举所有可移动存储设备生成一个设备标识集合跟lastDevices做差集。新增的就是插入消失的就是拔出。void UsbStorageMonitor::refresh() { QSetQString currentDevices; const QListQStorageInfo 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); } // 新增的设备 当前有但上次没有 QSetQString inserted currentDevices - lastDevices; // 移除的设备 上次有但当前没有 QSetQString 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.0Windows 下需要指定库文件路径和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 是 0x1234PID 是 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 下走 netlinkWindows 下走窗口消息。用法是先注册一个回调函数然后在一个循环里调用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文件内容类似# 当 VID1234 PID5678 的设备插入时创建一个标记文件 SUBSYSTEMusb, ATTR{idVendor}1234, ATTR{idProduct}5678, ACTIONadd, RUN/bin/touch /tmp/myusb_inserted SUBSYSTEMusb, ATTR{idVendor}1234, ATTR{idProduct}5678, ACTIONremove, 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_castMSG *(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 热插拔回调。这个选择没有绝对的对错取决于你的部署环境和团队维护成本。希望帮到你。本文还有配套的精品资源点击获取