This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


January 1999

Microsoft Systems Journal Homepage

C++ Q&A

Code for this article: Jan99CQA.exe (71KB)

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

     Q: I read your columns in the August and October 1997 issues of MSJ describing how to implement coolbars in MFC and your article about cool menus in the January 1998 issue. How can I implement a menu in my coolbar, as is done in Visual C++®, Outlook® and the Microsoft® Office products? I don't know where to begin.

     Many readers

     A: You don't know where to begin because it can't be done using any presupplied means. Probably you were hoping all you have to do is use the TBS_MENU flag in the CreateFooble function, and return an HMENU when you get an NM_PASSMETHEMENUPLEAZ notification. Unfortunately, life is never that good in windowland. Command bars, as they're known in the trade, require lots of programming. That's why they take up the entire column this month.

     The answer is this: you have to more or less reinvent menu bars from scratch. But before I switch to geek mode, some words of caution. I'll bet ten dollars the Microsofties will eventually make command bars available through some DLL or other, the same way they made common controls and the 3D look a standard part of Windows®. In fact, I'm certain of it. Why? Because command bars are already available in Windows CE. So if there's any way you can, wait. But if there's absolutely no way to stave off Mr. Acme Corp President from demanding the latest GUI look-now!-then keep reading. I'll show you how to make even Mr. Acme happy.

Figure 1 Coolbar Bands
         Figure 1 Coolbar Bands

     Just so everyone knows what I'm talking about, what are command bars, anyway? Command bars are toolbars that contain menus and can sit inside coolbars (also known as rebars) in apps like Microsoft Internet Explorer 4.0. Figure 1 shows a coolbar with a command bar, toolbar, and combo box as child windows (or bands). The main benefit of command bars is they don't have to span the entire width of the frame. Figure 2 shows the same app with all the bands on one line. With the high cost of pixel space-so many buttons competing for visibility-command bars are a win, which is why Windows CE already has them. (Not many pixels on those teeny palmtops.) Coolbars/rebars, command bars, and cool menus are all part of the new GUI look.

    

Figure 2 One-line Bands

     To implement command bars, I wrote an MFC class called CMenuBar. I should've called it CCommandBar, but I wrote the code before indoctrination to official barspeak (menu bar is more accurate, anyway). CMenuBar is trivial to use. Instantiate one in your coolbar class, create and load it, then add it to your coolbar like any other band. If you're using my CCoolBar class from the October issue, the place to do all this is in CCoolBar::OnCreateBands.

class CMyCoolBar : public CCoolBar {
protected:
  CMenuBar m_wndMenuBar;
  virtual BOOL  OnCreateBands();
};

BOOL CMyCoolBar::OnCreateBands()
{
  CMenuBar& mb = m_wndMenuBar;
  VERIFY(mb.Create(this, /* args */));
  mb.LoadMenu(IDR_MAINFRAME);
  CSize szMin = // whatever you want
  return InsertBand(&mb, szMin, 0x7ff);
}

     Visual C++ 6.0 has a new CReBar class for doing coolbars; if you want to use CReBar, you'll have to adjust accordingly. Either way, there's one more thing you have to do: write a PreTranslateMessage function that gives your CMenuBar a chance to translate messages.

BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
  return m_wndCoolBar.m_wndMenuBar.
    TranslateFrameMessage(pMsg) ? TRUE : 
      CFrameWnd::PreTranslateMessage(pMsg);
}

     That's it. Of course I designed CMenuBar to be easy. That's my job. But behind the simple interface lies some really gnarly MFC hacking. In fact, there's too much code to explain it all here; I can only give you the essentials and send you to the source code for details.

     But why all the code? What's the big deal with command bars? Ever since birth, Windows has conceived of menus as an attribute of top-level windows, not as windows in themselves. You specify the menu when you create the window or through a call to SetMenu. Windows does the rest. It draws the menu bar, handles mouse action, tracks the popups, handles keyboard mnemonics, and sends your app all kinds of warm fuzzy messages like WM_INITMENU so you can do stuff. But in Windows, only frame windows (not child windows) can have a menu, and a menu bar spans the entire width of the frame. For command bars, what you want is a little autonomous resizable window that's a menu. So what's a poor developer to do?

     Sadly, the only thing you can do is reinvent everything from scratch, inside a window. Well, not everything. You can call TrackPopupMenuEx to run popup menus, so all you really have to implement is the menu bar-the top-level row of menu items. There are many ways to do it-Visual C++ and the Office products each use their own special window classes-but the way Internet Explorer 4.0 does it is to start with a ToolbarWindow32 that has text (as opposed to iconic) buttons.

     Figure 3 shows the elements of a command bar implemented this way: the toolbar (menu bar), buttons (top-level menu items), and popup menus, all inside a coolbar. You supply the glue to make everything work. That is, the code that handles mouse and keyboard input to track the buttons and display the proper popup for each top-level menu item. It sounds easy but turns out to be the coding equivalent of mucking out the Augean stables. So let's roll up our sleeves and start cleaning!

    

Figure 3 Text Buttons

     CMenuBar is derived from CFlatToolBar from my October 1997 column. The first interesting CMenuBar function is LoadMenu. That's the function you call to load the menu, remember? It comes in three overloaded flavors, but only one does any work (see Figure 4).

     After deleting any old buttons, LoadMenu adds a button for each top-level menu item. The buttons have TBSTYLE_AUTOSIZE set, which tells the toolbar to size the button based on its text. Perspicacious readers will have noticed I omitted the detail that shows how you actually set the button text. Toolbars have a curious way of doing this. You don't just set the text as an LPCTSTR. Instead, you create a string with all the text labels, zero-terminated with a double-zero at the end:

LPCTSTR myStrings = 
_T("&File\0&Edit\0&View\0");

     Then you call AddStrings with this string, and for each button you specify the index of the text you want in TBBUTTON::iString. Pretty weird, huh? Even weirder is the fact you can't delete strings, you can only add them! But I want to let programmers call LoadMenu repeatedly to load different menus, so CMenuBar has to go through some minor conniptions to combat the toolbar. CMenuBar keeps an array of all the strings it's ever added, so it won't add the same string again. Otherwise the toolbar might eventually (after a few centuries) run out of memory. Also omitted to spare you is some chicanery with NULLing the frame menu. When you call LoadMenu, CMenuBar automatically sets your frame's menu to NULL. That's because it doesn't make sense to have a frame menu if you're using command bars. But MFC gets very upset if you do pFrame->SetMenu(NULL) while the frame is being created, so instead CMenuBar postpones the deed by posting a message to itself.

     At this point what you have is a toolbar with a bunch of text buttons that don't do anything. You press a button and nothing happens. What you want to happen is have a popup menu appear. For example, if you click the File button, you expect to see a popup menu with the File commands: Open, Save, Print, and so on. No problem. Just add some code to do it.

void CMenuBar::OnLButtonDown(UINT nFlags, CPoint pt)
{
  int iButton = HitTest(pt);
  if (iButton >= 0 && iButton<GetButtonCount())
    TrackPopup(iButton);
  else
    CFlatToolBar::OnLButtonDown(nFlags, pt);
}

     CMenuBar::HitTest returns the button hit, or -1 if none. It overrides TB_HITTEST, which has a bug in at least some versions of comctl32.dll. (If the button is clipped at the end of the toolbar, TB_HITTEST will return a hit for that button even if the mouse is totally outside the toolbar window!) The other function, TrackPopup, displays the popup.

void CMenuBar::TrackPopup(int iButton)
{
  PressButton(iButton,TRUE);
  HMENU hPopup = ::GetSubMenu(m_hmenu, iButton);
  TrackPopupMenuEx(hPopup, /* lot's o' args */);
  PressButton(iButton,FALSE);
}

     That is, push the button, run the popup, and unpush the button. All very simple. But when you run this code you run into brick wall number one. Your app starts fine, all the pretty buttons appear. You click File and TrackPopup displays the popup with Open, Save, and so on. But now try moving the mouse over a different menu bar item, like Edit. What happens? Anyone who's ever used Windows knows what should happen: the system should dismiss the File popup (Open, Save, Print) and display the Edit popup (Cut, Copy, Paste). Does this in fact happen? Noooo. Why not? Because CMenuBar is still waiting for TrackPopupMenuEx to return.

     When you call TrackPopupMenuEx, control disappears into a black hole-TrackPopupMenuEx's own little mini message loop-and doesn't come out again until the user selects an item or dismisses the menu by clicking outside it, pressing Escape, or kicking the disk hard enough to dislodge its platters. The user can move the mouse all he likes; TrackPopupMenuEx won't return control. So how are you supposed to track the popups when you don't have control? If you could somehow detect when the mouse has moved over a new button, you could send WM_CANCELMODE to cancel the popup, then display a different one. But how can you detect where the mouse is when your code doesn't have control?

     The answer is: install a Windows hook. Oh dear, not that. Windows hooks are to most programmers what a cross is to a vampire. Any time the answer is "install a Windows hook," you know you're in, as my geologist friend calls it, deep schist. But it's not really that bad. A Windows hook is no more than a callback function you can install to have Windows call you when something interesting happens. There are several kinds of hooks, but the one that does the trick here is a WH_MSGFILTER hook. If you install a WH_MSGFILTER hook, Windows calls it whenever there's any kind of input event-mouse, keyboard, cortical array-destined for a dialog box, message box, scroll bar, or menu. So the next rewrite of TrackPopup goes something like Figure 5.

     The idea is this. Before calling TrackPopupMenuEx, install a menu input hook. The hook watches mouse events. If the hook detects that the user has moved the mouse over a different toolbar button, it notes the button (m_iNewPopup) and cancels the popup. TrackPopupMenuEx returns, TrackPopup removes the hook and, seeing m_iNewPopup has been set, repeats the whole process, this time to display a different popup. If TrackPopupMenuEx returns for any other reason, m_iNewPopup will be -1 and TrackPopup quits. Pretty neat. Now, here's the hook function.

     Mouse input down. Next, the keyboard. There are several issues here. First is getting the right and left arrow keys to track popups the same as the mouse. As you'd suspect, the code is almost identical. The same OnMenuInput handler works for both.

