第9章  消息映射与命令绕行

Message Mapping and Command Routing

消息映射机制与命令绕行,活像是米诺托斯的迷宫,是 MFC 最曲折幽深的神秘地带。

你已经从前一章中彻底了解了MFC程序极端重要的Document/View 架构。本章的重点有两个,第一个是修改程序的人机接口,增添选单项目和工具列按钮。这一部分藉Visual C++ 工具之助,非常简单,但是我们往往不知道该在程序的什么地方(哪一个类之中)处理来自选单和工具列的消息(也就是 WM_COMMAND 消息)。本章第二个重点就是要解决这个迷惑,我将对所谓的消息映射(Message Map)和命令绕行(Command Routing)机制做深入的讨论。这两个机制宛如 MFC 最曲折幽深的神秘地带,是把杂乱无章的Windows API 函数和 Windows 消息面向对象化的大功臣。

到底要解决什么

Windows 程序的本质系借着消息来维持脉动。每一个消息都有一个代码,并以WM_ 开头的常数表示之。消息在传统SDK程序方法中的流动以及处置方式,在第1章已经交待得很清楚。

各种消息之中,来自选单或工具列者,都以WM_COMMAND 表示,所以这一类消息我们又称之为命令消息(Command Message),其wParam 记录着此一消息来自哪一个选单项目。

除了命令消息,还有一种消息也比较特殊,出现在对话框函数中,是控制组件(controls)传送给父窗口(即对话框)的消息。虽然它们也是以WM_COMMAND 为外衣,但特别归类为「notification 消息」。

注意:Windows 95 新的控制组件(所谓的common controls)不再传送 WM_COMMAND消息给对话框,而是送 WM_NOTIFY。这样就不会纠缠不清了。但为了回溯相容,旧有的控制组件(如 edit、list box、combo box...)都还是传送WM_COMMAND。

消息会循着Application Framework 规定的路线,游走于各个对象之间,直到找到它的依归(消息处理函数)。找不到的话,Framework 最终就把它交给::DefWindowProc 函数去处理。

但愿你记忆犹新,第6章曾经挖掘 MFC 原始代码,得知 MFC 在为我们产生窗口之前,如果我所指定的窗口类是 NULL,MFC 会自动先注册一个适当的窗口类。这个类在动态链接、除错版、非 Unicode 环境的情况下,可能是下列五种窗口类之一:

  • "AfxWnd42d"

  • "AfxControlBar42d"

  • "AfxMDIFrame42d"

  • "AfxFrameOrView42d"

  • "AfxOleControl42d"

每一个窗口类有它自己的窗口函数。根据 SDK 的基础,我们推想,不同窗口所获得的消息,应该由不同的窗口函数来处理。如果都没有能够处理,最后再交由Windows API函数::DefWindowProc 处理。

这是很直觉的想法,而且对于一般消息(如 WM_MOVE、WM_SIZE、WM_CREATE 等)也是天经地义的。但是今天 Application Framework 比传统的SDK 程序多出了一个Document/View 架构,试想,如果选单上有个命令项关乎文件的处理,那么让这个命令消息流到Document 类去不是最理想吗?一旦流入 Document 大本营,我们(程序员)就可以很方便地取得Document 成员变量、调用Document成员函数,做爱做的事。

但是Document 不是窗口,也没有对应的窗口类,怎么让消息能够七拐八弯地流往Document类去?甚至更往上流向 Application 类去?这就是所谓的命令绕行机制!

而为了让消息的流动有线路可循,MFC 必须做出一个巨大的网,实现所有可能的路线,这个网就是所谓的消息映射地图(Message map)。最后,MFC 还得实现一个消息推动引擎,让消息依 Framework 的意旨前进,该拐的时候拐,该弯的时候弯,这个捕获机制埋藏在各个类的WindowProc、OnCommand、OnCmdMsg、DefWindowProc 虚函数中。

没有命令绕行机制,Document/View 架构就像断了条胳臂,会少掉许多功用。

很快你就会看到所有的秘密。很快地,它们统统不再对你构成神秘。

消息分类

Windows 的消息都是以WMxxx 为名,WM 的意思是"Windows Message"。消息可以是来自硬件的「输入消息」,例如 WM_LBUTTONDOWN,也可以是来自 USER 模块的「窗口管理消息」,例如 WM_CREATE。这些消息在 MFC 程序中都是隐晦的(我的意思是不像在 SDK 程序中那般显明),我们不必在 MFC 程序中撰写 switch case 指令,不必一一识别并处理由系统送过来的消息;所有消息都将依循 Framework 制定的路线,并参照路中是否有拦路虎(你的消息映射表格)而流动。WM_PAINT 一定流往你的OnPaint函数去,WM_SIZE 一定流往你的 OnSize 函数去。

所有的消息在 MFC 程序中都是暗潮汹涌,但是表面无波。

MFC 把消息分为三大类:

  • 命令消息(WM_COMMAND):命令消息意味着「使用者命令程序做某些动作」。

    凡由UI对象产生的消息都是这种命令消息,可能来自选单或加速键或工具列按钮,并且都以WM_COMMAND 呈现。如何分辨来自各处的命令消息?SDK程序主要靠消息的wParam 辨识之,MFC程序则主要靠选单项目的识别代码(menu ID)辨识之——两者其实是相同的。

    什么样的类有资格接受命令消息?凡派生自 CCmdTarget 之类,皆有资格。从command target的字面意义可知,这是命令消息的目的地。也就是说,凡派生自CCmdTarget者,它的骨子里就有了一种特殊的机制。把整张MFC类阶层图摊开来看,几乎构造应用程序的最重要的几个类都派生自 CCmdTarget,剩下的不能接收消息的,是像CFile、CArchive、CPoint、CDao(数据库)、Collection Classes(纯粹数据处理)、GDI 等等「非主流」类。

  • 标准消息——除WMCOMMAND 之外,任何以WM 开头的都算是这一类。任何派生自CWnd 之类,均可接收此消息。

  • Control Notification - 这种消息由控制组件产生,为的是向其父窗口(通常是对话框)通知某种情况。例如当你在ListBox 上选择其中一个项目,ListBox 就会产生LBN_SELCHANGE传送给父窗口。这类消息也是以WM_COMMAND 形式呈现。

万流归宗 Command Target(CCmdTarget)

