MFC消息机制 3 (对1、2的合并)

1消息的分类

1.1队列消息、非队列消息

队列消息:

windows为每个应用程序都建立一个消息队列,那么通过消息队列,进行传送的消息都属于队列消息;一般来说,由鼠标、键盘产生的消息都属于队列消息。(为什么呢?想想,鼠标、键盘事件都是由系统捕获的,系统捕获后要传递给应用程序,就一定的通过消息队列);

非队列消息:

除了队列消息,剩下的自然而然就是非队列消息了;

队列消息是通过PostMessage()的方式投递消息的,这样的消息发送也叫寄送,该函数寄送消息即可返回,不需要等待程序处理结果;

非队列消息是通过SendMessage()的方式进行的,这样的消息发送叫发送;消息不需要进入窗口的消息队列,

然而不管是队列消息,还是非队列消息,消息处理的起点都是AfxWndProc不同的是队列消息,是操作系统把消息投放到消息队列,应用程序空闲是,通过一个消息循环,搜索消息队列,不停的从消息队列抓取消息,并处理。大致流程是:CwinThread->PumpMessage->CWnd->PreTranslateMessage->…………..->USER32内核->AfxWndProcBase->AfxWndProc->…….(继续处理)

而非队列消息呢(即通过SendMessage方式发送的消息)?它是直接进入了USER32内核,然后处理的流程和队列消息一样了。

注意,不管是队列消息,还是非队列消息,都是从USER32内核开始,转到了AfxWndProcBase(有时候不经过这里),再到AfxWndProc,所以可以认为AfxWndProc是消息传递与处理的起点!

补充一点儿:按理说,从USER32出来后,不管是队列消息,还是非队列消息,应该由Windosw系统发往各个窗口的消息处理函数(这个处理函数是DefWindowProc,这是很直觉的想法,而且传统的SDK程序确实是这样的,但是MFC程序比传统的SDK多了document/view,如果如果某个消息是做文档处理,那么就让这个消息直接流到document中去不是更好吗?所以才有了MFC命令传递机制、MFC消息映射的出现),但是为什么都统一到了AfxWndProc这里呢?这里用到了钩子技术。(关于这一段儿描述的,不管是深入浅出MFC”“MFC逆向分析精通MFC”都由介绍,但是都不是很清楚,需要再仔细揣摩。而且由于MFC版本的不同,实际的函数流程也可能和资料描述的不同)

历史渊源:

[深入浅出MFC.候捷]关于消息本来应该到哪里,而事实上如何都流到AfxWndProc,在MFC的不同版本中是不同的:

MFC2.5中,WinMain的第一个重要操作AfxWinInit,自动为程序注册了四个窗口类,并且把窗口函数一致设置为AfxWndProc,那么消息都流向AfxWndProc是自然而然的事儿了;

MFC4.x以后,Windows窗口类的窗口函数是窗口所对应的DefWindowProc,不是AfxWndProc了,但是消息还是一致流到AfxWinProc,并以此做为起点,MFC4.x其实隐藏了一些关节,它使用hook(钩子)技术,实现了AfxWinProc的汇集;

1.2命令消息、通知消息、一般消息

下面以表格来分析这几类消息:

形式

来源

谁能处理?

命令消息

WM_Command

菜单、工具栏

凡派生自CcmdTarget的类,都可以。范围比较广,比如框架、视图、文档、文档模板、应用程序类等

通知消息

经历了三种演变:

1、 窗口消息

2、 命令消息

3、 WM_Notify

子控件传给父控件的。只有标准控件能够触发:按钮、列表框、组合框、编辑框、树型控件、列表控件

一般消息(又叫标准消息、窗口消息)

有说,除了WM_Command,其它都属于标准消息,WM_(任意),比如WM_CreateWM_Move等等

操作系统、或控制其它窗口的窗口使用

派生自CWnd的类,才可以接收标准消息

以下是几个问题:

(((((一些问题:假如在CDialog上单击一个按钮,发现传递的是一个命令消息,为什么是命令消息,而不是一个控件通知消息呢?而且我们可以看到,在cpp文件的消息映射宏中,添加的代码是;

ON_BN_CLICKED(IDC_BUTTON1, &CTest234Dlg::OnBnClickedButton1)

而不是一般命令消息的OnCommand,我们追踪一下ON_BN_CLICKED,可以看到如下定义:

#define ON_BN_CLICKED(idmemberFxn  ON_CONTROL(BN_CLICKEDidmemberFxn)

再追踪一下ON_CONTROL

#define ON_CONTROL(wNotifyCodeidmemberFxn) \

WM_COMMAND, (WORD)wNotifyCode, (WORD)id, (WORD)idAfxSigCmd_v, \

(static_castAFX_PMSG > (memberFxn)) },

看到了没?本质上,单击按钮产生的也是一个WM_COMMAND消息)))))

