MFC消息机制 3 (对1、2的合并)
1消息的分类
1.1队列消息、非队列消息
l 队列消息:
windows为每个应用程序都建立一个消息队列,那么通过消息队列,进行传送的消息都属于队列消息;一般来说,由鼠标、键盘产生的消息都属于队列消息。(为什么呢?想想,鼠标、键盘事件都是由系统捕获的,系统捕获后要传递给应用程序,就一定的通过消息队列);
l 非队列消息:
除了队列消息,剩下的自然而然就是非队列消息了;
u 队列消息是通过PostMessage()的方式投递消息的,这样的消息发送也叫“寄送”,该函数寄送消息即可返回,不需要等待程序处理结果;
u 非队列消息是通过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_Create、WM_Move等等 |
操作系统、或控制其它窗口的窗口使用 |
凡派生自CWnd的类,才可以接收标准消息 |
|
以下是几个问题:
(((((一些问题:假如在CDialog上单击一个按钮,发现传递的是一个命令消息,为什么是命令消息,而不是一个控件通知消息呢?而且我们可以看到,在cpp文件的消息映射宏中,添加的代码是;
ON_BN_CLICKED(IDC_BUTTON1, &CTest234Dlg::OnBnClickedButton1)
而不是一般命令消息的OnCommand,我们追踪一下ON_BN_CLICKED,可以看到如下定义:
#define ON_BN_CLICKED(id, memberFxn) ON_CONTROL(BN_CLICKED, id, memberFxn)
再追踪一下ON_CONTROL,
#define ON_CONTROL(wNotifyCode, id, memberFxn) \
{ WM_COMMAND, (WORD)wNotifyCode, (WORD)id, (WORD)id, AfxSigCmd_v, \
(static_cast< AFX_PMSG > (memberFxn)) },
看到了没?本质上,单击按钮产生的也是一个WM_COMMAND消息)))))
搞不明白,什么样的操作产生命令消息?什么样的操作产生控件通知消息?又是什么样的操作产生标准消息?
好,现在来回答这个问题:
这是因为控件通知并不一定都是以WM_Notify形式出现的,通知消息经历了三个阶段的演变:
1、 窗口消息的子集形式。形式:WM_××××
2、 与命令消息共享格式。形式:WM_COMMAND。之上的ON_BN_CLICKED就是这种形式
(通知消息的命令消息,与标准的命令消息有区别,区别就在于lParam参数是否为空。标准WM_COMMAND的lParam参数是NULL)
3、 真正意义上的通知消息,形式:WM_NOTIFY。
为什么出现这种演变呢?因为Windows的控件越来越丰富,当向父窗口发送通知消息时,需要传递的信息越来越复杂,比如想在点击Tree-View Control控件时,传递点击的位置信息,那么WM_COMMAND就满足不了需要了,窗口消息更不行,所以从MFC4.0以后,提出了WM_NOTIFY消息形式。具体WM_NOTIFY怎么传参数,参考其它资料。
所以控件消息表现出多种形态,但通过消息映射宏,都能跟踪到到底是那种形态。
再看一个,假如在对话框上单击鼠标左键,怎么响应该消息呢?
需要自己添加宏ON_WM_LBUTTONDOWN()
然后在头文件中,自己添加消息响应函数:afx_msg void OnLButtonDown(UINT, CPoint);
就可以捕获该消息了!
那么ON_WM_LBUTTONDOWN()是个什么东西呢?追踪一下:
#define ON_WM_LBUTTONDOWN() \
{ WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< void (AFX_MSG_CALL CWnd::*)(UINT, CPoint) > ( &ThisClass :: OnLButtonDown)) },
看来,WM_LBUTTONDOWN就是所谓的标准消息(又叫一般消息、窗口消息等等,反正就是非命令消息)。为什么不用命令消息呢?因为命令消息传递的信息有限,而这里,点击鼠标左键需要传递点击位置信息。
再跟踪一下源码,标准消息是在
CWnd::OnWndMsg函数中,在筛除了WM_Command和WM_Notify消息之后处理的,这也就解释了前面那句话,“凡派生自CWnd的类,才可以接收标准消息”。因为OnWndMsg是CWnd定义的一个虚函数,像CDocument继承自CCmdTarget,根本就没有OnWndMsg,当然无法处理标准消息了。
然而,看一下MFC一些基类的继承关系,如下图:
Cobject
|
CCmdTarget
|
|----CWinThread---CWinApp---CMyWinApp
|
|----CWnd----
| |----CView
| |----CFrameWnd
|
|----CDocument----CmyDoc
看上图的继承关系,可以这么说,
a) 凡是派生自CWnd的类,可以拦截并处理任何Windows消息,不管是WM_Command,还是WM_Notify,还是其它什么WM_Create、WM_Move等消息;
b) 而与窗口无关的CwinApp类、Cdocument类,因为继承自CCmdTarget,只能处理WM_Command消息;因为没有继承自CWnd,不能处理一般消息。(想想,为什么不继承自CWnd,就不能处理非命令消息?想想类名吧,CCmdTarget)
2消息的处理流程
好了,有了前面1、2小节的基础,我们接下来来看消息的处理流程:
以下描述消息从AfxWndProc起点开始的处理流程:
1) AfxWndProc(HWND hWnd,Unit nMsg,….)
说明,这里的hWnd指定了到底从哪个窗口开始接收并处理消息;
在AfxWndProc中,调用了AfxCallWndProc函数,
pWnd已经不是句柄了,而是由句柄得到的一个具体的CWnd指针指向的窗口类CWnd* pWnd=CWnd::FormatHandlePermant(hWnd)
2) AfxCallWndProc(CWnd *pWnd,Unit nMsg,…..)
说明:,在AfxCallWndProc函数中,调用了pWnd->WindowProc (nMsg,…….)
其实至此,消息走到了具体的MFC管理范围之内,以后消息就交给MFC类来处理了!
这里需要注意的是,其实pWnd->WindowProc,并不一定调用的就是CWnd的WindowProc函数,因为WindowProc是个虚函数,实际中,要看从一开始,即AfxWndProc开始,hWnd指的是什么窗口界面,当然这个窗口类肯定继承自CWnd,所以才可以用CWnd基类指针嘛!实际中,CcontrolBar、COleControl、ColePropertyPage、CDialog、等等都重载了WindowProc,而且CFrameWnd、Cview都继承自CWnd。这里其实隐含了一个道理,即从AfxWndProc接收的消息,入口都是CWnd的继承类,可以是CDialog,CView(以及继承类),CframeWnd等等。
问题,AfxWndProc的起点可以是非CWnd子类吗?比如Cdocument?能否向一个Cdocument SendMessage? 好了先不说这个,继续往下走。。。。
3) CWnd::WindowProc()
在这个函数中,又调用了
OnWndMsg()
以及OnWndMsg处理失败后的DefWindowProc
先不管DefWindowProc,先来看看OnWndMsg(),当然具体调用哪个类的OnWndMsg,要看当前的CWnd子类是什么,是个Cview?CframeWnd?Cdialog?
CWnd的OnWndMsg函数是个虚函数,许多子类都重载了OnWndMsg(),在“深入浅出MFC”中,只分析了CWnd的OnWndMsg
好了继续往下走,看看CWnd的OnWndMsg是怎么处理消息的:
l 对于WM_Command消息,调用 OnCommand
l 对于WM_Notify消息,调用OnNotify
l 对于除此之外的一般消息,就依据消息映射表上溯了
接下来我们分析对WM_Command消息的处理
4) OnCommand
OnCommand是CWnd的一个虚函数,实际中要看This指针指得是CWnd的哪个派生类。这些类都重载了OnCommand,有CWnd、CframeWnd、CMDIFrameWnd、CspliteFrameWnd等等。(看了一下源码,CFrameWnd::Oncommand最终也调用了CWnd::OnCommand)
实际中,要看消息是从哪个类中进来的,比如消息是从CframeWnd进来的,那么就调用CframeWnd的OnCommand,
而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开始呢?往前想想,我们分析的这条消息是从哪里进来的?CMyFrameWnd,this指针指向CMyFrameWnd,当然要从CmyFrameWnd开始了,CmyFrameWnd只是借用了CcmdTarget的代码而已
8) 好,如果1路、2路、都处理不了,最后只能寄希望于3路了,即pApp->OnCmdMsg
如果3路也处理不了,那只好go back。。go back。。回到3)的DefWindowProc了
好了 我们完整分析了消息从CmyFrameWnd进来的处理流程,这个处理是参考”深入浅出MFC”中的,书中有一个图9-6,从大局上描述了这个流程。MFC消息处理确实很复杂,需要很多遍才稍微感觉有点儿明白了的。其实,这里面最关键的就是多态!多态!一定不要被代码里面写的类名迷糊,要时刻想想当前代码段儿的This指针指得对象是谁?消息是从哪里进来的?
还有几个问题不知道对不对,或者不理解,如下:
1、“深入浅出MFC”只列举了一个命令消息,从CframeWnd进来的处理流程,那么消息从其它地方进来的流程是什么呢?比如从Cview、Cdialog进来呢?
估计只是从CframeWnd进来的消息处理流程的一段儿,所以候捷只列举了一个,自己可以跟踪验证一下。Cview传入的消息,能不能被CframeWnd或者CmyApp处理呢?
2、“深入浅出MFC”只列举了命令消息(WM_Command)的处理流程,那么WM_Notify和其它一般消息的处理流程是什么呢?
3、注意,真正处理消息的是CCmdTarget::OnCmdMsg函数,这里说的真正处理,是指“对比消息映射表,找到对应的处理函数,并调用之”。所以才有了那句说法:一个类要想接收并处理消息,必须继承自CCmdTarget。
但实际上,许多类不仅继承自CCmdTarget,还重载了CCmdTaregt的OnCmdMsg函数,这是为什么呢?其实这里重载并不是为了改变前面说的“对比消息映射表,找到处理函数..”,重载是为了实现消息的转向,即消息的分发!
比如:CframeWnd实现了消息的重载,如下
CframeWnd::OnCmdMsg()
{
(注意,这里出现了多路分支)
1路、 Cview::pView->OnCmdMsg;
2路、 CWnd->OnCmdMsg
3路、 pApp->OnCmdMsg
}//CframeWnd重载OnCmdMsg只是为了实现消息的转向
Cview也重载了OnCmdMsg,也是为了实现消息的分发
Cview::OnCmdMsg
{
1、CWnd::OnCmdMsg A处
2、m_pDocument->OnCmdMsg
} //A处,其实没做什么,只是调用了CCmdTarget::OnCmdMsg代码,针对当前
This指针指向的对象,进行了“对比消息映射表,找到对应的处理函数,并调
用之”
所以一个消息如果是从一个CView进来的,那么消息的流向也就检查View,
然后Document,对于CDialog,如果一个消息是从一个CDialog进来的,那么消息
的流向也就直接是CCmdTarget了。(注意,CWnd没有重写OnCmdMsg,所以
实际调用的是CcmdTarget::OnCmdMsg)
4、关于OnCommand和OnCmdMsg的作用的不同
A、首先OnCommand是CWnd的虚函数;而OnCmdMsg是CCmdTarget的虚函数;
B、OnCommand调用了OnCmdMsg;OnCommand是用来处理消息(控件、菜单、加速键)的,而OnCmdMsg是用来分发消息的(当然CCmdTarget中的OnCmdMsg最终判断消息是否在当前对象的路由表中)
关于这点可查看MSDN:CWnd::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中的控件句柄。这样一来,wParam和lParam就都被填充了,没有额外的空间来传递一些其它的消息,例如鼠标按下的位置和时间。这下明白了~早期的一些控件通知消息,确实是以WM_Command方式发送的,后来才引入WM_Notify消息)
5、控件通知消息的流程:
首先,什么是控件通知消息呢?
附:关于怎么跟踪消息处理流程
1、在CWnd::OnCommand函数处设断点
总结:一些个人感悟
1、 想想,与Windows SDK编程相比?其实MFC也就多了一个CDocument,并且让命令消息能够流到CDocument中去。这一点而是通过引入CcmdTarget实现了的。
看看这个类继承关系:
CCmdTarget
|
|----CWinThread---CWinApp---CMyWinApp
|
|----CWnd----
| |----CView
| |----CFrameWnd
|
|----CDocument----CmyDoc
除了CwinThread和CDocument这两个类以及很少的继承类,其它很多都继承自CWnd