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 Alt
ernate 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:
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):
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)
{
HMENU hMenu = ::CreateMenu();
if (NULL != hMenu)
{
::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:
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:
m_hCreateMenu = ::CreateMenu();
if (m_hCreateMenu != NULL)
{
MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup1\\item1", 1);
MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup2\\item2", 2);
MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup3\\item3", 3);
}
void CAboutDlg::OnCreateMenu()
{
CMenu menu;
menu.Attach(m_hCreateMenu);
SetMenu(&menu);
menu.Detach();
DrawMenuBar();
}
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)
{
HMENU hMenu = ::CreatePopupMenu();
if (NULL != hMenu)
{
::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:
A much better result when used as a popup.
When a popup menu is used in a dialog as a top level menu:
m_hCreatePopupMenu = ::CreatePopupMenu();
if (m_hCreatePopupMenu != NULL)
{
MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup1\\item1", 1);
MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup2\\item2", 2);
MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup3\\item3", 3);
}
void CAboutDlg::OnCreatePopupMenu()
{
CMenu menu;
menu.Attach(m_hCreatePopupMenu);
SetMenu(&menu);
menu.Detach();
DrawMenuBar();
}
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:
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.
CMenu menu;
CMenu *pSub = NULL;
VERIFY(menu.LoadMenu(IDR_PREVIEW_PAGES));
pSub = menu.GetSubMenu(0);
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.
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.
wParam &= 0xfff0;
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.
void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
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);
}
case WM_SYSCOMMAND:
HandleSysCommand(hWnd, message, wParam, lParam);
void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
wParam &= 0xfff0;
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
::MessageBeep(-1);
break;
case 3:
::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 (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:
"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:
- The final level insert of a menu item
- Finding a popup menu (if it exists)
- 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;
ASSERT(itemText.GetLength() > 0);
ASSERT(itemID != 0);
ASSERT(hTargetMenu != NULL);
if (itemText.Find('\\') >= 0)
{
}
else
{
if (::AppendMenu(
hTargetMenu,
MF_STRING,
itemID,
itemText) > 0)
{
bSuccess = true;
}
}
return bSuccess;
}
The first thing we do is check the input parameters to the function with the use of ASSERT
ions. 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:
CString popupMenuName = itemText.Left(itemText.Find('\\'));
CString remainingText =
itemText.Right(itemText.GetLength() -
popupMenuName.GetLength() - 1);
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)
{
TCHAR buffer[MAX_PATH];
::GetMenuString(
hTargetMenu,
itemIndex,
buffer,
MAX_PATH,
MF_BYPOSITION);
if (popupMenuName == buffer)
{
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:
if (bFoundSubMenu)
{
bSuccess = AddMenuItem(
menuItemInfo.hSubMenu,
remainingText,
itemID);
}
else
{
HMENU hPopupMenu = ::CreatePopupMenu();
if (hPopupMenu != NULL)
{
bSuccess = AddMenuItem(
hPopupMenu,
remainingText,
itemID);
if (bSuccess)
{
if (::AppendMenu(
hTargetMenu,
MF_POPUP,
(UINT)hPopupMenu,
popupMenuName) > 0)
{
bSuccess = true;
}
else
{
bSuccess = false;
::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:
bool MenuFunctions::AddMenuItem(
HMENU hTargetMenu,
const CString& itemText,
UINT itemID)
{
bool bSuccess = false;
ASSERT(itemText.GetLength() > 0);
ASSERT(hTargetMenu != NULL);
if (itemText.Find('\\') >= 0)
{
CString popupMenuName = itemText.Left(itemText.Find('\\'));
CString remainingText =
itemText.Right(itemText.GetLength()
- popupMenuName.GetLength() - 1);
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)
{
TCHAR buffer[MAX_PATH];
::GetMenuString(
hTargetMenu,
itemIndex,
buffer,
MAX_PATH,
MF_BYPOSITION);
if (popupMenuName == buffer)
{
bFoundSubMenu = true;
}
}
}
if (bFoundSubMenu)
{
bSuccess = AddMenuItem(
menuItemInfo.hSubMenu,
remainingText,
itemID);
}
else
{
HMENU hPopupMenu = ::CreatePopupMenu();
if (hPopupMenu != NULL)
{
bSuccess = AddMenuItem(
hPopupMenu,
remainingText,
itemID);
if (bSuccess)
{
if (::AppendMenu(
hTargetMenu,
MF_POPUP,
(UINT)hPopupMenu,
popupMenuName) > 0)
{
bSuccess = true;
}
else
{
bSuccess = false;
::DestroyMenu(hPopupMenu);
}
}
}
}
}
else
{
if (itemID != 0)
{
if (::AppendMenu(
hTargetMenu,
MF_BYCOMMAND,
itemID,
itemText) > 0)
{
bSuccess = true;
}
}
else
{
if (::AppendMenu(
hTargetMenu,
MF_SEPARATOR,
itemID,
itemText) > 0)
{
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:
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:
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)
{
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));
dc.SaveDC();
dc.SelectObject(&menuFont);
int maxWidthBeforeTab = 0;
int maxWidthAfterTab = 0;
int itemCount = ::GetMenuItemCount(hMenu);
for (int item = 0 ; item < itemCount ; item++)
{
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)
{
itemWidth += f_popupArrowSize;
}
if (itemInfo.wID == 0)
{
}
else
{
GetMenuString(hMenu, item, menuItemText, _MAX_PATH, MF_BYPOSITION);
CSize textSize;
CString itemText = menuItemText;
itemText.Replace("&", "");
if (itemText.Find("\t") >= 0)
{
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:
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.