协慌网

登录 贡献 社区

创建单实例 WPF 应用程序的正确方法是什么?

在. NET(而不是 Windows Forms或控制台)下使用 C#和 WPF,创建只能作为单个实例运行的应用程序的正确方法是什么?

我知道它与某种称为互斥量的神话事物有关,我很少能找到一个烦人的人来阻止并解释其中的一个。

该代码还需要通知已经运行的实例用户试图启动第二个实例,并且还可能传递任何命令行参数(如果存在)。

答案

这是有关 Mutex 解决方案的非常好的文章。该文章描述的方法有两个方面的优势。

首先,它不需要依赖于 Microsoft.VisualBasic 程序集。如果我的项目已经依赖于该程序集,那么我可能会提倡使用另一个答案中所示的方法。但实际上,我不使用 Microsoft.VisualBasic 程序集,而我不想在项目中添加不必要的依赖项。

其次,本文介绍了当用户尝试启动另一个实例时如何将应用程序的现有实例置于前台。这是这里描述的其他互斥解决方案无法解决的一个很好的方面。


更新

截至 2014 年 8 月 1 日,我上面链接的文章仍处于活动状态,但该博客已有一段时间没有更新。这使我担心,最终它可能会消失,并且随之而来的是所倡导的解决方案。我在此转载本文的内容,以供后代参考。这些单词完全属于Sanity Free Coding 中的博客所有者。

今天,我想重构一些代码,这些代码禁止我的应用程序运行其自身的多个实例。

以前,我曾使用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 代码包装在如下代码中:

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,以将其置于顶部。

这是我最终得到的结果:

  • Program.cs
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;
    }
}

您可以使用 Mutex 类,但是很快您将发现需要实现代码以自己传递参数。好吧,当我阅读 Chris Sell 的书时,我在 WinForms 中进行编程时学到了一个技巧。这个技巧使用了框架中已经可用的逻辑。我不了解您,但是当我了解到可以在框架中重用的内容时,通常这就是我所采取的方法,而不是重新发明轮子。除非它当然不能满足我的所有需求。

当我进入 WPF 时,我想出了一种在 WPF 应用程序中使用相同代码的方法。该解决方案应根据您的问题满足您的需求。

首先,我们需要创建我们的应用程序类。在此类中,我们将重写 OnStartup 事件,并创建一个称为 Activate 的方法,该方法将在以后使用。

public class SingleInstanceApplication : System.Windows.Application
{
    protected override void OnStartup(System.Windows.StartupEventArgs e)
    {
        // Call the OnStartup event on our base class
        base.OnStartup(e);

        // Create our MainWindow and show it
        MainWindow window = new MainWindow();
        window.Show();
    }

    public void Activate()
    {
        // Reactivate the main window
        MainWindow.Activate();
    }
}

其次,我们将需要创建一个可以管理实例的类。在进行此操作之前,我们实际上将重用 Microsoft.VisualBasic 程序集中的某些代码。由于在此示例中使用的是 C#,因此必须引用该程序集。如果您使用的是 VB.NET,则无需执行任何操作。我们将使用的类是 WindowsFormsApplicationBase,并继承其实例管理器,然后利用属性和事件来处理单个实例。

public class SingleInstanceManager : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase
{
    private SingleInstanceApplication _application;
    private System.Collections.ObjectModel.ReadOnlyCollection<string> _commandLine;

    public SingleInstanceManager()
    {
        IsSingleInstance = true;
    }

    protected override bool OnStartup(Microsoft.VisualBasic.ApplicationServices.StartupEventArgs eventArgs)
    {
        // First time _application is launched
        _commandLine = eventArgs.CommandLine;
        _application = new SingleInstanceApplication();
        _application.Run();
        return false;
    }

    protected override void OnStartupNextInstance(StartupNextInstanceEventArgs eventArgs)
    {
        // Subsequent launches
        base.OnStartupNextInstance(eventArgs);
        _commandLine = eventArgs.CommandLine;
        _application.Activate();
    }
}

基本上,我们使用 VB 位来检测单个实例并进行相应处理。当第一个实例加载时,将触发 OnStartup。再次重新运行该应用程序时,将触发 OnStartupNextInstance。如您所见,我可以了解事件参数在命令行中传递的内容。我将值设置为实例字段。您可以在此处解析命令行,也可以通过构造函数和对 Activate 方法的调用将其传递给应用程序。

第三,是时候创建我们的 EntryPoint 了。无需像通常那样更新应用程序,我们将利用 SingleInstanceManager。

public class EntryPoint
{
    [STAThread]
    public static void Main(string[] args)
    {
        SingleInstanceManager manager = new SingleInstanceManager();
        manager.Run(args);
    }
}

好吧,我希望您能够了解所有内容并能够使用此实现并将其实现为自己的实现。

这里

跨进程 Mutex 的常见用法是确保一次只能运行程序实例。这是完成的过程:

class OneAtATimePlease {

  // Use a name unique to the application (eg include your company URL)
  static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo");

  static void Main()
  {
    // Wait 5 seconds if contended – in case another instance
    // of the program is in the process of shutting down.
    if (!mutex.WaitOne(TimeSpan.FromSeconds (5), false))
    {
        Console.WriteLine("Another instance of the app is running. Bye!");
        return;
    }

    try
    {    
        Console.WriteLine("Running - press Enter to exit");
        Console.ReadLine();
    }
    finally
    {
        mutex.ReleaseMutex();
    }    
  }    
}

Mutex 的一个好功能是,如果应用程序在不首先调用 ReleaseMutex 的情况下终止,则 CLR 将自动释放 Mutex。