VC开发单实例(Single Instance)应用软件
作者:马健
邮箱: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上的封装成了一个类,完整代码如下:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | BOOL CSingleInstanceApp::InitInstance() { WNDCLASS wc = {0}; wc.style = CS_BYTEALIGNWINDOW|CS_SAVEBITS|CS_DBLCLKS; wc.lpfnWndProc = DefDlgProc; wc.cbWndExtra = DLGWINDOWEXTRA; wc.hInstance = m_hInstance; wc.hIcon = LoadIcon(IDR_MAINFRAME); wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = CreateSolidBrush(GetSysColor(COLOR_BTNFACE)); wc.lpszClassName = _T( "SINGLE_INSTANCE_APP" ); //this name is from dialog's template ATOM cls = RegisterClass(&wc); HANDLE hMutex = CreateMutex(NULL, FALSE, "CSingleInstanceApp{B98C6AA5-57C0}" ); if ( WaitForSingleObject(hMutex, 1000) == WAIT_TIMEOUT ) { // // There is another instance out there, but it is taking too long to // locate, just exit. return FALSE; } HWND hWndApp,hWndPopup; if (hWndApp = ::FindWindow(_T( "SINGLE_INSTANCE_APP" ),NULL)) { hWndPopup = ::GetLastActivePopup(hWndApp); ::BringWindowToTop(hWndApp); if ( ::IsIconic(hWndPopup) ) ::ShowWindow(hWndPopup, SW_RESTORE); else ::SetForegroundWindow(hWndPopup); ReleaseMutex(hMutex); CloseHandle(hMutex); return FALSE; } ReleaseMutex(hMutex); CloseHandle(hMutex); #ifdef _AFXDLL Enable3dControls(); // Call this when using MFC in a shared DLL #else Enable3dControlsStatic(); // Call this when linking to MFC statically #endif CSingleInstanceDlg dlg; m_pMainWnd = &dlg; int nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: Place code here to handle when the dialog is // dismissed with OK } else if (nResponse == IDCANCEL) { // TODO: Place code here to handle when the dialog is // dismissed with Cancel } // Since the dialog has been closed, return FALSE so that we exit the // application, rather than start the application's message pump. return FALSE; } |
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中,不论是使用还是修改都很方便,全文如下:
//----------------------------------------------------------------------------- // SingleInst.h //----------------------------------------------------------------------------- #ifndef _SINGLEINST_H #define _SINGLEINST_H class CSingleInstance { public: CSingleInstance( LPTSTR strID ) : m_hFileMap(NULL), m_pdwID(NULL), m_strID(NULL) { if ( NULL != strID ) { m_strID = new TCHAR[ _tcslen( strID ) + 1 ]; if ( NULL != m_strID ) _tcscpy( m_strID, strID ); } } ~CSingleInstance() { // if we have PID we're mapped if( m_pdwID ) { UnmapViewOfFile( m_pdwID ); m_pdwID = NULL; } // if we have a handle close it if( m_hFileMap ) { CloseHandle( m_hFileMap ); m_hFileMap = NULL; } if ( NULL != m_strID ) { delete [] m_strID; m_strID = NULL; } } static BOOL CALLBACK enumProc( HWND hWnd, LPARAM lParam ) { DWORD dwID = 0; GetWindowThreadProcessId( hWnd, &dwID ); // JeffZi - 13800: when the tooltips_class32 was being created after the welcome page of the wizards, // it was being returned as the first window for this PID. so, make sure this window // has children before setting focus if( (dwID == (DWORD)lParam) && GetWindow(hWnd, GW_CHILD) ) { SetForegroundWindow( hWnd ); SetFocus( hWnd ); return FALSE; } return TRUE; } BOOL IsOpen( VOID ) { return !(Open()); } private: BOOL Open( VOID ) { BOOL bRC = FALSE; m_hFileMap = CreateFileMapping( (HANDLE)-1, NULL, PAGE_READWRITE, 0, sizeof(DWORD), m_strID ); if( NULL != m_hFileMap ) { if ( ERROR_ALREADY_EXISTS == GetLastError()) { // get the pid and bring the other window to the front DWORD* pdwID = static_cast<DWORD *>( MapViewOfFile( m_hFileMap, FILE_MAP_READ, 0, 0, sizeof(DWORD) ) ); if( pdwID ) { DWORD dwID = *pdwID; UnmapViewOfFile( pdwID ); EnumWindows( enumProc, (LPARAM)dwID ); } CloseHandle( m_hFileMap ); m_hFileMap = NULL; } else { m_pdwID = static_cast<DWORD *>( MapViewOfFile( m_hFileMap, FILE_MAP_WRITE, 0, 0, sizeof(DWORD) ) ); if ( NULL != m_pdwID ) { *m_pdwID = GetCurrentProcessId(); bRC = TRUE; } } } return bRC; } private: LPTSTR m_strID; HANDLE m_hFileMap; DWORD* m_pdwID; }; // class CSingleInstance #endif // _SINGLEINST_H
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2018-01-06 手机摄影:黄埔军校旧址(下)