1. 程式人生 > 其它 >採用單程序、單例項啟動客戶端程式

採用單程序、單例項啟動客戶端程式

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