如何创建 Internet Explorer 样式菜单栏

一开始,Microsoft Internet Explorer 5 及更高版本中的菜单栏看起来类似于标准菜单。 但在开始使用时,它的外观却大不相同。

以下屏幕截图显示选中“工具”菜单的 Windows Internet Explorer 菜单栏。

screen shot that shows the windows internet explorer menu bar, with the tools menu selected

菜单栏实际上是类似于标准菜单的工具栏控件。 菜单栏具有一系列纯文本按钮,在单击时显示下拉菜单,而不是顶级菜单项。 但是,作为专用类型的工具栏,菜单栏具有一些标准菜单缺少的功能:

  • 可以使用标准工具栏技术对其进行自定义,从而允许用户移动、删除或添加项。
  • 它可以具有热跟踪,以便用户知道何时处于顶级项之上,而无需先单击它。

菜单栏可以合并到 rebar 控件中,并为其提供以下功能:

  • 它可以有一个手柄,允许用户移动或调整带区的大小。
  • 它可以与其他带区共享 rebar 控件中的条带(如上图中的标准工具栏)。
  • 当它被相邻带区覆盖时,可以显示 V 形,使用户能够访问隐藏项。
  • 它可以具有应用程序定义的背景位图。

本主题讨论如何实现菜单栏。 由于菜单栏是工具栏控件的专用实现,因此焦点将位于特定于菜单栏的主题上。 有关如何将工具栏合并到 rebar 控件的讨论,请参阅如何创建 Internet Explorer 样式工具栏关于 Rebar 控件

将工具栏设入菜单栏中

若要将工具栏添加到菜单栏中,请:

  • 通过包括具有其他窗口样式标志的 TBSTYLE_FLAT 来创建平面工具栏。 TBSTYLE_FLAT 样式还支持热跟踪。 使用此样式,在用户激活按钮之前,菜单栏看起来非常类似于标准菜单。 然后,单击该按钮,该按钮似乎像从工具栏跳出一样按下,就像标准按钮一样。 由于启用了热跟踪,因此激活按钮所需的所有操作都是为了使光标悬停在按钮上。 如果光标移动到另一个按钮,将激活该按钮并停用旧按钮。
  • 通过包括具有其他窗口样式标志的 TBSTYLE_LIST 来创建列表样式按钮。 此样式创建更薄的按钮,看起来更像标准顶级菜单项。
  • 通过将按钮的 TBBUTTON 结构的 iBitmap 成员设置为 I_IMAGENONE 并将 iString 成员设置为按钮文本,将按钮设为纯文本。
  • 为每个按钮提供 BTNS_DROPDOWN 样式。 单击按钮时,工具栏控件会向应用程序发送 TBN_DROPDOWN 通知,以提示显示按钮的菜单。
  • 将菜单栏合并到 rebar 带区中。 启用手柄和 V 形,如如何创建 Internet Explorer 样式工具栏中所述。
  • 实现 TBN_DROPDOWN 处理程序,以在单击按钮时显示其下拉菜单。 下拉菜单是一种弹出菜单。 通过使用 TrackPopupMenu 函数创建,其左上角与按钮的左下角对齐。
  • 实现键盘导航,以便无需鼠标即可完全访问菜单栏。
  • 实现菜单热跟踪。 使用标准菜单时,一旦显示顶级菜单项的菜单,将光标移到另一个顶级项上会自动显示其菜单并折叠上一项的菜单。 工具栏控件将热跟踪光标并更改按钮图像,但会自动显示新菜单。 应用程序必须显式执行此操作。

其中大多数项都易于实现,并在其他位置进行讨论。 有关如何使用工具栏和 rebar 控件的一般讨论,请参阅如何创建 Internet Explorer 样式工具栏关于工具栏控件关于 Rebar 控件。 有关如何处理弹出菜单的讨论,请参阅菜单。 本主题的其余部分将讨论最后两个项(键盘导航和菜单热跟踪)。

禁用菜单热跟踪时处理导航

用户可以选择使用鼠标、键盘或两者的组合导航菜单栏。 若要实现菜单栏导航,应用程序必须处理工具栏通知并监视鼠标和键盘。 此任务可以分为两个不同的部分:使用和不使用菜单热跟踪。 本部分讨论在未显示菜单且未启用菜单热跟踪时如何处理导航。

鼠标导航

如果禁用菜单热跟踪,则可以将菜单栏视为普通工具栏。 如果用户使用鼠标导航,则应用程序只需处理 TBN_DROPDOWN 通知。 收到此通知后,显示相应的下拉菜单,并启用菜单热跟踪。 此后将进入启用菜单热跟踪时处理导航中讨论的菜单热跟踪方案。

