简介这是一份基于C#与WPF框架开发的轻量级桌面进程管理工具源码面向计算机、自动化等专业的本科生及初阶.NET开发者适用于课程设计、大作业或毕业设计参考帮助学习者掌握WPF界面构建、进程控制启动/停止/显示/隐藏/重启及系统API调用等核心实践能力。资源包共17个文件涵盖7个C#逻辑类如ProcessObject.cs、MainWindow.xaml.cs、2个XAML界面定义文件、1个解决方案文件.sln、1个项目配置文件.csproj、1个图标.ico及配置类资源.config、.settings、.resx等结构完整、模块职责清晰总大小仅25KB便于快速导入与调试。已有339人学习下载源码经严格测试可直接运行评审得分高达95分附带README说明文档适合作为WPF实战入门范例亦可作为二次开发基础——例如扩展进程树视图、资源监控或权限管理功能。1. 这不是任务管理器的复刻一个真正能嵌入产线工具链的 C# WPF 进程管理程序长什么样你有没有遇到过这种场景某高校实验室在部署视觉检测 Demo 时需要后台常驻一个图像采集服务进程但每次调试都得切到 Windows 任务管理器里手动查 PID、结束、重启——稍有不慎就漏掉子进程导致端口占用或内存泄漏某公司产线工控软件升级后旧版守护进程残留新旧版本冲突现场工程师只能靠远程桌面手点 20 分钟才理清依赖树。这不是小问题而是桌面端工业级应用落地时最真实的“最后一公里”卡点。这个标题里的「基于 C# 和 WPF 框架的桌面应用进程管理程序」核心价值从来不是炫技做 UI而是提供一套可嵌入、可扩展、可静默集成的进程生命周期控制能力支持按名称/路径/命令行模糊匹配、强制终止含子进程树、启动时注入环境变量、记录操作日志供审计回溯、甚至预留 IPC 接口供上层调度系统调用。它面向的是需要把进程管控能力下沉到具体业务模块的一线开发者不是想做个花哨外壳的初学者。如果你正被“怎么让我的 .NET 工具自动拉起/杀掉另一个 EXE 并确保干净退出”困扰这篇就是为你写的实战笔记。2. 从零搭起进程管理主干WPF 界面结构与核心数据模型设计2.1 主窗口布局为什么不用 ListView 而选 DataGrid 自定义列模板WPF 中展示进程列表最直观的控件是DataGrid而非ListView。原因很实际DataGrid原生支持列排序、列宽拖拽、多选、键盘导航CtrlA 全选、Shift方向键连续选而这些在产线工具中是刚需。更重要的是DataGrid的CellTemplate可以嵌入按钮控件如“终止”“刷新”且绑定逻辑清晰避免ListView中用ItemContainerStyle做复杂触发器的玄学写法。!-- MainWindow.xaml 片段 -- DataGrid x:NameProcessGrid AutoGenerateColumnsFalse CanUserSortColumnsTrue SelectionModeExtended SelectionUnitFullRow DataGrid.Columns DataGridTextColumn HeaderPID Binding{Binding Id} Width60/ DataGridTextColumn Header进程名 Binding{Binding ProcessName} Width120/ DataGridTextColumn HeaderCPU% Binding{Binding CpuUsage, StringFormat{}{0:F1}%, NotifyOnTargetUpdatedTrue} Width80/ DataGridTextColumn Header内存(MB) Binding{Binding WorkingSet64, Converter{StaticResource BytesToMBConverter}} Width100/ DataGridTextColumn Header启动时间 Binding{Binding StartTime, StringFormatyyyy-MM-dd HH:mm:ss} Width150/ DataGridTemplateColumn Header操作 Width120 DataGridTemplateColumn.CellTemplate DataTemplate StackPanel OrientationHorizontal HorizontalAlignmentCenter Button Content终止 ClickTerminateProcess_Click Margin2 Padding4,1/ Button Content详情 ClickShowDetails_Click Margin2 Padding4,1/ /StackPanel /DataTemplate /DataGridTemplateColumn.CellTemplate /DataGridTemplateColumn /DataGrid.Columns /DataGrid提示WorkingSet64是进程工作集内存单位字节需用IValueConverter转为 MB 显示。BytesToMBConverter必须实现Convert和ConvertBack后者可抛异常因只读显示。CpuUsage字段需后台定时采样计算不能直接取Process.TotalProcessorTime——那是累计值必须差分。2.2 进程数据模型为什么 Process 类不能直接绑定必须封装一层System.Diagnostics.Process是密封类sealed无法继承且其属性如Id,ProcessName是只读的不支持INotifyPropertyChanged。若直接将Process.GetProcesses()结果绑定到DataGrid.ItemsSourceUI 不会响应进程状态变化如被外部杀死后仍显示“运行中”。因此必须创建一个可绑定的包装类// ProcessItem.cs public class ProcessItem : INotifyPropertyChanged { private readonly Process _process; private double _cpuUsage; private long _workingSet64; private DateTime _startTime; public int Id _process.Id; public string ProcessName _process.ProcessName; public string MainModuleFileName _process.MainModule?.FileName ?? N/A; public string CommandLine GetCommandLine(_process); public double CpuUsage { get _cpuUsage; set { _cpuUsage value; OnPropertyChanged(); } } public long WorkingSet64 { get _workingSet64; set { _workingSet64 value; OnPropertyChanged(); } } public DateTime StartTime { get _startTime; set { _startTime value; OnPropertyChanged(); } } public ProcessItem(Process process) { _process process ?? throw new ArgumentNullException(nameof(process)); Refresh(); } public void Refresh() { try { if (!_process.HasExited) { _process.Refresh(); _workingSet64 _process.WorkingSet64; _startTime _process.StartTime; // CPU 使用率需两次采样差分此处仅设初始值由定时器更新 _cpuUsage 0; } } catch (InvalidOperationException) { /* 进程已退出忽略 */ } catch (Win32Exception ex) when (ex.NativeErrorCode 5) { /* 访问被拒绝跳过 */ } } private string GetCommandLine(Process p) { // Windows 10 可通过 QueryFullProcessImageName 获取完整路径命令行需 WMI 或 NtQueryInformationProcess // 此处简化返回主模块路径生产环境应调用 WMI Win32_Process.CommandLine return p.MainModule?.FileName ?? p.ProcessName; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }逻辑说明Refresh()方法是关键入口它封装了对原生Process对象的安全访问捕获InvalidOperationException进程已退出和Win32Exception错误码 5拒绝访问常见于系统进程。CpuUsage字段暂不计算因为 CPU 占用率必须基于两个时间点的TotalProcessorTime差值除以时间间隔单次Refresh()无法得出需配合后台定时器。GetCommandLine()是简化实现真实项目中必须通过 WMI 查询Win32_Process类的CommandLine属性否则无法获取带参数的完整启动命令MainModule.FileName只有路径。3. 核心能力落地进程启停、树状终止与资源监控的三重实现3.1 启动进程如何安全执行带参数的 EXE 并捕获标准输出用户常误以为Process.Start(notepad.exe, -x)就够了但实际产线需求远不止于此需要设置工作目录、注入环境变量、限制最大内存、超时自动杀掉、捕获 stdout/stderr 用于日志分析。ProcessStartInfo是唯一可控入口// ProcessManager.cs public bool TryStartProcess(string fileName, string arguments, string workingDirectory, Dictionarystring, string environmentVars null, int? timeoutMs null) { var startInfo new ProcessStartInfo { FileName fileName, Arguments arguments, WorkingDirectory workingDirectory, UseShellExecute false, // 关键设为 false 才能重定向 IO RedirectStandardOutput true, RedirectStandardError true, CreateNoWindow true }; // 注入环境变量仅对当前进程有效不影响系统 if (environmentVars ! null) { foreach (var kvp in environmentVars) { startInfo.EnvironmentVariables[kvp.Key] kvp.Value; } } try { var process Process.Start(startInfo); // 异步读取输出避免死锁父进程等待子进程输出子进程等待父进程读取 var outputTask process.StandardOutput.ReadToEndAsync(); var errorTask process.StandardError.ReadToEndAsync(); if (timeoutMs.HasValue) { if (!process.WaitForExit(timeoutMs.Value)) { process.Kill(); return false; // 超时退出 } } else { process.WaitForExit(); } var output outputTask.Result; var error errorTask.Result; // 记录日志此处可对接 Serilog/NLog Log($[{fileName}] exited with code {process.ExitCode}. Output: {output}, Error: {error}); return process.ExitCode 0; } catch (Win32Exception ex) when (ex.NativeErrorCode 2) { Log($Failed to start {fileName}: File not found.); return false; } catch (Exception ex) { Log($Exception starting {fileName}: {ex.Message}); return false; } }参数说明UseShellExecute false是硬性要求否则RedirectStandardOutput无效且无法获取ExitCode。CreateNoWindow true防止弹出黑框尤其在无界面服务中至关重要。异步读取StandardOutput/StandardError是血泪经验若子进程输出量大且未及时读取管道缓冲区满会导致子进程挂起deadlock表现为WaitForExit()永不返回。Win32Exception错误码 2 表示ERROR_FILE_NOT_FOUND比泛泛的FileNotFoundException更精准便于前端提示“路径错误”。3.2 终止进程树为什么 Kill() 不够必须递归查找子进程Windows 中Process.Kill()只杀目标进程其创建的子进程如cmd.exe /c start myapp.exe启动的子进程会变成孤儿进程继续运行导致端口占用、文件锁残留。真正的“干净终止”必须获取并终止整个进程树。方法是调用 Windows APINtQuerySystemInformation非托管或更稳定的 WMI 方式// ProcessTreeHelper.cs WMI 实现兼容性好 public static Listint GetChildProcessIds(int parentId) { var childPids new Listint(); try { using (var searcher new ManagementObjectSearcher( $SELECT ProcessId, ParentProcessId FROM Win32_Process WHERE ParentProcessId {parentId})) { foreach (ManagementObject mo in searcher.Get()) { if (mo[ProcessId] is uint pid) { childPids.Add((int)pid); // 递归获取孙子进程 childPids.AddRange(GetChildProcessIds((int)pid)); } } } } catch (ManagementException ex) { // WMI 不可用时降级为仅终止自身极少见 Log($WMI query failed for parent {parentId}: {ex.Message}); } return childPids; } public static void KillProcessTree(int rootPid) { var allPids new HashSetint { rootPid }; allPids.UnionWith(GetChildProcessIds(rootPid)); foreach (var pid in allPids) { try { var p Process.GetProcessById(pid); p.Kill(); p.WaitForExit(5000); // 等待优雅退出超时则强制 } catch (ArgumentException) { /* 进程已不存在 */ } catch (InvalidOperationException) { /* 进程已退出 */ } catch (Win32Exception ex) when (ex.NativeErrorCode 5) { /* 无权限跳过 */ } } }注意WMI 查询Win32_Process是微软官方推荐方式比解析tasklist /fo csv字符串更可靠。GetChildProcessIds递归调用需加深度限制如最多 5 层防止环形引用导致栈溢出虽罕见但某些恶意软件会伪造 ParentProcessId。3.3 实时 CPU/内存监控如何在不拖垮 UI 的前提下每秒更新直接在 UI 线程每秒调用Process.Refresh()会导致界面卡顿Refresh()是同步 I/O 操作。正确做法是后台线程定时采样计算差值后通过Dispatcher.Invoke更新 UI 绑定属性。// MainWindow.xaml.cs private DispatcherTimer _monitorTimer; private readonly Dictionaryint, ProcessItem _processItems new(); private void StartMonitoring() { _monitorTimer new DispatcherTimer { Interval TimeSpan.FromMilliseconds(1000) }; _monitorTimer.Tick OnMonitorTick; _monitorTimer.Start(); } private void OnMonitorTick(object sender, EventArgs e) { var now DateTime.Now; var processes Process.GetProcesses(); // 构建当前 PID 集合用于增量更新 var currentPids new HashSetint(processes.Select(p p.Id)); // 移除已退出进程 var toRemove _processItems.Keys.Where(pid !currentPids.Contains(pid)).ToList(); foreach (var pid in toRemove) _processItems.Remove(pid); // 更新或添加进程项 foreach (var p in processes) { if (_processItems.TryGetValue(p.Id, out var item)) { item.Refresh(); // 更新内存等静态值 UpdateCpuUsage(item, p); // 差分计算 CPU } else { var newItem new ProcessItem(p); _processItems[p.Id] newItem; } } // 绑定到 DataGrid使用 ObservableCollection 更佳此处简化 ProcessGrid.ItemsSource _processItems.Values.ToList(); } private void UpdateCpuUsage(ProcessItem item, Process p) { try { // 获取上次采样时间戳和 TotalProcessorTime var lastTime item.LastCpuSampleTime; var lastCpuTime item.LastCpuTime; var nowTime DateTime.Now; var nowCpuTime p.TotalProcessorTime; if (lastTime ! default nowTime lastTime) { var elapsedMs (nowTime - lastTime).TotalMilliseconds; var cpuMs (nowCpuTime - lastCpuTime).TotalMilliseconds; // CPU 使用率 (CPU 时间 / 经过时间) / 逻辑处理器数 var cpuPercent (cpuMs / elapsedMs) * 100.0 / Environment.ProcessorCount; item.CpuUsage Math.Max(0, Math.Min(100, cpuPercent)); // 限幅 } item.LastCpuSampleTime nowTime; item.LastCpuTime nowCpuTime; } catch { /* 忽略采样异常 */ } }逻辑说明DispatcherTimer在 UI 线程触发但Process.GetProcesses()和Refresh()本身是 I/O 密集型应尽量减少调用频次。此处每秒一次是平衡精度与性能的常见选择。UpdateCpuUsage中的Environment.ProcessorCount是逻辑核心数不是物理核心确保多线程 CPU 占用率计算准确。item.LastCpuSampleTime和item.LastCpuTime需作为字段加入ProcessItem类代码中省略否则无法差分。4. 避坑指南WPF 进程管理程序上线前必须跨过的 5 个深坑4.1 现象DataGrid 双击某行后再点击“终止”按钮实际终止的是另一行的进程原因DataGrid默认启用SelectionChanged事件但SelectedItem绑定未设置UpdateSourceTriggerPropertyChanged导致双击后SelectedItem未及时更新TerminateProcess_Click中取到的是旧选中项。解决在 XAML 中为DataGrid添加SelectionChanged事件处理或在TerminateProcess_Click中改用ProcessGrid.SelectedCells[0].Item获取当前单元格绑定对象而非依赖SelectedItem。4.2 现象调用KillProcessTree时程序崩溃报System.AccessViolationException原因WMI 查询中ManagementObject未显式Dispose()大量短生命周期对象堆积触发 GC 压力偶发非托管内存访问冲突。解决所有ManagementObjectSearcher和ManagementObject必须用using包裹或手动调用Dispose()。示例中已修正。4.3 现象在 Windows Server Core无桌面环境下启动失败报System.ComponentModel.Win32Exception: 拒绝访问原因Server Core 默认禁用 WMI 服务winmgmt且Process.GetProcesses()对系统进程权限要求更高。解决启动前检查ServiceController.GetServices().FirstOrDefault(s s.ServiceName winmgmt)?.Status若非Running则提示用户启用 WMI对GetProcesses()加try/catch降级处理仅显示用户有权访问的进程如过滤p.SessionId Process.GetCurrentProcess().SessionId。4.4 现象进程列表内存占用持续上涨运行 24 小时后达 1GB原因ProcessItem对象未实现IDisposable且Process对象持有非托管句柄Process.GetProcesses()返回的每个Process实例都需显式Dispose()。解决在ProcessItem中添加Dispose()方法内部调用_process?.Dispose()在OnMonitorTick中移除进程时显式调用item.Dispose()。4.5 现象中文路径进程如C:\工具\采集服务.exe无法被Process.Start()正确启动原因ProcessStartInfo.FileName对 Unicode 路径支持不一致尤其当路径含空格且未加引号时cmd.exe解析失败。解决对FileName执行Path.GetFullPath()并用双引号包裹startInfo.FileName $\{Path.GetFullPath(fileName)}\同时确保UseShellExecute true此时RedirectStandardOutput失效需权衡或改用ProcessStartInfo.Verb runas提升权限后UseShellExecute false。5. 进阶技巧把进程管理能力封装成可复用的 NuGet 包并对接企业级日志平台5.1 抽离核心逻辑创建独立类库 ProjectManager.Core将进程启停、树状终止、监控采样封装为无 UI 依赖的类库是工业级复用的前提。关键在于解耦ProcessItem与 WPF 绑定机制改用IObservableProcessSnapshot推送数据流// ProjectManager.Core/ProcessMonitor.cs public class ProcessMonitor : IDisposable { private readonly Timer _timer; private readonly SubjectProcessSnapshot _snapshotSubject new(); public IObservableProcessSnapshot Snapshots _snapshotSubject.AsObservable(); public ProcessMonitor(int intervalMs 1000) { _timer new Timer(_ CaptureSnapshot(), null, TimeSpan.Zero, TimeSpan.FromMilliseconds(intervalMs)); } private void CaptureSnapshot() { try { var processes Process.GetProcesses(); var snapshot new ProcessSnapshot { Timestamp DateTime.Now, Processes processes.Select(p new ProcessInfo { Id p.Id, Name p.ProcessName, CpuUsage CalculateCpuUsage(p), MemoryBytes p.WorkingSet64, StartTime p.StartTime, CommandLine GetCommandLine(p) }).ToArray() }; _snapshotSubject.OnNext(snapshot); } catch { /* 忽略瞬时异常 */ } } public void Dispose() { _timer?.Dispose(); _snapshotSubject?.OnCompleted(); } } public record ProcessSnapshot(DateTime Timestamp, ProcessInfo[] Processes); public record ProcessInfo(int Id, string Name, double CpuUsage, long MemoryBytes, DateTime StartTime, string CommandLine);提示System.ReactiveRx.NET是微软官方推荐的 .NET 响应式编程库IObservableT比事件更易组合如Snapshots.Where(s s.Processes.Any(p p.Name myapp)).Throttle(TimeSpan.FromSeconds(5))实现告警去抖。5.2 日志对接将进程操作写入 Elasticsearch 并配置 Kibana 告警产线工具必须满足审计要求。将TerminateProcessTree等敏感操作记录为结构化日志发送至 ELK 栈// LoggingHelper.cs public static void LogProcessOperation(string operation, int targetPid, string initiator, string reason null) { var logEvent new { timestamp DateTime.UtcNow.ToString(o), level INFO, component ProcessManager, operation, target_pid targetPid, initiator, reason, host Environment.MachineName, app_version Assembly.GetExecutingAssembly().GetName().Version.ToString() }; // 使用 NLog NLog.Targets.ElasticSearch 发送 LogManager.GetCurrentClassLogger().Info(JsonConvert.SerializeObject(logEvent)); } // 调用点TerminateProcess_Click 中 LogProcessOperation(terminate_tree, selectedPid, Operator-John, Upgrade v2.1);表格ELK 告警规则建议Kibana 中配置告警名称触发条件通知方式说明高频进程终止operation: terminate_tree且 5 分钟内 10 次企业微信邮件可能存在脚本误操作或攻击行为关键进程意外退出target_pid在白名单中如 vision_service且operation: auto_terminate电话告警产线停机风险需立即响应内存泄漏迹象MemoryBytes连续 10 次采样增长 50MB钉钉群机器人提示开发团队检查内存管理逻辑5.3 最后一条血泪经验永远不要信任“进程已退出”的返回值我曾在某跨平台系统中踩过最深的坑调用Process.Kill()后立即p.WaitForExit(100)返回true就认为进程已干净退出。结果在高负载服务器上WaitForExit返回true时进程的句柄尚未完全释放Process.GetProcessesByName(myapp)仍能查到该 PID导致后续启动失败。真正的安全做法是Kill()→WaitForExit(timeout)→try { Process.GetProcessById(pid) } catch { /* 不存在即成功 */ }循环验证最多重试 3 次每次间隔 100ms。这多出的 300ms 换来的是产线零事故——希望帮到你。本文还有配套的精品资源点击获取