你可以在程序的许多类之中设计拦路虎(我是指「消息映射表格」),接收并处理消息。只要是CWnd 派生类,就可以拦下任何Windows 消息。与窗口无关的MFC类(例如CDocument和CWinApp)如果也想处理消息,必须派生自 CCmdTarget,并且只可能收到 WM_COMMAND 命令消息。

会产生命令消息的,不外就是 UI 对象:选单项目和工具列按钮都是。命令消息必须有一个对应的处理函数,把消息和其处理函数「绑」在一块儿,这动作称为Command Binding,这个动作将由一堆宏完成。通常我们不直接手工完成这些宏内容,也就是说我们并不以文字编辑器一行一行地撰写相关的代码,而是藉助于 ClassWizard。

一个 Command Target 对象如何知道它可以处理某个消息?答案是它会看看自己的消息映射表。消息映射表使得消息和函数的对映关系形成一份表格,进而全体形成一张网,当Command Target 对象收到某个消息,便可由表格得知其处理函数的名称。

三个奇怪的宏,一张巨大的网

早在本书第1章我就介绍过消息映射的雏形了,不过那是小把戏,不登大雅之堂。第3章以 DOS 程序仿真消息映射,就颇有可观之处,因为那是「偷」MFC 的原始代码完成的,可以说具体而微。

试着思考这个问题:C++ 的继承与多态性质,使派生类与基类的成员函数之间有着特殊的关联。但这当中并没有牵扯到 Windows 消息。的确,C++ 语言完全没有考虑Windows 消息这一回事(那当然)。如何让 Windows 消息也能够在面向对象以及继承性质中扮演一个角色?既然语言没有支持,只好自求多福了。消息映射机制的三个相关宏就是 MFC 自求多福的结果。

「消息映射」是 MFC 内建的一个消息分派机制,只要利用数个宏以及固定形式的写法,类似填表格,就可以让 Framework 知道,一旦消息发生,该循哪一条路递送。每一个类只能拥有一个消息映射表格,但也可以没有。下面是 Scribble Document 建立消息映射表的动作:

首先你必须在类声明文件(.H)声明拥有消息映射表格:

class CScribbleDoc : public CDocument
{
    ...
    DECLARE_MESSAGE_MAP()
};

然后在类实现文件(.CPP)实现此一表格:

BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)

//{{AFX_MSG_MAP(CScribbleDoc)
ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)
...
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

这其中出现三个宏。第一个宏 BEGINMESSAGE_MAP 有两个参数,分别是拥有此消息映射表之类,及其父类。第二个宏是 ON_COMMAND,指定命令消息的处理函数名称。第三个宏 END_MESSAGE_MAP 作为结尾记号。至于夹在BEGIN 和END_ 之中奇奇怪怪的说明符号//}} 和 //{{,是ClassWizard 产生的,也是用来给它自己看的。记住,前面我就说了,很少人会自己亲手键入每一行代码,因为 ClassWizard 的表现相当不俗。

夹在BEGIN 和END 之中的宏,除了ON_COMMAND,还可以有许多种。标准的Windows 消息并不需要由我们指定处理函数的名称。标准消息的处理函数,其名称也是「标准」的(预设的),像是:

宏名称                   对映消息               消息处理函数

DECLARE_MESSAGE_MAP 宏

消息映射的本质其实是一个巨大的数据结构,用来为诸如 WM_PAINT 这样的标准消息决定流动路线,使它得以流到父类去;也用来为 WM_COMMAND 这个特殊消息决定流动路线,使它能够七拐八弯地流到类阶层结构的旁支去。

观察机密的最好方法就是挖掘原始代码:

注意:static 修饰词限制了数据的配置,使得每个「类」仅有一份数据,而不是每一个「对象」各有一份数据。

我们看到两个陌生的类型:AFX_MSGMAP_ENTRY和AFX_MSGMAP。继续挖原始代码,发现前者是一个struct:

很明显你可以看出它的最主要作用,就是让消息nMessage对应于函数 pfn。其中pfn 的数据类型 AFX_PMSG 被定义为一个函数指针:

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

出现在 DECLARE_MESSAGE_MAP宏中的另一个struct,AFX_MSGMAP,定义如下:

其中pBaseMap是一个指向「基类之消息映射表」的指针,它提供了一个走访整个继承串链的方法,有效地实现出消息映射的继承性。派生类将自动地「继承」其基类中所处理的消息,意思是,如果基类处理过A消息,其派生类即使未设计A消息之消息映射表项目,也具有对A消息的处理能力。当然啦,派生类也可以针对A消息设计自己的消息映射表项。

喝,真像虚函数!但Message Map没有虚函数所带来的巨大的overhead(额外负担)透过DECLARE_MESSAGE_MAP 这么简单的一个宏,相当于为类声明了图9-1的数据类型。注意,只是声明而已,还没有真正的实体。

图9-1 DECLARE_MESSAGE_MAP宏相当于声明了这样的数据结构

消息映射网的形成:BEGIN ON END宏

前置准备工作完成了,接下来的课题是如何实现并填充图 9-1 的数据结构内容。当然你马上就猜到了,使用的是另一组宏:

BEGIN_MESSAGE_MAP(CMyView, CView)
ON_WM_PAINT()
ON_WM_CREATE()
...
END_MESSAGE_MAP()

奥秘还是在原始代码中:

// 以下原始代码在 AFXWIN.H

注意:AfxSigend 在AFXMSG.H中被定义为0

// 以下原始代码在 AFXMSG_.H

于是,这样的宏:

BEGIN_MESSAGE_MAP(CMyView, CView)
ON_WM_CREATE()
ON_WM_PAINT()
END_MESSAGE_MAP()

便被展开成为这样的代码:

其中AFX_DATADEF和AFX_MSG_CALL又是两个看起来很奇怪的常数。你可以在两个文件中找到它们的定义:

// in \DEVSTUDIO\VC\MFC\INCLUDE\AFXVER_.H
#define AFX_DATA
#define AFX_DATADEF
// in \DEVSTUDIO\VC\MFC\INCLUDE\AFXWIN.H
#define AFX_MSG_CALL

显然它们就像afx_msg一样(我曾经在第6章的HellpMFC 原始代码一出现之后解释过),都只是个"intentional placeholder"(刻意保留的空间),可能在将来会用到,目前则为「无物」。

以图表示BEGIN ON END宏的结果为:

注意:图中的 AfxSigvv 和AfxSig_is 都代表签名符号(Signature)。这些常数在 AFXMSG.H中定义,稍后再述。

前面我说过了,所有能够接收消息的类,都应该派生自 CCmdTarget。那么我们这么推论应该是合情合理的:每一个派生自CCmdTarget的类都应该有DECLARE/BEGIN/END_ 宏组?

唔,错了,CWinThread 就没有!

可是这么一来,CWinApp 通往 CCmdTarget 的路径不就断掉了吗?呵呵,难道 CWinApp不能跳过 CWinThread 直接连上 CCmdTarget 吗?看看下面的 MFC 原始代码:

让我们看看具体的情况。图9-2 就是MFC的消息映射表。当你的派生类使用了DECLARE/BEGIN/END_宏,你也就把自己的消息映射表挂上去了——当然是挂在尾端。

如果没有把BEGIN_MESSAGE_MAP 宏中的两个参数(也就是类本身及其父类的名称)按照规矩来写,可能会发生什么结果呢?消息可能在不应该流向某个类时流了过去,在应该被处理时却又跳离了。总之,完美的机制有了破绽。程序没当掉算你幸运!

图9-2 MFC 消息映射表(也就是消息流动网)

我们终于了解,Message Map既可说是一套宏,也可以说是宏展开后所代表的一套数据结构;甚至也可以说 Message Map 是一种动作,这个动作,就是在刚刚所提的数据结构中寻找与消息相吻合的项目,从而获得消息的处理例程的函数指针。

虽然,C++ 程序员看到多态(Polymorphism),直觉的反应就是虚函数,但请注意,各个 Message Map 中的各个同名函数虽有多态的味道,却不是虚函数。乍想之下使用虚函数是合理的:你产生一个与窗口有关的C++ 类,然后为此窗口所可能接收的任何消息都提供一个对应的虚函数。这的确散发着 C++的味道和面向对象的精神,但现实与理想之间总是有些距离。

要知道,虚函数必须经由一个虚函数表(virtual function table,vtable)实现出来,每一个子类必须有它自己的虚函数表,其内至少有父类之虚函数表的内容复本(请参考第2章「类与对象大解剖」一节)。好哇,虚函数表中的每一个项目都是一个函数指针,价值 4 字节,如果基类的虚函数表有 100 个项目,经过 10 层继承,开枝散叶,总共需耗费多少内存在其中?最终,系统会被巨大的额外负担(overhead)拖垮!

这就是为什么 MFC 采用独特的消息映射机制而不采用虚函数的原因。

米诺托斯(Minotauros)与西修斯(Theseus)

截至目前我还有一些细节没有交待清楚,像是消息的比对动作、消息处理例程的调用动作、以及参数的传递等等,但至少现在可以先继续进行下去,我的目标瞄准消息唧筒(叫捕获也可以啦)。

窗口接收消息后,是谁把消息唧进消息映射网中?是谁决定消息该直直往父映射表走去?还是拐向另一条路(请回头看看图 9-2)?消息的绕行路线,以及MFC 的消息唧筒的设计,活像是米诺托斯的迷宫。不过别担心,我将扮演西修斯,让你免遭毒手。

米诺托斯(Minotauros),希腊神话里牛头人身的怪兽,为克里特岛国王迈诺斯之妻所生。迈诺斯造迷宫将米诺托斯藏于其中,每有人误入迷宫即遭吞噬。怪兽后为雅典王子西修斯(Theseus)所杀。

MFC2.5(注意,是2.5而非4.x)曾经在WinMain的第一个重要动作 AfxWinInit之中,自动为程序注册四个Windows窗口类,并且把窗口函数一致设为AfxWndProc:

下面是AfxWndProc 的内容:

MFC 2.5的CWinApp::Run调用PumpMessage,后者又调用::DispatchMessage,把消息源源推往AfxWndProc(如上),最后流向 pWnd->WindowProc 去。拿 SDK 程序的本质来做比对,这样的逻辑十分容易明白。

MFC 4.x仍旧使用AfxWndProc作为消息唧筒的起点,但其间却隐藏了许多关节。

但愿你记忆犹新,第6章曾经说过,MFC 4.x 适时地为我们注册 Windows 窗口类(在第一次产生该种型式之窗口之前)。这些个 Windows 窗口类的窗口函数各是「窗口所对应之 C++ 类中的 DefWindowProc 成员函数」,请参考第6章「CFrameWnd::Create产生主窗口」一节。这就和 MFC 2.5 的作法(所有窗口类共享同一个窗口函数)有了明显的差异。那么,推动消息的心脏,也就是 CWinThread::PumpMessage 中调用的::DispatchMessage(请参考第6章「CWinApp::Run 程序生命的活水源头」一节),照说应该把消息唧到对应之C++ 类的DefWindowProc 成员函数去。但是,我们发现MFC 4.x中仍然保有和MFC 2.5 相同的AfxWndProc,仍然保有AfxCallWndProc,而且它们扮演的角色也没有变。

事实上,MFC 4.x 利用 hook,把看似无关的动作全牵联起来了。所谓 hook,是 Windows程序设计中的一种高阶技术。通常消息都是停留在消息队列中等待被所隶属之窗口抓取,如果你设立 hook,就可以更早一步抓取消息,并且可以抓取不属于你的消息,送往你设定的一个所谓「滤网函数(filter)」。

请查阅Win32 API 手册中有关于SetWindowsHook和SetWindowsHookEx两函数,以获得更多的hook 信息。(可参考Windows 95:A Developer’s Guide 一书第6章Hooks)

MFC 4.x的hook 动作是在每一个CWnd 派生类之对象产生之际发生,步骤如下:

WH_CBT是众多hook 类型中的一种,意味着安装一个Computer-Based Training(CBT)滤网函数。安装之后,Windows 系统在进行以下任何一个动作之前,会先调用你的滤网函数:

  • 令一个窗口成为作用中的窗口(HCBT_ACTIVATE)

  • 产生或摧毁一个窗口(HCBT_CREATEWND、HCBT_DESTROYWND)

  • 最大化或最小化一个窗口(HCBT_MINMAX)

  • 搬移或缩放一个窗口(HCBT_MOVESIZE)

  • 完成一个来自系统选单的系统命令(HCBT_SYSTEMCOMMAND)

  • 从系统伫列中移去一个滑鼠或键盘消息(HCBT_KEYSKIPPED 、HCBT_CLICKSKIPPED)

