An MFC extension library to enable DLL plug-in technology for your application using MESSAGE_MAPs






4.85/5 (43 votes)
A plug-in architecture which allows you to write plug-in DLLs for your application and extend/modify its functionality.
The example application includes all currently developed plug-in sources
Table of content
- Overview
- How does this library work?
- Pre and Post calls to your plug-in maps
- Menu's and Accelerators
- Adding additional document types to your application
- Making your application plug-in enabled
- The plug-in classes
- Creating a plug-in DLL
- Additional functions to export
- Additional debugging notes
- Problems encountered
- Code acknowledgements, references and thanks
- Update History
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:
// example function from the CPIView class void CPIView::InitialisePlugIns() { CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp()); // get our pointers to any plug in maps m_pMaps = pApp->GetMessageMaps(this, m_MapCount); } CPlugInMap** CPlugInApp::GetMessageMaps(CCmdTarget *pObj, int &count) { count = 0; CRuntimeClass *pClass = pObj->GetRuntimeClass(); // get the class name to find plug in maps for // for each loaded DLL see whether a message map for // this class has been defined POSITION pos = CPlugInMap::GetHeadPosition(); while (pos) { if (CPlugInMap::GetAt(pos).m_pClass->IsPlugInFor(pClass)) { // its a match, count it count++; } CPlugInMap::MoveNext(pos); } CPlugInMap **pMaps = NULL; if (count > 0) { // now return the list of CPlugInMap* pointers pMaps = new CPlugInMap*[count]; // allocate pointer array 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); // found more than the last time around! 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 // when exported is used to merge menus MergeAccelerator // when exported is used to merge accelerator tables
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:
// example MergeMenu from MDI tabs example extern "C" void MergeMenu(CMyMultiDocTemplate *pTemplate) { ASSERT(pTemplate != NULL); // by default we merge our menu commands with all menu's! 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 // returns the number of exported document types GetDLLDocTemplate // returns the CMyMultiDocTemplate pointer(s)
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); // DLL's said it had one, but didn't supply it! 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); // asserts if cast failed, its not the right type of object! 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'sCYourApp::InitInstance()
function to enable the plug-in architecture:// InitInstance code // Load standard INI file options (including MRU) LoadStdProfileSettings(); // Load the plug-in DLL's. // This needs to be done before any document templates // are registered and before the mainframe is created // as the plug-in DLL's can hook the mainframe message. // Creation is a required message to hook // to allow additional toolbars and // floating windows etc to be added. LoadPlugInDLLs(); ReplaceDocManager(); // Register the application's document // templates. Document templates // serve as the connection between // documents, frame windows and views. CMyMultiDocTemplate* pDocTemplate; pDocTemplate = new CMyMultiDocTemplate( IDR_APPPLUTYPE, RUNTIME_CLASS(CAppPlugInCoreDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CAppPlugInCoreView)); AddDocTemplate(pDocTemplate); // now register any plug-in DLL document templates RegisterDLLDocumentTemplates(); // create main MDI Frame window CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) { return FALSE; } m_pMainWnd = pMainFrame; UpdateMenus(); UpdateAccelerators(); InitialisePlugIns(); // this does it for the application object only // Parse command line for standard shell //commands, DDE, file open 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 DLLsint GetPlugInDLLCount() const;
- Returns the number of loaded plug-in DLLsCDLLWrapper* GetDLL(int index);
- Returns aCDLLWrapper*
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 yourCMainFrame
object from this class.CPIChildframe
- Inherit yourCChildFrame
object(s) from this class.CPIDoc
- Inherit yourCDocument
object(s) from this class.CPIView
- Inherit yourCView
object(s) from this class.CPIScrollView
- Inherit yourCScrollView
object(s) from this class.CPIFormView
- Inherit yourCFormView
object(s) from this class.CPIDialog
- Inherit yourCDialog
object(s) from this class.
You will also have to make any dialog class have run time class information available by adding these lines:
// to .h file DECLARE_DYNCREATE(CYourDialogClass) // to .cpp file 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
objectHmm, 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.// Some plug-in header // ////////////////////////////////////////////////////////////////////// #if !defined(NEED_A_NEW_DEFINE) #define NEED_A_NEW_DEFINE #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 class CMyNewPlugInMapObject : public CPlugInMap { public: DECLARE_DYNCREATE(CMyNewPlugInMapObject) CMyNewPlugInMapObject(); CMyNewPlugInMapObject(bool special); virtual ~CMyNewPlugInMapObject(); // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CMFPlugIn) //}}AFX_VIRTUAL virtual CPlugInMap* CreateMapObject(); virtual bool IsPlugInFor(CRuntimeClass *pClass); // Implementation protected: // Generated message map functions //{{AFX_MSG(CMyNewPlugInMapObject) //}}AFX_MSG DECLARE_MESSAGE_MAP() }; #endif // !defined(NEED_A_NEW_DEFINE)
And for the .cpp file:
// Some plug-in implementation of the CMyNewPlugInMapObject class. // ////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "boiler_plate.h" ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CMyNewPlugInMapObject plugIn(true); IMPLEMENT_DYNCREATE(CMyNewPlugInMapObject, CPlugInMap) CMyNewPlugInMapObject::CMyNewPlugInMapObject() { } CMyNewPlugInMapObject::CMyNewPlugInMapObject(bool special) { AddObject(); } CMyNewPlugInMapObject::~CMyNewPlugInMapObject() { } BEGIN_MESSAGE_MAP(CMyNewPlugInMapObject, CPlugInMap) //{{AFX_MSG_MAP(CMyNewPlugInMapObject) //}}AFX_MSG_MAP END_MESSAGE_MAP() ////////////////////////////////////////////////// // CMyNewPlugInMapObject message handlers 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 __declspec(dllexport) // MergeMenu(CMyMultiDocTemplate *pTemplate) extern "C" void MergeMenu(CMyMultiDocTemplate *pTemplate) { ASSERT(pTemplate != NULL); // Note that the default menu for // the mainframe will have a GetDocClass of "" if (_tcscmp(L"CAppPlugInCoreDoc", pTemplate->GetDocClass()) == 0) { CMenu docMenu; CMenu append; docMenu.Attach(pTemplate->m_hMenuShared); append.LoadMenu(IDR_MENU1); // merge in the new menu items VERIFY(PIMergeMenu(&docMenu, &append, true)); // make sure the menu is not // destroyed as its owned by the document template 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 __declspec(dllexport) // MergeAccelerator(CMyMultiDocTemplate *pTemplate) extern "C" void MergeAccelerator(CMyMultiDocTemplate *pTemplate) { ASSERT(pTemplate != NULL); // Note that the default accelerator for //the mainframe will have a GetDocClass of "" if (_tcscmp(L"CAppPlugInCoreDoc", pTemplate->GetDocClass()) == 0) { HACCEL hMerge = LoadAccelerators(hDLLInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1)); // did the accelerator fail to be loaded? //Does it exist 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 theCPIDoc::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 becauseGetLongPathName()
was not present in NT's version of kernel32.dll. In the end I just commented this section fromCFileFinder
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 functionCPlugInApp::CallWindowMessageMap()
, as myCPlugInMap
object inherits fromCCmdTarget
and notCWnd
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:
- CFileFinder - Extend the functionality of CFileFind MFC class by Samuel Gonzalo
- Exporting a Doc/View from a dynamically loaded DLL by myself.
- Merging Two Menus by Oskar Wieland
- Automatic Tab Bar for MDI Frameworks by Paul Selormey - This is used as an example plug-in DLL for an MDI application!
- Exporting C++ Classes from an MFC Extension DLL by Steve Driessens
Thanks goes to
- Tomasz Sowlinski - Who answered some of my questions in the VC++ forum
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:
// header virtual LPCTSTR GetClass(); // c++ file LPCTSTR CMFPlugIn::GetClass() { // return the name of the class which this // is a plug in map for, e.g. "CMyApp" return L"CMainFrame" ; }
to
// header virtual bool IsPlugInFor(CRuntimeClass *pClass); // c++ file 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!