简介:这份资源是面向Windows平台下Qt6开发者的实战案例源码,聚焦如何借助QProcess调用系统命令获取计算机硬件信息,适合已掌握C++与Qt基础、希望深入理解进程通信与系统信息采集的初中级开发者参考。压缩包共5个文件,约5KB,以cpp源文件与h头文件为核心,配合pro工程文件与user配置,构成一个可直接编译运行的完整Qt工程,便于快速导入Qt Creator对照调试。案例围绕Windows管理工具wmic展开,演示通过命令行接口读取CPU、主板、硬盘等硬件参数,并借助QProcess捕获输出、解析结果,帮助读者掌握进程启动、参数传递与结果读取的完整链路。目前已有1029人学习下载,可作为课程设计、工具开发或系统监控类项目的起步模板,也可在此基础上扩展内存、显卡等信息采集与界面展示。
1. 用 QProcess 把硬件信息从系统底层捞出来:Qt6 桌面端绕不开的一步
做 Qt6 桌面工具的人迟早会撞上一个需求:把当前机器的 CPU 型号、内存容量、磁盘序列号、显卡信息读出来,显示在「关于」页面或者写进日志。Qt 本身没有跨平台的硬件信息 API,QSystemInfo 那套东西覆盖的字段又少得可怜。真正在生产里跑得通的方案,是用 QProcess 去调系统自带的命令行工具,把 stdout 抓回来解析。这个思路听起来土,但它是目前 Qt6 项目里最稳、最可控、依赖最少的做法。本文面向正在用 Qt6 写桌面端、需要采集本机硬件信息的开发者,从 QProcess 的调用姿势讲到各平台命令的差异,再到解析和踩坑,全部落到能直接抄的代码上。如果你正在纠结「Qt6 怎么读硬件信息」,这篇就是给你写的。
2. QProcess 调外部命令:同步、异步与信号槽怎么选
2.1 为什么不用 QSystemInfo 而用 QProcess
Qt 官方提供的 QSystemInfo 属于 Qt Systems 模块,字段覆盖有限,很多关键信息拿不到,比如内存条频率、磁盘序列号、主板型号。而且这个模块在不同平台上的实现质量参差不齐,Windows 上能读到的字段和 Linux 上完全不是一回事。相比之下,系统自带的命令行工具输出格式稳定、字段齐全,QProcess 只负责把进程跑起来、把输出读回来,剩下的解析逻辑完全由你控制。代价是你要为每个平台写一套命令和解析规则,但这是可控的复杂度,比依赖一个半残的跨平台 API 要踏实得多。
另一个现实原因是部署。QProcess 是 QtCore 的一部分,不需要额外链接任何模块,编译出来的二进制不会因为多依赖一个 Qt 模块而变大。对于工具类软件,这一点很关键。
2.2 同步调用:waitForFinished 的正确打开方式
最简单的用法是同步调用,适合在启动时一次性采集、不阻塞 UI 太久的场景。核心是 start 之后调 waitForFinished,然后读 readAllStandardOutput。
#include <QProcess> #include <QDebug> QString runCommand(const QString &program, const QStringList &args) { QProcess process; // 合并 stderr 到 stdout,避免部分工具把信息打到 stderr 导致读不到 process.setProcessChannelMode(QProcess::MergedChannels); process.start(program, args); // 超时设 5 秒,硬件查询一般不会超过这个时间 if (!process.waitForFinished(5000)) { qWarning() << "command timeout or failed:" << program; process.kill(); process.waitForFinished(); return QString(); } if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { qWarning() << "exit code:" << process.exitCode(); return QString(); } return QString::fromLocal8Bit(process.readAllStandardOutput()); }这段代码有几个关键点。setProcessChannelMode 设为 MergedChannels 是因为像 Windows 的 wmic 有时候会把部分输出写到 stderr,不合并就会丢信息。waitForFinished 的参数是毫秒,超时后必须 kill 再 waitForFinished,否则进程对象析构时会报警告。readAllStandardOutput 返回 QByteArray,用 fromLocal8Bit 而不是 fromUtf8,因为 Windows 命令行工具默认用本地代码页输出,直接按 UTF-8 解会乱码。
参数方面,超时时间不要设太短。机械硬盘上跑 smartctl 可能要好几秒,设 5 秒是保守值。如果你的场景确定只查 CPU 和内存,2 秒也够。
2.3 异步调用:不卡 UI 的采集方式
如果采集动作是在 UI 线程触发的,同步调用会让界面卡住。这时候用异步,靠 readyReadStandardOutput 和 finished 信号。
// 在某个 QObject 子类中 QProcess *m_proc = new QProcess(this); void HardwareCollector::collectAsync() { connect(m_proc, &QProcess::readyReadStandardOutput, this, [this]() { // 增量读取,适合输出量大的命令 m_buffer.append(m_proc->readAllStandardOutput()); }); connect(m_proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this](int code, QProcess::ExitStatus status) { if (status == QProcess::NormalExit && code == 0) { parseOutput(QString::fromLocal8Bit(m_buffer)); } m_buffer.clear(); }); m_proc->start("lscpu", QStringList()); }异步模式下最容易翻车的地方是缓冲区管理。readyReadStandardOutput 可能触发多次,每次只给你一部分数据,必须自己拼。另外 finished 信号有两个重载版本,用 QOverload 明确指定,否则编译不过。还有一个坑:如果进程启动失败,比如命令不存在,finished 不会触发,但 errorOccurred 会。生产代码里两个信号都要接。
2.4 各平台命令对照与选型
不同系统上能用的命令差别很大,下面这张表是我在实际项目里验证过的组合。
| 信息类型 | Windows 命令 | Linux 命令 | macOS 命令 |
|---|---|---|---|
| CPU 型号 | wmic cpu get name | lscpu 或 cat /proc/cpuinfo | sysctl -n machdep.cpu.brand_string |
| 内存容量 | wmic memorychip get capacity | free -b 或 cat /proc/meminfo | sysctl -n hw.memsize |
| 磁盘序列号 | wmic diskdrive get serialnumber | lsblk -o NAME,SERIAL | diskutil info / |
| 显卡 | wmic path win32_videocontroller get name | lspci | grep VGA | system_profiler SPDisplaysDataType |
Windows 上 wmic 在新版系统里已经被标记为弃用,但截至 Windows 11 仍然可用。如果考虑未来兼容性,可以改用 PowerShell 的 Get-CimInstance,但启动开销更大。我一般会先试 wmic,失败再 fallback 到 PowerShell。
Linux 上 lscpu 和 /proc/cpuinfo 各有优劣。lscpu 输出更结构化,但某些精简系统没装 util-linux 就没有这个命令。直接读 /proc/cpuinfo 最保险,但解析要自己处理多核重复的问题。
3. 解析输出:从原始文本到结构化数据
3.1 Windows wmic 输出的编码与格式陷阱
wmic 的输出有两个特点:一是行尾是 \r\r\n,二是编码是本地代码页。直接按行 split 会得到一堆空行。
QString parseWmicValue(const QString &raw, const QString &key) { // wmic 输出行尾是 \r\r\n,先统一成 \n QString normalized = raw; normalized.replace("\r\r\n", "\n"); normalized.replace("\r\n", "\n"); const QStringList lines = normalized.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { QString trimmed = line.trimmed(); // 跳过表头行 if (trimmed.compare(key, Qt::CaseInsensitive) == 0) continue; if (!trimmed.isEmpty()) return trimmed; } return QString(); }这段代码的关键是先把 \r\r\n 替换掉。wmic 的输出格式是第一行表头,第二行开始是数据,但中间可能夹杂空行。用 SkipEmptyParts 过滤空行后,第一行是表头,第二行就是值。如果查询返回多条记录,比如多根内存条,就需要改成收集所有非表头行。
编码问题在中文 Windows 上尤其明显。wmic 输出用的是 GBK,fromLocal8Bit 在中文系统上能正确解码,但如果你的程序跑在英文系统上而硬件名称含中文,就会乱码。稳妥做法是用 QTextCodec 显式指定,但 Qt6 已经把 QTextCodec 移到了核心外。替代方案是调 wmic 时加 /format:csv 参数,输出变成 UTF-8 兼容的 CSV 格式,解析起来更省心。
3.2 Linux /proc 与 lscpu 的字段提取
Linux 下读 /proc/cpuinfo 是最通用的方式,但每个核都会重复一遍,需要去重。
QString parseCpuModelFromProc() { QFile file("/proc/cpuinfo"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return QString(); QTextStream stream(&file); QString line; while (stream.readLineInto(&line)) { if (line.startsWith("model name")) { // 格式是 "model name : Intel(R) Core(TM) i7-9700K" int colon = line.indexOf(':'); if (colon != -1) return line.mid(colon + 1).trimmed(); } } return QString(); }/proc/cpuinfo 的字段名在不同架构上不一样。x86 上是 model name,ARM 上可能是 Processor 或 Hardware。如果要做通用工具,得同时匹配多个 key。lscpu 的输出更统一,但依赖 util-linux 包。我的做法是优先读 /proc/cpuinfo,拿不到再 fallback 到 lscpu。
内存信息读 /proc/meminfo 的 MemTotal 字段,单位是 kB。注意这个值是内核可用内存,会比物理内存条容量小一些,因为有一部分被内核保留了。如果需要精确的物理内存容量,得用 dmidecode,但它需要 root 权限。
3.3 磁盘序列号的获取与权限问题
磁盘序列号是最容易翻车的字段。Windows 上 wmic diskdrive get serialnumber 通常能拿到,但某些 NVMe 硬盘返回的序列号带一堆空格,需要 trim。Linux 上 lsblk -o NAME,SERIAL 不需要 root,但如果是 USB 硬盘,序列号可能为空。macOS 上 diskutil info 需要指定具体设备。
QStringList parseLsblkSerials(const QString &raw) { QStringList serials; const QStringList lines = raw.split('\n', Qt::SkipEmptyParts); for (int i = 1; i < lines.size(); ++i) { // 跳过表头 const QStringList parts = lines[i].split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); if (parts.size() >= 2 && !parts[1].isEmpty()) serials << parts[1]; } return serials; }lsblk 的输出列宽是动态的,用空白分割比按固定位置截取更可靠。但要注意,如果序列号本身含空格(极少见),这种解析会出错。更稳妥的方式是用 lsblk --json,输出结构化 JSON,用 QJsonDocument 解析。不过 --json 参数在旧版 util-linux 上不支持,需要做版本判断。
权限方面,dmidecode 和 smartctl 都需要 root。如果你的程序不是以管理员身份运行,这两条路走不通。替代方案是读 /sys/block/sda/device/serial,这个文件普通用户可读,但只对 SATA 盘有效,NVMe 盘对应的是 /sys/block/nvme0n1/device/serial。
4. 避坑与排查:QProcess 采集硬件信息的五个血泪教训
4.1 现象:waitForFinished 返回 true 但读不到任何输出
原因:命令把信息打到了 stderr,而默认的 channel mode 是 SeparateChannels,readAllStandardOutput 自然读不到。某些版本的 lspci 和 smartctl 就有这个行为。
解决:启动前调 setProcessChannelMode(QProcess::MergedChannels),或者同时读 readAllStandardError。我一般直接合并,省事。
4.2 现象:Windows 上中文硬件名称显示为乱码
原因:wmic 输出用本地代码页(中文系统是 GBK),而 fromUtf8 按 UTF-8 解码,必然乱码。
解决:用 fromLocal8Bit 替代 fromUtf8。如果程序需要跨语言环境,改用 wmic /format:csv 让输出变成 ASCII 兼容格式,或者调 PowerShell 时加 [Console]::OutputEncoding 设置。
4.3 现象:异步模式下 finished 信号不触发
原因:进程启动失败,比如命令路径不对、权限不足。这种情况下 QProcess 发的是 errorOccurred 信号,不是 finished。
解决:同时连接 errorOccurred 和 finished,在 errorOccurred 里做清理和错误提示。另外 start 之后可以用 waitForStarted 确认进程是否真的跑起来了。
4.4 现象:Linux 上 lscpu 命令找不到
原因:目标系统是精简安装,没有 util-linux 包。Docker 容器里尤其常见。
解决:不要硬依赖 lscpu,优先读 /proc/cpuinfo 和 /proc/meminfo,这两个文件在任何 Linux 上都有。如果非要 lscpu 的某些字段,用 QStandardPaths::findExecutable 先检查命令是否存在。
4.5 现象:采集耗时过长导致界面卡顿
原因:在 UI 线程里同步调用了多个命令,每个 waitForFinished 都在阻塞事件循环。
解决:把所有采集逻辑放到 QtConcurrent::run 或者单独的 QThread 里,采集完成后通过信号把结果传回 UI 线程。如果只是查 CPU 和内存,合并成一条命令也能减少进程启动开销。Windows 上可以用一条 wmic 查询多个字段,Linux 上可以用 sh -c 把多条命令串起来。
5. 进阶:用一条命令批量采集与结果缓存
实际项目里,逐个字段调命令的效率很低。Windows 上 wmic 支持一次查询多个属性,Linux 上可以用 sh -c 把多条命令的输出用分隔符拼起来,一次 QProcess 调用拿回所有信息。
// Linux 下一次采集 CPU、内存、磁盘序列号 QString script = "echo '---CPU---'; " "grep 'model name' /proc/cpuinfo | head -1; " "echo '---MEM---'; " "grep MemTotal /proc/meminfo; " "echo '---DISK---'; " "lsblk -o NAME,SERIAL -n 2>/dev/null | head -5"; QString output = runCommand("/bin/sh", {"-c", script}); // 按 ---XXX--- 分隔符切分后分别解析这种做法的好处是把多次进程启动合并成一次,在机械硬盘或者低配机器上能明显缩短采集时间。代价是解析逻辑稍微复杂一点,需要按自定义分隔符切分。分隔符要选得足够特殊,避免和命令输出冲突,我用的是 ---CPU--- 这种带横线的格式。
缓存策略也值得说一句。硬件信息在运行期间基本不会变,采集一次后缓存到成员变量里,后续直接读缓存。如果程序需要长时间运行,可以加一个手动刷新按钮,或者监听系统事件(比如 USB 设备插拔)来触发重新采集。但不要定时轮询,没意义还费电。
还有一个细节:QProcess 的 start 是异步的,即使你马上调 waitForFinished,中间也有一个极短的时间窗口。如果在这段时间内对象被销毁,会出问题。所以 QProcess 对象要么放在栈上确保生命周期覆盖整个调用,要么用智能指针管理。我一般用栈对象,简单直接。
最后说一个我自己的习惯:所有外部命令的路径都写成绝对路径或者用 QStandardPaths::findExecutable 解析后的路径,不要直接写命令名。因为 QProcess 查找可执行文件的逻辑依赖 PATH 环境变量,而 GUI 程序启动时的 PATH 可能和终端里不一样,这是很多人调试半天找不到原因的玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取