因此,经过上述hook 安装之后,任何窗口即将产生之前,滤网函数 _AfxCbtFilterHook一定会先被调用:

啊,非常明显,上面的函数合力做了偷天换日的勾当:把「窗口所属之 Windows 窗口类」中所记录的窗口函数,改换为AfxWndProc。于是,::DispatchMessage 就把消息源源推往AfxWndProc 去了。

这种看起来很迂回又怪异的作法,是为了包容新的3D Controls(细节就容我省略了吧),并与MFC 2.5相容。下图把前述的hook和subclassing 动作以流程图显示出来:

不能稍息,我们还没有走出迷宫!AfxWndProc 只是消息两万五千里长征的第一站!

两万五千里长征——消息的流窜

一个消息从发生到被攫取,直至走向它的归宿,是一条漫漫长路。上一节我们来到了漫漫长路的起头 AfxWndProc,这一节我要带你看看消息实际上如何推动。

消息的流动路线已隐隐有脉络可寻,此脉络是指由BEGIN_MESSAGE_MAP和END_MESSAGE_MAP 以及许许多多ON_WM_xxx 宏所构成的消息映射网。但是唧筒与方向盘是如何设计的?一切的线索还是要靠原始代码透露:

// in WINCORE.CPP(MFC 4.x)
LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    ...
    // messages route through message map
    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
WPARAM wParam = 0, LPARAM lParam = 0)
{
    ...
    // delegate to object's WindowProc
    lResult = pWnd->WindowProc(nMsg, wParam, lParam);
    ...
    return lResult;
}

整个MFC中,拥有虚函数WindowProc者包括CWnd、CControlBar、COleControl、COlePropertyPage、CDialog、CReflectorWnd、CParkingWnd。一般窗口(例如 Frame 窗口、View 窗口)都派生自CWnd,所以让我们看看 CWnd::WindowProc。这个函数相当于C++中的窗口函数:

// in WINCORE.CPP(MFC 4.x)
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
// OnWndMsg does most of the work, except for DefWindowProc call
    LRESULT lResult = 0;
    if (!OnWndMsg(message, wParam, lParam, &lResult))
        lResult = DefWindowProc(message, wParam, lParam);
    return lResult;
}
LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
    if (m_pfnSuper != NULL)
        return::CallWindowProc(m_pfnSuper,m_hWnd,nMsg,wParam,lParam);
    WNDPROC pfnWndProc;
    if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
        return::DefWindowProc(m_hWnd,nMsg,wParam,lParam);
    else
        return::CallWindowProc(pfnWndProc,m_hWnd,nMsg,wParam,lParam);
}

直线上溯(一般 Windows 消息)

CWnd::WindowProc 调用的OnWndMsg是用来分辨并处理消息的专职机构;如果是命令消息,就交给OnCommand处理,如果是通告消息(Notification),就交给OnNotify处理。WM_ACTIVATE和WM_SETCURSOR也都有特定的处理函数。而一般的Windows消息,就直接在消息映射表中上溯,寻找其归宿(消息处理例程)。为什么要特别区隔出命令消息WM_COMMAND和通告消息WM_NOTIFY两类呢?因为它们的上溯路径不是那么单纯地只往父类去,它们可能需要拐个弯。

直线上溯的逻辑实在是相当单纯的了,唯一做的动作就是比对消息映射表,如果吻合就调用表中项目所记录的函数。比对的对象有二,一个是原原本本的消息映射表(那个巨大的结构),另一个是MFC为求快速所设计的一个cache(cache 的实现太过复杂,我并没有把它的原始代码表现出来)。比对成功后,调用对应之函数时,有一个巨大的 switch/case动作,那是为了确保类型安全(type-safe)。稍后我有一个小节详细讨论之。

拐弯上溯(WM_COMMAND 命令消息)

如果消息是WM_COMMAND,你看到了,CWnd::OnWndMsg(上节所述)另辟蹊跷,交由OnCommand来处理。这并不一定就指的是CWnd::OnCommand,得视this 指针指向哪一种对象而定。在MFC之中,以下数个类都改写了OnCommand虚函数:

class CWnd : public CCmdTarget
class CFrameWnd : public CWnd
class CMDIFrameWnd : public CFrameWnd
class CSplitterWnd : public CWnd
class CPropertySheet : public CWnd
class COlePropertyPage : public CDialog

我们挑一个例子来看。假设消息是从 CFrameWnd 进来的好了,于是:

图9-3 当WM_PAINT发生于View窗口,消息的流动路线

// in FRMWND.CPP(MFC 4.0)
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
    ...
    // route as normal command
    return CWnd::OnCommand(wParam, lParam);
}
// in WINCORE.CPP(MFC 4.0)
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
    ...
    return OnCmdMsg(nID, nCode, NULL, NULL);
}

这里调用的OnCmdMsg 并不一定就是指CWnd::OnCmdMsg,得看this指针指向哪一种对象而定。目前情况是指向一个CFrameWnd对象,而MFC之中「拥有」OnCmdMsg的类(注意,此话有语病,我应该说MFC之中「曾经改写」过OnCmdMsg 的类)是:

class CCmdTarget : public CObject
class CFrameWnd : public CWnd
class CMDIFrameWnd : public CFrameWnd
class CView : public CWnd
class CPropertySheet : public CWnd
class CDialog : public CWnd
class CDocument : public CCmdTarget
class COleDocument : public CDocument

显然我们应该往 CFrameWnd 追踪:

这里非常明显地兵分三路,正是为了实践MFC这个Application Framework 对于命令消息的绕行路线的规划:

命令消息接收物的类型        处理次序

图9-4 MFC 对于命令消息WM_COMMAND的特殊处理顺序

让我们锲而不舍地追踪下去:

这反应出图9-4 搜寻路径中「先View 而后Document」的规划。由于CWnd并未改写OnCmdMsg,所以函数中调用的CWnd::OnCmdMsg,其实就是 CCmdTarget::OnCmdMsg:

其中的AfxFindMessageEntry动作稍早我已列出。

