Windows API 菜单
最后修改于 2023 年 10 月 18 日
在本 Windows API 教程中,我们创建菜单。一个菜单是一组位于菜单栏中的命令。一个菜单栏包含一个菜单列表。菜单可以包含菜单项或其他菜单,这些菜单调用子菜单。执行命令的菜单项称为命令项或命令。在 Windows 中,菜单栏有时被称为顶级菜单;菜单和子菜单被称为弹出菜单。菜单项通常分组到一些逻辑组中。这些组由分隔符分隔。分隔符是一条小的水平线。
一个简单的菜单
在下面的例子中,我们创建了一个菜单栏和三个菜单命令。我们还创建了一个分隔符。
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void AddMenus(HWND); #define IDM_FILE_NEW 1 #define IDM_FILE_OPEN 2 #define IDM_FILE_QUIT 3 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { MSG msg; WNDCLASSW wc = {0}; wc.lpszClassName = L"Simple menu"; wc.hInstance = hInstance; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(0, IDC_ARROW); RegisterClassW(&wc); CreateWindowW(wc.lpszClassName, L"Simple menu", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 350, 250, 0, 0, hInstance, 0); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_CREATE: AddMenus(hwnd); break; case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: MessageBeep(MB_ICONINFORMATION); break; case IDM_FILE_QUIT: SendMessage(hwnd, WM_CLOSE, 0, 0); break; } break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProcW(hwnd, msg, wParam, lParam); } void AddMenus(HWND hwnd) { HMENU hMenubar; HMENU hMenu; hMenubar = CreateMenu(); hMenu = CreateMenu(); AppendMenuW(hMenu, MF_STRING, IDM_FILE_NEW, L"&New"); AppendMenuW(hMenu, MF_STRING, IDM_FILE_OPEN, L"&Open"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); AppendMenuW(hMenu, MF_STRING, IDM_FILE_QUIT, L"&Quit"); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) hMenu, L"&File"); SetMenu(hwnd, hMenubar); }
两个菜单项发出短促的声音。第三个菜单项终止应用程序。
case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: MessageBeep(MB_ICONINFORMATION); break; case IDM_FILE_QUIT: SendMessage(hwnd, WM_CLOSE, 0, 0); break; } break;
如果我们选择一个菜单项,窗口过程会接收到 WM_COMMAND
消息。菜单项 ID 位于 wParam
值的低位字中。
hMenubar = CreateMenu(); hMenu = CreateMenu();
菜单栏和菜单使用 CreateMenu
函数创建。
AppendMenuW(hMenu, MF_STRING, IDM_FILE_NEW, L"&New"); AppendMenuW(hMenu, MF_STRING, IDM_FILE_OPEN, L"&Open"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); AppendMenuW(hMenu, MF_STRING, IDM_FILE_QUIT, L"&Quit"); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) hMenu, L"&File");
菜单项和子菜单使用 AppendMenuW
函数创建。我们要附加什么取决于标志。MF_STRING
附加标签,MF_SEPARATOR
附加分隔符,MF_POPUP
附加菜单。
SetMenu(hwnd, hMenubar);
最后,我们通过调用 SetMenu
函数来设置菜单栏。

