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

740
使用C#和WPF在.NET下(而不是Windows Forms或控制台),创建一个只能作为单个实例运行的应用程序的正确方法是什么?
我知道这与一种叫做互斥体的神秘东西有关,很少能找到有人愿意停下来解释这些东西。
代码还需要通知已经运行的实例,用户尝试启动第二个实例,并可能传递任何命令行参数(如果存在)。

17
当应用程序终止时,CLR会自动释放所有未释放的互斥量,不是吗? - Cocowalla
2
@Cocowalla:finalizer 应该释放未托管的互斥体,除非它无法知道互斥体是由托管应用程序创建还是附加到现有互斥体。 - Ignacio Soler Garcia
1
只是想更正我之前的陈述。传递参数给现有应用程序意味着您想要进行MDI(多文档界面)。我以为MDI是微软正在推出的一种方式(Word和Excel现在都是SDI)。但我意识到Chrome和IE都是MDI。也许我们处于MDI回归的年代???(但我仍然更喜欢SDI而不是MDI) - Eric Ouellet
13
CLR不管理本地资源。但是,如果进程终止,所有句柄都将由系统释放(而非CLR)。 - IInspectable
2
我更喜欢@huseyint的答案。它使用了微软自己的'SingleInstance.cs'类,因此您不必担心Mutexes和IntPtrs。而且,没有依赖于VisualBasic(yuk)。请参见http://codereview.stackexchange.com/questions/20871/wpf-single-instance-best-practices/25667#25667获取更多信息... - Riegardt Steyn
显示剩余2条评论
39个回答

609

这是一篇关于Mutex解决方案非常好的文章。该文章描述的方法有两个优点。

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

其次,该文章展示了如何在用户尝试启动另一个实例时将现有应用程序实例带到前台。这是其他在这里描述的Mutex解决方案没有解决的非常好的功能。


更新

截至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)。WaitOne返回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;
    }
}

9
基于此答案使用的代码和库更少,并提供了置顶功能,我将把它设为新的被接受的答案。如果有人知道使用API更正确的方法来将表单置于顶部,请自由添加。 - Nidonocu
13
@BlueRaja,您启动第一个应用程序实例。当您启动第二个应用程序实例时,它会检测到已经有另一个实例在运行,并准备关闭。在关闭之前,它会向第一个实例发送一个“SHOWME”本地消息,将第一个实例置于顶部。由于.NET中的事件不允许跨进程通信,因此使用本地消息进行通信。 - Matt Davis
9
有没有办法从另一个实例传递命令行呢? - gyurisc
26
@Nam,Mutex 构造函数只需要一个字符串,因此你可以提供任何你想要的字符串名称,例如,“这是我的 Mutex”。因为 'Mutex' 是可用于其他进程的系统对象,所以通常希望名称是唯一的,以避免与同一系统上的其他 'Mutex' 名称冲突。在这篇文章中,看起来晦涩的字符串是一个 'Guid'。你可以通过调用 System.Guid.NewGuid() 来以编程方式生成它。在本文的情况下,用户可能是通过 Visual Studio 来生成它的,如此处所示:http://msdn.microsoft.com/en-us/library/ms241442(VS.80).aspx。 - Matt Davis
7
互斥量方法是否假定相同的用户正在尝试重新启动应用程序?显然,在“切换用户”后将“现有应用程序实例带到前台”是没有意义的。 - dumbledad
显示剩余35条评论

122

你可以使用 Mutex 类,但是很快就会发现需要自己实现传递参数等代码。好吧,在我阅读 Chris Sell 的书 时,我学到了一个诀窍,用到了已经在框架中可用的逻辑。我不知道你怎么想,但当我学习到可以在框架中重复使用的东西时,通常会选择这条路,而不是重新发明轮子。除非它当然不能满足我所有的需求。

当我开始使用 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);
    }
}

希望你能理解所有内容并且能够使用这个实现方式,并将其应用于自己的项目中。