搞不明白,什么样的操作产生命令消息?什么样的操作产生控件通知消息?又是什么样的操作产生标准消息?

好,现在来回答这个问题:

这是因为控件通知并不一定都是以WM_Notify形式出现的,通知消息经历了三个阶段的演变:

1、 窗口消息的子集形式。形式:WM_××××

2、 与命令消息共享格式。形式:WM_COMMAND。之上的ON_BN_CLICKED就是这种形式

(通知消息的命令消息,与标准的命令消息有区别,区别就在于lParam参数是否为空。标准WM_COMMANDlParam参数是NULL

3、 真正意义上的通知消息,形式:WM_NOTIFY

为什么出现这种演变呢?因为Windows的控件越来越丰富,当向父窗口发送通知消息时,需要传递的信息越来越复杂,比如想在点击Tree-View Control控件时,传递点击的位置信息,那么WM_COMMAND就满足不了需要了,窗口消息更不行,所以从MFC4.0以后,提出了WM_NOTIFY消息形式。具体WM_NOTIFY怎么传参数,参考其它资料。

所以控件消息表现出多种形态,但通过消息映射宏,都能跟踪到到底是那种形态。

再看一个,假如在对话框上单击鼠标左键,怎么响应该消息呢?

需要自己添加宏ON_WM_LBUTTONDOWN()

然后在头文件中,自己添加消息响应函数:afx_msg void OnLButtonDown(UINTCPoint);

就可以捕获该消息了!

那么ON_WM_LBUTTONDOWN()是个什么东西呢?追踪一下:

#define ON_WM_LBUTTONDOWN() \

WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, \

(AFX_PMSG)(AFX_PMSGW) \

(static_castvoid (AFX_MSG_CALL CWnd::*)(UINTCPoint) > ( &ThisClass :: OnLButtonDown)) },

看来,WM_LBUTTONDOWN就是所谓的标准消息(又叫一般消息、窗口消息等等,反正就是非命令消息)。为什么不用命令消息呢?因为命令消息传递的信息有限,而这里,点击鼠标左键需要传递点击位置信息。

再跟踪一下源码,标准消息是在

CWnd::OnWndMsg函数中,在筛除了WM_CommandWM_Notify消息之后处理的,这也就解释了前面那句话,“派生自CWnd的类,才可以接收标准消息”。因为OnWndMsgCWnd定义的一个虚函数,像CDocument继承自CCmdTarget,根本就没有OnWndMsg,当然无法处理标准消息了。

然而,看一下MFC一些基类的继承关系,如下图:

Cobject

   |

CCmdTarget

   |

   |----CWinThread---CWinApp---CMyWinApp

   |

   |----CWnd----

   |           |----CView

   |           |----CFrameWnd

   |

   |----CDocument----CmyDoc

看上图的继承关系,可以这么说,

a) 凡是派生自CWnd的类,可以拦截并处理任何Windows消息,不管是WM_Command,还是WM_Notify,还是其它什么WM_CreateWM_Move等消息;

b) 而与窗口无关的CwinApp类、Cdocument类,因为继承自CCmdTarget,只能处理WM_Command消息;因为没有继承自CWnd,不能处理一般消息。(想想,为什么不继承自CWnd,就不能处理非命令消息?想想类名吧,CCmdTarget

2消息的处理流程

好了,有了前面12小节的基础,我们接下来来看消息的处理流程:

以下描述消息从AfxWndProc起点开始的处理流程:

1) AfxWndProc(HWND hWndUnit nMsg….)

说明,这里的hWnd指定了到底从哪个窗口开始接收并处理消息;

AfxWndProc中,调用了AfxCallWndProc函数,

pWnd已经不是句柄了,而是由句柄得到的一个具体的CWnd指针指向的窗口类CWnd* pWnd=CWnd::FormatHandlePermant(hWnd)

2) AfxCallWndProc(CWnd *pWndUnit nMsg…..)

说明:,在AfxCallWndProc函数中,调用了pWnd->WindowProc (nMsg…….)

其实至此,消息走到了具体的MFC管理范围之内,以后消息就交给MFC类来处理了!

