Click here to Skip to main content
15,881,852 members
Articles / Desktop Programming / MFC
Article

MFC extension library : Single instance Plug-In

Rate me:
Please Sign up or sign in to vote.
4.66/5 (6 votes)
8 Jun 2004CPOL7 min read 51.1K   904   37   4
Single instance Plug-In

Table of content

Introduction

This article is a follow on to the article An MFC extension library to enable DLL plug-in technology for your application using MESSAGE_MAPs. If you are not familiar with the library itself I would recommend that you look at the main article first.

This article outlines how I implemented a Single Instance plug-in for the framework which supports this functionality:

  • Single Instance - Only one instance of the application can run at any time.
  • When a new instance is started, it will close and set the focus to the first instance.
  • Any new instance command lines will be passed to the already running instance so it can process them.

Having the list of the functionality that this plug-in should give us we need to look at how it can be implemented.

Single Instance methods

If you search CodeProject for Single Instance, you will get a large list of articles on the subject. They cover various methods of how to implement single instanceness. Some are good, some bad, and some adequate. Having worked with P J Naughters single instance version before, and having also extended it to support command line passing, it was a good base to start from when implementing this plug-in. I suggest that you read his article to get a good understanding on how his method works.

How does this plug-in work?

The plug-in needs to create and keep track of a Memory mapped file (MMF). In this file, we have the HWND of the first applications CMainFrame, once it was created. This is all handled for us by PJs class CInstanceChecker. At the start of the plug-in file, we create the one CInstanceChecker object that will be referenced during tracking and switching.

CSIPlugIn plugIn(false);

// the single instance checker object
CInstanceChecker f_checker;

I gave it the name f_checker as the object has file scope only and is not referenced outside of the file.

Hooking up the MMF

After having created the CInstanceChecker object, we need to hook it up correctly into the construction of the CMainFrame object being used by the main application. If we hook the WM_CREATE message we do the following:

afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

BEGIN_MESSAGE_MAP(CSIPlugIn, CPlugInMap)
    //{{AFX_MSG_MAP(CSIPlugIn)
    ON_WM_CREATE()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

int CSIPlugIn::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (IsPreCall())
    {
        // setup the single instance MMF instance checker
        // we need to suppress window creation if the MMF already exists
        if (f_checker.PreviousInstanceRunning())
        {
            CString commandLine = AfxGetApp()->m_lpCmdLine;
            f_checker.ActivatePreviousInstance(commandLine);
            // we do not want it to create a new mainframe, suppress it
            SuppressThisMessage();
            return -1;              // stop further window creation
        }
        f_checker.TrackFirstInstanceRunning();
    }
    else
    {
        // its the post call, we need to setup the window handle so 
        // that other instances can send us their command lines
        CPIMainFrame *pPIMF = static_cast<CPIMainFrame*>(m_pPlugInFor);
        f_checker.SetWindowHandle(pPIMF->m_hWnd);
    }
    
    return 0;
}

So what does it do?

When were the first instance

On a Pre call, we first check the CInstanceChecker object which tells us whether an existing instance of our application is running. If no instance is running, we track ourselves as the first instance. We then fall through to the return 0 which allows the CMainFrame object to be created successfully. In the Post call to the function, the CMainFrame window should exist, so we take a copy of its HWND and place it in the MMF structure so that when a new instance gets created, it can send messages to us, either to make us active or open any files which the other instance would have on the command line.

When were a following instance

When we are not the first instance of the application, we need to stop a new CMainFrame from being created, and also pass any command line parameters across to the first instance that is running. We do this by taking a copy of the current command line and then calling f_checker.ActivatePreviousInstance(commandLine). We also suppress the message so that no mainframe window will be created and return -1 so that mainframe creation code will abort and close down the instance that is trying to startup.

Testing what we have

