65.9K
CodeProject 正在变化。 阅读更多。
Home

从初学者角度看菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (68投票s)

2004年6月23日

CPOL

30分钟阅读

viewsIcon

348287

downloadIcon

3139

从初学者的角度讨论操作系统菜单对象。我将带你从基础知识到更深入地理解菜单以及如何从代码中与它们进行交互。

目录

引言

在我研究一篇文章(一个属主绘制菜单插件)的过程中,我需要更多地了解菜单以及它们的工作原理。我认为这是一个编写初学者教程的理想主题。我还将所有与菜单相关的消息和可用于操作菜单的函数收集到一起,因为它们在现有文档中分散得很厉害——如果我遗漏了什么,请告诉我。

因此,本文旨在让您——无论您的能力水平如何——更好地理解菜单以及您的代码/应用程序将如何与它们进行交互。我将从基础知识开始,然后在此基础上进行讲解,在此过程中,我将介绍更高级的函数(以及它们的开发过程),以便您能够操作菜单。本指南并非详尽无遗。我还将探讨标准WIN32应用程序和MFC应用程序中菜单的区别。

免责声明

一个小小的声明。此处提供的信息可能不完整或不完全准确。这是我多年来对菜单的了解以及近期研究的结果。虽然我可能不完全准确,但此处提供的所有代码都经过了高度测试,在大多数情况下(如果不是全部)都应该是可靠的,但您仍需自行承担使用风险。

什么是菜单?

好了,再基础不过了。菜单是Windows中标准的 UI 工具,允许用户从一系列选项中选择一项。它允许将这些选项分组到相关功能类别中(例如,“文件”菜单),提供快捷信息(快捷键),并显示命令的可用性(项目禁用/启用)。

菜单与其他 Windows UI 对象不同。首先,它们不像编辑控件、列表框和其他标准 Windows 类型那样实现为窗口。这意味着它们无法以常规方式被覆盖。CP 上有很多关于如何实现属主绘制菜单的文章,因此,除了样式问题,这也不是我将在本文中涵盖的领域(呼!)。

菜单通常仅在用户从中进行选择所需的时间内显示。有一个例外,那就是应用程序或对话框的顶层菜单。在这样的顶层菜单中,会显示可用的菜单类别/选项。

键盘快捷键

请注意,大多数顶层菜单项名称下方都带有“_”(下划线)字符。这是使用键盘上的 Alt 键通过键盘访问菜单的快捷方式。这些快捷键是在定义菜单项文本时使用“&”字符设置的,例如“&File”。

通过使用这些顶层菜单,用户可以通过鼠标或键盘界面与任何选项进行交互(例如,Alt + F 将快速打开“文件”菜单)。

弹出菜单也可以有快捷键

Popup menu with keyboard shortcuts

在这种情况下,一旦显示弹出菜单,用户就可以按下带下划线的键来快速选择该项。在上例中,用户可以按下 P 来启动“打印”菜单选项。

Windows XP 中的一个最新发展是,下划线字符仅在按下 Alt 键时显示。如果菜单是通过鼠标打开的,则不显示它们。我认为这是一个倒退,但微软并没有就设计咨询我。幸运的是,这是用户可配置的。

菜单项的实际键盘快捷键(例如,Ctrl + P)可以在不显示菜单的情况下由用户选择,但必须正确地将正确的按键组合映射到正确的菜单命令 ID。这些快捷键是单独的主题。

项目选择提示

大多数(如果不是全部)Windows 程序中的一项功能是,当用户突出显示菜单中的某一项时,菜单命令的描述会显示在应用程序的 *状态栏* 中(如果它有状态栏)。

Menu item prompt in status bar

稍后我们将看到 MFC 应用程序如何实现这一点。

顶层菜单项顺序

Microsoft 的标准用户界面协议要求顶层菜单遵循标准布局。通常,第一个菜单允许访问文档和文件,倒数第二个是“窗口”菜单,最后一个是“帮助”菜单。遵循此标准布局可让所有程序向用户提供一致的界面,并且在设计您自己的应用程序的菜单布局时应遵循此规则。偏离此规则将自行承担风险并惹恼您的用户。

从程序员的角度看菜单

从程序员的角度看菜单,菜单或 HMENU 是一个系统 OS 对象,您通过其 HMENU HANDLE 值与之交互。OS 提供了一系列函数,允许您操作和创建这些类型的对象,因为您无法直接操作菜单对象。它还通过消息提供了一个接口,以便添加高级功能。MFC 提供了相同的访问函数,尽管它使用了一个包装类 CMenu,该类持有 HMENU 句柄。

系统提供的菜单函数

首先,让我们看一下系统提供的原始 WIN32 函数,以便您操作菜单。对于 WIN32 函数适用的任何内容,对于 CMenu 包装类中的 MFC 版本函数也同样适用。此处按相关功能对函数进行分组。

我不会在此处检查所有菜单函数。

菜单创建

