资讯动态

用C++和Qt从零实现任务管理器:进程枚举与CPU占用率实时监控

发布时间:2026/9/12 15:34:57 来源:尧图企业网站定制
简介使用C与Qt框架实现的高仿Windows 10任务管理器项目源码包适合具备基本C语法、希望入门Qt桌面开发的初学者也适合需要参考系统进程监控与UI布局案例的开发者。压缩包共11个文件包含3个C源文件、2个头文件、1个Qt Designer界面文件、1个工程文件以及3张效果截图整体仅100KB结构精简且便于快速定位核心代码。项目完整展示了如何用QProcess列举与管理系统进程通过QTableWidget呈现进程列表并结合QTimer、QChart等组件实时绘制CPU和内存使用情况同时利用QSS样式表模拟Win10任务管理器的现代外观。多线程处理部分使用QThread异步获取系统信息避免界面卡顿还涉及QSettings保存配置等实用技巧。目前已有165人学习该资源非常适合作为GUI编程、系统信息获取与多线程应用的综合练手项目。1. 从零仿一个任务管理器最难的其实不是界面如果只把“高仿Win10任务管理器”当成一个练习Qt布局的项目那用QWidget拖几个按钮半天就能交差。但真把任务管理器当作对标对象你会发现它本质是一个高频性能数据采集系统——进程列表要秒开、CPU曲线要实时、内存数字要跳得跟系统自带的一样快这背后涉及进程枚举、性能计数器、增量计算和UI刷新的协同。C在这条链路里的位置不可替代直接调Windows API没有托管层转译PDH查询和Toolhelp32快照都是原生接口Qt则负责把采集到的数据以图表和表格的形式稳定呈现。这组搭配正是很多系统监控类工具的实际方案而不是为了“高仿”去硬凑技术栈。这篇文章的目标读者是已经能写C但对Windows系统编程还不熟的人。你在里面会看到完整的进程枚举与性能数据采集代码、CPU占用率计算的数学逻辑、Qt Charts实时绘图的工程写法以及权限、句柄泄漏、刷新频率这些不跑到真实环境根本遇不到的坑。适合作为从业务开发转向系统工具开发的第一份可运行参考。2. 为什么是C加Qt以及任务管理器的数据到底从哪来仿制任务管理器第一步要回答的问题不是“界面怎么做”而是“数据从哪里来”。Windows不会直接给你一个结构体里面写着进程名、CPU占用率、内存占用、磁盘读写这些现成的字段。系统暴露的是底层的进程对象、性能计数器、内存映射文件任务管理器这类工具要自己把这些数据组装成一张可读的表。2.1 数据源选型Toolhelp32快照、PDH还是性能计数器Windows上获取进程列表和性能数据有几条主流路径各自适用场景不一样。最常用的是这三种。数据源系统API获取内容适用场景性能开销Toolhelp32CreateToolhelp32Snapshot进程列表、线程数、进程ID、父进程ID全量快照给表格提供基础行数据快但一次性数据PDHPdhOpenQuery / PdhGetFormattedCounterValueCPU使用率、内存、磁盘、网络计数器实时曲线和增量指标中等适合周期性采集性能计数器NtQuerySystemInformation更底层的系统状态特殊字段或驱动级数据不稳定依赖内部结构体做仿制版任务管理器我一般用Toolhelp32做进程列表主数据源用PDH做CPU和内存的性能曲线数据源。前者拿到的PROCESSENTRY32结构体里直接有szExeFile、cntThreads、th32ProcessID这些字段够表格用。但注意这个结构体里没有CPU占用率也没有内存工作集大小——这两项必须另取。Toolhelp32的具体路径是调用CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)拿到快照句柄然后Process32FirstW加Process32NextW循环遍历把每个进程的ID和可执行文件名存进一个表。这个调用开销不大但每次刷新都是一次全量遍历所以刷新频率一般控制在1到2秒一次不要用200毫秒刷一次这种激进策略。PDH是Windows性能数据采集的老牌接口。它通过计数器路径counter path来查询某个指标比如\Process(_Total)\% Processor Time。它的优势是系统已经帮你算好了增量值读出来直接就是百分比。劣势是查询路径字符串容易写错而且PdhAddCounter之后第一次读取返回的数据通常不可用需要先丢弃。还有一条路是直接用GetSystemTimes用于计算CPU总体使用率和GetProcessTimes用于计算单进程使用率。这两个API返回的是内核态和用户态的CPU时间单位是100纳秒的FILETIME。要做的是前后两次采样做差值再用差值除以系统总的历时时间得出占用率。这个方案的好处是没有PDH那么重的依赖而且可以精确到单进程。代价是CPU占用率的计算逻辑要自己写容易在小数精度和采样窗口上出错。2.2 CPU占用率的增量计算原理任务管理器里那个“3% CPU”并不是实时的瞬时值而是某个时间窗口内的平均值。常见的做法是隔一段时间采样一次然后计算差值。用GetProcessTimes获取进程的CPU时间用GetSystemTimes获取系统的总空闲时间和总时间然后按这个公式算// 计算单个进程的CPU使用率 // prevKernelTime, prevUserTime : 上一次采样得到的进程内核态用户态时间 // prevIdleTime, prevTotalTime : 上一次采样得到的系统空闲时间和总时间 double CalcProcessCpuUsage(uint64_t procTimeDiff, uint64_t sysTotalDiff) { if (sysTotalDiff 0) return 0.0; // 进程占用时间差 / 系统总时间差 * 100再乘以CPU核心数 // 因为系统总时间累加了所有核心的时间片 return (procTimeDiff / (double)sysTotalDiff) * 100.0; } // 示例获取进程CPU时间100ns为单位的FILETIME FILETIME createTime, exitTime, kernelTime, userTime; GetProcessTimes(hProcess, createTime, exitTime, kernelTime, userTime);这里最容易搞错的一点是CPU核心数。GetSystemTimes返回的系统总时间包含了所有核心的累加时间比如8核CPU1秒钟内系统总时间会累加8秒的CPU时间片。而单个进程只跑在其中某些核心上。所以公式里算出来的比例其实已经是“占所有核心总能力的百分比”不需要再除以核心数。但很多初学者会习惯性地除以核心数导致CPU占用率显示只有真实值的一个零头。PDH的\Processor(_Total)\% Processor Time计数器则直接返回处理过的百分比值范围是0到100系统已经处理了多核心累加的问题直接使用即可。2.3 Qt在监控类界面里适合干什么Qt在“高仿Win10任务管理器”这个项目里承担的是UI、交互和定时调度。长这样QTabWidget做顶部的“进程/性能/应用历史记录/启动”标签页QSplitter做进程列表的分栏QTableView配QStandardItemModel放进程表格QChart做性能页的CPU和内存曲线。QChart是QtCharts模块的组件曲线图、面积图、饼图都内置。任务管理器性能页的CPU使用率曲线本质是一个实时折线图用QChart很直接。QChart和QTableView之间通过定时器驱动QTimer每1000毫秒触发一次刷新槽函数把新采集到的数据追加到曲线序列和表格模型里。选Qt还有一个现实理由对系统API的封装足够薄。QProcess、QFileInfo这些封装不影响你直接用Windows原生接口而且Qt的QWidget体系做这种密集列表刷新时性能可控。用QWidget还是用QML做界面我建议你用QWidget——任务管理器是典型的桌面工具风格QWidget的QTableView加QSS改样式就能做出高仿效果QML反而在处理大量行的实时刷新时多一层间接层。3. 进程列表与性能数据采集的工程实现这一章给出能编译通过的最小实现方案。目标是打开程序后一秒内看到一个表格里面是当前系统所有进程包含进程名、PID、CPU使用率、内存占用每秒刷新一次。这个目标本身就是任务管理器最核心的功能。3.1 用Toolhelp32枚举进程的基础代码先写一个最精简的枚举函数拿到进程名和PID。注意用宽字符版本因为现代Windows的进程名几乎都是UTF-16编码的。#include windows.h #include tlhelp32.h #include vector #include string struct ProcessInfo { DWORD pid; std::wstring name; DWORD threadCount; }; // 枚举当前系统所有进程 std::vectorProcessInfo EnumProcessList() { std::vectorProcessInfo result; HANDLE snapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot INVALID_HANDLE_VALUE) { return result; } PROCESSENTRY32W pe; pe.dwSize sizeof(PROCESSENTRY32W); if (Process32FirstW(snapshot, pe)) { do { ProcessInfo info; info.pid pe.th32ProcessID; info.name pe.szExeFile; info.threadCount pe.cntThreads; result.push_back(info); } while (Process32NextW(snapshot, pe)); } CloseHandle(snapshot); return result; }创建快照时TH32CS_SNAPPROCESS表示只对进程做快照第二个参数传0表示对系统所有进程快照。PROCESSENTRY32W的dwSize必须在调用Process32FirstW之前赋值否则API会返回ERROR_INVALID_PARAMETER。CloseHandle必须在遍历结束后调用否则每次刷新都会泄漏一个内核句柄这个泄漏在长时间运行时会被任务管理器里的“句柄数”列直接暴露。3.2 获取进程的内存占用Process32FirstW拿不到工作集大小需要单独调用GetProcessMemoryInfo它在Psapi.h头文件里需要链接Psapi.lib。这个方法要求你先用OpenProcess拿到目标进程的内核句柄。#include psapi.h // 获取进程内存占用工作集大小单位字节 SIZE_T GetProcessMemoryWorkingSet(DWORD pid) { HANDLE hProcess OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (!hProcess) { // 常见失败原因目标进程是系统进程或受保护进程 // 拒绝访问时返回0界面显示为0即可不要弹错误 return 0; } PROCESS_MEMORY_COUNTERS pmc; pmc.cb sizeof(PROCESS_MEMORY_COUNTERS); SIZE_T result 0; if (GetProcessMemoryInfo(hProcess, pmc, sizeof(pmc))) { result pmc.WorkingSetSize; // 工作集大小约等于任务管理器里的“内存”列 } CloseHandle(hProcess); return result; }OpenProcess的第一个参数是访问权限。PROCESS_QUERY_LIMITED_INFORMATION是最小权限能查进程信息和内存计数器。不要用PROCESS_ALL_ACCESS那会频繁触发拒绝访问而且对系统进程必然失败。这里有一个界面显示上的小细节任务管理器里进程名列同时显示图标和名字图标用SHGetFileInfoW传进程的可执行文件路径来取路径要通过QueryFullProcessImageNameW获取这在OpenProcess之后调用也能顺带验证进程是否存在。3.3 用PDH拉取实时CPU使用率枚举进程和内存是快照型数据的典型实现。而CPU占用率是持续变化的只在每次刷新时读一次快照显示出来的数字会跳来跳去很难看。常见的工程做法是维护一个“前后两次采样差”状态用定时器周期性调用采样函数。我这里给出用PDH的实现方式适合做表格里的CPU列和性能页曲线#include pdh.h static PDH_HQUERY cpuQuery; static PDH_HCOUNTER cpuTotalCounter; static PDH_HCOUNTER cpuIdleCounter; // 初始化PDH void InitPdhCounters() { PdhOpenQueryW(nullptr, 0, cpuQuery); PdhAddEnglishCounterW(cpuQuery, L\\Processor(_Total)\\% Processor Time, 0, cpuTotalCounter); PdhAddEnglishCounterW(cpuQuery, L\\Processor(_Total)\\% Idle Time, 0, cpuIdleCounter); // 第一次采样通常返回PDH_INVALID_DATA需要先调一次CollectQuery PdhCollectQueryData(cpuQuery); } // 采集一次CPU数据并返回使用率 double QueryTotalCpuUsage() { // 两次collect之间要有间隔一般300ms以上否则差值太小 PdhCollectQueryData(cpuQuery); DWORD type 0; PDH_FMT_COUNTERVALUE value; PdhGetFormattedCounterValue(cpuTotalCounter, PDH_FMT_DOUBLE, type, value); return value.doubleValue; // 如果返回PDH_CSTATUS_INVALID_DATA说明还没准备好返回上次值 }PDH的核心思路是“双采样”PdhCollectQueryData第一次收集得到一个基准点第二次收集时计算两个时间戳之间的差值。所以初始化后第一个返回值必须丢弃。这里用PdhAddEnglishCounterW而不是PdhAddCounterW是因为本地化的Windows会翻译计数器路径英文路径不匹配时后者会返回PDH_CSTATUS_NO_OBJECT。3.4 刷新协调定时器、缓冲和UI更新分离把所有采集逻辑做成一个刷新循环后会遇到一个典型问题每秒钟采集所有进程的CPU和内存会导致UI卡顿。原因是OpenProcess加GetProcessTimes加GetProcessMemoryInfo全套跑一遍在有几百个进程的机器上可能要几十毫秒加上表格刷新主线程就忙不过来了。我一般把采集和UI分离一个QThread在后台跑采集循环采集完通过信号把QVector 发给主线程主线程只负责往QStandardItemModel里填充数据。QTableView的setUpdatesEnabled(false)可以在填充大量行时关掉绘制填完再打开这样滚动时也不会闪屏。// 后台采集线程的核心循环伪代码 class CollectWorker : public QThread { Q_OBJECT public: void run() override { while (!m_stop) { auto procs EnumProcessList(); QVectorProcessData dataList; for (auto p : procs) { ProcessData d; d.pid p.pid; d.name QString::fromStdWString(p.name); d.memory GetProcessMemoryWorkingSet(p.pid); d.cpu QueryProcessCpuUsage(p.pid); dataList.push_back(d); } emit dataReady(dataList); msleep(1000); } } };这是整个仿制工程里最容易出问题的一段。常见错误是忘掉对系统空闲进程“System Idle Process”做特殊处理它在Toolhelp32快照里PID为0OpenProcess对它调用会失败。另一个问题是ProcessData里带有字符串QVector在跨线程传递时会在主线程发生拷贝所以在emit之前用std::move减少一次拷贝是有实际收益的。4. 性能页曲线绘制和界面高仿的落地细节进程列表只是任务管理器的“进程”标签页。真正让人一眼觉得“像”的是性能页里那个动态刷新的CPU使用率曲线以及左侧那条逐步减淡的尾部轨迹。Win10任务管理器的曲线不是普通折线它是一条填充了渐变透明色的面积图新数据在右侧旧数据向左推进。4.1 QChart绘制实时CPU使用率曲线的完整代码用QtCharts实现这个效果核心是QLineSeries加QAreaSeries外加一个固定长度的环形缓冲区保存最近60秒的数据。#include QtCharts/QChartView #include QtCharts/QLineSeries #include QtCharts/QAreaSeries #include QtCharts/QValueAxis #include QTimer class CpuUsageChart : public QWidget { Q_OBJECT private: QLineSeries* line new QLineSeries(); QAreaSeries* area nullptr; QChart* chart nullptr; QValueAxis* axisX nullptr; QValueAxis* axisY nullptr; QTimer* timer; QVectordouble ringBuffer; // 固定容量60 public: CpuUsageChart(QWidget* parent nullptr) : QWidget(parent) { ringBuffer.resize(60, 0.0); line-setPen(QPen(QColor(0, 120, 215), 2)); area new QAreaSeries(line); area-setColor(QColor(0, 120, 215, 60)); // 半透明填充模拟Win10的渐隐效果 chart new QChart(); chart-addSeries(area); chart-legend()-hide(); chart-setAnimationOptions(QChart::NoAnimation); // 监控图表不需要平滑动画 chart-setBackgroundBrush(QColor(255, 255, 255)); axisX new QValueAxis(); axisX-setRange(0, 60); axisX-setTickCount(7); axisX-setLabelFormat(%d); axisX-setTitleText(时间秒); axisY new QValueAxis(); axisY-setRange(0, 100); axisY-setTickCount(5); chart-addAxis(axisX, Qt::AlignBottom); chart-addAxis(axisY, Qt::AlignLeft); area-attachAxis(axisX); area-attachAxis(axisY); QChartView* view new QChartView(chart); view-setRenderHint(QPainter::Antialiasing); QVBoxLayout* layout new QVBoxLayout(this); layout-addWidget(view); timer new QTimer(this); connect(timer, QTimer::timeout, this, [this]() { double value QueryTotalCpuUsage(); ringBuffer.remove(0); // 头部弹出 ringBuffer.append(value); // 新数据追加到尾部 UpdateSeries(); }); timer-start(1000); } };QAreaSeries默认不会生成第一条线到X轴的闭合区域需要把线段的起始点Y值设为0面积才能正确填充下边界。Win10的曲线在30秒和60秒两个时刻有透明度分段变化简化版是用同一颜色加Alpha来近似。QChart::setAnimationOptions在这里必须设为NoAnimation因为监控图表高频更新平滑动画会导致CPU占用率本身成为负担帧率也会下降。4.2 高仿界面的QSS样式和布局策略任务管理器左侧是导航栏右侧是内容区。中间进程表格能用样式表做成接近Win10的白色底、蓝色选中行效果。QTableView的交替行颜色设置为浅灰底选中色是系统蓝色这样视觉上最接近。table-setAlternatingRowColors(true); table-setSelectionBehavior(QAbstractItemView::SelectRows); table-setSelectionMode(QAbstractItemView::ExtendedSelection); table-setEditTriggers(QAbstractItemView::NoEditTriggers); table-verticalHeader()-setVisible(false); table-horizontalHeader()-setStretchLastSection(true);QSS方面可以这样覆盖默认样式QTableView { background-color: #ffffff; alternate-background-color: #f5f7f9; gridline-color: #e0e0e0; font-size: 13px; } QTableView::item:selected { background-color: #cce4f7; color: #000000; } QTabBar::tab { padding: 6px 8px; background: #f0f0f0; } QTabBar::tab:selected { background: #ffffff; border-left: 3px solid #0078d7; }这里有两点是Win10风格的关键一是选中态的颜色是浅蓝而不是深蓝二是左侧导航选中时左侧有3像素的蓝色竖条这是Win10 Fluent Design语言在传统控件上最明显的延续。把这些样式写进一个全局qss文件在main函数里统一加载比逐个控件设置样式更方便维护。4.3 表格列的排序和筛选任务管理器的进程列表支持点击表头排序和按名称搜索这两件事用QTableView都自带基础支持。排序用QSortFilterProxyModel搜索用setFilterFixedString。QSortFilterProxyModel* proxy new QSortFilterProxyModel(this); proxy-setSourceModel(model); table-setModel(proxy); table-setSortingEnabled(true); table-sortByColumn(0, Qt::AscendingOrder);注意QSortFilterProxyModel的排序会对每次数据刷新后的整个表做一次重排行数多时开销不小。一个常见的工程改动是只在用户点击表头时触发排序数据刷新时保持当前排序不动避免每秒钟都重排一次。做法是刷新数据时暂时断开sortIndicatorChanged信号或者直接在刷新前设置table-setSortingEnabled(false)刷新完成后再恢复。5. 权限提帧UAC自提权、数据缓冲与验证技巧仿制的版本要接近真实任务管理器必然要面对一个硬门槛很多高价值信息只有在管理员权限下才能看到比如受保护的进程、系统进程的完整命令行、其它用户的会话进程。真实任务管理器是系统托管的高权限工具你的程序默认以普通用户身份启动能看到的进程列表会比系统真实情况少一部分。5.1 自动请求管理员权限常见的做法是把项目里生成的exe嵌入一个UAC manifest让Windows在启动时自动弹出UAC确认框。Qt Creator的.pro文件里加一行就能实现CONFIG windows QMAKE_MANIFEST uac.manifestuac.manifest内容的核心部分是requestedExecutionLevel设为requireAdministrator这里的原理是UAC的“自动提权”机制让创建的进程token包含管理员权限。注意这一步不是所有机器都需要普通开发机器看自身情况决定但生产环境里如果不提权EnumProcessList拿到的列表会比“以管理员身份运行”时少系统进程。5.2 高频刷新下的数据缓冲设计任务管理器每秒钟刷新一次列表数据但CPU曲线是连续更新的。这两者共用一个采集线程时曲线的数据粒度会受列表刷新的阻塞影响。我的做法是CPU曲线的采样频率独立于列表曲线用500毫秒采样一次列表用1000毫秒刷新一次。采集线程内部维护两个时间点到500毫秒就只采CPU并emit曲线信号到1000毫秒才做全量枚举。这样曲线比列表平滑且列表卡顿不会拖拽曲线。5.3 用自带任务管理器做A/B验证写好之后怎么确认数据是对的我建议把程序跑起来和系统任务管理器并排对比进程总数一致排除提权差异后内存列里排名前几的进程和系统任务管理器一致CPU占用率数字的差距在5个百分点以内PID完全一致这里附带一个自查技巧用GetSystemTimes计算的系统总CPU占用率观察它在空载时应接近0到5满载时接近100。如果空载就显示50以上多半是内核时间差除以用户时间差的错误回到2.2节的公式再核对一遍。任务管理器项目的复杂度比你预期的高一点但价值也在这它混合了进程枚举、性能计算、GUI实时刷新、权限模型、图表绘制五个知识块任何一个单独拎出来都是系统工具开发的常见议题。调试时最管用的手段不是加日志而是把自己摆在“API会失败”的预设上——对OpenProcess和PdhCollectQueryData的每次失败都要有默认值兜底。整个工程跑起来之后你实际上已经掌握了一套“采集-计算-呈现”性能监控工具的通用骨架。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价