Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > January 1998
January 1998

Microsoft Systems Journal Homepage

Give Your Applications the Hot New Interface Look with Cool Menu Buttons

Paul DiLascia

This article assumes you're familiar with MFC

Code for this article: ROPview.exe (288KB)
Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

In one of my recent columns, I showed you how to implement the new flat-style toolbars and coolbars found in the Office 97 products, like Microsoft Outlook and Visual Studio 97 (MSJ, August 1997). At the end of the column, I made a crack that everyone would want the new menus next. Well, sure enough, many of you sent email asking how to add buttons to your menus. Naturally I was excited by the prospect of writing code that people were clamoring for.
Implementing the buttons seemed like an easy enough task—and, in theory, it was. Kinda sort of. It ended up over a thousand lines. Along the way I ran into so many quirks, pitfalls, and oddities that I decided the subject deserved a full article. If nothing else, it will amaze and amuse you to see how bizarre Windows
® can be. Amusing, that is, if you can somehow manage to keep from going postal.

GUI Fashion Rage

It seems that every couple of years, Windows-based programs get a new look. Someone comes out with a new idea like toolbars or 3D controls or Tommy Hilfiger prop sheets (just kidding) and everyone else has to follow suit. The latest ne plus ultra in GUI couture is what I call the cool look (for lack of a better term), since it includes the coolbars in the comctl32.dll that ships with Microsoft Internet Explorer 3.0. (See Strohm Armstrong's article in the October and November 1996 issues, and my August and October 1997 C++ Q&A columns.) The Microsoft Office 97 products and Visual Studio 97 have their own homegrown coolbars.
Figure 1
Figure 1 

In addition to coolbars, the new look incorporates menus with button bitmaps (see Figure 1). The idea is that showing the bitmap next to the command helps users learn which buttons perform which commands—the same way menus show accelerator names to help you learn keyboard shortcuts. I'm not sure whether cool menus are cool; to me it seems like what my graphic designer friends call screen junk. But then I thought the same thing about toolbars when they first came out. (Screen junk is a highly technical term—for an example, see Figure 2.)
Figure 2 Screen Junk
Figure 2 Screen Junk

Now I'm willing to bet a nickel that Microsoft will eventually incorporate cool menus into a new DLL, but for those of you who simply can't bear to be seen in anything less than up-to-the-minute GUI fashion chic, I'll show you how to implement cool menus ASAP. My cool menu manager can be installed in less time than it takes to compile your app, or in about thirty seconds if you type fast. Really.

Cruising the Cool Menu Manager

CCoolMenuManager is a C++ class I wrote to do cool menus in MFC apps. To use it, all you have to do is plop an instance in your frame window and call two functions:

 class CMainFrame : public CFrameWnd {
    CCoolMenuManager m_menuMgr;
 •
 •
 •
 };
 // (in your CMainFrame::OnCreate)
 m_menuMgr.Install(this);
 m_menuMgr.LoadToolbar(IDR_MAINFRAME);
That's it. That's all there is! The toolbar need not be displayed, or even instantiated as a CToolBar object. It only has to exist in your resource file. If you have more than one toolbar, you can call LoadToolbars (note the s).

 static UINT toolbars[] {
    ID_TOOLBAR_FILE,
    ID_TOOLBAR_EDIT,
    ID_TOOLBAR_FORMAT,
 };
 m_menuMgr.LoadToolbars(toolbars, 3);
I wrote LoadToolbars because it makes me weep when I see code like this:

 menuMgr.LoadToolbar(IDR_TOOLBAR1);
 menuMgr.LoadToolbar(IDR_TOOLBAR2);
 •
 •
 •
 menuMgr.LoadToolbar(IDR_TOOLBAR27);
That kind of repetitive, open coding typical of SDK sample programs is one reason there are apps that take over 80MB on disk. It announces to anyone who reads your code: "I am an ignoramus."
In any case, you can see how easy CCoolMenuManager is. Just create an object and call two functions. In addition to that basic picture, CCoolMenuManager offers some extra features that I'll discuss a bit later. For example, there's a flag that lets you turn the buttons on or off:

 // turn menu buttons off
 m_menuMgr.m_bShowButtons = FALSE;
I used m_bShowButtons in my demo program ROPView (described later) to implement a View | Hide Menu Buttons command. This will be well-appreciated by grumpy hackers like me who think retro is the ultimate GUI fashion statement. (Soon, the world will catch up to my advanced taste—I predict that before the year 2005, Windows will revert to its 3.1 look.)

Inside CCoolMenuManager

If all you want is buttons in your menus, you can stop reading, download the code from MSJ, add the four lines to your app, and compile. Then tell your boss you spent hours working late and deserve a raise. But if you're the type who takes your toys apart, keep reading. I'll show you how CCoolMenuManager works. As I said earlier, it's quite amusing. Plus, there are plenty more goodies in store.
The basic strategy behind CCoolMenuManager is simple. When the user invokes a menu, CCoolMenuManager converts every item to an owner-draw item that draws itself exactly the same way Windows does—using the same font, colors, and so on—with the added feature of drawing a bitmap in the left margin of every command that has a toolbar button. When the user is finished with the menu, CCoolMenuManager converts all the items back to their original state. Like I said, simple. But the problem with Windows isn't understanding what you want to do, it's getting Windows to understand, too.
Let's begin when your app calls LoadToolbar. (Figure 3 shows the source code so you can follow along.) LoadToolbar loads the bitmap first, then the toolbar. Remember, a toolbar resource is actually two resources with the same ID: a bitmap and a toolbar. Each step requires a trick.
The first trick is converting the bitmap from gray to the current 3D color scheme. MFC does this with the function AfxLoadSysColorBitmap. It converts the four shades—white, gray, dark gray, and black—to the current 3D system colors. MFC uses this function in its own CToolBar::LoadToolbar, but since AfxLoadSysColorBitmap requires module instance and resource handles, which I'm too lazy to get, I wrote a wrapper that lets you give just the resource ID.

 HBITMAP hbmToolbar = 
    PxLib::LoadSysColorBitmap(nID);
PxLib is a namespace I use for helper functions like these to avoid name collisions.
The second trick to loading toolbars is knowing what a toolbar resource looks like in memory. This you can discover from bartool.cpp in Figure 3.

 // RT_TOOLBAR resource
 struct TOOLBARDATA {
    WORD wVersion;           // should be 1
    WORD wWidth;             // bitmap width
    WORD wHeight;            // height 
    WORD wItemCount;         // num items
    WORD items[wItemCount];  // item IDs
 };
The actual size of the TOOLBARDATA is variable; the items array has wItemCount items. The nth word in the array is the command ID for the nth button in the bitmap, or zero for a separator. Figure 4 illustrates this. TOOLBARDATA is an MFC thing; there's no RT_TOOLBAR defined in winuser.h like there is for RT_ICON and RT_BITMAP—it's in afxres.h. But the resource compiler knows how to process the TOOLBAR statement, and the Developer Studio IDE knows how to generate it.
Figure 4  TOOLBARDATA
Figure 4 TOOLBARDATA

Once LoadToolbar loads the bitmap and TOOLBARDATA, it performs a few sanity checks and—assuming everything comes out clean—adds the bitmap to its image list, m_ilButtons, which contains all the bitmaps of all the loaded toolbars. Next, LoadToolbar adds the command IDs to its button map, m_mapIDtoImage. This association map (CMapWordToPtr) maps each command ID to its index in the image list, so m_mapIDtoImage[nID] is the image list index of the button associated with the command nID. For example, in my demo program ROPView (described later), m_mapIDtoImage[ID_FILE_NEW] is zero since File New is the first button in my toolbar bitmap.
In theory, using CMapWordToPtr instead of a normal array makes looking up IDs faster. I encapsulated the lookup in GetButtonIndex because it's more comprehensible than writing (int)m_mapIDtoImage[nID]. C++ operator overloading is cute, but it's usually clearer to use English names than some cryptic a[i] notation that requires a trip to the header file to grok.

Hooking Messages: CSubclassWnd

Once your app calls LoadToolbar or LoadToolbars, the menu manager has an image list and a way of mapping command IDs to bitmaps. The next step is to seize control of the menus. To do that, the menu manager needs to handle several messages: WM_INITMENUPOPUP, WM_MEASUREITEM, and WM_DRAWITEM, among others. How does CCoolMenuManager get these messages? This is where life gets interesting.
The most obvious way would be to derive a CCoolMenuFrame from CFrameWnd, and let you derive your CMainFrame from CCoolMenuFrame. Then CCoolMenuFrame would have its own message map and handler functions. But this approach is problematic because your app may already be deriving its CMainFrame from some other library class: CXyzFrame. This problem is inherent in C++, where the entire window class hierarchy must be rigidly specified at compile time; there's no way to derive classes dynamically at runtime, which is exactly what you need to do in this situation.
Another way would be to implement CCoolMenuManager as a non-window class with handler functions for your app to call. In this scenario, the application programmer (that's you, Sherlock) must implement the handlers, but all they do is call the menu manager:

 void CMainFrame::OnInitMenuPopup(...)
 {
    m_menuMgr.OnInitMenuPopup(...);
 }
Likewise for all the other messages. This avoids the tyranny of the rigid C++ class hierarchy, but it still makes you write many lines of code—and what if you forget one of the handlers? In order to make using CCoolMenuManager totally idiot-proof, I used my CSubclassWnd class (formerly CMsgHook), first described in my MFC Goodies article in the March 1997 MSJ. There I used CMsgHook/CSubclassWnd to implement a generic palette message handler. I used CSubclassWnd again in my June 1997 column to implement a generic custom caption painter.
CSubclassWnd is the perfect solution whenever you want to handle messages on behalf of a window without changing the window class hierarchy. In essence, CSubclassWnd turns the clock back by letting you do in C++ what you could always do in C: namely, subclass a window dynamically. CSubclassWnd subclasses the window by installing its own window proc, one that passes all messages to a virtual WindowProc function. Specific CSubclassWnd-derived classes like CCoolMenuManager must override this function to do stuff:

 LRESULT CCoolMenuManager::WindowProc(UINT msg, WPARAM wp,
                                     LPARAM lp)
 {
    switch(msg) {
    case WM_MEASUREITEM:
       if (OnMeasureItem((MEASUREITEMSTRUCT*)lp))
          return TRUE; // handled
       break;
    case WM_DRAWITEM:
       if (OnDrawItem((DRAWITEMSTRUCT*)lp))
          return TRUE; // handled
       break;
 •
 •
 •
    }
    return CSubclassWnd::WindowProc(msg, wp, lp);
 }
Hey, it looks just like it did in the old days—switch case and all! Note that CCoolMenuManager::WindowProc calls CSubclassWnd::WindowProc if it doesn't handle the message; this is essential. CSubclassWnd::WindowProc routes the message back to MFC—to AfxWndProc, to the CWnd and its message map, and to your handlers. CSubclassWnd is similar to CWnd, but it's not derived from CWnd. It's just an ordinary CObject. Most important, unlike CWnd you can have any number of CSubclassWnd objects "attached" to the same window. Within each CSubclassWnd object, the subclassed window is stored in a data member, m_pWndHooked. To hook the whole thing up, something has to call CSubclassWnd::HookWindow. For CCoolMenuManager, the Install function does it:

 void CCoolMenuManager::Install(CFrameWnd* pFrame)
 {
    m_pFrame = pFrame;
    HookWindow(pFrame);   // (in CSubclassWnd)
 }
Just to make sure you don't call any CSubclassWnd functions, I derived CCoolMenuManager privately from CSubclassWnd. This follows the general C++ paradigm that you use a private derivation when the base class is used to implement the derived class, whereas you use a public derivation when the relationship represents true inheritance ("is a"). If you want to learn more about CSubclassWnd, check out the March 1997 issue of MSJ, or download the source code.

Setting Up Owner-Draw

Now that you know how the menu manager handles messages, I can describe the actual handlers. When the user invokes a menu, Windows sends your frame a WM_ INITMENUPOPUP message. CCoolMenuManager::WindowProc intercepts it and passes control to OnInitMenuPopup, which in turn calls ConvertMenu. This rather large and unwieldy function is the heart of CCoolMenuManager. It converts menus from string to owner-draw and back.
Assuming m_bShowButtons is TRUE, ConvertMenu loops through all the menu items, changing each one to MFT_OWNERDRAW and creating a CMyItemData for each one. Whenever you implement owner-draw menus (or buttons, or whatever), you get to store your own DWORD as the item data. Normally, it's a pointer to some structure of your own design:

 struct CMyItemData {
    long     magicNum;
    CString  text;
    UINT     fType;
    int      iButton;
 };
The CString is the item text, like &Open or E&xit; iButton is the index into the image list of the button for this item, or -1 if there isn't any; and fType holds the original item's type flags, which I use later to restore the item. The magicNum field always stores the same four ASCII bytes: mid0 (My Item Data—version 0). CCoolMenuManager uses this to recognize its own owner-draw items, as opposed to other owner-draw items you may have in your app. I never tested it, but in theory you should be able to mix your own "true" owner-draw items with CCoolMenuManager.
As OnInitMenuPopup allocates each CMyItemData, it copies the item name and style flags, then stores the item data in the menu. It does this only if the item is not already owner-draw; if it is owner-draw, OnInitMenuPopup does nothing. So the first time you invoke a particular menu, CCoolMenuManager converts all the items to owner-draw. But if you move the mouse to another menu—from File to Edit, say—then revisit the File menu, the second time around OnInitMenuPopup does nothing because the menu is already converted.
All this sounds straightforward enough, but there are several subtle points that bear elaboration. First, to get information about the menu items, I use a Win32
® API function that has no MFC wrapper: GetMenuItemInfo. Instead of calling GetSubMenu, GetMenuItemID, and GetMenuString, GetMenuItemInfo lets you get all the info in one fell swoop. Also, it's the only way to get the owner-draw item data. Likewise, after OnInitMenuPopup determines what menu info it wants to change, it calls SetMenuItemInfo to set everything with one API call. To make life easier and a little more bulletproof, I implemented my own CMenuItemInfo (derived from MENUITEMINFO) with a constructor that zeroes the struct and sets cbSize. These little details are easy to forget, and when you do you're likely to spend several minutes in the debugger wondering why your code doesn't work.
One of the many gotchas that got me when I first wrote OnInitMenuPopup has to do with CCmdUI::SetText. My demo program, ROPView—which I still haven't formally introduced, but which you've probably guessed by now is the app I used to test all this stuff—has a command to turn the buttons on or off. ROPView has an ON_UPDATE_COMMAND_UI handler to dynamically change the name of this command based on the current state.

 void 
 CMainFrame::OnUpdateViewMenuButtons(CCmdUI* pCmdUI)
 {
    pCmdUI->SetText(m_bShowMenuButtons ?
       "Hide &Menu Buttons" : "Show &Menu Buttons");
 }
What I discovered, to my dismay, is that CCmdUI::SetText changes the menu item to MFT_STRING—clobbering my MFT_OWNERDRAW style!

 // in CCmdUI::SetText
 UINT nState = m_pMenu->GetMenuState(...);
 nState &= ~(MFT_BITMAP|MFT_OWNERDRAW|MFT_SEPARATOR);
 m_pMenu->ModifyMenu(... MFT_STRING | nState ... );
Well, shoot. But it makes sense. If you're setting the text of a menu item, it has to be a string, right? Conceptually, all the items in a cool menu are strings; it's just that I've implemented them as owner-draw so I can draw the darned buttons.
At this point I was thoroughly frustrated. It looked like I'd have to implement a new CCmdUI class for cool menus, one whose SetText function set the text in CMyItemData instead of calling ModifyMenu. Then I'd have to hook it into the whole command update mechanism—all of which would require copying dozens of lines of MFC code just to modify a few because this is one area where MFC doesn't give you the virtual functions you need. So I did what I always do in such circumstances, which is mow the lawn.
As I trudged across the green, I suddenly had an inspiration. The problem is that CCmdUI::SetText changes my menu items to strings. So? I can change them back to owner-draw. I even have a function to do it.
When I first wrote CCoolMenuManager, I handled WM_INITMENUPOPUP like so:

 // in CCoolMenuManager::WindowProc
 // This is WRONG!
 switch(msg) {
 case WM_INITMENUPOPUP:
    OnInitMenuPopup(...);
    break;
 }
 return CSubclassWnd::WindowProc(...);
When processing WM_INITMENUPOPUP, it's important to let CSubclassWnd process the message, too (as opposed to returning). That lets CFrameWnd::OnInitMenuPopup get it, which is where MFC does all its magic command update UI stuff for menu items. It's where MFC creates a little CCmdUI object for each menu item and routes to your ON_UPDATE_COMMAND_UI handlers, and where those handlers might call CCmdUI::SetText to clobber my owner-draw items.
But the question is, should CCoolMenuManager process WM_INITMENUPOPUP first, and then let CSubclassWnd/CFrameWnd get it—or the other way around? The first time I wrote the code, I did it in the first order—CCool-MenuManager first, then CFrameWnd—not because I thought about it, but because I figured it didn't matter. But it does! If I let CFrameWnd handle WM_INITMENUPOPUP first, then it's OK if some ON_UPDATE_COMMAND_UI handler calls CCmdUI::SetText to blow my owner-draw menu item out of the water, because the menu manager will fix it when it gets control afterward. So the correct code is:

 case WM_INITMENUPOPUP:
    // Let frame window handle it first!
    CSubclassWnd::WindowProc(...);
    OnInitMenuPopup(...);
    return 0;
Now if MFC changes an item to MFT_STRING, CCoolMenuManager::OnInitMenuPopup will call ConvertMenu, which changes it back to MFT_OWNERDRAW. I had to modify the logic in ConvertMenu slightly to never assume that once it had converted the menu it would stay converted, and also to adjust for the fact that when MFC modifies the item to MFT_STRING, the item data (CMyItemData) is still there. Apparently, when you call ModifyMenu to change a menu from owner-draw to string, Windows isn't smart enough to zero-out the item data. The old item data is still there, so there's no need to create a new one; all I have to do is update the item name. It's all very delicate and a bit hairy—as well as serendipitous—but it flies OK.
Figure 5
Figure 5
The last ConvertMenu problem has to do with the system menu. That's the menu that pops up from the icon at the left end of the title bar (see Figure 5). Old-time Windows hackers know you don't ever munge the system menu directly—for example, to add your own commands—but instead call GetSystemMenu to first create a copy. The app may or may not have done this, so to be safe, ConvertMenu simply ignores system commands (any command in the system menu that has an ID >= 0xF000).

Measuring Menu Items

At this point, the user has invoked a menu, Windows has sent WM_INITMENUPOPUP, and the menu manager has converted it to an owner-draw menu. Now what? Windows asks the frame to draw the menu. But first it has to know how big the menu is, so it sends the frame a WM_MEASUREITEM message for each item. CCoolMenuManager::WindowProc intercepts the message using its CSubclassWnd trick, and calls OnMeasureItem to measure the item. This function calls DrawText with the DT_CALCRECT flag to compute the size of the text, adds some extra space for margins, and returns the result to Windows in the MEASUREITEMSTRUCT. Very simple and straightforward, but once again you have to remember you're dealing with Windows.
The first problem is getting the menu font. To do that, you have to call ::SystemParametersInfo.

 CFont font;
 NONCLIENTMETRICS info;
 info.cbSize = sizeof(info);
 SystemParametersInfo(SPI_GETNONCLIENTMETRICS,
                      sizeof(info), &info, 0);
 font.CreateFontIndirect(&info.lfMenuFont);
I encapsulated the font-creation process in GetMenuFont, which creates the font in a new member, m_fontMenu.
The next problem comes in computing the size of the item. The height is easy, just use GetSystemMetrics(SM_CYMENU) or the height of the text, whichever is bigger. The width is a little more complicated. It's the width of the text plus a bunch of margins and spacing defined by some const symbols. I'll spare you the gory minutiae of menu metrics; Figure 6 tells the story. If you do this on your own, just remember: it's important to get this stuff right, down to the last pixel.
Figure 6  Menu Metrics
Figure 6 Menu Metrics

Once you've added up all the pixels, you're still not ready to return from OnMeasureItem because this is where you run into Windows kludge #1: whatever value you return in MEASUREITEMSTRUCT as the itemWidth, Windows will add the width of a checkmark. Oh, of course. Why didn't you think of that? Actually, experience shows that Windows adds one less than this value, so to make your menu item come out exact, you must subvert the Windows logic by subtracting from your final desired value:

 lpms->itemWidth -= GetSystemMetrics(SM_CXMENUCHECK)-1;
OK, I did that—proud that I'd remembered this piece of trivia from the old days, even if it was a waste of precious neurons. But that's not the end of the story. Even after I coded the fudge factor, my menu items still came out too wide. Try as OnMeasureItem might to tell Windows the width was x, Windows insisted on calling my draw function with x+30. And the strange thing is, it only happened for some menus!
I figured I must finally be losing my marbles. (Too much Windows programming isn't healthy, you know.) Since the lawn was already mowed, there was nothing to do but pull my hair and gnash my teeth. Eventually, I noticed that the width "correction" only occurred in popups that contain separators. Hmmm... something to do with separators.
In my original implementation of OnInitMenuPopup, I didn't bother to make the item owner-draw if it was a separator (MFT_SEPARATOR). Why should I bother to draw a separator when Windows can do it? Well, now I know the answer: if you let Windows draw the separators, it performs its own width calculations. It seems to add 30 pixels to the largest width returned from WM_MEASUREITEM. The truly astonishing thing is, what possible algorithm could ever cause the existence of a separator to make the menu wider? The lesson is, if you want to take over the menu, you have to do separators, too. Once I did that, my widths came out exact.

Drawing the Items

OK, it's almost halfway into the article and your computer still hasn't displayed the menu yet. But I'm almost there. Once Windows has measured the menu, it creates the menu window, sets up a device context and finally sends your frame a WM_DRAWITEM message, one for each item. Once again, CCoolMenuManager traps the message, and passes it to yet another function, OnDrawItem. Finally, it's time to draw the item! Using the Microsoft Office 97 products as my model (they're slightly different from Visual Studio), I drafted the following spec for how to draw menu buttons:
  • If a menu item has a button that is unselected (unhighlighted), the button should have the flat look (no 3D border).
  • If a menu item has a button that is selected (highlighted), the button should have the 3D popped-out look (button is up).
  • If a menu item has a button and the command is checked, the button should have the pushed-in look (button is down) with a light (item unselected) or normal (selected) background color.
  • When an item is checked but has no button, draw as above (3D pushed-in with changing background) using the standard checkmark or whatever bitmap is specified in the MENUITEMINFO.
  • If a menu item with a button is disabled, the button should appear with the disabled, embossed look.
As I pondered the possibilities (difficult as it is to restrain your fingers, it's always good to ponder before you type), it all seemed pretty straightforward. Drawing buttons is easy; CImageList::Draw does that. Coloring backgrounds and drawing 3D edges are likewise pretty simple. But how do you draw a disabled button? This turned into a major piece of detective work.
Figure 7 ROPView—An Exploration Tool
Figure 7 ROPView—An Exploration Tool

CImageList::Draw (ImageList_Draw) has a bunch of style flags for drawing images in different states. Perhaps it can draw disabled buttons? Not trusting the documentation, I wrote a little program to explore it. This program evolved into ROPView, which serves as both demo program and Windows exploration tool. Figure 7 shows what happens when ROPView draws my toolbar bitmap with each of the different ImageList_Draw flags. It's all quite interesting, but I don't see anything that looks like a disabled, embossed button. That's because ImageList_Draw was designed for drawing desktop/folder icons in different states, not disabled buttons.
Further searching through the docs, MSDN, and ultimately winuser.h uncovered a new Win32 function: DrawState. According to the documentation, "The DrawState function displays an image and applies a visual effect to indicate a state, such as a disabled or default state." Well, this must be my lucky day! It took a few minutes to grok the arcane, combine-seventeen-functions-into-one, assembly language mentality of the Windows API designers, but eventually I managed to wrap my already-overstressed brain around it. Fortunately, MFC has several CDC functions that wrap DrawState's 10 arguments into eight different overloaded varieties that make sense to a human.
DrawState can draw many different kinds of objects, but I'm only interested in bitmaps. As with image lists, there are several flags you can use, including DSS_DISABLED, which—according to the documentation—"embosses the image." Great! You even get to specify the brush to use to do the embossing. So I quickly added some lines to ROPView to explore what DrawState actually does (see Figure 8).
Figure 8 DrawState/Bitmap View
Figure 8 DrawState/Bitmap View

Now, I'm perfectly willing to admit that I still haven't figured out how to program this function properly. Perhaps tomorrow someone from Microsoft will send me email beginning with "Paul, you idiot!" and telling me all I need to do is select the dooble into the mumble while I touch my left pinky to my sternum and say "Windows is great." But I tried many different combinations of such things until my fingers were in danger of carpal collapse. At the very least it's fair to say this function is miserably documented. Frankly, I think it just doesn't work.
Figure 9 DrawState/Icon View
Figure 9 DrawState/Icon View

So, what else to try in search of the elusive embossed look? Well, probing the cryptic DrawState documentation again reveals that it can also draw icons. Maybe if I first extract the button image as an icon? To make a long story short, Figure 9 shows the result. Much better. So here's the magic code to draw a button image with the embossed look:

 HICON hIcon = imagelist.ExtractIcon(i);
 dc.DrawState(p, CSize(0,0), hIcon, DSS_DISABLED, 
              (HBRUSH)NULL);
 DestroyIcon(hIcon);
DrawState works fine for government work, and even for CCoolMenuManager, but it's not perfect. Extracting and destroying an icon for every paint operation is a bit on the pokey side. ROPView demonstrates this nicely: if you select the DrawState/Icon view from the menu or coolbar combobox and resize the window, you'll see how slowly it paints. Then select any other view and see how snappily it responds. On the other hand, ROPView draws 12 X 16 = 192 icons, whereas it's extremely unlikely any app will ever have more than five or six disabled buttons in any given popup menu. But for those especially masochistic speed purists like me, I wrote a function, PxLib::DrawEmbossed that draws disabled buttons manually, using BitBlt operations. There isn't space to describe it here; I plan to do so in a future column.

Back to Buttons

Now that I've shown you how to draw disabled buttons, the rest is more or less straightforward. (I almost said easy, but that would be an overstatement.) When Windows sends your frame WM_DRAWITEM, CCoolMenuManager intercepts it and calls OnDrawItem. This function looks rather messy, but the logic is mostly an application of the rules from the mini button spec I presented earlier. I won't take you through it step-by-step, but instead I'll focus on the tough spots.
Figure 10 Popup Menu
Figure 10 Popup Menu

The worst part is drawing checkmarks or radio buttons. Figure 10 shows a popup menu with normal and radio-style checkmarks redone with the 3D look. CCoolMenuManager::Draw3DCheckmark is the function that does it. There are two tricks here. The first is getting Windows to use the bitmap for the checkmark:

 CBitmap bm;
 bm.LoadOEMBitmap(OBM_CHECK);
OBM_CHECK is not normally defined in winuser.h unless you #define the symbol OEMRESOURCE before loading it. So, following in the hack footsteps of MFC, instead of asking you to define OEMRESOURCE in your StdAfx.h, I simply define the symbol I need:

 #ifndef OBM_CHECK
 #define OBM_CHECK 32760
 #endif
I guarantee you the value won't change anytime soon. Draw3DCheckmark only uses OBM_CHECK if the menu item doesn't have its own checkmark bitmap. You may not know this, but Windows lets you specify any bitmap to use on a per-item basis for a checked or unchecked menu item. In fact, that's how MFC implements the radio-style menu items you get when you call pCmdUI->SetRadio from your update handler.

 // in cmdtarg.cpp
 void CCmdUI::SetRadio(BOOL bOn)
 {
 •
 •
 •
    if (afxData.hbmMenuDot = = NULL)
       _AfxLoadDotBitmap();
    if (afxData.hbmMenuDot != NULL)
       SetMenuItemBitmaps(...,
          NULL, afxData.hbmMenuDot);
 •
 •
 •
 }
_AfxLoadDotBitmap doesn't actually load the bitmap, but creates it manually. Unfortunately, it does a sad job. Figure 11 shows what MFC generates if you select a large menu font, something you might do if you forgot to bring your glasses to work. Whoever wrote _AfxLoadDotBitmap went to heroic lengths to create a bitmap from raw hex data, when all he or she had to do was call CDC::Ellipse. Go figure.
Figure 11 BadDot
Figure 11 BadDot
In any case, I couldn't bear letting anyone using my menu code have such a pitiful radio button, so I wrote FixMFCDotBitmap to set the dot straight. This required more subterfuge. MFC stores the bitmap in afxData.hbmMenuDot, but there's no safe way to access afxData since it's a private structure. Instead, I create a menu with one item and set up a CCmdUI object for it. I call CCmdUI::SetRadio(TRUE), then call GetMenuItemInfo to see what bitmap MFC set as the checkmark. Pretty clever. Once I have the bitmap, I draw a proportional dot on it that's centered properly. Just to be nice, I broke out a separate static function, GetMFCDotBitmap, which you can use to customize the radio check your own way.
Astute readers may be wondering: if Windows lets you specify custom checked and unchecked bitmaps for any menu item, why not use them to implement menu buttons in the first place? Nice idea, but it doesn't work for two reasons. First, you only get two bitmaps (checked/unchecked), whereas cool menus need four because the image also depends on the item's selection state. Second, checkmark bitmaps can only be monochrome, not color.
In any case, if the MENUITEMINFO has a custom hbmpChecked or hbmpUnchecked, CCoolMenuManager uses it; otherwise it uses the default OBM_CHECK for checked items. Once Draw3DCheckmark has the bitmap, it gives it the 3D treatment: a pushed-in edge with background = COLOR_3DLIGHT (unselected) or COLOR_3DFACE (selected). To get the desired background color, I call SetBkColor on the destination (menu) DC, relying on the fact that the checkmark is a monochrome bitmap. When Windows translates a monochrome bitmap to color, it maps white to background and black to foreground colors. The rest of Draw3DCheckmark is just a lot of painful Cartesian arithmetic to get the bitmap perfectly centered, something even Visual Studio doesn't do right. The checkmark could be bigger or smaller than the standard 16
X 15 button size; to do it right, you have to anticipate either possibility.
Finally, after whatever button/checkmark is required, OnDrawItem draws the text. This entails drawing the highlighted background, if necessary, and then the text in the proper color. OnDrawItem uses a helper function, DrawMenuText, which draws the text in two stages. First it draws everything preceding the tab character (if there is one) using DT_LEFT for left-justification, then it draws everything to the right of the tab—usually the accelerator key name—using DT_RIGHT. This gives the menus the justified cool look. For disabled text, OnDrawItem draws the text twice: once for highlight, once for shadow. Finally, the item appears on the screen (see Figure 1). Amazing.

Remembering Mnemonics

At this point, the menu is converted, measured, and drawn. Everything looks hunky-dory and works fine, just the way it does in Windows—except for one little problem: menu mnemonics. You know, the letters that have an ampersand that gets converted to an underline, like &File for File. If you type Alt-F to get the File menu, Windows will display it; but if you then type O for Open or S for Save, nothing happens. Sigh. But, how do you expect Windows to know what to do with keyboard input when you went and changed all the items to owner-draw?
The solution is to reinvent the wheel. WM_MENUCHAR is the message Windows sends when you type a key; OnMenuChar is the CCoolMenuManager function that processes it. It's just a straightforward matter of looking for ampersand-prefixed characters that match what was typed and returning the proper magic code to tell Windows what to do. If there are no matches, return zero; if one, return MNC_EXECUTE; if there's more than one match, return MNC_SELECT. In the last two cases, you actually have to return a LONG with the code in the high word and the index of the menu item in the low word. It's all dreadfully tedious, so I'll spare you the details. Note that OnMenuChar applies to menu mnemonics only; accelerator keys remain unaffected by owner-draw because accelerators get translated in the program's main message loop.

Automagic Accelerators

While I was implementing CCoolMenuManager, I had a bright idea. Have you ever found it cumbersome to type accelerator names in all your menus? Say you decide to make Control-G a shortcut for the File | Gobble command. You add the accelerator, but do you remember to add the key name to your menu?

 MENUITEM "&Gobble\tCtrl-G"
Even if you're the fastidious sort of person who remembers, what if you have an item whose name you change dynamically by calling CCmdUI::SetText? Do you remember to add the accelerator then, too? And how do you abbreviate Control—Cntrl, Ctl, or Cntl? Personally, I can't be bothered to remember that stuff, so my bright idea was to let the code do it. In addition to all the other neat stuff CCoolMenuManager does, it adds all the correct accelerator names to your menus!
How does it perform this magic? If you remember way back, the very first thing your app did was call CCoolMenuManager::Install.

 // in CMainFrame::OnCreate
 m_menuMgr.Install(this);
So the menu manager has a pointer to your frame window, which it stores as m_pFrameWnd. From this it can get the handle of your app's accelerator table by calling m_pFrameWnd->GetDefaultAccelerator. Once it has the HACCEL, it's just a matter of writing a little code to figure out the key names and IDs. The API functions you need are CopyAcceleratorTable and GetKeyNameText. CopyAcceleratorTable copies the HACCEL into an array of ACCEL structures. GetKeyNameText does what its name implies. The mechanics are straightforward; see CCoolMenuManager:: LoadAccel and AppendAccelName for details.
Figure 12 Accelerator
Figure 12 Accelerator
LoadAccel is even clever enough to handle multiple accelerators for the same command. It does this by linking multiple ACCEL structures, using ACCEL::cmd as an offset to the next ACCEL. The upshot is that if you have more than one accelerator for a command—which is entirely possible—they all appear in the menu, as Figure 12 shows. If you don't want automagic accelerators, you can turn the feature off by setting m_bAutoAccel = FALSE.

Bye-bye Menu

I've shown you how CCoolMenuManager loads the toolbars, traps messages on behalf of your frame, and uses WM_INITMENUPOPUP to convert the menu to owner-draw. I also showed how it measures, draws, processes keystrokes, and even appends accelerators to your app's menu items automatically. The next and almost final step is cleanup. Remember all those little CMyItemData objects allocated in ConvertMenu? I need some way to destroy them before the app exits or else the Memory Police will be hot on my tail. It would be nice if Windows sent a WM_DELETEITEM message when it's time to delete the menu item data—the way it does with owner-draw lists and comboboxes. But no, with menus you have to make your own provisions to avoid memory leaks.
When I first implemented CCoolMenuManager, I simply added all the CMyItemData objects to a list and deleted them in the destructor. Easy. But later I changed the implementation entirely. To understand why, I have to tell you about my mouse.

Theoretical Digression

Before Windows was invented, I programmed for Interleaf on a workstation with a three-button mouse. Ever since, I simply can't live without a three-button mouse. Today I use the Logitech MouseMan, whose driver has the wonderful option of mapping the middle button to double-click. No more nervous finger-fiddling; I just press the middle button to open something or launch an app. MouseMan has another neat feature: if you press and hold the middle button you can invoke the menu of whatever app you're in. In effect, MouseMan brings the menu to the mouse, so you don't have to move your mouse to the menu. Very cool, and something Interleaf did ages ago.
The MouseMan driver does this by peeking inside the app and grabbing its menus. It feeds the app messages like WM_INITMENUPOPUP and so on to make it think you invoked the menu the normal way. The MouseMan driver is an example of a program that dynamically snoops other app's menus. Other programs that do this are debugging tools that record keyboard and mouse actions, and accessibility products for people with handicaps. For example, Kurzweil has a voice product that reads menus aloud for blind people; Microsoft Voice does the same thing.
Many of these products depend on being able to read the menu names as strings. In other words, they depend on the menu items being MFT_STRING; they may not work with owner-draw menus. The MouseMan driver does because it only needs to display the menu, not read the actual text in the items. But other programs that depend on reading the text will be completely bewildered by an owner-draw item. The Microsoft Office 97 and Visual Studio 97 products have the same problem: they fail completely with MouseMan because they don't have menus at all—they implement their "menus" as special command bar classes (msoCommandBar). The Office 97 products also fail with many accessibility products.
Has Microsoft hung blind people out to dry? No, of course not. Microsoft has a whole new scheme for doing accessibility. It's called MSAA.
To make a long story short, in order to make CCoolMenuManager as accessible as possible, I decided to convert all the menus back to strings as soon as the menu is closed. That way, only programs that try to read a CCoolMenuManager-enabled app's menus while they're actually displayed will fail. Programs that read the menus while your app is idle—for example, when the accessibility program starts up—should work fine.

Back to Cleanup

All that was just to explain why I chose to convert the menu back to its original state when I close it. How do you know when that happens? When Windows sends a WM_ MENUSELECT message with a NULL menu and the special flag 0xFFFF. This means the menu was closed because the user either selected a command or clicked outside the menu. Either way, CCoolMenuManager handles the event by calling the same ConvertMenu function used to make the menu owner-draw. Only this time it calls ConvertMenu with bShowButtons = FALSE, in which case ConvertMenu does the opposite of what it did before: it converts all the menu items back to their original MFT_STRING state. In the process, ConvertMenu also destroys all the CMyItemData objects, taking care of my cleanup problem.
There's just one catch. A single trip to the menu bar could end up firing several WM_INITMENUPOPUP messages as the user moves the mouse over File, Edit, View, and so on, whereas Windows only sends one WM_ MENUSELECT (0xFFFF) for the entire trip. So as ConvertMenu converts each menu to owner-draw, it adds the menu to a list. OnMenuSelect then uses this list to know which menus to unconvert.
I added some TRACE diagnostics to help you see what's going on. You can turn them on or off through the static member CCoolMenuManager::bTRACE. Figure 13 shows the output from CCoolMenuManager as I moved the mouse over the Help | About command a couple of times. The menu manager converts the menu to owner-draw, then handles a WM_MEASUREITEM, then handles a few WM_ DRAWITEMs, and finally converts the menu back to strings. It seems so simple, but now you know everything that goes on behind the scenes.
Figure 13 CCoolMenuManager Output
Figure 13 CCoolMenuManager Output

While I chose the convert/unconvert implementation to deal with accessibility as best I could, it turns out that it simplifies a number of things. For example, it's trivial to implement the m_bShowButtons flag. Since the menu is always in its normal (MFT_STRING) state, all OnInitMenuPopup has to do to turn off the cool look is not call ConvertMenu if the flag is off.
Also, because OnInitMenuPopup converts the menu to owner-draw each time, there's no need to do anything special if the user changes the system font, which can affect the size of the menu items. Normally, Windows sends WM_MEASUREITEM once and never again. This causes a problem if the user changes the menu font; you have to do something special to get Windows to send WM_ MEASUREITEM again (you have to call ModifyMenu to reset the type to MFT_OWNERDRAW, even though it already is). But the way I wrote it, if you select a new menu font, the code just works.
Of course, converting and unconverting the items every time a user invokes a menu requires a few more CPU cycles—but hey, this is the age of Pentium! It's not as if the user will ever notice.

Coping with Colors

The last problem I have to mention deals with system colors and fonts. This is always an agonizing bugaboo, and few commercial apps get it right (as Figure 14 shows). Not even the Windows Start menu is smart enough to shrink itself if you go from a large font to small font, so you've gotta reboot.
Figure 14
Figure 14
To cope with a changing world, always remember to use GetSysColor and GetSystemMetrics, and don't let any hardwired colors or dimensions sneak into your app. You must handle WM_SYSCOLORCHANGE and WM_SETTINGCHANGE as well. Figure 15 shows the system colors and metrics used in CCoolMenuManager. As for WM_SYSCOLORCHANGE and WM_SETTINGCHANGE, the most reliable way to handle them is the brute force approach: when you get either message, simply destroy everything and reconstruct the world. This is what CCoolMenuManager does in a function called Refresh. I don't know why so many programs don't handle these messages right because it's really not that hard. Of course, now that I said that, I just know someone is going to find a bug in my code.
There are plenty of other bizarre things to worry about. Take a look at Figure 16. Aside from the ghastly color scheme (apologies to anyone who likes brown), check out that menu! It has the MENUBARBREAK style set on one of the items to make it a two-column menu. The first time I tried this, I discovered bugs where my drawing code assumed the leftmost x-coordinate was zero, which is only true for a single-column menu. I also found what appears to be an extremely nutty bug in Windows: if you have a menu item with a MENUBARBREAK, then all items following the break lose all text following a tab character. You can try it yourself on a vanilla AppWizard app. There's no way to anticipate these things; you just have to find the bugs one by one—or download the code from someone who already has.
Figure 16 Multicolumned Menu
Figure 16 Multicolumned Menu

ROPView

To demonstrate CCoolMenuManager in action, I wrote a demo program called ROPView. I used ROPView to discover what ImageList_Draw and DrawState actually do with all those different flags. ROPView implements a right-button context menu with some of the same commands that appear in the main menu. Since Windows does the same WM_INITMENUPOPUP/WM_MENUSELECT shtick for context menus too, the menu manager automatically handles them without having to add extra code.
The full source code for ROPView is way too long to print here and not really that interesting anyway. Figure 3 shows CCoolMenuManager, which is the most important part. Happy programming!

Acknowledgment

I'd like to thank Gerry Polucci of MindStorm Technologies for helping me with CCoolMenuManager. Gerry developed a similar class on his own and sent it to me via email, hoping to publish it in MSJ. Alas, I was already booked for this article. Gerry was nevertheless gracious enough to swap notes, put me onto some good ideas and help me test my code. Thanks, Gerry!

From the January 1998 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.


For related information see: MFC 4.0: The Story Can Be Told, http://premium.microsoft.com/msdn/
library/periodic/periodic/msdnn/news4_6/mfc.htm.
Also check http://www.microsoft.com/msdn for daily updates on developer programs, resources and events.

© 1997 Microsoft Corporation. All rights reserved.
Terms of Use
.

© 2014 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft