简介

通常来说,应用程序可以启动任意数目的实例,前提是你的电脑内存足够大,想启动多少都可以。但有时候我们只希望程序同时只有一个实例在运行,应用程序不会重复运行。这就是应用程序的单例模式。

实现(WPF)

当然,实现的方法不是唯一的。

可以在程序启动的时候查找应用程序的窗口名字,如果查找的结果不为空,就说明已经有一个实例正在运行,但如果你的程序碰巧和其它程序的窗口名字重复就不好说了。

也可以在程序启动时获取应用程序进程的名字,然后再查找系统所有进程,看是否有重复的,原理和上一种类似。

这里要讲的并不是上面两种,而是通过Mutex来实现,使用比前两种更加简单和有效。

System.Threading.Mutex官方说明是可用于进程间同步的同步基元。

使用起来也很简单,重写应用程序的OnStartup方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// App.xaml.cs
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        Mutex mutex = new Mutex(true, "MutexName", out bool createNew);

        if (createNew)
        {
            base.OnStartup(e);
        }
        else
        {
            MessageBox.Show("程序已在运行中。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
            Application.Current.Shutdown();
        }
    }
}

注意Mutex的第2个参数,可以是任意字符串,越复杂越好,避免和其它程序冲突。 如果createNewfalse,就说明已经有一个实例正在运行了,直接退出当前程序。

注意:必须保证Mutex在程序运行过程中不被垃圾回收,否则就失效了。

上面的写法是可以的,但如果我们使用的是Caliburn.Micro或者Stylet等框架,程序启动时都是通过Bootstrapper的,在Bootstrapper中的写法会有一些不同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 使用 Stylet
public class Bootstrapper : Bootstrapper<ShellViewModel>
{
    /// <summary>
    /// 必须定义在类内部,一旦被释放就无效了
    /// </summary>
    private Mutex mutex;

    protected override void OnStart()
    {
        mutex = new Mutex(true, MUTEX_NAME, out bool createNew);
        if (!createNew)
        {
            MessageBox.Show("程序已在运行中。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);

            // 退出当前应用程序
            // 尽量不要使用
            // Application.Shutdown();
            // 因为在这里使用会触发主窗口的Closing事件
            System.Environment.Exit(0);
        }

        base.OnStart();
    }
}

需要注意的是要将Mutex定义在类的内部,如果定义在OnStartup函数体内,那么在程序运行时它就失效了,就不能使程序以单例模式运行了。

如果你想要写在其它地方,可以将Mutex定义为static,这样就可以保证它在应用程序运行过程中不会失效了。

还有就是在这里尽量使用System.Environment.Exit(0)而不是Application.Current.Shutdown(),因为我发现这里会触发主窗口的Closing事件,如果你不希望触发它,那么就使用Environment.Exit(0)

补充

关于应用程序单例模式还可以通过WindowsFormsApplicationBase来实现,但我也没试过。使用Mutex已经足够满足要求了。

最后再加一项功能,我想要在当前程序检测到已经存在正在运行的实例时,退出并激活已经存在的程序窗口。

这一点可以通过Windows的API来实现,思路就是查找应用程序的主窗口,并将其激活。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[DllImport("user32.dll", EntryPoint = "FindWindow")]
public static extern IntPtr FindWindow(string classname, string windowname);

[DllImport("user32.dll", EntryPoint = "ShowWindow")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

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

public const int SW_NORMAL = 1;
public const int SW_RESTORE = 9;

public static void FindWindowAndActive(string classname, string windowname)
{
    IntPtr hWnd = FindWindow(classname, windowname);
    ShowWindow(hWnd, SW_NORMAL);
    SetForegroundWindow(hWnd);
}

在应用程序退出之前调用FindWindowAndActive函数即可。

1
2
3
4
...
FindWindowAndActive(null, "Main Window");  // 激活已经存在的实例窗口
System.Environment.Exit(0);  // 退出当前应用程序
...