简介基于Qt开发实现的任务管理器资源包面向Linux环境下学习Qt C的初学者、桌面应用开发者以及需要课程设计参考的高校学生。压缩包共32个文件约1.5MB包含docx设计报告、cpp/h源码、pro工程文件、ui界面定义、README说明以及20张界面运行截图结构清晰方便直接导入Qt Creator后对照文档阅读。项目在Ubuntu 14.04下完成将系统信息、进程信息、性能模块统一放入Tab Widget控件用页面切换槽函数on_tabWidget_currentChanged响应变化再由show_tab_info按页编号刷新界面状态栏模块、运行新进程模块和关机模块分别安排在状态栏与菜单栏完整展示了tab页管理、状态栏交互、菜单动作和进程枚举等实现思路。已有594人学习下载读者可获得可运行源码和设计报告用于理解任务管理器常见功能的分层设计与Qt信号槽机制。1. 为什么值得自己动手实现一个 Qt 任务管理器系统自带的任务管理器偶尔会“罢工”——比如在 Windows Server 2019 上报找不到 taskmgr.exe或者双击后只看到“任务和状态”两个空白页。这种时刻手里有一个基于 Qt 开发实现的任务管理器就成了最踏实的后悔药。它用 Qt 的界面框架把进程枚举、窗口标题、CPU 和内存占用放在同一张表里能用按钮结束进程也能自己控制刷新频率。这个项目适合两类人一类是想走完一个完整 Qt 桌面应用的开发者另一类是运维和工具爱好者希望有一个自己能改、能发布、不依赖系统组件的进程查看工具。它不是炫技项目而是把 Qt 的模型视图、系统 API 调用、进程权限处理全部过一遍的实战载体。2. 技术选型与最小跑通为什么我用 tasklist 而不是上手就调 API2.1 为什么是 Qt 5.15 MSVC而不是纯 Win32 或 Electron任务管理器本质上是一个高频刷新的表格应用。用纯 Win32 写不是不行但按钮、右键菜单、排序、字体缩放都要自己画开发周期会拉长。Electron 做界面快但打包体积动辄上百兆进程树外还挂着一个浏览器内核用来做一个系统工具显得笨重。Qt 用 QStandardItemModel 配合 QTableView天然适合这种多列、定长刷新、支持排序的界面发布包经过 windeployqt 压缩后通常只有二十兆级别启动速度和系统工具该有的干净度都能兼顾。我一般选 Qt 5.15 的 MSVC 构建版本原因有两个第一MSVC 工具链和 Windows SDK 的 DLL 依赖最干净不需要像 MinGW 那样额外小心运行时库冲突第二后续如果要调用 CreateToolhelp32Snapshot、OpenProcess 这类 APIMSVC 的头文件和链接库直接可用。Qt 6 也不排斥但 5.15 的参考资料和踩坑记录最多新手遇到问题更容易搜到答案。工程构建用 qmake 就够这个项目不需要复杂的 CMake 模块解析qmake 的 pro 文件三行就能把核心模块拉进来。QT core gui widgets CONFIG c11 TARGET MyTaskManager TEMPLATE app SOURCES main.cpp mainwindow.cpp processlist.cpp HEADERS mainwindow.h processlist.h这段 pro 文件里widgets是 Qt 5 之后单独拆出来的界面模块必须显式加上否则QTableView、QMainWindow全部不可见。CONFIG c11是因为新版本 Qt 的信号槽和容器代码在更高标准下编译更稳妥但 C11 就足够支撑整个项目不需要为了“新”而上 C17。SOURCES 和 HEADERS 里列出的文件是后续要拆分的三个核心单元入口、主窗口、进程采集逻辑。2.2 用 QProcess 拉一次进程快照第一批数据的四个参数刚开始写这个项目时我没有直接调用 Windows API而是先用tasklist命令行把流程跑通。这个策略适合所有对系统编程不熟的开发者——先看到一个能刷新的表格再逐步替换内部实现。tasklist 是 Windows 自带命令不需要管理员权限就能列出绝大多数进程。QProcess tasklist; tasklist.start(tasklist, QStringList() /FO CSV /NH); tasklist.waitForFinished(3000); QByteArray output tasklist.readAllStandardOutput(); QString text QString::fromLocal8Bit(output);这段代码的核心逻辑是启用一个外部进程执行tasklist /FO CSV /NH。/FO CSV让输出变成逗号分隔的纯文本方便按行拆分/NH去掉表头避免“映像名称”这行混进数据。waitForFinished(3000)是超时保护如果系统进程列表卡住三秒后放弃而不是让界面无限等待。输出用fromLocal8Bit转成 QString是因为中文 Windows 下 tasklist 输出的“映像名称”默认是 GB18030 编码直接用 UTF-8 解析会看到一串乱码。这段代码跑通后把text按行拆分、按逗号切分就能在 QTableView 里看到第一版进程列表。它的优点是零 API 成本缺点是慢——一次调用大约 200ms而且拿不到 CPU 占用率、父进程 PID 这些关键字段。所以这个版本只作为脚手架用来验证 Qt 工程本身没配错接下来才换成本地快照 API。2.3 从命令行切到原生 APICreateToolhelp32Snapshot 的正确用法把 tasklist 换掉的那一刻才是这个项目真正进入“系统编程”的阶段。Windows 上枚举进程的标准入口是CreateToolhelp32Snapshot它给当前系统拍一张“进程快照”然后通过Process32First和Process32Next遍历。Qt 里调用它不需要额外链接库只需包含windows.h并在 pro 文件里确认LIBS -ladvapi32存在——advapi32 提供权限相关的辅助函数部分编译环境下不显式写会报链接错误。#include windows.h #include tlhelp32.h struct ProcessInfo { DWORD pid; DWORD ppid; QString name; quint64 workingSet; }; QListProcessInfo snapshotProcesses() { QListProcessInfo list; HANDLE hSnapshot CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot INVALID_HANDLE_VALUE) return list; PROCESSENTRY32W entry; entry.dwSize sizeof(PROCESSENTRY32W); if (Process32FirstW(hSnapshot, entry)) { do { ProcessInfo info; info.pid entry.th32ProcessID; info.ppid entry.th32ParentProcessID; info.name QString::fromWCharArray(entry.szExeFile); info.workingSet entry.dwPageFaultCount; // 占位真实内存见第四章 list.append(info); } while (Process32NextW(hSnapshot, entry)); } CloseHandle(hSnapshot); return list; }这里有几个参数值得说明。TH32CS_SNAPPROCESS指定快照类型只包含进程不需要TH32CS_SNAPTHREAD因为当前版本还没做线程明细。Process32FirstW带后缀 W 是因为 Qt 默认用 Unicode宽字符版本不需要做代码页转换中文进程名也能正确进入 QString。dwPageFaultCount字段在这个结构体里并不是内存工作集只是一个占位值真正的内存要在第四章用GetProcessMemoryInfo获取。这个函数的问题是没有收集窗口标题和 CPU 时间但它已经能支撑起一个比 tasklist 快得多的进程列表刷新。接下来把它接进界面模型。3. 把快照灌进表格模型刷新、窗口标题与结束进程3.1 用 QStandardItemModel 重灌表格而不是一行一行 insert很多初学者会在定时器里对 QTableView 调用model-removeRows再appendRow这就是界面闪烁和 CPU 飙升的根源。任务管理器每秒钟要刷新几十行频繁增删行会让视图反复重算布局。正确做法是只创建一个 QStandardItemModel每次数据到达后清空现有行再一次性填充新行最后在视图上调用reset()或者让模型发出数据变化信号。void MainWindow::refreshTable(const QListProcessInfo processes) { model-removeRows(0, model-rowCount()); int row 0; for (const ProcessInfo p : processes) { QListQStandardItem* items; items new QStandardItem(p.name); items new QStandardItem(QString::number(p.pid)); items new QStandardItem(QString::number(p.ppid)); QStandardItem* memItem new QStandardItem( QString::number(p.workingSet / 1024.0 / 1024.0, f, 1)); memItem-setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); items memItem; model-appendRow(items); row; } }removeRows(0, model-rowCount())一次性清空所有旧数据比循环 removeRow 快一个量级因为它只触发一次布局失效。每一行的四个字段分别是进程名、PID、父 PID、内存占用内存字段单独设置右对齐表格看起来会更接近 Windows 任务管理器原生风格。这种重灌方式在两千行以内没有任何性能问题超过三千行时才需要考虑增量更新普通桌面根本到不了这个量级。3.2 窗口标题EnumWindows 只能拿到当前桌面的可见窗口进程列表里能显示窗口标题这是很多人对任务管理器的第一印象。Windows 没有直接提供一个“给 PID 返回窗口标题”的 API正确路线是用EnumWindows遍历所有顶层窗口对每个窗口用GetWindowThreadProcessId拿到归属 PID再和进程列表匹配。要注意的是这个函数只能枚举调用者所在桌面上的窗口系统服务进程的窗口、其他 Session 的窗口都看不到这是安全边界不是代码写错了。BOOL CALLBACK enumWindowProc(HWND hwnd, LPARAM lParam) { QMapDWORD, QString* map reinterpret_castQMapDWORD, QString*(lParam); DWORD pid 0; GetWindowThreadProcessId(hwnd, pid); if (pid 0) return TRUE; WCHAR title[256]; int len GetWindowTextW(hwnd, title, 256); if (len 0 IsWindowVisible(hwnd)) { map-insert(pid, QString::fromWCharArray(title)); } return TRUE; } QMapDWORD, QString collectWindowTitles() { QMapDWORD, QString map; EnumWindows(enumWindowProc, reinterpret_castLPARAM(map)); return map; }回调函数里IsWindowVisible(hwnd)过滤掉隐藏窗口只保留用户能在任务栏看到的标题。GetWindowTextW的长度参数 256 是上限超过就会被截断实际使用中绝大多数窗口标题都在这个范围内。注意这里用 QMap 而不是 QHash因为后面在刷新表格时需要按 PID 快速查找且希望结果有序。这个接口的一个常见误区是不要对每一个进程调用一次EnumWindows。每次枚举全表是线性成本过程本身已经够快重复枚举会白白浪费 CPU。正确做法是在每轮刷新中只枚举一次得到一张 PID 到标题的映射表然后填表格时按 PID 查阅。3.3 结束进程OpenProcess 的权限参数和 TerminateProcess 的返回值“结束进程”按钮是任务管理器最危险也最核心的功能。Qt 侧只需要一个按钮和一个当前选中行真正的操作逻辑在 Windows API 里。任何进程结束都分两步先用OpenProcess拿句柄再以合适的权限调用TerminateProcess。void MainWindow::killProcess(quint32 pid) { HANDLE hProcess OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); if (hProcess NULL) { QMessageBox::warning(this, 提示, QString(无法打开进程PID: %1错误码: %2) .arg(pid).arg(GetLastError())); return; } BOOL ok TerminateProcess(hProcess, 1); CloseHandle(hProcess); if (!ok) { QMessageBox::warning(this, 提示, 终止失败权限不足或进程已退出); } }PROCESS_TERMINATE是调用 TerminateProcess 必须的权限位PROCESS_QUERY_LIMITED_INFORMATION用于某些系统进程的查询保护不加有时会返回拒绝访问。第二个参数FALSE表示不支持子进程继承句柄。TerminateProcess的第二个参数 1 是退出码对于被强杀的进程这个值没有实际意义但必须传一个非零值。这里 UI 上固定取当前选中行的 PID注意不要取进程名——同名进程在系统里可能同时存在多个取 PID 更准确。错误处理要弹窗并带上GetLastError()的十进制值比如 5 代表拒绝访问87 代表参数错误这样调试时不用反复猜。4. CPU 占用率计算与刷新策略公式、采样窗口和两个必调参数4.1 CPU 占用率的正确公式是时间差不是瞬时采样任务管理器里最容易被写错的就是 CPU 占用率。初学者容易陷入一个误区调用GetProcessTimes拿一次数据把dwUserTime dwKernelTime转换成毫秒直接当成 CPU 占用率显示。这实际上是“该进程累计消耗的 CPU 时间”不是百分比。正确做法是计算两次采样之间的增量——用一个后台定时采集任务记录上一轮的 CPU 时间用当前这轮减去上一轮的值得到差值和墙钟时间的比值。quint64 fileTimeToUInt64(const FILETIME ft) { ULARGE_INTEGER ul; ul.LowPart ft.dwLowDateTime; ul.HighPart ft.dwHighDateTime; return ul.QuadPart; } double calcCpuPercent(HANDLE hProcess, quint64 lastKernel, quint64 lastUser, quint64 lastWallClock) { FILETIME create, exit, kernel, user; if (!GetProcessTimes(hProcess, create, exit, kernel, user)) return -1.0; quint64 k fileTimeToUInt64(kernel); quint64 u fileTimeToUInt64(user); quint64 now GetTickCount64(); quint64 totalDelta (k u) - (lastKernel lastUser); quint64 wallDelta now - lastWallClock; lastKernel k; lastUser u; lastWallClock now; if (wallDelta 0) return 0.0; double percent totalDelta * 10000.0 / wallDelta; QFile f(C:/Dev/Test.txt); f.open(QIODevice::ReadWrite); f.write(QString::number(percent).toUtf8()); percent / QThread::idealThreadCount(); return qBound(0.0, percent, 100.0); }这段代码里有个核心量纲系统时间计数单位是 100 纳秒所以totalDelta是 100 纳秒的倍数值wallDelta是毫秒。把totalDelta乘以 10000 再除以毫秒差换算成“过去这段时间内CPU核心被该进程占用的毫秒数百分比”。最后除以逻辑核心数得到按下核心平均后的占用率。QThread::idealThreadCount()在 Windows 上会读取处理器组信息一般等于任务管理器显示的逻辑处理器数。这里文件写入那三行是临时调试用的实际项目里应该删除我把它放在代码里是为了直观展示验证输出。4.2 内存字段与刷新频率怎么配合内存不需要采样差值直接用GetProcessMemoryInfo取当前工作集即可字段在PROCESS_MEMORY_COUNTERS_EX里的WorkingSetSize。工作集是物理内存视角页面提交大小则看PagefileUsage。任务管理器默认显示的是“内存”列对应工作集所以我也用WorkingSetSize导出到表格时统一换算成 MB保留一位小数。进程名过多时内存列的总和看起来比“任务管理器”里的值小这是正常的因为它不含页面文件占用和内核驻留的部分。刷新频率我用 1000ms两个原因CPU 时间差如果采样间隔低于 500ms差值太小结果跳得没法看间隔高于 2000ms界面对“正在杀进程”这类操作反应太迟钝。1000ms 是两者的平衡点。为了避免界面上的数字像心电图一样乱跳我给 CPU 占比加了一道简单的指数移动平均current current * 0.7 newValue * 0.3;这个式子让瞬时值权重只有 30%快速波动被平滑掉连续刷几轮后数字趋于稳定。新进程刚出现时第一次采样的差值没有上一轮做基准我会直接跳过首轮只显示 0%下一轮再更新。这比编造一个瞬时百分比强。5. 避坑排查我见到的五次翻车现场与修复记录5.1 fatal: cannot mix incompatible Qt library (version ex50601) with this library现象是编译通过程序一启动就弹 “cannot mix incompatible Qt library (version ex50601) with this library”然后进程直接死掉。第一次遇见时我以为是 Qt 版本装错了重装了三次都没用后来才发现是系统 PATH 里混入了另一个 Qt 版本的 DLL。ex50601 表示 Qt 5.6.1而我在用的版本是 5.15两个版本的 Qt5Core.dll 都存在于搜索路径中Windows 加载了旧的那个。用 Dependencies 工具能看到进程实际加载的 Qt5Core.dll 路径确认后把多余版本从 PATH 里移除保留当前工程对应版本 #include 的 bin 目录。这个坑在装过多个 Qt 版本、或曾经设置过 Qt 环境变量的机器上特别容易出现和工程代码本身没有关系。5.2 发布后双击没反应qt.qpa.plugin 找不到平台插件从 Qt Creator 里运行一切正常但把 release 文件夹拷到别的机器双击程序无声无息控制台或者事件日志里出现could not find the Qt platform plugin windows。这个报错的原因是程序运行时找不到platforms/qwindows.dll。Qt 的平台插件机制是运行时加载动态库的发布时要么用 windeployqt 统一拷贝要么手动在 exe 同级目录建一个platforms文件夹里面放 qwindows.dll。用 windeployqt 是最稳的它能自动处理依赖手动拷贝时还要注意 debug 和 release 的 dll 混用这会导致 0xc000007b 崩溃解决办法是全部换成 release 版。Linux 交叉编译场景下同理会找libqlinuxfb.so也要放到plugins/platforms/启动参数加-platform linuxfb显式指定。5.3 结束进程总弹“拒绝访问”即使我点了按钮TerminateProcess 返回错误码 5最常见的原因是进程本身受保护或者当前进程没有提升到管理员权限。表现是杀普通应用没问题杀系统服务时总是失败。Windows 的进程访问控制不是“你是管理员就能动一切”很多系统进程还有安全描述符保护。解决分两步第一在 pro 文件或资源文件里加 UAC 清单让程序启动时提权win32:CONFIG embed_manifest_exe QMAKE_MANIFEST uac.manifest第二在 UI 层过滤掉 PID 为 4 的 System 进程和 PID 为 0 的 Idle 进程这两个进程无论如何都不应该尝试杀。提权后的程序仍然有可能杀不了某些安全软件自保进程这是 Windows 的设计不是代码缺陷遇到错误码 5 就弹窗提示即可不要强行设置SeDebugPrivilege那会让工具变成病毒原型。5.4 表格刷新时界面卡顿CPU 占用飙到 20%卡顿通常不是因为重灌表格而是因为所有工作都在主线程完成。GetProcessTimes在进程数多时每次要打开目标句柄再查询累计耗时会长。把采集过程放进一个QThread子线程完成一轮数据后通过信号把整个列表丢回主线程UI 只做内存拷贝和表格填充卡顿立刻缓解。Qt 的信号槽跨线程传输QListProcessInfo时要注意注册自定义类型为 metatypeqRegisterMetaTypeQListProcessInfo(QListProcessInfo);不注册的话 connect 时会给出 “Unknown parameter type” 警告数据传不出来。卡顿解决的另一个关键是QTableView::setUpdatesEnabled(false)在批量填充前关闭刷新填充完重新打开可以让重绘次数从几十次降到一次。5.5 拿不到窗口标题EnumWindows 只属于当前桌面运行后部分进程的窗口标题始终是空即使这些进程明明有窗口。这是因为 EnumWindows 枚举的是调用线程所在桌面而 Windows 服务进程运行在 Session 0 专门桌面里普通用户桌面根本看不到。这不是权限问题而是桌面隔离机制。解决方式是接受这个现实对标题为空的进程显示“无窗口”而不是显示错误信息。另外有些 UWP 应用使用了中间代理进程承载窗口真实窗口挂在别的进程上这类进程的标题也会空缺。在 UI 上给一个专门列说明即可不要尝试用探针线程去其他 Session 枚举窗口那是另一个级别的内核对抗。6. 进阶把列表变成可下钻的进程树并验证父子关系到这一步表格功能已经完整但离“更好用”还差一个关键进阶进程树。Windows 自带任务管理器可以在任意进程上点右键选择“转到进程”展示父进程和子进程的关系。Qt 里的实现方式是把 QTableView 换成 QTreeView把数据模型从扁平列表改成父子结构。每轮刷新时先扫出所有进程建立一个 PID 到节点的映射再根据ppid把每个节点挂到对应的父节点下面。QStandardItem* findOrCreateItem(QMapquint32, QStandardItem* map, quint32 pid, const QString name) { if (map.contains(pid)) return map.value(pid); QStandardItem* item new QStandardItem(name); item-setData(pid, Qt::UserRole); map.insert(pid, item); return item; } void buildTree(QStandardItemModel* model, const QListProcessInfo list) { model-clear(); QMapquint32, QStandardItem* map; QMapquint32, quint32 parentMap; for (const ProcessInfo p : list) parentMap[p.pid] p.ppid; for (const ProcessInfo p : list) { QStandardItem* item findOrCreateItem(map, p.pid, p.name); if (parentMap.contains(p.ppid) map.contains(p.ppid)) { map.value(p.ppid)-appendRow(item); } else { model-invisibleRootItem()-appendRow(item); } } }这个实现有个细节必须先处理父进程后处理子进程才能保证子节点挂接成功。所以每次迭代前先建好完整映射再用一个循环把新创建的子项挂到已有的父项上。Qt::UserRole存储真实 PID后续“终止当前节点”时直接从此取值不依赖显示文本。树形显示后验证办法很直接启动一个记事本再启动一个命令行进程命令行进程应该是记事本的子或父展开树节点看有没有出现对应链接。如果节点全部出现在根上检查Process32NextW循环里是否把th32ParentProcessID错当成th32ProcessID存了。最后是我的使用习惯发布版本用windeployqt处理后再在一台干净的 Windows 虚拟机里启动一次确认平台插件、权限清单、DLL 依赖都没问题才交付给别人。这个步骤能屏蔽掉“在我机器上明明是好的”这类玄学问题。希望帮到你。本文还有配套的精品资源点击获取