WinForm 開發中,「防止多開」是基礎且剛性的需求。單靠 Mutex 雖能阻擋第二個實例啟動,但無法實現「再次點擊圖標喚醒已運行視窗」的友好體驗。本文提供一套生產級方案:進程名+路徑雙重校驗 定位既有實例,結合 ShowWindowAsync / SetForegroundWindow 實現視窗恢復與前置,代碼可直接複製到 Program.cs 使用。
Process.GetCurrentProcess()Process.GetProcessesByName()MainModule.FileName 與當前組件路徑一致(防止同名不同程式誤判)。Application.Run(new frmMain())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() 或輪詢等待句柄有效。
Show(),架構更解耦,適合大型應用。| 現象 | 原因 | 對策 |
|---|---|---|
| 第二次點擊無反應 | 視窗被最小化到托盤,MainWindowHandle 為 0 | 主視窗最小化時隱藏而非 Hide(),或維護全域句柄 |
| 管理員權限下無法喚醒普通權限實例 | UIPI (User Interface Privilege Isolation) 阻擋跨權限 SetForegroundWindow | 雙方以相同權限運行,或使用 ChangeWindowMessageFilterEx |
| 攜帶啟動參數需傳遞給既有實例 | 本方案未處理參數傳遞 | 結合 WM_COPYDATA 或命名管道傳遞參數 |
本方案以「進程名+路徑」雙重校驗確保精準定位,配合 Win32 API 實現視窗喚醒與前置,代碼精簡、零依賴、即插即用。對於需支援參數傳遞、跨權限、托盤殘留等複雜場景,建議在此之上擴展 IPC 機制。將此段代碼納入專案模板,讓每個 WinForm 專案從誕生那一刻就具備專業的單實例體驗。