Programming Windows with MFC, Second Edition

[Previous] [Next]

Let's start by defining a few terms. The menu bar that appears at the top of a window is an application's top-level menu, and the commands in it are called top-level menu items. The menu that appears when a top-level menu item is clicked is a drop-down menu, and items in that menu are referred to as menu items. Menu items are identified by integer values called menu item IDs or command IDs. Windows also supports popup menus that look like drop-down menus but can be popped up anywhere on the screen. The context menu that appears when you right-click an object in the Windows shell is an example of a popup menu. Drop-down menus are actually popup menus that are submenus of an application's top-level menu.

Most top-level windows also feature a system menu containing commands for restoring, moving, sizing, minimizing, maximizing, and closing the window. Windows provides this menu, which you display by clicking the left mouse button on the small icon in the window's title bar, clicking the right mouse button in the body of the title bar, or pressing Alt-Spacebar.

MFC encapsulates menus and the actions that can be performed on them in the CMenu class. CMenu contains one public data member—an HMENU named m_hMenu that holds the handle of the corresponding menu—and several member functions that provide object-oriented wrappers around functions in the Windows API. CMenu::TrackPopupMenu, for example, displays a context menu, and CMenu::EnableMenuItem enables or disables a menu item. CMenu also contains a pair of virtual functions named DrawItem and MeasureItem that you can override if you want to create stylized menu items containing bitmaps and other graphical user interface elements.

You can create a menu in an MFC application in three ways:

The third method is far and away the most common because it allows you to define a menu off line using a resource editor or, if you'd prefer, a simple text editor. We'll focus on this method in the first half of the chapter.

Creating a Menu

The easiest way to create a menu is to add a menu template to your application's resource file. A resource file is a scriptlike text file that defines an application's resources; by convention, it is assigned the file name extension .rc and hence is often referred to as an RC file. A resource is a binary object such as a menu or an icon. Windows supports several types of resources, including (but not limited to) menus, icons, bitmaps, and strings. The resource compiler Rc.exe, which is provided with the Windows Software Development Kit (SDK) and is also part of Microsoft Visual C++, compiles the statements in an RC file and links the resulting resources into the application's EXE file. Every resource is identified by a string or an integer ID such as "MyMenu" (string) or IDR_MYMENU (integer). Integer resource IDs are given human-readable names such as IDR_MYMENU by means of #define statements in a header file. Once a resource is compiled and linked into an EXE, it can be loaded with a simple function call.

A menu template contains all the information the resource compiler needs to create a menu resource, including the menu's resource ID, the names of the menu items, and the IDs of the menu items. The menu template in Figure 4-1 comes from a project created by Visual C++'s MFC AppWizard. It defines a single menu resource consisting of a top-level menu and four submenus—File, Edit, View, and Help. IDR_MAINFRAME is the menu's resource ID. PRELOAD and DISCARDABLE are resource attributes. PRELOAD tells Windows to load the menu resource into memory when the application starts. DISCARDABLE allows Windows to discard the resource if the memory it occupies is needed for other purposes. (If it's needed again, a discarded resource can be reloaded from the application's EXE file.) PRELOAD and DISCARDABLE are both artifacts of 16-bit Windows and have no impact on either the performance or behavior of 32-bit applications.

Figure 4-1. A menu template generated by the MFC AppWizard.

IDR_MAINFRAME MENU PRELOAD DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New\tCtrl+N", ID_FILE_NEW MENUITEM "&Open...\tCtrl+O", ID_FILE_OPEN MENUITEM "&Save\tCtrl+S", ID_FILE_SAVE MENUITEM "Save &As...", ID_FILE_SAVE_AS MENUITEM SEPARATOR MENUITEM "Recent File", ID_FILE_MRU_FILE1,GRAYED MENUITEM SEPARATOR MENUITEM "E&xit", ID_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tCtrl+Z", ID_EDIT_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl+X", ID_EDIT_CUT MENUITEM "&Copy\tCtrl+C", ID_EDIT_COPY MENUITEM "&Paste\tCtrl+V", ID_EDIT_PASTE END POPUP "&View" BEGIN MENUITEM "&Toolbar", ID_VIEW_TOOLBAR MENUITEM "&Status Bar", ID_VIEW_STATUS_BAR END POPUP "&Help" BEGIN MENUITEM "&About MyApp...", ID_APP_ABOUT END END