BOOL CMenuBar::OnMenuInput(MSG& m)
{
    if (m.message==WM_MOUSEMOVE) {
    // as before
    .
    . 
    .
    } else if (m.message==WM_KEYDOWN) {
        if (/* VK_LEFT or VK_RIGHT */) {
            int iNewPopup = 
                GetNextOrPrevButton(m_iPopupTracking, 
                                    vkey==VK_LEFT);
        if (iNewPopup != m_iTracking) {
            m_iNewPopup = iNewPopup;
            GetOwner()->PostMessage(WM_CANCELMODE);
        }
  }
  return FALSE; // don't eat
}

     The difference is that instead of calling HitTest to determine what the new button/popup is, OnMenuInput calls another function, GetNextOrPrevButton. This function simply increments and decrements the button number, wrapping at either end.

     As usual, reality is not quite as simple as I've painted. If the highlighted menu item has a submenu, pressing right-arrow should not display the next top-level popup, but should display the submenu. In other words, don't do anything, let TrackPopupMenuEx do its thing. Likewise, if the user presses left-arrow to back out of a submenu, you don't want to display the previous top-level popup. So you need some code to detect and ignore these special cases. How does OnMenuInput know what popup/item TrackPopupMenuEx is currently highlighting? It doesn't. But TrackPopupMenuEx sends a WM_MENUSELECT message whenever the user selects a new menu item. The problem is Windows sends the message to whichever window you specified as the owner when you called TrackPopupMenuEx. For all the MFC message map stuff to work CMenuBar specifies its owner-the containing frame-as the popup owner. So that's where Windows sends WM_MENUSELECT. To intercept it, CMenuBar uses one of my standard programming tricks, CSubclassWnd. This little class lets any object intercept messages sent to a window. CMenuBar defines its own CSubclassWnd derivate to intercept WM_MENUSELECT messages sent to the frame.

LRESULT 
CMenuBarFrameHook::WindowProc(
  UINT msg, WPARAM wp, LPARAM lp)
{
  if (msg==WM_MENUSELECT) {
    m_pMenuBar->OnMenuSelect(
      (HMENU)lp, (UINT)LOWORD(wp));
  }
  return CSubclassWnd::WindowProc(msg, wp, lp);
}

     Following the usual paradigm, CMenuBarFrameHook passes the buck to a CMenuBar virtual function, OnMenuSelect, which does the real work.

void CMenuBar::OnMenuSelect(HMENU hmenu, UINT iItem)
{
  if (m_iTrackingState > 0) {
    m_bProcessRightArrow = 
      (::GetSubMenu(hmenu, iItem) == NULL);

    m_bProcessLeftArrow = 
      hmenu==m_hMenuTracking;
  }
}

     OnMenuSelect sets up some flags to tell OnMenuInput when to process arrow keys. CMenuBar will process VK_RIGHT only if the current menu item doesn't have a submenu; it processes VK_LEFT only if the current menu is the same one it originally passed to TrackPopupMenuEx (that is, not a submenu). Of course, you have to modify CMenuBar::TrackPopup to set m_hMenuTracking before calling TrackPopupMenuEx, and OnMenuInput to process the left/right keys only if m_bProcessLeft/RightArrow are set. Consider it done.

     That solves the tracking problem for keyboard input, but there are other keyboard issues. Once you start implementing keyboard support for command bars, you realize that there are actually three possible states your command bar can be in, as far as tracking goes: the resting state (CMenuBar::TRACK_NONE) where nothing is happening, TRACK_POPUP when you track popups, as I've been discussing, and TRACK_BUTTON state, which you enter by pressing F10 or Alt.

     In the TRACK_BUTTON state, one of the buttons (File, Edit, and so on) is highlighted and pressing right/left highlights the next/previous button, without displaying any popups. Implementing the TRACK_BUTTON state is a simple matter of processing keystrokes-no fancy hooks or anything. But how do you get the keystrokes? This is where CMenuBar::TranslateFrameMessage comes in. I told you earlier one of the things you have to do to use CMenuBar is implement a PreTranslateMessage function that calls CMenuBar::TranslateFrameMessage. Remember? This is what it looks like in pseudocode.

BOOL CMenuBar::TranslateFrameMessage(MSG* pMsg)
{
    if (/* F10 or Alt key */) {
        ToggleTrackButtonMode();
        return TRUE; // handled
    }
    else if (/* VK_LEFT or VK_RIGHT */) {
        SetHotItem(GetNextOrPrevButton(GetHotItem(), 
            vkey==VK_LEFT));
        return TRUE; // handled
    }
}

     ToggleTrackButtonMode toggles the command bar's state between TRACK_NONE and TRACK_BUTTON. SetHotItem highlights the ith button-this is a CFlatToolBar function, a wrapper for TB_SETHOTITEM. Once again I'm glossing over details. For example, F10 comes in as WM_SYSKEYDOWN, not WM_KEYDOWN, and you have to handle key up as well as key down events to nail the details. Also, if the user presses VK_DOWN (down-arrow) while in the TRACK_BUTTON state, CMenuBar goes into the TRACK_POPUP state; that is, it "enters" the popup for the hot button. There's nothing major here, just different uses of functions you've already met.

     What about other keys, like Alt-F to invoke the File menu or Alt-E for Edit? Or just typing F while in TRACK_BUTTON mode? Do you have to scan all the buttons looking for strings with an ampersand in them followed by a letter that matches? Fortunately not. This is one place where toolbars come to the rescue. The latest (4.72) version of comctl32.dll has a toolbar message called TB_MAPACCELERATOR that takes a key and the address of a UINT, and returns TRUE if the key matches a button mnemonic, with the command ID returned in the UINT. CFlatToolBar has a wrapper for this called MapAccelerator. So all you have to do to handle Alt-F and company is add another if clause to TranslateFrameMessage.

if (/* Alt + alphanumeric key,
  or alphanum in TRACK_BUTTON state */) {
  UINT nID;
  if (MapAccelerator(vkey, nID)) {
    TrackPopup(nID); // found mnemonic: track menu
    return TRUE;
  }
}

     For terminology pedants, TB_MAPACCELERATOR is a misnomer. It should be called TB_MAPMNEMONIC. Accelerators are shortcut keys defined by an accelerator table. The underlined & characters-such as the O in Open-are properly called mnemonics. CMenuBar doesn't have to do anything to handle accelerators; MFC handles them through its normal mechanisms.

     Speaking of MFC, how do command bars interact with the framework? Since CMenuBar passes the toolbar's owning window, which CFlatToolBar sets to be the frame window, to TrackPopupMenuEx, everything just works. TrackPopupMenuEx sends WM_INITMENUPOPUP and WM_COMMAND messages to the frame, just the way it would for a normal frame-level menu. All your ON_COMMAND and ON_COMMAND_UPDATE_UI handlers go on working.

     There is one other snag I should tell you about. The first time I ran my command bar, all my buttons looked rather sickly. Which is to say, grey. At first I was dumbfounded, but a little spelunking revealed the culprit. By default, MFC disables menu items that don't have handlers. To simplify implementation, I chose the index of the button as its ID, so the IDs have values like 0, 1, 2, and so on. It doesn't make sense to think of the button IDs as being command IDs, since they represent top-level menu choices, not commands-but try telling MFC that! Since the IDs 0, 1, 2, and so on have no ON_COMMAND handlers, MFC insisted on disabling them. To work around this, I implemented an ON_UPDATE_COMMAND_UI_RANGE handler for all IDs in the range 0 to 255, one that specifically enables the buttons. 256 is an arbitray but reasonable number. If your top-level menu has more than 256 items, you need to schedule some therapy sessions with Dr. GUI.

     My test program, MBTest (see Figures 1 and 2), which you can download from http://www.microsoft.com/msj, is a simple text editor based on CEditView. It combines all the cool UI classes from my previous columns: CFlatToolBar, CCoolBar, CCoolMenuManager, and CMenuBar. Figure 6 shows only the source for CMenuBar. Like I said, it's a lot of code. If it seems like too much, consider this: CMenuBar doesn't even handle MDI buttons (min/restore/close) for maximized MDI windows! And it doesn't do MSAA (Microsoft Accessibility API) either. I leave these minor details as exercises for the reader. Good luck!

     Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com.

From the January 1999 issue of Microsoft Systems Journal.