ZetCode

Windows API 菜单

最后修改于 2023 年 10 月 18 日

在本 Windows API 教程中,我们创建菜单。一个菜单是一组位于菜单栏中的命令。一个菜单栏包含一个菜单列表。菜单可以包含菜单项或其他菜单,这些菜单调用子菜单。执行命令的菜单项称为命令项或命令。在 Windows 中,菜单栏有时被称为顶级菜单;菜单和子菜单被称为弹出菜单。菜单项通常分组到一些逻辑组中。这些组由分隔符分隔。分隔符是一条小的水平线。

一个简单的菜单

在下面的例子中,我们创建了一个菜单栏和三个菜单命令。我们还创建了一个分隔符。

simplemenu.c
#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 函数来设置菜单栏。

A menu example
图:一个简单的菜单

一个弹出菜单

弹出菜单也称为上下文菜单。它是一组在某些上下文下出现的命令列表。例如,在 Firefox 网络浏览器中,当我们右键单击一个网页时,我们会得到一个上下文菜单。在这里我们可以重新加载页面,返回或查看页面源代码。如果我们在工具栏上右键单击,我们会获得另一个用于管理工具栏的上下文菜单。

popupmenu.c
#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 函数销毁。未分配给窗口的菜单必须显式销毁。

A popup menu
图:一个弹出菜单

一个复选菜单项

复选菜单项是在其标签前有一个复选标记的菜单项。可以使用 CheckMenuItem 函数选中或取消选中菜单项。

checkmenuitem.c
#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;

我们在窗口调整大小后调整状态栏的大小以适应窗口。

A check menu item
图:一个复选菜单项

单选菜单项

单选菜单项允许从互斥的选项列表中进行选择。单选菜单项使用 CheckMenuRadioItem 函数进行管理。

radiomenuitem.c
#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");
...

开始时,选择第一个单选项目。

A radio menu item
图:一个单选菜单项

子菜单

子菜单是位于另一个菜单中的菜单。

submenu.c
#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 函数添加到子菜单中。

Submenu
图:子菜单

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