简介:一份基于 C++ 与 Qt 框架的高仿 Windows 10 任务管理器源码项目,适合已有 C++ 基础、想学习 Qt GUI 与系统进程管理的开发者。项目包含进程列表、CPU/内存实时曲线、界面主题还原等核心模块,用到 QProcess 获取系统信息、QTableWidget 展示列表、QTimer 定时刷新、QChart 绘制图表,并通过 QThread 将耗时操作移出主线程,避免界面卡顿;同时利用 Qt Designer 与 QSS 实现类似 Win10 的现代界面布局。压缩包内共 11 个文件,以 3 个 cpp 源文件与 2 个 h 头文件为主,搭配 1 个 ui 界面文件和 1 个 pro 工程文件,另有 3 张 png 效果图可直接预览界面,整体约 100KB,结构轻量。已有 165 人学习,非常适合作 Qt 进阶练手项目,读者可对照源码理解 Win10 任务管理器常见的进程操作、性能监控与多线程刷新场景,并在此基础上扩展更多自定义功能。
1. 用 Qt 重写一个 Win10 任务管理器,值不值得
任务管理器这种系统工具,看起来不过是「一个表格加几个数字」,真正动手写才会发现进程枚举、CPU 采样、权限处理、界面刷新每一层都有各自的坑。这个名为 OSExperiment 的 Qt 工程,就是一个把 Win10 任务管理器从外观到核心行为整体复刻一遍的完整示例,源码里包含 processinfo.h/processinfo.cpp、mainwindow.ui、main.cpp 和工程文件,目录结构是标准的 Qt Creator 布局。它适合两类人:一类是操作系统课设或 Qt 课设需要「进程管理 + 性能监控」完整闭环的同学,另一类是已经在用 C++ 写业务、但没碰过系统级信息采集的开发者。下面按数据层、界面层、监控层、部署层逐层拆解,第四章的多线程刷新和最后一章的部署排错,对写过几年 Qt 的人也值得看一眼。
2. 进程数据层:processinfo 结构体与 tasklist 枚举
2.1 数据建模:一个进程快照需要哪些字段
打开 processinfo.h 能看到这个工程把进程抽象成一个普通的结构体而不是类,这是很务实的做法。任务管理器界面上每一行要展示的字段就那么几个:进程名、PID、CPU 占用、内存占用,必要时再加所属用户。把这些字段直接定义成struct ProcessInfo,用QList<ProcessInfo>承载一次快照,后续无论是填 QTableWidget 还是传给图表组件都很方便。
#ifndef PROCESSINFO_H #define PROCESSINFO_H #include <QString> struct ProcessInfo { qint64 pid; // 进程 ID,qint64 兼容 64 位系统 QString name; // 进程可执行文件名,如 chrome.exe QString userName; // 归属用户,tasklist /V 才有,默认留空 double cpuUsage; // 本次刷新的 CPU 占用率 qint64 memoryBytes; // 工作集内存,单位字节 ProcessInfo() : pid(0), cpuUsage(0.0), memoryBytes(0) {} }; #endif // PROCESSINFO_H字段类型上有两个细节。PID 用qint64而不是 int,虽然 Windows 上 PID 不会超过 int 范围,但 Qt 的槽函数信号参数用 qint64 更不容易在跨线程传递时截断。内存用字节而不是 KB,是因为展示层需要根据数值大小决定显示成 MB 还是 GB,原始数据保留最小单位,格式化交给 UI 层做。
2.2 用 QProcess 调 tasklist 拿进程表
进程枚举是任务管理器的基础能力。这个工程没有直接调用 Windows API,而是用 QProcess 拉起系统自带的 tasklist 命令,再解析它的 CSR 输出。这样做的好处是纯 Qt 代码就能跑通,不依赖 MSVC 的 Windows SDK 头文件,跨平台移植时把命令换成 ps 就行。
#include <QProcess> #include "processinfo.h" QList<ProcessInfo> fetchProcessList() { QList<ProcessInfo> list; QProcess p; p.start("tasklist", {"/FO", "CSV", "/NH"}); if (!p.waitForFinished(4000)) { p.kill(); // 超时兜底,避免界面卡死 return list; } const QString out = QString::fromLocal8Bit(p.readAllStandardOutput()); const QStringList lines = out.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { ProcessInfo info = parseTasklistLine(line); if (info.pid > 0) { list.append(info); } } return list; }/FO CSV把输出强制成带引号的 CSV 格式,/NH去掉第一行表头,这两参数组合是所有解析方案里最稳定的。waitForFinished一定带超时参数,tasklist 在极端情况下可能因为系统句柄耗尽而挂起,不带超时会把 UI 线程一起拖死。输出用fromLocal8Bit是因为 tasklist 在中文系统下返回的是当前代码页编码,直接fromUtf8会造成乱码。
2.3 解析 CSV 与内存字符串
tasklist 的 CSV 输出形如"chrome.exe","12345","Console","1","120,456 K",真正需要抠出来的只有第 0 列进程名、第 1 列 PID 和第 4 列内存。这里有个省事的解析技巧:字段之间是","分隔,直接用split("\",\"")切分,再把首尾残留的引号去掉。
ProcessInfo parseTasklistLine(const QString &line) { ProcessInfo info; QStringList cols = line.split("\",\""); if (cols.size() < 5) { return info; // 空行或异常行直接跳过 } info.name = cols.at(0).remove('"'); QString pidStr = cols.at(1).remove('"'); info.pid = pidStr.toLongLong(); QString memStr = cols.at(4); memStr.remove('"').remove(' ').remove(',').remove("K"); info.memoryBytes = memStr.toLongLong() * 1024; return info; }内存字段的坑在千分位逗号。tasklist 对超过 999 KB 的数值会输出成"120,456 K",直接toLongLong()会因为逗号解析出 0。remove(',')之后再转数字,最后乘以 1024 得到字节数。这样解析出的快照和任务管理器「内存」列对比,误差能控制在一个刷新周期内。
2.4 为什么不直接调 Windows API
很多有经验的开发者第一反应是用CreateToolhelp32Snapshot或EnumProcesses拿进程列表,性能比拉起外部进程高一个数量级。但工程里用 tasklist 是有取舍的,对这个规模的项目来说可读性优先。
| 方案 | 依赖 | 性能 | 进程 CPU 字段 | 适用场景 |
|---|---|---|---|---|
| QProcess + tasklist | 无 | 慢,约 200~400ms | 无 | 课程实验、跨平台原型 |
| QProcess + wmic | 无 | 中等,约 400ms | 有 | 需要 CPU 占比且不想引 SDK |
| CreateToolhelp32Snapshot | tlhelp32.h | 快,<50ms | 无 | 只要进程名和 PID |
| PDH 计数器 | pdh.h | 快 | 有 | 生产级监控工具 |
限制 QProcess 方案的决定性因素不是速度,而是字段缺失:tasklist 不提供进程级 CPU 占用率。所以工程里 CPU 数据要单独走 wmic 或性能计数器,下一章会展开。对初学者,先用 tasklist 把进程列表跑通,后续再替换成 SDK API,是更平滑的演进路径。
3. 主窗口与表格展示:mainwindow.ui 的组装逻辑
3.1 从 .ui 到 mainwindow.cpp:布局的默认状态
mainwindow.ui 是这个工程的界面骨架,用 Qt Designer 拖出来的布局从上到下依次是菜单栏、居中的 QTableWidget 和状态栏。编译时 uic 工具会把 .ui 文件生成ui_mainwindow.h里的setupUi()函数,所有控件以ui->tableWidget、ui->statusBar这样的成员形式暴露出来。构造函数里的ui->setupUi(this)就是在执行这个初始化过程。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_timer(new QTimer(this)) { ui->setupUi(this); setWindowTitle(QStringLiteral("任务管理器")); setupTable(); connect(m_timer, &QTimer::timeout, this, &MainWindow::refreshOnce); m_timer->start(1000); }强调一点:不要在setupUi之前访问任何ui->成员,那时控件还没创建,访问是未定义行为。菜单栏里如果预留了「文件 → 结束任务」「查看 → 刷新」这样的 QAction,在构造函数里把它们的triggered信号连到对应槽函数即可,信号槽机制天然解决了菜单和快捷键的复用问题。
3.2 QTableWidget 列配置与刷新替换策略
表格是任务管理器的主体。QTableWidget 是按条目管理的表格控件,和 Model/View 框架的 QTableView 相比,胜在可以直接setItem塞数据,缺点是数据量大时性能差,几千行进程列表足够它卡顿。Win10 普通场景下进程数在 200~400 之间,QTableWidget 够用。
void MainWindow::setupTable() { QStringList headers; headers << QStringLiteral("名称") << QStringLiteral("PID") << QStringLiteral("CPU") << QStringLiteral("内存"); ui->tableWidget->setColumnCount(headers.size()); ui->tableWidget->setHorizontalHeaderLabels(headers); ui->tableWidget->verticalHeader()->setVisible(false); ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->tableWidget->setSortingEnabled(true); }setSelectionBehavior(SelectRows)让用户点任意单元格都选中整行,这是任务管理器交互的细节。NoEditTriggers禁止双击编辑单元格,否则用户误点会把进程名改成别的。setSortingEnabled(true)开启点击表头排序,但刷新时排序状态会和数据插入互相干扰。
刷新替换数据时,常见的错误是直接clearContents()后逐行填充并保持排序开启。这样每插入一行表格就重新排序一次,视觉上会出现行跳动的动画。正确做法是刷新前临时关闭排序,填充完再打开。
void MainWindow::refreshTable(const QList<ProcessInfo> &procs) { ui->tableWidget->setSortingEnabled(false); ui->tableWidget->setRowCount(0); // 一次清空所有行 for (const ProcessInfo &p : procs) { int row = ui->tableWidget->rowCount(); ui->tableWidget->insertRow(row); ui->tableWidget->setItem(row, 0, new QTableWidgetItem(p.name)); ui->tableWidget->setItem(row, 1, new QTableWidgetItem(QString::number(p.pid))); QString cpuText = QString::number(p.cpuUsage, 'f', 1) + QStringLiteral("%"); ui->tableWidget->setItem(row, 2, new QTableWidgetItem(cpuText)); QString memText = formatMemory(p.memoryBytes); ui->tableWidget->setItem(row, 3, new QTableWidgetItem(memText)); } ui->tableWidget->setSortingEnabled(true); }逐行insertRow加setItem的写法在几百行数据下效率尚可。如果进程数量上万(比如服务器场景),考虑换成 QTableView + 自定义 QAbstractTableModel,beginResetModel一次性刷新,能省掉大量控件创建开销。setRowCount(0) 比逐行 removeRow 更快,因为它走的是内部批量清理路径。
3.3 QSS 模拟 Win10 浅色外观
Win10 任务管理器最显眼的视觉特征是浅灰背景、白色表头、选中行淡蓝高亮。Qt Widgets 的默认样式和这个差距很大,需要用 QSS 调整。QSS 语法和 CSS 基本一致,加载方式是在 main.cpp 里读一个 .qss 文件,或直接在代码里setStyleSheet。
QTableWidget { background-color: #f3f3f3; alternate-background-color: #fafafa; gridline-color: #e0e0e0; selection-background-color: #cce4f7; selection-color: #000000; outline: 0; font-size: 13px; } QTableWidget::item { padding: 4px 6px; border: none; } QHeaderView::section { background-color: #ffffff; border: none; border-bottom: 1px solid #d0d0d0; padding: 6px 8px; font-weight: bold; }QTableWidget::item里border: none很关键,否则单元格之间会出现默认的虚线边框。alternate-background-color开启隔行变色,需要先在代码里setAlternatingRowColors(true)才生效。想要完全复刻 Win10 的亚克力毛玻璃效果,QSS 做不到,需要setAttribute(Qt::WA_TranslucentBackground)配合无边框窗口,但会引入拖动、阴影、缩放等一系列问题,课程实验做到扁平化配色和统一高亮色就足够了。
3.4 排序、选中与右键菜单
右键菜单是任务管理器杀手级交互,在 Qt 里实现方式很直接:把表格的contextMenuPolicy设为CustomContextMenu,连接customContextMenuRequested信号,在槽里根据鼠标位置拿到当前行。
connect(ui->tableWidget, &QTableWidget::customContextMenuRequested, this, &MainWindow::showContextMenu); void MainWindow::showContextMenu(const QPoint &pos) { QTableWidgetItem *item = ui->tableWidget->itemAt(pos); if (!item) { return; } int row = item->row(); qint64 pid = ui->tableWidget->item(row, 1)->text().toLongLong(); QString name = ui->tableWidget->item(row, 0)->text(); QMenu menu(this); QAction *endAct = menu.addAction( QStringLiteral("结束任务 - %1").arg(name)); connect(endAct, &QAction::triggered, this, [=]() { terminateProcess(pid); }); menu.exec(ui->tableWidget->viewport()->mapToGlobal(pos)); }itemAt(pos)返回空表示点在了表格空白区域,此时直接 return 不弹菜单。菜单弹出的坐标要用mapToGlobal从 viewport 坐标换算成屏幕全局坐标,直接用pos会导致菜单偏移。PID 从第 1 列的 item 取文本再转数字,比在 QTableWidgetItem 的 data 里塞自定义类型更简单直白,课程项目完全够用。
4. 实时性能监控:QTimer 采样、CPU 计算与 QThread 分工
4.1 刷新节拍:QTimer 的周期选择
性能监控的骨架是定时器。QTimer 到点发timeout信号,槽函数里做一次「采集 + 更新」的完整刷新。周期选择直接决定用户体验和 CPU 开销的平衡。
m_timer = new QTimer(this); m_timer->setTimerType(Qt::CoarseTimer); // 允许系统合并定时器事件 connect(m_timer, &QTimer::timeout, this, &MainWindow::refreshOnce); m_timer->start(1000);Qt::CoarseTimer是默认类型,它允许系统把定时器精度放宽到 5% 左右,换取更少的唤醒次数。对于任务管理器这种 1 秒一刷的监控场景,不需要毫秒级精度,开 CoarseTimer 能省电。把周期改成 500 毫秒会更贴近 Win10 默认任务管理器的流畅感,但代价是 QProcess 创建外部进程的频率翻倍。
4.2 用 wmic 性能类拿进程 CPU,两次采样算增量
tasklist 拿不到 CPU 占用,工程里常见做法是改用 wmic 的Win32_PerfFormattedData_PerfProc_Process类,它直接提供进程名的格式化 CPU 数值。这个方案的优点是纯命令行、字段全,缺点是慢,一次查询 300ms 起步。
double readCpuPercentForPid(qint64 pid) { QProcess p; p.start("wmic", {"/NODE", ".", "path", "Win32_PerfFormattedData_PerfProc_Process", "where", QString("IDProcess=%1").arg(pid), "get", "PercentProcessorTime"}); if (!p.waitForFinished(3000)) { return 0.0; } QStringList lines = QString::fromLocal8Bit(p.readAllStandardOutput()) .split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { bool ok = false; double v = line.trimmed().toDouble(&ok); if (ok) { return qMin(v, 100.0); // 多核下数值可能超过 100 } } return 0.0; }注意PercentProcessorTime在物理多核机器上会超过 100%,8 核满载时单进程可能显示 800%。显示前用qMin封顶到 100 或不对数值做处理,取决于你想要的语义。回调逐进程查 wmic 的性能是灾难级的,几百个进程要跑满几十秒,所以实际工程里要么用 PDH 计数器做采样差值,要么把PercentProcessorTime一次性全部取回再按 PID 建索引。
4.3 交互卡顿的根源:把刷新任务交给 QThread
如果把「枚举进程 + 查 CPU」放在主线程,每次刷新窗口会阻塞 200~800 毫秒,拖动窗口、点击菜单都会明显卡顿。解决思路是把采集逻辑丢到工作线程,主线程只负责接收结果更新表格。Qt 里最稳妥的模式是moveToThread加信号槽,而不是继承 QThread 重写 run()。
class MonitorWorker : public QObject { Q_OBJECT public: explicit MonitorWorker(QObject *parent = nullptr) : QObject(parent) {} public slots: void doRefresh() { QList<ProcessInfo> procs = fetchProcessList(); // 耗时的采集 // 按 PID 补齐 CPU 与内存字段 emit refreshDone(procs); } signals: void refreshDone(const QList<ProcessInfo> &procs); };MainWindow 里把 worker 对象 move 到 QThread,定时器的 timeout 信号直接连到 worker 的槽。
m_thread = new QThread(this); m_worker = new MonitorWorker; m_worker->moveToThread(m_thread); m_thread->start(); connect(m_timer, &QTimer::timeout, m_worker, &MonitorWorker::doRefresh); connect(m_worker, &MonitorWorker::refreshDone, this, &MainWindow::onRefreshDone);这里依赖 Qt 的队列连接机制:m_timer属于主线程,m_worker属于工作线程,跨线程的信号槽自动按事件队列投递,槽函数在工作线程执行,不会阻塞 UI。结果信号refreshDone回到主线程的onRefreshDone里更新表格。这个模式的隐藏前提是工作线程必须有事件循环,QThread::start默认开启,所以队列连接才能生效。
不要在doRefresh里直接操作任何控件,控件只能在主线程创建和访问。如果非要在工作线程里发结果,务必用值类型的 QList 而不是指针,避免悬垂引用。
4.4 采样周期与 UI 更新频率的数据关系
| 刷新周期 | 单次采集耗时 | 体验 | 适用场景 |
|---|---|---|---|
| 200ms | 200~400ms | 基本卡死 | 不能用于 QProcess 方案 |
| 500ms | 200~400ms | 接近 Win10 快速刷新 | 中高性能 PC |
| 1000ms | 200~400ms | 流畅 | 默认选择,课程实验推荐 |
| 2000ms | 200~400ms | 响应偏慢 | 低配机器、远程桌面 |
QProcess 方案里单次采集耗时和刷新周期没有直接关系,但周期越短,创建外部进程的频率越高。每秒拉起一次 tasklist 和 wmic,额外内存开销在 5~10MB 波动,这部分可以在任务管理器的内存列里看到自己的影子。改用 PDH 等本地 API 后,单次采集可降到 10ms 以内,刷新周期才有压缩空间。
5. 结束进程、权限处理与 windeployqt 部署
5.1 结束进程的两种路径
界面上的「结束任务」按钮,最常见的实现是拉起taskkill命令,和枚举时用 QProcess 是一致的思路。
void MainWindow::terminateProcess(qint64 pid) { QProcess killer; killer.start("taskkill", {"/PID", QString::number(pid), "/F", "/T"}); if (!killer.waitForFinished(2000)) { killer.kill(); } int exitCode = killer.exitCode(); QString errMsg = QString::fromLocal8Bit(killer.readAllStandardError()); if (exitCode == 0) { statusBar()->showMessage(QStringLiteral("进程 %1 已结束").arg(pid), 3000); } else { // 常见报错: 拒绝访问 / 没有此进程的权限 QMessageBox::warning(this, QStringLiteral("结束任务失败"), errMsg); } }/F强制结束,/T连带结束该进程的子进程树,不加/T的话某些带子进程的程序会结束不干净。这里只判断exitCode是不够的,taskkill 失败时会往 stderr 写「拒绝访问」这样的中文提示,必须读出来给用户看,这是调试时最容易忽略的一步。
5.2 拒绝访问与权限提级
Win10 里对csrss.exe、winlogon.exe这类受保护进程调用 taskkill,返回的一定是「拒绝访问」,即使用户是管理员也一样。处理方式是给程序加 manifest 提权,让进程启动时就申请管理员权限。
在 .pro 文件里加:
CONFIG += windeployqt win32 { QMAKE_MANIFEST += $$PWD/app.manifest }app.manifest 内容里最关键的一行是:
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />加了这行后,双击 exe 会触发 UAC 弹窗。如果不想要弹窗,可以用asInvoker保持普通权限,结束普通进程足够,遇到拒绝访问时再提示用户手动以管理员运行。QProcess::startDetached配合runas动词也能运行时提权,但会再次拉起一个完整 exe 实例,业务上不好控制,不如 manifest 干净。
5.3 windeployqt 部署与平台插件报错
开发机上能跑,拷到别的机器报错,这是 Qt 程序最经典的部署问题。Qt 默认动态链接,发布目录必须带上依赖的 DLL 和插件。用官方工具 windeployqt 一条命令解决:
cd /d D:\build\TaskManager\release "D:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" TaskManager.exewindeployqt 会扫描 exe 的导入表,自动拷贝 Qt5Core.dll、Qt5Widgets.dll 等运行库,以及 platforms\qwindows.dll 平台插件。如果程序里用了 QSS 文件和图片资源,记得手动把对应文件拷贝到发布目录的相应位置,windeployqt 不会帮你管 qrc 以外的资源。
发布后最常见的报错是:
qt.qpa.platform: could not load the Qt platform plugin "windows" in ""这个错误九成原因是 platforms 目录缺失或与编译套件版本不匹配。排查时把环境变量QT_DEBUG_PLUGINS设为1再运行程序,Qt 会打印插件加载细节,能直接看到它从哪里找 plugins 目录。另一类高频报错是缺MSVCP140.dll,这说明目标机器没有安装 Microsoft Visual C++ 运行库,发布时连同vc_redist.x64.exe一起带上,或在构建机装好对应版本的运行时再执行 windeployqt,它会把编译器运行库一并拷出。
本文还有配套的精品资源,点击获取