ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#+WPF进程管理源码实战:从枚举到增量刷新

C#+WPF进程管理源码实战:从枚举到增量刷新 简介这是一份基于C#与WPF框架开发的桌面应用进程管理程序源码面向计算机、自动化等相关专业的学生及开发者可用于课程设计、大作业或毕业设计参考也适合希望学习WPF桌面开发与进程操作技术的入门者。压缩包共17个文件约25KB以cs源码文件为主配合xaml界面文件、sln解决方案、csproj项目文件及配置文件等结构完整可直接在Visual Studio中打开运行。程序核心功能包括对可执行文件的启动、停止、显示、隐藏与重启操作通过ProcessObject等类封装进程管理逻辑界面与业务代码分离清晰便于理解WPF的数据绑定与事件处理机制。该资源为个人毕设项目评审分达95分经过严格调试确保可运行已有338人学习下载。读者可借此掌握进程管理类桌面应用的完整实现思路并在此基础上修改调整扩展出类似功能。1. 从一份 C#WPF 进程管理源码说起桌面端到底需要什么样的进程工具任务管理器已经足够好用为什么还有人愿意花时间用 C# 和 WPF 重写一个进程管理程序这个问题我在第一次拿到类似源码时也问过自己。真实场景往往不是「替代任务管理器」而是把它嵌进自己的桌面端应用里上位机需要监控采集进程有没有假死安装器需要在升级前确认目标进程已经退出企业内网工具需要按白名单限制员工机器上不该出现的程序。这些需求用现成工具都做不干净只能自己写一个带 UI 的进程管理模块而 C# 加 WPF 恰好是 Windows 桌面端最顺手的组合。这份源码的价值不在于界面多花哨而在于它把「枚举进程、读取信息、结束进程、刷新列表」这条链路用 WPF 的数据绑定串了起来能直接当模板改。适合两类人一是刚学完 C# 基础、想找一个有真实系统调用的练手项目的开发者二是手上有个上位机或运维工具需要快速补一块进程监控面板的工程师。下面我按「先讲清原理和选型再落到能跑的代码最后说坑」的顺序拆开讲。2. 进程管理模块的技术选型为什么是 C# 加 WPF 而不是 WinForm2.1 Process 类能拿到什么拿不到什么.NET 里操作进程的核心是System.Diagnostics.Process。它能给你进程 Id、进程名、是否响应、启动时间、占用内存、主窗口标题、可执行文件路径这些足够撑起一个管理面板。但它拿不到的东西同样要提前知道CPU 占用率不是直接属性得靠PerformanceCounter或者两次采样算差值进程的完整命令行参数在 .NET Core 之后才通过Process.GetProcesses拿不到需要 WMI 或NtQueryInformationProcess这类更底层的手段至于进程所属用户、父进程 Id同样要绕道 WMI。所以选型的第一层判断是你要做的是「轻量监控面板」还是「类任务管理器」。前者用Process类加定时刷新就够了后者必须引入System.Management查 WMI。这份源码属于前者它的定位是嵌入到别的应用里做辅助面板不是做一个全能工具。想清楚这一点后面很多取舍就顺了。2.2 WPF 的数据绑定为什么比 WinForm 更适合进程列表进程列表的本质是「一个会高频变化的数据集合」。WinForm 里你通常得手动清空ListView再逐条Add刷新频繁时界面闪烁、选中项丢失是家常便饭。WPF 的ObservableCollectionT配合DataGrid或ListView集合变化会自动通知 UI配合INotifyPropertyChanged连单个进程的内存数值变化都能自动更新不用碰任何控件。这就是选 WPF 的核心理由进程管理天然是数据驱动的场景而 WPF 的绑定机制就是为这种场景设计的。代价是学习曲线DataTemplate、IValueConverter、Dispatcher这些概念对新手不友好。但一旦跨过去后面加排序、筛选、分组都是改 XAML 的事不用重写逻辑。如果你只是想要一个能跑的窗口WinForm 更快如果你预期这个面板会持续迭代WPF 的长期成本更低。2.3 项目结构怎么拆才不返工我一般把这类项目拆成三层Models放进程数据模型Services放所有Process和 WMI 调用ViewModels放绑定逻辑和刷新调度。UI 层只认 ViewModel绝不直接在MainWindow.xaml.cs里写Process.GetProcesses()。这样拆的好处是将来要把进程数据源从本地换成远程 Agent 上报只改 Service 层UI 一行不动。// Models/ProcessInfo.cs public class ProcessInfo : INotifyPropertyChanged { public int Id { get; set; } public string Name { get; set; } private long _memoryMb; public long MemoryMb { get _memoryMb; set { _memoryMb value; OnPropertyChanged(nameof(MemoryMb)); } } public bool IsResponding { get; set; } public DateTime? StartTime { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }这段模型类实现了INotifyPropertyChanged关键在MemoryMb这个属性。进程内存是刷新时变化最频繁的字段把它做成带通知的属性UI 上绑定的那一列就会自动更新不需要重建整个列表。Id、Name这类不常变的字段用普通属性即可减少通知开销。注意StartTime用可空类型因为部分系统进程拿不到启动时间直接访问会抛异常。3. 用 Process 类把进程列表跑起来枚举、刷新与结束的最小实现3.1 枚举进程并转成可绑定集合第一步是把系统里的进程读出来转成上一步定义的ProcessInfo集合。这里有个容易忽略的点Process.GetProcesses()返回的Process对象是IDisposable用完不释放会累积句柄长时间运行的工具迟早出问题。// Services/ProcessService.cs public ListProcessInfo GetAllProcesses() { var result new ListProcessInfo(); var processes Process.GetProcesses(); foreach (var p in processes) { try { result.Add(new ProcessInfo { Id p.Id, Name p.ProcessName, MemoryMb p.WorkingSet64 / 1024 / 1024, IsResponding p.Responding, StartTime TryGetStartTime(p) }); } catch (Exception) { // 系统进程或已退出进程会抛异常跳过即可 } finally { p.Dispose(); } } return result; } private DateTime? TryGetStartTime(Process p) { try { return p.StartTime; } catch { return null; } }逻辑上分三步遍历、逐个取值、释放。try-catch不是偷懒是必须的——系统进程和刚退出的进程访问属性会抛Win32Exception或InvalidOperationException不捕获整个刷新就崩了。WorkingSet64是物理内存工作集除以两次 1024 得到 MB。TryGetStartTime单独抽出来是因为StartTime抛异常的概率比其他属性高隔离处理更清晰。参数上没什么可调的但要注意GetProcesses()不带参数是取本机全部进程如果只想取有主窗口的可以加p.MainWindowHandle ! IntPtr.Zero过滤。3.2 定时刷新与 UI 线程调度进程列表要定时刷新但刷新不能阻塞 UI 线程也不能在后台线程直接改ObservableCollection否则会抛「集合被另一个线程修改」的异常。正确做法是用DispatcherTimer它的 Tick 回调本身就在 UI 线程上。// ViewModels/MainViewModel.cs private readonly DispatcherTimer _timer; private readonly ProcessService _service new ProcessService(); public ObservableCollectionProcessInfo Processes { get; } new(); public MainViewModel() { _timer new DispatcherTimer { Interval TimeSpan.FromSeconds(2) }; _timer.Tick (s, e) Refresh(); _timer.Start(); } private void Refresh() { var latest _service.GetAllProcesses(); // 简单策略整体替换适合进程数不多的场景 Processes.Clear(); foreach (var item in latest) Processes.Add(item); }DispatcherTimer的Interval设成 2 秒是个经验值。设成 500 毫秒进程多的时候枚举本身就要几百毫秒UI 会卡设成 10 秒用户点了结束进程后要等很久列表才更新体验差。2 秒是流畅度和开销的平衡点。这里的刷新策略是整体清空重建实现简单但有个副作用用户选中的行会丢失。进程数在 200 以内时这个策略够用超过之后建议改成按 Id 做增量更新只更新变化的字段避免Clear引起的界面闪烁。3.3 结束进程与权限边界结束进程看起来就一行p.Kill()但实际会翻车的地方不少。权限不够、进程受保护、进程已经退出三种情况抛的异常都不一样。public bool KillProcess(int pid, out string message) { try { var p Process.GetProcessById(pid); p.Kill(); p.WaitForExit(3000); // 最多等 3 秒 message 已结束; return true; } catch (ArgumentException) { message 进程已不存在; return false; } catch (Win32Exception ex) { message $权限不足或被系统保护{ex.Message}; return false; } }GetProcessById在进程已退出时抛ArgumentException这是最常见的「点了结束但进程早没了」的情况。Kill()抛Win32Exception基本就是权限问题普通用户结束系统进程或别的用户会话下的进程都会撞上。WaitForExit(3000)给一个超时避免某些进程卡住导致 UI 假死。如果你的工具需要结束高权限进程唯一正路是让整个程序以管理员身份运行在app.manifest里声明requireAdministrator而不是在代码里想办法提权。4. 进程管理程序的避坑清单五个真实踩过的坑4.1 刷新时句柄泄漏导致内存持续上涨现象程序开着不动任务管理器里自己这个进程的内存每隔几分钟涨一点跑一晚上涨到几百 MB。原因就是Process.GetProcesses()返回的对象没Dispose每个Process对象背后握着一个系统句柄句柄不释放关联的内核对象和内存就回收不了。解决就是在遍历时用try-finally保证每个p.Dispose()都执行或者用using。这个坑在短时间测试时完全看不出来只有长时间运行才暴露属于典型的血泪经验。4.2 在后台线程更新 ObservableCollection 直接崩溃现象把刷新逻辑放到Task.Run里界面随机抛「This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread」。原因是ObservableCollection不是线程安全的WPF 的绑定系统要求集合变更必须发生在 UI 线程。解决有两条路一是用DispatcherTimer让刷新天然在 UI 线程二是坚持后台线程的话用Application.Current.Dispatcher.Invoke把集合操作包起来。我一般选前者代码更干净。4.3 拿 CPU 占用率时 PerformanceCounter 首次调用极慢现象想给列表加一列 CPU 占用用PerformanceCounter读% Processor Time第一次调用要等一两秒界面明显卡顿。原因是PerformanceCounter首次访问要加载性能计数器库。解决是提前在程序启动时预热一次或者干脆放弃实时 CPU改成用两次采样的TotalProcessorTime差值自己算。后者更可控也不依赖性能计数器库是否损坏——那玩意儿损坏是 Windows 上出了名的玄学问题。4.4 结束进程后列表没更新用户以为没生效现象点了结束进程实际已经死了但列表里还挂着用户反复点。原因是刷新是定时触发的结束操作后没立即刷新。解决是在KillProcess成功后手动调一次Refresh()给用户即时反馈。更进一步可以在结束前把该行标记成「正在结束」的灰色状态避免用户重复点击。这类交互细节不影响功能但直接影响别人对这个工具的评价。4.5 以管理员运行时拖拽文件失效现象程序用管理员权限运行后从资源管理器往窗口里拖文件没反应。原因是 Windows 的 UAC 机制下普通权限的 explorer.exe 无法向高权限进程发送拖拽消息。解决是如果不需要拖拽功能就别开管理员如果既要管理员又要拖拽得改注册表EnableLUA相关设置或调用ChangeWindowMessageFilterEx放行WM_DROPFILES消息。这个坑很隐蔽因为功能本身没问题是权限隔离导致的。5. 把进程面板做成可复用的监控组件增量刷新与状态标记前面讲的整体替换刷新进程数一多就露馅。我后来改成按 Id 做增量更新思路是维护一个Dictionaryint, ProcessInfo每次刷新时对比新旧集合新出现的进程加进去消失的移除还在的只更新变化字段。这样ObservableCollection的变更事件数量从「全部」降到「实际变化的那几个」界面几乎不闪。private readonly Dictionaryint, ProcessInfo _cache new(); private void RefreshIncremental() { var latest _service.GetAllProcesses(); var latestIds latest.Select(x x.Id).ToHashSet(); // 移除已退出的 foreach (var id in _cache.Keys.ToList()) { if (!latestIds.Contains(id)) { Processes.Remove(_cache[id]); _cache.Remove(id); } } // 新增或更新 foreach (var item in latest) { if (_cache.TryGetValue(item.Id, out var existing)) { existing.MemoryMb item.MemoryMb; // 触发通知UI 自动更新 existing.IsResponding item.IsResponding; } else { _cache[item.Id] item; Processes.Add(item); } } }关键在existing.MemoryMb item.MemoryMb这一行。因为MemoryMb是带INotifyPropertyChanged的属性赋值就会通知 UI 更新那一格不需要动集合。Processes只在进程真正增减时才变选中状态自然保住了。这个模式在监控类界面上是通用解法值得记住。再往上走一步可以给ProcessInfo加一个Status枚举把「正常 / 无响应 / 即将结束」标出来配合IValueConverter在DataGrid里显示不同颜色。无响应的判断用IsResponding但要注意这个属性对没有消息循环的进程比如控制台程序恒为 true别拿它当万能健康指标。我自己的习惯是任何监控面板都要留一个「导出当前快照到 CSV」的按钮出问题时用户能直接把现场发给你比让他截图描述强一百倍。这个习惯帮我省过很多来回扯皮的时间希望帮到你。本文还有配套的精品资源点击获取
返回列表