在这里,我将快速检查菜单创建命令,并尝试使用每个命令的产物作为顶层菜单和弹出菜单。我在这里会跳过一些菜单修改命令,但稍后我们会看到它们是如何工作的。

  • HMENU hMenu = ::CreateMenu()

    此命令用于创建新的 *顶层菜单对象*。此新菜单最初是空的,可以使用稍后列出的“菜单修改”命令进行填充。除非由窗口拥有(正在使用或已选择到该窗口中),否则创建的任何菜单都需要在使用完毕后销毁,因为这些窗口会自动销毁它们使用的任何菜单。有关其他信息和函数,请参阅后面的“菜单销毁”部分。

    CreateMenu() 示例代码

    void CMenuView::OnLButtonDown(UINT nFlags, CPoint point) 
    {
        // display a menu created using CreateMenu()
        HMENU hMenu = ::CreateMenu();
        if (NULL != hMenu)
        {
            // add a few test items
            ::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);
        }
    }

    上面的示例代码给了我们一个看起来像这样的菜单:

    CreateMenu() popup result

    不太好!我们的选项在哪里?CreateMenu() 用于创建顶层菜单,因此最好与对话框或主窗口一起使用。

    当使用 CreateMenu() 创建的菜单作为顶层菜单使用时

    // code extracts
        m_hCreateMenu = ::CreateMenu();
        if (m_hCreateMenu != NULL)
        {
            // see later for the implementation of this function
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup1\\item1", 1);
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup2\\item2", 2);
            MenuFunctions::AddMenuItem(m_hCreateMenu, "Popup3\\item3", 3);
        }
    
    void CAboutDlg::OnCreateMenu() 
    {
        // switch to the CreateMenu created menu
        CMenu    menu;
        menu.Attach(m_hCreateMenu);
        SetMenu(&menu);
        menu.Detach();            // stop destructor destroying it
        DrawMenuBar();            // make sure UI is upto date
    }

    CreateMenu() used as a top level menu

    看起来不错。这就是我们需要的。

  • HMENU hMenu = ::CreatePopupMenu()

    此函数与 CreateMenu() 在细微之处有所不同。使用 CreateMenu 创建的 HMENU 会水平显示选项,而这些将垂直显示。像这样创建的弹出菜单也可以插入到其他菜单中。因此,当您需要添加新的弹出菜单时,这就是要使用的函数。

    CreatePopupMenu() 示例

    void CMenuView::OnRButtonDown(UINT nFlags, CPoint point) 
    {
        // display a menu created using CreateMenu()
        HMENU hMenu = ::CreatePopupMenu();
        if (NULL != hMenu)
        {
            // add a few test items
            ::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);
        }
    }

    上面的示例代码给了我们一个看起来像这样的菜单:

    CreatePopupMenu() popup result

    用作弹出菜单时,效果好多了。

    当弹出菜单在对话框中用作顶层菜单时

    // code extract
        m_hCreatePopupMenu = ::CreatePopupMenu();
        if (m_hCreatePopupMenu != NULL)
        {
            // see later for the implementation of this function
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup1\\item1", 1);
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup2\\item2", 2);
            MenuFunctions::AddMenuItem(m_hCreatePopupMenu, "Popup3\\item3", 3);
        }
    
    void CAboutDlg::OnCreatePopupMenu() 
    {
        // switch to the CreateMenu created menu
        CMenu    menu;
        menu.Attach(m_hCreatePopupMenu);
        SetMenu(&menu);
        menu.Detach();            // stop destructor destroying it
        DrawMenuBar();            // make sure the UI is upto date
    }

    CreatePopupMenu() used as a top level menu

    看起来太糟糕了!弹出菜单会垂直显示。因此,顶层菜单必须通过调用 CreateMenu() 来创建,而弹出菜单应通过调用 CreatePopupMenu() 来创建。

    微软为何在顶层菜单和弹出菜单之间做出这样的区分,这很奇怪。如果只有一个函数可以用于这两种方法,那么生成的界面会干净得多,会更受欢迎。但这就是我们拥有的。 :(

  • AppendMenu(HMENU, UINT, UINT_PTR, LPCTSTR)

    用于向现有菜单添加新菜单项或弹出菜单

    参数

    • HMENU - 要添加到的菜单的句柄。
    • UINT - 关于要添加的菜单项的标志。请参阅后面的“标志”部分。
    • UINT_PTR - 选项被选中时返回的 ID,或添加弹出菜单时新弹出菜单的 HMENU
    • LPCTSTR - 显示的菜单或弹出菜单项的文本。

    添加的项会放置在 HMENU 的底部/末尾。

  • InsertMenu(HMENU, UINT, UINT, UINT_PTR, LPCTSTR)

    将菜单项或弹出菜单添加到现有菜单的特定位置。

  • LoadMenu(HINSTANCE, LPCTSTR)

    加载菜单资源并返回其 HMENU 句柄,如果失败则返回 NULL

  • LoadMenuIndirect(CONST MENUTEMPLATE*)
  • DrawMenuBar(HWND)

    如果更改了正在被主窗口或对话框使用的菜单,则需要调用此函数。基本上,它会使窗口重新绘制顶层菜单,以便正确绘制任何更改。请注意,您传递的是要绘制的菜单所属窗口的 HWND,而不是 HMENU

    菜单修改

  • CheckMenuItem(HMENU, UINT, UINT)

    修改给定项以显示/隐藏项旁边的检查(勾选)标记。通常用于指示某个选项已启用或禁用。

  • EnableMenuItem(HMENU, UINT, UINT)

    在 WIN32 中,如果您想设置菜单项的启用/禁用状态,则此函数将为您完成此操作。

  • SetMenuDefaultItem(HMENU, UINT, UINT)

    突出显示给定菜单项并使其成为默认选择项。

  • ModifyMenu(HMENU, UINT, UINT, UINT_PTR, LPCTSTR)

    允许您更改/修改给定菜单项的文本/状态。

  • SetMenuContextHelpId(HMENU, DWORD)
  • CheckMenuRadioItem(HMENU, UINT, UINT, UINT, UINT)
  • SetMenuItemBitmaps(HMENU, UINT, UINT, HBITMAP, HBITMAP)
  • RemoveMenu(HMENU, UINT, UINT)

    此函数用于从给定菜单中删除特定项。

    参数

    • HMENU hMenu - 包含要移除项的菜单的句柄。
    • UINT uPosition - 位置或菜单命令 ID 标识符。
    • UINT uFlags - uPositionMF_BYCOMMAND(ID)还是 MF_BYPOSITION

    菜单查询函数

  • GetMenuItemCount(HMENU)

    此函数返回此 HMENU 对象中的项数。弹出项被视为单独的项,需要单独枚举。

        int itemCount = ::GetMenuItemCount(hMenu);
  • GetMenuItemID(HMENU, int)

    仅用于通过位置获取菜单项的 ID(命令)索引。

  • GetMenuState(HMENU, UINT, UINT)
  • GetMenuString(HMENU, UINT, LPTSTR, int, UINT)

    通过位置或 ID 返回给定菜单项的菜单文本。

  • GetMenuItemInfo(HMENU, int, BOOL, LPMENUITEMINFO)

    您将使用此函数来查询特定的 HMENU 项。这可以返回菜单项的文本、它是否为弹出菜单、其命令 ID 号等信息。

    参数

    • HMENU - 我们需要从中获取信息的菜单的句柄。
    • int - 项 ID 或位置 ID,取决于……
    • BOOL - 我们是按 MF_BYPOSITIONTRUE)还是按 MF_BYCOMMANDFALSE)获取菜单项信息。
    • LPMENUITEMINFO - 一个指向已初始化以指示我们要检索有关菜单项何种信息的 MENUITEMINFO 对象的指针。
  • GetSubMenu(HMENU, int)

    此函数用于 *通过位置* 从另一个菜单中提取(获取)弹出菜单的 HMENU。过去,我通常有一个大的菜单资源,其中包含我的应用程序所需的所有弹出/上下文菜单。菜单中的每个项都使用 enum 定义了其名称和位置,因此我可以在代码中提取正确的上下文菜单,如下所示:

        // MFC example
        CMenu    menu;
    
        if (menu.LoadMenu(IDR_MY_CONTEXT_MENUS))
        {
            CMenu *pContextMenu = menu.GetSubMenu(cm_RequiredContextMenu);
            pContextMenu->TrackPopupMenu(...);
        }

    参数

    • HMENU - 我们需要从中提取弹出菜单的菜单句柄。
    • int - 弹出菜单在菜单中的位置
  • GetMenuContextHelpId(HMENU)

    菜单销毁

  • DestroyMenu(HMENU)

    导致系统销毁给定的菜单资源。给定 HMENU 的任何弹出菜单也会被销毁。

  • DeleteMenu(HMENU, UINT, UINT)

    此函数与 RemoveMenu() 函数的工作方式完全相同。参数相同。