Once I had got it to this stage, I decided to test it. And once again, like in previous plug-ins I had a problem. :(

After careful breakpoints and various runs, I found that although message suppression works correctly for what we have, it should also suppress any Post plug in message handlers. For example, while testing this I also had the MDI tab bar example also running. This other plug-in also hooks the WM_CREATE message, but as a Post handler. It automatically assumes that the CMainFrame object was created successfully and tries to subclass some windows that do not exist. It then fails badly.

So off I went, back to the library source and did the required modifications in all the files necessary:

// old code
    if (!m_bSuppressThisMessage)
    {
        // allow the exe to process the message is it 
        // hasn't been suppressed by a DLL
        ret_app = CScrollView::OnWndMsg(message, wParam, lParam, pResult);
    }
    if (!stateCopy.IsDestroyed())
    {
        // allow DLL to process the message after the exe
        // note that DLLs get to do this whether the message 
        // has been suppressed or not
        ret_pre = pApp->ProcessWindowMessageMaps(stateCopy, false, 
          &m_bSuppressThisMessage, m_pMaps, m_MapCount, 
          message, wParam, lParam, pResult);
    }

// new code
    if (!m_bSuppressThisMessage)
    {
        // allow the exe to process the message is 
        // it hasn't been suppressed by a DLL
        ret_app = CScrollView::OnWndMsg(message, wParam, lParam, pResult);
        if (!stateCopy.IsDestroyed())
        {
            // allow DLL to process the message after the exe
            // note that DLLs get to do this whether the message 
            // has been suppressed or not
            ret_pre = pApp->ProcessWindowMessageMaps(stateCopy, false, 
             &m_bSuppressThisMessage, m_pMaps, m_MapCount, message,
             wParam, lParam, pResult);
        }
    }

This change was done for all OnWndMsg() and OnCmdMsg() overrides throughout the library, and any plug-ins (the advanced print preview plug-in has a custom plug-in class).

Once these changes in the library had been implemented, instance tracking started to work correctly.

Opening the command line in the other instance

PJs class was good at making sure that only a single instance of your application would be created, but it did not handle the case of passing command line parameters from any follow on instance to the primary instance. For example, it you were running your application and tried to open some of your applications files from explorer, focus goes to the current instance, but the new files are not opened!

I needed to go about implementing this functionality

If you look at PJs CInstanceChecker class, you will see he defines a class:

struct CWindowInstance
{
  HWND hMainWnd;
};

Now this is a memory mapped file, so its content is valid across process boundaries, so if we placed a data area for a string in here, then we can safely copy text from one application to another. Lets look at our new enhanced version of this class:

struct CWindowInstance
{
 HWND hMainWnd;
 TCHAR command_line[_MAX_PATH * 2]; 
    // buffer for command line when transferring control
 bool commandPending;    
    // set when a command is in the buffer, cleared when processed
};

Here we have a buffer that is _MAX_PATH * 2 in length. This is because a command line can contain the a full file path and optional statments to be applied to the file in question.

PJs original ActivatePreviousInstance() function did not take any parameters. Its been modified here to take the command line that needs to be passed to the first instance. Lets take a look at the new version of the function:

HWND CInstanceChecker::ActivatePreviousInstance(CString command_line)
{
    //Try to open the previous instances MMF
    HANDLE hPrevInstance = ::OpenFileMapping(FILE_MAP_ALL_ACCESS, 
       FALSE, MakeMMFFilename());
    if (hPrevInstance)
    {
        // Open up the MMF
        int nMMFSize = sizeof(CWindowInstance);
        CWindowInstance* pInstanceData = (CWindowInstance*)::MapViewOfFile(
                hPrevInstance, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, nMMFSize);
        if (pInstanceData != NULL) //Opening the MMF should work
        {
            bool done = false;
            HWND hWindow = NULL;
            CSingleLock dataLock(&m_instanceDataMutex);
            while (!done)
            {
                // Lock the data prior to reading from it
                dataLock.Lock(); // this waits until it becomes available
                //activate the old window
                hWindow = pInstanceData->hMainWnd;
                // check that the other instance has initialised and 
                // is ready for a new command line
                if (hWindow != NULL && !pInstanceData->commandPending)
                {
                    // copy in the command line
                    _tcscpy(pInstanceData->command_line, command_line);
                    pInstanceData->commandPending = true; 
                       // no more commands til this one processed
                    CWnd wndPrev;
                    wndPrev.Attach(hWindow);
                    CWnd* pWndChild = wndPrev.GetLastActivePopup();

                    //Restore the focus to the previous instance 
                    //and bring it to the foreground
                    if (wndPrev.IsIconic())
                    {
                        wndPrev.ShowWindow(SW_RESTORE);
                    }
                    pWndChild->SetForegroundWindow();

                    // Must be async message so other instance is 
                    // free to make outgoing RPC calls
                    pWndChild->PostMessage(WM_NEWCOMMANDLINE, 0, 0L);

                    //Detach the CWnd we were using
                    wndPrev.Detach();
                    done = true;
                }
                else
                {
                    dataLock.Unlock();
                    Sleep(10); // give the other instance time to
                               // initialize or process
                }
            }
            //Unmap the MMF we were using
            ::UnmapViewOfFile(pInstanceData);
            //Close the file handle now that we 
            ::CloseHandle(hPrevInstance);
            dataLock.Unlock();
            //return the Window handle of the previous instance
            return hWindow;
        }

        //Close the file handle now that we 
        ::CloseHandle(hPrevInstance);

        //When we have activate the previous instance, 
        //we can release the lock
        ReleaseLock();
    }
    return NULL;
}

If we look at the code, we can see some big changes from PJs version. When we lock the data buffer, we copy the command line parameter across into the buffer supplied, we set the commandPending flag also. We then activate the other instance and post it a registered window message to tell it that there is a new command line to be processed in the MMF structure. To avoid race conditions for example, where explorer is opening multiple files and all the new instances want the first instance to process them, we make use of a Mutex so that only one can do it at a time. There is also a wait involved where the first instance of the application starting up may yet not have setup the HWND of the mainframe to post messages to.

Processing the new command line

So the new command line is being sent to the first instance of the application, we now need to process it there. Lets see how its done:

// we need the message ID, which is constant across all instances
const UINT WM_NEWCOMMANDLINE = ::RegisterWindowMessage(WM_COMMANDLINE);

// we need a message map extry in our plug-in to handle the message
    afx_msg LRESULT OnNewCommandLine(WPARAM wParam, LPARAM lParam);
    
    ON_REGISTERED_MESSAGE(WM_NEWCOMMANDLINE, OnNewCommandLine)

// we need to handle the message

LRESULT CSIPlugIn::OnNewCommandLine(WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(wParam);
    UNREFERENCED_PARAMETER(lParam);

    if (IsPreCall())
    {
        if (f_checker.PreviousInstanceRunning())
        {
            CString    command_line;
            CWinApp *pApp = AfxGetApp();
        
            command_line = f_checker.GetCommandLine();
            CString text;
            InitCommandLineParameters(command_line);

            CCommandLineInfo cmd;
            ParseNewCommandLine(cmd);
            if (cmd.m_nShellCommand == CCommandLineInfo::FileNew)
            {
                cmd.m_nShellCommand = CCommandLineInfo::FileNothing;
            }
            pApp->ProcessShellCommand(cmd);
        }
    }
    return 0;
}

MFC provides a standard class CCommandLineInfo which we can use with the CWinApp::ProcessShellCommand() function to correctly handle the new command line, we just need to setup the CCommandLineInto object correctly to do it for us. So I needed to write a couple of functions to correct parse the new command line and setup the object.

void CSIPlugIn::ParseNewCommandLine(CCommandLineInfo& rCmdInfo)
{
    for (int i = 0; i < m_commandLineParameters.size(); i++)
    {
        BOOL bFlag = FALSE;
        BOOL bLast = ((i + 1) == m_commandLineParameters.size());
        if (m_commandLineParameters[i].GetAt(0) == '-' 
             || m_commandLineParameters[i].GetAt(0) == '/')
        {
            m_commandLineParameters[i] = 
m_commandLineParameters[i].Right(m_commandLineParameters[i].GetLength() - 1);
            // remove flag specifier
            bFlag = TRUE;
        }
        rCmdInfo.ParseParam(m_commandLineParameters[i], bFlag, bLast);
    }
}


void CSIPlugIn::InitCommandLineParameters(CString command)
{
    TCHAR* pArgs = command.GetBuffer(command.GetLength() + 1);
    CString parameter;
    bool bInQuotes = false;
    enum {TERM  = '\0', QUOTE = '\"'};
    
    // clear any previous command line parameters
    m_commandLineParameters.clear();
    
    while (*pArgs)
    {
        while (isspace(*pArgs)) // skip leading whitespace
        {
            pArgs++;
        }
        
        bInQuotes = (*pArgs == QUOTE);  // see if this token is quoted
        
        if (bInQuotes) // skip leading quote
        {
            pArgs++; 
        }
        
        parameter = "";
        
        // Find next token.
        // NOTE: Args are normally terminated by whitespace, unless the
        // arg is quoted.  That's why we handle the two cases separately,
        // even though they are very similar.
        if (bInQuotes)
        {
            // find next quote followed by a space or terminator
            while (*pArgs && !(*pArgs == QUOTE && 
                  (isspace(pArgs[1]) || pArgs[1] == TERM)))
            {
                parameter += *pArgs;
                pArgs++;
            }
            if (*pArgs == QUOTE)
            {
                pArgs++;
                if (pArgs[1])   // if quoted token not followed by a terminator
                {
                    pArgs += 2; // advance to next token
                }
            }
        }
        else
        {
            // skip to next non-whitespace character which delimts a file
            while (*pArgs)
            {
                if (isspace(*pArgs)) 
                {
                    // check to see if it correctly delimits an existing file
                    CFile file;
                    if (file.Open(parameter, CFile::modeRead))
                    {
                        // file exists, break the line here
                        file.Close();
                        break;
                    }
                    else
                    {
                        // file does not exist, continue parsing the line
                    }
                }
                parameter += *pArgs;
                pArgs++;
            }
            if (*pArgs && isspace (*pArgs)) // end of token
            {
                pArgs++;         // advance to next token or terminator
            }
        }
        if (!parameter.IsEmpty())
        {
            // we parsed a parameter, push onto list
            m_commandLineParameters.push_back(parameter);
        }
    }
    command.ReleaseBuffer(-1);
}

Parsing the command line has to take quoted " filenames into account, otherwise it assumes all command lines are space delimited on the command line. We also do not get passed the name of the application on the command line, which you would normally see when being started directly by the shell.

Other issues

Other minor issues that needed to be sorted out are that the MMF name used by the instance class needs to be unique for each application it is used in. If two different applications using the library were to use the same plug-in, then currently they would use the same MMF filename. So you could only start one instance of either application. A very simple way around this problem is to change PJs MakeMMFFilename() function to return the name of the application:

LPCTSTR CInstanceChecker::MakeMMFFilename()
{
    // use the unique name for the application so that 
    // if plug in is used by multiple
    // applications they do not clash!
    // m_productName is static as we need the buffer being returned
    m_productName.LoadString(AFX_IDS_APP_TITLE);
    TRACE("MMFFilename is %s\n", m_productName);
    return m_productName;
}

So now we have a fully working implementation of single instanceness for any application that wants it.

Conclusion

Another plug-in done, some minor problems in the library exposed and killed. Nice enhancements for any application using the library become available in a modular fashion.

References

Here are a list of related articles used in the making of this plug-in

Version history

  • V1.0 7th June 2004 - Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Sirius Analytical Instruments
United Kingdom United Kingdom
A research and development programmer working for a pharmaceutical instrument company for the past 17 years.

I am one of those lucky people who enjoys his work and spends more time than he should either doing work or reseaching new stuff. I can also be found on playing DDO on the Cannith server (Send a tell to "Maetrim" who is my current main)

I am also a keep fit fanatic, doing cross country running and am seriously into [url]http://www.ryushinkan.co.uk/[/url] Karate at this time of my life, training from 4-6 times a week and recently achieved my 1st Dan after 6 years.

Comments and Discussions

 
GeneralInstance Checking Pin
pjnaughter15-Jun-04 3:34
pjnaughter15-Jun-04 3:34 
GeneralRe: Instance Checking Pin
Roger Allen15-Jun-04 3:39
Roger Allen15-Jun-04 3:39 
GeneralRe: Instance Checking Pin
Member 239604227-Jun-08 5:29
Member 239604227-Jun-08 5:29 
AnswerRe: Instance Checking Pin
pjnaughter27-Jun-08 6:27
pjnaughter27-Jun-08 6:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.