我们在开发客户端应用程序时,经常会遇到这样的场景:
你开发好了一个客户端程序,无论是以绿色版的方式使用,还是以安装包的方式使用,绝大部分情况下都会在桌面上创建一个启动 exe 执行程序的快捷方式。用户在实际使用过程中,由于某些原因,很可能会多次双击快捷方式,导致同一个客户端程序启动了多个独立运行的实例,每个独立的实例其实对应着操作系统的一个独立的进程。
比如我们在 windows 操作系统上每次双击 notepad.exe 启动记事本程序时,都会启动一个新的记事本进程实例,如下图所示:

采用单进程、单实例启动客户端程序_xml

很多情况下这并不是我们所期望的现象,那么导致的结果就是:
不但造成硬件资源(比如内存资源)的浪费,甚至会导致代码运行逻辑的错误(比如客户端新版本下载升级,以及多个客户端实例读写相同的暂存文件资源等情况下,就会导致出现一些不必要的麻烦问题)。

我们期望的结果是:
不管点击多少次 exe 执行程序,只会运行一个客户端实例,在任务管理器中只会出现一个客户端进程。下面我们就用实际代码,采用两种方案来实现这种效果。由于目前 WPF 客户端开发比较流行,因此我就以普通 .NET Framework 4.0 创建的 WPF 程序为例来分享技术实现方案。


一、采用 Mutex 进程互斥方案

此方案实现步骤如下:


1 创建 WPF 程序,删掉 App.xaml 文件

WPF 程序默认情况下,通过 App.xaml 中读取 StartupUri 属性启动主窗体。
我们要想使用 Mutex 进程互斥方案,最好的办法就是自己通过编写代码的方式启动 WPF 程序,因此删除 App.xaml ,新建一个类,假如名称为 StartUp.cs ,编写代码如下:

class StartUp
{
    //互斥的唯一标识名称
    const string mutexName = "MyWpfApp";

    //自己编写一个 WPF 程序启动的入口
    [STAThread]
    static void Main(string[] args)
    {
        //是否允许创建新客户端实例
        bool createdNew;
        //创建 Mutex 实例,传入上面定义的互斥为止标识名称
        System.Threading.Mutex mutex = 
            new System.Threading.Mutex(true, mutexName, out createdNew);
        if (createdNew)
        {
            Application app = new Application();
            MainWindow win = new MainWindow();
            app.Run(win);
        }
        else
        {
            MessageBox.Show("程序已经在运行", "提示信息");
        }
    }
}

2 将应用程序的启动对象,设置为这个新创建的 StartUp 类即可:

在具体创建的项目上,通过鼠标右键选择【属性】,打开如下图所示的界面,选择启动对象即可。

采用单进程、单实例启动客户端程序_sed_02

这是一种非常简单的实现方案,不但可以在基于普通 .NET Framework 创建的 WPF 中使用,也可以在基于 .NET Core 创建的 WPF 中使用。实现的效果是:当已经启动了一个客户端实例后,再次点击 exe 启动的话,会弹出提示框。

我使用的是 VS2019 创建的项目,具体代码示例下载地址为:
javascript:void(0)


二、采用微软的 VB 组件方案

这种方案的实现原理为:VB 组件能够轻松实现单进程,通过 VB 组件的实现类,包装 WPF 的启动类。
此方案实现步骤如下:


1 创建 WPF 程序,修改主窗体 MainWindow 为单例模式

打开默认的主窗体 MainWindow.xaml 代码,为主窗体增加 Closed 事件。代码如下:

<Window x:Class="WpfApp2.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:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" Closed="Window_Closed" >
    <Grid>
        <TextBlock Name="tbDisplay" Text="我是主窗体(单进程启动Demo)" />
    </Grid>
</Window>

由于主窗体是单例模式,之所以增加 Closed 事件,目的是为了在主窗体关闭后,销毁主窗体对象。
其实这一步也可以省略,因为主窗体关闭后,程序就退出了,所有对象自然就销毁了。如果不是主窗体,而是普通窗体的单例模式的话,关闭普通窗体后就得要通过 Closed 事件销毁单例对象,要不然下次打开可能就会出问题。

打开主窗体 MainWindow.xaml 的后端 cs 代码,代码如下:

public partial class MainWindow : Window
{
    //将构造函数修改为 private,不允许通过 new 来进行实例化
    private MainWindow()
    {
        InitializeComponent();
    }

    //主窗体单例模式,声明一个静态的 MainWindow 对象
    public static MainWindow win;