菜单消息

并非涵盖所有单独的消息 - 只包含您最可能使用的。

当菜单处于使用状态时,您的应用程序可以接收的可能消息包括:

WM_INITMENUPOPUP

此消息由 OS 发送给即将显示的菜单的所有者窗口。这给了所有者一个配置菜单及其项外观的机会。您可以在此时添加/修改菜单并设置每个单独项的状态。所有这些都将在菜单显示之前完成。

在 MFC 中,此消息由主窗口菜单项的 CFrameWnd 处理。该过程扫描每个菜单项,并通过框架发出 ON_UPDATE_COMMAND_UI 调用。这允许标准的 MFC 架构查询您的应用程序有关每个菜单项状态的信息。

WM_UNINITMENUPOPUP

当弹出菜单在使用后隐藏时(无论是已做出选择,还是不再需要显示的弹出菜单被隐藏),会发送此消息。

WM_MENUCHAR

当菜单处于活动状态且用户按下不对应任何助记符或快捷键的键时,会向所有者发送 WM_MENUCHAR 消息。此消息发送给菜单的所有者窗口。

WM_MENUCOMMAND

当用户从菜单中做出选择时,会发送 WM_MENUCOMMAND 消息。

WM_MENUDRAG

当用户拖动菜单项时,会向拖放菜单的所有者发送 WM_MENUDRAG 消息。

WM_MENUGETOBJECT

当鼠标光标进入菜单项,或从项的中心移动到项的顶部或底部时,会将 WM_MENUGETOBJECT 消息发送给拖放菜单的所有者。

WM_MENURBUTTONUP

当用户在菜单项上释放鼠标右键时,会发送 WM_MENURBUTTONUP 消息。

WM_MENUSELECT

每当用户更改正在显示的菜单中突出显示(选定)的项时,都会将此消息发送给所有者窗口。WPARAM 字段包含突出显示的菜单项的 ID。通常,应用程序会处理此消息以在状态栏或等效位置显示命令的更详细描述。

WM_SYSCOMMAND

当用户从系统菜单中进行选择时,会将此消息发送给菜单的所有者窗口。有关更多信息,请参阅后面的“窗口/系统菜单”部分。

WM_COMMAND

当用户从您的菜单中选择一个菜单项时,所选命令 ID 将在 WPARAM 字段中发送到您的窗口。这允许您为命令采取正确的操作。

TrackPopupMenu() 和 TrackPopupMenuEx()

这两个函数对于显示弹出菜单以允许用户快速选择非常有用。通常用于上下文菜单,当您第一次遇到它时,其行为可能有些奇怪。根据传递的哪个窗口的 HWND 作为菜单的所有者,取决于 MFC 中相关的菜单选项的 ON_UPDATE_COMMAND_UI 处理程序是否被调用,或者即使使用 TPM_NONOTIFY 选项,是否调用了它们或 ON_COMMAND 处理程序。