混合导航中所述,还需要处理 TBN_HOTITEMCHANGE 通知来跟踪活动按钮。 如果用户仅使用鼠标导航,则此通知无关紧要,但当鼠标和键盘导航混合使用时,这一通知是必需的。

键盘导航

如上一部分所述,禁用菜单热跟踪时,用户可以使用键盘执行许多导航操作。 仅当工具栏控件具有焦点时才支持键盘导航。 它们也不会处理菜单栏所需的所有按键。 处理键盘导航最简单的解决方案是显式处理相关按键事件。

如果禁用菜单热跟踪,则应用程序必须通过以下方式处理这些按键事件:

  • F10 键。 使用 TB_SETHOTITEM 激活第一个按钮。
  • 向左箭头键和向右箭头键。 使用 TB_SETHOTITEM 激活相邻按钮。 如果用户尝试在菜单栏的任一端导航,请在另一端激活第一个按钮。
  • ESCAPE 键。 使用 TB_SETHOTITEM 停用活动按钮。 菜单栏应返回到激活第一个按钮之前的状态。
  • ALT-Key 快捷键。 使用 TB_MAPACCELERATOR 消息确定 Key 字符对应的按钮。 显示按钮的下拉菜单并启用菜单热跟踪。
  • DOWN ARROW 键。 如果按钮处于活动状态但未显示任何菜单,请显示按钮的菜单并启用菜单热跟踪。
  • ENTER 键。 使用 TB_SETHOTITEM 停用活动按钮。 菜单栏应返回到激活第一个按钮之前的状态。

混合导航

上一部分中概述的键盘导航处理程序基本上执行两项任务:跟踪活动按钮,并在选择按钮时显示相应的菜单。 只要用户仅使用键盘导航,这些处理程序便已足够。 但是,用户通常混合使用键盘和鼠标导航。 例如,他们可能使用 F10 键激活第一个按钮,使用鼠标激活其他按钮,然后使用向下箭头键打开其菜单。 如果只是监视按键以跟踪活动键,则将显示错误的菜单。 还必须处理 TBN_HOTITEMCHANGE 通知,以便准确跟踪活动按钮。

启用菜单热跟踪时处理导航

启用菜单热跟踪后,应用程序必须更改其响应用户导航的方式。 若要复制标准菜单的行为,必须显式实现以下功能。

使用鼠标导航:

  • 如果用户将光标移到另一个按钮上,该菜单将立即显示,并且上一个菜单消失。
  • 菜单热跟踪保持活动状态,直到用户选择命令或单击不属于菜单区域的位置。 任一操作将停用菜单热跟踪,直到单击另一个按钮。
  • 如果光标移出菜单,则当前下拉菜单将一直保留,直到光标移回其上,或者用户单击菜单覆盖的区域以外的某个位置。 如果光标返回到与当前显示的按钮不同的按钮,则旧菜单将折叠并显示新菜单。

使用键盘导航:

  • 向右箭头键将焦点向右移动。 如果项具有子菜单,则显示子菜单。 如果该项没有子菜单,则折叠菜单和任何子菜单,使用 TB_SETHOTITEM 激活下一个按钮,并显示相邻按钮的菜单。 如果收到此消息时最后一个按钮处于活动状态,则显示第一个按钮的菜单。

  • 向左箭头键将焦点向左移动。 如果该项是子菜单,则折叠并将焦点移动到其父菜单。 如果该项不是子菜单,则折叠菜单,使用 TB_SETHOTITEM 激活下一个按钮,并显示该按钮的菜单。

  • ESCAPE 键将后退显示一个步骤。

    • 如果显示子菜单,则会折叠,焦点将移动到父菜单。
    • 如果显示按钮的菜单,则会折叠并禁用菜单热跟踪。 该项的按钮保持活动状态。
    • 如果未显示菜单,但按钮处于活动状态,则会停用该按钮,并禁用菜单热跟踪。
  • 向上箭头键和向下箭头键仅用于在特定菜单中导航。

  • ENTER 键启动与菜单项关联的命令。 如果菜单项具有子菜单,ENTER 键将显示该菜单。

与菜单热跟踪禁用的情况一样,应用程序必须能够处理鼠标、键盘和混合导航。 但是,显示菜单意味着必须以某种方式处理消息传递。

菜单热跟踪的消息处理

菜单热跟踪要求菜单随时显示,除了切换到新菜单所需的短暂间隔。 但是,TrackPopupMenu 显示的下拉菜单是模式菜单。 应用程序将继续直接接收某些消息,包括 WM_COMMANDTBN_HOTITEMCHANGE 和常规菜单相关消息,例如 WM_MENUSELECT。 但是,它不会直接接收低级别键盘或鼠标消息。 若要处理 WM_MOUSEMOVE 等消息,必须设置消息挂钩以截获定向到菜单的消息。