15
我会坚持使用互斥锁的解决方案,因为它与表单无关。 - Steven Sudit
3
我之前尝试了其他方法,但遇到了问题,所以使用了这种方法。我相当确定它在底层使用了远程连接。我的应用出现了两个相关的问题 - 一些客户说尽管他们告诉它不要连接外网,它还是试图联网。仔细检查后,发现连接是到本地主机(localhost)。但他们一开始并不知道这一点。此外,我不能将远程连接用于其他目的,因为它已经被用于这个目的。当我尝试互斥量方法时,我可以再次使用远程连接。 - Richard Watson
6
请原谅我,除非我漏掉了什么,否则您避免编写三行代码,而是重复使用框架,只是为了编写相当冗长的代码来完成它。那么节约在哪里? - greenoldman
3
可以在 WinForms 中完成吗? - Jack
4
如果您不在应用程序实例上调用InitializeComponent(),则无法解析资源..._application = new SingleInstanceApplication(); _application.InitializeComponent(); _application.Run(); - Nick
显示剩余5条评论

96

来源于这里

跨进程 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。


7
我必须说,我更喜欢这个回答,仅仅因为它不依赖于WinForms。就我个人而言,我的大部分开发工作都已经转向WPF,我不想为了这样的事情而引入WinForm库。 - Switters
8
当然,为了完整的回答,您还需要描述如何将参数传递给另一个实例 :) - Simon Buchan
1
@Jason,好的,谢谢!但我更喜欢不传递任何超时。这太主观了,取决于很多变量。如果您想启用另一个应用程序启动,只需更快地释放互斥锁即可...例如,一旦用户确认关闭。 - Eric Ouellet
1
@EricOuellet:几乎每个具有选项卡的程序都会这样做 - Photoshop、Sublime Text、Chrome等。如果您有一个“主”进程的充分理由(比如说您有一个用于设置的进程内数据库),您可能希望它显示UI,就好像它是一个新进程一样。 - Simon Buchan
@Simon,你说得对。我刚才只是在质疑一件非常古老的事情...... MDI vs SDI(多文档界面 vs 单文档界面)。当你谈到选项卡时,你指的是MDI。1998年,微软的一本书建议消除每个MDI应用程序。微软将Word、Excel等转换为SDI,我认为它更加简单和好用。我知道Chrome和其他浏览器(现在包括IE)又回到了MDI。我个人(基于没有根据的个人感受)仍然认为,在选择文件关联时打开一个新的应用程序会更好。但我现在更好地理解了所提出的问题。谢谢! - Eric Ouellet
显示剩余2条评论

68

MSDN实际上提供了一个C#和VB的示例应用程序,可以完全做到这一点:http://msdn.microsoft.com/en-us/library/ms771662(v=VS.90).aspx

开发单实例检测最常见和可靠的技术是使用Microsoft .NET Framework远程调用基础结构 (System.Remoting)。Microsoft .NET Framework (2.0版本)包含一种类型 WindowsFormsApplicationBase,它封装了所需的远程调用功能。要将此类型合并到 WPF 应用程序中,需要从该类型派生另一种类型,并将其用作应用程序静态入口方法 Main 和 WPF 应用程序的 Application 类型之间的桥接。桥接会检测首次启动应用程序以及尝试进行后续启动,并将控制权交给 WPF 应用程序类型以确定如何处理启动。

  • C#程序员只需要深呼吸,忘记“我不想包含VisualBasic DLL”的想法。因为这个Scott Hanselman说的话,而且这几乎是问题最干净的解决方案,由比你了解框架更多的人设计。
  • 从可用性的角度来看,如果用户正在加载一个已经打开的应用程序,并且您给他们一个像“另一个应用程序实例正在运行。再见”这样的错误消息,那么他们将不会是非常满意的用户。在 GUI 应用程序中,您必须简单地切换到该应用程序并传递提供的参数 - 或者如果命令行参数没有意义,则必须弹出可能已被最小化的应用程序。

这个框架已经支持了这个功能,只不过某个笨蛋把 DLL 命名为 Microsoft.VisualBasic 而没有把它放到 Microsoft.ApplicationUtils 或者类似的地方。接受现实吧,或者打开 Reflector 来查看。

