在 .NET(而不是Windows 窗体或控制台)下使用 C# 和 WPF,创建只能作为单个实例运行的应用程序的正确方法是什么?
我知道这与一些称为互斥锁的神话有关,我很少能找到有人愿意停下来解释其中一个是什么。
代码还需要通知已经运行的实例用户尝试启动第二个实例,并且还可能传递任何命令行参数(如果存在)。
这是一篇关于 Mutex 解决方案的非常好的文章。这篇文章描述的方法是有利的,原因有两个。
首先,它不需要依赖于 Microsoft.VisualBasic 程序集。如果我的项目已经依赖于该程序集。但事实上,我不使用Microsoft.VisualBasic 程序集,我宁愿不向我的项目添加不必要的依赖项。
其次,本文展示了当用户尝试启动另一个实例时如何将应用程序的现有实例带到前台。这是这里描述的其他 Mutex 解决方案没有解决的非常好的方法。
截至 2014 年 8 月 1 日,我上面链接的文章仍然有效,但博客已经有一段时间没有更新了。这让我担心它最终可能会消失,随之而来的是提倡的解决方案。我在这里复制文章的内容以供后代使用。这些话完全属于Sanity Free Coding的博客所有者。
今天我想重构一些禁止我的应用程序运行多个自身实例的代码。 以前我使用System.Diagnostics.Process在进程列表中搜索 myapp.exe 的实例。虽然这可行,但它会带来很多开销,我想要一些更清洁的东西。 知道我可以为此使用互斥锁(但以前从未这样做过),我着手削减我的代码并简化我的生活。 在我的应用程序主类中,我创建了一个名为Mutex的静态对象:
今天我想重构一些禁止我的应用程序运行多个自身实例的代码。
以前我使用System.Diagnostics.Process在进程列表中搜索 myapp.exe 的实例。虽然这可行,但它会带来很多开销,我想要一些更清洁的东西。
知道我可以为此使用互斥锁(但以前从未这样做过),我着手削减我的代码并简化我的生活。
在我的应用程序主类中,我创建了一个名为Mutex的静态对象:
static class Program { static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}"); [STAThread] ... }
拥有一个命名的互斥体允许我们在多个线程和进程之间进行堆栈同步,这正是我正在寻找的魔法。 Mutex.WaitOne有一个重载,它指定我们等待的时间量。因为我们实际上并不想同步我们的代码(更多只是检查它是否正在使用),我们使用带有两个参数的重载:Mutex.WaitOne(Timespan timeout, bool exitContext)。如果可以进入,则等待一个返回 true,否则返回 false。在这种情况下,我们根本不想等待;如果我们的互斥体正在被使用,跳过它,然后继续,所以我们传入 TimeSpan.Zero(等待 0 毫秒),并将 exitContext 设置为 true,这样我们就可以在尝试获取锁之前退出同步上下文。使用它,我们将 Application.Run 代码包装在如下内容中:
拥有一个命名的互斥体允许我们在多个线程和进程之间进行堆栈同步,这正是我正在寻找的魔法。
Mutex.WaitOne有一个重载,它指定我们等待的时间量。因为我们实际上并不想同步我们的代码(更多只是检查它是否正在使用),我们使用带有两个参数的重载:Mutex.WaitOne(Timespan timeout, bool exitContext)。如果可以进入,则等待一个返回 true,否则返回 false。在这种情况下,我们根本不想等待;如果我们的互斥体正在被使用,跳过它,然后继续,所以我们传入 TimeSpan.Zero(等待 0 毫秒),并将 exitContext 设置为 true,这样我们就可以在尝试获取锁之前退出同步上下文。使用它,我们将 Application.Run 代码包装在如下内容中:
static class Program { static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}"); [STAThread] static void Main() { if(mutex.WaitOne(TimeSpan.Zero, true)) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); mutex.ReleaseMutex(); } else { MessageBox.Show("only one instance at a time"); } } }
因此,如果我们的应用程序正在运行,WaitOne 将返回 false,并且我们将收到一个消息框。 我没有显示消息框,而是选择使用一个小的 Win32 来通知我正在运行的实例有人忘记了它已经在运行(通过将其自身置于所有其他窗口的顶部)。为了实现这一点,我使用PostMessage向每个窗口广播一条自定义消息(自定义消息是由我正在运行的应用程序向RegisterWindowMessage 注册 的,这意味着只有我的应用程序知道它是什么)然后我的第二个实例退出。正在运行的应用程序实例将接收该通知并对其进行处理。为了做到这一点,我在主窗体中覆盖了WndProc并监听了我的自定义通知。当我收到该通知时,我将表单的 TopMost 属性设置为 true 以将其置于顶部。 这是我最终得到的结果: 程序.cs
因此,如果我们的应用程序正在运行,WaitOne 将返回 false,并且我们将收到一个消息框。
我没有显示消息框,而是选择使用一个小的 Win32 来通知我正在运行的实例有人忘记了它已经在运行(通过将其自身置于所有其他窗口的顶部)。为了实现这一点,我使用PostMessage向每个窗口广播一条自定义消息(自定义消息是由我正在运行的应用程序向RegisterWindowMessage 注册 的,这意味着只有我的应用程序知道它是什么)然后我的第二个实例退出。正在运行的应用程序实例将接收该通知并对其进行处理。为了做到这一点,我在主窗体中覆盖了WndProc并监听了我的自定义通知。当我收到该通知时,我将表单的 TopMost 属性设置为 true 以将其置于顶部。
这是我最终得到的结果:
static class Program { static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}"); [STAThread] static void Main() { if(mutex.WaitOne(TimeSpan.Zero, true)) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); mutex.ReleaseMutex(); } else { // send our Win32 message to make the currently running instance // jump on top of all the other windows NativeMethods.PostMessage( (IntPtr)NativeMethods.HWND_BROADCAST, NativeMethods.WM_SHOWME, IntPtr.Zero, IntPtr.Zero); } } }
NativeMethods.cs
// this class just wraps some Win32 stuff that we're going to use internal class NativeMethods { public const int HWND_BROADCAST = 0xffff; public static readonly int WM_SHOWME = RegisterWindowMessage("WM_SHOWME"); [DllImport("user32")] public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam); [DllImport("user32")] public static extern int RegisterWindowMessage(string message); }
Form1.cs(正面部分)
public partial class Form1 : Form { public Form1() { InitializeComponent(); } protected override void WndProc(ref Message m) { if(m.Msg == NativeMethods.WM_SHOWME) { ShowMe(); } base.WndProc(ref m); } private void ShowMe() { if(WindowState == FormWindowState.Minimized) { WindowState = FormWindowState.Normal; } // get our current "TopMost" value (ours will always be false though) bool top = TopMost; // make our form jump to the top of everything TopMost = true; // set it back to whatever it was TopMost = top; } }