尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用C++和Qt高仿Win10任务管理器:进程数据链路与界面实现

用C++和Qt高仿Win10任务管理器:进程数据链路与界面实现 简介一份基于C与Qt框架实现的高仿Windows 10任务管理器项目源码面向有一定C基础、希望系统学习Qt GUI开发和系统进程管理的开发者。资源包为zip格式大小仅100KB共11个文件包含cpp源文件、h头文件、ui界面文件、pro工程文件及数张png截图其中mainwindow与processinfo模块分别负责界面交互与进程信息处理代码结构清晰。已有165人学习下载。项目完整实现了进程列表展示、CPU与内存实时监控、多线程异步刷新等核心功能并利用Qt Designer布局、QSS样式模拟Win10界面同时演示了QProcess进程枚举、QTimer定时刷新、QThread耗时任务处理等技术的实际用法。附带的流程图、模块图和界面预览图能帮助读者快速建立整体认知无论是用于课程设计、毕业设计还是个人项目二次开发都是一份高性价比的参考资源。1. 用C和Qt仿写Win10任务管理器难点不在界面在数据链路如果以为任务管理器只是个显示进程列表的小工具那仿写起来确实没什么好说的。可一旦进入高仿阶段你会发现真正的难点几乎全在“数据”这一侧进程快照怎么枚举、内存占用怎么算、CPU占用为什么总是跳变、怎么在占用几万个句柄之后还不泄漏。Win10的任务管理器本身由C写成在Windows上做这个题材C配合Qt框架是技术栈上最顺的一条路。这篇文章我会把整条链路拆开先用系统API拿到进程数据再让Qt的模型视图把数据变成可排序表格最后补上CPU曲线和结束进程这些交互顺带把刷新策略和发布部署里的坑一次性讲完。适合已经能写出Qt小工具、想接触进程级系统编程的C开发者也适合拿来做Qt面试项目的人。2. 用Win32 API做进程枚举数据层先稳定再谈界面任务管理器最重要的要求是“每次打开看到的进程数都是准的”。这依赖系统API而不是Qt自己的封装——Qt没有直接枚举系统进程的跨平台接口所以第一步是回到Windows SDK。2.1 CreateToolhelp32Snapshot一次快照把进程表抓全所有进程信息的起点是一个快照句柄。常见做法是用CreateToolhelp32Snapshot它能把当前系统里的进程列表、模块列表或线程列表一次性拍进一份快照之后用Process32First/Process32Next顺序遍历。#include windows.h #include tlhelp32.h struct ProcInfo { DWORD pid; DWORD parentPid; std::wstring name; std::wstring exePath; SIZE_T workingSet; }; std::vectorProcInfo EnumerateProcesses() { std::vectorProcInfo result; HANDLE snap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap INVALID_HANDLE_VALUE) return result; PROCESSENTRY32W pe{ sizeof(PROCESSENTRY32W) }; for (BOOL ok Process32FirstW(snap, pe); ok; ok Process32NextW(snap, pe)) { ProcInfo info; info.pid pe.th32ProcessID; info.parentPid pe.th32ParentProcessID; info.name pe.szExeFile; info.workingSet pe.dwSize; result.push_back(std::move(info)); } CloseHandle(snap); return result; }这段代码里有两个容易忽略的细节。第一个是PROCESSENTRY32W的初始化必须把dwSize字段提前填成结构体大小否则Process32First会返回FALSE并报ERROR_INSUFFICIENT_BUFFER。第二个是快照句柄必须用CloseHandle关掉这个句柄不是普通文件句柄但泄漏它一样会让任务管理器的句柄数随刷新次数线性增长。2.2 从dwSize到完整内存占用别只看工作集PROCESSENTRY32W里的dwSize字段是工作集大小working set它表示进程当前物理驻留内存的量不等于任务管理器“内存”列显示的那个值。要获取更完整的性能计数器得另开一条路用OpenProcess拿句柄配合GetProcessMemoryInfo。#include psapi.h bool GetProcessMemory(DWORD pid, SIZE_T workingSet, SIZE_T privateBytes) { HANDLE hProc OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (!hProc) return false; PROCESS_MEMORY_COUNTERS_EX pmc{ sizeof(pmc) }; BOOL ok GetProcessMemoryInfo(hProc, reinterpret_castPROCESS_MEMORY_COUNTERS*(pmc), sizeof(pmc)); CloseHandle(hProc); if (!ok) return false; workingSet pmc.WorkingSetSize; privateBytes pmc.PrivateUsage; return true; }注意这里用PROCESS_QUERY_LIMITED_INFORMATION而不是PROCESS_ALL_ACCESS前者在受保护进程上也能部分成功而且不需要管理员权限。任务管理器里“内存(活动私有工作集)”依赖的是私有字节privateBytes而不是工作集两者差在共享页面在不在统计范围内。高仿时建议内存列默认按私有字节显示详细页再展示工作集。2.3 CPU占用率两次取样的差值才可靠采样间隔要稳定CPU占用无法单次算出它必须由系统时间在两个时刻的差值决定。机制分两层全局层面用GetSystemTimes拿内核态与用户态的总CPU时间进程层面用GetProcessTimes拿该进程在created与exit之间的kernelTime和userTime。通常做法是固定一个刷新周期比如1秒或2秒把两次快照的差值做除法。struct CpuSnapshot { ULARGE_INTEGER sysKernel, sysUser; ULARGE_INTEGER procKernel, procUser; }; double CalcCpuUsage(const CpuSnapshot prev, const CpuSnapshot now) { ULONGLONG sysDiff (now.sysKernel.QuadPart - prev.sysKernel.QuadPart) (now.sysUser.QuadPart - prev.sysUser.QuadPart); if (sysDiff 0) return 0.0; ULONGLONG procDiff (now.procKernel.QuadPart - prev.procKernel.QuadPart) (now.procUser.QuadPart - prev.procUser.QuadPart); return (double)procDiff * 100.0 / (double)sysDiff; }这部分的三个参数是踩坑重灾区。第一GetProcessTimes返回的是FILETIME它在API内部按0.1微秒为单位不能用普通LONGLONG直接做减法必须拆成ULARGE_INTEGER之后再做减法否则负数会直接截断。第二两次取样之间如果用户把系统睡眠唤醒sysDiff会包含睡眠时间导致所有进程的CPU都异常偏大常见做法是把跨越唤醒的采样点直接丢弃。第三线程是进程的子集任务管理器的表头里的CPU列其实默认显示的是“CPU总占比”你如果想改进为可展开线程级就得对进程的所有线程做一遍GetThreadTimes并累加那是另一个完整功能了本文先不展开。到这里数据层的骨架已经成型。下一章把它接进Qt的模型视图里让这些Win32数据能像任务管理器那样按列排序、可筛选地展示出来。3. QAbstractTableModel让进程列表支持点击排序和后台刷新Win10任务管理器最让人顺手的一点是“点列头就能排序”。Qt里最稳妥的做法是用“模型/视图”模式而不是把数据塞进QTableWidget手动操作单元格。这两者都能画表格但前者的数据与显示分离刷新400个进程时不会触发界面卡顿。3.1 把进程快照包进ModelQt::UserRole存原始数据先把进程列表封装成一个QAbstractTableModel子类。列数固定为进程名、PID、CPU、内存、状态。数据来源是上一章的结构体这里给一个可运行的model骨架。class ProcessTableModel : public QAbstractTableModel { Q_OBJECT public: enum Column { ColName 0, ColPid, ColCpu, ColMemory, ColState, ColCount }; int rowCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : m_procs.size(); } int columnCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : ColCount; } QVariant data(const QModelIndex idx, int role) const override { if (!idx.isValid()) return {}; const ProcInfo p m_procs.at(idx.row()); if (role Qt::DisplayRole) { switch (idx.column()) { case ColName: return QString::fromStdWString(p.name); case ColPid: return (qulonglong)p.pid; case ColCpu: return QString::number(p.cpuPercent, f, 1) QLatin1String( %); case ColMemory: return FormatMemory(p.privateBytes); case ColState: return stateToString(p.state); } } if (role Qt::UserRole) { // 排序和筛选都应基于原始数据而不是显示字符串 switch (idx.column()) { case ColCpu: return QVariant::fromValue(p.cpuPercent); case ColMemory: return QVariant::fromValue((qulonglong)p.privateBytes); default: return data(idx, Qt::DisplayRole); } } if (role Qt::TextAlignmentRole) { return (idx.column() ColName) ? Qt::AlignLeft : Qt::AlignRight; } return {}; } void refresh(const QVectorProcInfo procs) { beginResetModel(); m_procs procs; endResetModel(); } private: QVectorProcInfo m_procs; };核心思路是让DisplayRole只管画文本把“CPU的double值”“内存的字节数”这些原始量交给Qt::UserRole存一份。原因在于如果你用DisplayRole里“1.5 %”这个字符串去做数值排序结果是按字典序排的9.0%会排在10.0%后面。QSortFilterProxyModel默认只读DisplayRole因此需要让它比较UserRole。3.2 QSortFilterProxyModel接管排序列头点击即排高仿任务管理器里点任意列头应能立刻按该列升/降序切换且PID、内存、CPU各自独立记录排序方向。QTableView自带的setSortingEnabled(True)配合setSortIndicator就能做到。QTableView *table new QTableView(this); QSortFilterProxyModel *proxy new QSortFilterProxyModel(this); ProcessTableModel *model new ProcessTableModel(this); proxy-setSourceModel(model); proxy-setSortRole(Qt::UserRole); proxy-setDynamicSortFilter(false); table-setModel(proxy); table-setSortingEnabled(true); table-sortByColumn(ProcessTableModel::ColCpu, Qt::DescendingOrder);参数说明里需要交代两个非默认值。setSortRole(Qt::UserRole)是整个排序正确性的关键少掉这行CPU列还是会按字符串排。setDynamicSortFilter(false)则让排序只在refresh完成时做一次避免刷新过程中持续触发重排因为任务管理器“始终在最前”时每秒钟都可能来一次新快照动态排序会拖慢刷新主线程。此外sortByColumn写在setSortingEnabled之后静态指定首次进入页面的排列方式。3.3 后台采集线程与主线程之间只传快照界面和采集不能同线程。常见做法是开一个std::thread跑采集循环每轮把ProcInfo的QVector封装成Qt信号传给model。这里需要处理好“上一轮的CPU快照在采集线程内部保存不快照不传界面”这一件事不然获取进程数据的同时触发界面刷新可能读到半新半旧的数据。采集线程的骨架大致是void CollectLoop(std::atomicbool stop, std::functionvoid(QVectorProcInfo) emitFn) { CpuSnapshot prev{sampleSysTimes(), sampleProcTimes()}; while (!stop.load()) { std::this_thread::sleep_for(std::chrono::seconds(1)); CpuSnapshot now{sampleSysTimes(), sampleProcTimes()}; auto procs EnumerateProcesses(); for (auto p : procs) { p.cpuPercent CalcCpuUsage(prevMap[p.pid], nowMap[p.pid]); } QVectorProcInfo batch; // 把计算结果拷进batch立即发出 emitFn(batch); prev now; } }这里有一个我用下来比较顺的取舍每个进程的prev快照用一个unordered_map按pid做一次映射避免给每一个进程都存整个进程列表的历史。同时这条采样循环全部用普通STL容器不碰任何Qt对象保证线程安全是最容易的——跨线程的入口只有一个emitFn由信号在Qt主线程的槽里消费。最后注意一句sleep_for只能保证“至少1秒”不代表“恰好1秒”系统时钟回拨时CPU差值的计算结果会异常稳妥做法是除了定时唤醒之外再加一个单调时钟校准。4. 高仿Win10任务管理器的三个交互结束进程、CPU曲线、详情面板数据模型和刷新链路完成后功能层面已经能用了但高仿还差三处结束进程时的确认与权限处理性能页的折线图以及双击进程后的详情面板。这三处正是区分“能显示进程列表”和“像任务管理器”的分界线。4.1 结束进程的完整流程从提权到退出码判断强行杀掉进程在UI上只有两行但背后的权限处理不能省。常见路径是OpenProcess(PROCESS_TERMINATE, FALSE, pid)然后TerminateProcess最后检查GetLastError的值来判断失败原因。bool KillProcess(DWORD pid, DWORD* outError) { HANDLE h OpenProcess(PROCESS_TERMINATE, FALSE, pid); if (!h) { if (outError) *outError GetLastError(); // ERROR_ACCESS_DENIED(5) 多半是系统进程或保护进程 if (outError *outError ERROR_ACCESS_DENIED) return ReOpenWithDebugPrivilege(pid, outError); return false; } BOOL ok TerminateProcess(h, 1u); // 退出码1 *outError ok ? ERROR_SUCCESS : GetLastError(); CloseHandle(h); return ok TRUE; }这里需要处理一个绕不开的问题注入任何进程前都要求SE_DEBUG_NAME特权。系统进程、以其它用户身份运行的进程都在普通权限下OpenProcess会返回拒绝。常见做法是用LookupPrivilegeValueW与AdjustTokenPrivileges对当前令牌启用SeDebugPrivilege启用后对大多数保护进程仍然返回拒绝但至少能让以管理员身份运行时覆盖到大部分普通进程。任务管理器本身也是这么做的它在需要时弹出“已使用管理员凭据重启任务管理器”的UAC提示。仿写时如果不想做UAC重提权的完整闭环可以在结束进程失败且错误码是ERROR_ACCESS_DENIED时给用户展示“需要以管理员身份重新运行本程序”这也是业界普通工具最常见的处理方式。4.2 CPU曲线至少保留60个采样点两条曲线一个坐标轴Win10性能页的CPU曲线其实是一条“历史曲线当前值”数据本身很简单每次刷新把系统总CPU采样点插入一个定长环形队列然后用QPainter画即可不必引入QCharts那样重量级的依赖项。画一个60点、可隐藏网格和填充色的曲线核心逻辑如下。void CpuMonitorWidget::addSample(double usage) { m_samples.push_back(usage); while (m_samples.size() kMaxSamples) m_samples.pop_front(); // 保持窗口最多60个点 update(); // 触发paintEvent重绘 } void CpuMonitorWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); const int w width(), h height(); const int margin 8; double stepX (double)(w - margin * 2) / (kMaxSamples - 1); QPolygonF poly; for (int i 0; i m_samples.size(); i) { double x margin i * stepX; double y (100.0 - m_samples[i]) / 100.0 * (h - margin * 2) margin; poly QPointF(x, y); } p.drawPolyline(poly); }参数说明是重点。kMaxSamples用60是为了和任务管理器默认的“1分钟窗口”匹配每秒一个采样点正好一分钟。y轴做了一次翻转(100 - usage)因为屏幕坐标越往下越大而CPU使用率越往上应该越大。这里把usage限幅在0100之间再画否则采集线程一旦计算偶发超过100或负值曲线会瞬间冲出窗口外。另外paintEvent里不访问m_samples以外的数据也不做任何取样操作保证采样循环与绘图互不阻塞。4.3 详情面板与双击页复用同一份内存数据不另开进程查询任务管理器的详情页和进程页在视觉上是两个标签页但底层都是同一份进程快照。高仿时最省力的做法不是让详情页再发起一次枚举而是把进程页刷新得到的ProcInfo做成一个“当前选中进程”信号详情面板只负责把这份结构体做格式化展示。这样CPU和内存曲线会自动跟随刷新循环更新不会出现详情页数据比进程页旧几秒的观感差异。文件路径和命令行这类重量级字段放到双击时才用OpenProcess QueryFullProcessImageNameW去查。这个延迟查询策略是任务管理器工作量和流畅度之间的常见平衡点。5. 数据自耗、DPI适配与发布三个让仿写版更像定制品的检查点到了最后功能差不多齐了但这个项目最容易在交付时被打回的几个点上翻车这里逐个给检查手段和方法。5.1 采样自耗任务管理器不能把自己显示成0%任务管理器自己是会显示在进程列表里的而且它的CPU和内存也应该真实反映。仿写时常见的错误是把自己进程的CPU算成0或者在列表里把自己过滤掉。正确处理是自己的进程也要参与枚举和CPU计算。只需要在EnumerateProcesses返回后用GetCurrentProcessId()标识自己并在状态列显示一个“自”标记。更微妙的是采样循环如果和GUI刷新同处一个线程任务管理器自己的CPU会看起来偏高或抖动剧烈因为每一帧都在做枚举与排序。所以上一章说的采集线程独立是必须的它同时也是保证CPU自耗统计稳定的前提。用性能分析器验证一下在MSVC下按AltF2打开Performance Profiler观察CPU Usage采样把任务管理器置于后台1分钟你自己的进程总CPU时间不应持续超过2%。超过的话优先检查枚举循环里有没有在每次刷新时对每个进程调用CreateToolhelp32Snapshot(TH32CS_SNAPMODULE)来做模块遍历这是最常见的指标爆炸点任务管理器并不需要模块列表。5.2 高DPI下的表格行高与曲线缩放Windows 10默认会为Qt应用做缩放但Qt在部分系统手动设置了150%缩放时会出现表格行高被裁切的问题。高DPI适配的关键是不要在paintEvent里使用硬编码像素值。字体方面用QFontMetrics来动态计算行高fontMetrics().height() 6。表格设置QTableView::verticalHeader()-setDefaultSectionSize(fm.height() 10)之后150%缩放下各行依然能完整显示。曲线图部分上一节的paintEvent如果拿的是width()和height()它在缩放后会自动适配因为所有坐标都是相对值这个是最省心的一种写法如果你前期写死为“curveWidth800”那换到高DPI屏幕后曲线会被裁掉一截这是仿写任务管理器在真实用户电脑上排名第一的观感问题。5.3 发布检查清单依赖项与杀软误报Qt程序发布最稳定的方式还是windeployqt它会把运行所需的Qt库、平台插件和样式表一起拷到exe同目录。windeployqt --release --no-translations TaskManager.exe发布前我一般会做这几个检查在干净的Windows 10虚拟机里不安装任何Qt环境双击exe能启动说明windeployqt带的dll齐全。用Dependencies.exe打开exe动态加载的Qt6Core.dll确认没有依赖Visual C运行时以外的第三方库。检查生成的目录里platforms/qwindows.dll是否存在缺它会出现“应用无法正常启动0xc000007b”的启动报错。进程名与文件描述尽量保持简洁俗套例如“TaskManager.exe”描述可以写“System Performance Monitor”。对于杀软误报能把发行版体积压缩到10MB内release版开strip能显著降低被启发式判毒的概率Qt自带的QT_NO_DEBUG_OUTPUT宏在release构建里也要手动加上否则部分杀软会把大量输出的exe判定为可疑行为。5.4 资源泄漏自检句柄数必须收敛任务管理器最忌讳句柄泄漏。一个简单的验证办法是写个压力脚本让它自动运行600次枚举与刷新循环然后在Process Explorer里观察这个进程的Handle Count是否稳定在某个常数。这里给一个可复现的压力验证片段在main.cpp里临时增加验证完删掉即可。int StressTestLoop() { for (int i 0; i 600; i) { auto procs EnumerateProcesses(); for (auto p : procs) { SIZE_T w, priv; GetProcessMemory(p.pid, w, priv); } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } return 0; }这个循环结束后到任务管理器的“详细信息”里看句柄数如果600次后仍低于150说明CloseHandle都闭干净了如果看到每次循环增加12个句柄且不复原优先检查OpenProcess后每一个if提前return的路径是否都配了CloseHandle。特别留意上面GetProcessMemory的写法我见过很多仿写的实现把CloseHandle只放在成功分支里错误分支直接return false这就是句柄每秒涨一个的经典现场。这几个检查点做完整个高仿项目就可以正常拿去演示或者提交了。数据的准确性、界面的流畅度和发布环境的干净程度往往比代码本身更能决定这个项目给人留下的印象。本文还有配套的精品资源点击获取
返回列表