提示:如果你按照这个方法来做,并且已经有一个包含资源等内容的 App.xaml 文件,你也需要查看这个链接


谢谢您包含“看看这个也行”的链接。那正是我需要的。顺便说一下,您链接中的第三个解决方案是最好的一个。 - Eternal21
我也主张在可能的情况下委托给框架和专门设计的库。 - Eniola

26

这段代码应该放在主方法中。关于WPF中的主方法,请参考这里了解更多信息。

[DllImport("user32.dll")]
private static extern Boolean ShowWindow(IntPtr hWnd, Int32 nCmdShow);

private const int SW_SHOWMAXIMIZED = 3;

static void Main() 
{
    Process currentProcess = Process.GetCurrentProcess();
    var runningProcess = (from process in Process.GetProcesses()
                          where
                            process.Id != currentProcess.Id &&
                            process.ProcessName.Equals(
                              currentProcess.ProcessName,
                              StringComparison.Ordinal)
                          select process).FirstOrDefault();
    if (runningProcess != null)
    {
        ShowWindow(runningProcess.MainWindowHandle, SW_SHOWMAXIMIZED);
       return; 
    }
}

方法 2

static void Main()
{
    string procName = Process.GetCurrentProcess().ProcessName;
    // get the list of all processes by that name

    Process[] processes=Process.GetProcessesByName(procName);

    if (processes.Length > 1)
    {
        MessageBox.Show(procName + " already running");  
        return;
    } 
    else
    {
        // Application.Run(...);
    }
}

注意:上述方法假定您的进程/应用程序具有唯一名称。因为它使用进程名称来查找是否存在任何现有处理器。因此,如果您的应用程序具有非常普通的名称(例如:记事本),上述方法将不起作用。


3
如果计算机上有任何其他同名程序在运行,这种方法就不起作用。ProcessName返回可执行文件名称但不包括扩展名exe。例如,如果你的应用程序叫做“记事本”,而Windows自带的记事本正在运行,则它将检测到Windows自带的记事本正在运行你的应用程序。 - Jcl
2
谢谢你的回答。我发现了很多类似的问题,但是答案总是非常复杂或者令人困惑,所以它们对我来说毫无用处。这个答案(方法#1)很直接、清晰,最重要的是它真正帮助我让我的代码运行起来了。 - ElDoRado1239

20

好的,我有一个用于此目的的一次性类(disposable Class),适用于大多数情况:

使用方法如下:

static void Main()
{
    using (SingleInstanceMutex sim = new SingleInstanceMutex())
    {
        if (sim.IsOtherInstanceRunning)
        {
            Application.Exit();
        }

        // Initialize program here.
    }
}

这就是它:

/// <summary>
/// Represents a <see cref="SingleInstanceMutex"/> class.
/// </summary>
public partial class SingleInstanceMutex : IDisposable
{
    #region Fields

    /// <summary>
    /// Indicator whether another instance of this application is running or not.
    /// </summary>
    private bool isNoOtherInstanceRunning;

    /// <summary>
    /// The <see cref="Mutex"/> used to ask for other instances of this application.
    /// </summary>
    private Mutex singleInstanceMutex = null;

    /// <summary>
    /// An indicator whether this object is beeing actively disposed or not.
    /// </summary>
    private bool disposed;

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="SingleInstanceMutex"/> class.
    /// </summary>
    public SingleInstanceMutex()
    {
        this.singleInstanceMutex = new Mutex(true, Assembly.GetCallingAssembly().FullName, out this.isNoOtherInstanceRunning);
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets an indicator whether another instance of the application is running or not.
    /// </summary>
    public bool IsOtherInstanceRunning
    {
        get
        {
            return !this.isNoOtherInstanceRunning;
        }
    }

    #endregion

    #region Methods

    /// <summary>
    /// Closes the <see cref="SingleInstanceMutex"/>.
    /// </summary>
    public void Close()
    {
        this.ThrowIfDisposed();
        this.singleInstanceMutex.Close();
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            /* Release unmanaged ressources */

            if (disposing)
            {
                /* Release managed ressources */
                this.Close();
            }

            this.disposed = true;
        }
    }

    /// <summary>
    /// Throws an exception if something is tried to be done with an already disposed object.
    /// </summary>
    /// <remarks>
    /// All public methods of the class must first call this.
    /// </remarks>
    public void ThrowIfDisposed()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException(this.GetType().Name);
        }
    }

    #endregion
}

