我们在开发客户端应用程序时,经常会遇到这样的场景:
你开发好了一个客户端程序,无论是以绿色版的方式使用,还是以安装包的方式使用,绝大部分情况下都会在桌面上创建一个启动 exe 执行程序的快捷方式。用户在实际使用过程中,由于某些原因,很可能会多次双击快捷方式,导致同一个客户端程序启动了多个独立运行的实例,每个独立的实例其实对应着操作系统的一个独立的进程。
比如我们在 windows 操作系统上每次双击 notepad.exe 启动记事本程序时,都会启动一个新的记事本进程实例,如下图所示:
很多情况下这并不是我们所期望的现象,那么导致的结果就是:
不但造成硬件资源(比如内存资源)的浪费,甚至会导致代码运行逻辑的错误(比如客户端新版本下载升级,以及多个客户端实例读写相同的暂存文件资源等情况下,就会导致出现一些不必要的麻烦问题)。
我们期望的结果是:
不管点击多少次 exe 执行程序,只会运行一个客户端实例,在任务管理器中只会出现一个客户端进程。下面我们就用实际代码,采用两种方案来实现这种效果。由于目前 WPF 客户端开发比较流行,因此我就以普通 .NET Framework 4.0 创建的 WPF 程序为例来分享技术实现方案。对于 Winform 来说,其实现方式跟 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 类即可:
在具体创建的项目上,通过鼠标右键选择【属性】,打开如下图所示的界面,选择启动对象即可。
这是一种非常简单的实现方案,不但可以在基于普通 .NET Framework 创建的 WPF 中使用,也可以在基于 .NET Core 创建的 WPF 中使用。实现的效果是:当已经启动了一个客户端实例后,再次点击 exe 启动的话,会弹出提示框。
我使用的是 VS2019 创建的项目,WPF 和 WinForm 的具体代码示例下载地址为:
https://files.cnblogs.com/files/blogs/699532/MutexWpfDemo.zip
https://files.cnblogs.com/files/blogs/699532/MutexWinFormDemo.zip
二、采用微软的 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 组件,如下图所示:
新建一个 VB 组件实现类,假如名称为 SingleWrapper.cs ,这个类主要是实现单进程,WPF 的启动类通过该类进行包装,从而实现无论点击多少次 exe,始终只会启动一个进程。SingleWrapper.cs 的代码如下:
class SingleWrapper : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase
{
//构造函数
public SingleWrapper()
{
//设置为单进程实例
this.IsSingleInstance = true;
//在主窗体关闭后,结束程序进程
this.ShutdownStyle = ShutdownMode.AfterMainFormCloses;
}
//声明一个 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);
}
}
然后在具体创建的项目上,通过鼠标右键选择【属性】,打开如下图所示的界面,选择启动对象即可。
运行该程序实现的效果是:当已经启动了一个客户端实例后,再次点击 exe 启动的话,会直接再次显示原来的主窗体,这种方案实现的用户体验是最好的。但是有一个缺点:这种方案只能用于普通 .Net Framework 创建的 WPF 程序,目前 .NET Core 创建的 WPF 程序不支持这种方案。
我使用的是 VS2019 创建的项目,WPF 和 WinForm 的具体代码示例下载地址为:
https://files.cnblogs.com/files/blogs/699532/VBWpfDemo.zip
https://files.cnblogs.com/files/blogs/699532/VBWinFormDemo.zip
到此为止,两种实现方案已经介绍完毕,并提供了源代码可供下载和参考。
大家在实际工作中,可以根据具体的实际情况,采用不同的实现方案。我个人比较喜欢第二种方案,希望微软或者第三方公司能够在 .NET Core 版本的 WPF 程序中提供支持,这样就比较完美了。