☰
基于Qt开发实现的任务管理器:进程枚举、性能曲线与避坑指南
2026/9/26 8:18:30 网站建设 项目流程

简介:这是一份基于Qt开发的任务管理器项目资料,面向正在学习Qt GUI编程、系统进程管理或课程设计的学生与开发者。资源内含设计报告Word文档和完整项目源码,源码在Ubuntu 14.04环境下使用Qt Creator开发,实现了系统信息、进程信息、性能监控三大模块,并通过Tab Widget整合页面,利用信号槽机制响应页面切换并刷新对应内容;状态栏展示实时状态,菜单栏提供运行新进程与关机等操作,结构清晰,便于学习模块化界面设计与事件驱动编程。压缩包共32个文件,以C++源码(cpp/h)、Qt工程文件(pro/ui/qrc)、Word设计报告及20张运行截图PNG为主,整体仅1.5MB,轻量易解压。目前已有594人学习/下载,适合作为操作系统课程设计或Qt实战入门参考。

1. 基于Qt开发实现的任务管理器:与其等taskmgr恢复,不如自己写一份

Windows自带的任务管理器在绝大多数桌面上够用,但在两类场景里会让人很被动:一类是精简版Windows Server或办公终端上,taskmgr.exe被直接删掉或由组策略禁用,按Ctrl+Shift+Esc只会弹“找不到c:\windows\system32\taskmgr.exe”;另一类是生产环境里要按固定节奏记录某个进程的CPU峰值与内存水位,原生任务管理器没法自动导出。基于Qt开发实现的任务管理器,就是为这两类需求准备的:把进程枚举、性能采样、列表展示和进程操作完整串成一个桌面工具,源码包解压后可以按自己的字段和节奏去改。这篇笔记把它的架构、关键实现和发布坑一次讲完,适合刚学完Qt基础、想拿一个完整项目练手的读者,也适合要快速交付内部运维小工具的一线工程师。

2. 任务管理器的Qt架构选型:模块拆分、进程枚举与刷新策略

拿到这类源码包我习惯先看工程结构,而不是先点编译。Qt工程的坑大多出在“采集逻辑和UI逻辑缠在一起”:定时器里直接操作QTreeView,刷新一快界面就抖;QProcess在界面线程里waitForFinished,点一下就白屏。把这个项目拆成数据采集、数据模型、界面三层,后面的问题会少一半。

2.1 界面层与数据层解耦:MainWindow、ProcessListModel、SysMonitor 怎么分工

常见做法是三个类各管一段:

类职责关键成员
MainWindow窗口框架、表格视图、右键菜单、托盘QTreeView、QSystemTrayIcon
ProcessListModel把进程数据暴露给表格,负责行列数、排序QList 、QVariant data()
SysMonitor定时采集进程快照、CPU/内存水位QTimer、QProcess、QElapsedTimer

进程信息本身用一个轻量结构体承载:

