Dale Rogerson
Microsoft Developer Network Technology Group
April 18, 1995
Click to open or copy the files in the EasyBit sample application for this technical article.
Click to open or copy the files in the GLlib DLL for this technical article.
This article describes how to implement self-drawing menus with the Microsoft® Foundation Class (MFC) Library. Self-drawing menus provide an object-oriented way to implement owner-drawn menus. Owner-drawn menus are drawn by the menu owner, usually the frame window, instead of the Microsoft Windows® operating system. Self-drawing menus, on the other hand, are drawn by an object derived from the CMenu class. The EasyBit sample application, which accompanies this article, and CTRLTEST, which accompanies Visual C++™, demonstrate self-drawing menus.
Sometimes plain text is not enough. Sometimes you need different typefaces, different point sizes, or different attributes, such as bold or italics, to communicate information to your users. Sometimes a picture is worth a thousand words. This is true for menus: In most instances, plain text is perfectly suitable for explaining the function of menu items, but in some cases, using a picture results in a better, more intuitive, interface. A menu that shows colors instead of listing their names, a menu that illustrates fonts or font effects in addition to naming them, and an owner-drawn menu that shows pen widths instead of simply providing measurements are examples of menus that benefit from non-textual representation. Owner-drawn menus were created for these situations. Owner-drawn menus behave the same as normal menus, but they are drawn by the application and not by the Microsoft® Windows® operating system. The application can draw whatever it desires for an owner-drawn menu item.
The Microsoft Foundation Class (MFC) Library has introduced self-drawing menus, which extend the idea of owner-drawn menus. Self-drawing menus are drawn neither by the menu owner nor by Windows, but by an object derived from the MFC CMenu class. This article explores self-drawing menus and covers the following topics:
The Windows operating system is responsible for drawing menus, but it is not responsible for drawing owner-drawn menus. As the term "owner-drawn" indicates, the window that owns a menu is responsible for drawing owner-drawn menus. The menu sends WM_MEASUREITEM and WM_DRAWITEM messages to its owner whenever an owner-drawn menu item needs to be drawn. Figure 1 shows this process.
Figure 1. Process for owner-drawn menus
Owner-drawn menus add color and flair to a program. However, having the menu owner draw the menu isn't a very object-oriented approach, because it groups the menu-drawing code with the window-drawing code. Why doesn't the menu draw itself? That's exactly what MFC does with self-drawing menus. Instead of putting the drawing code in your view class, you place it in a menu class derived from CMenu. This process is illustrated in Figure 2.
Figure 2. Process for self-drawing menus
Basically, owner-drawn menus are the same as self-drawing menus—the only real difference lies in the location of the code. The code for an owner-drawn menu resides with the window-drawing code, whereas the code for a self-drawing menu is located in the menu's own class. Furthermore, it is easier to use self-drawing menu classes in other applications.
MFC adds a little magic behind the scenes to turn owner-drawn menus into self-drawing menus. The CWnd object that owns a menu forwards WM_MEASUREITEM and WM_DRAWITEM messages to the appropriate CMenu object for handling. The magic happens in CWnd::OnMeasureItem and CWnd::OnDrawItem, which can be found in WINCORE.CPP. The code for CWnd::OnMeasureItem (with some of the error-checking code removed) is shown below.
// Measure item implementation relies on unique control/menu IDs.
void CWnd::OnMeasureItem( int /*nIDCtl*/,
LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
if (lpMeasureItemStruct->CtlType == ODT_MENU)
{
CMenu* pMenu;
AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
if (pThreadState->m_hTrackingWindow == m_hWnd)
{
// Start from pop-up menu.
pMenu = CMenu::FromHandle(pThreadState->m_hTrackingMenu);
}
else
{
// Start from menu bar.
pMenu = GetMenu();
}
pMenu = FindPopupMenuFromID(pMenu,
lpMeasureItemStruct->itemID);
if (pMenu != NULL)
pMenu->MeasureItem(lpMeasureItemStruct);
else
TRACE1("Warning: unknown WM_MEASUREITEM for menu item 0x%04X.\n",
lpMeasureItemStruct->itemID);
}
else
{
CWnd* pChild
= GetDescendantWindow(lpMeasureItemStruct->CtlID, TRUE);
if (pChild != NULL && pChild->SendChildNotifyLastMsg())
return; // eaten by child
}
// Not handled - do default.
Default();
}
The key piece in the above code is the call to FindPopupMenuFromID, which is listed below.
static CMenu* FindPopupMenuFromID(CMenu* pMenu, UINT nID)
{
ASSERT_VALID(pMenu);
// Walk through all items, looking for ID match.
UINT nItems = pMenu->GetMenuItemCount();
for (int iItem = 0; iItem < (int)nItems; iItem++)
{
CMenu* pPopup = pMenu->GetSubMenu(iItem);
if (pPopup != NULL)
{
// Find child pop-up menu.
pPopup = FindPopupMenuFromID(pPopup, nID);
// check popups on this popup
if (pPopup != NULL)
return pPopup;
}
else if (pMenu->GetMenuItemID(iItem) == nID)
{
// It is a normal item inside our pop-up menu.
pMenu = CMenu::FromHandlePermanent(pMenu->m_hMenu);
return pMenu;
}
}
// Not found.
return NULL;
}
FindPopupMenuFromID starts with the main menu bar and descends to each child pop-up menu looking for the ID of the command that generated the WM_MEASUREITEM message. If it finds a pop-up menu that contains the ID, FindPopupMenuFromID looks into the permanent handle map for an instance of a CMenu-derived object attached to the handle of the pop-up menu. "Technical Note 3: Mapping of Windows Handles to Objects" (MSDN Library Archive, Technical Articles, Visual C++ 1.5 [16-bit] Articles, MFC 2.5 Technical Notes) discusses handle maps to some extent. Figure 3 illustrates the function of the permanent handle map.
Figure 3. Permanent handle map
The permanent handle map handles the mapping of Windows handles to MFC objects. Using the map, MFC gets a pointer to the instance of the CMenu object attached to the menu and calls its MeasureItem function:
pMenu->MeasureItem(lpMeasureItemStruct);
The process is similar for the WM_DRAWITEM message. CWnd::OnDrawItem looks in the permanent handle map for the CMenu object and calls its DrawItem method. The edited code for CWnd::OnDrawItem is shown below.
void CWnd::OnDrawItem( int /*nIDCtl*/,
LPDRAWITEMSTRUCT lpDrawItemStruct)
{
if (lpDrawItemStruct->CtlType == ODT_MENU)
{
CMenu* pMenu = CMenu::FromHandlePermanent(
(HMENU)lpDrawItemStruct->hwndItem);
if (pMenu != NULL)
{
pMenu->DrawItem(lpDrawItemStruct);
return; // eat it
}
}
else
{
CWnd* pChild =
CWnd::FromHandlePermanent(lpDrawItemStruct->hwndItem);
if (pChild != NULL && pChild->SendChildNotifyLastMsg())
return; // Eat it.
}
// Not handled - do default.
Default();
}
CWnd::FromHandlePermanent and CMenu::FromHandlePermanent are internal MFC functions; don't use these in your applications. These functions differ from CWnd::FromHandle and CMenu::FromHandle in that they return NULL if no MFC object is attached to the Windows handle, instead of creating a temporary MFC object attached to the handle.
The pop-up menu is at the lowest level of a self-drawing menu, as shown by FindPopupMenuFromID. In other words, a pop-up menu is attached to an MFC CMenu-derived object. A CMenu object cannot be attached to a single menu item, mainly because a single menu item doesn't have a handle. In Windows and MFC, menu items are not objects themselves but are part of the pop-up menu, which is an object. (This is similar to buttons in toolbars: The buttons are not objects; they are part of the toolbar.)
Figure 4. Pop-up menu attached to an MFC CMenu-derived object
The EasyBit sample application changes an existing pop-up menu so that it resembles Figure 4 (above). The CTRLTEST sample included with Microsoft Visual C++™ shows how to dynamically add a cascading pop-up menu with self-drawing items, as shown in Figure 5 (below).
Figure 5. Cascading pop-up menu attached to an MFC CMenu-derived object
If you want to call different drawing code for different menu items, your CMenu-derived object will have to handle the delegation to other drawing functions.
The EasyBit sample application contains a self-drawing menu that displays a cube, a pyramid, and a dodecahedron. The user selects an object to rotate by picking the picture of the object (see Figure 6) instead of picking the name of the object (see Figure 7). In this section, I'll discuss how I added support for these self-drawing menu items to EasyBit.
Figure 6. Self-drawing menus from EasyBit
Before I added self-drawing menus, EasyBit had normal menus with text (Figure 7). I created the menus and status bar strings using the resource editor in Visual C++. I added COMMAND and UPDATE_COMMAND_UI handlers to the view class for each menu item. The menu looked similar to Figure 7.
Figure 7. Before self-drawing menus
In other words, I had a normally functioning application with standard menus, just like any other MFC application.
To add self-drawing menus to EasyBit, my first task was to add a new class derived from CMenu. I called this class CShapeMenu. The code is in the CSHAPEMENU.H and CSHAPEMENU.CPP files. The header file for CShapeMenu is listed below:
class CShapeMenu : public CMenu
{
public:
enum enum_SHAPES {BOX, PYRAMID, DODEC} ;
// Operations
void ChangeMenuItem(UINT nID, enum_SHAPES shape);
void Init() ;
// Implementation
virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMIS);
virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
CShapeMenu();
virtual ~CShapeMenu();
private:
CSize m_sizeCheck ; // size of menu check mark
CGL* m_pScenes[3] ; // OpenGL scenes of the three shapes
CSimpleDIB m_DIB[3] ; // DIB sections on which scenes are rendered
};
Most of the work is done in CShapeMenu::MeasureItem and CShapeMenu::DrawItem. CShapeMenu::MeasureItem simply returns the size of the menu item.
#define MENU_HEIGHT 100
#define MENU_WIDTH 100
void CShapeMenu::MeasureItem(LPMEASUREITEMSTRUCT lpMIS)
{
int iWidthCheck = LOWORD(GetMenuCheckMarkDimensions()) ;
// All items are fixed in size.
lpMIS->itemHeight = MENU_HEIGHT;
// Remove space automatically added for the check mark.
lpMIS->itemWidth = MENU_WIDTH - iWidthCheck ;
}
The only trick is to remove the horizontal space the system automatically adds for the check mark. To remove the space, subtract the width of the check mark from the width of your menu. The width of the check mark is obtained using the command:
int iWidthCheck = LOWORD(GetMenuCheckMarkDimensions()) ;
CShapeMenu::DrawItem is not quite as simple as CShapeMenu::MeasureItem, as shown below:
void CShapeMenu::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
CDC* pDC = CDC::FromHandle(lpDIS->hDC);
enum_SHAPES nShape = (enum_SHAPES)lpDIS->itemData;
const COLORREF crSelect = RGB(255,0,0) ; // selection color
const COLORREF crCheck = RGB(0,0,255) ; // checked color
const COLORREF crNormal = RGB(0,255,0) ; // normal Color
COLORREF cr = crNormal;
if (lpDIS->itemAction & ODA_DRAWENTIRE)
{
// Draw the scene.
CPalette* pOldPal = NULL;
CPalette* pPalTemp = m_pScenes[nShape]->GetPalette() ;
if (pPalTemp != NULL)
{
pOldPal = pDC->SelectPalette(pPalTemp, FALSE) ;
pDC->RealizePalette() ;
}
m_DIB[nShape].Draw(pDC,lpDIS->rcItem.left, lpDIS->rcItem.top) ;
if (pOldPal != NULL) pDC->SelectPalette(pOldPal, FALSE ) ;
}
if (lpDIS->itemState & ODS_CHECKED)
{
// Menu item is checked.
cr = crCheck ;
}
if (lpDIS->itemAction & ODA_SELECT)
{
if (lpDIS->itemState & ODS_SELECTED)
{
// Menu item is selected.
cr = crSelect ;
}
}
// Draw the border.
CBrush br(cr);
CRgn rgn;
rgn.CreateRectRgnIndirect(&lpDIS->rcItem) ;
pDC->FrameRgn(&rgn,&br,4,4) ;
}
I drew the menu by blting the device-independent bitmap (DIB) that contains an image rendered by OpenGL™. For more information on the OpenGL code used in EasyBit, see my article series on OpenGL, including "OpenGL VI: Rendering on DIBs with PFD_DRAW_TO_BITMAP," in the MSDN Library.
Next, I drew a border around the image using FrameRgn. This function can create a border thicker than 1 pixel that doesn't extend outside the rectangle it frames. The color of the border depends on the state of the menu item. The state information is contained in DRAWITEMSTRUCT.itemState. For more information, see the MFC CMenu entry in the Visual C++ documentation.
Creating a new class doesn't do you any good unless you attach the class to your application somewhere. To attach CShapeMenu to EasyBit, the first step is to add the line:
CShapeMenu m_ShapeMenu ;
to the header file for CMainFrame.
The next step is to attach the Rotate pop-up menu to m_menuOwner. Adding the following code to CMainFrame::OnCreate does the trick:
CMenu* pFrameMenu = GetMenu() ;
CMenu* pSubMenu = pFrameMenu->GetSubMenu(1) ;
m_ShapeMenu.Attach(pSubMenu->GetSafeHmenu()) ;
The File menu is at position 0, and the Rotate menu is at position 1. The menu is detached in the destructor for CShapeMenu.
CShapeMenu::~CShapeMenu()
{
.
.
.
Detach() ;
ASSERT(m_hMenu == NULL) ;
}
Now that the pop-up menu is attached to the CShapeMenu object, we need to change the menu style to owner-drawn. I added a convenient helper function, ChangeMenuItem, to CShapeMenu. This function takes a menu ID and a value to determine which shape to draw. It is called from CMainFrame as follows:
m_ShapeMenu.ChangeMenuItem(ID_ROTATE_BOX,CShapeMenu::BOX) ;
m_ShapeMenu.ChangeMenuItem(ID_ROTATE_PYRAMID,CShapeMenu::PYRAMID) ;
m_ShapeMenu.ChangeMenuItem(ID_ROTATE_DODEC,CShapeMenu::DODEC) ;
The code for ChangeMenuItem consists simply of a call to CMenu::ModifyMenu:
void CShapeMenu::ChangeMenuItem(UINT nID, enum_SHAPES shape)
{
ModifyMenu( nID,
MF_BYCOMMAND | MF_ENABLED | MF_OWNERDRAW,
nID,
(LPCTSTR)shape) ;
}
That's all there is to making a menu a self-drawing menu. Pretty simple, isn't it?
The CTRLTEST sample application takes a slightly different approach. Instead of modifying an existing pop-up menu, it dynamically adds a new pop-up menu cascading off an existing pop-up menu, as simulated in Figure 8.
Figure 8. Example of cascading menu from CTRLTEST
In general, this doesn't differ from what I described in the previous section. The CColorMenu class is derived from CMenu. CColorMenu implements DrawItem and MeasureItem in the CUSTMENU.CPP file. The color menu items are added at run time using AppendMenu, which is called from CColorMenu::AppendColorMenu.
Because the menu items are added at run time instead of build time, MFC and ClassWizard don't know anything about them. This makes life a little more difficult. For more information on adding menu items dynamically, see the DYNAMENU sample application in the MSDN Library.
The CTestWindow::AttachCustomMenu function in CUSTMENU.CPP appends the pop-up menu to the Colors menu. This function is called by CTestWindow::SetupMenus, which is called by CTestApp::InitInstance.
I like the EasyBit method better than the CTRLTEST method because it allowed me to:
Self-drawing menus are very easy to add to an application. Because of the object-oriented nature of self-drawing menus, you can easily create different self-drawing menu classes that your application can reuse. These menu classes can make your application more informative, visually appealing, and easier to use.
Rogerson, Dale. "OpenGL VI: Rendering on DIBs with PFD_DRAW_TO_BITMAP." April 1995. (MSDN Library, Technical Articles)
"Technical Note 3: Mapping of Windows Handles to Objects." (MSDN Library Archive, Technical Articles, Visual C++ 1.5 [16-bit] Articles, MFC 2.5 Technical Notes)
"Technical Note 14: Custom Controls." (MSDN Library Archive, Technical Articles, Visual C++ 1.5 [16-bit] Articles, MFC 2.5 Technical Notes)