The statements between the opening and closing BEGIN and END statements define the contents of the menu, with POPUP statements defining top-level menu items and the associated submenus. The BEGIN and END statements following POPUP statements bracket MENUITEM statements defining the items in the submenus. The special MENUITEM SEPARATOR statement adds a thin horizontal line to the menu; it's used to provide visual separation between groups of menu items. The ampersands in the text of the menu items identify shortcut keys the user can press in combination with the Alt key to display submenus and select items from submenus. In this example, the File-Exit command can be selected by pressing Alt-F and then X. Windows underlines the F in "File" and the x in "Exit" so that they're easily identifiable as shortcut keys. If two or more items in the same menu are assigned the same shortcut key, the shortcut cycles among the menu items and no selection is made until the Enter key is pressed.

An ellipsis (...) in the text of a menu item indicates that further input is required after the item is selected. If the user selects Save, the document is saved immediately. But if the user selects Save As, a dialog box is displayed instead. To be consistent with other Windows applications, use an ellipsis for any menu item whose action is deferred until subsequent input is received from the user. If an item in the top-level menu executes a command instead of displaying a submenu, the text of the item should be followed with an exclamation mark, as in

IDR_MAINFRAME MENU PRELOAD DISCARDABLE BEGIN POPUP "&File" [...] POPUP "&Edit" [...] POPUP "&View" [...] POPUP "&Help" [...] MENUITEM "E&xit!", ID_APP_EXIT END

It's legal to include MENUITEM statements in top-level menus this way, but these days it's considered bad form. And it's likely to surprise your users, most of whom are accustomed to seeing top-level menu items display submenus rather than take action themselves.

The ID_ values following the menu item names in the MENUITEM statements are command IDs. Every menu item should be assigned a unique command ID because it is this value that identifies the menu item to your application when the user makes a selection. By convention, IDs are defined with #define statements, and each is given the name ID_ or IDM_ followed by an item name spelled in capital letters. MFC's Afxres.h header file defines ID_ values for commonly used commands such as File-New and Edit-Paste. When you write document/view applications, using the predefined IDs automatically connects certain menu items to handling functions the framework provides. In nondocument/view applications, use of the predefined IDs is optional.

Valid values for menu item IDs range from 1 through 0xEFFF, but MFC Technical Note #20 recommends restricting the range to 0x8000 through 0xDFFF. IDs equal to 0xF000 and higher are reserved for Windows—specifically, for items in the system menu. The range 0xE000 to 0xEFFF is reserved for MFC. In practice, it's perfectly safe to use values lower than 0x8000, and in fact, restricting item IDs to the range 1 through 0x7FFF sidesteps a nasty bug in Windows 95 that affects owner-draw menu items. This bug is explained—and work-arounds are presented—later in this chapter.

The text following the tab character in some of the menu items (for example, the "Ctrl+O" in "Open…\tCtrl+O") identifies an accelerator. An accelerator is a key or combination of keys that, when pressed, has the same effect as selecting a menu item. Commonly used accelerators include Ctrl-X for Edit-Cut, Ctrl-C for Edit-Copy, and Ctrl-V for Edit-Paste. Text strings denoting accelerator keys are preceded by tab characters for alignment purposes. The default font used in menus is proportionally spaced, so it's futile to try to align menu text with spaces.

When you define a menu item with MENUITEM, you also have the option of specifying the item's initial state. The GRAYED keyword accompanying the File-Recent File command in Figure 4-1 disables the menu item so that it can't be selected. A disabled item is "grayed out" as a visual reminder that it is disabled. Grayed menu text is displayed in the system color COLOR_GRAYTEXT, which defaults to gray, with a thin border added to provide a three-dimensional look. Another optional keyword is CHECKED, which places a check mark beside a menu item. Although common in Windows applications written in C using the SDK, menu item state specifiers are rarely used in MFC applications because the framework provides a powerful mechanism for updating menu items programmatically. You'll learn more about this mechanism shortly.