这里需要注意的是,其实pWnd->WindowProc,并不一定调用的就是CWndWindowProc函数,因为WindowProc是个虚函数,实际中,要看从一开始,即AfxWndProc开始,hWnd指的是什么窗口界面,当然这个窗口类肯定继承自CWnd,所以才可以用CWnd基类指针嘛!实际中,CcontrolBarCOleControlColePropertyPageCDialog、等等都重载了WindowProc,而且CFrameWndCview都继承自CWnd这里其实隐含了一个道理,即从AfxWndProc接收的消息,入口都是CWnd的继承类,可以是CDialogCView(以及继承类)CframeWnd等等。

问题,AfxWndProc的起点可以是非CWnd子类吗?比如Cdocument?能否向一个Cdocument SendMessage? 好了先不说这个,继续往下走。。。。

3) CWnd::WindowProc()

在这个函数中,又调用了

OnWndMsg()

    以及OnWndMsg处理失败后的DefWindowProc

    先不管DefWindowProc,先来看看OnWndMsg(),当然具体调用哪个类的OnWndMsg,要看当前的CWnd子类是什么,是个CviewCframeWndCdialog

   CWndOnWndMsg函数是个虚函数,许多子类都重载了OnWndMsg(),在深入浅出MFC”中,只分析了CWndOnWndMsg

    好了继续往下走,看看CWndOnWndMsg是怎么处理消息的:

对于WM_Command消息,调用 OnCommand

对于WM_Notify消息,调用OnNotify

对于除此之外的一般消息,就依据消息映射表上溯了

接下来我们分析对WM_Command消息的处理

4) OnCommand

OnCommandCWnd的一个虚函数,实际中要看This指针指得是CWnd的哪个派生类。这些类都重载了OnCommand,有CWndCframeWndCMDIFrameWndCspliteFrameWnd等等。(看了一下源码,CFrameWnd::Oncommand最终也调用了CWnd::OnCommand)

实际中,要看消息是从哪个类中进来的,比如消息是从CframeWnd进来的,那么就调用CframeWndOnCommand

CframeWnd::OnCommand(…)

{ ……

  CWnd::OnCommand()

}

CWnd::OnCommand()

{…..

   OnCmdMsg()

}

注意,这时候消息是从CframeWnd进来的,那么实际调用的就是CframeWnd::OnCmdMsg()

实际中,许多类都重写了OnCmdMsg函数,有如下:

CcmdTarget::Cobject

CframeWnd::CWnd

CMDIFrameWnd::CframeWnd

Cview::CWnd

Cdialog::CWnd

那么,消息从哪里进来,就调用谁的OnCmdMsg,比如从Cview进来,自然调用Cview->OnCmdMsg

下面来看看CframeWnd::OnCmdMsg()

5) CframeWnd::OnCmdMsg()

{

  (注意,这里出现了多路分支)

1、  Cview::pView->OnCmdMsg;

  2、  CWnd->OnCmdMsg

  3、  pApp->OnCmdMsg

}

6) 先说1

Cview::OnCmdMsg

{

1、 CWnd::OnCmdMsg   A

2、 m_pDocument->OnCmdMsg

}

1中,由于CWnd没有重写OnCmdMsg,所以实际调用的是CcmdTarget::OnCmdMsg,那么CcmdTarget::OnCmdMsg做什么工作呢?它的工作就是对比消息映射表,看看能不能拦截该消息。

(这里千万不要犯迷糊,不要被CcmdTarget迷惑,因为是从A进来的,那么其实对比的是1 pView的消息映射表)

如果Cview处理不了该消息,那就只好由2处的m_pDocument进行处理了,即

View然后文档

7) 如果1处理不了,则交给2处理

按照深入浅出MFC”的说法, 2实际上是判断当前框架的消息映射表,能不能处理当前消息,即沿着CMyFrameWnd->CFrameWnd->CWnd的路径来寻找消息映射匹配,刚开始我还不明白,在2处,明明是CWnd->OnCmdMsg,怎么会先从CmyFrameWnd开始呢?往前想想,我们分析的这条消息是从哪里进来的?CMyFrameWndthis指针指向CMyFrameWnd,当然要从CmyFrameWnd开始了,CmyFrameWnd只是借用了CcmdTarget的代码而已

8) 好,如果12、都处理不了,最后只能寄希望于3了,即pApp->OnCmdMsg

如果3也处理不了,那只好go back。。go back。。回到3)的DefWindowProc

好了 我们完整分析了消息从CmyFrameWnd进来的处理流程,这个处理是参考深入浅出MFC”中的,书中有一个图9-6,从大局上描述了这个流程。MFC消息处理确实很复杂,需要很多遍才稍微感觉有点儿明白了的。其实,这里面最关键的就是多态!多态!一定不要被代码里面写的类名迷糊,要时刻想想当前代码段儿的This指针指得对象是谁?消息是从哪里进来的?