一个弹出菜单
弹出菜单也称为上下文菜单。它是一组在某些上下文下出现的命令列表。例如,在 Firefox 网络浏览器中,当我们右键单击一个网页时,我们会得到一个上下文菜单。在这里我们可以重新加载页面,返回或查看页面源代码。如果我们在工具栏上右键单击,我们会获得另一个用于管理工具栏的上下文菜单。
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); #define IDM_FILE_NEW 1 #define IDM_FILE_OPEN 2 #define IDM_FILE_QUIT 3 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { MSG msg; WNDCLASSW wc = {0}; wc.lpszClassName = L"Popup menu"; wc.hInstance = hInstance; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(0, IDC_ARROW); RegisterClassW(&wc); CreateWindowW(wc.lpszClassName, L"Popup menu", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 350, 250, 0, 0, hInstance, 0); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { HMENU hMenu; POINT point; switch(msg) { case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: MessageBeep(MB_ICONINFORMATION); break; case IDM_FILE_QUIT: SendMessage(hwnd, WM_CLOSE, 0, 0); break; } break; case WM_RBUTTONUP: point.x = LOWORD(lParam); point.y = HIWORD(lParam); hMenu = CreatePopupMenu(); ClientToScreen(hwnd, &point); AppendMenuW(hMenu, MF_STRING, IDM_FILE_NEW, L"&New"); AppendMenuW(hMenu, MF_STRING, IDM_FILE_OPEN, L"&Open"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); AppendMenuW(hMenu, MF_STRING, IDM_FILE_QUIT, L"&Quit"); TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL); DestroyMenu(hMenu); break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProcW(hwnd, msg, wParam, lParam); }
我们有一个上下文菜单的例子,它有三个菜单项。
case WM_RBUTTONUP: point.x = LOWORD(lParam); point.y = HIWORD(lParam); ...
当用户在窗口的客户区中释放鼠标右键时,会发布 WM_RBUTTONUP
消息。lParam
的低位字指定光标的 x 坐标。高位字指定光标的 y 坐标。这些坐标是相对于客户区左上角的。
hMenu = CreatePopupMenu();
CreatePopupMenu
函数创建一个弹出菜单。它返回一个新创建的菜单的句柄。该菜单最初是空的。
ClientToScreen(hwnd, &point);
ClientToScreen
函数将指定点的客户坐标转换为屏幕坐标。我们需要这些坐标来显示上下文菜单。
AppendMenuW(hMenu, MF_STRING, IDM_FILE_NEW, L"&New"); AppendMenuW(hMenu, MF_STRING, IDM_FILE_OPEN, L"&Open"); AppendMenuW(hMenu, MF_SEPARATOR, 0, NULL); AppendMenuW(hMenu, MF_STRING, IDM_FILE_QUIT, L"&Quit");
创建了三个菜单项和一个分隔符。
TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL);
TrackPopupMenu
函数在指定位置显示一个上下文菜单,并跟踪菜单上项目的选择。
DestroyMenu(hMenu);
最后,菜单对象使用 DestroyMenu
函数销毁。未分配给窗口的菜单必须显式销毁。