Loading and Displaying a Menu

At run time, a menu resource needs to be loaded and attached to a window. When the window is displayed, the menu will also be displayed.

One way to attach a menu to a window is to pass the menu's resource ID to CFrameWnd::Create. The following statement creates a frame window and attaches the menu whose resource ID is IDR_MAINFRAME:

Create (NULL, _T ("My Application"), WS_OVERLAPPEDWINDOW, rectDefault, NULL, MAKEINTRESOURCE (IDR_MAINFRAME));

The sixth argument to Create identifies the menu resource. The MAKEINTRESOURCE macro converts an integer resource ID to an LPTSTR data type ID compatible with functions that expect string-based resource IDs. When the window appears on the screen, the menu will be visible just below the title bar.

A second method involves the CFrameWnd::LoadFrame function. Given a resource ID, LoadFrame creates a frame window and attaches a menu, much like Create. The statement

LoadFrame (IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL);

creates a window and attaches the menu IDR_MAINFRAME. Some MFC programs—particularly wizard-generated applications—use LoadFrame instead of Create because LoadFrame will load icons and other resources, too. MAKEINTRESOURCE isn't required in this example because it's built into LoadFrame.

Yet another method for loading a top-level menu and attaching it to a window is to construct a CMenu object, call CMenu::LoadMenu to load the menu resource, and call CWnd::SetMenu, like this:

CMenu menu; menu.LoadMenu (IDR_MAINFRAME); SetMenu (&menu); menu.Detach ();

In this example, CMenu::Detach is called to detach the menu from the CMenu object so that the menu won't be destroyed prematurely when menu goes out of scope. The CMenu class helps guard against resource leaks by calling CMenu::DestroyMenu from its destructor. As a rule, a menu loaded with LoadMenu should be destroyed with DestroyMenu before the application that loaded the menu terminates. However, a menu attached to a window is automatically destroyed when the window is destroyed, so detaching a menu from a CMenu object after attaching it to a window won't cause a resource leak unless the menu is later detached from the window without a subsequent call to DestroyMenu.

The SetMenu technique offers no advantage over simply passing the menu ID to Create or LoadFrame when a program contains just one menu, but it's very useful in programs that contain two or more menus. Suppose you want to write an application that allows the user to choose short or long menus. Here's one way to go about it. First, create two menu resources—one for the short menus, another for the long. At startup, load the menu resources into CMenu data members named m_menuLong and m_menuShort. Then choose the menu type based on the value of a BOOL data member named m_bShortMenu, which is TRUE if short menus are selected and FALSE if they're not. Here's what the window's constructor might look like:

Create (NULL, _T ("My Application")); m_menuLong.LoadMenu (IDR_LONGMENU); m_menuShort.LoadMenu (IDR_SHORTMENU); SetMenu (m_bShortMenu ? &m_menuShort : &m_menuLong);

In response to a command from the user, the following code would switch from long menus to short menus:

m_bShortMenu = TRUE; SetMenu (&m_menuShort); DrawMenuBar ();

And these statements would switch back to long menus:

m_bShortMenu = FALSE; SetMenu (&m_menuLong); DrawMenuBar ();

CWnd::DrawMenuBar redraws the menu bar to reflect the change. You should always follow calls to SetMenu with calls to DrawMenuBar unless the window isn't visible on the screen.

What about code to delete the menus, since only one will be attached to a window when the application ends? If m_menuLong and m_menuShort are data members of the frame window class, their destructors will be called when the frame window is destroyed and the menus associated with them will also be deleted. Therefore, explicit calls to DestroyMenu aren't required.

Responding to Menu Commands

When the user pulls down a menu, the window to which the menu is attached receives a series of messages. Among the first to arrive is a WM_INITMENU message notifying the window that a top-level menu item was selected. Before a submenu is displayed, the window receives a WM_INITMENUPOPUP message. Windows programs sometimes take this opportunity to update the submenu's menu items—for example, putting a check mark next to the Toolbar item in the View menu if the application's toolbar is displayed or unchecking the menu item if the toolbar is currently hidden. As the highlight travels up and down the menu, the window receives WM_MENUSELECT messages reporting the latest position in the menu. In SDK-style programs, WM_MENUSELECT messages are sometimes used to display context-sensitive menu help in a status bar.

The most important message of all is the WM_COMMAND message sent when the user selects an item from the menu. The low word of the message's wParam parameter holds the item's command ID. SDK programmers often use switch-case logic to vector execution to the appropriate handling routine, but MFC provides a better way. An ON_COMMAND statement in the message map links WM_COMMAND messages referencing a particular menu item to the class member function, or command handler, of your choice. The following message-map entry tells MFC to call OnFileSave when the ID_FILE_SAVE menu item is selected:

ON_COMMAND (ID_FILE_SAVE, OnFileSave)

Other items in the File menu might be mapped like this:

ON_COMMAND (ID_FILE_NEW, OnFileNew) ON_COMMAND (ID_FILE_OPEN, OnFileOpen) ON_COMMAND (ID_FILE_SAVE, OnFileSave) ON_COMMAND (ID_FILE_SAVE_AS, OnFileSaveAs) ON_COMMAND (ID_FILE_EXIT, OnFileExit)

Now OnFileNew will be activated when File-New is selected, OnFileOpen will be called when File-Open is selected, and so on.

Command handlers take no arguments and return no values. The OnFileExit function, for example, is typically implemented like this

void CMainWindow::OnFileExit () { PostMessage (WM_CLOSE, 0, 0); }

This command handler terminates the application by posting a WM_CLOSE message to the application's main window. This message ultimately ends the application by causing a WM_QUIT message to appear in the application's message queue.

You can name command handlers whatever you like. There are no naming criteria as there are for WM_ message handlers. Handlers for WM_PAINT and WM_CREATE must be named OnPaint and OnCreate unless you care to rewrite MFC's ON_WM_PAINT and ON_WM_CREATE macros. But you could just as easily have written the message-map entries for our File menu like this:

ON_COMMAND (ID_FILE_NEW, CreateMeAFile) ON_COMMAND (ID_FILE_OPEN, OpenMeAFile) ON_COMMAND (ID_FILE_SAVE, SaveThisFile) ON_COMMAND (ID_FILE_SAVE_AS, SaveThisFileUnderAnotherName) ON_COMMAND (ID_FILE_EXIT, KillThisAppAndDoItNow)

Command Ranges

Sometimes it's more efficient to process a group of menu item IDs with a single command handler than to provide a separate member function for each ID. Consider a drawing application that contains a Color menu from which the user can choose red, green, or blue. Selecting a color from the menu sets a member variable named m_nCurrentColor to 0, 1, or 2 and subsequently changes the color of what the user draws on the screen. The message-map entries and command handlers for these menu items might be implemented as follows:

// In CMainWindow's message map ON_COMMAND (ID_COLOR_RED, OnColorRed) ON_COMMAND (ID_COLOR_GREEN, OnColorGreen) ON_COMMAND (ID_COLOR_BLUE, OnColorBlue) void CMainWindow::OnColorRed () { m_nCurrentColor = 0; } void CMainWindow::OnColorGreen () { m_nCurrentColor = 1; } void CMainWindow::OnColorBlue () { m_nCurrentColor = 2; }

This isn't a terribly efficient way to process messages from the Color menu because each message handler does essentially the same thing. And the inefficiency would be compounded if the menu contained 10 or 20 different colors rather than just 3.

One way to reduce the redundancy in the command handlers for the Color menu is to map all three items to the same CMainWindow member function and retrieve the menu item ID with CWnd::GetCurrentMessage, as shown below.

