首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# WinForm 單實例運行完整方案:檢測、激活與前置顯示一站式實作

     編輯:關於C語言

前言

WinForm 開發中,「防止多開」是基礎且剛性的需求。單靠 Mutex 雖能阻擋第二個實例啟動,但無法實現「再次點擊圖標喚醒已運行視窗」的友好體驗。本文提供一套生產級方案:進程名+路徑雙重校驗 定位既有實例,結合 ShowWindowAsync / SetForegroundWindow 實現視窗恢復與前置,代碼可直接複製到 Program.cs 使用。

核心邏輯流程

  1. 獲取當前進程:Process.GetCurrentProcess()
  2. 同名進程遍歷:Process.GetProcessesByName()
  3. 雙重過濾:排除自己(PID 不同)+ 驗證 MainModule.FileName 與當前組件路徑一致(防止同名不同程式誤判)。
  4. 決策分支:
    • 無既有實例 → 正常 Application.Run(new frmMain())
    • 有既有實例 → 呼叫 Win32 API 顯示並前置,隨後退出當前進程。

完整 Program.cs 實作

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Process existing = RunningInstance();
        if (existing == null)
        {
            Application.Run(new frmMain()); // 替換為你的主視窗
        }
        else
        {
            HandleRunningInstance(existing);
        }
    }

    #region 單實例核心邏輯

    /// 
    /// 查找是否已有相同路徑的實例在運行
    /// 
    private static Process RunningInstance()
    {
        Process current = Process.GetCurrentProcess();
        Process[] processes = Process.GetProcessesByName(current.ProcessName);

        string currentPath = Assembly.GetExecutingAssembly().Location.Replace('/', '\\');

        foreach (Process p in processes)
        {
            if (p.Id != current.Id)
            {
                try
                {
                    if (p.MainModule.FileName == currentPath)
                        return p;
                }
                catch
                {
                    // 無權限訪問 MainModule(如系統進程),直接跳過
                }
            }
        }
        return null;
    }

    /// 
    /// 喚醒既有視窗:恢復最小化、置前、激活
    /// 
    private static void HandleRunningInstance(Process instance)
    {
        // SW_SHOWNORMAL = 1,若視窗最小化則恢復
        ShowWindowAsync(instance.MainWindowHandle, 1);
        // 置於最前並激活
        SetForegroundWindow(instance.MainWindowHandle);
    }

    [DllImport("User32.dll")]
    private static extern bool ShowWindowAsync(IntPtr hWnd, int cmdShow);

    [DllImport("User32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    #endregion
}

關鍵細節深度解析

為什麼要比對完整路徑?

僅比對 ProcessName 會導致「同名不同程式」誤判(如多個專案輸出同名 MyApp.exe)。比對 MainModule.FileName 可精準鎖定同一份二進制檔案。

權限陷阱與防禦

遍歷進程時,嘗試讀取 p.MainModule.FileName 可能因權限不足拋出 System.ComponentModel.Win32Exception(常見於系統進程或管理員權限進程)。範例以 try-catch 吞噬異常,避免導致啟動失敗。

視窗句柄可能為零

若既有進程尚未建立視窗(如啟動初期),MainWindowHandle 為 IntPtr.Zero,API 呼叫無效果。生產環境可加入 WaitForInputIdle() 或輪詢等待句柄有效。

替代方案:Mutex + 廣播訊息

  • Mutex:系統級命名互斥體,跨 Session/用戶也能生效,但無法直接喚醒視窗。
  • RegisterWindowMessage + PostMessage:自定義訊息廣播,既有實例收到訊息自行 Show(),架構更解耦,適合大型應用。

常見問題 FAQ

現象原因對策
第二次點擊無反應視窗被最小化到托盤,MainWindowHandle 為 0主視窗最小化時隱藏而非 Hide(),或維護全域句柄
管理員權限下無法喚醒普通權限實例UIPI (User Interface Privilege Isolation) 阻擋跨權限 SetForegroundWindow雙方以相同權限運行,或使用 ChangeWindowMessageFilterEx
攜帶啟動參數需傳遞給既有實例本方案未處理參數傳遞結合 WM_COPYDATA 或命名管道傳遞參數

總結

本方案以「進程名+路徑」雙重校驗確保精準定位,配合 Win32 API 實現視窗喚醒與前置,代碼精簡、零依賴、即插即用。對於需支援參數傳遞、跨權限、托盤殘留等複雜場景,建議在此之上擴展 IPC 機制。將此段代碼納入專案模板,讓每個 WinForm 專案從誕生那一刻就具備專業的單實例體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved