news 2026/9/4 8:14:26

C++ Qt实战:开发Windows实时系统监控工具全程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ Qt实战:开发Windows实时系统监控工具全程解析

我接触过不少刚学完 C++ 语法的人,最常听到的问题不是“接下来学什么”,而是“我能做个什么项目”。有人写图书管理系统,有人写贪吃蛇,但如果目标是真正把 C++、Qt 和 Windows 系统编程串起来,我最想推荐的反而是这个方向:用 C++ Qt 开发一个 Windows 实时系统监控工具。它没有复杂到让人劝退,也没有简单到学不到东西,而是刚好把真实 API、GUI 线程、数据刷新和发布部署全都逼着你走一遍。做完它,你会明显感觉到自己和以前不一样了。

很多人容易低估这个项目,觉得它不过是读几个系统参数,再画几张图表。但它的价值根本不在图表,而在它逼你面对系统编程里最核心的一类问题:怎么从操作系统拿数据、怎么把数据算准、怎么不卡界面、怎么保证长时间稳定。这四个问题一旦都能解决,你已经掌握了做很多客户端工具的基础能力。这篇文章我会按一个实际上手顺序,从选型、架构、最小版本、线程模型,一直讲到打包和长稳测试。

1. 为什么我推荐它作为系统编程的第一个工程

1.1 图书管理、贪吃蛇给不了你的那部分

图书管理、贪吃蛇、计算器这类项目,主要集中在语法、类设计、数据结构和简单逻辑上。它们不是不好,而是和真实系统之间几乎没有交互。你写的代码只在你的内存里自洽,操作系统对你来说仍然是一层黑盒子。

系统监控就不一样。它必须去读 Windows 的真实系统状态:CPU 忙不忙、内存还剩多少、磁盘是不是满了、系统到底启动多久了。你会被迫调用 Windows API,被迫处理返回码,被迫理解结构体里的字段含义。这个过程中,你会第一次真切感受到“程序连接了现实系统”的体感。这种体感,在刷题和纯逻辑小项目里是得不到的。

1.2 它把系统工程拆成了五块可验证的能力

我之所以觉得这个项目适合当“第一个工程”,不是因为它难,而是因为它把完整的工程链条拆成了五个边界清晰的模块:

能力模块对应工程问题验证方式
数据采集怎么用 Windows API 拿到 CPU、内存等数据和任务管理器对照
数据建模怎么用结构体表示一次系统快照打印日志、写单元测试
GUI 开发怎么用 Qt 控件实时展示数据拖动窗口时刷新仍然流畅
并发与调度怎么避免采集任务卡死 UI长时间运行界面不假死
工程化怎么把程序发给别人也能跑在干净机器上直接启动

这五个模块刚好对应系统编程入门的核心。更关键的是,它们可以独立验证:数据对不对,一眼就看得出来;界面卡不卡,手一拖就知道;换台电脑能不能跑,实际测一次就明白。这种“每走一步都能看到结果”的特性,对新手建立信心非常重要。

1.3 不要一开始就做成任务管理器

很多新手一上来就想着把 CPU、内存、磁盘、网络、进程列表、显卡、电池全部监控一遍,最后往往卡在某个细节里出不来。更合理的做法是:只做 CPU 和内存两个指标,先把整条链路跑通,再逐步加磁盘、网络和进程列表。

判断这个项目完成了第一阶段,不需要功能很多,只要满足三条:显示的是真实数据,每秒能稳定刷新,界面拖动时不卡顿。做到这三条,你已经把“采集→数据处理→界面渲染”这条最核心的链路趟过一遍了。

2. 先想清楚数据链路,再动手写第一行代码

2.1 实时监控的本质是周期性的“生产-消费”模型

在写代码之前,可以先建立一个心智模型。实时监控工具本质上是一个周期性的生产-消费系统:操作系统内核是数据生产者,它会持续维护 CPU 时间、内存占用、进程状态等信息;你的程序是消费者,负责周期性采样,把这些信息从操作系统接口里拿出来;界面是终端消费环节,负责把数据呈现给用户。

这里要注意,“实时监控”里的“实时”并不是操作系统意义上的硬实时,而是“周期性采样、延迟可接受、趋势可观察”。你要做的是每隔一段时间拍一张系统快照,然后根据这些快照计算出变化趋势。理解这一点,后面很多设计决策就顺理成章了。

2.2 推荐的四层结构

真正动手时,我建议把项目分成四层:

  • 采集层:只负责调用 Windows API,获取原始数据,不关心界面。
  • 领域层:负责把原始数据封装成系统快照,计算百分比、格式化文本。
  • UI 层:负责把快照渲染到 QLabel、QProgressBar 或图表控件上。
  • 调度层:负责用 QTimer 或后台线程周期性触发采集层。

一个简单的目录结构可以是:

SysMonitor/ src/ main.cpp MainWindow.h/.cpp collector/ SysInfoCollector.h/.cpp model/ SystemSnapshot.h ui/ MainWindow.ui

这样的分层能保证以后新增监控项时,不会把 UI 代码和系统 API 搅在一起。比如你要加一个网络监控,只需要在采集层加一个网络接口查询函数,在快照结构体里加几个字段,然后新增一个 UI 区域去显示。中间的业务逻辑和调度逻辑基本不用改。

2.3 技术选型:Qt6、Qt5、MSVC 还是 MinGW

如果你是刚接触 Qt,直接选 Qt6 会更省心。Qt6 对高 DPI、新编译器的支持更完整,安装也简单。编译套件方面,学习阶段用 MinGW 或 MSVC 都可以;如果你确定后面要接入某些 Windows 特有的 SDK,MSVC 的生态兼容性会更好一点。

图表组件这块,我建议第一版先别上 QChart 或 QCustomPlot,直接用 QLabel 和 QProgressBar 把数值显示出来就够。原因是第一版的核心目标是打通数据链路,而不是把图表做得漂亮。等 CPU 和内存采集都稳定了,再考虑引入图表,把历史趋势画出来。

还有一个容易踩的选型坑:不要为了省事用 PowerShell 子进程定时查询系统信息。虽然 PowerShell 也能拿到 CPU 和内存数据,但每次启动子进程的代价非常高。实时监控要求周期性执行,用子进程方案会导致延迟大、资源占用高,甚至界面卡顿。系统编程的场景下,直接调用系统 API 才是正确路径。

2.4 为什么采集操作不能直接放在 UI 线程

Qt 的 UI 事件循环负责处理重绘、鼠标点击、拖拽、按钮响应等所有界面事件。如果在 QTimer 回调里直接执行耗时操作,事件循环就会被阻塞。最直观的表现就是窗口失去响应,拖不动,点按钮没反应。

一次系统查询看似很快,但如果你把 CPU、内存、磁盘、网络、进程列表全部放在一个定时器回调里,单次执行时间很可能超过几十毫秒甚至几百毫秒。界面刷新一旦跟不上,用户就会觉得程序卡顿。这个问题的解药不是优化单次 API 调用速度,而是从一开始就不要让采集任务占用 UI 线程。更具体的设计,我会在第四部分展开。

3. 最小可用版本:先做 CPU 和内存的实时展示

3.1 第一步:创建 Qt Widgets 项目并摆好工程结构

用 Qt Creator 新建一个 Qt Widgets Application,主窗口放几个 QLabel、一个 QProgressBar 和一个关闭按钮。不要一上来就设计复杂布局,先把数据源打通。

然后在项目里新建一个 collector 目录,放一个 SysInfoCollector 类。这个类只负责采集系统信息,不依赖任何 Qt UI 控件。这么做的好处是,你可以在没有界面的情况下单独测试采集逻辑,也可以以后把它复用到命令行版本里。

3.2 内存数据:GlobalMemoryStatusEx 与 MEMORYSTATUSEX

Windows 上获取内存状态最常用的是GlobalMemoryStatusEx。这个函数会填充一个MEMORYSTATUSEX结构体,里面包含了当前内存使用百分比、物理内存总量和可用量等信息。常见的调用方式大概是这样:

MEMORYSTATUSEX mem; mem.dwLength = sizeof(mem); if (GlobalMemoryStatusEx(&mem)) { double usagePercent = mem.dwMemoryLoad; qulonglong totalBytes = mem.ullTotalPhys; qulonglong availBytes = mem.ullAvailPhys; }