// In CMainWindow's message map ON_COMMAND (ID_COLOR_RED, OnColor) ON_COMMAND (ID_COLOR_GREEN, OnColor) ON_COMMAND (ID_COLOR_BLUE, OnColor) void CMainWindow::OnColor () { UINT nID = (UINT) LOWORD (GetCurrentMessage ()->wParam); m_nCurrentColor = nID _ ID_COLOR_RED; }

This approach works just fine as long as the command IDs constitute a contiguous range beginning with ID_COLOR_RED, but it's an imperfect solution because it relies on the value of wParam. If the meaning of the wParam parameter accompanying WM_COMMAND messages changes in a future release of Windows (as it did between Windows 3.1 and Windows 95), you might have to modify this code to get it to work properly. And even though you've reduced the number of command handlers from three to one, you're still adding three separate entries to the class's message map at a cost of 24 bytes each.

A better solution is the MFC ON_COMMAND_RANGE macro, which maps a range of contiguous command IDs to a common handling function. Assuming ID_COLOR_RED is the lowest value in the range and ID_COLOR_BLUE is the highest, ON_COMMAND_RANGE allows you to rewrite the code for the Color menu like this:

// In CMainWindow's message map ON_COMMAND_RANGE (ID_COLOR_RED, ID_COLOR_BLUE, OnColor) void CMainWindow::OnColor (UINT nID) { m_nCurrentColor = nID _ ID_COLOR_RED; }

When OnColor is called because the user chose an item from the Color menu, nID contains ID_COLOR_RED, ID_COLOR_GREEN, or ID_COLOR_BLUE. One simple statement sets m_nCurrentColor to the proper value, no matter which menu item was selected.

Updating the Items in a Menu

In many applications, menu items must be constantly updated to reflect internal states of the application or its data. When a color is selected from a Color menu, for example, the corresponding menu item should be checked or bulleted to indicate which color is currently selected. An application that features an Edit menu with Cut, Copy, and Paste commands should disable the Cut and Copy menu items when nothing is selected and disable the Paste menu item when the clipboard is empty. Menus are more than just lists of commands. Deployed properly, they provide visual feedback to the user about the current state of the application and make clear what commands are (and are not) available at any given moment.

Windows programmers have traditionally taken one of two approaches to keeping menu items up to date. The first approach is illustrated by the following code sample, which is a modified version of the OnColor function presented in the previous section:

void CMainWindow::OnColor (UINT nID) { CMenu* pMenu = GetMenu (); pMenu->CheckMenuItem (m_nCurrentColor + ID_COLOR_RED, MF_UNCHECKED); pMenu->CheckMenuItem (nID, MF_CHECKED); m_nCurrentColor = nID _ ID_COLOR_RED; }

In this example, the Color menu is updated the moment an item is selected. First CMenu::CheckMenuItem is called with an MF_UNCHECKED flag to uncheck the item that's currently checked. Then CheckMenuItem is called with an MF_CHECKED flag to place a check mark by the item that was just selected. The next time the Color menu is pulled down, the check mark will identify the current color.

The second approach is to move the code that updates the menu to an OnInitMenuPopup handler that's activated in response to WM_INITMENUPOPUP messages. This strategy positions the check mark each time the Color menu is pulled down, just before the menu is actually displayed. OnInitMenuPopup receives three parameters: a CMenu pointer referencing the submenu that's about to be displayed, a UINT value holding the submenu's 0-based index in the top-level menu, and a BOOL value that's nonzero if the message pertains to the system menu instead of a submenu. Here's what an OnInitMenuPopup handler for the Color menu might look like. COLOR_MENU_INDEX is an index specifying the Color menu's position in the top-level menu:

// In CMainWindow's message map ON_WM_INITMENUPOPUP () void CMainWindow::OnInitMenuPopup (CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { if (!bSysMenu && (nIndex == COLOR_MENU_INDEX)) { pPopupMenu->CheckMenuItem (ID_COLOR_RED, MF_UNCHECKED); pPopupMenu->CheckMenuItem (ID_COLOR_GREEN, MF_UNCHECKED); pPopupMenu->CheckMenuItem (ID_COLOR_BLUE, MF_UNCHECKED); pPopupMenu->CheckMenuItem (m_nCurrentColor + ID_COLOR_RED, MF_CHECKED); } }