显示下拉菜单时,通过调用 idHook 参数设置为 WH_MSGFILTER 的 SetWindowsHookEx 函数来设置消息挂钩。 适用于菜单的所有消息都将传递到 MessageProc。 例如,以下代码片段设置一个消息挂钩,用于捕获要转到下拉菜单的消息。 MsgHook 是挂钩过程的名称,hhookMsg 是该过程的句柄。

hhookMsg = SetWindowsHookEx(WH_MSGFILTER, MsgHook, HINST_THISDLL, 0);

通过将挂钩过程的 nCode 参数设置为 MSGF_MENU 来标识菜单消息。 lParam 参数将指向包含消息的 MSG 结构。 本主题的其余部分详细讨论需要处理的消息以及处理方式。

应用程序必须通过调用 CallNextHookEx 函数将所有消息传递到下一个消息挂钩。 还必须将鼠标消息直接发送到工具栏控件,确保将任何点坐标转换为工具栏客户端坐标空间。 直接发送消息可确保工具栏控件接收相应的鼠标消息。 例如,工具栏需要接收 WM_MOUSEMOVE 消息才能热跟踪其按钮。

激活新按钮后,应用程序必须折叠带有 WM_CANCELMODE 消息的旧下拉菜单,然后显示新菜单。 通过键盘导航或单击菜单区域外从菜单栏中获取焦点时,还必须折叠下拉菜单。 每当折叠菜单时,都应使用 UnhookWindowsHookEx 函数释放其消息挂钩。 如果需要显示另一个下拉菜单,请创建新的消息挂钩。 启动命令时,菜单将自动折叠,但必须显式释放消息挂钩。

下面的代码示例释放上一示例中设置的消息挂钩。

UnhookWindowsHookEx(hhookMsg);

鼠标导航

当普通工具栏控件热跟踪按钮时,它会突出显示活动按钮,并在每次激活新按钮时向应用程序发送 TBN_HOTITEMCHANGE 通知。 应用程序负责显示相应的下拉菜单。 条件为:

  • 处理 TBN_HOTITEMCHANGE 通知以跟踪活动按钮。 当活动按钮发生更改时,折叠旧菜单并创建新菜单。
  • 处理单击按钮时发送的 TBN_DROPDOWN 通知。 然后应折叠菜单并禁用菜单热跟踪。 按钮保持活动状态。
  • 在消息挂钩过程中处理 WM_LBUTTONDOWNWM_RBUTTONDOWNWM_MOUSEMOVE 消息,以跟踪鼠标位置。 如果在菜单区域外单击鼠标,则折叠当前下拉菜单,停用菜单热跟踪,并将菜单栏返回到其预激活状态。
  • 启动菜单命令时,禁用菜单热跟踪。 菜单将自动折叠,但必须显式释放消息挂钩。

还必须处理与菜单相关的消息传送,例如菜单项需要显示子菜单时发送的 WM_INITMENUPOPUP 消息。 有关如何处理此类消息的讨论,请参阅菜单

键盘导航

应用程序必须在消息挂钩过程中处理键盘消息,并处理影响菜单热跟踪的键盘消息。 必须处理以下按键:

  • ESCAPE 键。 ESCAPE 键将后退显示一个级别。 如果显示子菜单,则必须折叠该子菜单。 如果显示按钮的主菜单,则折叠并禁用菜单热跟踪。 按钮保持活动状态。
  • RIGHT ARROW 键。 如果项具有子菜单,则显示该菜单。 如果该项没有子菜单,则折叠菜单和任何子菜单,使用 TB_SETHOTITEM 激活下一个按钮,并显示其菜单。 如果收到此通知时最后一个按钮处于活动状态,则显示第一个按钮的菜单。
  • LEFT ARROW 键。 如果该项在子菜单中,则折叠并将焦点移动到其父菜单。 如果该项不是子菜单,则折叠菜单,使用 TB_SETHOTITEM 激活相邻按钮,并显示其菜单。 如果收到此通知时第一个按钮处于活动状态,则显示最后一个按钮的菜单。
  • 向上箭头键和向下箭头键。 这些键用于在菜单中导航,但不直接影响菜单热跟踪。
  • ALT-Key 快捷键。 使用 TB_MAPACCELERATOR 消息确定 Key 字符对应的按钮。 如果对应与当前活动按钮不同的按钮,则折叠当前下拉菜单,使用 TB_SETHOTITEM 激活新按钮,并显示相邻按钮的菜单。 如果 Key 字符对应当前显示的按钮,则折叠下拉菜单并禁用菜单热跟踪。 该按钮应保持活动状态。