C# Winform 程序EXE單例模式的三種方案詳細總結

Winform 是一個很容易上手的C# 應用模式,但是他和MFC一樣也沒有幫我們實現EXE單例模式,所以我們必須自己手敲代碼,但是你懂的C#沒提供很多好用的API,所以得處處從C++裏導過來,我先講網上大家流傳的兩種方式,最後講講我個人思考的一種比較完美手法,未經項目實戰,但是測試穩定先賣個關子,耐心往下看。



方式一:利用System.Thread.Mutex的一個重載構造函數

  //
        // 摘要:
        //     使用一個指示調用線程是否應擁有互斥體的初始所屬權的布爾值、一個作爲互斥體名稱的字符串,以及一個在方法返回時指示調用線程是否被授予互斥體的初始所屬權的布爾值來初始化
        //     System.Threading.Mutex 類的新實例。
        //
        // 參數:
        //   initiallyOwned:
        //     如果爲 true,則給予調用線程已命名的系統互斥體的初始所屬權(如果已命名的系統互斥體是通過此調用創建的);否則爲 false。
        //
        //   name:
        //     System.Threading.Mutex 的名稱。如果值爲 null,則 System.Threading.Mutex 是未命名的。
        //
        //   createdNew:
        //     在此方法返回時,如果創建了局部互斥體(即,如果 name 爲 null 或空字符串)或指定的命名系統互斥體,則包含布爾值;則爲 true;如果指定的命名系統互斥體已存在,則爲
        //     false。該參數未經初始化即被傳遞。
        //
        // 異常:
        //   System.UnauthorizedAccessException:
        //     命名的互斥體存在並具有訪問控制安全性,但用戶不具有 System.Security.AccessControl.MutexRights.FullControl。
        //
        //   System.IO.IOException:
        //     發生了一個 Win32 錯誤。
        //
        //   System.Threading.WaitHandleCannotBeOpenedException:
        //     無法創建命名的互斥體,原因可能是與其他類型的等待句柄同名。
        //
        //   System.ArgumentException:
        //     name 的長度超過 260 個字符。
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        [SecurityCritical]
        public Mutex(bool initiallyOwned, string name, out bool createdNew);

啓動時採用如下這些語句,利用是系統只有一個命名爲“TestSingleStart”的Mutex變量的方式,所以在沒釋放前都是創建不成功的,所以createdNew變量只有第一次開啓程序才能被賦值上true。

Boolean createdNew;
System.Threading.Mutex instance = new System.Threading.Mutex(true, "TestSingleStart", out createdNew); //同步基元變量 
if (createdNew)
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
    instance.ReleaseMutex();
}
else
{
    MessageBox.Show("已經啓動了一個程序,請先退出");
    Application.Exit();
}


注意:說點題外話, 實際上大家要特別注意的一點是其實Mutex這個類我個人感覺一開始被設計出來更多是用來解決同一個進程內,多線程同步問題,舉個例子

