我們在開發客戶端應用程序時,經常會遇到這樣的場景:
你開發好了一個客戶端程序,無論是以綠色版的方式使用,還是以安裝包的方式使用,絕大部分情況下都會在桌面上創建一個啟動 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 程序中提供支持,這樣就比較完美了。