采用單進程、單實例啟動客戶端程序


我們在開發客戶端應用程序時,經常會遇到這樣的場景:
你開發好了一個客戶端程序,無論是以綠色版的方式使用,還是以安裝包的方式使用,絕大部分情況下都會在桌面上創建一個啟動 exe 執行程序的快捷方式。用戶在實際使用過程中,由於某些原因,很可能會多次雙擊快捷方式,導致同一個客戶端程序啟動了多個獨立運行的實例,每個獨立的實例其實對應着操作系統的一個獨立的進程。
比如我們在 windows 操作系統上每次雙擊 notepad.exe 啟動記事本程序時,都會啟動一個新的記事本進程實例,如下圖所示:

image

很多情況下這並不是我們所期望的現象,那么導致的結果就是:
不但造成硬件資源(比如內存資源)的浪費,甚至會導致代碼運行邏輯的錯誤(比如客戶端新版本下載升級,以及多個客戶端實例讀寫相同的暫存文件資源等情況下,就會導致出現一些不必要的麻煩問題)。

我們期望的結果是:
不管點擊多少次 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 類即可:

在具體創建的項目上,通過鼠標右鍵選擇【屬性】,打開如下圖所示的界面,選擇啟動對象即可。

image

這是一種非常簡單的實現方案,不但可以在基於普通 .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 組件,如下圖所示:

image

新建一個 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);
    }
}

然后在具體創建的項目上,通過鼠標右鍵選擇【屬性】,打開如下圖所示的界面,選擇啟動對象即可。

image

運行該程序實現的效果是:當已經啟動了一個客戶端實例后,再次點擊 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 程序中提供支持,這樣就比較完美了。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM