The example application includes all currently developed plug-in sources
Table of content
Overview
In the past I have written a plug-in architecture for one of my MFC projects. This architecture,
although it worked, was limited in what it could provide, in that the executable/DLL had to know about
one another to a certain degree. I presented a sub set of this method in a previous article
Exporting a Doc/View from a dynamically
loaded DLL. What I wanted to do was provide a streamlined and consistent plug-in architecture to allow
any MFC app to be converted across with ease. I have also had requests to post an article on the subject.
That's how this library came about.
How does this library work?
The library is an MFC extension DLL which when linked to, provides a set of base classes which need to be
derived from in your MFC application. There are classes for Application, Mainframe, document, view(s), dialog
and plug-in map. If you derive your MFC project objects from these library ones, then by default they gain the
plug-in architecture which allows you to expand/modify their standard operations by providing additional
MESSAGE_MAP
s, menu options and accelerators. In fact the library itself can easily be extended
to cover additional window types, for example, if you made all your CEdit
objects inherit from a
base class equivalent to those in the library, you could make all your edit controls have plug-in features as well.
I just didn't take the class library that far.
When a DLL supplies a plug-in for one of the executable classes it does this as a MESSAGE_MAP
,
which is constructed in exactly the same manner as those created by Class Wizard. All these plug-in message
maps derive from the base class CPlugInMap
, so to create a new plug-in you would derive a new class from
this in a DLL.
When a plug-in enabled object type is created, the library queries all the loaded plug-in message map objects to see
whether they are a plug-in for the object type in question:
void CPIView::InitialisePlugIns()
{
CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
m_pMaps = pApp->GetMessageMaps(this, m_MapCount);
}
CPlugInMap** CPlugInApp::GetMessageMaps(CCmdTarget *pObj, int &count)
{
count = 0;
CRuntimeClass *pClass = pObj->GetRuntimeClass();
POSITION pos = CPlugInMap::GetHeadPosition();
while (pos)
{
if (CPlugInMap::GetAt(pos).m_pClass->IsPlugInFor(pClass))
{
count++;
}
CPlugInMap::MoveNext(pos);
}
CPlugInMap **pMaps = NULL;
if (count > 0)
{
pMaps = new CPlugInMap*[count];
pos = CPlugInMap::GetHeadPosition();
int index = 0;
while (pos)
{
if (CPlugInMap::GetAt(pos).m_pClass->IsPlugInFor(pClass))
{
CPlugInMap *pMap = CPlugInMap::GetAt(pos).m_pClass->CreateMapObject();
pMap->SetPlugInFor(pObj);
ASSERT(index < count);
pMaps[index] = pMap;
index++;
}
CPlugInMap::MoveNext(pos);
}
}
return pMaps;
}
This means that every plug-in object must override the virtual
function
bool CPlugInMap::IsPlugInFor(CRuntimeClass *pClass)
and use the CRuntimeClass*
object passed in to see whether it is an object type it is a plug-in for. In the MDI tab example provided this is done
like this:
bool CMFPlugIn::IsPlugInFor(CRuntimeClass *pClass)
{
return (_tcscmp(pClass->m_lpszClassName, _T("CMainFrame")) == 0);
}
In this case, our plug-in map is a plug-in for the CMainFrame
class object. A plug-in can be used for many
different objects, for example, the owner drawn menu plug-in just returns true
for all plugable objects so
that it can handle all the menu drawing for them.
Pre and Post calls to your plug-in maps
A message map which has a matching entry for the message being processed, will be called twice, once before the
regular message map class (the Pre call), and once after (the Post call). This allows you to do code before and/or after
the message being processed by the regular application supplied function (if there is one), or in the Pre call you
can choose to suppress the message entirely, so that the regular application message map function is not called at all.
Library change (V1.4) - If you suppress a message in the Pre function, then no Post message handlers in your own
or any other plug-ins will be called.
You may need to make sure, your code is only executed once, so you can check whether it is the Pre or
Post call to your plug-in message map object. The IsPreCall()
and IsPostCall()
member
functions allow you to check which call is being performed.
WARNING: In the case of mapping WM_CREATE
you would normally use the Post version of your
method as you may be wanting to create additional windows, and in such a case you need an already valid window to have
been created to be the parent of the new windows.
Menu's and Accelerators
To add support for new menu items or accelerator keys, the library allows every plug-in DLL to export two functions:
MergeMenu
MergeAccelerator
To add extra menu/accelerator items, you would create a DLL menu resource/accelerator with the extra items and
this menu/accelerator would be merged into those used by the document template of the object you want to modify:
extern "C" void MergeMenu(CMyMultiDocTemplate *pTemplate)
{
ASSERT(pTemplate != NULL);
CMenu docMenu;
CMenu append;
docMenu.Attach(pTemplate->m_hMenuShared);
append.LoadMenu(IDR_TABBARMENU);
VERIFY(PIMergeMenu(&docMenu, &append, true));
docMenu.Detach();
}
The above example adds all the menu commands which are present if the IDR_TABBARMENU
to all CDocTemplate
objects and the CMainFrame
default menu used when no documents are open. It is possible at this point to check
which CDocTemplate
object you want to add the menu items to. This would be done by calling the
CMyMultiDocTemplate::GetDocClass()
function and comparing the class name as required (the default CMainFrame
object does not have a class name).
Merging accelerators works exactly the same, except that you would call the PIMergeAccelerator(HACCEL& hDestination, HACCEL hToMerge)
function.
Adding additional document types to your application
The library also supports the addition of new document types to the application. If the DLL exports the functions:
GetDLLDocTemplateCount
GetDLLDocTemplate
During the application initialization phase, your application calls the plug-in library function RegisterDLLDocumentTypes()
which looks at all the loaded plug-in DLLs and adds any supplied document types:
void CPlugInApp::RegisterDLLDocumentTemplates()
{
CMyMultiDocTemplate *pDocTemplate = NULL;
for (int i = 0; i < m_PlugInDLLCount; ++i)
{
for (int j = 0;
j < m_pPlugInDLLs[i].GetDocTemplateCount(); ++j)
{
pDocTemplate = m_pPlugInDLLs[i].GetDocTemplate(j);
ASSERT(pDocTemplate);
AddDocTemplate(pDocTemplate);
}
}
}
So your DLL would supply all the Doc/View/Child frame object types used by your new document type.
It should also be noted that if a plug-in DLL supplied a new document/view class which derived from the plug-in
architecture, then these plug-in DLLs could also have plug-in maps/menus/accelerators supplied for them as well!
WARNING : The biggest problem you will have is that you must make sure that added menu commands and resources
have unique ID numbers across all your DLL/EXE projects in use. If not, then you could find multiple commands
being executed from a single menu option!
Well that's the general stuff taken care of.
Making your application plug-in enabled
To get the plug-in architecture to work for any window, you need to inherit from the correct base class
(listed later). These base classes have overridden either one or both of the virtual functions OnCmdMsg(...)
and OnWndMsg(...)
declared in the MFC. In these new functions, we query any loaded plug-ins for additional
message maps that need to be considered in the execution process. If they are found, then an object of that type is
created and added to the list of plug-ins for that window/document. This allows the plug-ins to persist their state
between messages.
There are some differences between the plug-in map and the regular one
When program execution reaches your plug-in map function, the this
pointer which you see is a pointer
to your CPlugInMap
inherited class object. After all, your plug-in needs to keep track of state information
about what it's doing, so it can access its own information, and can also query the CPlugInMap::m_pPlugInFor
member variable, which is a CCmdTarget*
pointer to the object that this is a plug-in for. If you need to
access the actual object type and its members, you will have to cast the pointer to the correct object type, such
as:
CMyObject* pMyObject = static_cast<CMyObject*>(m_pPlugInFor);
ASSERT(pMyObject);
The plug-in classes
To get the plug-in architecture to work for a new application/mainframe/document/view/dialog, you need to derive
your standard MFC class from the correct plug-in object type. These are:
-
CPlugInApp
: The main application plug-in which has the additional code to load the plug-in DLLs.
You need to inherit your CWinApp
derived application class from this and add the following code to your
class's CYourApp::InitInstance()
function to enable the plug-in architecture:
LoadStdProfileSettings();
LoadPlugInDLLs();
ReplaceDocManager();
CMyMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMyMultiDocTemplate(
IDR_APPPLUTYPE,
RUNTIME_CLASS(CAppPlugInCoreDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CAppPlugInCoreView));
AddDocTemplate(pDocTemplate);
RegisterDLLDocumentTemplates();
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
{
return FALSE;
}
m_pMainWnd = pMainFrame;
UpdateMenus();
UpdateAccelerators();
InitialisePlugIns();
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
To enable plug-ins for the other objects in your application, you must call InitialisePlugIns()
in the inherited class
object's constructor. This procedure queries the loaded plug-in DLL's for any plug-in maps which can be used for an object of that
type (be it, mainframe, documents, view etc...). These matches are performed on a class name basis.
By default the application looks for the plug-in DLL'S in the PlugIns sub-directory of your application's
executable location. Please make sure that this directory exists in your debug/release and installation directories and that
the plug-in DLLs are located there!
This class also provides the following additional function(s) in its public
interface:
CString GetApplicationPath()
- returns the location of the application executable - also used internally to locate the plug-in DLLs
int GetPlugInDLLCount() const;
- Returns the number of loaded plug-in DLLs
CDLLWrapper* GetDLL(int index);
- Returns a CDLLWrapper*
pointer to a loaded DLL.
You also need to add the line:
#include "PlugInLib.h"
to your applications stdafx.h file to automatically link to the library and use the exported classes.
CPIMainframe
- Inherit your CMainFrame
object from this class.
CPIChildframe
- Inherit your CChildFrame
object(s) from this class.
CPIDoc
- Inherit your CDocument
object(s) from this class.
CPIView
- Inherit your CView
object(s) from this class.
CPIScrollView
- Inherit your CScrollView
object(s) from this class.
CPIFormView
- Inherit your CFormView
object(s) from this class.
CPIDialog
- Inherit your CDialog
object(s) from this class.
You will also have to make any dialog class have run time class information available by adding these lines:
DECLARE_DYNCREATE(CYourDialogClass)
IMPLEMENT_DYNCREATE(CYourDialogClass, CPIDialog)
For all of these class types, you will need to call InitialisePlugIns()
in the constructor of your inherited class.
Why? Because the plug-in architecture does the matching using the class name and due to the rules of inheritance and the way
the RUNTIME_CLASS
information works, this will not be setup correctly until all the bases classes have been fully
constructed, so you would not get the right class name when checking if a class needed a plug-in map.
Creating a plug-in DLL
To create a new plug-in DLL for your now enabled plug-in application, use the VS option to create a new project of the type
MFC extension DLL.
- Once the project has been created add the line:
#include "PlugInLib.h"
to the stdafx.h file.
- Add a post build step
I also like to add a post build step to the DLL's build procedure, which copies the newly compiled version of the DLL
to the target application's DLL plug-in directory: copy Debug\SomeDLL.dll ..\TargetApplication\debug\PlugIns
This is done so that your application will always use the latest version during debugging etc.
- Create a plug-in
MESSAGE_MAP
object
Hmm, for some reason Visual Studio does not make this an easy option, so here is a copy of the boiler-plate code used to create a new plug-in MESSAGE_MAP
object.
#if !defined(NEED_A_NEW_DEFINE)
#define NEED_A_NEW_DEFINE
#if _MSC_VER > 1000
#pragma once
#endif
class CMyNewPlugInMapObject : public CPlugInMap
{
public:
DECLARE_DYNCREATE(CMyNewPlugInMapObject)
CMyNewPlugInMapObject();
CMyNewPlugInMapObject(bool special);
virtual ~CMyNewPlugInMapObject();
public:
public:
virtual CPlugInMap* CreateMapObject();
virtual bool IsPlugInFor(CRuntimeClass *pClass);
protected:
DECLARE_MESSAGE_MAP()
};
#endif
And for the .cpp file:
#include "stdafx.h"
#include "boiler_plate.h"
CMyNewPlugInMapObject plugIn(true);
IMPLEMENT_DYNCREATE(CMyNewPlugInMapObject, CPlugInMap)
CMyNewPlugInMapObject::CMyNewPlugInMapObject()
{
}
CMyNewPlugInMapObject::CMyNewPlugInMapObject(bool special)
{
AddObject();
}
CMyNewPlugInMapObject::~CMyNewPlugInMapObject()
{
}
BEGIN_MESSAGE_MAP(CMyNewPlugInMapObject, CPlugInMap)
END_MESSAGE_MAP()
CPlugInMap* CMyNewPlugInMapObject::CreateMapObject()
{
return new CMyNewPlugInMapObject;
}
bool CMyNewPlugInMapObject::IsPlugInFor(CRuntimeClass *pClass)
{
return (_tcscmp(pClass->m_lpszClassName, _T("ClassToPlugInFor")) == 0);
}
Copies of this code can be found in the library. See the files boiler_plate.h and boiler_plate.cpp.
Once you have an object created and registering with the library, it should become active. Just add regular MESSAGE_MAP
entries as you would,
in a regular MFC project to handle the messages you wish to.
Note the object being declared at the beginning of the file:
CMyNewPlugInMapObject plugIn(true);
This global version of the object is used to register the plug-in map object with the library. Without it, your plug-in would not be called.
Additional functions to export
Depending on what features you need in your application, your plug-in DLLs will need to export any of the following functions to enable the features you need.
MergeMenu(CMyMultiDocTemplate* pTemplate)
This function is used by the framework to add additional menu items into the application's document template menus. If your DLL adds new functions, you
may need to expose menu options for them. This is where you add them. In your DLL create the resource template for the menu items you wish to add. You would
then export this function from your DLL by adding a global function to it like this:
extern "C" void MergeMenu(CMyMultiDocTemplate *pTemplate)
{
ASSERT(pTemplate != NULL);
if (_tcscmp(L"CAppPlugInCoreDoc", pTemplate->GetDocClass()) == 0)
{
CMenu docMenu;
CMenu append;
docMenu.Attach(pTemplate->m_hMenuShared);
append.LoadMenu(IDR_MENU1);
VERIFY(PIMergeMenu(&docMenu, &append, true));
docMenu.Detach();
}
}
If you do not export directly using __declspec(dllexport)
, you need to add a line to the DLL project's .DEF
file:
; PlugIn1.def : Declares the module parameters for the DLL.
LIBRARY "SomePlugIn"
DESCRIPTION 'SomePlugIn Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
MergeMenu
MergeAccelerator(CMyMultiDocTemplate* pTemplate)
This function is used by the framework to add additional accelerator items into the application's document template accelerator. If your DLL adds new functions, they may have accelerator keys for them. This is where you add them. In your DLL create the resource template for the accelerator items you wish to add.
extern "C" void MergeAccelerator(CMyMultiDocTemplate *pTemplate)
{
ASSERT(pTemplate != NULL);
if (_tcscmp(L"CAppPlugInCoreDoc",
pTemplate->GetDocClass()) == 0)
{
HACCEL hMerge = LoadAccelerators(hDLLInstance,
MAKEINTRESOURCE(IDR_ACCELERATOR1));
ASSERT(hMerge);
VERIFY(PIMergeAccelerator(pTemplate->m_hAccelTable, hMerge));
DestroyAcceleratorTable(hMerge);
}
}
Again, you may need to add this line to your projects .DEF file in the exports section:
MergeAccelerator
Note : As mentioned above, make sure that your new menu options etc have unique IDs across EXE and DLLs!
InitialiseDLL(CWinApp *pApp)
This procedure is called by the framework, after all the plug-in DLLs have been loaded to allow then to initialise any variables etc that they may need to. Put your DLL setup code in this exported function.
ReleaseDLL()
As your application shuts down, this function will be called just before the framework unloads your plug-in DLL. You should release all dynamically allocated objects and resources by the end of this procedure if you have any, in your application.
GetDLLDocTemplateCount()
Use this function to return the number of additional document templates supported by the plug-in DLL.
GetDLLDocTemplate(int index)
Use this function to return the document templates that will be registered with the application. This will allow your plug-in application to support extra document types. After all you might as well make full use of that MDI interface!
Additional debugging notes
When you need to debug your application/DLLs, the best way I have found is to insert the DLL projects into the workspace of the application. You can then also add the DLLs in the application's projects settings to the Debug:Additional DLLs section, to include your plug-in DLLs directly. As the VC debugger will know about them all at start-up time, you will be able to set breakpoints in both your application code and your DLL code.
You should also set the DLL projects as dependencies of the EXE so that when you build, the versions available will always be up to date.
Making your application load faster
Your application and its plug-ins will load faster, if you re-base each of the DLLs in your project(s) so that they do not clash in memory usage. Every time they do, windows will relocate the offending DLL and this takes time. You can tell when its happening in debug mode as you get a message such as:
LDR: Automatic DLL Relocation in AppPlugInCore.exe
LDR: Dll m_res.dll base 10000000 relocated due to collision with
E:\Personal\CodeProject\Projects\AppPlugInCore\Debug\PlugInLibrary.dll
Its easy enough to change, just go to the DLL's project settings in the Link tab and select the Output category. Choose a base address that will not cause it to overlap with an existing DLL in your project.
Other notes
The library should be UNICODE compatible. I just haven't tested it, but where strings manipulation/comparison code is used, I have tried to use the _tcs...
version which will compile to the correct code under UNICODE or a standard ANSI application.
There are still some areas of this library that need extra extension. Off the top of my head the following should be added. People who can provide the functions to do this will be highly praised.
- There is no support for serialization, so your plug-ins cannot persist their data. I am thinking of doing this by providing a virtual function in
CPlugInMap
which is called
by the CPIDoc::Serialize()
as a post option only. This will allow them to tag their data onto the end of the archive, with associated checking to make sure it's your
data when loading. In fact a similar method should be used for all types of virtual functions, it's just a very large amount of work that I do not have time for, right this minute. :(
- Not all of the major plug-in classes have been 100% checked at this time.
Problems encountered
- When I set out to develop this library I started on my home PC which runs Windows 98, I then continued this code on my work PC, which runs Windows NT. At this point I kept getting a run time error in Samuel Gonzalo's
CFileFinder
extension class because GetLongPathName()
was not present in NT's version of kernel32.dll. In the end I just commented this section from CFileFinder
as I was not making use of it.
- Nasty casting - To get the
OnWndMsg
plug-in code to work, I had to do a nasty cast 102 times, 1 for each of the window's message map prototypes which are called in the function CPlugInApp::CallWindowMessageMap()
, as my CPlugInMap
object inherits from CCmdTarget
and not CWnd
like the compiler is expecting. This just gets around the problem. As far as I can tell, there is no danger in doing it this way, as execution passes directly across to the message map function without any problems.
- When it came to merging accelerator tables, a search showed no articles which covered this. So after a little bit of head scratching and perseverance with the MSDN, I wrote a really quick and simple function to do it. :-D
- The main code of the library has been through about 3 iterations of modification, where some of the core functionality was changed.
- I never seemed to have enough time to work on this, or play CounterStrike.
Code acknowledgements, references and thanks
Whilst constructing this library I used code/information from the following articles:
Thanks goes to
Update History
The library and example(s) have been through the following changes:
V1 3rd October 2002
Initial release.
V1.1 8th October 2003
Several CP members reported a problem when using the library on Windows XP/Me where the libarry crashes during the close down of the application. It took a compiler upgrade on my work XP
machine before I was able to reproduce the problem. Once I could do that, I then had to work around the fact that JIT debugging does not work on
my PC, so after using AllocConsole
and WriteFile
to allow me to trace the execution path, I found that when the CMainFrame
object recieves the
WM_NCDESTROY
message, it calls delete this
on itself! So when the the library tried to call the Post message handler on any plug-in maps, they were in fact
invalid as they had already been deleted. A code clean-up in the destructor of this and all the other classes (just in case!) solves the problem.
V1.2 9th May 2004
I reworked the plug-in destruction code to better solve the shut down problem experienced by early users of the class. Where before I was relying
on the NULL pointers etc not to be called, this was not quite good enough. I made use of the method suggested by Brian (H) in the comments section below. Doing this
fully cleared up any close/shut down problems I have seen with the library.
Additional work was done on the method message supression and plug-in use. This was so that the owner-drawn menu plug-in would work correctly.
to upgrade any existing plug-ins to use the new library version, you have to change any code that reads:
virtual LPCTSTR GetClass();
LPCTSTR CMFPlugIn::GetClass()
{
return L"CMainFrame" ;
}
to
virtual bool IsPlugInFor(CRuntimeClass *pClass);
bool CMFPlugIn::IsPlugInFor(CRuntimeClass *pClass)
{
return (_tcscmp(pClass->m_lpszClassName, L"CMainFrame") == 0);
}
This allows a plug-in to work for multiple target classes.
V1.3 21st May 2004
During the development of the "Enhanced print preview" plug-in, a flaw in the message suppression was found. This occurs when you need to
suppress a message and a hidden message pump occurs during functions called by the plug-in map message handler. To fix the problem, I needed
to introduce a suppression stack which handles the saving of the message suppression state on a per message basis. This has no knock on effect
to any existing plug-ins.
V1.4 7th June 2004
While developing the "Single Instance" plug-in, a problem with the message suppression was found. The library needed to be changed such that
whenever a plug-in suppressed a message in the Pre handler, then no Post plug-in handlers get called. This required changes to all
classes which implement the virtual overrides OnCmdMsg()
and onWndMsg()
.
Enjoy!