新手最容易在这个函数上犯的错误是:没有先给dwLength赋值就直接调用。这个字段是用来告诉 API 结构体大小的,不赋值会导致函数直接失败。很多“为什么我的内存数据读不出来”的问题,根源就在这一行。

另一个要注意的点是ullTotalPhysullAvailPhys是 64 位无符号整数。如果你在 32 位环境下不小心截断,大内存机器上就会显示错误。Qt 里可以直接用qulonglong来承接,避免类型宽度不够。

3.3 CPU 数据:GetSystemTimes 与两次采样差值

CPU 占用率比内存要麻烦。Windows 提供的方法是GetSystemTimes,它返回三个时间值:空闲时间、内核时间、用户时间。但注意,这些时间是从系统启动以来累计的计数,不是当前瞬间的占用率。想得到某一时间段的 CPU 占用率,必须做两次采样,然后计算差值。

代码思路是这样:

FILETIME idleTime, kernelTime, userTime; if (GetSystemTimes(&idleTime, &kernelTime, &userTime)) { // 把 FILETIME 转换成 64 位整数后保存下来 // 下一次采样时,用两次结果的增量计算占用率 }

通常的公式是:总增量 = 内核增量 + 用户增量,空闲增量单独计算,占用率 = 1 - 空闲增量 / 总增量。这里有一个非常容易错的地方:kernelTime本身已经包含了idleTime。所以算总增量时不能把idleTime再加一遍,否则会出现百分比超过 100 或者数值不对的诡异情况。

新手最常见的错误是只采样了一次,然后直接用当前值当占用率,结果永远是 0 或 100。另一个常见错误是第一次采样后立刻做第二次采样,间隔太短导致总增量为 0,出现除零。工程实践里,两次采样间隔至少要有几百毫秒,所以放在 1 秒的定时器里是比较合理的。

注意:不要一上来就把刷新频率拉满,先用 1 秒间隔跑通整个流程,确认数据有变化、界面还流畅,再考虑缩短间隔。

3.4 用 QTimer 做最简单的刷新循环

最小版本最简单的方式,是在 MainWindow 构造函数里创建一个 QTimer,把它和刷新槽函数连接起来:

QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MainWindow::refresh); timer->start(1000);

构造函数末尾主动调用一次refresh(),避免界面启动后先显示空的 0 值。

refresh()里,调用采集函数,然后更新 QLabel 文本和 QProgressBar 的值。这个版本能跑起来,数据也在变,但它只能算“最小可用”,不是真正的工程方案。把它当作流程验证即可,下一步就要进入线程模型。

3.5 如何和任务管理器对照验证

程序写完后,先不要急着加功能。打开 Windows 任务管理器,和你的程序并排对比。内存数值通常能基本对齐,任务管理器显示多少,你的程序也差不太多。CPU 占用率可能不会完全一致,因为任务管理器有自己的一套采样算法和展示逻辑,但趋势应该是吻合的:跑点负载时上升,空闲时下降。

如果 CPU 数值始终为 0 或一直显示 100,大概率不是电脑的问题,而是你的计算公式或采样逻辑有问题。这时候按下面的排查链路走。

3.6 CPU 数值不对时的排查顺序

排查点检查内容
是否做了两次采样确认代码里调用了两次 GetSystemTimes,而不是只调一次
两次采样间隔间隔是否太短,建议至少 200ms,一般 1s 足够
API 返回值GetSystemTimes 是否返回 FALSE,失败时要用 GetLastError 拿到错误码
公式是否正确确认 kernelTime 已包含 idleTime,不要重复计算
是否整数除法两个整数相除会丢掉小数,导致结果接近 0 或 100
显示类型确认得到的是 0~1 还是 0~100,别在展示时又乘了一次

这套排查顺序也适用于后面加磁盘、网络等指标:先看现象,再看输入,再看 API 返回,最后看计算公式。

4. 把采集搬进线程,才是这个项目从“能跑”到“能用”的分水岭

4.1 从一个卡顿现象讲起

