TN021:命令和消息传送

注意

以下技术说明在首次包括在联机文档中后未更新。 因此,某些过程和主题可能已过时或不正确。 要获得最新信息,建议你在联机文档索引中搜索热点话题。

本说明介绍常规窗口消息路由中的命令路由和调度体系结构以及高级主题。

请参阅 Visual C++ 以了解有关此处所述的体系结构的一般详细信息,尤其是 Windows 消息、控件通知与命令之间的区别。 本说明假设你非常熟悉打印的文档中介绍的问题,只讨论非常高级的主题。

命令路由和调度 MFC 1.0 功能演变为 MFC 2.0 体系结构

Windows 包含重载的 WM_COMMAND 消息,用于提供菜单命令通知、快捷键和对话框控件通知。

MFC 1.0 基于这一点而构建,具体方式是允许调用 CWnd 派生类中的命令处理程序(例如“OnFileNew”)来响应特定 WM_COMMAND。 这与称为消息映射的数据结构相结合,形成了一种空间效率非常高的命令机制。

MFC 1.0 还提供用于将控件通知与命令消息分开的附加功能。 命令由 16 位 ID 表示(有时称为命令 ID)。 命令通常从 CFrameWnd 发起(即通过菜单选择或转换的快捷键)并路由到其他各种窗口。

MFC 1.0 在有限的意义上使用命令路由实现多文档接口 (MDI)。 (MDI 框架窗口会将命令委托到其活动 MDI 子窗口。)

此功能已在 MFC 2.0 中得到通用化并扩展,以允许由更广泛的对象处理命令(而不仅仅是窗口对象)。 它为路由消息提供了一个更加正式和可扩展的体系结构,并重用命令目标路由,以便不仅可处理命令,而且可更新 UI 对象(如菜单项和工具栏按钮)以反映命令的当前可用性。

命令 ID

有关命令路由和绑定过程的说明,请参阅 Visual C++。 技术说明 20 包含有关 ID 命名的信息。

我们将泛型前缀“ID_”用于命令 ID。 命令 ID >= 0x8000。 如果存在 ID 与命令 ID 相同的 STRINGTABLE 资源,则消息行或状态栏会显示命令说明字符串。

在应用程序的资源中,命令 ID 可以出现在多个位置:

  • 在一个与消息行提示符具有相同 ID 的 STRINGTABLE 资源中。

  • 可能在许多附加到调用相同命令的菜单项的 MENU 资源中。

  • (高级)在 GOSUB 命令的对话框按钮中。

在应用程序的源代码中,命令 ID 可以出现在多个位置:

  • 在 RESOURCE.H(或其他主符号头文件)中用于定义特定于应用程序的命令 ID。

  • 可能在用于创建工具栏的 ID 数组中。

  • 在 ON_COMMAND 宏中。

  • 可能在 ON_UPDATE_COMMAND_UI 宏中。

当前,MFC 中唯一需要命令 ID >= 0x8000 的实现是 GOSUB 对话框/命令的实现。

GOSUB 命令,在对话框中使用命令体系结构

路由和启用命令的命令体系结构适用于框架窗口、菜单项、工具栏按钮、对话栏按钮、其他控件条和其他用户界面元素(旨在按需更新并将命令或控件 ID 路由到主命令目标(通常为主框架窗口))。 该主命令目标可以根据情况将命令或控件通知路由到其他命令目标对象。

如果将对话框的控件 ID 分配给相应的命令 ID,则对话框(有模式或无模式)可以受益于命令体系结构的某些功能。 对话框支持不是自动的,因此可能需要编写一些额外代码。

请注意,若要使所有这些功能可正常工作,命令 ID 应 >= 0x8000。 由于许多对话框可以路由到相同框架,因此共享命令应 >= 0x8000,而特定对话框中的非共享 IDC 应 <= 0x7FFF。

可以将普通按钮置于普通有模式对话框中,并且按钮的 IDC 设置为相应的命令 ID。 当用户选择按钮时,对话框的所有者(通常是主框架窗口)会获取命令,如同任何其他命令一样。 这称为 GOSUB 命令,因为它通常用于启动另一个对话框(第一个对话框的 GOSUB)。

还可以在对话框中调用函数 CWnd::UpdateDialogControls,并将它传递给主框架窗口的地址。 此函数会基于对话框控件是否在框架中具有命令处理程序来启用或禁用这些控件。 在应用程序的空闲循环中,系统会自动为你对控件条调用此函数,但你必须对希望具有此功能的普通对话框直接调用它。

调用 ON_UPDATE_COMMAND_UI 时