1
这个程序很容易就能运行。只是在将Application.Exit();更改为return;之后才解决了无法关闭第二个应用程序的问题,但是除此之外一切都很好。尽管如此,我承认我会仔细研究之前使用接口的解决方案。 - hal9000

15

一个新的应用程序,它使用互斥锁和进程间通信技术(IPC),并且可以将所有命令行参数传递给正在运行的实例,名为WPF单实例应用程序


我非常成功地使用了这个。如果你将NamedPipes与此结合,你也可以将命令行参数传递给原始应用程序。这个类'SingleInstance.cs'是由微软编写的。我还添加了另一个链接到Arik Poznanski在CodeProject上更易读的博客版本。 - Riegardt Steyn
6
链接现在已经失效。 - Mike Lowery
1
尝试这个(同一日期,同一作者的名字,因此可能是同一篇文章):https://www.codeproject.com/articles/84270/wpf-single-instance-application - Daerst
请在此处找到复制品。https://gist.github.com/karthikeyan1241997/f1490a40bcb0551b1a9110cde760c128 - Karthikeyan Subramanian

13
C# .NET单实例应用程序的代码是标记答案的参考,这是一个很好的开始。
然而,我发现它不能很好地处理已经存在的实例有模态对话框打开的情况,无论该对话框是托管的(例如另一个表单,如关于框之类的),还是不受控制的(例如使用标准.NET类时的OpenFileDialog)。使用原始代码,主窗体被激活,但模态窗口保持非活动状态,这看起来很奇怪,用户必须点击它才能继续使用应用程序。
因此,我创建了一个SingleInstance实用程序类,以自动处理所有这些Winforms和WPF应用程序。 Winforms
1)像这样修改Program类:
static class Program
{
    public static readonly SingleInstance Singleton = new SingleInstance(typeof(Program).FullName);

    [STAThread]
    static void Main(string[] args)
    {
        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        Singleton.RunFirstInstance(() =>
        {
            SingleInstanceMain(args);
        });
    }

    public static void SingleInstanceMain(string[] args)
    {
        // standard code that was in Main now goes here
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

2) 将主窗口类修改如下:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    protected override void WndProc(ref Message m)
    {
        // if needed, the singleton will restore this window
        Program.Singleton.OnWndProc(this, m, true);

        // TODO: handle specific messages here if needed
        base.WndProc(ref m);
    }
}

WPF:

1) 修改应用程序页(并确保将其构建操作设置为页面,以便重新定义Main方法):

public partial class App : Application
{
    public static readonly SingleInstance Singleton = new SingleInstance(typeof(App).FullName);

    [STAThread]
    public static void Main(string[] args)
    {
        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        Singleton.RunFirstInstance(() =>
        {
            SingleInstanceMain(args);
        });
    }

    public static void SingleInstanceMain(string[] args)
    {
        // standard code that was in Main now goes here
        App app = new App();
        app.InitializeComponent();
        app.Run();
    }
}

2) 将主窗口类修改为如下:

public partial class MainWindow : Window
{
    private HwndSource _source;

    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        _source = (HwndSource)PresentationSource.FromVisual(this);
        _source.AddHook(HwndSourceHook);
    }

    protected virtual IntPtr HwndSourceHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        // if needed, the singleton will restore this window
        App.Singleton.OnWndProc(hwnd, msg, wParam, lParam, true, true);

        // TODO: handle other specific message
        return IntPtr.Zero;
    }

这里是实用类:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;

namespace SingleInstanceUtilities
{
    public sealed class SingleInstance
    {
        private const int HWND_BROADCAST = 0xFFFF;

        [DllImport("user32.dll")]
        private static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        private static extern int RegisterWindowMessage(string message);

