C# 单实例应用程序, 能够从系统托盘恢复(使用 Mutex)
一种防止您的应用程序多次打开,并聚焦/激活第一个实例窗口的方法。
介绍
我注意到,虽然有其他关于单实例应用程序的文章,但似乎有一个特定的空白尚未被填补。我在这里所做的,是尝试汇集他人的智慧,并感谢他们的贡献,以一种我希望有所帮助的方式呈现。
背景
创建“单实例”应用程序一定是常见的需求,因为有大量的相关文章,不仅在 The Code Project 上如此。在 Google 上搜索“C# 单实例”,您会找到关于该主题的许多链接页面。
我想实现一个尽可能简单、优雅,代码量尽可能少的解决方案。我需要以下功能:
- 当用户第二次尝试打开应用程序时,屏幕上不会打开第二个实例。
- 相反,第一个实例会显示到前台。如果它已最小化,则会恢复。
- 如果应用程序已最小化到系统托盘,则会从那里恢复。.
(此外,我希望多个应用程序能够使用完全相同的代码而不会出现奇怪的行为。)
我找到了许多能够满足前两个要求但不能满足第三个要求的解决方案。那些确实满足我第三个要求的解决方案似乎过于复杂。我真的希望我的应用程序监听一个端口或写入一个注册表键仅仅为了这个小小的功能吗?这似乎有些小题大做。
最终,我能够汇集各种来源的想法,拼凑出一个解决方案,它对我而言足够简单,但仍能满足我所有的要求。
目标 #1:阻止第二个实例打开
似乎有三种基本方法可以实现此目的:
- 使用互斥量(Mutex)
- 遍历进程列表,查看同名进程是否已在运行
- 使用 Visual Basic 的单实例应用程序系统(可以从 C# 访问)
我喜欢互斥锁(MUTEX)方法。
遍历进程似乎缓慢、笨拙且容易出错。虽然我发现的许多文章都提倡这种方法,但真正的专家似乎都认为使用互斥量更好。(有关使用 `Process.GetProcessesByName()` 问题的描述,请参阅这篇文章:被误解的互斥量。)
"Mutex" 代表互斥。简而言之,互斥量是一种程序对象,用于防止并发使用公共资源。
有几种不同的方法来实现基于互斥锁的方法。这是我最喜欢的方法:
[STAThread]
static void Main()
{
bool onlyInstance = false;
Mutex mutex = new Mutex(true, "UniqueApplicationName", out onlyInstance);
if (!onlyInstance) {
return;
}
Application.Run(new MainForm);
GC.KeepAlive(mutex);
}
此方法在文章 确保 .NET 应用程序只有一个实例运行 中讨论。
我喜欢这种方法,因为它不需要太多的代码。它没有使用 WaitOne()
方法,因为那是不必要的。
注意最后一行代码:GC.KeepAlive()
这样做的目的是保护互斥量免受垃圾回收,直到程序关闭。您需要以某种方式保护互斥量免受垃圾回收,否则程序运行一段时间后互斥量就会消失,然后用户就能够打开第二个实例。
还有其他方法可以保护互斥锁免受垃圾回收。您可以使用 static
变量作为互斥锁。或者您可以按照以下文章的建议,将互斥锁放在 "using
" 语句中:
旁注 #1:《UniqueApplicationName》的问题
使用互斥锁确实存在一个潜在的安全风险。恶意黑客可能会通过找出您的互斥锁名称并创建一个首先占用互斥锁的应用程序,从而阻止您的应用程序打开(一种“拒绝服务”攻击)。这对我来说似乎不是一个巨大的担忧,但它是可能的。此外,两个应用程序可能会意外地拥有相同的名称,或者开发人员可能会重用互斥锁代码而不更改互斥锁名称。使用 GUID 作为互斥锁名称将有助于防止此类事情发生。我附带的示例代码展示了如何做到这一点。
旁注 #2:互斥锁作用域
为了防止在同一台机器上的多个会话之间出现多个实例,请在互斥锁名称前加上 Global\(例如 mutexName = @"Global\" + myGuid;)
目标 #2:激活第一个实例
您可以使用 WinAPI 函数来激活窗口并将其带到前台。
首先,您需要获取窗口句柄。我见过几种不同的方法(例如遍历进程列表或枚举窗口)。在本文的早期版本中,我使用了 `FindWindow()` API 函数,但我后来了解到使用该函数存在缺点。
关于为什么 `FindWindow()` 是邪恶的讨论,请参阅以下文章:
下面的示例代码仍然使用 `FindWindow()`,但稍后我将展示一种更好的方法。
[DllImportAttribute ("user32.dll")]
public static extern IntPtr FindWindow (string lpClassName, string lpWindowName);
[DllImportAttribute ("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImportAttribute ("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
public static void ShowToFront(string windowName)
{
IntPtr firstInstance = FindWindow(null, windowName);
ShowWindow(firstInstance, 1);
SetForegroundWindow(firstInstance);
}
然后只需在以下代码中插入对 `ShowToFront()` 的调用
if (!onlyInstance) {
ShowToFront(applicationName);
return;
}
目标 #3。如果第一个实例已最小化到系统托盘(又称“通知区域”),则将其恢复。
这才是棘手的地方。我真正想要的是第二实例能够向第一实例发出信号,让它自己显示,而不是让第二实例使用 WinAPI 函数来抓取第一实例并强制其显示出来。
我的应用程序的一个实例如何与另一个实例通信?我找到的解决方案似乎过于复杂。一个使用了内存映射文件(一种在应用程序之间共享内存的方式)。另一个使用了远程处理(一种应用程序通过套接字通信的方式,您的标准网络客户端/服务器方式)。
然后我发现了这篇文章,C# .NET 单实例应用程序。
我不喜欢作者处理互斥锁的方式,但他使用 `PostMessage` 的想法非常出色。它需要的代码量少得多,而且比远程处理优雅得多。
使用 HWND_BROADCAST
时必须小心。如果您广播的消息代码被其他应用程序内部用于其自身目的,则可能会发生奇怪的行为。避免这种情况的一种方法是使用 RegisterWindowMessage()
。这将生成一个保证唯一的消息代码。
我们希望创建可以在多个应用程序中重复使用的代码。因此,对 RegisterWindowMessage()
的调用必须包含应用程序特定的文本。实现这一点的一种方法是将程序集 GUID 附加到消息名称。
整合
我将所有 WinAPI 相关代码都放在一个名为 WinApi
的独立类中。
static public class WinApi
{
[DllImport("user32")]
public static extern int RegisterWindowMessage(string message);
public static int RegisterWindowMessage(string format, params object[] args)
{
string message = String.Format(format, args);
return RegisterWindowMessage(message);
}
public const int HWND_BROADCAST = 0xffff;
public const int SW_SHOWNORMAL = 1;
[DllImport("user32")]
public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
[DllImportAttribute ("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImportAttribute ("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
public static void ShowToFront(IntPtr window)
{
ShowWindow(window, SW_SHOWNORMAL);
SetForegroundWindow(window);
}
}
我将所有互斥锁代码放在一个名为 SingleInstance
的独立类中。
static public class SingleInstance { public static readonly int WM_SHOWFIRSTINSTANCE = WinApi.RegisterWindowMessage("WM_SHOWFIRSTINSTANCE|{0}", ProgramInfo.AssemblyGuid); static Mutex mutex; static public bool Start() { bool onlyInstance = false; string mutexName = String.Format("Local\\{0}", ProgramInfo.AssemblyGuid); // if you want your app to be limited to a single instance // across ALL SESSIONS (multiple users & terminal services), then use the following line instead: // string mutexName = String.Format("Global\\{0}", ProgramInfo.AssemblyGuid); mutex = new Mutex(true, mutexName, out onlyInstance); return onlyInstance; } static public void ShowFirstInstance() { WinApi.PostMessage( (IntPtr)WinApi.HWND_BROADCAST, WM_SHOWFIRSTINSTANCE, IntPtr.Zero, IntPtr.Zero); } static public void Stop() { mutex.ReleaseMutex(); } }
互斥锁名称和对 `RegisterWindowMessage()` 的调用都需要一个应用程序特定的 GUID。我为此使用了一个名为 `ProgramInfo` 的独立类。`ProgramInfo.AssemblyGuid` 获取与程序集自动关联的 GUID。
static public class ProgramInfo { static public string AssemblyGuid { get { object[] attributes = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), false); if (attributes.Length == 0) { return String.Empty; } return ((System.Runtime.InteropServices.GuidAttribute)attributes[0]).Value; } } }
要使所有这些代码在您的应用程序中工作,您只需要做两件事。
首先,在你的 Main() 函数中,你必须调用 SingleInstance
中的函数... Start()
、ShowFirstInstance()
和 Stop()
。
static class Program { [STAThread] static void Main() { if (!SingleInstance.Start()) { SingleInstance.ShowFirstInstance(); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); try { MainForm mainForm = new MainForm(); Application.Run(mainForm); } catch (Exception e) { MessageBox.Show(e.Message); } SingleInstance.Stop(); } }
其次,在您的主窗体中,必须添加以下代码:
protected override void WndProc(ref Message message)
{
if (message.Msg == SingleInstance.WM_SHOWFIRSTINSTANCE) {
ShowWindow();
}
base.WndProc(ref message);
}
public void ShowWindow()
{
// Insert code here to make your form show itself.
WinApi.ShowToFront(this.Handle);
}
看到这里发生了什么吗?第二个实例不再使用 FindWindow()
来查找并显示第一个实例的窗口,而是广播一条消息,说“显示你自己”。第一个实例听到这条消息,然后显示自己。
如果您的应用程序已最小化到系统托盘,ShowWindow
是您放置恢复代码的地方。此示例代码展示了实现这一点的一种方法。
进一步
有些人需要更进一步,将命令行参数从第二个实例传递给第一个实例。如果您需要实现这一点,那么似乎您有两个选择:
- 您可以使用 Visual Basic 的单实例应用程序系统(可以从 C# 访问),如本文所述。
- 或者,您可以使用纯 C# 解决方案,该解决方案使用类似远程处理的技术在实例之间进行通信。我发现关于此的最佳文章是这篇文章。
历史
- 2009年1月29日:初始发布
- 2009 年 8 月 9 日:重大修订,去除了 `FindWindow()`