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);
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)
ON_WM_CREATE()
END_MESSAGE_MAP()
int CSIPlugIn::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (IsPreCall())
{
if (f_checker.PreviousInstanceRunning())
{
CString commandLine = AfxGetApp()->m_lpCmdLine;
f_checker.ActivatePreviousInstance(commandLine);
SuppressThisMessage();
return -1;
}
f_checker.TrackFirstInstanceRunning();
}
else
{
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:
if (!m_bSuppressThisMessage)
{
ret_app = CScrollView::OnWndMsg(message, wParam, lParam, pResult);
}
if (!stateCopy.IsDestroyed())
{
ret_pre = pApp->ProcessWindowMessageMaps(stateCopy, false,
&m_bSuppressThisMessage, m_pMaps, m_MapCount,
message, wParam, lParam, pResult);
}
if (!m_bSuppressThisMessage)
{
ret_app = CScrollView::OnWndMsg(message, wParam, lParam, pResult);
if (!stateCopy.IsDestroyed())
{
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];
bool commandPending;
};
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)
{
HANDLE hPrevInstance = ::OpenFileMapping(FILE_MAP_ALL_ACCESS,
FALSE, MakeMMFFilename());
if (hPrevInstance)
{
int nMMFSize = sizeof(CWindowInstance);
CWindowInstance* pInstanceData = (CWindowInstance*)::MapViewOfFile(
hPrevInstance, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, nMMFSize);
if (pInstanceData != NULL)
{
bool done = false;
HWND hWindow = NULL;
CSingleLock dataLock(&m_instanceDataMutex);
while (!done)
{
dataLock.Lock();
hWindow = pInstanceData->hMainWnd;
if (hWindow != NULL && !pInstanceData->commandPending)
{
_tcscpy(pInstanceData->command_line, command_line);
pInstanceData->commandPending = true;
CWnd wndPrev;
wndPrev.Attach(hWindow);
CWnd* pWndChild = wndPrev.GetLastActivePopup();
if (wndPrev.IsIconic())
{
wndPrev.ShowWindow(SW_RESTORE);
}
pWndChild->SetForegroundWindow();
pWndChild->PostMessage(WM_NEWCOMMANDLINE, 0, 0L);
wndPrev.Detach();
done = true;
}
else
{
dataLock.Unlock();
Sleep(10);
}
}
::UnmapViewOfFile(pInstanceData);
::CloseHandle(hPrevInstance);
dataLock.Unlock();
return hWindow;
}
::CloseHandle(hPrevInstance);
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:
const UINT WM_NEWCOMMANDLINE = ::RegisterWindowMessage(WM_COMMANDLINE);
afx_msg LRESULT OnNewCommandLine(WPARAM wParam, LPARAM lParam);
ON_REGISTERED_MESSAGE(WM_NEWCOMMANDLINE, OnNewCommandLine)
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);
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 = '\"'};
m_commandLineParameters.clear();
while (*pArgs)
{
while (isspace(*pArgs))
{
pArgs++;
}
bInQuotes = (*pArgs == QUOTE);
if (bInQuotes)
{
pArgs++;
}
parameter = "";
if (bInQuotes)
{
while (*pArgs && !(*pArgs == QUOTE &&
(isspace(pArgs[1]) || pArgs[1] == TERM)))
{
parameter += *pArgs;
pArgs++;
}
if (*pArgs == QUOTE)
{
pArgs++;
if (pArgs[1])
{
pArgs += 2;
}
}
}
else
{
while (*pArgs)
{
if (isspace(*pArgs))
{
CFile file;
if (file.Open(parameter, CFile::modeRead))
{
file.Close();
break;
}
else
{
}
}
parameter += *pArgs;
pArgs++;
}
if (*pArgs && isspace (*pArgs))
{
pArgs++;
}
}
if (!parameter.IsEmpty())
{
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()
{
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
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.