使用这些函数时,TPM_RETURNCMD 是一个很好的标志,因为它允许您将所有实际执行菜单命令的代码集中在一个位置,而不是(至少在 MFC 中)通过每个选项的 ON_COMMAND 处理程序进行处理。因为 TrackPopupMenu() 函数本身返回所选选项的命令 ID,如果菜单在没有选择选项的情况下中止,则返回 0

    // some typical menu code using the TPM_RETURNCMD option:
    CMenu menu;
    CMenu *pSub = NULL;
    
    // popup a menu to get the number of pages to display
    VERIFY(menu.LoadMenu(IDR_PREVIEW_PAGES));
    pSub = menu.GetSubMenu(0);
    
    // NOTE : If you need to enable or disable the menu items
    //  in this list based on the number of pages in your printout,
    // you can either do it here before the menu is displayed, or write a
    // handler for the WM_INITMENUPOPUP message and configure the
    // enabled/disabled state at that point.
    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;
    }

菜单函数标志

大多数(如果不是全部)菜单修改函数都使用可以作为参数传递的标志。我在这里列出了我在文档中找到的所有标志及其功能:

  • MF_BYCOMMAND - 指定 ID 是命令 ID 而不是菜单中的位置索引。
  • MF_BYPOSITION - 指定 ID 是菜单中的位置索引而不是命令 ID。
  • MF_BITMAP - 使用位图作为菜单项。
  • MF_DISABLED - 禁用菜单项,使其无法被选中,但该标志不会使其变灰。
  • MF_ENABLED - 启用菜单项,使其可以被选中,并将其从灰色状态恢复。
  • MF_GRAYED - 禁用菜单项并将其变灰,使其无法被选中。
  • MF_MENUBARBREAK - 对于菜单栏,其功能与 MF_MENUBREAK 标志相同。对于下拉菜单、子菜单或快捷菜单,新列将与旧列用一条垂直线分隔。
  • MF_MENUBREAK - 将项放在新行(对于菜单栏)或新列(对于下拉菜单、子菜单或快捷菜单)上,而不分隔列。
  • MF_OWNERDRAW - 指定该项是属主绘制项。在菜单首次显示之前,拥有菜单的窗口会收到一个 WM_MEASUREITEM 消息(针对每个属主绘制项),以获取菜单项的宽度和高度。然后,当菜单项的外观需要更新时,会将 WM_DRAWITEM 消息发送给属主窗口的过程。
  • MF_POPUP - 指定菜单项打开一个下拉菜单或子菜单。此标志用于向菜单栏添加菜单名称,或向下拉菜单、子菜单或快捷菜单添加一个打开子菜单的菜单项。
  • MF_SEPARATOR - 绘制一条水平分隔线。此标志仅用于下拉菜单、子菜单或快捷菜单。该线不能被灰色、禁用或突出显示。
  • MF_STRING - 指定菜单项是一个文本字符串。
  • MF_UNCHECKED - 不在项旁边显示复选标记(默认)。如果应用程序提供了复选标记位图(参见 SetMenuItemBitmaps),则此标志会在菜单项旁边显示清除位图。
  • MF_CHECKED - 在菜单项旁边显示复选标记。如果应用程序提供了复选标记位图(参见 SetMenuItemBitmaps),则此标志会在菜单项旁边显示复选标记位图。
  • MF_USECHECKBITMAPS -
  • MF_DEFAULT - 可能使该项成为默认选择。参见 SetMenuDefaultItem()
  • MF_MOUSESELECT - 该项是通过鼠标选择的。
  • MF_SYSMENU - 该项包含在控制菜单中。
  • MF_HELP - 可能在顶层菜单中右对齐一项(例如,旧版 3.1 应用程序中的帮助菜单等是右对齐的)。
  • MF_RIGHTJUSTIFY - 功能与 MF_HELP 相同,因为它们在 Windows 头文件中定义的值相同。
  • MF_END - 用于从资源加载菜单时,但现已废弃。

其中一些标志将在消息发送给您时使用(例如,MF_MOUSESELECT),而另一些则用于设置菜单项的属性。

以下标志组不能一起使用:

  • MF_BITMAPMF_STRINGMF_OWNERDRAW
  • MF_CHECKEDMF_UNCHECKED
  • MF_DISABLEDMF_ENABLEDMF_GRAYED
  • MF_MENUBARBREAKMF_MENUBREAK

窗口/系统菜单

这个菜单多年来经历了多次身份危机。目前被称为“*窗口*”菜单,过去在文档中也曾被称为“*系统*”或“*控制*”菜单,也可能被这样称呼。我在这里称之为系统菜单。

The System menu

任何对话框或主应用程序窗口都可以/通常包含一个系统菜单。这是一个可以通过窗口的图标(显示在窗口左上角)访问的菜单。单击该图标会显示此系统菜单,作为弹出菜单。通过此菜单选择的菜单选项作为 WM_SYSCOMMAND 消息发送。

访问 WIN32 中的系统菜单

您可以使用 WIN32 函数 ::GetSystemMenu(HWND hWnd, BOOL bRestore) 来获取应用程序或对话框的系统菜单的 HMENU。此函数根据 bRestore 参数的值可以执行两项操作:

  • FALSE - 函数返回应用程序正在使用的系统菜单的实际 HMENU
  • TRUE - 函数将您的窗口的系统菜单重置为默认状态(即,删除您可能对其进行的任何修改)并返回 NULL。传递 TRUE 时,您无法获得 HMENU