当命令消息兵分三路的第一路走到消息映射网的末尾一个类 CCmdTarget,没有办法再「节外生枝」,只能乖乖比对CCmdTarget的消息映射表。如果没有发现吻合者,传回FALSE,引起CView::OnCmdMsg接下去调用 m_pDocument->OnCmdMsg。如果有吻合者,调用全局函数 DispatchCmdMsg:

以下是另一路 CDocument 的动作:

图9-5画出FrameWnd窗口收到命令消息后的四个尝试路径。第3章曾经以一个简单的DOS 程序仿真出这样的绕行路线。

图9-5 FrameWnd 窗口收到命令消息后的四个尝试路径。

第3章曾经以一个 简单的DOS 程序仿真出这样的绕行路线。

OnCmdMsg是各类专门用来对付命令消息的函数。每一个「可接受命令消息之对象」(Command Target)在处理命令消息时都会(都应该)遵循一个游戏规则:调用另一个目标类的OnCmdMsg。这才能够将命令消息传送下去。如果说AfxWndProc是消息流动的「唧筒」,各类的OnCmdMsg 就是消息流动的「转辙器」。

以下我举一个具体例子。假设命令消息从Scribble 的【Edit/Clear All】发出,其处理常式位在CScribbleDoc,下面是这个命令消息的流浪过程:

1.MDI主 视 窗 ( CMDIFrameWnd)收到命令消息WM_COMMAND,其ID为ID_EDIT_CLEAR_ALL。

2.MDI主窗口把命令消息交给目前作用中的MDI子窗口(CMDIChildWnd)。

3.MDI子窗口给它自己的子窗口(也就是View)一个机会。

4.View检查自己的Message Map。

5.View发现没有任何处理例程可以处理此命令消息,只好把它传给 Document。

6.Document检查自己的Message Map,它发现了一个吻合项:

BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
...
END_MESSAGE_MAP()

于是调用该函数,命令消息的流动路线也告终止。

如果上述的步骤 6 仍没有找到处理函数,那么就:

7.Document 把这个命令消息再送到Document Template 对象去。

8.还是没被处理,于是命令消息回到View。

9.View 没有处理,于是又回给MDI子窗口本身。

10.传给CWinApp 对象——无主消息的终极归属。

图9-6 是构成「消息捕获」之各个函数的调用次序。此图可以对前面所列之各个原始代码组织出一个大局观来。

图9-6 构成「消息捕获」之各个函数的调用次序

罗塞达碑石:AfxSig_xx 的奥秘

大架构建立起来了,但我还没有很仔细地解释在消息映射「网」中的 _messageEntries[]数组内容。为什么消息经由推动引擎(上一节谈的那整套家伙)推过这些数组,就可以找到它的处理例程?

Paul DiLascia 在他的文章(¨Meandering Through the Maze of MFC Message and Command Routing¨,Microsoft Systems Journal,1995/07)中形容这些数组之内一笔一笔的记录像是罗塞达碑石,呵呵,就靠它们揭开消息映射的最后谜底了。

罗塞达碑石(Rosetta Stone),1799 年拿破仑远征埃及时,由一名官员在尼罗河口罗塞达发现,揭开了古埃及象形文字之谜。石碑是黑色玄武岩,高 114 公分,厚 28 公分,宽72 公分。经法国学者 Jean-Francois Champollion 研究后,世人因得顺利研读古埃及文献。

消息映射表的每一笔记录是这样的形式:

struct AFX_MSGMAP_ENTRY
{
    UINT nMessage; // windows message
    UINT nCode; // control code or WM_NOTIFY code
    UINT nID; // control ID (or 0 for windows messages)
    UINT nLastID; // used for entries specifying a range of control id's
    UINT nSig; // signature type (action) or pointer to message #
    AFX_PMSG pfn; // routine to call (or special value)
};

内中包括一个Windows 消息、其控制组件ID以及通告代码(notification code,对消息的更多描述,例如ENCHANGED或CBN_DROPDIOWN 等)、一个签名记号、以及一个CCmdTarget 派生类的成员函数。任何一个ON 宏会把这六个项目初始化起来。例如:

你看到了可怕的类型转换动作,这完全是为了保持类型安全(type-safe)。

有一个很莫名其妙的东西:AfxSig_。要了解它作什么用,你得先停下来几分钟,想想另一个问题:当上一节的推动引擎比对消息并发现吻合之后,就调用对应的处理例程,但它怎么知道要交给消息处理例程哪些参数呢?要知道,不同的消息处理例程需要不同的参数(包括个数和类型),而其函数指针(AFX_PMSG)却都被定义为这付德行:

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

这么简陋的信息无法表现应该传递什么样的参数,而这正是 AfxSig_ 要贡献的地方。当推动引擎比对完成,欲调用某个消息处理例程 lpEntry->pfn 时,动作是这样子地(出现在 CWnd::OnWndMsg 和 DispatchCmdMsg 中):

union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
switch (lpEntry->nSig)
{
    case AfxSig_is:
        lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);
        break;
    case AfxSig_lwl:
        lResult = (this->*mmf.pfn_lwl)(wParam, lParam);
        break;
    case AfxSig_vv:
        (this->*mmf.pfn_vv)();
        break;
    ...
}

注意两样东西:MessageMapFunctions和AfxSig。AfxSig 定义于 AFXMSG_.H 檔:

MessageMapFunctions 定义于 WINCORE.CPP檔:

其实呢,真正的函数只有一个pfn,但通过union,它有许多态态不同的形象。pfn_vv 代表「参数为void,传回值为void」;pfn_lwl代表「参数为wParam 和lParam,传回值为LRESULT」;pfn_is代表「参数为LPTSTR 字符串,传回值为 int」。

相当精致,但是也有点儿可怖,是不是?使用 MFC 或许应该像吃蜜饯一样;蜜饯很好吃,但你最好不要看到蜜饯的生产过程!唔,我真的不知道!

无论如何,我把所有的神秘都揭开在你面前了。

Scribble Step2 :UI 对象的变化

理论基础建立完毕,该是实现的时候。Step2 将新增三个选单命令项,一个工具列按钮,并维护这些UI对象的使用状态。

改变选单

