Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An examination of menus from a beginner's point of view

0.00/5 (No votes)
22 Jun 2004 14  
A discussion of the OS menu object from a beginner's viewpoint. I take you through the basics to a more advanced understanding of menus and how to interact with them from code.

Table of content

Introduction

In the process of researching one of my articles (An owner-drawn menu plug-in), I needed to know a lot more about menus and how they work. I thought it would be an ideal subject to write a beginners' tutorial on the subject. I have also collated together in one place all the messages sent/received and functions that can be used to manipulate menus, as they are rather spread out through the available documentation - please let me know if I have missed any.

So this article aims to give you the reader, whatever your level of competence, a better understanding of menus and how your code/application will interact with them. I will be starting from the fundamentals and working from there, during which I will present more advanced functions (and the development of them) to allow you to manipulate menus. This guide is not exhaustive. I will also examine the difference between menus in standard WIN32 and MFC applications.

Minor disclaimer

One small note. The information presented here may not be complete or 100% accurate. It is what I have learnt about menus over the years and the result of recent research into them. Although I may not be 100% accurate, all the code presented here is tested to a high degree and should be reliable in most, if not all circumstances, but you still use it at your own risk.

What is a menu?

Well you cannot get any more basic than this. A menu is a standard user interface tool of Windows to allow a user to select a choice from a series of options. It allows these options to be grouped together into categories of related functionality (e.g., the File menu), present short cut information (Accelerators), and also show the availability of the commands themselves (items disabled / enabled).

Menus are different to all other UI interface objects in Windows. For a start, they are not implemented as windows like Edit controls, listboxes and other standard Windows types are. This means that they cannot be overridden in the usual way. There are many articles here on CP about how to implement owner drawn menus, so apart from style issues, it's an area I will not be covering in this article (phew!).

A menu is usually only displayed for the time taken for the user to make a selection from it. There is one exception to this, in the case of a top level menu in an application or a dialog. In such top level menus, the available menu categories/options are shown.

Keyboard accelerators

Note that most top level menu item names have '_' (underscore) characters beneath a letter. This is a keyboard short cut to the menu by using the Alternate key on your keyboard. These short cut keys are setup in a menu by using the & character when defining the menu item text, e.g., "&File".

By using these top level menus, a user can interact with any option by either the mouse or through the keyboard interface (e.g., Alt + F would quick open the File menu).

Shortcut keys can also be available in a popped up menu:

Popup menu with keyboard shortcuts

In these cases, once a popup menu is displayed the user can press the underlined key to quick select that item. In the example above, the user could press P to start the Print menu option.

A recent development in Windows XP is that the underline characters are only shown while the Alt key is pressed. If the menu is opened via the mouse then they are not shown. I think this is a backwards step, but then Microsoft did not consult me on the design. Fortunately, this is user configurable.

Actual keyboard accelerators for menu items (e.g., Ctrl + P) can be selected by the user without displaying the menu at all, but the correct keycode combination(s) must correctly map to the right menu command ID. These accelerators are a separate topic.

Item selection prompts

One feature available in most if not all Windows programs is that when the user highlights an item in a menu, a description of the menu command is shown in the StatusBar of the application (if it has one):

Menu item prompt in status bar

We will see later how an MFC application implements this.

Top level menu item order

Microsoft's standard user interface protocol requires that top level menus follow a standard layout. Normally, the first menu allows access to documents and files, while the 2nd from last is the Window menu and the last is the Help menu. Following this standard layout allows all programs to present a consistent interface to users, and should be followed when designing your own application's menu layout. Deviate from this at your own risk and your users' wrath.

A menu from a programmer's perspective

When looking at a menu from the programmer's point of view, a menu or HMENU is a system OS object which you interact with through its HMENU HANDLE value. The OS provides a series of functions which allow you to manipulate and create these types of objects, as you cannot manipulate the menu objects directly. It also provides an interface through the use of messages so that advanced features can be added. MFC provides the same access functions although it uses a wrapper class CMenu which holds the HMENU handle.

System supplied menu functions

First, let's take a look at the raw WIN32 functions provided by the system to allow you to manipulate menus. Anything that goes for the WIN32 function is also true for the MFC version of the function in the CMenu wrapper class. The functions are grouped together here by related functionality:

I will not be examining all the menu functions here.

Menu creation