现在,让我们继续讨论 WM_SYSCOMMAND 消息,当您从系统菜单中进行选择时,此消息会发送给您。WIN32 中此消息的原型为:

  • WIN32: LRESULT CALLBACK WindowProc(HWND, UINT = WM_SYSCOMMAND, WPARAM, LPARAM)

    MSDN 对 WIN32 中的此消息是这样说的:

    WM_SYSCOMMAND 消息中,WPARAM 参数的四位低位由系统内部使用。为了在测试 WPARAM 值时获得正确的结果,应用程序必须使用按位 AND 运算符将值 0xFFF0WPARAM 值组合起来。

        // example clearing low order bits
        wParam &= 0xfff0;    // clear system used bits for correct comparision

    我创建了一个测试应用程序来检查这一点。使用 VC6,我创建了一个“*WIN32 应用程序*”类型的项目,子选项类型为“*一个典型的“Hello world”应用程序*”。这提供了标准 WIN32 应用程序的样板代码。从这里,我为 WM_SYSCOMMAND 消息插入了一个处理程序,并测试了每个标准的系统菜单选项。我还添加了一个 ID 为 3 的新菜单选项,该选项在使用 0xFFF0 掩码时可能会出现问题。

    // This is the extra code added to the basic template. You can also find
    // this in the test application
    
    // a function used to handle the WM_SYSCOMMAND message (forward declaration)
    void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    
    // added at end of InitInstance
    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);
    }
    
    // The WndProc switch entry inserted just before the default: option
            case WM_SYSCOMMAND:
                HandleSysCommand(hWnd, message, wParam, lParam);
                // fall through to the default handler as well
    
    // the function to handle WM_SYSCOMMAND
    void HandleSysCommand(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        wParam &= 0xfff0;    // clear system used bits
        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
            // beep to show we recognised the message
            ::MessageBeep(-1);
            break;
        case 3:
            // handle our inserted System menu command
                ::MessageBox(hWnd, 
                        "My System menu command", 
                        "WM_SYSCOMMAND", 
                        MB_OK);
                break;
        default:
            ::MessageBox(hWnd, 
                    "Unrecognised System command message", 
                    "WM_SYSCOMMAND", 
                    MB_OK);
            break;
        }
    }

    请注意 case 3: 选项,它应该处理我们插入的菜单命令。当应用程序运行时,并且选择了菜单选项,实际运行的是 default: 条目,因为应用的 wParam 掩码会丢失我们的菜单命令 ID!这意味着您需要保留 ID 的副本,并使用掩码检查标准的 SC_... 消息。如果不是那些消息之一,您需要根据未掩码的 wParam 值进行 switch

    我们修改后的过程如下所示:

    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 of the unmasked wParam value
            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;
            }
        }
    }

    这一个确实按宣传的那样工作。所以,似乎您确实必须使用 0xFFF0 掩码,*但仅用于 SC_... 消息,而不用于您自己添加的菜单选项*。

    系统菜单的奇怪之处

    系统菜单包含这些标准的菜单命令:

    • SC_RESTORE - 将窗口恢复到正常(非最大化)位置和大小。
    • SC_MOVE - 允许用户移动窗口。
    • SC_SIZE - 允许用户调整窗口大小。
    • SC_MINIMIZE - 最小化窗口。
    • SC_MAXIMIZE - 最大化窗口。
    • SC_CLOSE - 选择关闭选项。

    但是,如果您查看上面的函数,我们会发现列出了许多其他 SC_... 命令。基本上,Windows OS 为用户可以对应用程序窗口执行的各种操作(例如,使用鼠标更改窗口大小,或单击应用程序的标题栏)提供了快捷方式。所有这些操作都会生成 WM_SYSCOMMAND 消息,并将相关的 SC_... 代码放入消息的 WPARAM 参数中,因此可以在不先显示系统菜单的情况下接收 WM_SYSCOMMAND 消息。

    其他 SC_... 消息包括:

    • SC_MOUSEMENU - 在包含系统菜单的菜单即将显示时接收。
    • SC_NEXTWINDOW - 移至下一个窗口。
    • SC_PREVWINDOW - 移至上一个窗口。
    • SC_VSCROLL - 垂直滚动。
    • SC_HSCROLL - 水平滚动。
    • 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)

    WIN32 中的 WPARAM 或 MFC 中的 UINT 可以包含这些命令代码。

    通常,您可以允许对系统菜单选项的默认处理来为您处理这些情况。

更多高级菜单功能

在本节中,我希望介绍一套新的函数来帮助操作菜单对象。我将展示我是如何开发这些函数的,并提供最终经过测试的函数。

向现有菜单添加动态菜单项

当您想向现有的菜单资源添加项时,您需要能够处理未知的弹出菜单结构。我们需要以有条理的方式迭代此结构,并在存在的情况下在适当的级别插入新菜单项,或按需添加此新级别。经过初步考虑,我决定对这个问题使用递归方法。我的函数主要针对 MFC,但可以轻松地改编为支持纯 WIN32。

让我们看一些新项的示例,我们的过程可能会遇到:

    // in these examples, I am using the '\' character to delimit different
    // popup/menu items internally in a string
    
    // example new menu items
    "My menu item"
    "Popup name\menu item name"
    "Popup name\next popup name\Additional popup names....\menu item name"

因此,菜单项可以有 *0 个或多个* 弹出级别和项名。这意味着我们可以搜索现有菜单结构以查找每个弹出名称。如果它存在,我们就递归地处理这个新的弹出菜单。我们继续这样做,直到到达一个菜单项名称,该名称将被插入。我还定义了一个分隔符字符 '\',用于分割弹出菜单名称和最终项名称。其效果是,任何菜单项/弹出项文本中都不能包含 '\' 字符。如果您需要该字符,则需要在最终提供的过程中对其进行更改。

因此,我们的函数的伪代码如下:

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
}

开发代码

让我们首先将编码分解为 3 个阶段:

  1. 菜单项的最终级别插入
  2. 查找弹出菜单(如果存在)
  3. 添加到/创建弹出菜单

让我们看看我们如何处理这些阶段,并提供实现此目的的代码。

  • 第一阶段:处理直接插入操作

    当有一个项没有需要找到/创建的弹出菜单级别时,我们只需要直接插入该项。让我们看看这段代码看起来像什么:

    bool MenuFunctions::AddMenuItem(HMENU hTargetMenu, 
                          const CString& itemText, UINT itemID)
    {
        bool bSuccess = false;
    
        // debug checks for correct calling of function
        ASSERT(itemText.GetLength() > 0);
        ASSERT(itemID != 0);        // menu items must have non-0 IDs
        ASSERT(hTargetMenu != NULL);
    
        // first, does the menu item have any
        // required submenus to be found/created?
        if (itemText.Find('\\') >= 0)
        {
            // code yet to be written to handle popups
        }
        else
        {
            // no sub menus required,
            // add this item to this HMENU
            if (::AppendMenu(
                    hTargetMenu, 
                    MF_STRING, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the item to the menu
                bSuccess = true;
            }
        }
        return bSuccess;
    }

    我们首先使用 ASSERTion 检查函数的输入参数。这将允许我们在调试模式下捕获任何无效的函数调用,并且还会为您提供有关进入函数时参数应处于何种状态的信息。

    接下来,我们使用“'\'”子菜单分隔符检查菜单项字符串。如果找不到,我们就知道我们处于项插入级别,并且不必导航任何子菜单级别。我们直接调用 AppendMenu 函数,并将菜单项添加到传入的 hTargetMenu 的末尾。

  • 第二阶段:查找弹出菜单(如果存在)

    如果我们有任何需要应用的弹出菜单级别,那么我们需要搜索传入的 HMENU,看看我们需要的弹出菜单是否已存在。让我们看看我们需要什么代码:

            // 1:get the popup menu name
            CString popupMenuName = itemText.Left(itemText.Find('\\'));
    
            // 2:get the rest of the menu item name
            // minus the delimiting '\' character
            CString remainingText = 
               itemText.Right(itemText.GetLength() - 
               popupMenuName.GetLength() - 1);
    
            // 3:See whether the popup menu already exists
            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)
                {
                    // this menu item is a popup
                    // menu (non popups give 0)
                    // get the popup menu items name
                    TCHAR    buffer[MAX_PATH];
                    ::GetMenuString(
                            hTargetMenu, 
                            itemIndex, 
                            buffer, 
                            MAX_PATH, 
                            MF_BYPOSITION);
                    if (popupMenuName == buffer)
                    {
                        // this is the popup menu
                        // we have to add to
                        bFoundSubMenu = true;
                    }
                }
            }

    我们首先从菜单项文本的前面提取到“'\'”字符为止的弹出菜单名称。然后,我们获取剩余的菜单文本,不包括弹出菜单名称和分隔符字符,因为这些信息对于递归调用将是必需的。获取弹出菜单名称后,我们需要搜索我们所在的菜单,以查看具有该名称的弹出菜单是否已存在。

    首先,我们使用 ::GetMenuItemCount(HMENU) 获取菜单中的项数。我们在循环中使用 ::GetMenuItemInfo() 函数逐个获取菜单中每个项的信息。请注意第三个参数“TRUE”,这意味着我们是通过 MF_BYPOSITION 而不是 MF_BYCOMMAND 获取菜单信息的。MENUITEMINFO 结构中设置的标志将在 hSubMenu 字段中返回信息。这是我们感兴趣的字段,因为如果它返回非零值,则该项是弹出菜单,我们需要检查其名称以查看是否是我们想要的。如果是弹出菜单(hSubMenu != 0),则我们使用 ::GetMenuString() 提取此弹出菜单项的文本名称,并将其与我们正在查找的弹出名称进行比较。如果它是正确的,我们就设置一个标志(bFoundSubMenu)并退出循环,以便我们可以在递归函数调用中使用 menuItemInfo.hSubMenu 参数。

    如果我们在 itemIndex 循环结束时 bFoundSubMenu 仍为 true,则表明不存在具有所需名称的弹出菜单,并且我们将不得不自己创建它并将其追加到我们的菜单末尾。

  • 第三阶段:添加到/创建弹出菜单

    我们的过程的最后阶段要求我们对 menuItemInfo.hSubMenu 进行递归调用,或创建一个新的弹出菜单进行递归调用。让我们看看我们需要什么代码:

            // 4: If exists, do recursive call,
            // else create and do a recursive call and then insert it
            if (bFoundSubMenu)
            {
                bSuccess = AddMenuItem(
                        menuItemInfo.hSubMenu, 
                        remainingText, 
                        itemID);
            }
            else
            {
                // we need to create a new sub menu and insert it
                HMENU hPopupMenu = ::CreatePopupMenu();
                if (hPopupMenu != NULL)
                {
                    bSuccess = AddMenuItem(
                            hPopupMenu, 
                            remainingText, 
                            itemID);
                    if (bSuccess)
                    {
                        if (::AppendMenu(
                                hTargetMenu, 
                                MF_POPUP, 
                                (UINT)hPopupMenu, 
                                popupMenuName) > 0)
                        {
                            bSuccess = true;
                            // hPopupMenu now owned by hTargetMenu,
                            // we do not need to destroy it
                        }
                        else
                        {
                            // failed to insert the popup menu
                            bSuccess = false;
                            // stop a resource leak
                            ::DestroyMenu(hPopupMenu);
                        }
                    }
                }
            }

    如果弹出菜单存在,我们尝试将 remainingText 添加到该弹出菜单中。递归调用将处理我们所需的任何其他弹出菜单级别的导航。如果弹出菜单不存在,我们使用 CreatePopupMenu() 函数创建一个,并使用 remainingText 对其进行递归调用。这还将处理创建其他弹出菜单的情况。在任何进一步的递归成功完成后,我们将弹出菜单追加到传入的菜单中。请注意 MF_POPUP 样式,它使 AppendMenu 将参数 3(UINT itemID)视为 HMENU 而不是项 ID。我们还为弹出菜单指定了将显示的名称。如果未能追加弹出菜单,我们会销毁它以避免资源泄漏。

最终函数 - 差不多了

我们这里的代码运行良好,除了一个问题。我们没有处理插入分隔符。我们需要在项插入级别添加一个特殊情况。由于菜单项的 ID 不能为 0(对于 TrackPopupMenu,返回 0 表示菜单被中止),我们可以使用此 ID 号在最终插入操作中区分常规菜单项和分隔符。因此,最终函数如下所示:

// this is a recursive function which will attempt
// to add the item "itemText" to the menu with the
// given ID number. The "itemText" will be parsed for
// delimiting "\" characters for levels between
// popup menus. If a popup menu does not exist, it will
// be created and inserted at the end of the menu
// itemID of 0 will cause a separator to be added
bool MenuFunctions::AddMenuItem(
        HMENU hTargetMenu, 
        const CString& itemText, 
        UINT itemID)
{
    bool bSuccess = false;

    ASSERT(itemText.GetLength() > 0);
    ASSERT(hTargetMenu != NULL);

    // first, does the menu item have
    // any required submenus to be found/created?
    if (itemText.Find('\\') >= 0)
    {
        // yes, we need to do a recursive call
        // on a submenu handle and with that sub
        // menu name removed from itemText

        // 1:get the popup menu name
        CString popupMenuName = itemText.Left(itemText.Find('\\'));

        // 2:get the rest of the menu item name
        // minus the delimiting '\' character
        CString remainingText = 
            itemText.Right(itemText.GetLength() 
                   - popupMenuName.GetLength() - 1);

        // 3:See whether the popup menu already exists
        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)
            {
                // this menu item is a popup menu (non popups give 0)
                TCHAR    buffer[MAX_PATH];
                ::GetMenuString(
                        hTargetMenu, 
                        itemIndex, 
                        buffer, 
                        MAX_PATH, 
                        MF_BYPOSITION);
                if (popupMenuName == buffer)
                {
                    // this is the popup menu we have to add to
                    bFoundSubMenu = true;
                }
            }
        }
        // 4: If exists, do recursive call,
        // else create do recursive call
        // and then insert it
        if (bFoundSubMenu)
        {
            bSuccess = AddMenuItem(
                    menuItemInfo.hSubMenu, 
                    remainingText, 
                    itemID);
        }
        else
        {
            // we need to create a new sub menu and insert it
            HMENU hPopupMenu = ::CreatePopupMenu();
            if (hPopupMenu != NULL)
            {
                bSuccess = AddMenuItem(
                        hPopupMenu, 
                        remainingText, 
                        itemID);
                if (bSuccess)
                {
                    if (::AppendMenu(
                            hTargetMenu, 
                            MF_POPUP, 
                            (UINT)hPopupMenu, 
                            popupMenuName) > 0)
                    {
                        bSuccess = true;
                        // hPopupMenu now owned by hTargetMenu,
                        // we do not need to destroy it
                    }
                    else
                    {
                        // failed to insert the popup menu
                        bSuccess = false;
                        // stop a resource leak
                        ::DestroyMenu(hPopupMenu);
                    }
                }
            }
        }        
    }
    else
    {
        // no sub menus required, add this item to this HMENU
        // item ID of 0 means we are adding a separator
        if (itemID != 0)
        {
            // its a normal menu command
            if (::AppendMenu(
                    hTargetMenu, 
                    MF_BYCOMMAND, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the item to the menu
                bSuccess = true;
            }
        }
        else
        {
            // we are inserting a separator
            if (::AppendMenu(
                    hTargetMenu, 
                    MF_SEPARATOR, 
                    itemID, 
                    itemText) > 0)
            {
                // we successfully added the separator to the menu
                bSuccess = true;
            }
        }
    }

    return bSuccess;
}

对标准 MFC MDI 文档/视图应用程序菜单进行了函数示例测试。以下代码在 CDocTemplate 对象注册后添加到了 InitInstance() 函数中:

    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);

调用这些函数后的菜单看起来像这样:

Dynmaic menu item test output

动态菜单项插入成功了,除了我们的菜单项是禁用的!它们被禁用是由于 MFC 内部的工作方式。当 MFC 首次显示菜单时,它会检查该项是否存在 ON_UPDATE_COMMAND_UI 处理程序;如果存在,它会调用它来获取菜单项的正确状态(禁用或启用等);如果没有 ON_UPDATE_COMMAND_UI 处理程序,如果不存在 ON_COMMAND 处理程序,MFC 会自动禁用该项。因此,从这一点开始,我们需要为这些菜单项编写处理程序才能使它们启用。所以,我们所做的已经成功了!

