ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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 特有的 SDKMSVC 的生态兼容性会更好一点。图表组件这块我建议第一版先别上 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 与 MEMORYSTATUSEXWindows 上获取内存状态最常用的是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 结构体大小的不赋值会导致函数直接失败。很多“为什么我的内存数据读不出来”的问题根源就在这一行。另一个要注意的点是ullTotalPhys和ullAvailPhys是 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.dll、libstdc-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 和内存这个项目也已经完成了它作为“系统编程第一个工程”的使命。接下来不管继续加告警还是去接触其他监控体系你都会比没写过完整链路的人更清楚每一步改动会落在哪一层。
返回列表