Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / MFC
Article

MFC extension library - A plugin to handle owner drawn menus

Rate me:
Please Sign up or sign in to vote.
4.41/5 (7 votes)
11 May 2004CPOL12 min read 75.9K   1.6K   45   3
Continuation of the Plugin Architecture series

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.

Example application running with this plug-in

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.

// old method
LPCTSTR CMFPlugIn::GetClass()
{
    // return the name of the class which this is 
   // a plug in map for, e.g. "CMyApp"
    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.

// new method
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.

// early version of the OnInitMenuPopup() function
void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
    if (IsPostCall())
    {
        // iterate any menu about to be displayed and make sure
        // all the items have the ownerdrawn style set
        // We receive a WM_INITMENUPOPUP as each menu 
        // is displayed, even if the user
        // switches menus or brings up a sub menu. This means we only need to
        // set the style at the current popup level.
        // we also set the user item data to the HMENU handle to allow
        // us to measure/draw the item correctly later
        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 TRACEing 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)
{
    // check that the call is for an owner drawn menu
    if (IsPreCall() && lpMeasureItemStruct->CtlType == ODT_MENU)
    {
        MeasureItem(lpMeasureItemStruct);
        // we have handled it, stop it flowing through regular MFC
        SuppressThisMessage();
    }
}

void CODMenu::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    if (IsPreCall() && lpDrawItemStruct->CtlType == ODT_MENU)
    {
        DrawItem(lpDrawItemStruct);
        // we have handled it, stop it flowing through regular MFC
        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:

// ANSI
WINBASEAPI BOOL WINAPI EnumResourceNamesA(
        HMODULE hModule, 
        LPCSTR lpType, 
        ENUMRESNAMEPROC lpEnumFunc, 
        LONG lParam);
// UNICODE
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 HINSTANCEs 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()
{
    // This procedure is called by the InitialiseDLL function

    // When we intialise, we need to enumerate all the toolbar
    // resources in all the plug-in DLLs and the application
    // this allows us to generate a map of menu items which have toolbar
    // images available for them
    
    // First enumerate and use the appplications toolbar(s)
    CPlugInApp *pApp = static_cast<CPlugInApp*>(AfxGetApp());
    if (pApp)
    {
        TRACE("Enumerating application\n");
        EnumResourceNames(pApp->m_hInstance, RT_TOOLBAR,
             (ENUMRESNAMEPROC)EnumResNameProc, 0);
        
        // now enumerate all the plug-in DLL toolbars
        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);
    // There should be 2 resources,one of type RT_TOOLBAR, 
    // which enumerates the command IDs
    // the other of type RT_BITMAP, which are the images for each button

    // load in the RT_TOOLBAR button indexes
    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;      // version of the resource (always 1)
    int    imageSizeX;   // the X size of the toolbar images
    int    imageSizeY;   // the Y size of the toolbar images
    int    tableEntries;  // the number of toolbar button entries
    int    buttons[tableEntries];   // button indexes (0 is separator)
};

So having learnt this we load and construct the map as follows:

    // from examination of a toolbar resource, the layout it:
    // WORD : Version number(usually 1)
    // WORD : X_SIZE of image(16)
    // WORD : Y_SIZE of image(15)
    // WORD : NUMBER OF TOOLBAR BUTTONS
    // x * WORD : TOOLBAR BUTTON ID's
    // NOTE THAT A TOOLBAR BUTTON ID OF 0 IS A SEPARATOR
    if (pToolbarData[0] == 0x0001)
    {
        // we recognise this toolbar version, go ahead
        if (pToolbarData[1] == m_iconX && pToolbarData[2] == m_iconY)
        {
            // buttons are the correct size for a menu, 
                 // lets load these images
            CBitmap    toolbarBitmap;
            // make sure we load the bitmap image from the correct module
            HINSTANCE old = AfxGetResourceHandle();
            AfxSetResourceHandle(hModule);
            toolbarBitmap.LoadBitmap(lpszName);
            AfxSetResourceHandle(old);

            // extract the IDs of each toolbar button
            for(int buttonIndex = 0 ; buttonIndex < pToolbarData[3] ;
                buttonIndex++)
            {
                if (pToolbarData[4 + buttonIndex] != 0)
                {
                    // this is not a separator
                    // make sure there is not already a map entry for this button
                    if (m_commandToImage.find(pToolbarData[4 + buttonIndex]) 
                         == m_commandToImage.end())
                    {
                        // does not exist, add it
                        m_commandToImage[pToolbarData[4 + buttonIndex]] 
                          = m_commandToImage.size();
                    }
                    else
                    {
                        TRACE("Warning, duplicate toolbar button"
                               " %1d found, using first\n", 
                                pToolbarData[4 + buttonIndex]);
                    }
                }
            }
            // add the toolbar images on mass
            m_buttonImages.Add(&toolbarBitmap, ::GetSysColor(COLOR_3DFACE));
            toolbarBitmap.DeleteObject();
        }
    }
    // return TRUE to keep enumerating any other toolbar 
      // resources in the same module
    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:

// we now have all the toolbars loaded into the
// main image list m_buttonImages
// generate the disabled versions of the images used
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:

System menu icons 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.

// a fixed version of the OnInitMenuPopup() function
void CODMenu::OnInitMenuPopup(CMenu* pPopupMenu,
   UINT nIndex, BOOL bSysMenu)
{
    if (IsPostCall())
    {
        // iterate any menu about to be displayed and make sure
        // all the items have the ownerdrawn style set
        // We receive a WM_INITMENUPOPUP as each menu is 
        //displayed, even if the user
        // switches menus or brings up a sub menu. This means we only need to
        // set the style at the current popup level.
        // we also set the user item data to the index into the menu to allow
        // us to measure/draw the item correctly later
        if (pPopupMenu != NULL)
        {
            m_menuBeingProcessed = pPopupMenu->m_hMenu;   
                // only valid for measure item calls
            int itemCount = pPopupMenu->GetMenuItemCount();
            for(int item = 0; item < itemCount; item++)
            {
                int itemID = pPopupMenu->GetMenuItemID(item);
                // make sure we do not change the state of the menu items as
                // we set the owner drawn style
                MENUITEMINFO    itemInfo;

                memset(&itemInfo, 0, sizeof(MENUITEMINFO));
                itemInfo.cbSize = sizeof(MENUITEMINFO);

                itemInfo.fMask = MIIM_STATE;
                pPopupMenu->GetMenuItemInfo(item, 
                        &itemInfo, 
                        TRUE);                // by position
                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.

System menu when made ownerdrawn

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)
{
    // solve problem with system menu items which we
        // do not receive a WM_MEASUREITEM for
    if (lpMIS->itemWidth < m_minSystemMenuWidth)
    {
        // set to minimum width for correct draw
        lpMIS->itemWidth = m_minSystemMenuWidth;
    }
}
System menu with mini-hack installed

Default items not measured correctly

Incorrect size for default menu items

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);// Select menu font in...

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.

// always measure using the bold version of the font
//if (state & ODS_DEFAULT)
pFont = pDC->SelectObject(&m_menuFontBold);
//else
//    pFont = pDC->SelectObject(&m_menuFont);
            // Select menu font in...

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Sirius Analytical Instruments
United Kingdom United Kingdom
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.

Comments and Discussions

 
GeneralPlugInLib.h missing Pin
Dieter Hammer12-May-04 0:19
Dieter Hammer12-May-04 0:19 
GeneralRe: PlugInLib.h missing Pin
Roger Allen12-May-04 0:20
Roger Allen12-May-04 0:20 
This file is part of the download on the main MFC plug-in architecture article.

Roger Allen - Sonork 100.10016
Strong Sad: I am sad I am flying
Who is your favorite Strong?

GeneralRe: PlugInLib.h missing Pin
Vadim Tabakman12-May-04 14:50
Vadim Tabakman12-May-04 14:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.