留给读者的练习

为了看看您是否在听,我有一个面向有兴趣的读者的练习。尝试扩展上面的函数,以便在添加新菜单项时,您可以指定插入位置。我的示例上面将弹出菜单“*Test popup*”添加到了菜单的末尾。这可能不是您想要的。修改代码,以便您可以将弹出菜单插入到您想要的位置。下载源文件中提供了此方案的解决方案。

估算菜单大小

本节的灵感来自 **Michael Dunn**,他在 C++ 论坛上回答了一个问题。

当您有一个弹出菜单时,您可能需要在显示它之前知道它的大小。这是为了能够正确地在屏幕上定位它。没有标准的函数可用于计算您要显示的菜单的大小。所以我们必须自己开发一个。

根据我们想知道的内容,我们可以尝试计算高度(相对容易)或宽度(困难)。首先,让我们看看弹出菜单的高度:

int CalculateMenuHeight(HMENU hMenu)
{
    int height;

    height = ::GetMenuItemCount(hMenu) * ::GetSystemMetrics(SM_CYMENUSIZE);
    return height;
}

估算菜单宽度更难。我们不能简单地取项数并乘以高度,我们必须考虑每个菜单项的文本、是否存在图标,以及一个项是否为弹出菜单(因为它旁边有一个 >)。因此,在思考了一段时间后,我实现了这个函数:

// constants used in the calculation of the menu width
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)
{
    // create a copy of the font that should be used to render a menu
    // so we can measure text correctly
    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));       // get screen DC
    dc.SaveDC();
    dc.SelectObject(&menuFont);

    // look at each item and work out its width
    int maxWidthBeforeTab = 0;
    int maxWidthAfterTab = 0;
    int itemCount = ::GetMenuItemCount(hMenu);

    for (int item = 0 ; item < itemCount ; item++)
    {
        // get each items data
        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)
        {
            // its a popup menu, include the width of the > arrow
            itemWidth += f_popupArrowSize;
        }
        if (itemInfo.wID == 0)
        {
            // its a separator, dont measure the text
        }
        else
        {
            GetMenuString(hMenu, item, menuItemText, _MAX_PATH, MF_BYPOSITION);

            // measure the text using the font
            CSize textSize;
            CString itemText = menuItemText;

            // remove any accelerator key mnemonics
            itemText.Replace("&", "");

            if (itemText.Find("\t") >= 0)
            {
                // we have a tab, measure both parts
                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;
}

当然,如果您使用 TrackPopupMenu/Ex 函数来处理菜单的高度/宽度,那么您可以使用 TPM_LEFTALIGNTPM_RIGHTALIGNTPM_TOPALIGNTPM_BOTTOMALIGN 标志在您需要的位置正确对齐菜单。

MFC 和菜单

每个 MFC 应用程序都有一个默认菜单资源,通常称为 IDR_MAINFRAME。这是应用程序在应用程序中没有打开文档时使用的菜单。如果您打开了文档,CMainFrame 将选择当前文档所属的 CDocTemplate 对象的菜单。

CDocTemplate 和菜单

文档类型的菜单存储在 HMENU m_hMenuShared 成员变量中。当处理 WM_MDIACTIVATE 消息时,或当新 MDI 窗口刚刚创建并激活时,此菜单将被选中到 CMainFrame 中。

一件奇怪的事情是,如果该类型的文档在该应用程序中打开并处于活动状态,菜单的开头会插入一个系统菜单弹出菜单。我曾受此问题影响,当时我有一个带有下拉箭头的工具栏按钮。单击时,我需要显示一个属于“*文件*”菜单的弹出菜单。我的代码转到 CMultiDocTemplate::m_m_hMenuShared,并尝试提取我需要显示的正确弹出菜单。当没有该类型的文档打开并激活时,这工作正常。当有文档时,我不断收到一个“*文件*”菜单弹出窗口的 CTempMenu 对象,这是一个临时的 MFC 包装器,用于已添加的系统菜单。我需要在我的代码中进行一些额外的检查,以确定我是否正在访问活动文档的共享菜单资源。

MFC 中的状态栏菜单项提示

我们知道,当用户用鼠标或键盘突出显示菜单项时,操作系统会将拥有菜单的窗口发送一个 WM_MENUSELECT 消息。CFrameWnd 类拥有 MFC 中的顶层菜单,它会处理此消息,然后向自身发送一个 WM_SETMESSAGESTRING 消息。MFC 根据 WPARAMLPARAM 的值以两种方式使用此消息:

  • wParam = 0lParam = LPCTSTR

    在这种情况下,当 wParam 为 0 时,LPARAM 包含一个指向 NULL 终止字符串的指针,该字符串将显示在状态栏消息窗格中。

  • wParam = menuItemIDlParam = NULL

    在这种情况下,wParam 包含菜单项命令 ID。MFC 调用 CFrameWnd::GetMenuString() 函数,该函数提取具有相同 ID 的字符串表资源条目。它在 \t (tab) 字符处(如果字符串中存在)断开此字符串,并将结果显示在状态栏的消息窗格中。

定义状态栏菜单项字符串的示例:

Editing a menu item in VC6

好了,差不多就是这样了。当我想出新想法或收到建议时,我会添加新章节。

更新历史

  • 首次发布 - 2004 年 6 月 23 日。
© . All rights reserved.