最近因为工作需要,研究了一下桌面应用程序。在winform、WPF、Electron等几种技术里,最终选择了WPF作为最后的选型。WPF最吸引我的地方,就是MVVM模式了。MVVM模式完全把界面和业务剥离开来,页面所有操作都通过数据来驱动。更替页面不用修改业务代码逻辑。

以一个查杀进程的小工具来作为初次学习的成果总结。日常开发Java Web程序的时候,进程遇到端口占用问题,通过命令查找端口、查找进程、杀死进程,这一套命令敲下来过于麻烦。于是就写了这么一个小Demo,即作为学习使用,也为以后工作降低工作量。

需求设计

  1. 进程列表:展示所有经常的列表,按照应用名称正序排序。列表展示进程名、PID、协议、本机IP:端口、远程IP:端口、进程路径
  2. 搜索框:进行端口搜索,在经常列表中展示搜索结果
  3. 刷新按钮:刷新进程列表
  4. 杀死按钮:选中进程,进行进程的杀死。杀死进程后刷新进程列表

关键要点

  1. DataContext
    DataContext主要作用是用于绑定数据源,默认值为null。
    DataContext是FrameworkElement中的一个属性。而绝大部分的UI组件都继承路径中都有FrameworkElement类,所以我们可以认为,大部分UI组件都有DataContext属性。并且设置了某个对象的DataContext,那么会对这个对象的所有子对象都会产生同样的影响。
    所以一般来说,我们都会在顶级对象(Window对象)中去设置DataContext属性。

  2. 使用MVVM的意义
    使用统一开发模式最大的优点,是统一团队的思维方式和实现方式,从思维上保持代码的整洁。每个理解了模式的人都知道代码该怎么写。此外,MVVM模式在架构上解耦的比较彻底,数据驱动界面的模式也可让结构更清晰。由于业务和界面剥离,业务代码的可测性、可读性、可替换性得到提升。所以,既然WPF支持MVVM模式,就不要把WPF写成WinForm。

  3. View 和 ViewModel
    View是UI、ViewModel是界面的数据模型。ViewModel和View是怎么沟通的呢?ViewModel只会给View传递两种数据:属性数据和操作数据。传递数据用一般的数据模型来处理,传递操作用命令属性来处理。

项目结构

引用包说明

  1. MaterialDesignThemes:主要用于界面的美化,通过NuGet包管理搜索MaterialDesignThemes直接安装
  2. 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>
10-09 10:08