防止重複啟動本質上是「檢測同一個應用程式是否已在運行」。主流三大思路:
Process.GetProcessesByName 計數或比對路徑。FindWindow 定位已有視窗並喚醒,適合需「喚醒既有視窗」場景。放在 Program.cs Main() 最開頭,using 區塊確保進程結束自動釋放。
static class Program
{
[STAThread]
static void Main()
{
bool createdNew;
// 使用產品名稱作為 Mutex 名稱,確保唯一性
using (var mutex = new Mutex(true, Application.ProductName, out createdNew))
{
if (createdNew)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
else
{
MessageBox.Show("應用程式已在執行中...", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 可選:喚醒既有視窗 (需配合方案三/四)
}
}
}
}
關鍵點:Mutex(true, name, out createdNew) 參數意義——initiallyOwned=true 申請擁有權、name 全系統唯一、createdNew 告知是否為首次建立。
var procs = Process.GetProcessesByName(Application.CompanyName); // 或 ProcessName
if (procs.Length >= 1) { /* 已運行 */ }
⚠️ 缺陷:同名不同路徑程式會誤判;VS 偵錯時進程名為 xxx.vshost 導致誤判。
Process current = Process.GetCurrentProcess();
var procs = Process.GetProcessesByName(current.ProcessName.Replace(".vshost", ""));
foreach (var p in procs)
{
if (p.Id != current.Id &&
Assembly.GetExecutingAssembly().Location.Replace("/", "\\") == p.MainModule.FileName)
{
// 找到同路徑既有進程
return p;
}
}
return null;
適用於「已有視窗最小化」情況,無法處理縮至系統托盤 (無視窗句柄)。
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hWnd);
const int SW_SHOWNORMAL = 1;
static void ActivateExisting(Process proc)
{
ShowWindow(proc.MainWindowHandle, SW_SHOWNORMAL);
SetForegroundWindow(proc.MainWindowHandle);
}
當主視窗 Hide() 或縮至托盤時,MainWindowHandle 為 0。需改用 FindWindow 依類別名/標題尋找,再 SwitchToThisWindow 強制切換。
[DllImport("user32.dll")] static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")] static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
[DllImport("user32.dll")] static extern int ShowWindow(IntPtr hWnd, int nCmdShow);
const int SW_RESTORE = 9;
static void Main()
{
Process current = Process.GetCurrentProcess();
var procs = Process.GetProcessesByName(current.ProcessName.Replace(".vshost", ""));
if (procs.Length > 1) // 含自己
{
foreach (var p in procs)
{
if (p.Id == current.Id) continue;
IntPtr hWnd = p.MainWindowHandle;
if (hWnd == IntPtr.Zero) // 可能隱藏/托盤
{
// 依視窗標題尋找 (需確保標題唯一)
hWnd = FindWindow(null, "MainForm"); // 或類別名
}
if (hWnd != IntPtr.Zero)
{
ShowWindow(hWnd, SW_RESTORE); // 還原最小化/隱藏
SwitchToThisWindow(hWnd, true); // 切換到前台
}
}
return; // 結束新啟動的實例
}
// 首次啟動
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
| 需求 | 推薦方案 | 備註 |
|---|---|---|
| 最簡單、標準、無需喚醒視窗 | 方案一 Mutex | 官方推薦模式,代碼最少 |
| 需喚醒最小化視窗 | 方案一 + 方案三 | Mutex 判斷 + Win32 喚醒 |
| 需喚醒托盤隱藏視窗 | 方案四 | FindWindow + SwitchToThisWindow |
| 無視覺介面 (服務/Console) | 方案一 Mutex | 同樣適用 |
| 跨使用者 Session 隔離 | Mutex 名稱加 Global\\ 前綴 | 需管理員權限 |
.vshost,正式發布無此問題;掃描法務必 Replace(".vshost", "")。Application.ProductName 或 Guid 確保唯一;加 Global\\ 可跨 Session 但需高權限。FindWindow 建議用類別名 (#32770 等) 或自訂 this.Text 保證唯一。Global\\。