一个复选菜单项
复选菜单项是在其标签前有一个复选标记的菜单项。可以使用 CheckMenuItem
函数选中或取消选中菜单项。
#include <windows.h> #include <commctrl.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void AddMenus(HWND); #define IDM_VIEW_STB 1 HWND ghSb; HMENU ghMenu; int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { MSG msg; WNDCLASSW wc = {0}; wc.lpszClassName = L"Check menu item"; wc.hInstance = hInstance; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(0, IDC_ARROW); RegisterClassW(&wc); CreateWindowW(wc.lpszClassName, L"Check menu item", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 350, 250, 0, 0, hInstance, 0); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { UINT state; switch(msg) { case WM_CREATE: AddMenus(hwnd); InitCommonControls(); ghSb = CreateWindowExW(0, STATUSCLASSNAMEW, NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) 1, GetModuleHandle(NULL), NULL); break; case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_VIEW_STB: state = GetMenuState(ghMenu, IDM_VIEW_STB, MF_BYCOMMAND); if (state == MF_CHECKED) { ShowWindow(ghSb, SW_HIDE); CheckMenuItem(ghMenu, IDM_VIEW_STB, MF_UNCHECKED); } else { ShowWindow(ghSb, SW_SHOWNA); CheckMenuItem(ghMenu, IDM_VIEW_STB, MF_CHECKED); } break; } break; case WM_SIZE: SendMessage(ghSb, WM_SIZE, wParam, lParam); break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProcW(hwnd, msg, wParam, lParam); } void AddMenus(HWND hwnd) { HMENU hMenubar; hMenubar = CreateMenu(); ghMenu = CreateMenu(); AppendMenuW(ghMenu, MF_STRING, IDM_VIEW_STB, L"&Statusbar"); CheckMenuItem(ghMenu, IDM_VIEW_STB, MF_CHECKED); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) ghMenu, L"&View"); SetMenu(hwnd, hMenubar); }
在这个例子中,我们有一个“视图”菜单,其中有一个菜单项。该菜单项将显示或隐藏状态栏。当状态栏可见时,菜单项被选中。
#define IDM_VIEW_STB 1
这是用于显示或隐藏状态栏的菜单项的 ID。
InitCommonControls();
状态栏是一个通用控件。它必须使用 InitCommonControls
函数进行初始化。
ghSb = CreateWindowExW(0, STATUSCLASSNAMEW, NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) 1, GetModuleHandle(NULL), NULL);
此代码行创建一个状态栏控件。
state = GetMenuState(ghMenu, IDM_VIEW_STB, MF_BYCOMMAND);
我们使用 GetMenuState
函数获取状态栏菜单项的状态。
if (state == MF_CHECKED) { ShowWindow(ghSb, SW_HIDE); CheckMenuItem(ghMenu, IDM_VIEW_STB, MF_UNCHECKED); } else { ShowWindow(ghSb, SW_SHOWNA); CheckMenuItem(ghMenu, IDM_VIEW_STB, MF_CHECKED); }
根据其状态,我们使用 ShowWindow
函数显示或隐藏状态栏控件。菜单项会根据情况使用 CheckMenuItem
函数选中或取消选中。
case WM_SIZE: SendMessage(ghSb, WM_SIZE, wParam, lParam); break;
我们在窗口调整大小后调整状态栏的大小以适应窗口。

单选菜单项
单选菜单项允许从互斥的选项列表中进行选择。单选菜单项使用 CheckMenuRadioItem
函数进行管理。
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void AddMenus(HWND); #define IDM_MODE_MAP 1 #define IDM_MODE_SAT 2 #define IDM_MODE_TRA 3 #define IDM_MODE_STR 4 HMENU hMenu; int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { MSG msg; WNDCLASSW wc = {0}; wc.lpszClassName = L"Radio menu item"; wc.hInstance = hInstance; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(0, IDC_ARROW); RegisterClassW(&wc); CreateWindowW(wc.lpszClassName, L"Radio menu item", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 350, 250, 0, 0, hInstance, 0); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_CREATE: AddMenus(hwnd); break; case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_MODE_MAP: CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_MAP, MF_BYCOMMAND); MessageBeep(MB_ICONERROR); break; case IDM_MODE_SAT: CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_SAT, MF_BYCOMMAND); MessageBeep(0xFFFFFFFF); break; case IDM_MODE_TRA: CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_TRA, MF_BYCOMMAND); MessageBeep(MB_ICONWARNING); break; case IDM_MODE_STR: CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_STR, MF_BYCOMMAND); MessageBeep(MB_ICONINFORMATION); break; } break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProcW(hwnd, msg, wParam, lParam); } void AddMenus(HWND hwnd) { HMENU hMenubar; hMenubar = CreateMenu(); hMenu = CreateMenu(); AppendMenuW(hMenu, MF_STRING, IDM_MODE_MAP, L"&Map"); AppendMenuW(hMenu, MF_STRING, IDM_MODE_SAT, L"&Satellite"); AppendMenuW(hMenu, MF_STRING, IDM_MODE_TRA, L"&Traffic"); AppendMenuW(hMenu, MF_STRING, IDM_MODE_STR, L"Street &view"); CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_MAP, MF_BYCOMMAND); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) hMenu, L"&Map mode"); SetMenu(hwnd, hMenubar); }
在这个例子中,我们有四个单选菜单项;一次只能选择其中一个。每个单选菜单项都会发出不同的声音。
#define IDM_MODE_MAP 1 #define IDM_MODE_SAT 2 #define IDM_MODE_TRA 3 #define IDM_MODE_STR 4
这些是单选菜单项的 ID。
case IDM_MODE_MAP: CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_MAP, MF_BYCOMMAND); MessageBeep(MB_ICONERROR); break;
CheckMenuRadioItem
选中指定的菜单项,并将其设置为单选项目。此外,它会清除关联菜单项组中的所有其他菜单项。该函数的第一个参数是包含菜单项组的菜单的句柄。最后一个参数表示前面三个参数的含义;当指定 MF_BYCOMMAND
时,这些参数是菜单项的 ID。第二个参数是组中第一个菜单项的 ID,第三个参数是组中最后一个菜单项的 ID。第四个参数是要选中的菜单标识符。
... AppendMenuW(hMenu, MF_STRING, IDM_MODE_STR, L"Street &view"); CheckMenuRadioItem(hMenu, IDM_MODE_MAP, IDM_MODE_STR, IDM_MODE_MAP, MF_BYCOMMAND); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) hMenu, L"&Map mode"); ...
开始时,选择第一个单选项目。