Here, I will do a quick examination of the menu creation commands, and try and use the product of each as a top level menu and as a popup menu. I jump ahead slightly with some of the menu modification commands, but we will see how these work later.

  • HMENU hMenu = ::CreateMenu()

    This command is used to create a new top level menu object. This new menu is initially empty and can be filled out using the Menu modification commands listed later. Any menu created this way needs to be destroyed when you have finished using it unless it is owned (being used, or selected into) by a window that is destroyed, as these windows automatically destroy any menu they are using. See the Menu destruction section later for additional information and functions.

    CreateMenu() example code:

    void CMenuView::OnLButtonDown(UINT nFlags, CPoint point) 
    {
        // display a menu created using CreateMenu()
    
        HMENU hMenu = ::CreateMenu();
        if (NULL != hMenu)
        {
            // add a few test items
    
            ::AppendMenu(hMenu, MF_STRING, 1, "Item 1");
            ::AppendMenu(hMenu, MF_STRING, 2, "Item 2");
            ::AppendMenu(hMenu, MF_STRING, 3, "Item 3");
    
            ClientToScreen(&point);
    
            int sel = ::TrackPopupMenuEx(hMenu, 
                    TPM_CENTERALIGN | TPM_RETURNCMD,
                    point.x,
                    point.y,
                    m_hWnd,
                    NULL);
            ::DestroyMenu(hMenu);
        }
    }

    The above example code gives us a menu which looks like:

    CreateMenu() popup result

    Not very good! Where are our options? CreateMenu() is for the creation of top level menus, so it would be best used with a dialog or a mainframe window.

    When a menu created using CreateMenu() is used as a top level menu:

    // code extracts
    
        m_hCreateMenu = ::CreateMenu();
        if (m_hCreateMenu != NULL)
        {
            // see later for the implementation of this function
    
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup1\\item1", 1);
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup2\\item2", 2);
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup3\\item3", 3);
        }
    
    void CAboutDlg::OnCreateMenu() 
    {
        // switch to the CreateMenu created menu
    
        CMenu    menu;
        menu.Attach(m_hCreateMenu);
        SetMenu(&menu);
        menu.Detach();            // stop destructor destroying it
    
        DrawMenuBar();            // make sure UI is upto date
    
    }

    CreateMenu() used as a top level menu

    Looks good. That's what we need.

  • HMENU hMenu = ::CreatePopupMenu()

    This function differs from CreateMenu() in a subtle way. Where a HMENU created using CreateMenu would show options horizontally, these will be shown vertically. Popup menus created like this can also be inserted into other menus. So, when you need to add a new popup menu, this is the function to use.

    CreatePopupMenu() example:

    void CMenuView::OnRButtonDown(UINT nFlags, CPoint point) 
    {
        // display a menu created using CreateMenu()
    
        HMENU hMenu = ::CreatePopupMenu();
        if (NULL != hMenu)
        {
            // add a few test items
    
            ::AppendMenu(hMenu, MF_STRING, 1, "Item 1");
            ::AppendMenu(hMenu, MF_STRING, 2, "Item 2");
            ::AppendMenu(hMenu, MF_STRING, 3, "Item 3");
    
            ClientToScreen(&point);
    
            int sel = ::TrackPopupMenuEx(hMenu, 
                    TPM_CENTERALIGN | TPM_RETURNCMD,
                    point.x,
                    point.y,
                    m_hWnd,
                    NULL);
            ::DestroyMenu(hMenu);
        }
    }

    The above example code gives us a menu which looks like:

    CreatePopupMenu() popup result

    A much better result when used as a popup.

    When a popup menu is used in a dialog as a top level menu:

    // code extract
    
        m_hCreatePopupMenu = ::CreatePopupMenu();
        if (m_hCreatePopupMenu != NULL)
        {
            // see later for the implementation of this function
    
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup1\\item1", 1);
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup2\\item2", 2);
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup3\\item3", 3);
        }
    
    void CAboutDlg::OnCreatePopupMenu() 
    {
        // switch to the CreateMenu created menu
    
        CMenu    menu;
        menu.Attach(m_hCreatePopupMenu);
        SetMenu(&menu);
        menu.Detach();            // stop destructor destroying it
    
        DrawMenuBar();            // make sure the UI is upto date
    
    }

    CreatePopupMenu() used as a top level menu

    It looks terrible! The popups get shown vertically. So, a top level menu must be created through a call to CreateMenu(), while a popup menu should be created by a call to CreatePopupMenu().

    Why Microsoft made a distinction such as this between a top level menu and a popup menu is weird. A much cleaner interface would have been generated if only one function which produced a menu that could be used for either method would have been much nicer. But this is what we have. :(

  • AppendMenu(HMENU, UINT, UINT_PTR, LPCTSTR)

    Used to add new menu items or popups to an existing menu:

    Parameters:

    • HMENU - The handle to the menu you need to add to.
    • UINT - Flags about the menu item you want to add. See the Flags section later.
    • UINT_PTR - The returned ID when the option gets selected, or the HMENU of a new popup when appending a new popup menu.
    • LPCTSTR - The text displayed for the menu or popup item.

    The item added is placed at the bottom/end of the HMENU.

  • InsertMenu(HMENU, UINT, UINT, UINT_PTR, LPCTSTR)

    Adds a menu item or popup to a specific position in an existing menu.

  • LoadMenu(HINSTANCE, LPCTSTR)

    Loads a menu resource and returns the HMENU handle of it, or NULL if it fails.

  • LoadMenuIndirect(CONST MENUTEMPLATE*)
  • DrawMenuBar(HWND)

    You need to call this function if you change a menu that is in use by a mainframe or a dialog. Basically, it gets the window to draw the top level menu again so that any changes will be drawn correctly. Note that you pass the HWND of the window that owns the menu to be drawn, and not the HMENU.

    Menu modification

  • CheckMenuItem(HMENU, UINT, UINT)

    Modifies the given item to show/hide a check (tick) mark next to the item. Usually used to show that a given option is enabled or disabled.

  • EnableMenuItem(HMENU, UINT, UINT)

    In WIN32, if you want to set the enable/disabled state of a menu item, then this function will do it for you.

  • SetMenuDefaultItem(HMENU, UINT, UINT)

    Highlights the given menu item and makes it the default item that is selected.

  • ModifyMenu(HMENU, UINT, UINT, UINT_PTR, LPCTSTR)

    Allows you to change/modify a given menu item's text/status.

  • SetMenuContextHelpId(HMENU, DWORD)
  • CheckMenuRadioItem(HMENU, UINT, UINT, UINT, UINT)
  • SetMenuItemBitmaps(HMENU, UINT, UINT, HBITMAP, HBITMAP)
  • RemoveMenu(HMENU, UINT, UINT)

    This function is used to delete specific items from the given menu.

    Parameters

    • HMENU hMenu The handle of the menu in which the item to remove is present.
    • UINT uPosition The position or menu command ID identifier.
    • UINT "#flags">uFlags Whether uPosition is MF_BYCOMMAND (ID) or MF_BYPOSITION.

    Menu query functions

  • GetMenuItemCount(HMENU)

    This function returns the number of items in this HMENU object. Popup items are treated as individual items, and need to be enumerated separately.

        int itemCount = ::GetMenuItemCount(hMenu);
  • GetMenuItemID(HMENU, int)

    Used to get the ID (command) index of a menu item by position only.

  • GetMenuState(HMENU, UINT, UINT)
  • GetMenuString(HMENU, UINT, LPTSTR, int, UINT)

    Returns the menu text of a given menu item by either position or ID.

  • GetMenuItemInfo(HMENU, int, BOOL, LPMENUITEMINFO)

    You would use this function to query a specific HMENU item. This can return information such as the menu item's text, whether it's a popup menu, its command ID number etc.

    Parameters

    • HMENU - Handle of the menu we need to get information from.
    • int - The item ID or the position ID depending on...
    • BOOL - Whether we are getting menu item information MF_BYPOSITION (TRUE) or MF_BYCOMMAND (FALSE).
    • LPMENUITEMINFO - A pointer to a MENUITEMINFO object which has been initialized to tell what information about the menu item we want to retrieve.
  • GetSubMenu(HMENU, int)

    This function is used to extract (get) the HMENU of a popup menu from another menu by position. I have typically, in the past, had one big menu resource which contained all the popup/context menus that my application needed. Each one in the menu had its name and position defined using an enum, so I was able to extract the correct context menu in code like this:

        // MFC example
    
        CMenu    menu;
    
        if (menu.LoadMenu(IDR_MY_CONTEXT_MENUS))
        {
            CMenu *pContextMenu = menu.GetSubMenu(cm_RequiredContextMenu);
            pContextMenu->TrackPopupMenu(...);
        }

    Parameters

    • HMENU - Handle of the menu we need to extract the popup menu from.
    • int - The popup menu's position in the menu
  • GetMenuContextHelpId(HMENU)

    Menu destruction

  • DestroyMenu(HMENU)

    Causes the given menu resource to be destroyed by the system. Any popup menus which are part of the given HMENU are also destroyed.

  • DeleteMenu(HMENU, UINT, UINT)

    This works exactly the same as the RemoveMenu() function. Parameters are the same.

Menu messages

Not all the individual messages are covered - just those you are most likely to use.

The possible messages that can be received by your application when a menu is in use are:

WM_INITMENUPOPUP

This message is sent by the OS to the window that owns the menu about to be displayed. This gives the owner a chance to configure the appearance of the menu and its items. You can add/modify the menu at this point and set the state of each of the individual items. All this is done just before the menu is displayed.

In MFC, this message is handled by the CFrameWnd for the main window menu items. The procedure scans each menu item and issues an ON_UPDATE_COMMAND_UI call through the framework. This allows the standard MFC architecture to query your application on the state of each of the menu items.

WM_UNINITMENUPOPUP

This message is sent when a popup menu has been hidden after use, either when a selection has been made, or if a popup menu that no longer needs to be shown gets hidden.

WM_MENUCHAR

The WM_MENUCHAR message is sent when a menu is active and the user presses a key that does not correspond to any mnemonic or accelerator key. This message is sent to the window that owns the menu.

WM_MENUCOMMAND

The WM_MENUCOMMAND message is sent when the user makes a selection from a menu.

WM_MENUDRAG

The WM_MENUDRAG message is sent to the owner of a drag-and-drop menu when the user drags a menu item.

WM_MENUGETOBJECT

The WM_MENUGETOBJECT message is sent to the owner of a drag-and-drop menu when the mouse cursor enters a menu item, or moves from the center of the item to the top or bottom of the item.

WM_MENURBUTTONUP

The WM_MENURBUTTONUP message is sent when the user releases the right mouse button while the cursor is on a menu item.

WM_MENUSELECT

Every time the user changes the highlighted (selected) item in a menu being displayed, it sends the owning window this message. The WPARAM field contains the ID of the highlighted menu item. Typically, an application would process this message to display a more detailed description of what the command does in the status bar or equivalent location.

WM_SYSCOMMAND

This message is sent to the window that owns the menu when a System menu option has been selected. See the The Window/System menu section later for more information.

WM_COMMAND

When the user select a menu item from your menu, the command ID selected is sent to your window in the WPARAM field. This allows you to take the correct action for the command.

TrackPopupMenu() and TrackPopupMenuEx()

These two functions are very useful for displaying a popup menu to allow the user to make a quick selection. Usually used for a context menu, its behavior can be odd when you first encounter it. Depending on which window's HWND gets passed through as the owner of the menu, depends on whether in MFC the relevant menu option's ON_UPDATE_COMMAND_UI handlers get called, or also if using the TPM_NONOTIFY option, whether they or the ON_COMMAND handlers get called at all.

TPM_RETURNCMD is a good flag to use with these functions, as it allows you to group all the actual code which acts for the commands in the menu in one location, instead of it going out (in MFC anyway) through the ON_COMMAND handlers for each option. As, the TrackPopupMenu() function itself returns the command ID of the selected option or 0 if the menu was aborted without an option being selected.

    // some typical menu code using the TPM_RETURNCMD option:

    CMenu menu;
    CMenu *pSub = NULL;
    
    // popup a menu to get the number of pages to display

    VERIFY(menu.LoadMenu(IDR_PREVIEW_PAGES));
    pSub = menu.GetSubMenu(0);
    
    // NOTE : If you need to enable or disable the menu items

    //  in this list based on the number of pages in your printout,

    // you can either do it here before the menu is displayed, or write a

    // handler for the WM_INITMENUPOPUP message and configure the

    // enabled/disabled state at that point.

    int command = pSub->TrackPopupMenu(
            TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD, 
            point.x, 
            point.y, 
            this);
    
    switch (command)
    {
    case ID_PAGES_1PAGE :
        m_Across = 1;
        m_Down = 1;
        m_nZoomOutPages = 1;
        break;
    case ID_PAGES_2PAGES :
        m_Across = 2;
        m_Down = 1;
        m_nZoomOutPages = 2;
        break;
    case ID_PAGES_3PAGES :
        m_Across = 3;
        m_Down = 1;
        m_nZoomOutPages = 3;
        break;
    case ID_PAGES_4PAGES :
        m_Across = 2;
        m_Down = 2;
        m_nZoomOutPages = 4;
        break;
    case ID_PAGES_6PAGES :
        m_Across = 3;
        m_Down = 2;
        m_nZoomOutPages = 6;
        break;
    case ID_PAGES_9PAGES :
        m_Across = 3;
        m_Down = 3;
        m_nZoomOutPages = 9;
        break;
    default :
        return;
    }

Menu function flags

Most, if not all menu modification functions, make use of flags which can be passed through as parameters. I list here all those I could find in the documentation, and their functions:

  • MF_BYCOMMAND - Specifies that an ID is a command ID and not a position index into a menu.
  • MF_BYPOSITION - Specifies that an ID is a position index into the menu and not a command ID.
  • MF_BITMAP - Uses a bitmap as the menu item.
  • MF_DISABLED - Disables the menu item so that it cannot be selected, but the flag does not gray it.
  • MF_ENABLED - Enables the menu item so that it can be selected, and restores it from its grayed state.
  • MF_GRAYED - Disables the menu item and grays it so that it cannot be selected.
  • MF_MENUBARBREAK - Functions the same as the MF_MENUBREAK flag for a menu bar. For a drop-down menu, submenu, or shortcut menu, the new column is separated from the old column by a vertical line.
  • MF_MENUBREAK - Places the item on a new line (for a menu bar) or in a new column (for a drop-down menu, submenu, or shortcut menu) without separating columns.
  • MF_OWNERDRAW - Specifies that the item is an owner-drawn item. Before the menu is displayed for the first time, the window that owns the menu receives a WM_MEASUREITEM message (for each owner-drawn item) to retrieve the width and height of the menu item. The WM_DRAWITEM message is then sent to the window procedure of the owner window whenever the appearance of the menu item must be updated.
  • MF_POPUP - Specifies that the menu item opens a drop-down menu or submenu. This flag is used to add a menu name to a menu bar, or a menu item that opens a submenu to a drop-down menu, submenu, or shortcut menu.
  • MF_SEPARATOR - Draws a horizontal dividing line. This flag is used only in a drop-down menu, submenu, or shortcut menu. The line cannot be grayed, disabled, or highlighted.
  • MF_STRING - Specifies that the menu item is a text string.
  • MF_UNCHECKED - Does not place a check mark next to the item (default). If the application supplies check-mark bitmaps (see SetMenuItemBitmaps), this flag displays the clear bitmap next to the menu item.
  • MF_CHECKED - Places a check mark next to the menu item. If the application provides check-mark bitmaps (see SetMenuItemBitmaps), this flag displays the check-mark bitmap next to the menu item.
  • MF_USECHECKBITMAPS -
  • MF_DEFAULT - Probably makes the item the default selection. See SetMenuDefaultItem().
  • MF_MOUSESELECT - Item was selected with a mouse.
  • MF_SYSMENU - Item is contained in the Control menu.
  • MF_HELP - Probably right justifies an item in a top level menu (e.g., the Help menu in old 3.1 apps etc. was right justified).
  • MF_RIGHTJUSTIFY - Does the same as MF_HELP, as it has the same value as defined in the Windows header files.
  • MF_END - Used when loading a menu from a resource, but now obsolete.

Some of these flags will be used when messages are sent to you (e.g., MF_MOUSESELECT), while others are used when setting properties of menu items.

The following groups of flags cannot be used together:

  • MF_BITMAP, MF_STRING, and MF_OWNERDRAW.
  • MF_CHECKED and MF_UNCHECKED.
  • MF_DISABLED, MF_ENABLED, and MF_GRAYED.
  • MF_MENUBARBREAK and MF_MENUBREAK.

The Window/System menu

This menu has gone through several identity crises over the years. Currently known as the Window menu, in the past, it has also been known in documentation as the System or Control menu, and may be referred to as such. I will call it the System menu here.

The System menu

Any dialog or main application window can/usually contains a System menu. This is a menu that can be accessed through that window's icon, shown in the top left corner of the window. Clicking on the icon shows this System menu as a popup. Menu options selected through this menu are sent as WM_SYSCOMMAND messages.

Accessing your System menu in WIN32

You can get a HMENU for your application's or dialog's System menu by using the WIN32 function ::GetSystemMenu(HWND hWnd, BOOL bRestore). This function can do 2 things depending on the value of the bRestore parameter:

  • FALSE - The function returns the actual HMENU of the System menu in use by your application.
  • TRUE - The function resets your window's System menu to the default state (i.e., removes any modifications you may have done to it) and returns NULL. You cannot get a HMENU when passing TRUE to this function.

Now, let's move onto the WM_SYSCOMMAND message, which you will be sent for any selections the user makes from your System menu. The prototype for this message in WIN32 is:

  • WIN32: LRESULT CALLBACK WindowProc(HWND, UINT = WM_SYSCOMMAND, WPARAM, LPARAM)

    The MSDN has this to say about this message in WIN32:

    In WM_SYSCOMMAND messages, the four low-order bits of the WPARAM parameter are used internally by the system. To obtain the correct result when testing the value of WPARAM, an application must combine the value 0xFFF0 with the WPARAM value by using the bitwise AND operator.

        // example clearing low order bits
    
        wParam &= 0xfff0;    // clear system used bits for correct comparision

    I created a test application to check this. Using VC6, I created a project of type WIN32 Application, sub option type A typical "Hello world" application". This gives us the boiler plate code for a standard WIN32 application. From here, I inserted a handler for the WM_SYSCOMMAND message, and tested for each of the standard System menu options. I also appended a new menu option with an ID of 3 which could have problems if we use the 0xFFF0 mask mentioned above.

    // This is the extra code added to the basic template. You can also find
    
    // this in the test application
    
    
    // a function used to handle the WM_SYSCOMMAND message (forward declaration)
    
    void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    
    // added at end of InitInstance
    
    HMENU hSysMenu = ::GetSystemMenu(hWnd, FALSE);
    if (::AppendMenu(hSysMenu, 
            MF_BYCOMMAND | MF_STRING, 
            3, 
            "Test sysmenu popup item") == 0)
    {
        ::MessageBox(hWnd, 
                "Failed to add menu item", 
                "Problem!", 
                MB_OK);
    }
    
    // The WndProc switch entry inserted just before the default: option
    
            case WM_SYSCOMMAND:
                HandleSysCommand(hWnd, message, wParam, lParam);
                // fall through to the default handler as well
    
    
    // the function to handle WM_SYSCOMMAND
    
    void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        wParam &= 0xfff0;    // clear system used bits
    
        switch (wParam)
        {
        case SC_RESTORE:
        case SC_MOVE:
        case SC_SIZE:
        case SC_MINIMIZE:
        case SC_MAXIMIZE:
        case SC_CLOSE:
        case SC_MOUSEMENU:
        case SC_NEXTWINDOW:
        case SC_PREVWINDOW:
        case SC_VSCROLL:
        case SC_HSCROLL:
        case SC_KEYMENU:
        case SC_ARRANGE:
        case SC_TASKLIST:
        case SC_SCREENSAVE:
        case SC_HOTKEY:
    #if(WINVER >= 0x0400)
        case SC_DEFAULT:
        case SC_MONITORPOWER:
        case SC_CONTEXTHELP:
        case SC_SEPARATOR:
    #endif
            // beep to show we recognised the message
    
            ::MessageBeep(-1);
            break;
        case 3:
            // handle our inserted System menu command
    
                ::MessageBox(hWnd, 
                        "My System menu command", 
                        "WM_SYSCOMMAND", 
                        MB_OK);
                break;
        default:
            ::MessageBox(hWnd, 
                    "Unrecognised System command message", 
                    "WM_SYSCOMMAND", 
                    MB_OK);
            break;
        }
    }

    Notice the case 3: option which should handle our inserted menu command. When the application is run, and the menu option selected, the actual entry run is the default: entry, as the wParam mask applied loses our menu command ID! This means that you need to keep a copy of the ID and check for the standard SC_... messages using the mask. If it's not one of those, you need to switch on the un-masked wParam value.

    Our re-worked procedure would look like:

    void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        bool bRecognised = false;
        switch (wParam & 0xfff0)
        {
        case SC_RESTORE:
        case SC_MOVE:
        case SC_SIZE:
        case SC_MINIMIZE:
        case SC_MAXIMIZE:
        case SC_CLOSE:
        case SC_MOUSEMENU:
        case SC_NEXTWINDOW:
        case SC_PREVWINDOW:
        case SC_VSCROLL:
        case SC_HSCROLL:
        case SC_KEYMENU:
        case SC_ARRANGE:
        case SC_TASKLIST:
        case SC_SCREENSAVE:
        case SC_HOTKEY:
    #if(WINVER >= 0x0400)
        case SC_DEFAULT:
        case SC_MONITORPOWER:
        case SC_CONTEXTHELP:
        case SC_SEPARATOR:
    #endif
            bRecognised = true;
            break;
        }
    
        if (!bRecognised)
        {
            // switch of the unmasked wParam value
    
            switch (wParam)
            {
            case 3:
                ::MessageBox(hWnd, 
                        "My SYS menu command", 
                        "WM_SYSCOMMAND", 
                        MB_OK);
                break;
            default:
                ::MessageBox(hWnd, 
                        "Unrecognised Sys command message", 
                        "WM_SYSCOMMAND", 
                        MB_OK);
                break;
            }
        }
    }

    This one does indeed work as advertised. So, it seems you do have to use the 0xFFF0 mask, but only for SC_... messages, not for your own added menu options.

    System menu oddities

    The System menu contains these standard menu commands:

    • SC_RESTORE - Restores the window to normal (non maximized) position and size.
    • SC_MOVE - Allows the user to move the window.
    • SC_SIZE - Allows the user to resize the window.
    • SC_MINIMIZE - Minimizes the window.
    • SC_MAXIMIZE - Maximizes the window.
    • SC_CLOSE - The close option was selected.

    But if you look at the functions above, we have many other SC_... commands listed. Basically, the Windows OS is taking short cuts for various actions the user can perform on the application window, such as changing the size of the window using the mouse, or clicking on the application's title bar. All these actions generate WM_SYSCOMMAND messages with the relevant SC_... code in the WPARAM parameters of the message, so WM_SYSCOMMAND messages can be received without first displaying the System menu.

    The other SC_... messages are:

    • SC_MOUSEMENU - Received when a menu containing a System menu is about to be displayed.
    • SC_NEXTWINDOW - Moves to the next window.
    • SC_PREVWINDOW - Moves to the previous window.
    • SC_VSCROLL - Scrolls vertically.
    • SC_HSCROLL - Scrolls horizontally.
    • SC_KEYMENU
    • SC_ARRANGE
    • SC_TASKLIST
    • SC_SCREENSAVE
    • SC_HOTKEY
    • SC_DEFAULT
    • SC_MONITORPOWER
    • SC_CONTEXTHELP
    • SC_SEPARATOR
  • MFC: ON_WM_SYSCOMMAND() -> afx_msg void OnSysCommand(UINT, LONG)

    The WPARAM in WIN32 or the UINT in MFC can contain these command codes.

    Normally, you can allow the default processing of System menu options to handle these occurrences for you.

