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

我知道它与某种叫做互斥的神秘事物有关,我很少能找到有人费心停下来解释其中一个是什么。

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


当前回答

代码c# . net单实例应用程序作为标记答案的参考是一个很好的开始。

然而,我发现它不能很好地处理已经存在的实例打开了一个模态对话框的情况,无论该对话框是托管的(像另一个表单,如about框),还是非托管的(像OpenFileDialog,甚至在使用标准的. net类时)。使用原始代码,主表单被激活,但模态表单保持不激活状态,这看起来很奇怪,而且用户必须点击它才能继续使用应用程序。

因此,我已经创建了一个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)像这样修改App页面(并确保你将其构建动作设置为page,以便能够重新定义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);
        }
    }
}

其他回答

请看下面的代码。它是防止一个WPF应用程序有多个实例的一个很好的简单解决方案。

private void Application_Startup(object sender, StartupEventArgs e)
{
    Process thisProc = Process.GetCurrentProcess();
    if (Process.GetProcessesByName(thisProc.ProcessName).Length > 1)
    {
        MessageBox.Show("Application running");
        Application.Current.Shutdown();
        return;
    }

    var wLogin = new LoginWindow();

    if (wLogin.ShowDialog() == true)
    {
        var wMain = new Main();
        wMain.WindowState = WindowState.Maximized;
        wMain.Show();
    }
    else
    {
        Application.Current.Shutdown();
    }
}

我找到了一个更简单的解决方案,与戴尔·拉根的方法类似,但稍加修改。它基于标准的Microsoft WindowsFormsApplicationBase类,几乎可以做你需要的所有事情。

首先,你创建SingleInstanceController类,你可以在所有其他使用Windows窗体的单实例应用程序中使用它:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.VisualBasic.ApplicationServices;


namespace SingleInstanceController_NET
{
    public class SingleInstanceController
    : WindowsFormsApplicationBase
    {
        public delegate Form CreateMainForm();
        public delegate void StartNextInstanceDelegate(Form mainWindow);
        CreateMainForm formCreation;
        StartNextInstanceDelegate onStartNextInstance;
        public SingleInstanceController(CreateMainForm formCreation, StartNextInstanceDelegate onStartNextInstance)
        {
            // Set whether the application is single instance
            this.formCreation = formCreation;
            this.onStartNextInstance = onStartNextInstance;
            this.IsSingleInstance = true;

            this.StartupNextInstance += new StartupNextInstanceEventHandler(this_StartupNextInstance);                      
        }

        void this_StartupNextInstance(object sender, StartupNextInstanceEventArgs e)
        {
            if (onStartNextInstance != null)
            {
                onStartNextInstance(this.MainForm); // This code will be executed when the user tries to start the running program again,
                                                    // for example, by clicking on the exe file.
            }                                       // This code can determine how to re-activate the existing main window of the running application.
        }

        protected override void OnCreateMainForm()
        {
            // Instantiate your main application form
            this.MainForm = formCreation();
        }

        public void Run()
        {
            string[] commandLine = new string[0];
            base.Run(commandLine);
        }
    }
}

然后你可以在你的程序中使用它,如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using SingleInstanceController_NET;

namespace SingleInstance
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        static Form CreateForm()
        {
            return new Form1(); // Form1 is used for the main window.
        }

        static void OnStartNextInstance(Form mainWindow) // When the user tries to restart the application again,
                                                         // the main window is activated again.
        {
            mainWindow.WindowState = FormWindowState.Maximized;
        }
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);            
            SingleInstanceController controller = new SingleInstanceController(CreateForm, OnStartNextInstance);
            controller.Run();         
        }
    }
}

程序和SingleInstanceController_NET解决方案都应该引用Microsoft。VisualBasic。如果您只是想在用户试图重新启动正在运行的程序时重新激活正在运行的应用程序作为一个正常窗口,那么SingleInstanceController中的第二个参数可以为空。在给定的示例中,窗口是最大化的。

一个节省时间的c# Winforms解决方案…

Program.cs:

using System;
using System.Windows.Forms;
// needs reference to Microsoft.VisualBasic
using Microsoft.VisualBasic.ApplicationServices;  

namespace YourNamespace
{
    public class SingleInstanceController : WindowsFormsApplicationBase
    {
        public SingleInstanceController()
        {
            this.IsSingleInstance = true;
        }

        protected override void OnStartupNextInstance(StartupNextInstanceEventArgs e)
        {
            e.BringToForeground = true;
            base.OnStartupNextInstance(e);
        }

        protected override void OnCreateMainForm()
        {
            this.MainForm = new Form1();
        }
    }

    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            string[] args = Environment.GetCommandLineArgs();
            SingleInstanceController controller = new SingleInstanceController();
            controller.Run(args);
        }
    }
}

基于Matt Davis的答案,为方便起见包装成一个类。

public static class SingleAppInstanceChecker
{
    /// <summary>
    /// Arbitrary unique string
    /// </summary>
    private static Mutex _mutex = new Mutex(true, "0d12ad74-026f-40c3-bdae-e178ddee8602");

    public static bool IsNotRunning()
    {
        return _mutex.WaitOne(TimeSpan.Zero, true);
    }
}

使用示例:

private void Application_Startup(object sender, StartupEventArgs e)
{
    if (!SingleAppInstanceChecker.IsNotRunning())
    {
        MessageBox.Show("Application is already running.");

        // Exit application using:
        // Environment.Exit(1);
        // Application.Current.Shutdown();
        // Etc...

        return;
    }
    
    // Allow startup and continue with normal processing
    // ...
}

基于命名互斥的方法不是跨平台的,因为命名互斥在Mono中不是全局的。基于进程枚举的方法没有任何同步,可能会导致不正确的行为(例如,同时启动的多个进程可能都根据时间自行终止)。在控制台应用程序中不需要基于windows系统的方法。这个解决方案建立在Divin的答案之上,解决了所有这些问题:

using System;
using System.IO;

namespace TestCs
{
    public class Program
    {
        // The app id must be unique. Generate a new guid for your application. 
        public static string AppId = "01234567-89ab-cdef-0123-456789abcdef";

        // The stream is stored globally to ensure that it won't be disposed before the application terminates.
        public static FileStream UniqueInstanceStream;

        public static int Main(string[] args)
        {
            EnsureUniqueInstance();

            // Your code here.

            return 0;
        }

        private static void EnsureUniqueInstance()
        {
            // Note: If you want the check to be per-user, use Environment.SpecialFolder.ApplicationData instead.
            string lockDir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                "UniqueInstanceApps");
            string lockPath = Path.Combine(lockDir, $"{AppId}.unique");

            Directory.CreateDirectory(lockDir);

            try
            {
                // Create the file with exclusive write access. If this fails, then another process is executing.
                UniqueInstanceStream = File.Open(lockPath, FileMode.Create, FileAccess.Write, FileShare.None);

                // Although only the line above should be sufficient, when debugging with a vshost on Visual Studio
                // (that acts as a proxy), the IO exception isn't passed to the application before a Write is executed.
                UniqueInstanceStream.Write(new byte[] { 0 }, 0, 1);
                UniqueInstanceStream.Flush();
            }
            catch
            {
                throw new Exception("Another instance of the application is already running.");
            }
        }
    }
}