Programming Windows with MFC, Second Edition

[Previous] [Next]

The first half of this chapter covered probably 80 percent of everything you'll ever need to know about menus. Occasionally, however, you'll need to go beyond the basics and do something extra. The following "something extras" are discussed in the second half of the chapter:

We'll close out this chapter by revising the Shapes application to include both an owner-draw Color menu and a right-click context menu.

Creating Menus Programmatically

Loading a menu resource from your application's EXE file isn't the only way to create a menu. You can also do it programmatically using MFC's CMenu class and its member functions. We've yet to explore CMenu in any depth because basic menu support doesn't require a CMenu. CMenu comes in handy when you want to create a menu on the fly, perhaps from information that isn't available until run time, or when you want to modify an existing menu (a subject we'll cover in the next section). In situations such as these, CMenu will be your best friend.

You create menus programmatically using a combination of CMenu::CreateMenu, CMenu::CreatePopupMenu, and CMenu::AppendMenu. You build a top-level menu and its submenus by creating a menu with CreateMenu, creating the submenus with CreatePopupMenu, and attaching the submenus to the top-level menu with AppendMenu. The following program listing creates a menu identical to the one featured in the Shapes application and attaches it to the frame window. The only difference is that the application, not the resource editor, creates this menu:

CMenu menuMain; menuMain.CreateMenu (); CMenu menuPopup; menuPopup.CreatePopupMenu (); menuPopup.AppendMenu (MF_STRING, ID_FILE_EXIT, "E&xit"); menuMain.AppendMenu (MF_POPUP, (UINT) menuPopup.Detach (), "&File"); menuPopup.CreatePopupMenu (); menuPopup.AppendMenu (MF_STRING, ID_SHAPE_CIRCLE, "&Circle\tF7"); menuPopup.AppendMenu (MF_STRING, ID_SHAPE_TRIANGLE, "&Triangle\tF8"); menuPopup.AppendMenu (MF_STRING, ID_SHAPE_SQUARE, "&Square\tF9"); menuMain.AppendMenu (MF_POPUP, (UINT) menuPopup.Detach (), "&Shape"); SetMenu (&menuMain); menuMain.Detach ();

The first two statements create a CMenu object named menuMain that represents an empty top-level menu. The next block of statements creates the File menu and attaches it to the top-level menu. The MF_POPUP parameter passed to AppendMenu tells Windows that the second parameter is a menu handle, not a menu item ID, and Detach both detaches the menu from the menuPopup object and retrieves the menu handle. The third statement block creates the Shape menu and attaches it to the top-level menu. Finally, the call to SetMenu attaches the newly formed menu to the frame window, and Detach disassociates the top-level menu and menuMain so the top-level menu won't be destroyed as soon as the function ends. If the window is visible when SetMenu is called, DrawMenuBar should also be called to paint the menu on the screen.

Modifying Menus Programmatically

In addition to creating menus dynamically, you can modify existing menus. The following table lists the CMenu member functions used to add, modify, and delete menu items.

Function Description
AppendMenu Adds an item to the end of a menu
InsertMenu Inserts an item into a menu at a specified location
ModifyMenu Changes the command ID, text, or other characteristics of a menu item
DeleteMenu Deletes a menu item and the submenu associated with it, if any
RemoveMenu Deletes a menu item

The difference between RemoveMenu and DeleteMenu is that if the item being removed has a submenu, DeleteMenu removes the item and destroys the submenu, too. RemoveMenu removes the item but leaves the submenu extant in memory. DeleteMenu is the one you'll usually want to use, but RemoveMenu is useful if you want to preserve the submenu for later use.

Before you can modify a menu by adding, changing, or deleting menu items, you need a CMenu pointer referencing the menu. MFC's CWnd::GetMenu function returns a CMenu pointer for a window's top-level menu or NULL if the window doesn't have a top-level menu. Let's say you want to delete the Shapes application's Shape menu at run time. Here's the code to do it:

CMenu* pMenu = GetMenu (); pMenu->DeleteMenu (1, MF_BYPOSITION);

The 1 passed to DeleteMenu is the Shape menu's 0-based index. The File menu occupies position 0, the Shape menu position 1. MF_BYPOSITION tells DeleteMenu that the first parameter is a positional index and not a menu item ID. In this case, your only choice is to identify the menu item by position because Shape is a submenu that has no menu item ID.

To apply DeleteMenu and other CMenu functions to items in a submenu, you need a pointer either to the main menu or to the submenu. CMenu::GetSubMenu returns a pointer to a submenu. The following code fragment uses GetMenu to get a pointer to the main menu and GetSubMenu to get a pointer to the Shape menu. It then deletes the Square and Circle commands.

CMenu* pMenu = GetMenu ()->GetSubMenu (1); pMenu->DeleteMenu (2, MF_BYPOSITION); // Delete Square pMenu->DeleteMenu (ID_SHAPE_CIRCLE, MF_BYCOMMAND); // Delete Circle

The first call to DeleteMenu identifies the menu item by its position in the menu; the second identifies it by its command ID. The MF_BYPOSITION and MF_BYCOMMAND flags tell Windows which means of identification you're using. If you specify neither, the default is MF_BYCOMMAND. The lone parameter passed to GetSubMenu is the 0-based index of the submenu. Because you identified Circle by ID and not by position, you could also delete it by calling DeleteMenu through the pointer to the main menu, like this:

CMenu* pMenu = GetMenu (); pMenu->DeleteMenu (ID_SHAPE_CIRCLE, MF_BYCOMMAND);

As long as a menu item is identified by ID, you can access it through a pointer to the menu in which it appears or a pointer to any higher-level menu. Don't try to use MF_BYPOSITION to delete an item in a submenu with the pointer returned by GetMenu—you might delete a submenu by mistake.

To change the characteristics of an existing menu item, use CMenu::ModifyMenu. If pMenu refers to the Shape menu, the statements

pMenu->ModifyMenu (ID_SHAPE_TRIANGLE, MF_STRING ¦ MF_BYCOMMAND, ID_SHAPE_TRIANGLE, "&Three-Sided Polygon"); pMenu->ModifyMenu (2, MF_STRING ¦ MF_BYPOSITION, ID_SHAPE_SQUARE, "&Four-Sided Polygon");

modify the Triangle and Square commands to read "Three-Sided Polygon" and "Four-Sided Polygon," respectively. The third parameter passed to the ModifyMenu function is the menu item's new command ID, which should be the same as the original if you don't want to change it. If the item you're changing represents a submenu rather than an ordinary menu item, the third parameter holds the menu handle instead of a menu item ID. Given a CMenu pointer to a submenu, you can always get the menu handle from the object's m_hMenu data member.

The System Menu

Just as a window can call CWnd::GetMenu to obtain a CMenu pointer to its top-level menu, it can call CWnd::GetSystemMenu to obtain a pointer to its system menu. Most applications are content to let Windows manage the system menu, but every now and then the need to do something special arises, such as adding an item of your own to the system menu or changing the behavior of an existing item.

Suppose you want to add an About MyApp menu item to your application's system menu. About commands are normally placed in the Help menu, but maybe your application doesn't have a Help menu. Or maybe your application is a small utility program that doesn't have any menus at all, in which case adding About MyApp to the system menu is more efficient than loading an entire menu for the benefit of just one command.

The first step is to get a pointer to the system menu, like this:

CMenu* pSystemMenu = GetSystemMenu (FALSE);

The FALSE parameter tells GetSystemMenu that you want a pointer to a copy of the system menu that you can modify. (TRUE resets the system menu to its default state.)

The second step is to add "About MyApp" to the system menu:

pSystemMenu->AppendMenu (MF_SEPARATOR); pSystemMenu->AppendMenu (MF_STRING, ID_SYSMENU_ABOUT, _T ("&About MyApp"));

The first call to AppendMenu adds a menu item separator to set your menu item apart from other items in the system menu; the second adds "About MyApp," whose ID is ID_SYSMENU_ABOUT. A good place to put this code is in the main window's OnCreate handler. Be aware that items added to the system menu should be assigned IDs that are multiples of 16 (16, 32, 48, and so on). Windows uses the lower four bits of the system menu's command IDs for its own purposes, so if you use any of those bits, you could receive some unexpected results.

As it stands now, the new item will show up in the system menu but it won't do anything. When the user picks an item from the system menu, the window receives a WM_SYSCOMMAND message with wParam equal to the menu item ID. The following OnSysCommand handler inspects the menu item ID and displays an About box if the ID equals ID_SYSMENU_ABOUT:

// In CMainWindow's message map ON_WM_SYSCOMMAND () void CMainWindow::OnSysCommand (UINT nID, LPARAM lParam) { if ((nID & 0xFFF0) == ID_SYSMENU_ABOUT) { // Display the About box. return; } CFrameWnd::OnSysCommand (nID, lParam); }

An nID value equal to ID_SYSMENU_ABOUT means that "About MyApp" was selected. If nID equals anything else, you must call the base class's OnSysCommand handler or else the system menu (and other parts of the program, too) will cease to function. Before you test the nID value passed to OnSysCommand, be sure to AND it with 0xFFF0 to strip any bits added by Windows.

You can also use OnSysCommand to modify the behavior of items Windows places in the system menu. The following message handler disables the system menu's Close command in a frame window:

void CMainWindow::OnSysCommand (UINT nID, LPARAM lParam) { if ((nID & 0xFFF0) != SC_CLOSE) CFrameWnd::OnSysCommand (nID, lParam); }

This version of OnSysCommand tests nID and passes the message to CFrameWnd only if nID represents an item other than Close. Alternatives to disabling Close with an OnSysCommand handler include disabling the menu item with CMenu::EnableMenuItem or deleting it altogether with CMenu::DeleteMenu, as shown here:

CMenu* pSystemMenu = GetSystemMenu (FALSE); pSystemMenu->EnableMenuItem (SC_CLOSE, // Disable it. MF_BYCOMMAND ¦ MF_DISABLED); pSystemMenu->DeleteMenu (SC_CLOSE, MF_BYCOMMAND); // Delete it.

The command IDs for Close and other system menu items are listed in the documentation for OnSysCommand.

Owner-Draw Menus

Menus that display strings of text are fine for most applications, but some menus cry out for pictures instead of text. One example is a Color menu containing Cyan and Magenta commands. Many users won't know that cyan is a 50-50 mix of blue and green, or that magenta is a mix of equal parts red and blue. But if the menu contained color swatches instead of text, the meanings of the menu items would be crystal clear. Graphical menus are a little more work to put together than text menus, but the reward can be well worth the effort.

The easiest way to do graphical menus is to create bitmaps depicting the menu items and use them in calls to CMenu::AppendMenu. MFC represents bitmapped images with the class CBitmap, and one form of AppendMenu accepts a pointer to a CBitmap object whose image then becomes the menu item. Once a CBitmap object is appended to the menu, Windows displays the bitmap when the menu is displayed. The drawback to using bitmaps is that they're fixed in size and not easily adapted to changes in screen metrics.

A more flexible way to replace text with graphics in a menu is to use owner-draw menu items. When a menu containing an owner-draw item is displayed, Windows sends the menu's owner (the window to which the menu is attached) a WM_DRAWITEM message saying, "It's time to draw the menu item, and here's where I want you to draw it." Windows even supplies a device context in which to do the drawing. The WM_DRAWITEM handler might display a bitmap, or it could use GDI functions to draw the menu item at the specified location. Before a menu containing an owner-draw menu item is displayed for the first time, Windows sends the menu's owner a WM_MEASUREITEM message to inquire about the menu item's dimensions. If a submenu contains, say, five owner-draw menu items, the window that the menu is attached to will receive five WM_MEASUREITEM messages and five WM_DRAWITEM messages the first time the submenu is displayed. Each time the submenu is displayed thereafter, the window will receive five WM_DRAWITEM messages but no further WM_MEASUREITEM messages.

The first step in implementing an owner-draw menu is to stamp all the owner-draw items with the label MF_OWNERDRAW. Unfortunately, MF_OWNERDRAW can't be specified in a MENU template unless the template is manually changed to a MENUEX resource, and the Visual C++ resource editor doesn't support the owner-draw style, anyway. Therefore, the best way to create MF_OWNERDRAW items in an MFC application is to convert conventional items into owner-draw items programmatically using CMenu::ModifyMenu.

The second step is adding an OnMeasureItem handler and associated message-map entry to respond to WM_MEASUREITEM messages. OnMeasureItem is prototyped as follows:

afx_msg void OnMeasureItem (int nIDCtl, LPMEASUREITEMSTRUCT lpmis)

nIDCtl contains a control ID identifying the control to which the message pertains and is meaningless for owner-draw menus. (WM_MEASUREITEM messages are used for owner-draw controls as well as owner-draw menus. When OnMeasureItem is called for a control, nIDCtl identifies the control.) lpmis points to a structure of type MEASUREITEMSTRUCT, which has the following form:

typedef struct tagMEASUREITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemWidth; UINT itemHeight; DWORD itemData; } MEASUREITEMSTRUCT;

OnMeasureItem's job is to fill in the itemWidth and itemHeight fields, informing Windows of the menu item's horizontal and vertical dimensions, in pixels. An OnMeasureItem handler can be as simple as this:

lpmis->itemWidth = 64; lpmis->itemHeight = 16;

To compensate for differing video resolutions, a better approach is to base the width and height of items in an owner-draw menu on some standard such as the SM_CYMENU value returned by ::GetSystemMetrics:

lpmis->itemWidth = ::GetSystemMetrics (SM_CYMENU) * 4; lpmis->itemHeight = ::GetSystemMetrics (SM_CYMENU);

SM_CYMENU is the height of the menu bars the system draws for top-level menus. By basing the height of owner-draw menu items on this value and scaling the width accordingly, you can ensure that owner-draw items have roughly the same proportions as menu items drawn by Windows.

The CtlType field of the MEASUREITEMSTRUCT structure is set to ODT_MENU if the message pertains to an owner-draw menu and is used to differentiate between owner-draw UI elements if a window contains owner-draw controls as well as owner-draw menu items. CtlID and itemData are not used for menus, but itemID contains the menu item ID. If the owner-draw menu items your application creates are of different heights and widths, you can use this field to determine which menu item OnMeasureItem was called for.

The third and final step in implementing owner-draw menu items is to provide an OnDrawItem handler for WM_DRAWITEM messages. The actual drawing is done inside OnDrawItem. The function is prototyped as follows:

afx_msg void OnDrawItem (int nIDCtl, LPDRAWITEMSTRUCT lpdis)

Once again, nIDCtl is undefined for owner-draw menu items. lpdis points to a DRAWITEMSTRUCT structure, which contains the following members:

typedef struct tagDRAWITEMSTRUCT { UINT CtlType; UINT CtlID; UINT itemID; UINT itemAction; UINT itemState; HWND hwndItem; HDC hDC; RECT rcItem; DWORD itemData; } DRAWITEMSTRUCT;

As in MEASUREITEMSTRUCT, CtlType is set to ODT_MENU if the message pertains to an owner-draw menu item, itemID holds the menu item ID, and CtlID and itemData are unused. hDC holds the handle of the device context in which the menu item is drawn, and rcItem is a RECT structure containing the coordinates of the rectangle in which the item appears. The size of the rectangle described by rcItem is based on the dimensions you provided to Windows in response to the WM_MEASUREITEM message for this particular menu item. Windows doesn't clip what you draw to the rectangle but instead relies on your code to be "well-behaved" and stay within the bounds described by rcItem. hwndItem holds the handle of the menu to which the menu item belongs. This value isn't often used because the other fields provide most or all of the information that's needed.

DRAWITEMSTRUCT's itemAction and itemState fields describe the drawing action required and the current state of the menu item—checked or unchecked, enabled or disabled, and so on. For an owner-draw item, itemAction contains one of two values: ODA_DRAWENTIRE means that you should draw the entire item, and ODA_SELECT means that you can optionally redraw just the part of the item that changes when the item is highlighted or unhighlighted. When the highlight bar is moved from one owner-draw menu item to another, the menu's owner receives a WM_DRAWITEM message without the ODA_SELECT flag for the item that's losing the highlight and another WM_DRAWITEM message with an ODA_SELECT flag for the item that's becoming highlighted. Programs that use owner-draw menus often ignore the value in itemAction and redraw the menu item in its entirety no matter what the value of itemAction, using itemState to decide whether the item should be drawn with or without highlighting.

itemState contains zero or more of the bit flags shown in the following table specifying the menu item's current state.

Value Meaning
ODS_CHECKED The menu item is currently checked.
ODS_DISABLED The menu item is currently disabled.
ODS_GRAYED The menu item is currently grayed out.
ODS_SELECTED The menu item is currently selected.

This state information is important because it tells you how you should draw the menu item. Which of the bit flags you examine depends on which states you allow the menu item to assume. You should always check the ODS_SELECTED flag and highlight the menu item if the flag is set. If your application includes code to check and uncheck owner-draw menu items, you should look for ODS_CHECKED and draw a check mark next to the menu item if the flag is set. Similarly, if you allow the item to be enabled and disabled, look for an ODS_DISABLED flag and draw accordingly. By default, MFC disables a menu item if you provide neither an ON_COMMAND handler nor an ON_UPDATE_COMMAND_UI handler for it, so it's possible for menu items to become disabled even though your application didn't explicitly disable them. You can disable this feature of MFC for frame windows by setting CFrameWnd::m_bAutoMenuEnable to FALSE.

An alternative method for implementing owner-draw menus is to attach the menu to a CMenu object and override CMenu's virtual MeasureItem and DrawItem functions to do the drawing. This technique is useful for creating self-contained menu objects that do their own drawing rather than rely on their owners to do it for them. For cases in which a menu is loaded from a resource and attached to a window without using a CMenu object as an intermediary, however, it's just as easy to let the window that owns the menu draw the menu items as well. That's the approach we'll use when we modify Shapes to include an owner-draw Color menu.

OnMenuChar Processing

One drawback to using owner-draw menus is that Windows doesn't provide keyboard shortcuts such as Alt-C-R for Color-Red. Even if you define the menu item text as "&Red" before using ModifyMenu to change the menu item to MF_OWNERDRAW, Alt-C-R will no longer work. Alt-C will still pull down the Color menu, but the R key will do nothing.

Windows provides a solution to this problem in the form of WM_MENUCHAR messages. A window receives a WM_MENUCHAR message when a menu is displayed and a key that doesn't correspond to a menu item is pressed. By processing WM_MENUCHAR messages, you can add keyboard shortcuts to owner-draw menu items. MFC's CWnd::OnMenuChar function is prototyped as follows:

afx_msg LRESULT OnMenuChar (UINT nChar, UINT nFlags, CMenu* pMenu)

When OnMenuChar is called, nChar contains the ANSI or Unicode character code of the key that was pressed, nFlags contains an MF_POPUP flag if the menu to which the message pertains is a submenu, and pMenu identifies the menu itself. The pointer stored in pMenu might be a temporary one created by the framework and shouldn't be saved for later use.

The value returned by OnMenuChar tells Windows how to respond to the keystroke. The high word of the return value should be set to one of the following values:

If the high word of the return value is 2, the low word should hold the ID of the corresponding menu item. Windows provides a MAKELRESULT macro for setting the high and low words of an LRESULT value. The following statement sets the high word of an LRESULT value to 2 and the low word to ID_COLOR_RED:

LRESULT lResult = MAKELRESULT (ID_COLOR_RED, 2);

Of course, you can always rely on keyboard accelerators instead of keyboard shortcuts. They work just fine with owner-draw menu items. But thanks to WM_MENUCHAR messages, you have the option of providing conventional keyboard shortcuts as well.

Cascading Menus

When you click the Start button in the taskbar, a popup menu appears listing the various options for starting applications, opening documents, changing system settings, and so on. Some of the menu items have arrows next to them indicating that clicking invokes another menu. And in some cases, these menus are nested several levels deep. Click Start-Programs-Accessories-Games, for example, and the Games menu is the fourth in a series of menus cascaded across the screen. This multitiered menu structure permits items in the Start menu to be organized hierarchically and prevents individual menus from being so cluttered that they become practically useless.

Cascading menus aren't the sole property of the operating system; application programs can use them, too. Creating a cascading menu is as simple as inserting one menu into another as if it were a menu item. Windows sweats the details, which include drawing the arrow next to the item name and displaying the cascaded menu without a button click if the cursor pauses over the item. Here's how Shapes' top-level menu would be defined if the Shape menu was nested inside an Options menu.

IDR_MAINFRAME MENU PRELOAD DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "E&xit", ID_APP_EXIT END POPUP "&Options" BEGIN POPUP "&Shape" BEGIN MENUITEM "&Circle\tF7", ID_SHAPE_CIRCLE MENUITEM "&Triangle\tF8", ID_SHAPE_TRIANGLE MENUITEM "&Square\tF9", ID_SHAPE_SQUARE END MENUITEM "&Color…", ID_OPTIONS_COLOR MENUITEM "Si&ze…", ID_OPTIONS_SIZE END END

Figure 4-12 shows how the resulting menu would look. Selecting Shape from the Options menu displays a cascading menu. Moreover, the remainder of the program works as it did before, so the command and update handlers associated with the items in the Shape menu needn't change.

Figure 4-12. Cascading menus.

You don't have to edit menu resources by hand to create cascading menus. Instead, you can create a nested menu in Visual C++'s menu editor by checking the Pop-up check box in the Menu Item Properties dialog box, as shown in Figure 4-13.

Figure 4-13. Creating a nested menu.

Context Menus

Windows uses right-click context menus extensively to make objects displayed by the shell easier to manipulate. Right-clicking the My Computer icon on the desktop, for example, displays a context menu containing a concise list of actions that can be performed on My Computer: Explore, Rename, Map Network Drive, and so on. Right-clicking the desktop produces an entirely different context menu. Developers are encouraged to build context menus into their applications to be consistent with the shell and to reinforce the object-oriented UI paradigm. Windows makes it easy by sending your application a WM_CONTEXTMENU message when the right mouse button is clicked in a window and the resulting right-button message isn't processed.

A context menu is nothing more than a submenu that isn't attached to a top-level menu. MFC's CMenu::TrackPopupMenu function displays such a menu. Here's the function prototype:

BOOL TrackPopupMenu (UINT nFlags, int x, int y, CWnd* pWnd, LPCRECT lpRect = NULL)

x and y identify the location on the screen (in screen coordinates) at which the menu will appear. nFlags contains bit flags specifying the menu's horizontal alignment relative to x and which mouse button (or buttons) can be used to select items from the menu. The alignment flags TPM_LEFTALIGN, TPM_CENTERALIGN, and TPM_RIGHTALIGN tell Windows that x specifies the location of the menu's left edge, center, and right edge, respectively, and the TPM_LEFTBUTTON and TPM_RIGHTBUTTON flags specify whether menu selections will be made with the left or the right mouse button. Only one of the alignment flags can be specified, but either or both of the button flags can be used. pWnd identifies the window that will receive messages emanating from actions in the menu, and lpRect points to a CRect object or RECT structure containing the screen coordinates of the rectangle within which the user can click without dismissing the menu. If lpRect is NULL, clicking outside the menu dismisses it. Assuming pMenu is a CMenu pointer that references a submenu, the statement

pMenu->TrackPopupMenu (TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON, 32, 64, AfxGetMainWnd ());

displays the menu whose upper left corner is positioned 32 pixels right and 64 pixels down from the upper left corner of the screen. The user can make selections from the menu with either the left or the right mouse button. While the menu is displayed, the application's main window receives messages just as if the menu were part of a top-level menu. Once the menu is dismissed, the messages will cease until the menu is displayed again.

TrackPopupMenu is typically called in response to WM_CONTEXTMENU messages. MFC's ON_WM_CONTEXTMENU macro maps WM_CONTEXTMENU messages to the message handler OnContextMenu. OnContextMenu receives two parameters: a CWnd pointer identifying the window in which the click occurred and a CPoint containing the cursor's screen coordinates:

afx_msg void OnContextMenu (CWnd* pWnd, CPoint point)

If necessary, you can translate the screen coordinates passed in point into client coordinates with CWnd::ScreenToClient. It might seem curious that OnContextMenu receives a pointer identifying a window since mouse messages go to the window under the cursor. However, there's a reason. Unlike other messages, WM_CONTEXTMENU messages percolate upward through the window hierarchy if a right-click occurs in a child window (for example, a push button control) and the child window doesn't process the message. Therefore, if a window contains child windows, it could receive WM_CONTEXTMENU messages with pWnd containing a pointer to one of its children.

It's important for an OnContextMenu handler to call the base class's OnContextMenu handler if it examines pWnd or point and decides not to process the message. Otherwise, WM_CONTEXTMENU messages won't percolate upward. Worse, right-clicking the window's title bar will no longer display the system menu. The following OnContextMenu handler displays the context menu referenced by pContextMenu if the button click occurs in the upper half of the window and passes it to the base class if the click occurs elsewhere:

void CChildView::OnContextMenu (CWnd* pWnd, CPoint point) { CPoint pos = point; ScreenToClient (&pos); CRect rect; GetClientRect (&rect); rect.bottom /= 2; // Divide the height by 2. if (rect.PtInRect (pos)) { pContextMenu->TrackPopupMenu (TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON, point.x, point.y, AfxGetMainWnd ()); return; } CWnd::OnContextMenu (pWnd, point); }

In a view-based application like Shapes, the WM_CONTEXTMENU handler is typically placed in the view class because that's where the objects that are subject to right clicks are displayed.

How do you get a pointer to a context menu in order to display it? One method is to construct a CMenu object and build the menu with CMenu member functions. Another is to load the menu from a resource in the same way that a top-level menu is loaded. The following menu template defines a menu that contains one submenu:

IDR_CONTEXTMENU MENU BEGIN POPUP "" BEGIN MENUITEM "&Copy", ID_CONTEXT_COPY MENUITEM "&Rename", ID_CONTEXT_RENAME MENUITEM "&Delete", ID_CONTEXT_DELETE END END

The following statements load the menu into a CMenu object and display it as a context menu:

CMenu menu; menu.LoadMenu (IDR_CONTEXTMENU); CMenu* pContextMenu = menu.GetSubMenu (0); pContextMenu->TrackPopupMenu (TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON, point.x, point.y, AfxGetMainWnd ());

If your application uses several context menus, you can define each context menu as a separate submenu of IDR_CONTEXTMENU and retrieve CMenu pointers by varying the index passed to GetSubMenu. Or you can define each one as a separate menu resource. In any event, attaching the context menu to a CMenu object that resides on the stack ensures that the menu will be destroyed when the object goes out of scope. The menu is no longer needed after TrackPopupMenu returns, so deleting it frees up memory that can be put to other uses.

The TPM_RETURNCMD Flag

How do you process context menu commands? The same way you process commands from conventional menus: by writing command handlers. You can write update handlers for commands in a context menu, too. In fact, it's perfectly legal to assign a command in a conventional menu and a command in a context menu the same command ID and let one command handler (and, if you'd like, one update handler) service both of them.

Occasionally, you'll want to get a return value from TrackPopupMenu indicating which, if any, menu item was selected and to process the command on the spot rather than delegate to a command handler. That's why TPM_RETURNCMD exists. Passed a TPM_RETURNCMD flag in its first parameter, TrackPopupMenu returns the command ID of the item selected from the menu. A 0 return means that the menu was dismissed with no selection. Assuming pContextMenu references the context menu used in the example in the previous section, the following statements demonstrate how to display the menu and act immediately on the user's selection:

int nCmd = (int) pContextMenu->TrackPopupMenu (TPM_LEFTALIGN ¦ TPM_LEFTBUTTON ¦ TPM_RIGHTBUTTON ¦ TPM_RETURNCMD, point.x, point.y, AfxGetMainWnd ()); switch (nCmd) { case ID_CONTEXT_COPY: // Copy the object. break; case ID_CONTEXT_RENAME: // Rename the object. break; case ID_CONTEXT_DELETE: // Delete the object. break; }

A menu displayed this way still generates a WM_COMMAND message when an item is selected. That's normally not a problem, because if you don't provide a command handler for the item, the message is passed harmlessly on to Windows. But suppose you'd like to suppress such messages, perhaps because you've used the same ID for an item in a conventional menu and an item in a context menu and you want the item in the context menu to behave differently than the one in the conventional menu. To do it, simply include a TPM_NONOTIFY flag in the call to TrackPopupMenu.

Don't forget that by default, MFC disables menu items for which no command and update handlers are provided. Therefore, if you use the TPM_RETURNCMD flag, you'll probably find it necessary to set m_bAutoMenuEnable to FALSE in your frame window.

Категории