不少应用程序有单一实例的需求,也就是同时只能开启一个实例(一般也就是一个进程)。
实现的方式可能有判断进程名字,使用特殊文件等等,但是最靠谱的方式还是使用系统提供的 Mutex 工具。
Mutex是互斥体,命名的互斥体可以跨进程使用,所以可以用以实现程序单一实例这个需求。相关的例子网上应该不少,不过很多给出的例子中并没有注意到一些细节,这里就完整总结下。
Mutex 需要一个名字,这个名字需要唯一,一般的方式是使用一个固定的 GUID 作为名字。
对于 .NET 应用,可以通过 Assembly 上的GuidAttribute来获取。默认情况下建立工程的时候 VS 就会生成一个 GUID 给 Assembly,这样无需自己再生成一个 GUID 来使用。
另外,为了调试方面,最好给 GUID 加一个便于人识别的前缀,一般就是程序的名字。这样使用一些查看系统对象的工具时,可以方便找到这个 Mutex。
一般在程序启动的代码中进行判断,判断的方式是使用 Mutex 上的WaitOne方法。但是有两点需要注意:
- 程序异常退出,WaitOne 会抛出
AbandonedMutexException
异常,需要处理。 - 如果程序使用了
Application.Restart
来重新启动,就需要 WaitOne 等待更长的时间。这是因为Application.Restart
会在程序退出前启动新程序实例,需要等待原程序完全退出释放 Mutex。
简单一点也可以使用Mutex的构造函数来判断
// // 摘要: // 使用可指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称的 Boolean 值和当线程返回时可指示调用线程是否已赋予互斥体的初始所有权的 // Boolean 值初始化 System.Threading.Mutex 类的新实例。 // // 参数: // initiallyOwned: // 如果为 true,则给予调用线程已命名的系统互斥体的初始所属权(如果已命名的系统互斥体是通过此调用创建的);否则为 false。 // // name: // System.Threading.Mutex 的名称。如果值为 null,则 System.Threading.Mutex 是未命名的。 // // createdNew: // 在此方法返回时,如果创建了局部互斥体(即,如果 name 为 null 或空字符串)或指定的命名系统互斥体,则包含布尔值 true;如果指定的命名系统互斥体已存在,则为 // false。此参数未经初始化即被传递。 // // 异常:。。。。 public Mutex(bool initiallyOwned, string name, out bool createdNew);
createdNew参数为true
则可以正常启动,否则程序已在运行。
有些场景下,如果应用已在运行,用户再启动应用时,需要将已在运行的应用显示给用户。如果应用已经有自己的进程间通讯方式,那就可以直接利用,如果没有,则可以使用 Windows 系统的消息广播。
还有些场景下,需要将程序参数传递给已在运行的应用,也可以使用Windows系统的消息。
综上,将上述功能封装在XMutex类型中,如下,
/// <summary> /// 互斥体辅助类型 /// </summary> static class XMutex { /// <summary> /// 拷贝数据结构 /// </summary> public struct CopyDataStruct { public IntPtr dwData; public int cbData; public IntPtr lpData; } private static Mutex _mutex; private static int _showMeMessage; /// <summary> /// 运行程序, /// 如果互斥体已经有另一个实例在运行,就展示该实例,否则运行新实例 /// </summary> /// <param name="run">运行实例的方法</param> /// <param name="args">入口参数</param> public static void Run(Action run, string[] args) { //取应用程序所在的程序集的Guid,本例中应该是Program程序所在的程序集的Guid var guidAttr = typeof(Program).Assembly.GetCustomAttribute<GuidAttribute>(); //使用该Guid拼接一个字符串作为互斥体的名称 var key = string.Format("XMutex-{0}", guidAttr.Value); bool flag; //尝试创建互斥体 _mutex = new Mutex(true, key, out flag); //注册消息代码 _showMeMessage = RegisterWindowMessage(key); if (flag) { //互斥体创建成功,运行程序 run(); } else { //互斥体已经存在,获取该实例的窗口句柄 IntPtr intPtr = GetRunning(); //发送消息到目标实例 PostMessage(intPtr, _showMeMessage, IntPtr.Zero, IntPtr.Zero); //如果有入口参数,发送参数消息 if (args.Length > 0) { SendCopyData(intPtr, args); } } } /// <summary> /// 获取已运行实例的主窗口句柄 /// </summary> /// <returns></returns> private static IntPtr GetRunning() { //当前进程名称 string procName = Process.GetCurrentProcess().ProcessName; //和当前进程名称相同的所有进程 Process[] processes = Process.GetProcessesByName(procName); if (processes.Length > 0) { //取获取到的进程的第一个主窗口句柄 return processes.FirstOrDefault(x => x.MainWindowHandle != IntPtr.Zero)?.MainWindowHandle ?? IntPtr.Zero; } return IntPtr.Zero; } /// <summary> /// 释放互斥体 /// </summary> public static void Release() { _mutex.ReleaseMutex(); } /// <summary> /// 拷贝数据消息代码 /// </summary> public const int WM_COPYDATA = 74; #region API [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool IsZoomed(IntPtr hWnd); [DllImport("User32.dll")] private static extern bool ShowWindowAsync(IntPtr hWnd, int cmdShow); [DllImport("user32.dll")] private static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, ref CopyDataStruct lParam); //[DllImport("user32.dll", EntryPoint = "SendMessage")] //private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern int RegisterWindowMessage(string message); #endregion public static bool WndProc(Form form, Action<string[]> argsAction, ref Message m) { if (m.Msg == _showMeMessage) { var hwnd = form.Handle; if (IsIconic(hwnd)) {//取消最小化 ShowWindowAsync(hwnd, 9); } //else if (IsZoomed(hwnd)) //{//最大化 // ShowWindowAsync(hwnd, 3); //} //else //{//普通大小 // ShowWindowAsync(hwnd, 1); //} //取消隐藏 if (!form.Visible) { form.Show(); } //将窗口移到最顶层 var top = form.TopMost; form.TopMost = true; form.TopMost = top; //激活窗口并获取焦点 form.Activate(); m.Result = IntPtr.Zero; return true; } if (m.Msg == WM_COPYDATA) { //接收参数消息 CopyDataStruct copyDataStruct = (CopyDataStruct)Marshal.PtrToStructure(m.LParam, typeof(CopyDataStruct)); int num = copyDataStruct.dwData.ToInt32(); if (num == 1025) { byte[] array = new byte[copyDataStruct.cbData]; Marshal.Copy(copyDataStruct.lpData, array, 0, copyDataStruct.cbData); string @string = Encoding.UTF8.GetString(array); var args = @string.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(x => x.Substring(1, x.Length - 2)).ToArray(); //处理参数 argsAction?.Invoke(args); } m.Result = IntPtr.Zero; return true; } return false; } /// <summary> /// 发送参数消息 /// </summary> /// <param name="hWnd"></param> /// <param name="args"></param> /// <returns></returns> private static int SendCopyData(IntPtr hWnd, string[] args) { string data = string.Join(" ", args.Select(x => string.Format("'{0}'", x))); byte[] bytes = Encoding.UTF8.GetBytes(data); CopyDataStruct copyDataStruct = new CopyDataStruct { dwData = (IntPtr)1025, cbData = bytes.Length, lpData = Marshal.AllocHGlobal(bytes.Length) }; Marshal.Copy(bytes, 0, copyDataStruct.lpData, bytes.Length); IntPtr intPtr = Marshal.AllocHGlobal(Marshal.SizeOf(copyDataStruct)); Marshal.StructureToPtr(copyDataStruct, intPtr, true); try { return SendMessage(hWnd, WM_COPYDATA, (IntPtr)1025, ref copyDataStruct); } finally { Marshal.FreeHGlobal(copyDataStruct.lpData); Marshal.DestroyStructure(intPtr, typeof(CopyDataStruct)); Marshal.FreeHGlobal(intPtr); } } }
程序入口点的启动方法修改为:
/// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main(string[] args) { XMutex.Run(() => { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); //程序结束时释放Mutex XMutex.Release(); }, args); }
Form1类型部分代码如下
public partial class Form1 : Form { public Form1() { InitializeComponent(); } /// <summary> /// 处理新参数的方法 /// </summary> /// <param name="args"></param> private void LoadArgs(string[] args) { MessageBox.Show(string.Join(" ", args)); } /// <summary> /// windows消息处理 /// </summary> /// <param name="m"></param> protected override void WndProc(ref Message m) { if (XMutex.WndProc(this, LoadArgs, ref m)) return; base.WndProc(ref m); } }
以上代码是以Winform为例,将代码稍作修改也可以应用在WPF中。
本解决方案中有一个遗留问题,就是GetRunning方法,目前实现方案是通过进程名称找到同名进程,再找到进程的主窗口句柄,这就有问题了,如果存在相同名称的进程,后面的消息发送可能就不会产生预期的效果了,当然也可以对所有同名进程的主窗口句柄广播消息。
理想的处理方法应该是通过互斥体的实例获取句柄,但是我不知道怎么实现,谁有方案请指教。