Step2 将增加一个【Pen】选单,其中有两个命令项目;并在【Edit】选单中增加一个【Clear All】命令项目:

  • 【Pen/Thick Line】:这是一个切换开关,允许设定使用粗笔或细笔。如果使用者设定粗笔,我们将在这项目的旁边打个勾(所谓的checked);如果使用者选择细笔(也就是在打勾的本项目上再按一下),我们就把勾号去除(所谓的unchecked)。

  • 【Pen/Pen Widths】:这会唤起一个对话框,允许设定笔的宽度。对话框的设计并不在本章范围,那是下一章的事。

  • 【Edit/Clear All】:清除目前作用之Document 数据。当然对应之View窗口内容也应该清干净。

Visual C++ 整合环境中的选单编辑器拥有非常方便的鼠标拖放(drag and drop)功能,所以做出上述的选单命令项不是难事。不过这些命令项目还得经过某些动作,才能与程序代码关联起来发生作用,这方面ClassWizard 可以帮助我们。稍后我会说明这一切。

以下利用Visual C++整合环境中的选单编辑器修改选单:

  • 启动选单编辑器(请参考第4章)。Scribble 有两份选单,IDR_MAINFRAME

适用于没有任何子窗口的情况,IDR_SCRIBTYPE 适用于有子窗口的情况。我

们选择后者。

  • IDR_SCRIBTYPE 选单内容出现于画面右半侧。加入新增的三个命令项。每个

命令项会获得一个独一无二的识别代码,定义于 RESOURCE.H 或任何你指定的文件中。图下方的【Menu Item Properties】对话框在你双击某个命令项后出现,允许你更改命令项的识别代码与提示字符串(将出现在状态列中)。如果你对操作过程不熟练,请参考Visual C++ User's Guide(Visual C++ Online 上附有此书之电子版)。

  • 三个新命令项的ID 值以及提示字符串整理于下:
【Pen/Thick Line】
ID : ID_PEN_THICK_OR_THIN
prompt : "Toggles the line thickness between thin and thick\nToggle pen"
【Pen/Pen Widths】
ID : ID_PEN_WIDTHS
prompt : "Sets the size of the thin and thick pen\nPen thickness"
【Edit/Clear All】
ID : ID_EDIT_CLEAR_ALL (这是一个预先定义的 ID,有预设的提示字符串,请更改如下)prompt : "Clears the drawing\nErase All"

注意:每一个提示字符串都有一个\n 子字符串,那是作为工具列按钮的「小黄卷标」的标签内容。「小黄标签」(学名叫作 tool tips)是 Windows 95 新增的功能。

对Framework 而言,命令项的ID是用以识别命令消息的唯一依据。你只需在【Properties】对话框中键入你喜欢的 ID 名称(如果你不满意选单编辑器自动给你的那个),至于它真正的数值不必在意,选单编辑器会在你的 RESOURCE.H 檔中加上定义值。

经过上述动作,选单编辑器影响我们的程序代码如下:

// in RESOURCE.H
#define ID_PEN_THICK_OR_THIN 32772
#define ID_PEN_WIDTHS 32773
(注:另一个ID ID_EDIT_CLEAR_ALL已预先定义于AFXRES.H 中)
// in SCRIBBLE.RC
IDR_SCRIBBTYPE MENU PRELOAD DISCARDABLE
BEGIN
...
POPUP "&Edit"
BEGIN
...
MENUITEM "Clear &All", ID_EDIT_CLEAR_ALL
END
POPUP "&Pen"
BEGIN
MENUITEM "Thick &Line", ID_PEN_THICK_OR_THIN
MENUITEM "Pen &Widths...", ID_PEN_WIDTHS
END
...
END
STRINGTABLE DISCARDABLE
BEGIN
ID_PEN_THICK_OR_THIN  "Toggles the line thickness between thin and thick\nToggle pen"
ID_PEN_WIDTHS "Sets the size of the thin and thick pen\nPen thickness"
END
STRINGTABLE DISCARDABLE
BEGIN
ID_EDIT_CLEAR_ALL "Clears the drawing\nErase All"
...
END

改变工具列

过去,也就是Visual C++ 4.0之前,改变工具列有点麻烦。你必须先以图形编辑器修改工具列对应之bitmap图形,然后更改程序代码中对应的工具列按钮识别代码。现在可就轻松多了,工具列编辑器让我们一气呵成。主要原因是,工具列现今也成为了资源的一种。下面是 Scribble Step1 的工具列:

现在我希望为【Pen/Thick Line】命令项设计一个工具列按钮,并且把 Scribble 用不到的三个预设按钮去除(分别是 Cut、Copy、Paste):

编辑动作如下:

  • 启动工具列编辑器,选择IDR_MAINFRAME。有一个绘图工具箱出现在最右侧。

    将三个用不着的按钮除去:以鼠标拖拉这些按钮,拉到工具列以外即可。

  • 在工具列最右侧的空白按钮上作画,并将它拖拉到适当位置。

  • 为了让这个新的按钮起作用,必须指定一个ID给它。我们希望这个按钮相当于【Pen/Thick Line】命令项,所以它的ID当然应该与该命令项的ID相同,也就是ID_PEN_THICK_OR_THIN。双击这个新按钮,出现【Toolbar Button Properties】对话框,请选择正确的ID。注意,由于此一ID 先前已定义好,所以其提示字符串以及小黄卷标也就与此一工具列按钮产生了关联。

  • 存文件。

  • 工具列编辑器为我们修改了工具列的bitmap图形文件内容:

IDR_MAINFRAME  BITMAP  MOVEABLE PURE "res\\Toolbar.bmp"

同时,工具列项目也由原来的:

IDR_MAINFRAME TOOLBAR DISCARDABLE  16, 15
BEGIN
BUTTON ID_FILE_NEW
BUTTON ID_FILE_OPEN
BUTTON ID_FILE_SAVE
SEPARATOR
BUTTON ID_EDIT_CUT
BUTTON ID_EDIT_COPY
BUTTON ID_EDIT_PASTE
SEPARATOR
BUTTON ID_FILE_PRINT
BUTTON ID_APP_ABOUT
END

改变为:

IDR_MAINFRAME TOOLBAR DISCARDABLE  16, 15
BEGIN
BUTTON ID_FILE_NEW
BUTTON ID_FILE_OPEN
BUTTON ID_FILE_SAVE
SEPARATOR
BUTTON ID_PEN_THICK_OR_THIN
SEPARATOR
BUTTON ID_FILE_PRINT
BUTTON ID_APP_ABOUT
END