        [DllImport("user32.dll")]
        private static extern bool SetForegroundWindow(IntPtr hWnd);

        public SingleInstance(string uniqueName)
        {
            if (uniqueName == null)
                throw new ArgumentNullException("uniqueName");

            Mutex = new Mutex(true, uniqueName);
            Message = RegisterWindowMessage("WM_" + uniqueName);
        }

        public Mutex Mutex { get; private set; }
        public int Message { get; private set; }

        public void RunFirstInstance(Action action)
        {
            RunFirstInstance(action, IntPtr.Zero, IntPtr.Zero);
        }

        // NOTE: if this always return false, close & restart Visual Studio
        // this is probably due to the vshost.exe thing
        public void RunFirstInstance(Action action, IntPtr wParam, IntPtr lParam)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            if (WaitForMutext(wParam, lParam))
            {
                try
                {
                    action();
                }
                finally
                {
                    ReleaseMutex();
                }
            }
        }

        public static void ActivateWindow(IntPtr hwnd)
        {
            if (hwnd == IntPtr.Zero)
                return;

            FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(hwnd));
        }

        public void OnWndProc(IntPtr hwnd, int m, IntPtr wParam, IntPtr lParam, bool restorePlacement, bool activate)
        {
            if (m == Message)
            {
                if (restorePlacement)
                {
                    WindowPlacement placement = WindowPlacement.GetPlacement(hwnd, false);
                    if (placement.IsValid && placement.IsMinimized)
                    {
                        const int SW_SHOWNORMAL = 1;
                        placement.ShowCmd = SW_SHOWNORMAL;
                        placement.SetPlacement(hwnd);
                    }
                }

                if (activate)
                {
                    SetForegroundWindow(hwnd);
                    FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(hwnd));
                }
            }
        }

#if WINFORMS // define this for Winforms apps
        public void OnWndProc(System.Windows.Forms.Form form, int m, IntPtr wParam, IntPtr lParam, bool activate)
        {
            if (form == null)
                throw new ArgumentNullException("form");

            if (m == Message)
            {
                if (activate)
                {
                    if (form.WindowState == System.Windows.Forms.FormWindowState.Minimized)
                    {
                        form.WindowState = System.Windows.Forms.FormWindowState.Normal;
                    }

                    form.Activate();
                    FormUtilities.ActivateWindow(FormUtilities.GetModalWindow(form.Handle));
                }
            }
        }

        public void OnWndProc(System.Windows.Forms.Form form, System.Windows.Forms.Message m, bool activate)
        {
            if (form == null)
                throw new ArgumentNullException("form");

            OnWndProc(form, m.Msg, m.WParam, m.LParam, activate);
        }
