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

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

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


当前回答

下面是我使用的一个轻量级解决方案,它允许应用程序将一个已经存在的窗口带到前台,而无需求助于自定义窗口消息或盲目地搜索进程名。

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

static readonly string guid = "<Application Guid>";

static void Main()
{
    Mutex mutex = null;
    if (!CreateMutex(out mutex))
        return;

    // Application startup code.

    Environment.SetEnvironmentVariable(guid, null, EnvironmentVariableTarget.User);
}

static bool CreateMutex(out Mutex mutex)
{
    bool createdNew = false;
    mutex = new Mutex(false, guid, out createdNew);

    if (createdNew)
    {
        Process process = Process.GetCurrentProcess();
        string value = process.Id.ToString();

        Environment.SetEnvironmentVariable(guid, value, EnvironmentVariableTarget.User);
    }
    else
    {
        string value = Environment.GetEnvironmentVariable(guid, EnvironmentVariableTarget.User);
        Process process = null;
        int processId = -1;

        if (int.TryParse(value, out processId))
            process = Process.GetProcessById(processId);

        if (process == null || !SetForegroundWindow(process.MainWindowHandle))
            MessageBox.Show("Unable to start application. An instance of this application is already running.");
    }

    return createdNew;
}

编辑:你也可以静态地存储和初始化互斥量和createdNew,但是一旦你完成了它,你需要显式地释放/释放互斥量。就我个人而言,我更喜欢将互斥锁保持在本地,因为即使应用程序在未到达Main结束时就关闭,它也会被自动销毁。

其他回答

基于命名互斥的方法不是跨平台的,因为命名互斥在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.");
            }
        }
    }
}

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

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);
}

请看下面的代码。它是防止一个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();
    }
}

使用互斥量解决方案:

using System;
using System.Windows.Forms;
using System.Threading;

namespace OneAndOnlyOne
{
static class Program
{
    static String _mutexID = " // generate guid"
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        Boolean _isNotRunning;
        using (Mutex _mutex = new Mutex(true, _mutexID, out _isNotRunning))
        {
            if (_isNotRunning)
            {
                Application.Run(new Form1());
            }
            else
            {
                MessageBox.Show("An instance is already running.");
                return;
            }
        }
    }
}
}

通常,这是我用于单实例Windows窗体应用程序的代码:

[STAThread]
public static void Main()
{
    String assemblyName = Assembly.GetExecutingAssembly().GetName().Name;

    using (Mutex mutex = new Mutex(false, assemblyName))
    {
        if (!mutex.WaitOne(0, false))
        {
            Boolean shownProcess = false;
            Process currentProcess = Process.GetCurrentProcess();

            foreach (Process process in Process.GetProcessesByName(currentProcess.ProcessName))
            {
                if (!process.Id.Equals(currentProcess.Id) && process.MainModule.FileName.Equals(currentProcess.MainModule.FileName) && !process.MainWindowHandle.Equals(IntPtr.Zero))
                {
                    IntPtr windowHandle = process.MainWindowHandle;

                    if (NativeMethods.IsIconic(windowHandle))
                        NativeMethods.ShowWindow(windowHandle, ShowWindowCommand.Restore);

                    NativeMethods.SetForegroundWindow(windowHandle);

                    shownProcess = true;
                }
            }

            if (!shownProcess)
                MessageBox.Show(String.Format(CultureInfo.CurrentCulture, "An instance of {0} is already running!", assemblyName), assemblyName, MessageBoxButtons.OK, MessageBoxIcon.Asterisk, MessageBoxDefaultButton.Button1, (MessageBoxOptions)0);
        }
        else
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form());
        }
    }
}

本地组件的位置:

[DllImport("User32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern Boolean IsIconic([In] IntPtr windowHandle);

[DllImport("User32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern Boolean SetForegroundWindow([In] IntPtr windowHandle);

[DllImport("User32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern Boolean ShowWindow([In] IntPtr windowHandle, [In] ShowWindowCommand command);

public enum ShowWindowCommand : int
{
    Hide                   = 0x0,
    ShowNormal             = 0x1,
    ShowMinimized          = 0x2,
    ShowMaximized          = 0x3,
    ShowNormalNotActive    = 0x4,
    Minimize               = 0x6,
    ShowMinimizedNotActive = 0x7,
    ShowCurrentNotActive   = 0x8,
    Restore                = 0x9,
    ShowDefault            = 0xA,
    ForceMinimize          = 0xB
}