最小版本在只有 CPU 和内存两个监控项时,UI 线程可能还撑得住。但当你把磁盘、网络、进程列表都加进来,定时器回调的执行时间会明显变长。拖着窗口移动时,你可能会发现标题栏跟着鼠标走的动画都变得顿挫。

原理其实很直接:QTimer 的 timeout 信号是在 UI 线程里处理的,回调执行多久,UI 事件循环就被阻塞多久。一次采样 100ms,界面的帧率就会掉到 10fps 以下,用户体感就会非常糟糕。

这里有一个很适合反复强调的原则:

一切可能超过一帧预算的工作,都不应该放在 UI 线程里。

一帧预算通常是 16ms。一次真实系统查询可能达不到 16ms,但如果你查询的是进程列表,就可能达到几十毫秒。更关键的是,系统 API 的耗时并不稳定,不能只按理想情况设计。

4.2 推荐模式:QObject 工作对象 + moveToThread

在 Qt 里做后台采集,我比较推荐的方式是把SysInfoCollector设计成一个 QObject 子类,然后通过moveToThread把它移到一个常驻的 QThread 里。

工作对象内部可以自己使用一个 QTimer 来驱动周期采集。每次采集完成后,发出一个信号,携带一个SystemSnapshot快照对象。主线程的 UI 槽函数收到这个信号后,再更新界面。

这样做之所以安全,是因为 Qt 的跨线程信号使用的是队列连接。信号不是直接调函数,而是投递到接收者线程的事件循环里,等接收者线程空闲时再执行。全程不需要你手动加锁,Qt 的信号槽机制已经处理了线程切换。

一个要注意的点是,工作线程的启动和退出要规范。程序关闭时,先让工作对象停止定时器,再调用线程的quit()wait(),确保线程真正退出了再销毁对象。否则很容易出现“程序关了但进程还在”的诡异问题。

4.3 新手应该避开的几种做法

  • 不要在QThread::run()里直接new QWidget或操作界面控件。UI 对象只能在 UI 线程创建和操作,这是 Qt 的硬约束。
  • 不要用全局变量作为共享缓存,然后在两个线程里不加锁地读写。除非你完全清楚自己在做什么,否则一定会有偶发问题。
  • 不要频繁创建和销毁线程。采集线程应该常驻,而不是每采样一次就开一个新线程。
  • 不要在 UI 线程里调用某个会一直等待的同步接口,比如阻塞等待工作线程完成。这会让 UI 直接死锁。

4.4 刷新频率不是越快越好

很多新手以为实时监控就是刷新越快越好,最好每 10ms 刷一次。实际上,系统指标本身是随时间平滑变化的,过高的采样频率并不能带来更高的真实信息量,只会白白增加 CPU 功耗和界面重绘开销。

工程上,监控刷新频率设在 1 秒到 2 秒之间比较合适。任务管理器默认也差不多是这个节奏。如果你要画历史趋势,可以让采集线程每秒采一次,图表只保留最近 60 个点;如果要做长时间趋势,甚至可以把采样频率降成每分钟一次。

更好的做法是“采集频率”和“界面刷新频率”分离。后台按固定周期采集数据,UI 按另一套节奏刷新界面,尽量避免一次刷新把几十个控件全部重建一遍。

4.5 数据快照:用结构体把一次采集结果打包

采集线程每轮会得到 CPU 使用率、内存占用、磁盘空间等一堆数据。如果每个数据都用独立信号发送,UI 槽函数会很零散。推荐的做法是定义一个SystemSnapshot结构体,把一次采样的所有结果放在一起:

struct SystemSnapshot { double cpuUsagePercent; double memoryUsagePercent; qulonglong totalPhysBytes; qulonglong availPhysBytes; // 未来可以继续加磁盘、网络等字段 };

工作线程每次完成后只发一个信号,UI 槽函数统一处理这个快照。以后加监控项时,只需要扩展结构体和采集逻辑,UI 层的链接方式不用大改。这种写法看起来平淡,但对后续维护非常友好。

5. 别小看这些细节:格式化、权限、打包和长稳

5.1 数值格式化里的三个细节

第一个细节是内存单位。GB、MB、KB 之间是 1024 进制,不是 1000。很多程序显示的内存偏大或偏小,都是因为用了十进制换算。

第二个细节是格式化字符串。Qt 里可以用QString::number(value, 'f', 1)保留一位小数,也可以配合arg做对齐。要避免直接用默认的%d转换浮点数,因为结果可能是 0 或乱码。

第三个细节是百分比范围。API 返回 0~100 时,不要在显示层再乘 100;返回 0~1 时,也不要直接显示成 0.5。这个看似简单的问题,在实际代码里反复出现。

更好的做法是写一个formatBytes(qulonglong bytes)函数,统一处理单位转换,而不是在每一个控件里单独写除法。

5.2 权限与显示问题

如果只是读取本机 CPU、内存、磁盘这些常规指标,普通用户权限通常已经够用。如果你后续要读取其他进程的详细信息,或者某些系统级资源,Windows 可能会要求权限提升。遇到这种情况,建议按最小权限原则在工程 manifest 里声明请求级别,不要无脑要求管理员权限。频繁使用管理员权限运行本身就会带来麻烦。

另一个容易遇到的是 DPI 显示问题。Qt6 默认开启了高 DPI 支持,界面在 4K 屏上一般不会模糊。如果混用 Qt5,或者手动调用了一些 DPI 相关接口,就可能出现字体模糊或控件错位。

还有一个隐藏比较深的坑:32 位程序在 64 位系统上访问另一些系统信息时存在限制。如果你想摆脱这类问题,直接编译成 64 位版本通常是最省心的选择。

5.3 发布:windeployqt 只是第一步

很多新手在 Release 构建结束后,直接双击 exe 发现能跑,就觉得打包完成了。等把 exe 发给别人,对方却提示缺少 DLL,才发现事情没那么简单。

Qt 官方提供了windeployqt工具,它会自动把 Qt 模块相关的 DLL 复制到 exe 目录。执行完windeployqt后,本地可能能跑,但这只解决了 Qt 依赖,没有解决编译器运行时。如果使用 MSVC 编译,目标机器可能需要安装 VC++ Redistributable;如果使用 MinGW,则可能有libgcc_s_seh-1.dlllibstdc++-6.dll等运行库需要一并带上。

最可靠的验证方式是找一台没有装过 Qt 的电脑跑一次。缺什么 DLL 就补什么,比在本地反复猜测更高效。

5.4 长稳测试:实时程序最怕泄漏和线程退出挂死

系统监控工具是要长年累月挂在后台的产品形态。新手可能只跑了几分钟,看到 CPU 和内存数据正常就以为完成了。但实时程序最容易出现的问题恰恰是长期运行后才暴露的。

建议做一个简单的“夜测”:程序连续运行 3 小时以上,隔一段时间看一次内存占用。如果内存曲线持续上涨,优先怀疑句柄泄漏、GDI 对象没有释放、new出来的对象没有 delete、QObject在退出时仍然连接着信号。

长时间没人看它,程序也要能自己稳定地跑下去。这才是监控工具和演示工具的分界线。

除了内存,还要测最小化、最大化、锁屏、切换用户这些真实场景。很多线程问题不是平时不出来,而是藏在这些特殊场景里。

6. 项目完成之后,下一步可以往哪走

6.1 从“展示”到“告警”

纯展示数据只是监控的第一步。真正有价值的监控系统,能够在指标异常时主动通知你。你可以把历史数据缓存到内存环形队列或 SQLite 中,当 CPU 或内存连续 N 次超过阈值时,触发日志记录、托盘气泡通知等。

这一步的工程价值是把程序从“系统信息查看器”升级成“监控告警工具”。告警规则怎么设计、怎么避免误报、怎么保留现场数据,这些都有很多值得琢磨的地方。

6.2 从“本机”到“远端”和技术生态

如果你对监控方向感兴趣,接下来可以考虑把采集逻辑独立成库,UI 只负责展示,后台通过网络接口获取远程主机数据。也可以研究现有监控体系,比如把指标上报给 Prometheus、InfluxDB 这类系统。

但这里要提醒一句:不要为了追技术热点而盲目扩大项目边界。先把本机版本稳定住,再考虑远端和生态对接。一个跑不稳定的本机监控工具,接入再多的外部系统也只会增加排查难度。

6.3 迁移到 Linux,验证系统编程能力的迁移性

做完 Windows 版本后,你可以试着把采集层替换成 Linux 下的/proc文件系统或sysinfo()接口。Qt 部分的界面和业务逻辑基本可以复用,变化主要发生在采集层。

这个迁移过程会让你意识到:系统编程的核心能力不是记住某个平台的 API,而是理解操作系统暴露了哪些接口、数据怎么组织、资源怎么管理。换一个平台时,你只是换了一套数据来源,架构和工程化思路仍然有效。

6.4 做完后,回看三个问题

项目结束时,我建议你问自己这三个问题:

  • 采集线程退出时,能不能保证不再有新的信号投递到 UI?
  • 如果 CPU 数值算错了,我能只通过看日志就定位是哪一层的问题吗?
  • 如果要把刷新频率从 1 秒改成 500ms,我只需要改几处代码?

这三个问题的答案,决定了这个项目在你手里是“能跑”,还是“工程化”。能跑是第一步,但工程化才是系统编程真正要训练的东西。

最后说一点我的经验。很多人做完这个项目后会忍不住加功能,把界面做得越来越像任务管理器。但以我多年观察,真正让这类工具拉开差距的,不是功能数量或界面精美程度,而是它能不能在真实环境里长时间稳定运行、数据是不是可信、出了问题好不好排查。如果你能把这三点做到,哪怕只监控 CPU 和内存,这个项目也已经完成了它作为“系统编程第一个工程”的使命。接下来不管继续加告警,还是去接触其他监控体系,你都会比没写过完整链路的人更清楚每一步改动会落在哪一层。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 8:02:58

51单片机数字秒表设计:LCD1602驱动与定时器12T模式详解

简介:本资源是面向单片机初学者与嵌入式入门学习者的中级实践例程,聚焦数字秒表功能开发与LCD1602液晶显示驱动,解决硬件定时控制、字符型液晶接口编程及实时数据显示等核心问题。压缩包共10个文件,含2个C源文件(main.…

作者头像 李华
网站建设 2026/9/4 1:03:19

校园信息发布平台毕业设计:Spring Boot+Vue前后端分离实战指南

简介:这是一套面向计算机专业本科生的高质量毕业设计级校园信息发布平台源码,聚焦高校场景下的通知公告、活动发布、招聘对接等核心需求,适用于Web开发入门到进阶的学习者开展项目实践与二次开发。资源共410个文件,20.3MB&#xf…

作者头像 李华
网站建设 2026/9/2 9:45:57

DDR协议与FPGA控制器设计:从原理到MIG实战

简介:面向FPGA工程师与硬件设计人员,围绕DDR内存工作原理及FPGA控制器实现的学习资料包,覆盖从理论到工程落地的完整链路,适用于项目开发与学习入门,可解决高速存储接口设计中的时序、信号完整性与调试难题。资源以rar…

作者头像 李华
网站建设 2026/9/4 13:05:42

单片机MD5加密源代码详解:从原理到STM32移植与固件校验实战

简介:面向瑞萨、ST(STM)等单片机平台的MD5加密实现资源,适用于物联网节点、传感器网关等嵌入式设备中的数据完整性校验、密码存储与防篡改场景,特别照顾到内存受限与计算能力有限的环境,适合有一定C语言基础…

作者头像 李华
网站建设 2026/9/4 15:25:43

AI智能体控制物理设备:MHS安全规范解读与工程落地准备

模型硬件标准(MHS)是 Anthropic 最近放出的一个研究预览方向,核心目标是把 AI 智能体操作物理设备的规则标准化。简单说,以前智能体只能读网页、写文件、调接口,现在它开始接收摄像头画面、控制机械臂、操作测试台、读…

作者头像 李华
网站建设 2026/9/2 9:43:19

数字考古与归档实践:从地下音乐资源处理到文化遗产保存

你点开一个压缩包,文件名是那种典型的、混杂着大小写和下划线的神秘字符。解压后,十几个音频文件躺在文件夹里,音质带着明显的年代感,有些失真,有些粗糙,但那股原始的、未经雕琢的冲击力,却透过…

作者头像 李华