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.
When I first published the MFC plug-in architecture I always intended to write a series of standard
plug-ins which add functionality to all applications. This is the second in that series, the first
being the example plug-in with the main article.
This article introduces a new plug-in that handles owner drawn menus using a modified version of
Brent Corcums
Cool Owner Drawn Menus with Bitmaps - Version 3.03.
I will cover in this article how I developed the plug-in and problems that were encountered while doing so.
Initial work
When I first started to develop this plug-in I realized that there was a problem in the actual
plug-in library. This was that you had to provide the name of the class(es) that the plug-in was used in.
In the case of owner-drawn menus, we need to be a plug-in for many classes, except I do not know the
names of all of these in advance.
LPCTSTR CMFPlugIn::GetClass()
{
return L"CMainFrame";
}
I needed to switch around the logic such that the plug-in gets asked "Are you a plug-in for this class?" and
returns true
/false
itself instead of supplying a
class name. So in our case, we just return true
for all objects which are plug-in enabled
so that we can handle making any menus they show ownerdrawn.
bool CMFPlugIn::IsPlugInFor(CRuntimeClass *pClass)
{
return (_tcscmp(pClass->m_lpszClassName, "CMainFrame") == 0);
}
This also forced me to re-work the method of suppressing message processing back in the main application, as
previously you had to cast the m_pPlugInFor
pointer to the right object and set a member variable.
Which is not really possible when you do not know what class you are a plug in for!
Since this plug-in only works with V1.2 or later of the library (V1.2 has the required modifications to
the library to properly support this plug-in), existing users of the library should
visit the main article and upgrade to the latest source versions. You will also have to update any of your
own existing plug-ins to conform with the changes made. Instructions for this procedure are present in
the main article.
What messages do we need to implement
So we want to do an owner drawn menu plug-in, which messages should this plug-in handle to implement
its behaviour? Well it comes down to just 3, these are:
WM_INITMENUPOPUP
This message is sent when a menu is about to be displayed, in fact just before any popup menu or when
the user switches between multiple top level menus (e.g. between File and Edit). In this message handler,
we need to set the ownerdrawn style bit MF_OWNERDRAW
on every menu item in the menu about to be
displayed.
void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
if (IsPostCall())
{
if (pPopupMenu != NULL)
{
int itemCount = pPopupMenu->GetMenuItemCount();
for(int item = 0; item < itemCount; item++)
{
pPopupMenu->ModifyMenu(item,
MF_BYPOSITION | MF_OWNERDRAW,
itemID,
(LPCTSTR)pPopupMenu->m_hMenu);
}
}
}
}
Because we do the MF_OWNERDRAW
style setting on demand, we do not need to subclass or replace
any of the existing CMenu
member functions. The programmer does not need to worry about keeping the
menu(s) they modify in the correct ownerdrawn state.
As the library supports Pre and Post calls on plug-in messages and we do not want to do this
flag setting multiple times, we need to choose whether we do it before (pre) or after (post) the standard MFC
processing of the message. I chose after for these reasons:
- MFC calls the
ON_UPDATE_COMMAND_UI
handlers for the menu items
- MFC expands some menu options (e.g. recent file list, open documents in Window) with extra menu options, and we need
all items in the menu to be ownerdrawn.
WM_MEASUREITEM
Once a menu item has the MF_OWNERDRAW
style bit set for it, the application is going to be receiving
WM_MEASUREITEM
messages so that the OS knows how big each display element is when a menu item is about
to be displayed. This message is a Pre message handler as we need to suppress the message being processed in the
regular MFC call list. Suppressing this message stops MFC TRACE
ing out a warning message when in debug
as it thinks the application is not handling the message (which it is not, as our plug-in is).
In this message, we also have to return the size of the menu option being measured. A modification of Brents BCMenu
code supplies the required functionality. That code is not covered here, examine the source fo further information.
WM_DRAWITEM
Once a menu item has the MF_OWNERDRAW
style bit set for it, the application is going to be receiving
these messages so that the items get drawn when required. This is usually the complicated bit of implementing
owner drawn menus. Once again, a modified version of the BCMenu::DrawItem
code comes to my rescue!
I did initially try and implement my own owner drawn menu code, but it was too much work. :(
These messages should only be processed when the object type is ODT_MENU
. As WM_MEASUREITEM
and WM_DRAWITEM
can be received for many different control types.
void CODMenu::OnMeasureItem(int nIDCtl,
LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
if (IsPreCall() && lpMeasureItemStruct->CtlType == ODT_MENU)
{
MeasureItem(lpMeasureItemStruct);
SuppressThisMessage();
}
}
void CODMenu::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
if (IsPreCall() && lpDrawItemStruct->CtlType == ODT_MENU)
{
DrawItem(lpDrawItemStruct);
SuppressThisMessage();
}
}
Setting up the menu's image lists
The BCMenu
class would normally have calls made to LoadToolbar
during application
initialization.
This is something this plug-in cannot have done for it, so it needs to find and use all the toolbar resources in the application
and other plug-ins automatically. So how do we do this?
Well, when the library has been fully initialized and all plug-in DLLs loaded, each plug-in DLL receives a call to its
IntialiseDLL()
function (if it has one implemented). We need to put some code in this function to scan the
exe/dlls for any toolbar resources, a quick search through the MSDN
turned up the function
EnumResourceNames
, which looks an ideal candidate to do this.
extern "C" void InitialiseDLL()
{
CODMenu::EnumerateAndLoadToolbars();
}
If you look at the EnumResourceNames
function prototype:
WINBASEAPI BOOL WINAPI EnumResourceNamesA(
HMODULE hModule,
LPCSTR lpType,
ENUMRESNAMEPROC lpEnumFunc,
LONG lParam);
WINBASEAPI BOOL WINAPI EnumResourceNamesW(
HMODULE hModule,
LPCWSTR lpType,
ENUMRESNAMEPROC lpEnumFunc,
LONG lParam);
You can see that it requires an HMODULE
for each call. That will be the HINSTANCE
of the application
or the HINSTANCE
of each loaded plug-in DLL.
But how do we get the HINSTANCE
s of the loaded DLLs? This was another problem in the library,
which I solved by adding 2 extra functions to the main CPlugInApp
class:
int CPlugInApp::GetPlugInDLLCount()
Returns the number of plug-in DLLs.
CDLLWrapper* CPlugInApp::GetDLL(int index)
returns a pointer to the object wrapping the plug-in
DLLs exposed
The class CDLLWrapper
also had one extra interface function added:
HINSTANCE CDLLWrapper::GetHInstance()
returns the HINSTANCE
of the plug-in DLL.
If we have the HINSTANCE
of each module we need to search, we can use the system function
EnumResourceNames()
and pass through RT_TOOLBAR
as the type of resource we want to enumerate.
The system then calls a static
member CALLBACK
procedure and we get the information there on
every toolbar found in every module!
void CODMenu::EnumerateAndLoadToolbars()
{
CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
if (pApp)
{
TRACE("Enumerating application\n");
EnumResourceNames(pApp->m_hInstance, RT_TOOLBAR,
(ENUMRESNAMEPROC)EnumResNameProc, 0);
int dllCount = pApp->GetPlugInDLLCount();
for(int dll = 0 ; dll < dllCount; dll++)
{
TRACE("Enumerating DLL %1d\n", dll);
EnumResourceNames(pApp->GetDLL(dll)->GetHInstance(),
RT_TOOLBAR,
(ENUMRESNAMEPROC)EnumResNameProc,
0);
}
}
In the callback we load the toolbar resource entry which specifies the WM_COMMAND
id's for each
toolbar button. We are also able to load the bitmap resource with the same name and add this on-mass to our imagelist
which is used to plot the actual toolbar images used when the menu is displayed.
But how do we get the button ID's from the toolbar resource? We do not know its structure!
Toolbar resource structure
The standard MFC MDI toolbar is defined in the .rc file like this:
IDR_MAINFRAME TOOLBAR DISCARDABLE 16, 15
BEGIN
BUTTON ID_FILE_NEW
BUTTON ID_FILE_OPEN
BUTTON ID_FILE_SAVE
SEPARATOR
BUTTON ID_EDIT_CUT
BUTTON ID_EDIT_COPY
BUTTON ID_EDIT_PASTE
SEPARATOR
BUTTON ID_FILE_PRINT
SEPARATOR
BUTTON ID_APP_ABOUT
BUTTON ID_CONTEXT_HELP
END
When we enumerate each toolbar resource and load it, we gets a pointer to the resource like this:
BOOL CALLBACK CODMenu::EnumResNameProc(HMODULE hModule,
LPCTSTR lpszType, LPTSTR lpszName, LONG lParam)
{
TRACE("Toolbar found, module %x, Type %1d, Name %1d\n",
hModule, lpszType, lpszName);
HRSRC hrsrcToolbar = ::FindResource(hModule, lpszName, lpszType);
HGLOBAL hToolbar = ::LoadResource(hModule, hrsrcToolbar);
int size = ::SizeofResource(hModule, hrsrcToolbar);
WORD* pToolbarData = (WORD*)::LockResource(hToolbar);
This gives us a WORD*
pointer to the toolbar resource, but what does the data mean? After a little investigation what
we have is a pointer to a structure like this:
struct ToolbarResource
{
int version;
int imageSizeX;
int imageSizeY;
int tableEntries;
int buttons[tableEntries];
};
So having learnt this we load and construct the map as follows:
if (pToolbarData[0] == 0x0001)
{
if (pToolbarData[1] == m_iconX && pToolbarData[2] == m_iconY)
{
CBitmap toolbarBitmap;
HINSTANCE old = AfxGetResourceHandle();
AfxSetResourceHandle(hModule);
toolbarBitmap.LoadBitmap(lpszName);
AfxSetResourceHandle(old);
for(int buttonIndex = 0 ; buttonIndex < pToolbarData[3] ;
buttonIndex++)
{
if (pToolbarData[4 + buttonIndex] != 0)
{
if (m_commandToImage.find(pToolbarData[4 + buttonIndex])
== m_commandToImage.end())
{
m_commandToImage[pToolbarData[4 + buttonIndex]]
= m_commandToImage.size();
}
else
{
TRACE("Warning, duplicate toolbar button"
" %1d found, using first\n",
pToolbarData[4 + buttonIndex]);
}
}
}
m_buttonImages.Add(&toolbarBitmap, ::GetSysColor(COLOR_3DFACE));
toolbarBitmap.DeleteObject();
}
}
return TRUE;
}
During the process of enumerating the toolbars, we also construct a std::map<int, int>
object which relates
WM_COMMAND
ids to image list index position (where the image is in the list).
Once all toolbars have been enumerated and the map constructed, we can create the disabled bitmap images used when the menu
is rendered:
CBitmap disabledImage;
CWindowDC dc(NULL);
dc.SaveDC();
disabledImage.CreateCompatibleBitmap(&dc, m_iconX, m_iconY);
dc.SelectObject(&disabledImage);
for(int image = 0 ; image < m_buttonImages.GetImageCount() ; image++)
{
CBitmap bmp;
GetBitmapFromImageList(&dc, &m_buttonImages, image, bmp);
DitherBlt3(&dc, bmp, ::GetSysColor(COLOR_3DFACE));
m_disabledImages.Add(&bmp, ::GetSysColor(COLOR_3DFACE));
}
dc.RestoreDC(-1);
Doing this at the start saves us on processing time during menu display, as Brents original BCMenu
code would
generate the disabled bitmap image every time it needed to be rendered.
System menu icons
To get system menu icons to render correctly, I added a toolbar resource to the ownerdrawn menu plug-in. which is defined as follows:
IDR_TOOLBAR TOOLBAR DISCARDABLE 16, 15
BEGIN
BUTTON 61472
BUTTON 61488
BUTTON 61536
BUTTON 61728
END
The numbers shown are the actual code values for SC_CLOSE, RC_RESTORE...
etc. I also added the toolbar image:
Problems encountered
While I was writing this plug-in I encountered the following problems:
What item am I measuring?
To be able to correctly measure an item, I was originally setting the menu items user data to the HMENU
of which it is a member of. This is because the LPMEASUREITEMSTRUCT
does not contain information of which menu you are actually
measuring an item for. Initially this appeared to be OK. I was using the itemID
value supplied to get the menu info
MF_BYCOMMAND
, this works fine for normal menu items, but as soon as you get a popup menu or a separator, you get an ID
of -1
or 0
which GetMenuItemInfo()
would fail on. So I was unable to measure popup/separator menu items!
After some further thought, I no longer saved the HMENU
in the user data, but the position in the menu.
This allowed the GetMenuItemInfo()
function to work correctly on popup menus when
retrieving information
MF_BYPOSITION
. But now I do not know which menu to do the call on! So I introduced a class member variable
HMENU m_hMenuBeingProcessed
and set this in the OnInitMenuPopup
handler. This works OK as we never
get a WM_INITMENUPOPUP
message between measuring all the items in that menu.
Saving the user data as the position in the menu also helps us process the menu item fully in the DrawItem
handler, although we are provided with the correct HMENU
of the item we are drawing for there.
All menu items are always enabled
When I wanted to get owner drawn disabled/checked items working, I added some items to the menu, setup
the correct ON_UPDATE_COMMAND_UI
handlers and set the relevant states. But in my DrawItem
code, the state of the item was always enabled and unchecked. In fact the only flag which was different between
calls was the ODS_SELECTED
for which item was currently highlighted. I tried various methods in the
DrawItem
procedure to get the correct state of the item with no success. So I went away and slept on this
problem.
When I awoke, I had the solution: The problem was in the OnInitMenuPopup
function where I was iterating
the menu about to be displayed and setting the MF_OWNERDRAW
flag. As this is a Post call, the MFC
has already done all its calls through to the ON_UPDATE_COMMAND_UI
handlers, and my MenuModify
call was just setting the MF_OWNERDRAW
flag only, thus overwriting the actual state of the item setup by
the ON_UPDATE_COMMAND_UI
handlers. The solution was to read the state of the menu item at this point and
or |
in the MF_OWNERDRAW
flag and not lose the state.
void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu,
UINT nIndex, BOOL bSysMenu)
{
if (IsPostCall())
{
if (pPopupMenu != NULL)
{
m_menuBeingProcessed = pPopupMenu->m_hMenu;
int itemCount = pPopupMenu->GetMenuItemCount();
for(int item = 0; item < itemCount; item++)
{
int itemID = pPopupMenu->GetMenuItemID(item);
MENUITEMINFO itemInfo;
memset(&itemInfo, 0, sizeof(MENUITEMINFO));
itemInfo.cbSize = sizeof(MENUITEMINFO);
itemInfo.fMask = MIIM_STATE;
pPopupMenu->GetMenuItemInfo(item,
&itemInfo,
TRUE);
pPopupMenu->ModifyMenu(item,
itemInfo.fState | MF_BYPOSITION | MF_OWNERDRAW,
itemID,
(LPCTSTR)item);
}
}
}
}
System menu(s) do not display correctly
During the debugging phase of this plug-in, I noticed that when we were rendering a system menu, we were drawing the
items incorrectly. After further study I found out that system menus do not seem to work correctly, ever! The code correctly
sets the MF_OWNERDRAWN
bit on every item of the system menu, and we receive the WM_DRAWITEM
message
correctly for all the items, yet we only receive 2 of the WM_MEASUREITEM
messages, when there are 7 items in the
menu.
The items we received the messages for were: Restore and Move. These menu strings are much shorter than
Close\tAlt + F4, which is the one item that does not render correctly (in an unmodified system menu).
Well the only way around this problem I could find was a mini-hack! That is we set a member variable flag in the class
when we are about to measure the items of a system menu:
m_bSysMenu = (bSysMenu != FALSE);
Then at the end of the MeasureItem()
procedure, we check the flag. If true
and the item we have
just finished measuring is smaller than the width for an item with the text of Close\tAlt + F4 then I just set the width
of the item to this so that the system menu will render correctly.
if (m_bSysMenu)
{
if (lpMIS->itemWidth < m_minSystemMenuWidth)
{
lpMIS->itemWidth = m_minSystemMenuWidth;
}
}
Default items not measured correctly
When I added default menu items, which have the ODS_DEFAULT style in the state member, these items never returned
the correct length of the text for these items. The code I had to measure the item was this:
if (state & ODS_DEFAULT)
pFont = pDC->SelectObject(&m_menuFontBold);
else
pFont = pDC->SelectObject(&m_menuFont);
After some investigation it turned out that we never receive or can get the correct state information of the menu item in the
OnMeasureItem
function. The easiest way of dealing with this issue was just to always measure all items using the
bold font. This would make standard menu items appear slightly larger, but makes default items render correctly, which
I think users of the class would prefer.
pFont = pDC->SelectObject(&m_menuFontBold);
Conclusion
This plug-in demonstrates some of the power of the plug-in architecture and provides some standard functionality we as developers
like to have in our applications. It fast tracks the development cycle of the end product and does so in a modular fashion.
Along the way, I have illustrated some of the problems I encountered and how I resolved them.
Well that's about it for this plug-in. Its been an interesting journey into menus, and I have also learnt a few nice enumeration
functions and understand menus in a much better way. I wrestled with Brents owner drawn menu code - the main problem (from my
point of view) being his coding style. Overall the whole project integrates very well.
I hope you have enjoyed reading (and using) this article.
References
Here are a list of related articles used in the making of this plug-in
Version history
- V1.0 10th May 2004 - Initial release