ARTICLE DETAIL

资讯详情

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

C++ Qt实战:从零构建Windows实时系统监控工具

C++ Qt实战:从零构建Windows实时系统监控工具 如果你已经学完 C 语法也看过不少 Qt 的基础教程但始终觉得“离真正写一个能上手的程序还差一步”那这个项目就是为你准备的用 C 和 Qt 做一个 Windows 实时系统监控工具。我能直接给出的判断是这是目前系统编程入门性价比最高的第一个完整工程项目。它的代码量不大但覆盖了系统 API 调用、进程遍历、性能计数器、GUI 实时刷新、权限处理、软件发布等一整条真实开发链路。做完它你对 Windows 系统编程的认知会从“学过概念”变成“知道怎么用”。这篇文章不是只给一个成品源码而是从需求设计、技术选型、核心原理、分步实现到运行验证、常见踩坑、工程化建议完整带你走一遍。读完你可以得到一个可以运行、可以继续扩展的监控工具基础版本。1. 为什么实时系统监控工具值得作为第一个系统编程项目学 C 和 Qt 的初学者通常会遇到一个尴尬期语法都认识控件也会拖但一说到“做个项目”就不知道做什么。游戏项目偏游戏逻辑编辑器项目偏数据结构像“图书管理系统”这类项目又停留在数据库增删改查和系统编程没什么关系。系统监控工具正好补上这个断层。它要完成的事情非常具体把 CPU 使用率、内存使用率、进程列表这些 Windows 系统数据采集出来再通过 Qt 界面展示给用户。这里的每个功能点都会逼着你接触真实的系统编程知识进程列表怎么拿你需要调用 Windows 系统 API 进行快照遍历。CPU 使用率怎么算你不能直接“读取”而是要通过系统性能计数器不断采样。界面实时刷新时会不会卡如果你在 UI 线程里做耗时操作窗口就会假死这逼着你理解 Qt 的事件循环和线程模型。结束进程时权限不够怎么办这会让你理解 Windows 的权限隔离机制。从工程结构看这个项目天然适合分层采集层负责系统 API 调用界面层负责展示刷新调度层通过定时器连接两者。这种分层思路在后续做任何桌面应用、工具软件时都会反复用到。作为对比用 Python 加psutil库写一个类似的监控脚本确实十几行就能跑通。但正因为 Python 把底层细节都封装好了你很难真正理解系统编程发生了什么。C 加 Qt 的方案里你需要自己面对 Windows API 的类型、句柄管理和调用时机这些才是系统编程的核心能力。这个项目非常适合三类人刚学完 C 和 Qt 基础的在校学生、想转向桌面客户端开发的初级开发者、需要在 Windows 上做内部工具的运维或测试工程师。如果你只想提高 Qt 控件熟练度这个项目同样适用只是它给你的会更多。2. 项目功能设计与技术选型在写代码之前先把功能范围定义清楚。一个可运行的基础版只需要四个功能功能说明技术实现CPU 总使用率显示整体 CPU 占用百分比PDH 性能计数器内存总使用率显示物理内存占用百分比GlobalMemoryStatusEx进程列表列出 PID、进程名、内存占用、线程数Toolhelp 快照 API结束选中进程对指定 PID 结束进程OpenProcess TerminateProcess后续可以扩展的功能包括 CPU 历史趋势图、每个进程的 CPU 占用率、网络流量监控、进程启动路径等。但基础版先跑通最重要。技术选型上有两个地方需要解释清楚。第一为什么 CPU 使用率用 PDH 而不是其他方案。Windows 下获取 CPU 使用率的常见方式有三种读取注册表、调用GetSystemTimes、使用 PDH 性能计数器。前两种要么数据更新不及时要么解析复杂。PDH 是 Windows 官方提供的性能数据访问接口专门用于读取性能计数器我们只要添加计数器路径\Processor(_Total)\% Processor Time再定时调用采集函数就能拿到系统整体的 CPU 使用率不需要自己做差值计算。第二为什么进程列表用 Toolhelp API 而不是EnumProcesses。EnumProcesses只能拿到 PID 列表想拿到进程名和线程数还得再逐一 OpenProcess代码更啰嗦。Toolhelp 的CreateToolhelp32Snapshot一次快照就能拿到整个系统进程的完整信息包括 PID、进程名、线程数、父进程 ID 等。它更符合这个项目的需求。关于定时刷新频率建议设置为 2 秒一次。PDH 的 CPU 使用率计算依赖两次采样之间的差值刷新间隔太短会导致数据不稳定太长则不够“实时”。2 秒是比较平衡的选择对于展示型工具已经完全够用。还有一点要提前说明这里说的“实时”是 GUI 应用意义上的定时刷新不是嵌入式领域的硬实时。Windows 不是硬实时操作系统QTimer的精度也达不到硬件级实时要求但对于系统监控工具这种场景2 秒刷新已经能提供足够的连续性。3. 核心概念先搞懂这几个系统编程名词如果直接开始写代码很容易被那些莫名其妙的 API 名字劝退。这里先把实现过程中必然会遇到的几个核心概念讲清楚。3.1 句柄HANDLEWindows 系统编程里到处是HANDLE。你可以把它理解成系统资源的“取号牌”快照句柄、进程句柄、查询句柄都是同一个概念。调用 API 打开某个资源后返回一个句柄用完必须通过CloseHandle或对应的关闭函数释放。很多人写 Windows 程序内存不涨但句柄数疯狂上涨就是因为只 Open 不 Close。在这个项目里CreateToolhelp32Snapshot返回快照句柄OpenProcess返回进程句柄PdhOpenQuery返回 PDH 查询句柄。三个都要记得关闭尤其是进程列表循环里的OpenProcess进程多的时候很容易漏。3.2 PDH 性能计数器的采样逻辑PDH 的用法非常固定四步走PdhOpenQuery创建查询对象。PdhAddCounter添加计数器路径。PdhCollectQueryData采集当前数据。PdhGetFormattedCounterValue获取格式化后的数值。关键点在于 CPU 使用率的计算原理。第一次PdhCollectQueryData采集的数据只是作为基准点之后每次调用时PDH 会比较当前采样值和上一次采样值计算这段时间窗口内的平均使用率。所以程序刚启动时第一次getCpuUsagePercent返回值往往不准最稳妥的做法是在init阶段先调用一次采集函数作为基准之后每轮刷新都先采集再读取。3.3 快照与遍历模式进程列表用快照方式获取。所谓快照就是拿到当前系统进程列表的一个静态副本然后通过Process32FirstW和Process32NextW遍历。这个模式在 Windows 系统编程里非常常见遍历文件、遍历进程、遍历模块都是类似套路First 拿第一个Next 循环拿下一个。3.4 Qt 事件循环与 QTimerQt 程序启动后进入QApplication::exec()实际上进入了一个事件循环。定时器事件、鼠标事件、消息事件都在这个循环里被分发处理。如果某个事件处理函数里执行了耗时操作后续事件就会被堵住表现就是窗口无响应。系统监控工具里进程列表遍历和打开进程句柄都有一定耗时。如果刷新间隔是 2 秒而一次刷新消耗了 300 毫秒这个耗时在主线程里仍可以接受。但如果后续扩展了更复杂的逻辑就必须把采集工作放到QThread工作线程通过信号槽把结果传回主线程。这也是后续优化方向。4. 环境准备与项目配置开发环境方面本文以 Windows 10 或 Windows 11 为例Qt 版本可以使用 Qt 5.15 或 Qt 6.x两者在本文涉及的核心 API 上基本一致。编译器建议使用 MSVC因为pdh.h和psapi.h头文件在 Windows SDK 中自带MSVC 环境下链接最直接。MinGW 环境也可以只是依赖库的处理略有不同。如果你的电脑上还没有 Qt第一步是安装 Qt。开源用户可以直接从 Qt 官方下载安装程序在线安装时根据你的编译器选择对应的组件。为了减少后续麻烦建议使用 Qt 6 的长期支持版本并选择 MSVC 2022 的编译套件。创建项目时可以直接选择 Qt Widgets Application也可以手动编写工程文件。下面给出 CMake 和 qmake 两种方式。4.1 CMake 配置# CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(SysMonitor LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) add_executable(SysMonitor WIN32 main.cpp mainwindow.cpp sysinfocollector.cpp ) target_link_libraries(SysMonitor PRIVATE Qt6::Widgets pdh psapi ) if(MSVC) target_compile_options(SysMonitor PRIVATE /utf-8) endif()pdh和psapi是 Windows SDK 提供的系统库链接它们才能使用性能计数器和进程内存查询函数。WIN32标志表示这是一个 GUI 程序启动时不会弹出控制台窗口。4.2 qmake 配置# SysMonitor.pro QT core gui widgets CONFIG c17 TARGET SysMonitor TEMPLATE app SOURCES \ main.cpp \ mainwindow.cpp \ sysinfocollector.cpp HEADERS \ mainwindow.h \ sysinfocollector.h LIBS -lpdh -lpsapi # MSVC 下解决中文乱码问题 msvc { QMAKE_CXXFLAGS /utf-8 }QMAKE_CXXFLAGS /utf-8是专门针对 MSVC 的中文处理。MSVC 默认对没有 BOM 的源码文件按本地代码页解析如果源文件是 UTF-8 编码代码里的中文字符串在界面上大概率会变成乱码。加上/utf-8编译选项后强制编译器按 UTF-8 处理源码这是一个很实用的细节很多刚接触 Qt 的人在这里浪费过时间。4.3 关于 Qt Charts 的说明如果你后续要加 CPU 曲线图还需要在 Qt 安装时勾选 Charts 模块。它不是核心功能但能明显提升工具的上限。本文会在进阶部分给出示例代码基础版不需要它也能跑通。5. 完整实现代码现在进入核心环节。整个项目按三个文件组织系统信息采集类SysInfoCollector、主窗口类MainWindow、程序入口main.cpp。5.1 系统信息采集模块这个类负责所有 Windows API 调用界面层不直接接触系统 API这样后续换数据源、增加采集项都可以在这个类内部完成。// sysinfocollector.h #ifndef SYSINFOCOLLECTOR_H #define SYSINFOCOLLECTOR_H #include QObject #include QString #include QVector #include windows.h #include pdh.h #include psapi.h #include tlhelp32.h struct ProcessInfo { DWORD pid 0; QString name; SIZE_T memoryKB 0; DWORD threadCount 0; }; class SysInfoCollector : public QObject { Q_OBJECT public: explicit SysInfoCollector(QObject *parent nullptr); ~SysInfoCollector(); bool init(); double getCpuUsagePercent(); double getMemoryUsagePercent(); QVectorProcessInfo getProcessList(); private: PDH_HQUERY m_cpuQuery nullptr; PDH_HCOUNTER m_cpuCounter nullptr; }; #endif // SYSINFOCOLLECTOR_H// sysinfocollector.cpp #include sysinfocollector.h SysInfoCollector::SysInfoCollector(QObject *parent) : QObject(parent) { } SysInfoCollector::~SysInfoCollector() { if (m_cpuQuery) { PdhCloseQuery(m_cpuQuery); } } bool SysInfoCollector::init() { if (PdhOpenQuery(nullptr, 0, m_cpuQuery) ! ERROR_SUCCESS) { return false; } if (PdhAddCounter(m_cpuQuery, L\\Processor(_Total)\\% Processor Time, 0, m_cpuCounter) ! ERROR_SUCCESS) { return false; } // 第一次采集作为基准数据避免第一次读取时为 0 PdhCollectQueryData(m_cpuQuery); return true; } double SysInfoCollector::getCpuUsagePercent() { if (!m_cpuQuery || !m_cpuCounter) { return 0.0; } PdhCollectQueryData(m_cpuQuery); PDH_FMT_COUNTERVALUE value; if (PdhGetFormattedCounterValue(m_cpuCounter, PDH_FMT_DOUBLE, nullptr, value) ERROR_SUCCESS) { return value.doubleValue; } return 0.0; } double SysInfoCollector::getMemoryUsagePercent() { MEMORYSTATUSEX memInfo; memInfo.dwLength sizeof(memInfo); if (GlobalMemoryStatusEx(memInfo)) { return static_castdouble(memInfo.dwMemoryLoad); } return 0.0; } QVectorProcessInfo SysInfoCollector::getProcessList() { QVectorProcessInfo result; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot INVALID_HANDLE_VALUE) { return result; } PROCESSENTRY32W entry; entry.dwSize sizeof(PROCESSENTRY32W); if (Process32FirstW(snapshot, entry)) { do { ProcessInfo info; info.pid entry.th32ProcessID; info.name QString::fromWCharArray(entry.szExeFile); info.threadCount entry.cntThreads; // 打开进程获取内存信息权限不足时跳过不影响整体流程 HANDLE hProcess OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID); if (hProcess) { PROCESS_MEMORY_COUNTERS pmc; if (GetProcessMemoryInfo(hProcess, pmc, sizeof(pmc))) { info.memoryKB pmc.WorkingSetSize / 1024; } CloseHandle(hProcess); } result.append(info); } while (Process32NextW(snapshot, entry)); } CloseHandle(snapshot); return result; }这个类有几个容易出错的地方需要特别说明。PdhAddCounter的计数器路径使用宽字符串因为这属于 Windows API 的宽字符版本。如果你传入普通窄字符串编译会报类型不匹配。在PdhGetFormattedCounterValue中PDH_FMT_DOUBLE表示让系统把计数器值格式化成 double方便前端展示。进程内存信息来自GetProcessMemoryInfo它需要先OpenProcess。注意权限参数用的是PROCESS_QUERY_LIMITED_INFORMATION这是获取基本查询权限的最小请求不需要管理员权限就能成功。如果对每个进程都请求PROCESS_ALL_ACCESS非管理员环境下大量进程会打开失败完全没有必要。5.2 主窗口实现主窗口负责界面展示和定时刷新。布局采用 QVBoxLayout上方是 CPU 和内存的进度条与标签中间是进程表格下方是结束进程按钮。// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QTimer #include sysinfocollector.h class QTableWidget; class QLabel; class QProgressBar; class QPushButton; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void refreshData(); void killSelectedProcess(); private: void setupUI(); void updateSystemInfo(double cpuPercent, double memoryPercent); SysInfoCollector m_collector; QTimer m_timer; QTableWidget *m_processTable nullptr; QLabel *m_cpuLabel nullptr; QLabel *m_memoryLabel nullptr; QProgressBar *m_cpuBar nullptr; QProgressBar *m_memoryBar nullptr; QPushButton *m_killButton nullptr; }; #endif // MAINWINDOW_H// mainwindow.cpp #include mainwindow.h #include QHeaderView #include QLabel #include QMessageBox #include QProgressBar #include QPushButton #include QTableWidget #include QVBoxLayout MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setupUI(); if (!m_collector.init()) { QMessageBox::warning(this, QStringLiteral(提示), QStringLiteral(PDH 初始化失败CPU 使用率将无法显示)); } connect(m_timer, QTimer::timeout, this, MainWindow::refreshData); m_timer.start(2000); } void MainWindow::setupUI() { auto *centralWidget new QWidget(this); auto *layout new QVBoxLayout(centralWidget); auto *headerLayout new QHBoxLayout; m_cpuLabel new QLabel(QStringLiteral(CPU: --), centralWidget); m_memoryLabel new QLabel(QStringLiteral(内存: --), centralWidget); m_cpuBar new QProgressBar(centralWidget); m_memoryBar new QProgressBar(centralWidget); m_cpuBar-setRange(0, 100); m_memoryBar-setRange(0, 100); headerLayout-addWidget(m_cpuLabel); headerLayout-addWidget(m_cpuBar); headerLayout-addWidget(m_memoryLabel); headerLayout-addWidget(m_memoryBar); layout-addLayout(headerLayout); m_processTable new QTableWidget(centralWidget); m_processTable-setColumnCount(4); m_processTable-setHorizontalHeaderLabels({ QStringLiteral(PID), QStringLiteral(进程名), QStringLiteral(内存(KB)), QStringLiteral(线程数) }); m_processTable-horizontalHeader()-setStretchLastSection(true); m_processTable-setSelectionBehavior(QAbstractItemView::SelectRows); m_processTable-setEditTriggers(QAbstractItemView::NoEditTriggers); layout-addWidget(m_processTable); m_killButton new QPushButton(QStringLiteral(结束选中进程), centralWidget); layout-addWidget(m_killButton); connect(m_killButton, QPushButton::clicked, this, MainWindow::killSelectedProcess); setCentralWidget(centralWidget); setWindowTitle(QStringLiteral(Windows 实时系统监控工具)); resize(900, 600); } void MainWindow::updateSystemInfo(double cpuPercent, double memoryPercent) { m_cpuLabel-setText(QStringLiteral(CPU: %1%).arg(cpuPercent, 0, f, 1)); m_cpuBar-setValue(static_castint(cpuPercent)); m_memoryLabel-setText(QStringLiteral(内存: %1%).arg(memoryPercent, 0, f, 1)); m_memoryBar-setValue(static_castint(memoryPercent)); } void MainWindow::refreshData() { double cpu m_collector.getCpuUsagePercent(); double memory m_collector.getMemoryUsagePercent(); updateSystemInfo(cpu, memory); QVectorProcessInfo processes m_collector.getProcessList(); // 刷新期间禁用排序避免行数据错位 m_processTable-setSortingEnabled(false); m_processTable-setRowCount(processes.size()); for (int row 0; row processes.size(); row) { const ProcessInfo info processes.at(row); m_processTable-setItem(row, 0, new QTableWidgetItem(QString::number(info.pid))); m_processTable-setItem(row, 1, new QTableWidgetItem(info.name)); m_processTable-setItem(row, 2, new QTableWidgetItem(QString::number(info.memoryKB))); m_processTable-setItem(row, 3, new QTableWidgetItem(QString::number(info.threadCount))); } m_processTable-setSortingEnabled(true); } void MainWindow::killSelectedProcess() { int row m_processTable-currentRow(); if (row 0) { QMessageBox::information(this, QStringLiteral(提示), QStringLiteral(请先选择一个进程)); return; } bool ok false; DWORD pid m_processTable-item(row, 0)-text().toULong(ok); if (!ok) { return; } QString name m_processTable-item(row, 1)-text(); // 对系统关键进程做拦截提示 if (name QStringLiteral(explorer.exe) || name QStringLiteral(svchost.exe)) { QMessageBox::warning(this, QStringLiteral(警告), QStringLiteral(该进程为系统关键进程建议不要结束)); return; } QMessageBox::StandardButton btn QMessageBox::question( this, QStringLiteral(确认), QStringLiteral(确认结束进程 %1 (PID: %2)?).arg(name).arg(pid)); if (btn ! QMessageBox::Yes) { return; } HANDLE hProcess OpenProcess(PROCESS_TERMINATE, FALSE, pid); if (hProcess) { TerminateProcess(hProcess, 1); CloseHandle(hProcess); refreshData(); } else { QMessageBox::warning(this, QStringLiteral(错误), QStringLiteral(打开进程失败请尝试以管理员身份运行)); } }主窗口逻辑里refreshData是每 2 秒触发一次的总入口。它先刷新 CPU 和内存数据再拉取进程列表填入表格。这里比较值得注意的设计是刷新期间先setSortingEnabled(false)填完数据再重新开启。如果表格启用了排序而你在逐行写入数据用户此时又刚好点击了列头排序Qt 可能会在写入过程中重排行数据导致行和内容错位。结束进程时做了一个关键进程拦截explorer.exe和svchost.exe被直接拦截。实际项目中还可以维护一个更大的系统进程白名单。原因很明显结束svchost.exe可能导致系统服务异常结束explorer.exe会导致桌面图标和任务栏消失。这个保护机制不是多余的尤其当你在自己电脑上测试时。5.3 程序入口// main.cpp #include mainwindow.h #include QApplication int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }这个入口非常简单。Qt 应用必须创建QApplication对象进入事件循环后QTimer才会周期性触发。5.4 可选扩展CPU 历史折线图基础的进度条只能看到当前值看不到变化趋势。如果希望工具更像真正的系统监控软件可以加入 Qt Charts 折线图。这里给出接入代码的核心片段。先确认你的 Qt 安装中包含 Charts 模块然后在 CMake 中加入find_package(Qt6 REQUIRED COMPONENTS Widgets Charts) target_link_libraries(SysMonitor PRIVATE Qt6::Charts)在主窗口的头文件中增加两个 series 成员#include QtCharts/QChartView #include QtCharts/QLineSeries QT_CHARTS_USE_NAMESPACE // 成员变量 QLineSeries *m_cpuSeries nullptr; QLineSeries *m_memorySeries nullptr; int m_pointCount 0;在setupUI中初始化图表QChart *chart new QChart(); chart-setTitle(QStringLiteral(CPU / 内存历史趋势)); m_cpuSeries new QLineSeries(); m_memorySeries new QLineSeries(); m_cpuSeries-setName(QStringLiteral(CPU)); m_memorySeries-setName(QStringLiteral(内存)); chart-addSeries(m_cpuSeries); chart-addSeries(m_memorySeries); chart-createDefaultAxes(); chart-axes(Qt::Vertical).first()-setRange(0, 100); QChartView *chartView new QChartView(chart); chartView-setRenderHint(QPainter::Antialiasing); layout-addWidget(chartView);在refreshData中追加数据点并控制曲线长度m_cpuSeries-append(m_pointCount, cpu); m_memorySeries-append(m_pointCount, memory); m_pointCount; // 最多保留最近 60 个数据点 if (m_cpuSeries-count() 60) { m_cpuSeries-remove(0); m_memorySeries-remove(0); }这里用m_pointCount自增作为横坐标目的是保持曲线的连续性。如果直接用行号作为横坐标刷新过程中界面会不停变化反而看不出趋势。createDefaultAxes会在没有坐标轴的情况下自动创建之后再设置纵轴范围 0 到 100。一个小提醒QChart 的坐标系和 QWidget 的坐标系不同如果曲线不显示优先检查是否显式设置了坐标轴范围以及 series 是否在createDefaultAxes之前添加到了 chart。这是 Qt Charts 使用中最常见的几个问题之一。6. 运行结果与效果验证代码写完后的验证思路很重要不能只看“程序能跑起来”。建议按下面的顺序验证。编译通过后运行程序主窗口出现时顶部 CPU 标签显示当前使用率进度条随之变化。用下面的方法判断数据是否可信打开 Windows 自带的任务管理器Ctrl Shift Esc对比 CPU 使用率和内存占用率的数值。两者会有一些时间差和算法差异但总体应在合理范围内。连续观察 10 秒以上确认 CPU 数值是动态变化的而不是固定在一个值。打开一些大型软件观察 CPU 数值明显上升说明数据采集确实有效。进程表中应该能看当前正在运行的进程包括浏览器、编辑器等。内存列应该与任务管理器中的“内存(活动私有工作集)”或“工作集”比较接近。如果程序出现了进程列表空白、CPU 一直为 0、窗口无响应等问题直接跳到下一节的排查清单。验证结束进程功能时建议先创建一个测试进程比如打开记事本notepad.exe然后选中它点击“结束选中进程”。不要在验证阶段直接结束自己电脑上不熟悉的系统进程更不要为了验证功能去结束杀毒软件或系统服务。运行效果上基础版预期是窗口一般保持在几十兆内存占用CPU 占用几乎可以忽略。如果你发现程序自身 CPU 占用异常高基本可以确认是在主线程里做了过重的操作后面会讲怎么优化。7. 常见问题与排查思路这个项目虽然代码量不大但新手实际运行时遇到的问题往往比想象中多。下面是几个高频问题问题现象可能原因排查方式解决方案编译报错pdh.h找不到未链接 Windows SDK 头文件路径检查 CMake 中 find_package 和编译器版本安装“Windows SDK”组件确认使用 MSVC 编译器链接失败提示 pdh.lib 未找到未链接库检查 target_link_libraries添加pdh和psapi依赖程序启动后 CPU 显示 0 且不变PDH 初始化失败或两次采集间隔过短在 init 中检查返回值确认路径正确先调用一次 PdhCollectQueryData 作为基准进程列表为空快照创建失败或权限不足检查 CreateToolhelp32Snapshot 返回值确认程序位数与系统位数一致界面刷新时卡顿进程列表遍历耗时过长打开任务管理器观察程序 CPU 占用降低刷新频率或把采集放入 QThread中文显示乱码MSVC 默认编码导致检查源码文件编码加编译选项/utf-8结束进程失败权限不足查看 OpenProcess 返回以管理员身份运行程序windeployqt 发布后缺少 DLL运行库路径不对检查部署输出目录确认从对应的 Qt 工具链目录运行 windeployqt这里展开说明几个容易出现误诊断的问题。PDH 初始化失败并不是代码写错很可能只是系统版本比较老或者系统性能计数器被禁用。Windows 服务列表里有一个“Performance Logs Alerts”服务如果这个服务被停用PDH 查询会异常。不过 Windows 10 和 Windows 11 上一般都能正常工作。程序位数也是一个隐蔽问题。如果你用 32 位 Qt 编译器编译程序就是 32 位进程在 64 位系统上运行时会通过 WOW64 兼容层访问系统 API虽然大部分 API 可用但某些 PDH 计数器和路径行为可能和 64 位进程不同。建议一律使用 64 位构建这也是 Qt 6 时代的默认趋势。进程列表循环里已经处理了无权限进程的问题OpenProcess失败时直接跳过内存统计但进程本身仍然会出现在表格里。所以不要因为看到某些系统进程的内存列为空或为 0 就认为代码有误。8. 最佳实践与工程建议跑通基础版之后如果希望这个项目继续服务于你的成长下面这些工程层面的建议值得认真对待。8.1 强制分层不要让 UI 里堆系统调用上面的代码里所有 Windows API 都在SysInfoCollector中界面只负责调用。这样做的价值在于你后续换成 Linux 采集逻辑时界面完全不用改你为采集类写单元测试时也不需要启动 Qt 界面。即使只是一个小工具分层带来的维护收益也立竿见影。8.2 线程模型要有预判当前版本的进程遍历是在主线程里做的2 秒刷新一次可接受。但如果进程数量很多或者你后续加了每个进程的 CPU 采样主线程就会卡顿。正确的演进方向是把SysInfoCollector中的耗时采集放到QThread中执行通过信号槽把采集结果发送回主线程刷新 UI。这里要注意的是SysInfoCollector中涉及 Windows API 的操作并不依赖 QObject 的线程亲和性但跨线程传递ProcessInfo需要使用QVectorProcessInfo注册元类型或者直接使用Qt::QueuedConnection传递自定义结构体。8.3 刷新频率要可配置一个经验值是把刷新间隔做成变量或设置项而不是硬编码 2000 毫秒。默认 2 秒窗口最小化时可以暂停刷新用户希望更灵敏时可以调到 1 秒。对工具软件来说浪费无谓的 CPU 和功耗是不专业的。8.4 安全与权限边界结束进程功能必须保持敬畏。代码里对explorer.exe和svchost.exe的拦截只是一个示例真正发布工具时还应该做到二次确认对话框要显示完整的进程名和 PID结束失败时提示可能的原因而不是静默失败尽量使用最小的权限请求。另外这个工具如果要在团队内分发建议先获得公司安全合规的批准不要随意结束员工电脑上的进程。8.5 完整发布流程开发环境运行正常只是第一步。Qt 项目需要做部署Windows 下最常用的方式是windeployqt。在 Qt 命令行环境里执行windeployqt SysMonitor.exe这个工具会自动把程序依赖的 Qt DLL 拷贝到 exe 所在目录。注意必须使用与编译工具链匹配的 Qt 环境比如你用 MSVC 编译就要从“Qt 6.x (MSVC 2022 64-bit)”命令行里执行。如果使用了 Qt Chartswindeployqt也会一并处理。发布前还应该在干净的 Windows 环境下运行一次确认没有缺失 DLL。8.6 界面体验细节监控工具这类软件的界面虽然不复杂但细节决定观感表格内容应支持按内存降序排序方便找出占用最高的进程双击进程行可以查看更多信息刷新时不要闪动可以用setUpdatesEnabled(false)批量更新后重新
返回列表