就像lock關鍵字,詳細參見微軟官方SDK(https://msdn.microsoft.com/zh-cn/library/system.threading.mutex(v=vs.100).aspx)

大概意思就是說,舉例子吧,以下兩段代碼等效。

代碼段一:

        static Object lockobj = new Object();
        private void testMultiplyThread() 
        {
            lock (lockobj)
            {
                Console.WriteLine("同步處理的代碼");
            }
        }

代碼段二:

//小注:無參構造函數默認是 給調用線程賦予互斥體的初始所屬權,所以第一次mutex.WaitOne不會阻塞

  static System.Threading.Mutex mutex =new System.Threading.Mutex();
        private void testMultiplyThread()
        {
            mutex.WaitOne();
            Console.WriteLine("同步處理的代碼");
            mutex.ReleaseMutex();
        }

評價:方式一巧妙利用Mutex的基元單位命名規則是系統內唯一,來判定是否是重開了一個exe,但是更人性化的處理是順便把焦點定位到之前運行的exe上。

優點:簡潔,精確。

缺點:沒有點位焦點到之前運行好的EXE窗體上


方式二:利用進程名判斷是否有同名的進程已經運行如果有的話,得到該進程並獲取窗口句柄,得到句柄後調用ShowWindowAsync和SetForegroundWindow

這個直接上代碼:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SingleStart
{
    static class Program
    {
        /// <summary>
        /// 應用程序的主入口點。
        /// </summary>
        [STAThread]
        static void Main()
        {
          
            try
            {
                Process instance = RunningInstance();
                if (instance == null)
                {
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    Application.Run(new pbform());
                }
                else
                {
                    HandleRunningInstance(instance);
                }
            }
            catch (Exception e) { }
        }

        [DllImport("User32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);
        [DllImport("User32.dll")]
        private static extern bool ShowWindowAsync(IntPtr hWnd, int cmdShow);
        private static void HandleRunningInstance(Process instance)
        {
            // 確保窗口沒有被最小化或最大化 
            ShowWindowAsync(instance.MainWindowHandle, 4);
            // 設置真實例程爲foreground  window  
            SetForegroundWindow(instance.MainWindowHandle);// 放到最前端 
        }

        private static Process RunningInstance()
        {
            Process current = Process.GetCurrentProcess();
            Process[] processes = Process.GetProcessesByName(current.ProcessName);
            foreach (Process process in processes)
            {
                if (process.Id != current.Id)
                {
                    // 確保例程從EXE文件運行 
                    if (Assembly.GetExecutingAssembly().Location.Replace("/", "\\") == current.MainModule.FileName)
                    {
                        return process;
                    }
                }
            }
            return null;
        }
    }
}
評價:這種方式的確人性化很多,而且在大部分情況下都能夠準確判定,但是如果要鑽牛角尖的話,如果把EXE拷貝一份改個名字,照樣可以新開啓一個EXE額,所以這一點是美中不足的,方式二提升了人性化,但是準確性降低了了。(我聽到無數人罵我鑽牛角尖了呵呵)

優點:第二次啓動EXE可以直接使前面還在運行的EXE得到焦點,人性化很好。

缺點:在一些特殊情況下還是能開啓第二個EXE。


方式三:這個方式其實說來是我無意中在實現別的功能中,可以說是意外聯想到,容我慢慢道來。

大概是2015年初剛開班的時候,有個項目需求我使用chrome瀏覽器去打開本地一個EXE,而且是帶參數打開,而且當然還要求一點就是如果參數是一樣的那麼如果之前曾經打開過那個一樣的參數EXE就必須直接獲取焦點,參數不一樣就重寫打開新EXE產生一個線程,相比看到這裏你應該明白了吧,上面的方式一和方式二肯定是滿足不了我的需求,實際上如果要滿足我的需求說簡單也簡單容我一一列舉。


方式三第一個版本:直接產生持久化文件,管理所有打開過的EXE,但是這樣會不會容易產生文件佔用呢,或許實現起來也不輕鬆

        方式三第二個版本:使用一個WIndows服務作爲協助,把所有開過的EXE,且傳過去的參數進行管理呢,這或許是一個萬能的手段,但是又要多謝一個Windows服務。

        

經過以上推敲,我個人無意中發現MemoryMappedFile這個類,內存文件映射,有人說這不是和第一個版本一樣嗎,我要說NO,這大不一樣,我說這一切的原因都在於

MemoryMappedFile.CreateOrOpen這個方法

  //
        // 摘要:
        //     在系統內存中創建或打開一個具有指定容量的內存映射文件。
        //
        // 參數:
        //   mapName:
        //     要分配給內存映射文件的名稱。
        //
        //   capacity:
        //     要分配給內存映射文件的最大大小(以字節爲單位)。
        //
        // 返回結果:
        //     具有指定名稱和大小的內存映射文件。
        //
        // 異常:
        //   System.ArgumentException:
        //     mapName 是空字符串。
        //
        //   System.ArgumentNullException:
        //     mapName 爲 null。
        //
        //   System.ArgumentOutOfRangeException:
        //     capacity 大於邏輯地址空間的大小。 - 或 - capacity 小於或等於零。
        public static MemoryMappedFile CreateOrOpen(string mapName, long capacity);

使用內存管理的方式把不同的參數壓縮成一個字符串mapName,然後使用MemoryMappedFile 來創建或打開並管理他,這不就是最好的終極解決方案嗎,更加美妙是的,由於是內存管理,可以把結構體指針寫進去,這不就是說可以把窗口句柄寫進去的意思一樣,所以一些思路都清晰了,所需只是下面兩個函數將完美解決我想要的一切,事實證明當內存足夠大時候,把問題放在內存裏解決是多麼美妙的的一件事大笑,太有成就感了下面貼出關鍵兩個函數。

寫函數

 MemoryMappedFile mmf = MemoryMappedFile.CreateOrOpen(key, 1024000);

 using (MemoryMappedViewStream stream = mmf.CreateViewStream()) //注意這裏的偏移量
 {
     using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
    {
        accessor.Write(0, ref handler);//這裏的handler就是我們窗口句柄
    }
 }

讀函數

 static IntPtr GetMemory(string key)
 {
    using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(key))
    {
        using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
        {
             IntPtr handler = IntPtr.Zero;
             accessor.Read(0, out handler);
             return handler;//<span style="font-family: Arial, Helvetica, sans-serif;">這裏的handler就是我們窗口句柄</span>
        }
     }
}

這是我自己使用方案三:爲了自己製作的單例模式的簡易DEMO,從項目代碼裏抽出來不容易啊只要1分,下載地址





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章