More advanced menu functions

In this section, I hope to present a new set of functions to aid in the manipulation of menu objects. I will show how I went about developing these functions and provide the final tested functions.

Adding dynamic menu items to an existing menu

When you want to add items to an existing menu resource, you need to be able to handle an unknown popup menu structure. We will need to iterate this structure in a methodical way, and insert new menu items at the appropriate level if it exists or add this new level as required. After some initial consideration, I decided to use a recursive approach for this problem. My functions are aimed at MFC, but could be easily adapted to work for straight WIN32.

Let's look at a few examples of new items our procedure could meet:

    // in these examples, I am using the '\' character to delimit different

    // popup/menu items internally in a string

    
    // example new menu items

    "My menu item"
    "Popup name\menu item name"
    "Popup name\next popup name\Additional popup names....\menu item name"

So, a menu item can have 0 or more popup levels and the item name. This means that we can search the existing menu structure for each popup name. If it exists, we then recurse on this new popup menu. We continue to do this until we get to a menu item name, which is inserted. I have also defined a delimiting character '\' which is used to split the popup menu names and the final item name. An effect of this is that you cannot have a '\' character in any menu item/popup item text. If you need to have this character, you need to change it in the final procedure presented.

So, our function's pseudo code would look like:

Function AddMenuItem(menu, ItemName, ID)
{
if popup/item delimiter in ItemName
    get popup name
    remove popup name from item name
    look for existing popup
    if found
        recurse AddMenuItem(foundPopup, shorterItemName, id)
    else
        create new popup
        recurse AddMenuItem(createdPopup, shorterItemName, id)
        append new popup
else
    append new item
}

