Introduction
When first posted the article I wrote: "I have seen many samples on limiting
application instance but some of them were too complicated while the others
required multiple additions in different classes because of message handling on
window level." Well, some time ago I understood that it is better not
to make statements like that because actually faced so much problems that I
lost myself maybe not in the complexity but in the amount of code which wrote
trying to solve them and limit that damned application to a single instance. :)
I wanted to have all realization in one class only and handle the messages on
the application level, not windows. Note - VB 6.0 users have the simplest
solution - they can check App.PrevInstance to limit instance, it's
real and then ActivateApp or smth. like that.
Usage
Derive the application class from CWinAppEx and just check InitInstance()
function's ( with some GUID in parameter) return value and exit instance
in case it is FALSE. Also add EnableShellOpen and RegisterShellFileTypes
for SDI and MDI applications. That is all. ( The previous instance will popup
the main window itself if you won't override OnAnotherInstance virtual
function to do some specific actions when another instance is started ). Note:
if lpszUID is not specified in the InitInstance(), the
instance will not be limited.
BOOL CSomeApp::InitInstance( void )
{
if( CWinAppEx::InitInstance( _T( "{6BDD1FC6-...}" ) ) )
return FALSE;
AfxEnableControlContainer();
EnableShellOpen();
RegisterShellFileTypes( TRUE );
return TRUE;
}
Some History
Once in the MSDN I found the BroadcastSystemMessage function: MSDN:
"The BroadcastSystemMessage function sends a message to the
specified recipients. The recipients can be applications, installable drivers,
network drivers, system-level device drivers, or any combination of these
system components". Well, it allowed handling the messages on the
application level and create this class.
Description
If you look in the sources you'll see that in the InitInstance the
application registers app-unique message with UID passed and holds the value
returned in the private variable to identify another instance messages sent
later. Then it searches for another instance using (tradition) mutexes (FindAnotherInstance
function) and in case it finds it, broadcasts the unique message using BroadcastSystemMessage
(PostInstanceMessage function) and exits. App. previous instance
(if any) receives the message, compares it with own one and if they are
equal, calls virtual function OnAnotherInstanceMessage, whose
default implementation popups the main window. (These steps are required for
dialog based applications and for SDI and MDI apps when launching
application executable (not associated document) and in some cases
that are described later).
The BroadcastSystemMessage required SE_TCB_NAME privilege
enabled on NT to send message to all desktops, so I wrote EnableTokenPrivilege
function, that enables or disables privileges in the specified access token,
which is called before broadcasting the message. I thought that it can be
useful also outside this class and placed only its code in the article body
removing the old code pieces, because the sources are totally overwritten and
appeared to be too large for this article. To look at implementation you should
open the source files.
BOOL CWinAppEx::EnableTokenPrivilege( LPCTSTR lpszSystemName,
BOOL bEnable )
{
ASSERT( lpszSystemName != NULL );
BOOL bRetVal = FALSE;
if( ::GetVersion() < 0x80000000 ) {
HANDLE hToken = NULL;
if( ::OpenProcessToken( ::GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
{
TOKEN_PRIVILEGES tp = { 0 };
if( ::LookupPrivilegeValue( NULL, lpszSystemName, &tp.Privileges[0].Luid ) )
{
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = ( bEnable ? SE_PRIVILEGE_ENABLED : 0 );
if( ::AdjustTokenPrivileges( hToken, FALSE, &tp,
sizeof( TOKEN_PRIVILEGES ), (PTOKEN_PRIVILEGES)NULL, NULL ) )
{
bRetVal = ( ::GetLastError() == ERROR_SUCCESS );
}
}
::CloseHandle( hToken );
}
}
return bRetVal;
}
Many people complained about thye lack of DDE support. They have had problems opening
documents directly from explorer double-clicking them - the old implementation
was just limiting its instance in any case. MDI applications
have DDE support, so the user has nothing to do for limiting
the application instance when double-clicking associated document, while SDI
hasn't.
Thinking how to solve that I eventually stumbled on the reg
file which is automatically generated for SDI apps (it contains all document
registration) and is inlcluded into the projects - and that was the solution! I
overrode CWinApp::RegisterShellFileTypes to perform DDE
registration for SDI apps also. If we look in the code we'll see
that first MFC registers the file types and then an additional loop is
performed over all document types registering DDE. This code was also
large enough to be placed here so you have to open the source files
again. Now if user clicks the associated document, the SDI application opens it in
the same instance, just the way MDI app does. The operating system handles that
without any help from the program.
But there is one more problem - if we run the application with the
associated document in parameter from the windows run dialog, then no
DDE will save us and a new instance will be started. To correct that,
application passes document file name to another instance and exits only if
command line contains FileNew or FileOpen parameters, in other cases such as
FilePrint, etc., it does not exit instance because framework will process these
commands in new instance in invisible mode and will exit itself.
Notes
When testing RegisterShellFileTypesEx again under XP, and then clicking the
associated file, OS gave me some strange message that the associated
file is not found and I should search for it, though OS opened it
normally. ( I remembered that it ran normally at work under 2000 and Windows
98). Cursing my code and everything I began to compute what could cause that
error, and checking registry values, removed one key by chance, after which all
gone perfect. If we look in the registry under the application document's key
"ddeexec" key we'll find "application" key. The reg file told us that the
"application" key is optional; it defaults to the app name in "command"
key. Well, I added XP version check that does not add any
"Application" key and even removes it if present in case of XP
OS. But frankly speaking I do not understand why it gives that error, and would
be very grateful to anyone who will explain it.
CWinAppEx Class Members
Data Members
m_uMsgCheckInst |
Holds application unique message ID. |
Operations
PostInstanceMessage |
Posts a message to another instance of the application (if any). |
EnableTokenPrivilege |
Enables or disables privileges in the specified access token (Windows NT only). |
RegisterShellFileTypes |
Registers all of your application's document types with the Windows File
Manager, also performs additional registration for SDI applications. |
UnregisterShellFileTypes |
Unregisters all of your application's document types with the Windows File
Manager, also unregisters additional data required for SDI applications. |
Overridables
InitInstance |
Initializes new instance of your application running, if has parameter
specified (GUID) then returns FALSE if any other instance of application with
this UID is found. |
OnAnotherInstanceMessage |
Application receives this message from another started instance. The default
implementation is as follows - the started application sends this message (with
its command line if needed) to the previous one, and the previous instance
receives it, processes the command line (if any) and popups the main window in
this function. |
Globals
GetApp |
Obtains a pointer to the CWinAppEx object. |
Version History
-
21-May-2002 - Posted the article.
-
27-Aug-2002 - Mostly rewritten code, added DDE support for SDI applications,
added token privilege adjustment.