最近因为工作需要,研究了一下桌面应用程序。在winform、WPF、Electron等几种技术里,最终选择了WPF作为最后的选型。WPF最吸引我的地方,就是MVVM模式了。MVVM模式完全把界面和业务剥离开来,页面所有操作都通过数据来驱动。更替页面不用修改业务代码逻辑。
以一个查杀进程的小工具来作为初次学习的成果总结。日常开发Java Web程序的时候,进程遇到端口占用问题,通过命令查找端口、查找进程、杀死进程,这一套命令敲下来过于麻烦。于是就写了这么一个小Demo,即作为学习使用,也为以后工作降低工作量。
项目代码
https://github.com/zer0Black/WinPidKiller
需求设计
- 进程列表:展示所有经常的列表,按照应用名称正序排序。列表展示进程名、PID、协议、本机IP:端口、远程IP:端口、进程路径
- 搜索框:进行端口搜索,在经常列表中展示搜索结果
- 刷新按钮:刷新进程列表
- 杀死按钮:选中进程,进行进程的杀死。杀死进程后刷新进程列表
关键要点
-
DataContext
DataContext主要作用是用于绑定数据源,默认值为null。
DataContext是FrameworkElement中的一个属性。而绝大部分的UI组件都继承路径中都有FrameworkElement类,所以我们可以认为,大部分UI组件都有DataContext属性。并且设置了某个对象的DataContext,那么会对这个对象的所有子对象都会产生同样的影响。
所以一般来说,我们都会在顶级对象(Window对象)中去设置DataContext属性。 -
使用MVVM的意义
使用统一开发模式最大的优点,是统一团队的思维方式和实现方式,从思维上保持代码的整洁。每个理解了模式的人都知道代码该怎么写。此外,MVVM模式在架构上解耦的比较彻底,数据驱动界面的模式也可让结构更清晰。由于业务和界面剥离,业务代码的可测性、可读性、可替换性得到提升。所以,既然WPF支持MVVM模式,就不要把WPF写成WinForm。 -
View 和 ViewModel
View是UI、ViewModel是界面的数据模型。ViewModel和View是怎么沟通的呢?ViewModel只会给View传递两种数据:属性数据和操作数据。传递数据用一般的数据模型来处理,传递操作用命令属性来处理。
项目结构
引用包说明
- MaterialDesignThemes:主要用于界面的美化,通过NuGet包管理搜索MaterialDesignThemes直接安装
- Prism.Wpf:是实现MVVM的框架,通过NuGet包管理搜索Prism.Wpf直接安装
项目目录结构说明
WinPidKiller 项目名
- Models 业务数据模型层
NetworkInfo.cs 网络端口数据模型
ProcessInfo.cs 进程数据模型
- Services 业务逻辑层
IProcessInfoService.cs 进程业务操作接口
- impl 业务逻辑实现
ProcessInfoService.cs 进程业务操作实现类
- ViewModels 视图数据模型层,沟通View和Model的重要组件
ProcessItemViewModel.cs 单行进程视图数据模型(列表中每行数据的模型)
MainWindowViewModel.cs 主视图数据模型
- Views 界面层
MainWindow.xmal 主窗口文件
代码解释说明
Models
数据模型仅针对于业务数据
NetworkInfo.cs
namespace WinPidKiller.Models
{
class NetworkInfo
{
public string Pid { get; set; }
public string AgreeMent { get; set; }
public string LocalIp { get; set; }
public string RemoteIp { get; set; }
}
}
ProcessInfo.cs
namespace WinPidKiller.Models
{
class ProcessInfo
{
public string Name { get; set; }
public string Pid { get; set; }
public string AgreeMent { get; set; }
public string LocalIp { get; set; }
public string RemoteIp { get; set; }
}
}
Services
仅包含ProcessInfoService类,主要实现端口的查询(通过调用cmd进程),进程的获取和杀死等操作
ProcessInfoService.cs
namespace WinPidKiller.Services.Impl
{
class ProcessInfoService : IProcessInfoService
{
/**
* 若port为空则获取所有进程信息
* 若port不为空则获取占用port的线程
*/
public List<ProcessInfo> GetAllProcessInfo(String port)
{
List<ProcessInfo> processInfoList = new List<ProcessInfo>();
// 拿到所有进程
Dictionary<int, Process> processMap = GetAllProcess();
List<NetworkInfo> networkInfos = null;
if (!(string.IsNullOrEmpty(port)))
{
// 根据port查询出对应的端口信息,展示对应进程信息
networkInfos = GetPortInfo(port);
} else
{
networkInfos = GetPortInfo();
}
foreach (NetworkInfo networkInfo in networkInfos)
{
ProcessInfo processInfo = new ProcessInfo();
int.TryParse(networkInfo.Pid, out int pid);
Process process = processMap[pid];
processInfo.Name = process.ProcessName;
processInfo.Pid = process.Id.ToString();
processInfo.AgreeMent = networkInfo.AgreeMent;
processInfo.LocalIp = networkInfo.LocalIp;
processInfo.RemoteIp = networkInfo.RemoteIp;
processInfoList.Add(processInfo);
}
return processInfoList;
}
/**
* 获取所有进程信息
*/
public List<ProcessInfo> GetAllProcessInfo()
{
return GetAllProcessInfo(null);
}
/**
* 根据pid列表杀死所有进程
*/
public void KillProcess(List<string> pidList)
{
if (pidList == null || pidList.Count == 0)
{
MessageBox.Show("请选择正确的进程号");
return;
}
Dictionary<int, Process> processMap = GetAllProcess();
StringBuilder sb = new StringBuilder();
foreach (var pidStr in pidList)
{
int.TryParse(pidStr, out int pid);
Process process = processMap[pid];
try
{
process.Kill();
sb.Append("已杀掉");
sb.Append(process.ProcessName);
sb.Append("进程!!!");
}
catch (Win32Exception e)
{
sb.Append(process.ProcessName);
sb.Append(e.Message.ToString());
}
catch (InvalidOperationException e)
{
sb.Append(process.ProcessName);
sb.Append(e.Message.ToString());
}
}
MessageBox.Show(sb.ToString());
}
/**
* 获取所有原始进程信息,并封装为Dictionary
*/
private Dictionary<int, Process> GetAllProcess()
{
Process[] processes = Process.GetProcesses();
return processes.ToDictionary(key => key.Id, process => process);
}
/**
* 获取所有端口信息
*/
private List<NetworkInfo> GetPortInfo()
{
return GetPortInfo(null);
}
/**
* 通过端口取出所有相关的数据
*/
private List<NetworkInfo> GetPortInfo(string port)
{
List<NetworkInfo> networkInfoList = new List<NetworkInfo>();
Process process = CreateCmd();
process.Start();
if (string.IsNullOrEmpty(port))
{
process.StandardInput.WriteLine(string.Format("netstat -ano"));
} else
{
process.StandardInput.WriteLine(string.Format("netstat -ano|find "{0}"", port));
}
process.StandardInput.WriteLine("exit");
StreamReader reader = process.StandardOutput;
string strLine = reader.ReadLine();
while (!reader.EndOfStream)
{
strLine = strLine.Trim();
if (strLine.Length > 0 && ((strLine.Contains("TCP") || strLine.Contains("UDP"))))
{
Regex r = new Regex(@"s+");
string[] strArr = r.Split(strLine);
// 解析数据格式为 TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 692
int defaultResultLength = 5;
if (strArr.Length == defaultResultLength)
{
NetworkInfo networkInfo = new NetworkInfo();
// 只拿第一行数据,拿完就撤(每个PID展示一个port就行)
networkInfo.AgreeMent = strArr[0];
networkInfo.LocalIp = strArr[1];
networkInfo.RemoteIp = strArr[2];
networkInfo.Pid = strArr[4];
networkInfoList.Add(networkInfo);
}
}
strLine = reader.ReadLine();
}
reader.Close();
process.Close();
return networkInfoList;
}
/**
* 创建cmd控件
*/
private Process CreateCmd()
{
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
return process;
}
}
}
ViewModels
主要实现进程列表中单个进程的数据模型ProcessItemViewModel的实现,ProcessItemViewModel比业务数据模型多了选中属性selectItem。另外包含主窗体模型,完成剩下的数据和命令传递。
ProcessItemViewModel.cs
namespace WinPidKiller.ViewModels
{
class ProcessItemViewModel : BindableBase
{
public ProcessInfo ProcessInfo { get; set; }
private Boolean selectItem;
public Boolean SelectItem
{
get { return selectItem; }
set
{
selectItem = value;
SetProperty(ref selectItem, value);
}
}
}
}
MainWindowViewModel.cs
namespace WinPidKiller.ViewModels
{
/**
* 做双向绑定,port提供查询框用,processInfo列表提供dataGrid用
*/
class MainWindowViewModel : BindableBase
{
private int port;
public int Port
{
get { return port; }
set {
port = value;
SetProperty(ref port, value);
}
}
/**
* 如果这个DataList列表的内容需要同步刷新,
* 则类型必须是ObservableCollection。
* 否则就算控件与数据绑定成功,控件只在初始化时能够正确显示数据,
* 之后数据发生改变时,控件不会自动刷新。
*/
private ObservableCollection<ProcessItemViewModel> processItemList;
public ObservableCollection<ProcessItemViewModel> ProcessItemList
{
get { return processItemList; }
set {
processItemList = value;
SetProperty(ref processItemList, value);
}
}
public MainWindowViewModel()
{
// 加载数据
LoadProcessInfo();
QueryPortCommand = new DelegateCommand(new Action(QueryPortCommandExec));
KillCommand = new DelegateCommand(new Action(KillCommandExec));
RefreshCommand = new DelegateCommand(new Action(RefreshCommandExec));
}
private void LoadProcessInfo()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList = new ObservableCollection<ProcessItemViewModel>();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo()));
}
// 绑定检索命令 和 kill命令
public DelegateCommand QueryPortCommand { get; set; }
public DelegateCommand KillCommand { get; set; }
public DelegateCommand RefreshCommand { get; set; }
private void QueryPortCommandExec()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList.Clear();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo(port.ToString())));
}
private void RefreshCommandExec()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList.Clear();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo()));
}
private void KillCommandExec()
{
List<String> pidList = new List<string>();
foreach (var processItem in processItemList)
{
if (processItem.SelectItem)
{
pidList.Add(processItem.ProcessInfo.Pid);
}
}
IProcessInfoService processInfoService = new ProcessInfoService();
processInfoService.KillProcess(pidList);
// 杀死进程后,重新加载列表
this.QueryPortCommandExec();
}
/**
* 将ProcessInfo列表转为ProcessItemViewModel列表
*/
private List<ProcessItemViewModel> GetProcessItemViewModel(List<ProcessInfo> processInfos)
{
List<ProcessItemViewModel> itemList = new List<ProcessItemViewModel>();
foreach(ProcessInfo processInfo in processInfos){
ProcessItemViewModel item = new ProcessItemViewModel() { ProcessInfo = processInfo };
itemList.Add(item);
}
return itemList;
}
}
}
主窗体界面
MainWindow.xaml.cs
namespace WinPidKiller
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}
MainWindow.xaml
<Window x:Class="WinPidKiller.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WinPidKiller"
mc:Ignorable="d"
Title="Pid Killer" Height="450" Width="800"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<materialDesign:Card Grid.Row="0" Padding="8" Margin="8,5,8,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="110"></ColumnDefinition>
<ColumnDefinition Width="110"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=Port}" HorizontalAlignment="Stretch" Margin="0,0,110,0" FontSize="20" VerticalAlignment="Center"/>
<Button Content="检索" Grid.Column="0" Width="100" HorizontalAlignment="Right" Command="{Binding QueryPortCommand}"/>
<Button Content="刷新" Grid.Column="1" Width="100" HorizontalAlignment="Right" Command="{Binding RefreshCommand}"/>
<Button Content="杀死" Grid.Column="2" Width="100" HorizontalAlignment="Right" Command="{Binding KillCommand}"/>
</Grid>
</materialDesign:Card>
<materialDesign:Card Grid.Row="1" Padding="8" Margin="8,5,8,5" >
<DataGrid
x:Name="dataGrid"
FontSize="15"
AlternationCount="2"
GridLinesVisibility="Vertical"
AutoGenerateColumns="False"
IsReadOnly="True"
ItemsSource="{Binding Path=ProcessItemList}"
>
<DataGrid.Columns>
<DataGridCheckBoxColumn Width="50" Header="" Binding="{Binding Path=SelectItem,UpdateSourceTrigger=PropertyChanged}" IsReadOnly="False" CanUserSort="False" />
<DataGridTextColumn Width="Auto" Header="进程名" Binding="{Binding Path=ProcessInfo.Name}"/>
<DataGridTextColumn Width="100" Header="PID" Binding="{Binding Path=ProcessInfo.Pid}"/>
<DataGridTextColumn Width="80" Header="协议" Binding="{Binding Path=ProcessInfo.AgreeMent}"/>
<DataGridTextColumn Width="200" Header="本机IP:端口" Binding="{Binding Path=ProcessInfo.LocalIp}"/>
<DataGridTextColumn Width="200" Header="远程IP:端口" Binding="{Binding Path=ProcessInfo.RemoteIp}"/>
</DataGrid.Columns>
</DataGrid>
</materialDesign:Card>
</Grid>
</Window>