This method is more robust than the first because it decouples the code that processes commands from the code that updates the menu. Now any function anywhere in the application can change the drawing color, and the menu will be updated automatically the next time it's displayed.

MFC provides a similar but more convenient mechanism for keeping menu items updated. Through ON_UPDATE_COMMAND_UI macros in the message map, you can designate selected member functions to serve as update handlers for individual menu items. When the user pulls down a menu, MFC traps the ensuing WM_INITMENUPOPUP message and calls the update handlers for all the items in the menu. Each update handler is passed a pointer to a CCmdUI object whose member functions can be used to modify the menu item. And because the CCmdUI class isn't specific to any particular type of user interface (UI) element, the same update handler that serves a menu item can serve toolbar buttons and other UI objects, too. Abstracting UI updates in this way simplifies the program logic and helps make an application independent of the operating system it's written for.

Here's how to rewrite the code for the Color menu to take advantage of update handlers:

// In CMainWindow's message map ON_COMMAND_RANGE (ID_COLOR_RED, ID_COLOR_BLUE, OnColor) ON_UPDATE_COMMAND_UI (ID_COLOR_RED, OnUpdateColorRed) ON_UPDATE_COMMAND_UI (ID_COLOR_GREEN, OnUpdateColorGreen) ON_UPDATE_COMMAND_UI (ID_COLOR_BLUE, OnUpdateColorBlue) void CMainWindow::OnColor (UINT nID) { m_nCurrentColor = nID _ ID_COLOR_RED; } void CMainWindow::OnUpdateColorRed (CCmdUI* pCmdUI) { pCmdUI->SetCheck (m_nCurrentColor == 0); } void CMainWindow::OnUpdateColorGreen (CCmdUI* pCmdUI) { pCmdUI->SetCheck (m_nCurrentColor == 1); } void CMainWindow::OnUpdateColorBlue (CCmdUI* pCmdUI) { pCmdUI->SetCheck (m_nCurrentColor == 2); }

ON_UPDATE_COMMAND_UI connects menu items to update handlers just as ON_COMMAND connects menu items to command handlers. Now selecting a color from the Color menu will activate CMainWindow::OnColor, and before the Color menu is displayed, each item's update handler will be called. The handlers shown here do their updating by calling CCmdUI::SetCheck to check or uncheck the corresponding menu item. Called with a nonzero value, SetCheck adds a check mark to the corresponding menu item; called with a 0, it displays no check mark.

SetCheck is just one of the CCmdUI methods that you can use to update a menu item. The following table shows a complete list, along with a description of each function's effect on a menu item.

Function Description
CCmdUI::Enable Enables or disables a menu item
CCmdUI::SetCheck Checks or unchecks a menu item
CCmdUI::SetRadio Bullets or unbullets a menu item
CCmdUI::SetText Changes the text of a menu item

SetRadio works like SetCheck but adds or removes a bullet instead of a check mark. SetRadio is one of those MFC functions that doesn't have a direct counterpart in the Windows API; the framework does some work behind the scenes to allow menu items to be bulleted rather than checked. Ideally, you'd use a bullet to indicate which item in a group of mutually exclusive menu items is currently selected and a check mark to indicate whether a feature is on or off. (In practice, check marks are frequently used for both.) Enable enables or disables a menu item, and SetText allows you to change the text of the menu item on the fly.

Update Ranges

For updating groups of menu items with a single update handler, MFC provides the ON_UPDATE_COMMAND_UI_RANGE macro, which is to ON_COMMAND_RANGE as ON_UPDATE_COMMAND_UI is to ON_COMMAND. To understand how ON_UPDATE_COMMAND_UI_RANGE is used, let's revisit the Color menu and assume that it contains eight color choices: black, blue, green, cyan, red, magenta, yellow, and white, in that order. The corresponding menu item IDs are ID_COLOR_BLACK through ID_COLOR_WHITE. Let's also assume that we want to put a bullet by the current color. Here's the most concise way to do it.

