Winform 是一个很容易上手的C# 应用模式,但是他和MFC一样也没有帮我们实现EXE单例模式,所以我们必须自己手敲代码,但是你懂的C#没提供很多好用的API,所以得处处从C++里导过来,我先讲网上大家流传的两种方式,最后讲讲我个人思考的一种比较完美手法,未经项目实战,但是测试稳定先卖个关子,耐心往下看。
方式一:利用System.Thread.Mutex的一个重载构造函数
//
// 摘要:
// 使用一个指示调用线程是否应拥有互斥体的初始所属权的布尔值、一个作为互斥体名称的字符串,以及一个在方法返回时指示调用线程是否被授予互斥体的初始所属权的布尔值来初始化
// System.Threading.Mutex 类的新实例。
//
// 参数:
// initiallyOwned:
// 如果为 true,则给予调用线程已命名的系统互斥体的初始所属权(如果已命名的系统互斥体是通过此调用创建的);否则为 false。
//
// name:
// System.Threading.Mutex 的名称。如果值为 null,则 System.Threading.Mutex 是未命名的。
//
// createdNew:
// 在此方法返回时,如果创建了局部互斥体(即,如果 name 为 null 或空字符串)或指定的命名系统互斥体,则包含布尔值;则为 true;如果指定的命名系统互斥体已存在,则为
// false。该参数未经初始化即被传递。
//
// 异常:
// System.UnauthorizedAccessException:
// 命名的互斥体存在并具有访问控制安全性,但用户不具有 System.Security.AccessControl.MutexRights.FullControl。
//
// System.IO.IOException:
// 发生了一个 Win32 错误。
//
// System.Threading.WaitHandleCannotBeOpenedException:
// 无法创建命名的互斥体,原因可能是与其他类型的等待句柄同名。
//
// System.ArgumentException:
// name 的长度超过 260 个字符。
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[SecurityCritical]
public Mutex(bool initiallyOwned, string name, out bool createdNew);
启动时采用如下这些语句,利用是系统只有一个命名为“ TestSingleStart”的Mutex变量的方式,所以在没释放前都是创建不成功的,所以 createdNew变量只有第一次开启程序才能被赋值上true。
Boolean createdNew;
System.Threading.Mutex instance = new System.Threading.Mutex(true, "TestSingleStart", out createdNew); //同步基元变量
if (createdNew)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
instance.ReleaseMutex();
}
else
{
MessageBox.Show("已经启动了一个程序,请先退出");
Application.Exit();
}
注意:说点题外话, 实际上大家要特别注意的一点是其实Mutex这个类我个人感觉一开始被设计出来更多是用来解决同一个进程内,多线程同步问题,举个例子
就像lock关键字,详细参见微软官方SDK(https://msdn.microsoft.com/zh-cn/library/system.threading.mutex(v=vs.100).aspx)
大概意思就是说,举例子吧,以下两段代码等效。
代码段一:
static Object lockobj = new Object();
private void testMultiplyThread()
{
lock (lockobj)
{
Console.WriteLine("同步处理的代码");
}
}
代码段二:
//小注:无参构造函数默认是 给调用线程赋予互斥体的初始所属权,所以第一次mutex.WaitOne不会阻塞
static System.Threading.Mutex mutex =new System.Threading.Mutex();
private void testMultiplyThread()
{
mutex.WaitOne();
Console.WriteLine("同步处理的代码");
mutex.ReleaseMutex();
}
评价:方式一巧妙利用Mutex的基元单位命名规则是系统内唯一,来判定是否是重开了一个exe,但是更人性化的处理是顺便把焦点定位到之前运行的exe上。
优点:简洁,精确。
缺点:没有点位焦点到之前运行好的EXE窗体上
方式二:利用进程名判断是否有同名的进程已经运行如果有的话,得到该进程并获取窗口句柄,得到句柄后调用ShowWindowAsync和SetForegroundWindow
这个直接上代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace SingleStart
{
static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
try
{
Process instance = RunningInstance();
if (instance == null)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new pbform());
}
else
{
HandleRunningInstance(instance);
}
}
catch (Exception e) { }
}
[DllImport("User32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("User32.dll")]
private static extern bool ShowWindowAsync(IntPtr hWnd, int cmdShow);
private static void HandleRunningInstance(Process instance)
{
// 确保窗口没有被最小化或最大化
ShowWindowAsync(instance.MainWindowHandle, 4);
// 设置真实例程为foreground window
SetForegroundWindow(instance.MainWindowHandle);// 放到最前端
}
private static Process RunningInstance()
{
Process current = Process.GetCurrentProcess();
Process[] processes = Process.GetProcessesByName(current.ProcessName);
foreach (Process process in processes)
{
if (process.Id != current.Id)
{
// 确保例程从EXE文件运行
if (Assembly.GetExecutingAssembly().Location.Replace("/", "\\") == current.MainModule.FileName)
{
return process;
}
}
}
return null;
}
}
}
评价:这种方式的确人性化很多,而且在大部分情况下都能够准确判定,但是如果要钻牛角尖的话,如果把EXE拷贝一份改个名字,照样可以新开启一个EXE额,所以这一点是美中不足的,方式二提升了人性化,但是准确性降低了了。(我听到无数人骂我钻牛角尖了呵呵)
优点:第二次启动EXE可以直接使前面还在运行的EXE得到焦点,人性化很好。
缺点:在一些特殊情况下还是能开启第二个EXE。
方式三:这个方式其实说来是我无意中在实现别的功能中,可以说是意外联想到,容我慢慢道来。
大概是2015年初刚开班的时候,有个项目需求我使用chrome浏览器去打开本地一个EXE,而且是带参数打开,而且当然还要求一点就是如果参数是一样的那么如果之前曾经打开过那个一样的参数EXE就必须直接获取焦点,参数不一样就重写打开新EXE产生一个线程,相比看到这里你应该明白了吧,上面的方式一和方式二肯定是满足不了我的需求,实际上如果要满足我的需求说简单也简单容我一一列举。
方式三第一个版本:直接产生持久化文件,管理所有打开过的EXE,但是这样会不会容易产生文件占用呢,或许实现起来也不轻松
方式三第二个版本:使用一个WIndows服务作为协助,把所有开过的EXE,且传过去的参数进行管理呢,这或许是一个万能的手段,但是又要多谢一个Windows服务。
经过以上推敲,我个人无意中发现MemoryMappedFile这个类,内存文件映射,有人说这不是和第一个版本一样吗,我要说NO,这大不一样,我说这一切的原因都在于
MemoryMappedFile.CreateOrOpen这个方法
//
// 摘要:
// 在系统内存中创建或打开一个具有指定容量的内存映射文件。
//
// 参数:
// mapName:
// 要分配给内存映射文件的名称。
//
// capacity:
// 要分配给内存映射文件的最大大小(以字节为单位)。
//
// 返回结果:
// 具有指定名称和大小的内存映射文件。
//
// 异常:
// System.ArgumentException:
// mapName 是空字符串。
//
// System.ArgumentNullException:
// mapName 为 null。
//
// System.ArgumentOutOfRangeException:
// capacity 大于逻辑地址空间的大小。 - 或 - capacity 小于或等于零。
public static MemoryMappedFile CreateOrOpen(string mapName, long capacity);
使用内存管理的方式把不同的参数压缩成一个字符串mapName,然后使用MemoryMappedFile 来创建或打开并管理他,这不就是最好的终极解决方案吗,更加美妙是的,由于是内存管理,可以把结构体指针写进去,这不就是说可以把窗口句柄写进去的意思一样,所以一些思路都清晰了,所需只是下面两个函数将完美解决我想要的一切,事实证明当内存足够大时候,把问题放在内存里解决是多么美妙的的一件事

写函数
MemoryMappedFile mmf = MemoryMappedFile.CreateOrOpen(key, 1024000);
using (MemoryMappedViewStream stream = mmf.CreateViewStream()) //注意这里的偏移量
{
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
accessor.Write(0, ref handler);//这里的handler就是我们窗口句柄
}
}
读函数
static IntPtr GetMemory(string key)
{
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(key))
{
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
IntPtr handler = IntPtr.Zero;
accessor.Read(0, out handler);
return handler;//<span style="font-family: Arial, Helvetica, sans-serif;">这里的handler就是我们窗口句柄</span>
}
}
}
这是我自己使用方案三:为了自己制作的单例模式的简易DEMO,从项目代码里抽出来不容易啊只要1分, 下载地址