利用 ClassWizard 连接命令项识别代码与命令处理函数

新增的三个命令项和一个工具列按钮,都会产生命令消息。接下来的任务就是为它们指定一个对应的命令消息处理例程。下面是一份整理:

UI 对象(命令项)       项目识别代码         处理例程

【Pen/Thick Line】 ID_PEN_THICK_OR_THIN  OnPenThickOrThin

【Pen/Pen Widths】 ID_PEN_WIDTHS         OnPenWidths(第10章再处理)

【Edit/Clear All】 ID_EDIT_CLEAR_ALL     OnEditClearAll

消息与其处理例程的连接关系是在程序的Message Map中确立,而 Message Map可藉由ClassWizard 或WizardBar 完成。第8章已经利用这两个工具成功地为三个标准的Windows 消息(WM_LBUTTONDOWN、WM_LBUTTONUP、WM_MOUSEMOVE)设立其消息处理函数,现在我们要为 Step2 新增的命令消息设立消息处理例程。过程如下:

  • 首先你必须决定,在哪里拦截【Edit/Clear All】才好?本章前面对于消息映射与命令绕行的深度讨论这会儿派上了用场。【Edit/Clear All】这个命令的目的是要清除文件,文件的根本是在数据的「体」,而不在数据的「面」,所以把文件的命令处理例程放在 Document 类中比放在 View 类来得高明。命令消息会不会流经 Document 类?经过前数节的深度之旅,你应该自有定论了。

  • 所以,让我们在CScribbleDoc的WizardBar选择【Object IDs】为ID_EDIT_CLEAR_ALL,并选择【Messages】为COMMAND。

  • 猜猜看,如果你在【Object IDs】中选择CScribbleDoc,右侧的【Messages】清单会出现什么?什么都没有!因为Document类只可能接受WM_COMMAND,这一点你应该已经从前面所说的消息递送过程中知道了。如果你在 CScribbleApp的WizardBar上选择【Object IDs】为CScribbleApp,右侧的【Messages】清单中也是什么都没有,道理相同。

  • 你会获得一个对话框,询问你是否接受一个新的处理例程。选择Yes,于是文字编辑器中出现该函数之骨干,等待你的幸临...。

这样就完成了命令消息与其处理函数的连接工作。这个工作称为"command binding"。我们的原始代码获得以下修改:

  • Document 类之中多了一个函数声明:

    class CScribbleDoc : public CDocument
    {
    protected:
        afx_msg void OnEditClearAll();
               ...
    }
    
  • Document类的Message Map中多了一笔记录:

    BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
    ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
    ...
    END_MESSAGE_MAP()
    
  • Document 类中多了一个函数空壳:

    void CScribbleDoc::OnEditClearAll()
    {
    }
    
  • 现在请写下 OnEditClearAll 函数代码:

依此要领,我们再设计OnPenThickOrThin 函数。此一函数用来更改现行的笔宽,与Document 有密切关系,所以在Document 类中放置其消息处理例程是适当的:

void CScribbleDoc::OnPenThickOrThin()
{
    // Toggle the state of the pen between thin or thick.
    m_bThickPen = !m_bThickPen;
    // Change the current pen to reflect the new user-specified width.
    ReplacePen();
}
void CScribbleDoc::ReplacePen()
{
    m_nPenWidth = m_bThickPen? m_nThickWidth : m_nThinWidth;
    //Change the current pen to reflect the new user-specified width.
    m_penCur.DeleteObject();
    m_penCur.CreatePen(PS_SOLID,m_nPenWidth,RGB(0,0,0));//solid black
}

注意,ReplacePen 并非由WizardBar(或ClassWizard)加上去,所以我们必须自行在CScribbleDoc 类中加上这个函数的声明:

class CScribbleDoc : public CDocument
{
protected:
    void ReplacePen();
    ...
}

OnPenThickOrThin 函数用来更换笔的宽度,所以CScribbleDoc势必需要加些新的成员变量。变量m_bThickPen 用来记录目前笔的状态(粗笔或细笔),变量m_nThinWidth和m_nThickWidth 分别记录粗笔和细笔的笔宽——在Step2中此二者固定为2和5,原本并不需要变量的设置,但下一章的 Step3 中粗笔和细笔的笔宽可以更改,所以这里未雨绸缪:

class CScribbleDoc : public CDocument
{
// Attributes
protected:
    UINT     m_nPenWidth;     // current user-selected pen width
    BOOL     m_bThickPen;     // TRUE if current pen is thick
    UINT     m_nThinWidth;
    UINT     m_nThickWidth;
    Cpen     m_penCur;        // pen created according to
    ...
}

现在重新考虑文件初始化的动作,将Step1的:

void CScribbleDoc::InitDocument()
{
    m_nPenWidth = 2; // default 2 pixel pen width
    // solid, black pen
    m_penCur.CreatePen(PS_SOLID, m_nPenWidth, RGB(0,0,0));
}

改变为Step2 的:

void CScribbleDoc::InitDocument()
{
    m_bThickPen = FALSE;
    m_nThinWidth = 2;   // default thin pen is 2 pixels wide
    m_nThickWidth = 5;  // default thick pen is 5 pixels wide
    ReplacePen();       // initialize pen according to current width
}

维护UI对象状态(UPDATE_COMMAND_UI)

上一节我曾提过WizardBar 右侧的【Messages】清单中,针对各个命令项,会出现COMMAND和UPDATE_COMMAND_UI两种选择。后者做什么用?

一个选单拉下来,使用者可以从命令项的状态(打勾或没打勾、灰色或正常)得到一些状态提示。如果Document中没有任何数据的话,【Edit/Clear All】照道理就不应该起作用,因为根本没数据又如何"Clear All" 呢 ?! 这时候我们应该把这个命令项除能(disable)。又例如在粗笔状态下,程序的【Pen/Thick Line】命令项应该打一个勾(所谓的check mark),在细笔状态下不应该打勾。此外,选单命令项的状态应该同步影响到对应之工具列按钮状态。

所有UI对象状态的维护可以依赖所谓的UPDATE_COMMAND_UI 消息。

