《深入浅出MFC》– 深入消息映射与命令传递

1.前面已经在MFC六大关键技术之仿真中描述了大概的原理,现在我们来继续深入这个问题。

 

2.MFC把消息分为三类:

①命令消息(WM_COMMAND):命令消息意味着 “使用者命令程序做某些操作”。凡由UI对象产生的消息都是这种命令消息,可能来自菜单或加速键或工具栏按钮,并且都以WM_COMMAND呈现,如何分辨来自各处的命令消息?SDK程序主要靠消息的wParam识别之,MFC程序则主要靠菜单项的识别码(menu ID)识别之—-两者其实是相同的。

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

②标注消息:除WM_COMMAND之外,任何以WM_开头的都算是这一类。任何派生自CWnd之类,均可接收此消息。

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

 

3.三个奇怪的宏

第一个宏:BEGIN_MESSAGE_MAP有两个参数,分别是拥有此消息映射表的类,及其父类。

第二个宏:ON_COMMAND,指定命令消息的处理函数名称。还可以有许多种,ON_WM_CHAR、ON_WM_CLOSE之类。

第三个宏:END_MESSAGE_MAP作为结尾记号。

 

4.我们说过,所有能够接收消息的类,都应该派生自CCmdTarget。那么我们这么推论应该是合情合理:每一个派生自CCmdTarget的类都应该有上面的三个宏?

不对!CWinThread就没有。

那么CWinApp通往CCmdTarget的路径不就断掉了吗?不,不,不…

我们来看CWinApp的宏:

BEGIN_MESSAGE_MAP(CWinApp,  CCmdTarget)    //注意第二个参数是CCmdTarget,而不是CWinThread。

 

5.为什么MFC采用消息映射机制而不采用虚函数呢?

要知道,虚函数必须经由一个虚函数表实现出来,每一个子类必须有它自己的虚函数表,其内至少有父类之虚函数表的内容复本。那好,虚函数表中的每一个项目都是一个函数指针,价值4字节,如果基类的虚函数表有100项,经过10层继承,开枝散叶,总共需耗费多少内存在其中?最终,系统会被巨大的额外负担拖垮!

 

6.MFC2.5的CWinApp::Run调用PumpMessage,后者又调用::DispatchMessage,把消息源源推往AfxWndProc,最后流向pWnd->WindowProc去。

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

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

MFC4.x的hook操作是在每一个CWnd派生类之对象时发生,在任何窗口即将产生之前,过滤函数一定会先被调用。我们直接来看图:

image

 

7.整个MFC中,拥有虚函数WindowProc者包括CWnd、CControlBar、CDialog…一般窗口(Frame窗口,View窗口)都派生自CWnd,所以直接看Cwnd::WindowProc,相当于窗口函数。

Cwnd::WindowProc调用了OnWndMsg用来分辨并处理消息,如果是命令消息,就交给OnCommand处理,如果是通知消息(Notification),就交给OnNotify处理。为啥要区别?一般的Windows消息,直接在消息映射表中上溯,寻找其消息处理程序,而命令消息的上溯路径不是那么单纯地只往父类去,他们可能要拐弯。

 

8.直线上溯:

image

比较消息映射表,如果吻合就调用表中项目所记录的函数。

 

9.拐弯上溯:

image

下面是这个命令消息的流浪过程:

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 对象– 无主消息的终极归属。

 

10.AfxSig_xx的奥秘

当我们找到消息的处理程序后,怎么知道要交给消息处理程序哪些参数呢?

由于消息处理函数的类型各异,MFC使用了AfxSig_来说明消息处理函数的类型。在找到某消息的消息处理函数之后,判断其类型再进行响应转换。

union MessageMapFunctions mmf;

mmf.pfn=lpEntry->pfn;

switch(lpEntry->nSig)

{

  case AfxSig_isg:

       lResult=(this->*mmf.pfn_is)(LPTSTR)lParam);

       break;

   case Afx_Sig_lwl:

       lResult=(this->*mmf.pfn_lwl)(wParam,lParam);

       break;

   case AfxSig_vv:

      (this->*mmf.pfn_vv)();

      break;

    ……..

}

注意:MessageMapFunctions 和AfxSig_  他们定义于AFXMSG_.H文件。位于:C:\Program Files (x86)\Microsoft Visual Studio\VC98\MFC\Include

真正的函数只有一个pfn,但通过union,它有许多不同的形象。

AfxSig_is代表参数为LPTSTR字符串,返回值为int.

Afx_lwl代表参数wiewParam和lParam。返回值为LRESULT。

Afx_vv代表参数和返回值都为void.

(this->*mmf.pfn_vv)();中的pfn_vv是union MessageMapFunctions的一个成员。如

union MessageMapFunctions

{

   AFX_PMSG  pfn;

   bool (AFX_MSG_CALL CWnd::*pfn_bD)(CDC*);

   void (AFX_MSG_CALL CWnd::*pfn_VV)(CDC*);

   ………….

};

注意MessageMapFunctions是union类型的哦。

 

11.ON_COMMAND和ON_UPDATE_COMMAND_UI的运行。

image

 

 

转载请标注来源:http://www.alonemonkey.com

By:AloneMonkey

本文链接:http://www.alonemonkey.com/mfc-seven.html