Developing the code

Let's first break the coding down in 3 stages:

  1. The final level insert of a menu item
  2. Finding a popup menu (if it exists)
  3. Adding to/creating the popup menu

Let's see how we handle each of these stages and present the code to do this.

  • Stage 1: Handling a direct insert operation

    When we have an item that has no popup menu levels to be found/created, we only need to insert the item directly. Let's see what this code would look like:

    bool MenuFunctions::AddMenuItem(HMENU hTargetMenu, 
                          const CString& itemText, UINT itemID)
    {
        bool bSuccess = false;
    
        // debug checks for correct calling of function
    
        ASSERT(itemText.GetLength() > 0);
        ASSERT(itemID != 0);        // menu items must have non-0 IDs
    
        ASSERT(hTargetMenu != NULL);
    
        // first, does the menu item have any
    
        // required submenus to be found/created?
    
        if (itemText.Find('\\') >= 0)
        {
            // code yet to be written to handle popups
    
        }
        else
        {
            // no sub menus required,
    
            // add this item to this HMENU
    
            if (::AppendMenu(
                    hTargetMenu, 
                    MF_STRING, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the item to the menu
    
                bSuccess = true;
            }
        }
        return bSuccess;
    }

    The first thing we do is check the input parameters to the function with the use of ASSERTions. This will allow us to pick up any invalid calls to the function in debug mode, and also gives you information on what state the parameters should have on entering the function.

    The next thing we do is check the menu item string for the submenu delimiter '\'. If it is not found then we know we are at the item insert level, and do not have to navigate any submenu levels. We go straight to the AppendMenu function call and add the menu item to the end of the hTargetMenu passed in.

  • Stage 2: Finding a popup menu (if it exists)

    If we have any popup menu levels that need to be applied, then we need to search the HMENU passed in to see whether the popup menu we need already exists. Let's see the code we need:

            // 1:get the popup menu name
    
            CString popupMenuName = itemText.Left(itemText.Find('\\'));
    
            // 2:get the rest of the menu item name
    
            // minus the delimiting '\' character
    
            CString remainingText = 
               itemText.Right(itemText.GetLength() - 
               popupMenuName.GetLength() - 1);
    
            // 3:See whether the popup menu already exists
    
            int itemCount = ::GetMenuItemCount(hTargetMenu);
            bool bFoundSubMenu = false;
            MENUITEMINFO menuItemInfo;
    
            memset(&menuItemInfo, 0, sizeof(MENUITEMINFO));
            menuItemInfo.cbSize = sizeof(MENUITEMINFO);
            menuItemInfo.fMask = 
               MIIM_TYPE | MIIM_STATE | 
               MIIM_ID | MIIM_SUBMENU;
            for (int itemIndex = 0 ; 
               itemIndex < itemCount 
               && !bFoundSubMenu ; itemIndex++)
            {
                ::GetMenuItemInfo(
                        hTargetMenu, 
                        itemIndex, 
                        TRUE, 
                        &menuItemInfo);
                if (menuItemInfo.hSubMenu != 0)
                {
                    // this menu item is a popup
    
                    // menu (non popups give 0)
    
                    // get the popup menu items name
    
                    TCHAR    buffer[MAX_PATH];
                    ::GetMenuString(
                            hTargetMenu, 
                            itemIndex, 
                            buffer, 
                            MAX_PATH, 
                            MF_BYPOSITION);
                    if (popupMenuName == buffer)
                    {
                        // this is the popup menu
    
                        // we have to add to
    
                        bFoundSubMenu = true;
                    }
                }
            }

    We first extract from the front of the menu item text, the name of the popup menu up to the '\' character. We then also get the remaining menu text, without the popup menu name and the delimiter character, as this information will be needed for a recursive call. Having got the popup menu name, we need to search the menu we are in to see whether a popup menu with that name already exists.

    First, we get the number of items in the menu using ::GetMenuItemCount(HMENU). We make use of the ::GetMenuItemInfo() function in a loop to get information on each item in the menu one by one. Note the third parameter 'TRUE' which means we are getting the menu information by MF_BYPOSITION and not by MF_BYCOMMAND. The flags set in the MENUITEMINFO structure will return information in the hSubMenu field. This is the field of interest to us as, if this returns as non-zero, then the item is a popup menu which we need to check the name of to see if it's the one we want. If it is a popup menu (hSubMenu != 0), then we use ::GetMenuString() to extract the textual name of this popup menu item, and compare it to the popup name we are looking for. If it's the right one, we set a flag (bFoundSubMenu) and break out of the loop so we can make use of the menuItemInfo.hSubMenu parameter in our recursive function call.

    If we get to the end of the itemIndex loop without bFoundSubMenu becoming true, then a popup menu with the required name does not exist, and we will have to create it ourselves and append it to the end of our menu.

  • Stage 3: Adding to/creating the popup menu

    The final stage in our procedure requires us to do a recursive call on the menuItemInfo.hSubMenu or create a new popup menu to recurse on. Let's look at the code we need:

            // 4: If exists, do recursive call,
    
            // else create and do a recursive call and then insert it
    
            if (bFoundSubMenu)
            {
                bSuccess = AddMenuItem(
                        menuItemInfo.hSubMenu, 
                        remainingText, 
                        itemID);
            }
            else
            {
                // we need to create a new sub menu and insert it
    
                HMENU hPopupMenu = ::CreatePopupMenu();
                if (hPopupMenu != NULL)
                {
                    bSuccess = AddMenuItem(
                            hPopupMenu, 
                            remainingText, 
                            itemID);
                    if (bSuccess)
                    {
                        if (::AppendMenu(
                                hTargetMenu, 
                                MF_POPUP, 
                                (UINT)hPopupMenu, 
                                popupMenuName) > 0)
                        {
                            bSuccess = true;
                            // hPopupMenu now owned by hTargetMenu,
    
                            // we do not need to destroy it
    
                        }
                        else
                        {
                            // failed to insert the popup menu
    
                            bSuccess = false;
                            // stop a resource leak
    
                            ::DestroyMenu(hPopupMenu);
                        }
                    }
                }
            }

    If the popup menu exists, we try and add the remainingText to that popup menu. The recursive call will handle any additional popup menu level navigation we require. If the popup menu does not exist, we create one using the CreatePopupMenu() function, and do a recursive call on that popup with the remainingText. This will also handle the cases of additional popup menus being created. After any further recursion has completed successfully, we append the popup menu to the menu passed in. Note the MF_POPUP style, which makes AppendMenu treat parameter 3 (UINT itemID) as a HMENU instead of an item ID. We also give the popup menu the name it will be shown under. If we fail to append the popup menu, we destroy it to avoid a resource leak.

The final function - almost

What we have here works well except for one thing. We have not handled the inserting of separators. We need to add a special case to the item insert level. As a menu item cannot have an ID of 0 (a return value of 0 means the menu was aborted for TrackPopupMenu), we can use this ID number in the final insert operation to distinguish between a proper menu item and a separator. So, the final function looks like:

// this is a recursive function which will attempt

// to add the item "itemText" to the menu with the

// given ID number. The "itemText" will be parsed for

// delimiting "\" characters for levels between

// popup menus. If a popup menu does not exist, it will

// be created and inserted at the end of the menu

// itemID of 0 will cause a separator to be added

bool MenuFunctions::AddMenuItem(
        HMENU hTargetMenu, 
        const CString& itemText, 
        UINT itemID)
{
    bool bSuccess = false;

    ASSERT(itemText.GetLength() > 0);
    ASSERT(hTargetMenu != NULL);

    // first, does the menu item have

    // any required submenus to be found/created?

    if (itemText.Find('\\') >= 0)
    {
        // yes, we need to do a recursive call

        // on a submenu handle and with that sub

        // menu name removed from itemText


        // 1:get the popup menu name

        CString popupMenuName = itemText.Left(itemText.Find('\\'));

        // 2:get the rest of the menu item name

        // minus the delimiting '\' character

        CString remainingText = 
            itemText.Right(itemText.GetLength() 
                   - popupMenuName.GetLength() - 1);

        // 3:See whether the popup menu already exists

        int itemCount = ::GetMenuItemCount(hTargetMenu);
        bool bFoundSubMenu = false;
        MENUITEMINFO menuItemInfo;

        memset(&menuItemInfo, 0, sizeof(MENUITEMINFO));
        menuItemInfo.cbSize = sizeof(MENUITEMINFO);
        menuItemInfo.fMask = 
          MIIM_TYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU;
        for (int itemIndex = 0 ; 
           itemIndex < itemCount && !bFoundSubMenu ; itemIndex++)
        {
            ::GetMenuItemInfo(
                    hTargetMenu, 
                    itemIndex, 
                    TRUE, 
                    &menuItemInfo);
            if (menuItemInfo.hSubMenu != 0)
            {
                // this menu item is a popup menu (non popups give 0)

                TCHAR    buffer[MAX_PATH];
                ::GetMenuString(
                        hTargetMenu, 
                        itemIndex, 
                        buffer, 
                        MAX_PATH, 
                        MF_BYPOSITION);
                if (popupMenuName == buffer)
                {
                    // this is the popup menu we have to add to

                    bFoundSubMenu = true;
                }
            }
        }
        // 4: If exists, do recursive call,

        // else create do recursive call

        // and then insert it

        if (bFoundSubMenu)
        {
            bSuccess = AddMenuItem(
                    menuItemInfo.hSubMenu, 
                    remainingText, 
                    itemID);
        }
        else
        {
            // we need to create a new sub menu and insert it

            HMENU hPopupMenu = ::CreatePopupMenu();
            if (hPopupMenu != NULL)
            {
                bSuccess = AddMenuItem(
                        hPopupMenu, 
                        remainingText, 
                        itemID);
                if (bSuccess)
                {
                    if (::AppendMenu(
                            hTargetMenu, 
                            MF_POPUP, 
                            (UINT)hPopupMenu, 
                            popupMenuName) > 0)
                    {
                        bSuccess = true;
                        // hPopupMenu now owned by hTargetMenu,

                        // we do not need to destroy it

                    }
                    else
                    {
                        // failed to insert the popup menu

                        bSuccess = false;
                        // stop a resource leak

                        ::DestroyMenu(hPopupMenu);
                    }
                }
            }
        }        
    }
    else
    {
        // no sub menus required, add this item to this HMENU

        // item ID of 0 means we are adding a separator

        if (itemID != 0)
        {
            // its a normal menu command

            if (::AppendMenu(
                    hTargetMenu, 
                    MF_BYCOMMAND, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the item to the menu

                bSuccess = true;
            }
        }
        else
        {
            // we are inserting a separator

            if (::AppendMenu(
                    hTargetMenu, 
                    MF_SEPARATOR, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the separator to the menu

                bSuccess = true;
            }
        }
    }

    return bSuccess;
}

An example test of the function on a standard MFC MDI Doc/View application menu was performed. The following code was added to the InitInstance() function just after the CDocTemplate object has been registered:

    MenuFunctions::AddMenuItem(pDocTemplate->m_hMenuShared, 
                    "&File\\Test popup\\My new item", 100);
    MenuFunctions::AddMenuItem(pDocTemplate->m_hMenuShared, 
                    "&File\\Test popup\\Separator", 0);
    MenuFunctions::AddMenuItem(pDocTemplate->m_hMenuShared, 
                    "&File\\Test popup\\My new item 2", 101);

The menu after these functions were called looked like:

Dynmaic menu item test output

The dynamic menu item insert worked, except that our menu items are disabled! They are disabled due to the way the MFC works internally. When a menu is first shown by MFC, it checks to see whether a ON_UPDATE_COMMAND_UI handler exists for that item; if it does, it calls it to get the correct state of the menu item (disabled or enabled etc.), if no ON_UPDATE_COMMAND_UI handler exists, MFC automatically disables the item if no ON_COMMAND handler exists for the menu item. So from this point, we need to write handlers for these menu items to get them to be enabled. So, what we have has worked correctly!

Exercise for the reader

To see if you have been paying attention, I have an exercise for those of you who are interested. Try and extend the function above so that when adding a new menu item, you can specify the insertion position. My example above added the popup menu Test popup to the end of the menu. This may not be what you want. Modify the code so that you can insert the popup at the position you want. A solution to this is present in the download source.

Estimating the size of a menu

This section was inspired by Michael Dunn who answered a question in the C++ forum

When you have a popup menu, you may need to know the size of it before you display it. This is so that you can position it correctly on the screen. There are no standard functions available to calculate the size of a menu you want to display. So we have to develop one for ourselves:

Depending on what we want to know, we can either try and calculate the height (relatively easy) or the width (difficult). First, let's look at the height of a popup menu:

int CalculateMenuHeight(HMENU hMenu)
{
    int height;

    height = ::GetMenuItemCount(hMenu) * ::GetSystemMetrics(SM_CYMENUSIZE);
    return height;
}

Estimating the width of a menu is more difficult. We cannot simply take the number of items and multiply by the height, we have to consider the text of each of the menu items, whether an icon is present, and also whether an item is a popup (as it has a > at the side of it). So, after thinking about it for a while, I went and implemented this function:

// constants used in the calculation of the menu width

const int f_iconWidth = 30;
const int f_menuBorder = 12;
const int f_popupArrowSize = 16;
const int f_acceleratorGap = 8;

int CalculateMenuWidth(const HMENU hMenu)
{
    // create a copy of the font that should be used to render a menu

    // so we can measure text correctly

    NONCLIENTMETRICS nm;
    LOGFONT lf;
    CFont menuFont;
    TCHAR menuItemText[_MAX_PATH];

    nm.cbSize = sizeof(NONCLIENTMETRICS);
    VERIFY(SystemParametersInfo(SPI_GETNONCLIENTMETRICS, nm.cbSize,&nm, 0));
    lf = nm.lfMenuFont;
    menuFont.CreateFontIndirect(&lf);

    CDC dc;

    dc.Attach(::GetDC(NULL));       // get screen DC

    dc.SaveDC();
    dc.SelectObject(&menuFont);

    // look at each item and work out its width

    int maxWidthBeforeTab = 0;
    int maxWidthAfterTab = 0;
    int itemCount = ::GetMenuItemCount(hMenu);

    for (int item = 0 ; item < itemCount ; item++)
    {
        // get each items data

        int itemWidth = f_iconWidth + f_menuBorder;
        MENUITEMINFO    itemInfo;

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

        itemInfo.fMask = MIIM_SUBMENU | MIIM_ID;
        ::GetMenuItemInfo(hMenu, item, TRUE, &itemInfo);

        if (itemInfo.hSubMenu != 0)
        {
            // its a popup menu, include the width of the > arrow

            itemWidth += f_popupArrowSize;
        }
        if (itemInfo.wID == 0)
        {
            // its a separator, dont measure the text

        }
        else
        {
            GetMenuString(hMenu, item, menuItemText, _MAX_PATH, MF_BYPOSITION);

            // measure the text using the font

            CSize textSize;
            CString itemText = menuItemText;

            // remove any accelerator key mnemonics

            itemText.Replace("&", "");

            if (itemText.Find("\t") >= 0)
            {
                // we have a tab, measure both parts

                CString afterText = itemText.Right(itemText.GetLength()
                     - itemText.Find("\t") - 1);
                textSize = dc.GetTextExtent(afterText);

                if (textSize.cx > maxWidthAfterTab)
                {
                    maxWidthAfterTab = textSize.cx;
                }
                itemText = itemText.Left(itemText.Find("\t"));
                itemWidth += f_acceleratorGap;
            }

            textSize = dc.GetTextExtent(itemText);
            itemWidth += textSize.cx;
        }
        if (itemWidth > maxWidthBeforeTab)
        {
            maxWidthBeforeTab = itemWidth;
        }
    }

    dc.RestoreDC(-1);
    ::ReleaseDC(NULL, dc.Detach());
    return maxWidthBeforeTab + maxWidthAfterTab;
}

Of course, if you're dealing with the height/width of a menu using the TrackPopupMenu/Ex functions, then you can make use of the TPM_LEFTALIGN, TPM_RIGHTALIGN, TPM_TOPALIGN and TPM_BOTTOMALIGN flags to correctly position the menu for you where you need it.

MFC and menus

Every MFC application has a default menu resource normally called IDR_MAINFRAME. This is the menu that is used by the application when no document is open in the application. If you have a document open, the CMainFrame will have selected into it the menu of the CDocTemplate object that the active document is a type of.

CDocTemplate and menus

The menu for the document type is stored in the HMENU m_hMenuShared member variable. This menu is selected into the CMainFrame when the WM_MDIACTIVATE message is processed, or a new MDI window has just been created and activated.

One thing that is odd is that the menu will have a System menu popup inserted at the start of it, if a document of this type is open and active in your application. I was affected by this problem once when I had a toolbar button with a drop arrow. When it was clicked, I needed to show a popup menu which was part of the File menu. My code went to the CMultiDocTemplate::m_hMenuShared and tried to extract the correct popup menu I needed to display. This worked fine when no document of that type was open and active. When one was, I kept getting returned a CTempMenu object for the File menu popup, which was a temporary MFC wrapper for the System menu which had been added. I needed some extra checks in my code to determine whether I was accessing an active document's shared menu resource.

StatusBar menu item prompts in MFC

As we know, when a menu item is highlighted by the user with the mouse or keyboard, the OS sends the window that owns the menu a WM_MENUSELECT message. The CFrameWnd class which owns the top level menu in MFC processes this and then sends itself a WM_SETMESSAGESTRING message. This message is used by MFC in two ways depending on the values of the WPARAM and LPARAM:

  • wParam = 0, lParam = LPCTSTR

    In this case, when wParam is 0, the LPARAM contains a pointer to a NULL terminated string which will be displayed in the status bar message pane.

  • wParam = menuItemID, lParam = NULL

    In this case, wParam contains the menu item command ID. MFC calls the CFrameWnd::GetMenuString() function which extracts the string table resource entry with the same ID. It breaks this string at the \t (tab) character (if present in the string) and displays the result in the message pane of the statusbar.

An example of defining the status bar menu string for a menu item:

Editing a menu item in VC6

Well, that's about it for now. I will add new sections as I come up with new ideas or get suggestions.

Update history

  • First release - 23rd June 2004.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here