子菜单
子菜单是位于另一个菜单中的菜单。
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); void AddMenus(HWND); #define IDM_FILE_NEW 1 #define IDM_FILE_IMPORT 2 #define IDM_IMPORT_MAIL 11 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow) { MSG msg; WNDCLASSW wc = {0}; wc.lpszClassName = L"Submenu"; wc.hInstance = hInstance; wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE); wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(0, IDC_ARROW); RegisterClassW(&wc); CreateWindowW(wc.lpszClassName, L"Submenu", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 350, 250, 0, 0, hInstance, 0); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int) msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_CREATE: AddMenus(hwnd); break; case WM_COMMAND: switch(LOWORD(wParam)) { case IDM_FILE_NEW: MessageBoxW(NULL, L"New file selected", L"Information", MB_OK); break; case IDM_IMPORT_MAIL: MessageBoxW(NULL, L"Import mail selected", L"Information", MB_OK); } break; case WM_DESTROY: PostQuitMessage(0); break; } return DefWindowProcW(hwnd, msg, wParam, lParam); } void AddMenus(HWND hwnd) { HMENU hMenubar = CreateMenu(); HMENU hMenu = CreateMenu(); HMENU hSubMenu = CreatePopupMenu(); AppendMenuW(hMenu, MF_STRING, IDM_FILE_NEW, L"&New"); AppendMenuW(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hSubMenu, L"&Import"); AppendMenuW(hSubMenu, MF_STRING, IDM_IMPORT_MAIL, L"Import &mail"); AppendMenuW(hMenubar, MF_POPUP, (UINT_PTR) hMenu, L"&File"); SetMenu(hwnd, hMenubar); }
在这个例子中,我们有两个菜单项;一个位于“文件”菜单中,另一个位于“文件”的“导入”子菜单中。选择每个菜单项都会导致显示一个消息框。
case IDM_IMPORT_MAIL: MessageBoxW(NULL, L"Import mail selected", L"Information", MB_OK);
当我们选择“导入邮件”子菜单项时,会显示一个消息框,其中包含“导入邮件已选择”文本。
HMENU hSubMenu = CreatePopupMenu();
使用 CreatePopupMenu
函数创建子菜单。
AppendMenuW(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hSubMenu, L"&Import");
使用 AppendMenuW
函数,我们将一个子菜单添加到“文件”菜单中。MF_POPUP
标志用于弹出菜单和子菜单。
AppendMenuW(hSubMenu, MF_STRING, IDM_IMPORT_MAIL, L"Import &mail");
菜单项通常使用 AppendMenuW
函数添加到子菜单中。

在本 Windows API 教程中,我们介绍了菜单。