还有几个问题不知道对不对,或者不理解,如下:

1深入浅出MFC”只列举了一个命令消息,从CframeWnd进来的处理流程,那么消息从其它地方进来的流程是什么呢?比如从CviewCdialog进来呢?

估计只是从CframeWnd进来的消息处理流程的一段儿,所以候捷只列举了一个,自己可以跟踪验证一下Cview传入的消息,能不能被CframeWnd或者CmyApp处理呢?

2深入浅出MFC”只列举了命令消息(WM_Command)的处理流程,那么WM_Notify和其它一般消息的处理流程是什么呢?

3注意,真正处理消息的是CCmdTarget::OnCmdMsg函数,这里说的真正处理,是指对比消息映射表,找到对应的处理函数,并调用之。所以才有了那句说法:一个类要想接收并处理消息,必须继承自CCmdTarget

   但实际上,许多类不仅继承自CCmdTarget,还重载了CCmdTaregtOnCmdMsg函数,这是为什么呢?其实这里重载并不是为了改变前面说的对比消息映射表,找到处理函数..”,重载是为了实现消息的转向,即消息的分发!

            比如:CframeWnd实现了消息的重载,如下

            CframeWnd::OnCmdMsg()

   {

      (注意,这里出现了多路分支)

1、  Cview::pView->OnCmdMsg;

  2、  CWnd->OnCmdMsg

  3、  pApp->OnCmdMsg

           }//CframeWnd重载OnCmdMsg只是为了实现消息的转向

 

           Cview也重载了OnCmdMsg,也是为了实现消息的分发

          Cview::OnCmdMsg

   {

1CWnd::OnCmdMsg   A

2m_pDocument->OnCmdMsg

           } //A处,其实没做什么,只是调用了CCmdTarget::OnCmdMsg代码,针对当前

          This指针指向的对象,进行了对比消息映射表,找到对应的处理函数,并调 

          用之

            所以一个消息如果是从一个CView进来的,那么消息的流向也就检查View 

        然后Document,对于CDialog,如果一个消息是从一个CDialog进来的,那么消息

         的流向也就直接是CCmdTarget了。(注意,CWnd没有重写OnCmdMsg,所以

         实际调用的是CcmdTarget::OnCmdMsg

4、关于OnCommandOnCmdMsg的作用的不同

   A、首先OnCommandCWnd的虚函数;而OnCmdMsgCCmdTarget的虚函数;

   BOnCommand调用了OnCmdMsgOnCommand是用来处理消息(控件、菜单、加速键)的,而OnCmdMsg是用来分发消息的(当然CCmdTarget中的OnCmdMsg最终判断消息是否在当前对象的路由表中)

   关于这点可查看MSDNCWnd::OnCommand   The framework calls this member function when the user selects an item from a menu, when a child control sends a notification message, or when an accelerator keystroke is translated.

这里有个不明白的地方,子控件通知消息怎么也被OnCommand捕获了呢?

(关于OnCommand,请在MSDN中搜索Command Routing

(关于OnCommand,请在MSDN中搜索Command Routing

解答:在最初的windows3.x中,根本就不存在什么WM_NOTIFY,控件通知它们父窗口,如鼠标点击,控件背景绘制事件,通过发送一个消息到父窗口。简单的通知仅发送一个WM_COMMAND消息,包含一个通知码和一个在wParam中的控件ID及一个在lPraram中的控件句柄。这样一来,wParamlParam就都被填充了,没有额外的空间来传递一些其它的消息,例如鼠标按下的位置和时间。这下明白了~早期的一些控件通知消息,确实是以WM_Command方式发送的,后来才引入WM_Notify消息)

5、控件通知消息的流程:

   首先,什么是控件通知消息呢?

附:关于怎么跟踪消息处理流程

1、CWnd::OnCommand函数处设断点

总结:一些个人感悟

1、 想想,与Windows SDK编程相比?其实MFC也就多了一个CDocument,并且让命令消息能够流到CDocument中去。这一点而是通过引入CcmdTarget实现了的。

看看这个类继承关系:

CCmdTarget

   |

   |----CWinThread---CWinApp---CMyWinApp

   |

   |----CWnd----

   |           |----CView

   |           |----CFrameWnd

   |

   |----CDocument----CmyDoc

   除了CwinThreadCDocument这两个类以及很少的继承类,其它很多都继承自CWnd

         

posted @ 2011-01-19 16:15  pjh123  阅读(628)  评论(0编辑  收藏  举报