作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:https://www.cnblogs.com/stronghorse
发布:2025.01.05
所谓单实例(Single Instance),是指在系统中同时只能有应用的一个实例在运行,即启动第二个实例的时候,如果发现已经有第一个实例在运行,则要么直接退出第二个实例,要么通知第一个实例退出,由第二个实例接棒。
在我发行的免费软件中,NoteIcon就是一个典型的单实例应用,因为如果同时有多个实例在运行,就会对NoteIcon.txt文件造成访问冲突,解决起来既麻烦,又没有必要,不如干脆限制成单实例。
最近想把TrayApp也改成单实例,所以对目前搜集的一些单实例技术进行了总结。
单实例最关键的技术,其实就是当第二个实例启动的时候,以什么特征判断第一个实例已经启动?这个特征判断要求既简单又可靠,我见过的技术包括:
一、以互斥量(Mutex)为特征
如果只需要简单判断是否已经有第一个实例在运行,发现已经在运行就直接退出第二个实例,两个实例之间不需要进行任何通讯,那么最简单的办法是使用互斥量(Mutex)。这种方法是我在一个开源小软件里看来的,名字叫NetworkIndicator,但忘记是从codeguru还是codeproject上下载的源代码了。这个小软件的功能就是模拟以前Win98的功能,在桌面右下角的系统托盘区(现在似乎叫系统通知区)显示一个小图标,展现当前网络的连接状态。这种托盘区的小软件一般都要做单实例判断,以免在系统托盘区中重复加入图标。
在NetworkIndicator中只用了三行做判断:
HANDLE hMutexOneInstance = ::CreateMutex( NULL, FALSE, _T("NetworkIndicator")); DWORD dwLastErr = ::GetLastError(); BOOL bAlreadyRunning = (dwLastErr == ERROR_ALREADY_EXISTS || dwLastErr == ERROR_ACCESS_DENIED);
如果要更具有普适性,CreateMutex的第三个参数其实最好用GUID串。
二、以主窗口标题(Caption,或Title)为特征
如果需要在第一个实例与第二个实例之间进行通讯,比如说在第二个实例发现第一个实例后,自动激活第一个实例,然后自己再退出,则上面的Mutext方法就不够用。这种时候如果是有主窗口的应用,且主窗口的标题比较有特色,那么可以采用如下简单方法:
1、调用EnumWindows函数开始枚举窗口。
2、在回调函数中调用GetWindowText获取当前枚举到的窗口的标题,如果发现是自己的标题,那么就有了第一个实例主窗口的窗口句柄(HWND),不论是激活它,还是给它发消息,都是毛毛雨啦。
这种方法一般只适用于基于对话框的应用,不适用于文档-视(Document-View)结构的应用,因为不论是SDI还是MDI,一般MainFrame的标题都会随着当前所打开的文档而变化,不固定。
如果觉得仅凭主窗口标题还不是很保险,可以再加入通讯确认机制:第二个实例在枚举出第一个实例的主窗口后,调用SendMessageTimeout向第一个实例的主窗口发送一条约定好的自定义消息,如果没有超时,并且返回值也是双方约定好的值,则可以确认第一个实例主窗口的有效性和唯一性。
三、以主窗口类名(ClassName)为特征
对于文档-视(Document-View)结构的应用,既然主窗口的标题不固定,那么还可以在创建主窗口之前,调用AfxRegisterClass函数给主窗口注册一个够独特的类名(ClassName),然后用FindWindowEx函数找具有这个类名的窗口,找到了就可以给窗口激活、发消息。如果不保险,也可以和前面说的Mutex结合起来,先检查互斥量是否已经创建,发现已经被创建过再找窗口。
这个方法不仅适用于Document-View结构的应用,也适用于对话框为主窗口的应用,包括没有标题条的对话框。
这种方法的代码我最早是在codeguru上看到的一个开源项目,提供了现成的SingleInstanceApp.h、SingleInstanceApp.cpp,直接用就好。后来在codeproject上也看到类似的项目“Dialog based single instance applications improved”,技术上是一样的,但没有像codeguru上的封装成了一个类。
NoteIcon用的就是这种技术,所以如果用Spy++去看它主窗口的类名,就会看到长长一串字符串。
四、以进程ID(ProcessID)为特征
如果应用软件干脆就没有主窗口,那么不论是EnumWindows还是FindWindowEx,都将失去作用,只能用其他方法。
我在Windows 2003源代码中看到的一种方法是使用共享内存,即用CreateFileMapping创建一个具有约定名称(GUID)的共享内存,创建成功说明当前是第一个示例,可以把进程ID(Process ID)等写到共享内存里;创建不成功说明当前进程已经是第二个示例,可以从共享内存中读取出第一个示例的Process ID,然后进行进一步的操作。
在Windows 2003的原版代码中,取得第一个进程的Process ID后,是调用EnumWindows找窗口,找到后激活之。但如果应用软件没有主窗口,其实可以改成通过Mutex或Event通知第一个实例说“第二个实例已经上线”,然后双方通过共享内存进行双向通讯。
Windows 2003的相关代码封装在单独一个文件singleinst.h中,不论是使用还是修改都很方便。