// In CMainWindow's message map ON_COMMAND_RANGE (ID_COLOR_BLACK, ID_COLOR_WHITE, OnColor) ON_UPDATE_COMMAND_UI_RANGE (ID_COLOR_BLACK, ID_COLOR_WHITE, OnUpdateColorUI) void CMainWindow::OnColor (UINT nID) { m_nCurrentColor = nID _ ID_COLOR_BLACK; } void CMainWindow::OnUpdateColorUI (CCmdUI* pCmdUI) { pCmdUI->SetRadio (pCmdUI->m_nID - ID_COLOR_BLACK == m_nCurrentColor); }

m_nID is a public data member of CCmdUI that holds the ID of the menu item for which the update handler was called. By comparing m_nID minus ID_COLOR_BLACK to m_nCurrentColor and passing the result to SetRadio, you can ensure that only the current color is bulleted.

Just how useful is MFC's command-update mechanism? Later in this chapter, we'll develop a sample program that uses two identical Color menus—one that's invoked from a top-level menu and another that's invoked from a right-click context menu. The same command and update handler will serve both menus, and no matter how a color is selected, both menus will be updated to match—with one line of code no less. It's hard to imagine how updating menu items could be any easier.

Keyboard Accelerators

As you design your application's menus, you have the option of using keyboard accelerators to assign shortcut keys to any or all of the menu items. An accelerator produces a WM_COMMAND message just as making a menu selection does. Adding keyboard accelerators to your application is simplicity itself. You create an accelerator table resource—a special resource that correlates menu item IDs to keys or combinations of keys—and load the resource into your program with a function call. If the application's main window is a frame window, Windows and the framework do the rest, automatically trapping presses of accelerator keys and notifying your application with WM_COMMAND messages.

An accelerator table resource is defined by an ACCELERATORS block in an RC file. Here is the general format:

ResourceID ACCELERATORS BEGIN END

ResourceID is the accelerator table's resource ID. The statements between BEGIN and END identify the accelerator keys and the corresponding menu item IDs. The MFC AppWizard generates accelerator tables using the following format:

IDR_MAINFRAME ACCELERATORS PRELOAD MOVEABLE BEGIN "N", ID_FILE_NEW, VIRTKEY,CONTROL "O", ID_FILE_OPEN, VIRTKEY,CONTROL "S", ID_FILE_SAVE, VIRTKEY,CONTROL "Z", ID_EDIT_UNDO, VIRTKEY,CONTROL "X", ID_EDIT_CUT, VIRTKEY,CONTROL "C", ID_EDIT_COPY, VIRTKEY,CONTROL "V", ID_EDIT_PASTE, VIRTKEY,CONTROL VK_BACK, ID_EDIT_UNDO, VIRTKEY,ALT VK_DELETE, ID_EDIT_CUT, VIRTKEY,SHIFT VK_INSERT, ID_EDIT_COPY, VIRTKEY,CONTROL VK_INSERT, ID_EDIT_PASTE, VIRTKEY,SHIFT END

In this example, IDR_MAINFRAME is the accelerator table's resource ID. PRELOAD and MOVEABLE are load options that, like the equivalent keywords in MENU statements, have no effect in the Win32 environment. Each line in the table defines one accelerator. The first entry in each line defines the accelerator key, and the second identifies the corresponding menu item. The VIRTKEY keyword tells the resource compiler that the first entry is a virtual key code, and the keyword following it—CONTROL, ALT, or SHIFT—identifies an optional modifier key. In this example, Ctrl-N is an accelerator for File-New, Ctrl-O is an accelerator for File-Open, and so on. The Edit menu's Undo, Cut, Copy, and Paste functions each have two accelerators defined: Ctrl-Z and Alt-Backspace for Undo, Ctrl-X and Shift-Del for Cut, Ctrl-C and Ctrl-Ins for Copy, and Ctrl-V and Shift-Ins for Paste.

