採用單程序、單例項啟動客戶端程式
我們在開發客戶端應用程式時,經常會遇到這樣的場景:
你開發好了一個客戶端程式,無論是以綠色版的方式使用,還是以安裝包的方式使用,絕大部分情況下都會在桌面上建立一個啟動 exe 執行程式的快捷方式。使用者在實際使用過程中,由於某些原因,很可能會多次雙擊快捷方式,導致同一個客戶端程式啟動了多個獨立執行的例項,每個獨立的例項其實對應著作業系統的一個獨立的程序。
比如我們在 windows 作業系統上每次雙擊 notepad.exe 啟動記事本程式時,都會啟動一個新的記事本程序例項,如下圖所示:
很多情況下這並不是我們所期望的現象,那麼導致的結果就是:
不但造成硬體資源(比如記憶體資源)的浪費,甚至會導致程式碼執行邏輯的錯誤(比如客戶端新版本下載升級,以及多個客戶端例項讀寫相同的暫存檔案資源等情況下,就會導致出現一些不必要的麻煩問題)。
我們期望的結果是:
不管點選多少次 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 類即可:
在具體建立的專案上,通過滑鼠右鍵選擇【屬性】,開啟如下圖所示的介面,選擇啟動物件即可。
這是一種非常簡單的實現方案,不但可以在基於普通 .NET Framework 建立的 WPF 中使用,也可以在基於 .NET Core 建立的 WPF 中使用。實現的效果是:當已經啟動了一個客戶端例項後,再次點選 exe 啟動的話,會彈出提示框。
我使用的是 VS2019 建立的專案,具體程式碼示例下載地址為:
https://files.cnblogs.com/files/blogs/699532/MutexWpfDemo.rar
二、採用微軟的 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;
}
//宣告一個 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 建立的專案,具體程式碼示例下載地址為:
https://files.cnblogs.com/files/blogs/699532/VBWpfDemo.rar
到此為止,兩種實現方案已經介紹完畢,並提供了原始碼可供下載和參考。大家在實際工作中,可以根據具體的實際情況,採用不同的實現方案。我個人比較喜歡第二種方案,希望微軟或者第三方公司能夠在 .NET Core 版本的 WPF 程式中提供支援,這樣就比較完美了。