#endif

        public void ReleaseMutex()
        {
            Mutex.ReleaseMutex();
        }

        public bool WaitForMutext(bool force, IntPtr wParam, IntPtr lParam)
        {
            bool b = PrivateWaitForMutext(force);
            if (!b)
            {
                PostMessage((IntPtr)HWND_BROADCAST, Message, wParam, lParam);
            }
            return b;
        }

        public bool WaitForMutext(IntPtr wParam, IntPtr lParam)
        {
            return WaitForMutext(false, wParam, lParam);
        }

        private bool PrivateWaitForMutext(bool force)
        {
            if (force)
                return true;

            try
            {
                return Mutex.WaitOne(TimeSpan.Zero, true);
            }
            catch (AbandonedMutexException)
            {
                return true;
            }
        }
    }

    // NOTE: don't add any field or public get/set property, as this must exactly map to Windows' WINDOWPLACEMENT structure
    [StructLayout(LayoutKind.Sequential)]
    public struct WindowPlacement
    {
        public int Length { get; set; }
        public int Flags { get; set; }
        public int ShowCmd { get; set; }
        public int MinPositionX { get; set; }
        public int MinPositionY { get; set; }
        public int MaxPositionX { get; set; }
        public int MaxPositionY { get; set; }
        public int NormalPositionLeft { get; set; }
        public int NormalPositionTop { get; set; }
        public int NormalPositionRight { get; set; }
        public int NormalPositionBottom { get; set; }

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool SetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);

        private const int SW_SHOWMINIMIZED = 2;

        public bool IsMinimized
        {
            get
            {
                return ShowCmd == SW_SHOWMINIMIZED;
            }
        }

        public bool IsValid
        {
            get
            {
                return Length == Marshal.SizeOf(typeof(WindowPlacement));
            }
        }

        public void SetPlacement(IntPtr windowHandle)
        {
            SetWindowPlacement(windowHandle, ref this);
        }

        public static WindowPlacement GetPlacement(IntPtr windowHandle, bool throwOnError)
        {
            WindowPlacement placement = new WindowPlacement();
            if (windowHandle == IntPtr.Zero)
                return placement;

            placement.Length = Marshal.SizeOf(typeof(WindowPlacement));
            if (!GetWindowPlacement(windowHandle, ref placement))
            {
                if (throwOnError)
                    throw new Win32Exception(Marshal.GetLastWin32Error());

                return new WindowPlacement();
            }
            return placement;
        }
    }

    public static class FormUtilities
    {
        [DllImport("user32.dll")]
        private static extern IntPtr GetWindow(IntPtr hWnd, int uCmd);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetActiveWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("kernel32.dll")]
        public static extern int GetCurrentThreadId();

        private delegate bool EnumChildrenCallback(IntPtr hwnd, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern bool EnumThreadWindows(int dwThreadId, EnumChildrenCallback lpEnumFunc, IntPtr lParam);

        private class ModalWindowUtil
        {
            private const int GW_OWNER = 4;
            private int _maxOwnershipLevel;
            private IntPtr _maxOwnershipHandle;

            private bool EnumChildren(IntPtr hwnd, IntPtr lParam)
            {
                int level = 1;
                if (IsWindowVisible(hwnd) && IsOwned(lParam, hwnd, ref level))
                {
                    if (level > _maxOwnershipLevel)
                    {
                        _maxOwnershipHandle = hwnd;
                        _maxOwnershipLevel = level;
                    }
                }
                return true;
            }

            private static bool IsOwned(IntPtr owner, IntPtr hwnd, ref int level)
            {
                IntPtr o = GetWindow(hwnd, GW_OWNER);
                if (o == IntPtr.Zero)
                    return false;

                if (o == owner)
                    return true;

                level++;
                return IsOwned(owner, o, ref level);
            }

            public static void ActivateWindow(IntPtr hwnd)
            {
                if (hwnd != IntPtr.Zero)
                {
                    SetActiveWindow(hwnd);
                }
            }

            public static IntPtr GetModalWindow(IntPtr owner)
            {
                ModalWindowUtil util = new ModalWindowUtil();
                EnumThreadWindows(GetCurrentThreadId(), util.EnumChildren, owner);
                return util._maxOwnershipHandle; // may be IntPtr.Zero
            }
        }

        public static void ActivateWindow(IntPtr hwnd)
        {
            ModalWindowUtil.ActivateWindow(hwnd);
        }

        public static IntPtr GetModalWindow(IntPtr owner)
        {
            return ModalWindowUtil.GetModalWindow(owner);
        }
    }
}

11

以下是一个示例,它允许您拥有应用程序的单个实例。当任何新实例加载时,它们将它们的参数传递给正在运行的主实例。

public partial class App : Application
{
    private static Mutex SingleMutex;
    public static uint MessageId;

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        IntPtr Result;
        IntPtr SendOk;
        Win32.COPYDATASTRUCT CopyData;
        string[] Args;
        IntPtr CopyDataMem;
        bool AllowMultipleInstances = false;

        Args = Environment.GetCommandLineArgs();

        // TODO: Replace {00000000-0000-0000-0000-000000000000} with your application's GUID
        MessageId   = Win32.RegisterWindowMessage("{00000000-0000-0000-0000-000000000000}");
        SingleMutex = new Mutex(false, "AppName");

