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
复制代码

 

posted @   strnghrs  阅读(190)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2018-01-06 手机摄影:黄埔军校旧址(下)
点击右上角即可分享
微信分享提示