    //通过静态方法获取 MainWindow 主窗体对象
    public static MainWindow GetMainWindow()
    {
        if (win == null)
        {
            win = new MainWindow();
        }

        return win;
    }

    //当主窗体关闭时,销毁静态的 MainWindow 对象
    private void Window_Closed(object sender, EventArgs e)
    {
        win = null;
    }
}

2 删掉 App.xaml 文件,采用代码的方式启动 WPF 程序

我们在这里还是把默认的 WPF 启动文件 App.xaml 删掉,采用自己编写的代码启动 WPF 程序。我们新创建一个应用程序类,假如名称为 WpfApp.cs 。这个类的功能跟 App.xaml 的后端代码 App.xaml.cs 一样,都继承自 System.Windows.Application 类,都是用来启动 WPF 程序,唯一的不同是:App.xaml 使用 StartupUri 属性来启动 WPF 主窗体,而 WpfApp.cs 通过后端代码的 OnStartup 事件来启动 WPF 的主窗体。 WpfApp.cs 代码如下:

class WpfApp : System.Windows.Application
{
    //通过 OnStartup 事件来启动 WPF 主窗体
    protected override void OnStartup(StartupEventArgs e)
    {
        showWindow();
    }

    //单独写一个创建并显示主窗体的方法
    //VB实现类也需要调用这个方法
    public void showWindow()
    {
        //通过上面第一步中的单例模式的静态方法获取主窗体
        MainWindow win = MainWindow.GetMainWindow();
        win.Show();
        //激活主窗体,使其比较引人注目
        win.Activate();

        //下面这两行代码,不是多余的
        //这两行代码的目的是:当窗体被遮住的话,让窗体直接显示在最顶层
        //这两行代码是一个比较实用的技巧
        win.Topmost = true;
        win.Topmost = false;
    }
}

3 创建一个 VB 组件实现类,用来包装启动第 2 步的 WPF 启动类

在项目上添加引用 Microsoft.VisualBasic 组件,如下图所示:

采用单进程、单实例启动客户端程序_sed_03

新建一个 VB 组件实现类,假如名称为 SingleWrapper.cs ,这个类主要是实现单进程,WPF 的启动类通过该类进行包装,从而实现无论点击多少次 exe,始终只会启动一个进程。SingleWrapper.cs 的代码如下:

class SingleWrapper : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase
{
    //构造函数
    public SingleWrapper()
    {
        //设置为单进程实例
        this.IsSingleInstance = true;
    }

    //声明一个 wpf 启动类实例
    WpfApp app;
    //通过 OnStartup 包装启动 wpf 启动类
    protected override bool OnStartup(StartupEventArgs eventArgs)
    {
        app = new WpfApp();
        app.Run();
        //这个地方返回 ture 还是 false 都可以
        return false;
    }

    //当再次点击 exe 时会触发这个事件
    //这里就直接调用 wpf 启动类里面的创建并展示主窗体方法
    protected override void OnStartupNextInstance(StartupNextInstanceEventArgs eventArgs)
    {
        app.showWindow();
    }
}

4 创建一个 WPF 启动入口类,采用 VB 实现类启动 WPF 程序

新建一个类,假如名称为 StartUp.cs ,写一个程序入口 Main 方法,采用 VB 实现类来启动 WPF 程序,代码如下:

class StartUp
{
    [STAThread]
    public static void Main(string[] args)
    {
        //每次启动一个进程
        //WpfApp app = new WpfApp();
        //app.Run();

        //最多只启动一个进程
        SingleWrapper sw = new SingleWrapper();
        sw.Run(args);
    }
}

然后在具体创建的项目上,通过鼠标右键选择【属性】,打开如下图所示的界面,选择启动对象即可。

采用单进程、单实例启动客户端程序_xml_04

运行该程序实现的效果是:当已经启动了一个客户端实例后,再次点击 exe 启动的话,会直接再次显示原来的主窗体,这种方案实现的用户体验是最好的。但是有一个缺点:这种方案只能用于普通 .Net Framework 创建的 WPF 程序,目前 .NET Core 创建的 WPF 程序不支持这种方案。

我使用的是 VS2019 创建的项目,具体代码示例下载地址为:
javascript:void(0)


到此为止,两种实现方案已经介绍完毕,并提供了源代码可供下载和参考。
大家在实际工作中,可以根据具体的实际情况,采用不同的实现方案。我个人比较喜欢第二种方案,希望微软或者第三方公司能够在 .NET Core 版本的 WPF 程序中提供支持,这样就比较完美了。