        if ((AllowMultipleInstances) || (!AllowMultipleInstances && SingleMutex.WaitOne(1, true)))
        {
            new Main();
        }
        else if (Args.Length > 1)
        {
            foreach (Process Proc in Process.GetProcesses())
            {
                SendOk = Win32.SendMessageTimeout(Proc.MainWindowHandle, MessageId, IntPtr.Zero, IntPtr.Zero,
                    Win32.SendMessageTimeoutFlags.SMTO_BLOCK | Win32.SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                    2000, out Result);

                if (SendOk == IntPtr.Zero)
                    continue;
                if ((uint)Result != MessageId)
                    continue;

                CopyDataMem = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Win32.COPYDATASTRUCT)));

                CopyData.dwData = IntPtr.Zero;
                CopyData.cbData = Args[1].Length*2;
                CopyData.lpData = Marshal.StringToHGlobalUni(Args[1]);

                Marshal.StructureToPtr(CopyData, CopyDataMem, false);

                Win32.SendMessageTimeout(Proc.MainWindowHandle, Win32.WM_COPYDATA, IntPtr.Zero, CopyDataMem,
                    Win32.SendMessageTimeoutFlags.SMTO_BLOCK | Win32.SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                    5000, out Result);

                Marshal.FreeHGlobal(CopyData.lpData);
                Marshal.FreeHGlobal(CopyDataMem);
            }

            Shutdown(0);
        }
    }
}

public partial class Main : Window
{
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        HwndSource Source;

        Source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
        Source.AddHook(new HwndSourceHook(Window_Proc));
    }

    private IntPtr Window_Proc(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam, ref bool Handled)
    {
        Win32.COPYDATASTRUCT CopyData;
        string Path;

        if (Msg == Win32.WM_COPYDATA)
        {
            CopyData = (Win32.COPYDATASTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32.COPYDATASTRUCT));
            Path = Marshal.PtrToStringUni(CopyData.lpData, CopyData.cbData / 2);

            if (WindowState == WindowState.Minimized)
            {
                // Restore window from tray
            }

            // Do whatever we want with information

            Activate();
            Focus();
        }

        if (Msg == App.MessageId)
        {
            Handled = true;
            return new IntPtr(App.MessageId);
        }

        return IntPtr.Zero;
    }
}

public class Win32
{
    public const uint WM_COPYDATA = 0x004A;

    public struct COPYDATASTRUCT
    {
        public IntPtr dwData;
        public int    cbData;
        public IntPtr lpData;
    }

    [Flags]
    public enum SendMessageTimeoutFlags : uint
    {
        SMTO_NORMAL             = 0x0000,
        SMTO_BLOCK              = 0x0001,
        SMTO_ABORTIFHUNG        = 0x0002,
        SMTO_NOTIMEOUTIFNOTHUNG = 0x0008
    }

    [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)]
    public static extern uint RegisterWindowMessage(string lpString);
    [DllImport("user32.dll")]
    public static extern IntPtr SendMessageTimeout(
        IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam,
        SendMessageTimeoutFlags fuFlags, uint uTimeout, out IntPtr lpdwResult);
}

这是一个非常好的例子,正是我想要做的。Nathan,所有的参数都是使用这种方法发送的吗?我的应用程序中有7个左右,我认为这段代码会起作用。 - kevp
1
在我的例子中,只发送了第一个参数,但可以更改为发送所有参数。 - Nathan Moinvaziri

11

一些想法: 有时候,要求一个应用程序只有一个实例并不像一些人所说的那样“无聊”。如果允许单个用户的多个应用程序实例访问数据库等,这将是一个数量级更高的难题(你知道,在多个应用程序实例中打开的所有记录更新,等等)。 首先,对于“名称冲突”问题,不要使用可读的名称-改用GUID,甚至最好是GUID +可读名称。名称冲突的可能性已经消失了,而Mutex则不关心。正如有人指出的那样,DOS攻击会很糟糕,但如果恶意人士费力获取Mutex名称并将其纳入其应用程序中,则您几乎是一个目标,必须采取比仅仅调整Mutex名称更多的措施来保护自己。 此外,如果使用以下变体: new Mutex(true,“某个GUID加名称”,out AIsFirstInstance),则已经具有指示器,表明Mutex是否为第一个实例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接