传统SDK 程序中要改变选单命令项状态,可以调用EnableMenuItem或是CheckMenuItem,但这使得程序杂乱无章,因为你没有一个固定的位置和固定的原则处理命令项状态。MFC 提供一种直觉并且仍旧依赖消息观念的方式,解决这个问题,这就是UPDATE_COMMAND_UI 消息。其设计理念是,每当选单被拉下并尚未显示之前,其命令项(以及对应之工具列按钮)都会收到 UPDATE_COMMAND_UI 消息,这个消息和 WM_COMMAND 有一样的绕行路线,我们(程序员)只要在适当的类中放置其处理函数,并在函数中做某些判断,便可决定如何显示命令项。

这种方法的最大好处是,不但把问题的解决方式统一化,更因为 Framework传给UPDATE_COMMAND_UI处理例程的参数是一个「指向CCmdUI对象的指针」,而CCmdUI 对象就代表着对应的选单命令项,因此你只需调用CCmdUI 所准备的,专门用来处理命令项外观的函数(如 Enable 或 SetCheck)即可。我们的工作量大为减轻。

图9-7 ON_COMMAND和ON_UPDATE_COMMAND_UI的运作

图9-7以【Edit/Clear All】实例说明ON_COMMAND和 ON_UPDATE_COMMAND_UI的运作。为了拦截UPDATE_COMMAND_UI 消息,你的 Command Target 对象(也许是Application,也许是windows,也许是Views,也许是Documents)要做两件事情:

1. 利用WizardBar(或ClassWizard)加上一笔Message Map 项目如下:

ON_UPDATE_COMMAND_UI(ID_xxx, OnUpdatexxx)

2. 提供一个OnUpdatexxx 函数。这个函数的写法十分简单,因为 Framework 传来一个代表UI对象(也就是选单命令项或工具列按钮)的 CCmdUI 对象指针,而对UI对象的各种操作又都已设计在CCmdUI成员函数中。举个例子:

void CScribbleDoc::OnUpdateEditClearAll(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(!m_strokeList.IsEmpty());
}
void CScribbleDoc::OnUpdatePenThickOrThin(CCmdUI* pCmdUI)
{
    pCmdUI->SetCheck(m_bThickPen);
}

如果命令项与某个工具列按钮共享同一个命令ID,上述的Enable动作将不只影响命令项,也影响按钮。命令项的打勾(checked)即是按钮的按下(depressed),命令项没有打勾(unchecked)即是按钮的正常化(松开)。

现在,Scribble 第二版全部修改完毕,制作并测试之:

  • 在整合环境中按下【Build/Build Scribble】编译并链接。

  • 按下【Build/Execute】执行 Scribble。测试细笔粗笔的运作情况,以及【Edit /Clear All】是否生效。

从写程序(而不是挖背后意义)的角度去看 Message Map,我把Step2 所进行的选单改变对Message Map 造成的影响做个总整理。一共有四个相关成份会被ClassWizard(或WizardBar)产生出来,下面就是相关原始代码,其中只有第 4 项的函数内容是我们撰写的,其它都由工具自动完成。

1. CSRIBBLEDOC.CPP

BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
//{{AFX_MSG_MAP(CScribbleDoc)
ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)
ON_UPDATE_COMMAND_UI(ID_EDIT_CLEAR_ALL, OnUpdateEditClearAll)
ON_UPDATE_COMMAND_UI(ID_PEN_THICK_OR_THIN,OnUpdatePenThickOrThin)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

不要去掉//{{ 和//}},否则下次ClassWizard或WizardBar不能正常工作。

2. CSRIBBLEDOC.H

class CScribbleDoc : public CDocument
{
    ...
    // Generated message map functions
    protected:
    //{{AFX_MSG(CScribbleDoc)
    afx_msg void OnEditClearAll();
    afx_msg void OnPenThickOrThin();
    afx_msg void OnUpdateEditClearAll(CCmdUI* pCmdUI);
    afx_msg void OnUpdatePenThickOrThin(CCmdUI* pCmdUI);
    //}}AFX_MSG
    ...
};

3. RESOURCE.H

#define ID_PEN_THICK_OR_THIN 32772
#define ID_PEN_WIDTHS 32773

(另一个项目ID_EDIT_CLEAR_ALL已经在AFXRES.H中定义了)

4. SCRIBBLEDOC.CPP

void CScribbleDoc::OnEditClearAll()
{
    DeleteContents();
    SetModifiedFlag(); //Mark the document as having been modified,for
    //purposes of confirming File Close.
    UpdateAllViews(NULL);
}
void CScribbleDoc::OnPenThickOrThin()
{
    // Toggle the state of the pen between thin or thick.
    m_bThickPen = !m_bThickPen;
    // Change the current pen to reflect the new user-specified width.
    ReplacePen();
}
void CScribbleDoc::ReplacePen()
{
    m_nPenWidth = m_bThickPen? m_nThickWidth : m_nThinWidth;
    // Change the current pen to reflect the new user-specified width.
    m_penCur.DeleteObject();
    m_penCur.CreatePen(PS_SOLID,m_nPenWidth,RGB(0,0,0));//solid black
}
void CScribbleDoc::OnUpdateEditClearAll(CCmdUI* pCmdUI)
{
    // Enable the command user interface object (menu item or tool bar
    // button) if the document is non-empty, i.e., has at least one stroke.
    pCmdUI->Enable(!m_strokeList.IsEmpty());
}
void CScribbleDoc::OnUpdatePenThickOrThin(CCmdUI* pCmdUI)
{
    // Add check mark to Draw Thick Line menu item, if the current
    // pen width is "thick".
    pCmdUI->SetCheck(m_bThickPen);
}

本章回顾

这一章主要为Scribble Step2 增加新的选单命令项。在这个过程中我们使用了工具列编辑器和ClassWizard(或Wizardbar)等工具。工具的使用很简单,但是把消息的处理常式加在什么地方却是关键。因此本章一开始先带你深入探索MFC原始代码,了解消息的递送以及所谓Message Map背后的意义,并且也解释了命令消息(WM_COMMAND) 特异的绕行路线及其原因。

我在本章中挖出了许多MFC原始代码,希望藉由原始代码的自我说明能力,加深你对消息映射与消息绕行路径的了解。这是对 MFC「知其所以然」的重要关键。这个知识基础不会因为MFC的原始代码更动而更动,我要强调的,是其原理。