Like menus, keyboard accelerators must be loaded and attached to a window before they'll do anything. For a frame window, LoadAccelTable does the loading and attaching in one step:

LoadAccelTable (MAKEINTRESOURCE (IDR_MAINFRAME));

LoadFrame also does the job nicely. In fact, the same function call that loads the menu also loads the accelerator table if the two resources share the same ID:

LoadFrame (IDR_MAINFRAME, WS_OVERLAPPEDWINDOW, NULL, NULL);

For accelerators to work, the message loop must include a call to the API function ::TranslateAccelerator, as shown here:

while (GetMessage (&msg, NULL, 0, 0)) { if (!TranslateAccelerator (hwnd, hAccel, &msg)) { TranslateMessage (&msg); DispatchMessage (&msg); } }

MFC's CFrameWnd class handles this part for you. Specifically, it overrides the virtual PreTranslateMessage function that it inherits from CWnd and calls ::TranslateAccelerator if it sees an accelerator table has been loaded—that is, if the frame window's m_hAccelTable data member contains a non-NULL accelerator table handle. Not surprisingly, LoadAccelTable loads an accelerator resource and copies the handle to m_hAccelTable. LoadFrame does the same by calling LoadAccelTable.

Accelerators must be handled differently when loaded for nonframe windows that lack the accelerator support in CFrameWnd. Suppose you derive a custom window class from CWnd and want to use accelerators, too. Here's how you'd go about it:

  1. Add an m_hAccelTable data member (type HACCEL) to the derived class.
  2. Early in your application's lifetime, use the API function ::LoadAccelerators to load the accelerator table. Copy the handle returned by ::LoadAccelerators to m_hAccelTable.
  3. In the window class, override PreTranslateMessage and call ::TranslateAccelerator with the handle stored in m_hAccelTable. Use the value returned by ::TranslateAccelerator as the return value for PreTranslateMessage so that the message won't be translated and dispatched if ::TranslateAccelerator has dispatched it already.
  4. Here's how it looks in code:

// In CMainWindow's constructor m_hAccelTable = ::LoadAccelerators (AfxGetInstanceHandle (), MAKEINTRESOURCE (IDR_ACCELERATORS)); // PreTranslateMessage override BOOL CMainWindow::PreTranslateMessage (MSG* pMsg) { if (CWnd::PreTranslateMessage (pMsg)) return TRUE; return ((m_hAccelTable != NULL) && ::TranslateAccelerator (m_hWnd, m_hAccelTable, pMsg)); }

With this framework in place, a CWnd-type window will use accelerators just as a frame window does. Note that accelerators loaded with ::LoadAccelerators (or LoadAccelTable) don't need to be deleted before termination because Windows deletes them automatically.

Using accelerators to provide shortcuts for commonly used menu commands is preferable to processing keystroke messages manually for two reasons. The first is that accelerators simplify the programming logic. Why write WM_KEYDOWN and WM_CHAR handlers if you don't have to? The second is that if your application's window contains child windows and a child window has the input focus, keyboard messages will go to the child window instead of the main window. (Child windows are discussed in Chapter 7.) As you learned in Chapter 3, keyboard messages always go to the window with the input focus. But when an accelerator is pressed, Windows makes sure the resulting WM_COMMAND message goes to the main window even if one of its children has the input focus.

Accelerators are so useful for trapping keystrokes that they're sometimes used apart from menus. If you want to be notified any time the Ctrl-Shift-F12 combination is pressed, for example, simply create an accelerator for that key combination with a statement like this one:

VK_F12, ID_CTRL_SHIFT_F12, VIRTKEY, CONTROL, SHIFT

Then map the accelerator to a class member function by adding an

ON_COMMAND (ID_CTRL_SHIFT_F12, OnCtrlShiftF12)

entry to the message map. Presses of Ctrl-Shift-F12 will thereafter activate OnCtrlShiftF12, even if no menu item is assigned the ID ID_CTRL_SHIFT_F12.

Категории