始终维护程序所有菜单项的已启用/选中状态可能是计算成本高昂的问题。 一种常见方法是仅在用户选择 POPUP 时才启用/选中菜单项。 CFrameWnd 的 MFC 2.0 实现会处理 WM_INITMENUPOPUP 消息,并使用命令路由体系结构通过 ON_UPDATE_COMMAND_UI 处理程序确定菜单的状态。

CFrameWnd 还会处理 WM_ENTERIDLE 消息,以描述在状态栏(也称为消息行)上选择的当前菜单项。

应用程序的菜单结构(由 Visual C++ 编辑)用于表示 WM_INITMENUPOPUP 时可用的潜在命令。 ON_UPDATE_COMMAND_UI 处理程序可以修改菜单的状态或文本,或者对于高级用法(如文件 MRU 列表或 OLE 谓词弹出菜单),在绘制菜单之前实际修改菜单结构。

当应用程序进入空闲循环时,会对工具栏(和其他控件条)执行相同类型的 ON_UPDATE_COMMAND_UI 处理。 有关控件条的详细信息,请参阅类库参考技术说明 31

嵌套弹出菜单

如果使用嵌套菜单结构,你会注意到弹出菜单中第一个菜单项的 ON_UPDATE_COMMAND_UI 处理程序会在两种不同情况下进行调用。

首先,对弹出菜单本身调用它。 这是必要的,因为弹出菜单没有 ID,我们使用弹出菜单的第一个菜单项的 ID 引用整个弹出菜单。 在这种情况下,CCmdUI 对象的 m_pSubMenu 成员变量会是非 NULL,并指向弹出菜单。

其次,恰好在绘制弹出菜单中的菜单项之前调用它。 在这种情况下,ID 仅引用第一个菜单项,CCmdUI 对象的 m_pSubMenu 成员变量会是 NULL。

这样便可以启用与菜单项不同的弹出菜单,但需要编写一些菜单感知代码。 例如,在具有以下结构的嵌套菜单中:

File>
    New>
    Sheet (ID_NEW_SHEET)
    Chart (ID_NEW_CHART)

可以独立启用或禁用 ID_NEW_SHEET 和 ID_NEW_CHART 命令。 如果启用了这两个命令中的任意一个,则应启用 New 弹出菜单。

ID_NEW_SHEET(弹出菜单中的第一个命令)的命令处理程序会如下所示:

void CMyApp::OnUpdateNewSheet(CCmdUI* pCmdUI)
{
    if (pCmdUI->m_pSubMenu != NULL)
    {
        // enable entire pop-up for "New" sheet and chart
        BOOL bEnable = m_bCanCreateSheet || m_bCanCreateChart;
        // CCmdUI::Enable is a no-op for this case, so we
        // must do what it would have done.
        pCmdUI->m_pMenu->EnableMenuItem(pCmdUI->m_nIndex,
            MF_BYPOSITION |
            (bEnable  MF_ENABLED : (MF_DISABLED | MF_GRAYED)));

        return;
    }
    // otherwise just the New Sheet command
    pCmdUI->Enable(m_bCanCreateSheet);
}

ID_NEW_CHART 的命令处理程序会是普通更新命令处理程序,如下所示:

void CMyApp::OnUpdateNewChart(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(m_bCanCreateChart);
}

ON_COMMAND 和 ON_BN_CLICKED

ON_COMMAND 和 ON_BN_CLICKED 的消息映射宏是相同的。 MFC 命令和控件通知路由机制仅使用命令 ID 确定要路由到的位置。 控件通知代码为零 (BN_CLICKED) 的控件通知会解释为命令

注意

事实上,所有控件通知消息都经过命令处理程序链。 例如,从技术上讲,可以在文档类中为 EN_CHANGE 编写控件通知处理程序。 通常不建议这样做,因为此功能的实际应用很少,此功能不受 ClassWizard 支持,并且使用此功能可能会导致脆弱的代码。

禁用按钮控件的自动禁用

如果你将按钮控件放置在对话栏中,或是在你自己调用 CWnd::UpdateDialogControls 的对话框使用中,则会注意到,框架会自动为你禁用没有 ON_COMMAND 或 ON_UPDATE_COMMAND_UI 处理程序的按钮。 在某些情况下,你无需使用处理程序,但希望按钮保持启用状态。 实现此目标的最简单方法是添加虚拟命令处理程序(可使用 ClassWizard 轻松执行),在其中不执行任何操作。

窗口消息路由

下面介绍有关 MFC 类以及 Windows 消息路由和其他主题如何影响它们的一些更高级主题。 此处的信息仅进行了简要介绍。 有关公共 API 的详细信息,请参阅类库参考。 有关实现详细信息的更多信息,请参阅 MFC 库源代码。

有关窗口清理的详细信息,请参阅技术说明 17,这是适用于所有 CWnd 派生类的一个非常重要的主题

CWnd 问题

实现成员函数 CWnd::OnChildNotify 为子窗口(也称为控件)提供了一种强大且可扩展的体系结构,以便挂钩或以其他方式了解发送到其父级(或“所有者”)的消息、命令和控件通知。 如果子窗口(控件)是 C++ CWnd 对象本身,则首先使用来自原始消息(即 MSG 结构)的参数调用虚拟函数 OnChildNotify。 子窗口可以不处理消息、处理它或是为父级修改消息(罕见)。

默认 CWnd 实现处理以下消息,并使用 OnChildNotify 挂钩允许子窗口(控件)首先访问消息

  • WM_MEASUREITEM 和 WM_DRAWITEM(用于自我绘制)

  • WM_COMPAREITEM 和 WM_DELETEITEM(用于自我绘制)

  • WM_HSCROLL 和 WM_VSCROLL

  • WM_CTLCOLOR

  • WM_PARENTNOTIFY

你会注意到,OnChildNotify 挂钩用于将所有者绘制消息更改为自我绘制消息。

除了 OnChildNotify 挂钩之外,滚动消息具有进一步的路由行为。 有关滚动条以及 WM_HSCROLL 和 WM_VSCROLL 消息源的更多详细信息,请参阅下文

CFrameWnd 问题

CFrameWnd 类提供大多数命令路由和用户界面更新实现。 这主要用于应用程序的主框架窗口 (CWinApp::m_pMainWnd),但适用于所有框架窗口。

主框架窗口是带有菜单栏的窗口,是状态栏或消息行的父级。 请参阅上面有关命令路由和 WM_INITMENUPOPUP 的讨论。

CFrameWnd 类提供活动视图的管理。 以下消息通过活动视图进行路由:

  • 所有命令消息(活动视图首先访问它们)。

  • 来自同级滚动条的 WM_HSCROLL 和 WM_VSCROLL 消息(请参阅下文)。

  • WM_ACTIVATE(以及适用于 MDI 的 WM_MDIACTIVATE),转换为对虚拟函数 CView::OnActivateView 的调用

CMDIFrameWnd/CMDIChildWnd 问题

这两个 MDI 框架窗口类都派生自 CFrameWnd,因此对于 CFrameWnd 中提供的相同类型命令路由和用户界面更新都处于启用状态。 在典型 MDI 应用程序中,只有主框架窗口(即 CMDIFrameWnd 对象)包含菜单栏和状态栏,因此是命令路由实现的主要源

常规路由方案是活动 MDI 子窗口首先访问命令。 默认 PreTranslateMessage 函数处理 MDI 子窗口(首先)和 MDI 框架(其次)的快捷键表,以及通常由 TranslateMDISysAccel 处理的标准 MDI 系统命令快捷键(最后)

滚动条问题

处理滚动消息(WM_HSCROLL/OnHScroll 和/或 WM_VSCROLL/OnVScroll)时,应尝试编写处理程序代码,使它不依赖于滚动条消息的来源。 这不仅是常规 Windows 问题,因为滚动消息可以来自真正的滚动条控件,也可以来自不是滚动条控件的 WS_HSCROLL/WS_VSCROLL 滚动条。

MFC 扩展了这一点,允许滚动条控件是所滚动的窗口的子级或同级(事实上,滚动条与所滚动的窗口之间的父/子关系可以是任何关系)。 这对于具有拆分器窗口的共享滚动条尤其重要。 有关 CSplitterWnd 实现的详细信息(包括有关共享滚动条问题的详细信息),请参阅技术说明 29

另一方面,有两个 CWnd 派生类,其中在创建时指定的滚动条样式会被捕获,不会传递给 Windows。 传递到创建例程时,WS_HSCROLL 和 WS_VSCROLL 可以独立设置,但在创建后无法更改。 当然,不应直接测试或设置创建它们的窗口的 WS_SCROLL 样式位。

对于 CMDIFrameWnd,传入到 Create 或 LoadFrame 的滚动条样式用于创建 MDICLIENT。 如果希望具有可滚动的 MDICLIENT 区域(如 Windows 程序管理器),请务必为用于创建 CMDIFrameWnd 的样式同时设置这两种滚动条样式 (WS_HSCROLL | WS_VSCROLL)。

对于 CSplitterWnd,滚动条样式适用于拆分器区域的特殊共享滚动条。 对于静态拆分器窗口,通常不会设置任一滚动条样式。 对于动态拆分器窗口,通常为将拆分的方向设置滚动条样式,即在可以拆分行时为 WS_HSCROLL,在可以拆分列时为 WS_VSCROLL

另请参阅

按编号列出的技术说明
按类别列出的技术说明