第一点:类别型录网的搭建:
类别型录网搭建的目的是为了实现所谓的"执行期类型识别",也就是在程序运行的时候识别出某个对象是否是某个类的实例(基类也可以)。这里还不是很明白为什么需要实现"执行期类型识别",这种技巧具体被应用在哪里。
例如在MFC中CView继承于CWnd,那么可以进行这样的判断:
CView view;
bool result = view.IsKindOf(CWnd); // result == true
如上,通过调用IsKindOf函数,可以判断出view对象是一个CWnd类的实例。
MFC通过建立一张型录网来实现这样的功能。也就是把类登记到一个表中,传入特定的参数之后在这张表上进行查找比对,从而实现"执行期类型识别"。
具体说来比对过程如下:
为了在不同的对象和类之间互相比对,肯定类里得有一个特殊的标识才行,MFC通过为每个类添加一个CRuntimeClass类型的静态成员来作为这个标识。
注意这里是静态成员,其原因不言自明,如果是普通成员的话,不同的对象之间成员都不一样,无法实现比对。
每个类都有了特殊标识之后,仅仅能进行一对一的比对,也就是说只能进行CView.IsKindOf(CView)这样的操作,无法判断一个CView对象是否也是一个CWnd对象。
MFC实现这种功能的方法类似于链表的实现:链表中有一个指针专门指向它下一个成员的位置,遍历时依靠这个指针来不断指向下一个。
CRuntimeClass类包含一个指针叫m_pBaseClass,对于CView,它有一个CRuntimeClass成员(就是之前说的特殊标识),只要使得这个成员的m_pBaseClass指针指向CWnd的CRuntimeClass成员,那么就建立起了类似链表的结构。
当判断CView.IsKindOf(CWnd)时,首先判断CView的CRuntimeClass成员和CWnd的CRuntimeClass成员是不是一致,发现不一致之后,在CView的CRuntimeClass成员中根据m_pBaseClass来得到CView的父类CWnd的CRuntimeClass成员,之后再进行比对,发现是一致的,因此可以判断CView.IsKindOf(CWnd)为真。
下面介绍MFC中对上述机制的具体实现方法:
1.为每个类添加特定标识CRuntimeClass成员:
使用DECLARE_DYNAMIC宏:
class CView : public CWnd
{
DECLARE_DYNAMIC(CView)
如上,使用了DECLARE_DYNAMIC宏之后,CView类中多了一个CRuntimeClass类型的静态成员 classCView(名为classXXXX,也就是在类名之前加一个class),也就是之前所说的具有比较功能的"特殊标识"
2.建立类别型录表:
也就是初始化classCView,使它的m_pBaseClass指针指向父类
IMPLEMENT_DYNCREATE(CView, CWnd)
如上,静态成员的初始化需要在实现文件中进行,在实现文件中使用了IMPLEMENT_DYNAMIC宏之后,classCView的m_pBaseClass指针指向了CWnd的classCWnd成员
3.实现类型识别IsKindOf:
this->IsKindOf(RUNTIME_CLASS(CWnd))
如上,IsKindOf函数的参数有点特别,是一个RUNTIME_CLASS宏,这个宏的功能其实非常简单,其实就是一个函数调用:RUNTIME_CLASS(CWnd)等价于CWnd::GetThisClass(),这个函数的返回值就是CWnd的CRuntimeClass成员,也就是CWnd的"特殊标识",把这个特殊标识传递给IsKindOf函数之后,事情就好办许多,逐个提取CView及其父类的CRuntimeClass成员与这个标识进行比对就可以达到判断的目的了。因为是静态变量,所以只存有一份拷贝,可以直接把指针作为比较时的参照。
第二点:消息映射表的搭建:
搭建消息映射表的目的是为了找到一个消息对应的消息处理函数。对于一个CMyView窗口来说,它的某个消息处理函数有可能并不是存在于CMyView类中,而是存在于它的父类甚至是别的类里面(例如数据操作应该放在CDoc类里面处理比较合适),MFC为了找到正确的消息处理函数,遂给每个类都建立一个表来存储这个类所拥有的消息处理函数,并通过指针连接起来,这样就可以通过遍历查找来找到正确的那个消息处理函数。
之前类别型录网的搭建是以CRuntimeClass作为一个类的特别标识,而这里需要标识的则是消息和它对应的消息处理函数。也就是说,每个类里都存储一张表,表里包括了这个类可以处理的消息和对应的处理函数,这样对于每个类,得到一条消息之后,将这条消息和表里的条目进行比对,如果比对成功,调用对应的处理函数就可以了。
除此之外,还需要一个指针来指向这个类的父类
下面介绍MFC对上面机制的具体实现方法:
1.为每个类添加消息条目和指针成员:
AFX_MSGMAP_ENTRY[] 数组用来存储消息和对应的消息响应函数指针
AFX_MSGMAP 结构,其中包含一个AFX_MSGMAP类型的指针指向基类,以及一个AFX_MSGMAP_ENTRY指针,指向之前的数组。
这样一个类就需要两个静态成员就行了,一个是AFX_MSGMAP类型,一个是AFX_MSGMAP_ENTRY数组。
MFC中使用DECLARE_MESSAGE_MAP宏来实现为一个类添加这两个成员的功能。
2.建立消息映射表:
也就是为AFX_MSGMAP_ENTRY[]数组添加成员,并且把指针指向基类的AFX_MSGMAP静态成员:
BEGIN_MESSAGE_MAP(theClass, baseClass)
ON_COMMAND(MSGID,msgpfn)
END_MESSAGE_MAP()
另外,MFC还为每个类添加了一个虚函数GetMessageMap用于得到这个类的AFX_MSGMAP静态成员指针,由此指针即可进行遍历。
第三点:命令绕行
命令绕行的目的是找到一个消息正确的消息处理函数。
对于WM_LBUTTONDOWN这样的消息来说,消息处理函数都在本窗口类(或者父类)里面定义,使用GetMessageMap得到消息映射表指针之后遍历映射表就能找到对应的消息处理函数。
但对于WM_COMMAND消息来说,消息处理函数不一定是在本类里面,CFrameWnd窗口接收到的WM_COMMAND消息,其消息处理函数有可能在CView里面。之所以会这样应该是与MFC Frame\View\Doc框架有关,具体原因以后进一步来理解,这里主要讲解一下绕行的实现机制。
绕行的实现机制其实非常简单,就是一个if语句的判断,例如对于CFrameWnd,先看看CView里有没有这个消息的处理函数,如果没有,再遍历自己的映射表看看有没有,如果还是没有,就看看CWinApp里有没有,再没有的话,就交给默认函数处理。
下面先给出消息绕行时的路径:
MFC消息必然是属于某个窗口的(MSG结构里还有个HWND字段呢),也就是说在MFC框架中,窗口的产生者只能是CWnd的派生类(CView和CFrameWnd等)。
而这些窗口所使用的窗口过程函数其实都是同一个全局函数AfxWndProc,也就是说消息产生之后都会被放到AfxWndProc中进行处理。
省去中间的调用步骤,AfxWndProc在接收到不同窗口的消息之后会调用CWnd->WindowProc()函数:
1.如果是WM_XXXX函数,直接使用GetMessageMap得到消息映射表指针,遍历查找消息处理函数。
2.如果是WM_COMMAND函数,则调用CWnd::OnCommand(),对于不同的窗口对象,由于多态的原因,调用的也不会是同一个OnCommand()函数,例如有CFrameWnd::OnCommand()等等。这个函数其实算不上重点,真正起作用的是CWnd::OnCmdMsg()函数。
OnCmdMsg()函数是CCmdTarget类里的函数,其中的关键代码就是遍历消息映射表找到消息处理函数。在CCmdTarget的子类中有几个类重写了这个函数,这几个类分别是CFrameWnd,CView,CDoc。
假如现在是CFrameWnd窗口接收到了WM_COMMAND消息,如下是CFrameWnd::OnCmdMsg()的主要代码:
|
如上,在CFrameWnd的OnCmdMsg函数里,分别调用了CView,CWnd(并没有重载,所以实际上调用的是CCmdTarget的OnCmdMsg函数),CMyApp类的OnCmdMsg函数。也就是分别在CView、CWnd(其实就是CCmdTarget)、CWinApp里寻找消息处理函数。一旦找到消息处理函数之后,就调用之,然后OnCmdMsg函数返回。
如上,即解释了来自CFrameWnd窗口的WM_COMMAND消息是如何绕行的,其实也就是分别调用CView、CWnd(CCmdTarget)、CWinApp的OnCmdMsg函数而已。
同理,CView和CWinApp以及CDocument类都以类似的方式来对OnCmdMsg函数来进行调用,从而实现命令绕行机制。
以面向对象的思想理解MFC
最开始的MFC框架只是用两个类来对原本的SDK流程进行封装,MFC使用两个类来抽象这个流程。一个是CWinApp,封装了创建窗口(通过实例化一个Frame对象来实现),消息循环的主流程;一个是CMainFrame,封装了窗口注册,创建,及消息处理等内容。
CWinApp(也许说CWinThread更合适一点)类封装了传统Win32程序的主流程。一般来说一个Win32程序里会有注册窗口类,创建窗口,进行消息循环这几个步骤。这几个步骤在CWinApp类里都存在着,其中注册窗口类并创建窗口的过程被封装到InitInstance成员函数里面,消息循环被封装到了Run成员函数里面。
CMainFrame(也许说CWnd更合适一点)类封装了Win32程序中有关窗口的那部分东西,具体说来就是窗口类的注册,窗口的创建,以及窗口消息的处理。其中窗口注册被封装在PreCreateWindow函数里面,窗口的创建则被封装在Create函数里面(更准确地说是封装在CWnd::CreateEx函数里),还有一个窗口过程函数在哪里这个暂时还没有搞清楚。
后来之所以有了CView和CDoc,是因为原来的CFrame负担了过多的责任。这里把数据的管理交给了CDoc类来负责,把数据的显示交给了CView类来负责,明确了各自的责任。
这里CView类仍然是一个单独的窗口,从功能上来说应该和CFrame处于同等地位的,仍然有自己的窗口过程函数。只是从职责上来讲CView只负责数据的显示,而CFrame作为CView外部的一个框架提供别的一些功能。
由上可知,程序的功能实际上是被封装到了Frame/View/Doc三个类里,其实是这三个类来合作完成程序的某个功能,正因为这样,对于一个COMMEND消息,就可以交给这三个类里的某一个类来处理,不管这个消息是来源于Frame还是View。比如在菜单上单击一个"更新"的选项。本来这个消息是由Frame接收到的,但是由View来处理会更简单,因为View类本身持有更新时所需要的一些信息。
从这点上可以理解,Frame/View/Doc三个类是相互关联的一个整体。
接着上面的阐述多说两句:
MFC之所以创造了消息机制是为了实现其Frame/View/Doc三位一体的架构。
Frame/View/Doc架构的意义在于将处理消息的职责分配到合理的类中去处理,例如在菜单上点击一个"保存"选项,处理这个消息就应该交给Doc类来实现,而如果点击"更新"选项,则将这个消息交给View类来处理更方便一些。
MFC的消息机制就负责将消息交给合适的类去处理。下面解释消息机制的实现思路:
1.比对思路和消息表:为每一个类建立一个消息表,这个消息表里包括了这个类能够处理的消息有哪些,消息和其处理函数也一一对应。这样的话就可以遍历这张消息表并进行比对来知道这个类可以处理那些消息,并能够一个消息的处理函数。
2.遍历思路:要让Frame里产生的消息在Doc类里进行处理,其思路也是简单的遍历:先看看View里有没有能处理的,要是没有,再看看自己类里能不能处理,要是再不行,再看看Doc类里有没有对应的处理函数。
3.Frame/View/Doc三位一体:这个我也没记清楚,貌似在创建一个Frame的时候都会顺带创建出其对应的View和Doc(单文档的情况,多文档貌似要创建多个)。所以上面可以由View找到其对应的Doc。