struct ProcessInfo { qint64 pid = 0; QString name; // 映像名,exe文件名,不含路径 QString sessionName; // 会话名,Console / Services 等 quint64 memoryBytes = 0; // 内存占用,单位是字节,表格里再转成KB/MB double cpuPercent = 0.0; // 最近一个采样周期的CPU占用率 };

为什么要单独定义ProcessInfo而不是直接塞QStandardItem:列表数据要反复比较,按PID做增量更新时需要一个可哈希的对象;而且ProcessInfo将来要落日志、按条件过滤,从QStandardItem里再抠回来反而别扭。SysMonitor拿到快照后通过信号发给MainWindow,MainWindow再交给ProcessListModel,数据流是单向的,谁改坏了谁负责。

2.2 进程列表怎么来:先用 tasklist 把全流程跑通

最快能跑通的方式不是直接调Windows API,而是用QProcess执行系统的tasklist命令。它开箱即用,兼容Win7到Server 2019,适合第一版验证解析流程和刷新节奏:

bool SysMonitor::fetchProcessListViaTasklist(QList<ProcessInfo> &out) { QProcess proc; proc.start("tasklist", QStringList() << "/fo" << "csv" << "/nh"); if (!proc.waitForFinished(5000)) { proc.kill(); return false; } const QByteArray raw = proc.readAllStandardOutput(); QString text = QString::fromLocal8Bit(raw); // 关键:按系统ANSI代码页解析 const QStringList lines = text.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { QString clean = line.trimmed(); if (clean.isEmpty()) continue; // 示例行: "chrome.exe","1234","Console","1","12,345 K" QStringList fields = clean.split("\",\""); if (fields.size() < 5) continue; ProcessInfo info; info.name = fields.at(0).remove('"').trimmed(); info.pid = fields.at(1).remove('"').trimmed().toLongLong(); info.sessionName = fields.at(2).remove('"').trimmed(); QString mem = fields.at(4).remove('"').trimmed(); mem.remove(','); // 千分位逗号必须去掉 info.memoryBytes = mem.toULongLong(); // 单位是K out.append(info); } return !out.isEmpty(); }

这段代码的逻辑不复杂,但有三个参数值得说。waitForFinished(5000)是管道等待超时,系统进程多时tasklist可能要跑几秒,超时后kill掉返回false,避免调用方永远等下去;QString::fromLocal8Bit是按当前系统ANSI代码页解码,简体中文环境是GBK,英文环境进程名本身是ASCII,问题不大,但如果你用UTF-8硬解,中文进程名会直接变成问号;toULongLong前把逗号清掉,是因为tasklist输出的内存字段带千分位分隔符,直接转数字会得到0。

tasklist方案的缺点也要说清楚:它会额外拉起一个进程,上千进程时每次枚举都是一次完整快照,开销比API方式明显;在某些被第三方工具改写过PATH的环境里,tasklist可能指向了同名但输出完全不同的程序。所以这个方案适合“先跑通”,正式工具建议用下一节的原生API。

2.3 换成 Toolhelp32 快照:进程名不再依赖代码页

在Windows平台上,更可靠的做法是用Toolhelp32系列API直接读进程快照。它返回宽字符,天然避开代码页和命令行文本解析的坑:

#ifdef Q_OS_WIN #define NOMINMAX // 防止windows.h里的min/max宏污染Qt代码 #include <windows.h> #include <tlhelp32.h> QList<ProcessInfo> SysMonitor::fetchProcessListSnapshot() { QList<ProcessInfo> list; HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap == INVALID_HANDLE_VALUE) { return list; } PROCESSENTRY32W pe; pe.dwSize = sizeof(pe); if (Process32FirstW(snap, &pe)) { do { ProcessInfo info; info.pid = static_cast<qint64>(pe.th32ProcessID); info.name = QString::fromWCharArray(pe.szExeFile); info.sessionName = QStringLiteral("Session %1").arg(pe.th32SessionID); list.append(info); } while (Process32NextW(snap, &pe)); } CloseHandle(snap); return list; } #endif

CreateToolhelp32Snapshot返回一个句柄,Process32FirstW/Process32NextW是“先取第一条、循环取后续”的遍历模式,遍历完必须CloseHandle。这里用PROCESSENTRY32W而不是PROCESSENTRY32,是因为宽字符版本的szExeFile对中文路径无压力,QString::fromWCharArray直接转成QString。NOMINMAX那句很关键,windows.h里的min/max宏会让Qt源码里大量使用std::min/max的地方翻车,编译错误千奇百怪。

这块代码还有一个好处:sessionName来自th32SessionID数字,不依赖本地会话的编码和语言,部署到英文系统或繁体系统中也不会出现“会话名列乱码”。代价是拿不到tasklist输出里的“内存使用”,Toolhelp32快照结构本身没有内存工作集字段,后续性能曲线数据要从性能计数器补,这个在后文单独处理。

2.4 刷新节奏:QTimer多路节拍与增量更新

进程列表和性能曲线对刷新频率的要求不一样,一个定时器不够。我一般用两个QTimer,CPU/内存曲线1秒采样一次,进程列表2秒整表刷新一次:

monitorTimer_.setInterval(1000); // 性能曲线采样 listTimer_.setInterval(2000); // 进程列表刷新

CPU占用率本质是两次采样之间的CPU时间差除以墙钟时间差,采样间隔太短连数字都跳不出来;列表刷新太快则会让表格不断reset,用户正在按的右键菜单和选中行全丢。所以曲线1秒、列表2秒是比较均衡的组合,你要追踪某个诡异进程的瞬时波动时再临时把listTimer调到500ms。

刷新动作本身也建议做成增量模式。先拿到新旧两批进程的PID集合,用QSet做差集,只对新增和退出的进程发信号:

QSet<qint64> oldPids = currentPids_; QSet<qint64> newPids; for (const ProcessInfo &info : snapshot) { newPids.insert(info.pid); } emit processAdded(newPids - oldPids); emit processRemoved(oldPids - newPids);

QSet差集运算在几百个进程时开销可以忽略,但它让界面层不用动不动重排整个表格,选中行也保住了。这一步在进程数量大时收益明显,是让任务管理器“用起来不闪”的关键。

3. 核心功能逐块落地:进程列表、性能曲线与进程操作

架构定了之后,剩下的就是往三个方向填实现:列表能刷、曲线能画、进程能杀。这三个功能各有各的细节,按顺序逐个落地。

3.1 定时刷新流程:信号槽连接与增量更新的最小实现

SysMonitor负责采集,MainWindow只负责把结果交给Model。初始化时的连接方式如下:

void MainWindow::initRefresh() { connect(&monitor_, &SysMonitor::processSnapshotReady, this, &MainWindow::applySnapshot); listTimer_ = new QTimer(this); listTimer_->setInterval(2000); connect(listTimer_, &QTimer::timeout, this, [this]() { monitor_.requestProcessSnapshot(); }); listTimer_->start(); }

listTimer_的timeout到了之后只是发一个“请求采集”的信号,真正的采集动作发生在SysMonitor内部。这样设计是为了让采集可以随时换实现:今天用tasklist,明天改成Toolhelp32或WinAPI,MainWindow一行都不用改。QTimer的lambda连接也避免了自己写槽函数还要记得在析构里断开的老问题。

表格侧的最小刷新实现是整表reset,代码最少但代价最大:

void MainWindow::applySnapshot(const QList<ProcessInfo> &snapshot) { model_->beginResetModel(); model_->setSnapshot(snapshot); model_->endResetModel(); }

beginResetModel会清掉表格视图的全部状态,包括用户排好序的列、选中的行、折叠的树节点。进程数少的一个任务管理器够用,但进程列表动辄两三百行时,每次reset都会让视图抖动。改进方向是QStandardItemModel的setItem只更新变化的单元格,或者用QAbstractTableModel的子集接口配合rowsInserted/rowsRemoved。第一版先用reset,把流程走通,再换成增量模式,这是我反复用的做法。

3.2 用QPainter画CPU与内存历史曲线:环形缓冲区与自适应宽度

性能曲线部分不引入QCustomPlot这类第三方库,直接用QWidget的paintEvent画折线。环形缓冲区用QVector加pop_front实现,保持最近两分钟的采样:

class CpuCurve : public QWidget { public: void addSample(double percent) { if (samples_.size() >= maxPoints_) { samples_.pop_front(); // 去掉最老的点 } samples_.push_back(percent); update(); // 触发重绘 } protected: void paintEvent(QPaintEvent *) override { QPainter p(this); p.fillRect(rect(), Qt::white); // 横向网格:分成4格,便于估读百分比 p.setPen(QPen(QColor(0xE0, 0xE0, 0xE0), 1)); const int gridLines = 4; for (int i = 0; i <= gridLines; ++i) { int y = i * height() / gridLines; p.drawLine(0, y, width(), y); } if (samples_.size() < 2) return; // 按当前控件宽度把采样点映射到x坐标 QPolygonF points; const qreal stepX = width() / qreal(maxPoints_ - 1); for (int i = 0; i < samples_.size(); ++i) { qreal x = i * stepX; qreal y = height() - samples_.at(i) / 100.0 * height(); points.append(QPointF(x, y)); } p.drawPolyline(points); } private: static const int maxPoints_ = 120; // 120个点,1秒一个点即2分钟历史 QVector<double> samples_; };

addSample在收到监控数据时调用,然后把重绘交给Qt的事件循环。paintEvent里的逻辑全是纯绘制:先铺白底、画网格,再用drawPolyline把采样点连起来。这里有个取舍:没有开Antialiasing,因为性能曲线每秒重绘一次,开抗锯齿会让低配机掉帧;曲线出现明显锯齿时再按需打开也不迟。宽度用maxPoints_-1做除数,意思是曲线的右端点永远对应最新采样,窗口拉宽时曲线会被横向拉伸,这比固定像素宽度更符合“最近两分钟”的直觉。

内存曲线的画法一样,只是把y轴的100换成内存总量。内存总量推荐用GlobalMemoryStatusEx一次性拿到:

quint64 totalMemoryBytes() { #ifdef Q_OS_WIN MEMORYSTATUSEX status; status.dwLength = sizeof(status); GlobalMemoryStatusEx(&status); return static_cast<quint64>(status.ullTotalPhys); #endif return 0; }

MEMORYSTATUSEX比老的GlobalMemoryStatus多支持4G以上物理内存,这个结构体在Server 2019上没有任何兼容问题。采样时把进程内存总和除以总量,得到百分比,再走addSample。

3.3 进程操作落地:结束进程、提权重试与设置优先级

列表能看之后,下一步是让进程能杀。最常见的实现是调taskkill,和列表一样,先跑通:

bool killProcessById(qint64 pid) { if (pid <= 0) return false; QProcess killer; killer.start("taskkill", QStringList() << "/pid" << QString::number(pid) << "/f"); if (!killer.waitForFinished(3000)) { return false; } return killer.exitCode() == 0; }

taskkill最后一个/f是强制终止,不加这个参数时很多进程会拒绝结束。waitForFinished(3000)比列表解析的超时短,因为结束进程操作不能一直挂在那儿,用户点完菜单应该在几秒内得到成败反馈。如果返回false,最常见的失败原因是权限不足而不是进程本身顽固,这时候需要提权重试:

void killProcessWithElevation(qint64 pid) { QString args = QString("/pid %1 /f").arg(pid); QProcess::startDetached("powershell", QStringList() << "-Command" << "Start-Process taskkill -ArgumentList '" + args + "' -Verb RunAs"); }

这句命令会触发UAC弹窗,属于预期行为。在Windows Server上如果当前账号没有管理员组成员资格,UAC会直接拒绝,提示联系管理员,这个也正常。优先级设置走的wmic,快速实现如下:

QProcess::startDetached("wmic", QStringList() << "process" << "where" << QString("processid=%1").arg(pid) << "call" << "setpriority" << priorityValue);

wmic的优先级档位是整数,64/128/256/32768/32771分别对应空闲/低于标准/标准/高于标准/高,具体以目标系统实测为准。这里提一句:wmic在Win11里已不再预装,新工具建议直接调OpenProcess加SetPriorityClass。这里给wmic版本是因为它和taskkill一样是命令行通路,调试时一眼能看出参数对错。

4. 避坑指南:Qt任务管理器从“能编译”到“能发布”的六个关键问题

这段写我在复现和改造这类项目时反复踩的坑,按“现象、原因、解决”列出来,能省很多网上翻帖子的时间。

4.1 中文字段乱码:tasklist解析时编码不对

现象:进程名列表里中文程序名全部变成问号,会话名也出现“锟斤拷”,但英文进程正常。

原因:tasklist输出的是系统ANSI代码页文本,在简体中文Windows上是GBK。QString::fromUtf8或默认的fromLatin1去解析,中文必然坏。

解决:用QString::fromLocal8Bit(raw)读取原始字节,保证代码页匹配。如果项目要放到多个语言版本Windows上跑,就别用tasklist方案,直接切到Toolhelp32宽字符版本,从根上消灭编码问题。我在最终版本里把tasklist方案做成调试开关,默认走Toolhelp32。

4.2 界面卡成白屏:waitForFinished 阻塞了GUI线程

现象:点击刷新后窗口变成“未响应”,过几秒才恢复;在进程特别多的机器上,几乎每次刷新都卡。

原因:QProcess的waitForFinished在界面线程中进入事件循环等待,期间Qt的绘制和输入事件无法正常分发。tasklist在几百个进程时本身要跑一两秒,叠加网络驱动进程,卡顿就会被放大。

解决:把QProcess改成异步信号槽套路,主线程不阻塞:

proc_ = new QProcess(this); connect(proc_, &QProcess::readyReadStandardOutput, this, [this]() { rawOutput_.append(proc_->readAllStandardOutput()); }); connect(proc_, &QProcess::finished, this, [this](int exitCode) { if (exitCode != 0) return; parseTasklistRaw(rawOutput_); rawOutput_.clear(); }); proc_->start("tasklist", QStringList() << "/fo" << "csv" << "/nh");

这种写法在子进程退出时才触发解析,主线程的事件循环始终畅通。更彻底的做法是把枚举函数丢到QtConcurrent::run线程池,拿到结果后用信号回主线程更新Model。对任务管理器这类常驻工具,我推荐后者,因为列表和曲线同时刷新时对GUI线程的压力是叠加的。

4.3 fatal: cannot mix incompatible qt library:运行环境里混入了多个Qt版本

现象:程序一启动就弹“fatal: cannot mix incompatible qt library (version ex50601) with this library”,连窗口都出不来。

原因:编译时用的Qt库和运行时加载的Qt库不是同一套,常见于开发机装了MinGW和MSVC两套Qt,或者PATH里残留了旧版本Qt的bin目录,导致exe启动时优先加载了错误版本的Qt5Core.dll。

解决:先删除build目录做一次干净重建;然后检查PATH环境变量,把当前编译套件的Qt bin目录提到最前面。怎么确认exe到底加载了哪个Qt5Core.dll:用Process Explorer看模块路径,或者dumpbin /dependents看导入表,把结果和qmake -v输出的版本对比。MSVC和MinGW的库不能互相换着用,这是死规矩。

4.4 结束进程被拒绝:UAC权限与SESSION 0隔离

现象:能杀掉普通应用进程,但杀服务进程或system进程时提示“拒绝访问”,有些进程杀完过几秒又自己起来了。

原因:没有管理员令牌时,OpenProcess想获取PROCESS_TERMINATE权限会被拒绝;Session 0隔离的进程连枚举都不完整,更别说结束。一些受保护进程即使有管理员权限也杀不掉,这是系统故意设计的。

解决:在pro文件里指向管理员manifest:

win32 { QMAKE_MANIFEST = $$PWD/app.manifest }
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="requireAdministrator" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>

但一直弹UAC会让工具变得难用,我的做法是“结束进程”先按普通权限试一次,失败后再提权重试;“设置优先级”这种操作几乎一定要管理员,直接提权。在启用AppLocker或由管理策略限制运行的环境里,Start-Process -Verb RunAs也可能被拦下,表现是没有任何弹窗,事件日志里多一条拒绝记录,这种情况只能换一个提权入口或用系统服务中转。

4.5 发布后报 qt.qpa.plugin: could not find the qt platform plugin “windows”

现象:用windeployqt打包后,在干净机器上双击报“qt.qpa.plugin: could not find the qt platform plugin “windows” in”,程序退出。

原因:platforms目录下的qwindows.dll没有跟exe放在一起,Qt找不到窗口平台插件。常见原因是打包时指定了错误的Qt路径,或者手工挑dll时漏掉了platforms子目录。

解决:重新用与编译相同套件的windeployqt跑一遍,输出目录里必须存在platforms/qwindows.dll。手动拷贝时还要注意plugins/imageformats下的qjpeg.dll、qsvg.dll这些格式插件,程序里如果用到了特定图片格式,缺了对应的插件,图标会静默消失而不是报错。经验表明,exe目录放一个qt.conf能让Qt少走弯路,内容很简单:

[Paths] Platforms=plugins/platforms

4.6 偶发崩溃 0xC0000005:悬空指针和过期PID

现象:程序运行几分钟后随机崩溃,事件查看器里是0xC0000005访问冲突,通常在右键菜单或列表滚动的瞬间。

原因:进程快照里记录的PID已经被系统回收复用,或者用户在列表点击后进程刚好退出,代码里拿着旧PID去查询或结束操作,指针悬空。QStandardItem的指针在endResetModel后也可能失效,继续访问会直接崩溃。

解决:所有对进程的引用都改成按PID访问,操作前先判断PID是否还存在于当前快照中;Model更新完以后不要再缓存QStandardItem指针,需要改值时用model->item(row, col)现取。抓崩溃现场时,把Qt的pdb文件留好,用WinDbg打开dmp文件,输入!analyze -v,能看到崩溃在哪个模块。很多0xC0000005其实发生在QStandardItemModel内部,原因就是我说的“Model重置后还拿着旧index不放”。这套“不长期持有行指针”的规则,是所有Qt表格类项目通用的保命技巧。

5. 从“能跑”到“能发布”:托盘、单实例与windeployqt打包

5.1 托盘、单实例与一分钟发布检查

功能和坑都填完了,剩下的是发布体验。任务管理器这类工具常驻后台比较顺手,几个收尾配置值得一并做掉。

首先是托盘。QSystemTrayIcon在Windows上原生可用,放个退出菜单和“显示主窗口”就够了:

trayIcon_ = new QSystemTrayIcon(QIcon(":/icons/app.ico"), this); QMenu *trayMenu = new QMenu(this); trayMenu->addAction(QStringLiteral("显示主窗口"), this, &MainWindow::showNormal); trayMenu->addAction(QStringLiteral("退出"), qApp, &QCoreApplication::quit); trayIcon_->setContextMenu(trayMenu); trayIcon_->show();

托盘图标一定要用.ico而不是png,否则任务栏通知区域在某些系统主题下会显示成空白方块。QSystemTrayIcon::show要在setContextMenu之后调用,顺序反了右键菜单可能弹不出来。

其次是单实例。任务管理器重复打开只会增加混乱,用QLockFile是最省事的写法,注意lock对象必须长期持有,不能写成局部变量:

lockFile_ = new QLockFile(QDir::temp().filePath("TaskManager.lock")); if (!lockFile_->tryLock(100)) { QTimer::singleShot(0, qApp, &QCoreApplication::quit); return; }

tryLock抢不到说明已经有一个实例在跑,直接退出。抢到锁的实例要一直持有这个QLockFile对象,一旦析构,锁文件被释放,第二个实例就能进来。

最后是发布。用Qt自带的windeployqt,命令是:

cd dist C:\Qt\5.15.2\mingw81_64\bin\windeployqt.exe TaskManager.exe

它会把缺的运行时库和插件自动铺好,关键产物如下表:

文件/目录作用缺失时的表现
platforms/qwindows.dllWindows平台插件启动即报qt.qpa.plugin错误
Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll基础运行库提示缺少xxx.dll
libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dllMinGW运行库提示缺少xxx.dll
styles/qmodernwindowsstyle.dll控件风格插件界面回到朴素字面风格

发布前的检查我固定在干净虚拟机里做:把整个dist目录拷过去,不装Qt、不配PATH,双击exe能出主窗口,再右键结束一个自己启动的记事本进程,这套动作通过才敢发给别人用。每次改完代码发布,我都会先想过一次这个流程,它帮我挡掉过不少次“在我电脑上明明是好的”的翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询