MFC六大核心机制概述html
咱们选择了C++,主要是由于它够艺术、够自由,使用它咱们能够实现各类想法,而MFC将多种可灵活使用的功能封装起来,咱们岂能忍受这种“黑盒”操做?因而研究分析MFC的核心机制成为必然。ios
首先,列出要讲的MFC六大核心机制:程序员
一、MFC程序的初始化。
二、运行时类型识别(RTTI)。
三、动态建立。
四、永久保存。
五、消息映射。
六、消息传递。编程
本文讲第一部分,MFC程序的初始化过程。小程序
简单的MFC窗口程序windows
设计一个简单完整MFC程序,产生一个窗口。固然这不能让AppWizard自动为咱们生成。咱们能够在Win32 Application工程下面那样写:数组
C++代码数据结构
1 #include <afxwin.h> 2 class MyApp : public CWinApp 3 { 4 public: 5 BOOL InitInstance() //②程序入点 6 { 7 CFrameWnd *Frame=new CFrameWnd();//构造框架 8 m_pMainWnd=Frame; //将m_pMainWnd设定为Frame; 9 Frame->Create(NULL,"最简单的窗口");//创建框架 10 Frame->ShowWindow(SW_SHOW); //显示框架 11 return true; //返回 12 } 13 }; 14 MyApp theApp; //①创建应用程序。
设定连接MFC库,运行,便可看见一个窗口。框架
从上面,你们能够看到创建一个MFC窗口很容易,只用两步:一是从CWinApp派生一个应用程序类(这里是MyApp),而后创建应用程序对象(theApp),就能够产生一个本身须要的窗口(即须要什么样就在InitInstance()里建立就好了)。编辑器
整个程序,就改写一个InitInstance()函数,建立那么一个对象(theApp),就是一个完整的窗口程序。这就是“黑盒”操做的魔力!
在咱们正想为微软鼓掌的时候,咱们忽然以为内心空荡荡的,咱们想知道微软帮咱们作了什么事情,而咱们想编本身的程序时又须要作什么事情,哪怕在上面几行的程序里面,咱们还有不清楚的地方,好比,干吗有一个m_pMainWnd指针变量,它从哪里来,又要到哪里去呢?想想在DOS下编程是多么美妙的一件事呵,咱们须要什么变量,就声明什么变量,须要什么样的函数,就编写什么样的函数,或者引用函数库……可是如今咱们怎么办?
咱们能够逆向思惟一下,MFC要达到这种效果,它是怎么作的呢?首先咱们要弄明白,VC++不是一种语言,它就象咱们学c语言的时候的一个相似记事本的编辑器(请原谅个人不贴切的比喻),因此,在VC里面咱们用的是C++语言编程,C++才是根本(初学者老是觉得VC是一门什么新的什么语言,一门比C++先进不少的复杂语言,汗)。说了那么多,我想用一句简单的话归纳“MFC黑箱’,就是为咱们的程序加入一些固化的‘C++代码’的东西”。
既然MFC黑箱帮咱们加入了代码,那么你们想一想它会帮咱们加入什么样的代码呢?他会帮咱们加入求解一元二次方程的代码吗?固然不会,因此它加入的其实是每次编写窗口程序必须的,通用的代码。
再往下想,什么才是通用的呢?咱们每次视窗编程都要写WinMain()函数,都要有注册窗口,产生窗口,消息循环,回调函数……即然每次都要的东西,就让它们从咱们眼前消失,让MFC帮忙写入!
手动模拟MFC程序的初始化
要知道MFC初始化过程,你们固然能够跟踪执行程序。但这种跟踪很麻烦,我相信你们都会跟踪的晕头转向。本人以为哪怕你理解了MFC代码,也很容易让人找不着北,咱们彻底不懂的时候,在成千上万行程序的迷宫中如何能找到出口?
咱们要换一种方法,不如就来从新编写个MFC库吧,哗!你们不要笑,当心你的大牙,我不是疯子(虽然疯子也说本身不疯)。咱们要写的就是最简单的MFC类库,就是把MFC宏观上的,理论上的东西写出来。咱们要用最简化的代码,简化到恰好能运行。
1、须要“重写”的MFC库
既然,咱们这一节写的是MFC程序的初始化过程,上面咱们还有了一个可执行的MFC程序。程序中只是用了两个MFC类,一个是CWinApp,另外一个是CFrameWnd。固然,还有不少一样重要MFC类如视图类,文档类等等。但在上面的程序能够不用到,因此暂时省去了它(总之是为了简单)。
好,如今开始写MFC类库吧……唉,面前又有一个大难题,就是让你们背一下MFC层次结构图。天,那张鱼网怎么记得住,但既然咱们要理解他,总得知道它是从那里派生出来的吧。
考虑到你们都很辛苦,那咱们看一下上面两个类的父子关系(箭头表明派生):
CObject->CCmdTarget->CWinThread->CWinApp->本身的重写了InitInstance()的应用程序类。
CObject(同上)->CCmdTarget(同上)->CWnd->CFrameWnd
看到层次关系图以后,终于能够开始写MFC类库了。按照上面层次结构,咱们能够写如下六个类(为了直观,省去了构造函数和析构函数)。
C++代码
15 /////////////////////////////////////////////////////////
16 class CObiect{};//MFC类的基类。
17 class CCmdTarget : public CObject{};
18 ------------------------------------------------
19 class CWinThread : public CCmdTarget{};
20 class CWinApp : public CWinThread{};
21 ------------------------------------------------
22 class CWnd : public CCmdTarget{};
23 class CFrameWnd : public CWnd{};
24 /////////////////////////////////////////////////////////
你们再想一下,在上面的类里面,应该有什么?你们立刻会想到,CWinApp类或者它的基类CCmdTarget里面应该有一个虚函数virtual BOOL InitInstance(),是的,由于那里是程序的入口点,初始化程序的地方,那天然少不了的。可能有些朋友会说,反正InitInstance()在派生类中必定要重载,我不在CCmdTarget或CWinApp类里定义,留待CWinApp的派生类去增长这个函数可不能够。扯到这个问题可能有点越说越远,但我想信C++的朋友对虚函数应该是没有太多的问题的。总的来讲,做为程序员若是清楚知道基类的某个函数要被派生类用到,那定义为虚函数要方便不少。
也有不少朋友问,C++为何不自动把基类的全部函数定义为虚函数呢,这样能够省了不少麻烦,这样全部函数都遵守派生类有定义的函数就调用派生类的,没定义的就调用基类的,不用写virtual的麻烦多好!其实,不少面向对象的语言都这样作了。但定义一个虚函数要生成一个虚函数表,要占用系统空间,虚函数越多,表就越大,有时得不偿失!这里哆嗦几句,是由于日后要说明的消息映射中你们更加会体验到这一点,好了,就此打往。
上面咱们本身解决了一个问题,就是在CCmdTarge写一个virtual BOOL InitInstance()。
2、WinMain()函数和CWinApp类
你们再往下想,咱们还要咱们MFC“隐藏”更多的东西:WinMain()函数,设计窗口类,窗口注册,消息循环,回调函数……咱们立刻想到封装想封装他们。你们彷佛隐约地感受到封装WinMain()不容易,以为WinMain()是一个特殊的函数,许多时候它表明了一个程序的起始和终结。因此在之前写程序的时候,咱们写程序习惯从WinMain()的左大括写起,到右大括弧返回、结束程序。
咱们换一个角度去想,有什么东西能够拿到WinMain()外面去作,许多初学者们,总以为WinMain()函数是天大的函数,什么函数都好象要在它里面才能真正运行。其实这样了解很片面,甚至错误。咱们能够写一个这样的C++程序:
C++代码
25 ////////////////////////////////////////////////////
26 #include <iostream.h>
27 class test{
28 public:
29 test(){cout<<"请改变你对main()函数的见解!"<<endl;}
30 };
31 test test1;
32 /**************************/
33 void main(){}
34 ////////////////////////////////////////////////////
在上面的程序里,入口的main()函数表面上什么也不作,但程序执行了(注:实际入口函数作了一些咱们能够不了解的事情),并输出了一句话(注:全局对象比main()首先运行)。如今你们能够知道咱们的WinMain()函数能够什么都不作,程序依然能够运行,但没有这个入口函数程序会报错。
那么WinMain()函数会放哪一个类上面呢,请看下面程序:
C++代码
35 #include <afxwin.h>
36 class MyApp : public CWinApp
37 {
38 public:
39 BOOL InitInstance() //②程序入点
40 {
41 AfxMessageBox("程序依然能够运行!");
42 return true;
43 }
44 };
45
46 MyApp theApp; //①创建应用程序。
你们能够看到,我并无构造框架,而程序却能够运行了——弹出一个对话框(若是没有WinMain()函数程序会报错)。上面我这样写仍是为了直观起见,其实咱们只要写两行程序:
#include <afxwin.h>
CWinApp theApp; //整个程序只构造一个CWinApp类对象,程序就能够运行!
因此说,只要咱们构造了CWinApp对象,就能够执行WinMain()函数。咱们立刻相信WinMain()函数是在CWinApp类或它的基类中,而不是在其余类中。其实这种见解是错误的,咱们知道编写C++程序的时候,不可能让你在一个类中包含入口函数,WinMain()是由系统调用,跟咱们的平时程序自身调用的函数有着本质的区别。咱们能够暂时简单想象成,当CWinApp对象构造完的时候,WinMain()跟着执行。
如今你们明白了,大部分的“通用代码(咱们想封装隐藏的东西)”均可以放到CWinApp类中,那么它又是怎样运行起来的呢?为何构造了CWinApp类对象就“自动”执行那么多东西。
你们再仔细想一下,CWinApp类对象构造以后,它会“自动”执行本身的构造函数。那么咱们能够把想要“自动”执行的代码放到CWinApp类的构造函数中。
那么CWinApp类可能打算这样设计(先不计较正确与否):
C++代码
47 class CWinApp : public CWinThead{
48 public:
49 virtual BOOL InitInstance(); //解释过的程序的入点
50 CWinApp ::CWinApp(){ //构造函数
51 ////////////////////////
52 WinMain(); //这个是你们一眼看出的错误
53 Create(); //设计、建立、更新显示窗口
54 Run(); //消息循环
55 //////////////////////
56 }
57 };
写完后,你们又立刻感受到彷佛不对,WinMain()函数在这里好象真的一点用处都没有,而且能这样被调用吗(请容许我把手按在圣经上声明一下:WinMain()不是普通的函数,它要肩负着初始化应用程序,包括全局变量的初始化,是由系统而不是程序自己调用的,WinMain()返回以后,程序就结束了,进程撤消)。再看Create()函数,它能肯定设计什么样的窗口,建立什么样的窗口吗?若是能在CWinApp的构造函数里肯定的话,咱们之后设计MFC程序时窗口就一个样,这样彷佛不太合理。
回过头来,咱们可让WinMain()函数一条语句都不包含吗?不能够,咱们看一下WinMain() 函数的四个参数:
WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
其中第一个参数指向一个实例句柄,咱们在设计WNDCLASS的时候必定要指定实例句柄。咱们窗口编程,确定要设计窗口类。因此,WinMain()再简单也要这样写:
int WinMain(HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{ hInstance=hinst }
既然实例句柄要等到程序开始执行才能知道,那么咱们用于建立窗口的Create()函数也要在WinMain()内部才能执行(由于若是等到WinMain()执行完毕后,程序结束,进程撤消,固然Create()也不可能建立窗口)。
再看Run()(消息循环)函数,它能在WinMain()函数外面运行吗?众所周知,消息循环就是相同的那么几句代码,但咱们也不要企图把它放在WinMain()函数以外执行。
因此咱们的WinMain()函数能够像下面这样写:
WinMain(……)
{
……窗口类对象执行建立窗口函数……
……程序类对象执行消息循环函数……
}
对于WinMain()的问题,得总结一下,咱们封装的时候是不能够把它封装到CWinApp类里面,但因为WinMain()的不变性(或者说有规律可循),MFC彻底有能力在咱们构造CWinApp类对象的时候,帮咱们完成那几行代码。
转了一个大圈,咱们仿佛又回到了SDK编程的开始。但如今咱们如今能清楚地知道,表面上MFC与SDK编程大相径庭,但实质上MFC只是用类的形式封装了SDK函数,封装以后,咱们在WinMain()函数中只须要几行代码,就能够完成一个窗口程序。咱们也由此知道了应如何去封装应用程序类(CWinApp)和主框架窗口类(CFrameWnd)。下面把上开始设计这两个类。
3、MFC库的“重写”
为了简单起见,咱们忽略这两个类的基类和派生类的编写,可能你们会认为这是一种很不负责任的作法,但本人以为这既可减轻负担,又免了你们在各种之间穿来穿去,更好理解一些(咱们在关键的地方做注明)。还有,我把所有代码写在同一个文件中,让你们看起来不用那么吃力,但这是最不提倡的写代码方法,你们不要学哦!
C++代码
58 #include <windows.h>
59 HINSTANCE hInstance;
60
61 class CFrameWnd
62 {
63 HWND hwnd;
64 public:
65 CFrameWnd(); //也能够在这里调用Create()
66 virtual ~CFrameWnd();
67 int Create(); //类就留意这一个函数就好了!
68 BOOL ShowWnd();
69 };
70 class CWinApp1
71 {
72 public:
73 CFrameWnd* m_pMainWnd;//在真正的MFC里面
74 //它是CWnd指针,但这里因为不写CWnd类
75 //只要把它写成CFrameWnd指针
76 CWinApp1* m_pCurrentWinApp;//指向应用程序对象自己
77 CWinApp1();
78 virtual ~CWinApp1();
79 virtual BOOL InitInstance();//MFC本来是必须重载的函数,最重要的函数!!!!
80 virtual BOOL Run();//消息循环
81 };
82 CFrameWnd::CFrameWnd(){}
83 CFrameWnd::~CFrameWnd(){}
84
85 int CFrameWnd::Create() //封装建立窗口代码
86 {
87 WNDCLASS wndcls;
88 wndcls.style=0;
89 wndcls.cbClsExtra=0;
90 wndcls.cbWndExtra=0;
91 wndcls.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
92 wndcls.hCursor=LoadCursor(NULL,IDC_CROSS);
93 wndcls.hIcon=LoadIcon(NULL,IDC_ARROW);
94 wndcls.hInstance=hInstance;
95 wndcls.lpfnWndProc=DefWindowProc;//默认窗口过程函数。
96 //你们能够想象成MFC通用的窗口过程。
97 wndcls.lpszClassName="窗口类名";
98 wndcls.lpszMenuName=NULL;
99 RegisterClass(&wndcls);
100
101 hwnd=CreateWindow("窗口类名","窗口实例标题名",WS_OVERLAPPEDWINDOW,0,0,600,400,NULL,NULL,hInstance,NULL);
102 return 0;
103 }
104
105 BOOL CFrameWnd::ShowWnd()//显示更新窗口
106 {
107 ShowWindow(hwnd,SW_SHOWNORMAL);
108 UpdateWindow(hwnd);
109 return 0;
110 }
111
112 /////////////
113 CWinApp1::CWinApp1()
114 {
115 m_pCurrentWinApp=this;
116 }
117 CWinApp1::~CWinApp1(){}
118 //如下为InitInstance()函数,MFC中要为CWinApp的派生类改写,
119 //这里为了方便理解,把它放在CWinApp类里面完成!
120 //你只要记住真正的MFC在派生类改写此函数就好了。
121 BOOL CWinApp1::InitInstance()
122 {
123 m_pMainWnd=new CFrameWnd;
124 m_pMainWnd->Create();
125 m_pMainWnd->ShowWnd();
126 return 0;
127 }
128
129 BOOL CWinApp1::Run()//////////////////////封装消息循环
130 {
131 MSG msg;
132 while(GetMessage(&msg,NULL,0,0))
133 {
134 TranslateMessage(&msg);
135 DispatchMessage(&msg);
136 }
137 return 0;
138 } //////////////////////////////////////////////////////封装消息循环
139
140 CWinApp1 theApp; //应用程序对象(全局)
141
142 int WINAPI WinMain( HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
143 {
144 hInstance=hinst;
145 CWinApp1* pApp=theApp.m_pCurrentWinApp;
146 //真正的MFC要写一个全局函数AfxGetApp,以获取CWinApp指针。
147 pApp->InitInstance();
148 pApp->Run();
149 return 0;
150 }
代码那么长,实际上只是写了三个函数,一是CFrameWnd类的Create(),第二个是CWinApp类的InitInstance()和Run()。在此特别要说明的是InitInstance(),真正的MFC中,那是咱们跟据本身构造窗口的须要,本身改写这个函数。
你们能够看到,封装了上面两个类之后,在入口函数WinMain中就写几行代码,就能够产生一个窗口程序。在MFC中,由于WinMain函数就是固定的那么几行代码,因此MFC绝对能够帮咱们自动完成(MFC的特长就是帮咱们完成有规律的代码),也所以咱们建立MFC应用程序的时候,看不到WinMain函数。
MFC六大核心机制之二:运行时类型识别(RTTI)
typeid运算子
运行时类型识别(RTTI)便是程序执行过程当中知道某个对象属于某个类,咱们平时用C++编程接触的RTTI通常是编译器的RTTI,便是在新版本的VC++编译器里面选用“使能RTTI”,而后载入typeinfo.h文件,就能够使用一个叫typeid()的运算子,它的地位与在C++编程中的sizeof()运算子相似的地方(包含一个头文件,而后就有一个熟悉好用的函数)。typdid()关键的地方是能够接受两个类型的参数:一个是类名称,一个是对象指针。因此咱们判别一个对象是否属于某个类就能够象下面那样:
C++代码
1 if (typeid (ClassName)== typeid(*ObjectName)){
2 ((ClassName*)ObjectName)->Fun();
3 }
像上面所说的那样,一个typeid()运算子就能够轻松地识别一个对象是否属于某一个类,但MFC并非用typeid()的运算子来进行动态类型识别,而是用一大堆使人费解的宏。不少学员在这里很疑惑,好象MFC在大部分地方都是故做神秘。使们你们编程时很迷惘,只知道在这里加入一组宏,又在那儿加入一个映射,而不知道咱们为何要加入这些东东。
其实,早期的MFC并无typeid()运算子,因此只能沿用一个老办法。咱们甚至能够想象一下,若是MFC早期就有template(模板)的概念,可能更容易解决RTTI问题。
因此,咱们要回到“古老”的年代,想象一下,要完成RTTI要作些什么事情。就好像咱们在一个新型(新型到咱们还不认识)电器公司里面,咱们要识别哪一个是电饭锅,哪一个是电磁炉等等,咱们要查看登记的各电器一系列的信息,咱们才能够比较、鉴别,那个东西是什么!
CRuntimeClass链表的设计
要登记一系列的消息并非一件简单的事情,你们可能首先想到用数组登记对象。但若是用数组,咱们要定义多大的数组才好呢,大了浪费空间,小了更加不行。因此咱们要用另外一种数据结构——链表。由于链表理论上可大可小,能够无限扩展。
链表是一种经常使用的数据结构,简单地说,它是在一个对象里面保存了指向下一个同类型对象的指针。咱们大致能够这样设计咱们的类:
C++代码
4 struct CRuntimeClass
5 {
6 ……类的名称等一切信息……
7 CRuntimeClass * m_pNextClass;//指向链表中下一CRuntimeClass对象的指针
8 };
链表还应该有一个表头和一个表尾,这样咱们在查链表中各对象元素的信息的时候才知道从哪里查起,到哪儿结束。咱们还要注明自己是由哪能个类派生。因此咱们的链表类要这样设计:
C++代码
9 struct CRuntimeClass
10 {
11 ……类的名称等一切信息……
12 CRuntimeClass * m_pBaseClass;//指向所属的基类。
13 CRuntimeClass * m_pNextClass;//定义表尾的时候只要定义此指针为空就能够 了。
14 static CRuntimeClass* pFirstClass;//这里表头指针属于静态变量,由于咱们知道static变量在内存中只初始化一次,就能够为各对象所用!保证了各对象只有一个表头。
15 };
有了CRuntimeClass结构后,咱们就能够定义链表了:
C++代码
16 static CRuntimeClass classCObject={NULL,NULL}; //这里定义了一个CRuntimeClass对象,由于classCObject无基类,因此m_pBaseClass为NULL。由于目前只有一个元素(即目前没有下一元素),因此m_pNextClass为NULL(表尾)。
至于pFirstClass(表头),你们可能有点想不通,它到什么地方去了。由于咱们这里并不想把classCObject做为链表表头,咱们还要在前面插入不少的CRuntimeClass对象,而且由于pFirstClass为static指针,便是说它不是属于某个对象,因此咱们在用它以前要先初始化:CRuntimeClass* CRuntimeClass::pFirstClass=NULL;。
如今咱们能够在前面插入一个CRuntimeClass对象,插入以前我得重要申明一下:若是单纯为了运行时类型识别,咱们未必用到m_pNextClass指针(更可能是在运行时建立时用),咱们关心的是类自己和它的基类。这样,查找一个对象是否属于一个类时,主要关心的是类自己及它的基类。
C++代码
17 CRuntimeClass classCCmdTarget={ &classCObject, NULL};
18 CRuntimeClass classCWnd={ &classCCmdTarget ,NULL };
19 CRuntimeClass classCView={ &classCWnd , NULL };
好了,上面只是仅仅为一个指针m_pBaseClass赋值(MFC中真正CRuntimeClass有多个成员变量和方法),就链接成了链表。假设咱们如今已所有构造完成本身须要的CRuntimeClass对象,那么,这时候应该定义表头。即要用pFirstClass指针指向咱们最后构造的CRuntimeClass对象——classCView。
C++代码
20 CRuntimeClass::pFirstClass=&classCView;
如今链表有了,表头表尾都完善了,问题又出现了,咱们应该怎样访问每个CRuntimeClass对象?要判断一个对象属于某类,咱们要从表头开始,一直向表尾查找到表尾,而后才能比较得出结果吗。确定不是这样!
类中构造CRuntimeClass对象
你们能够这样想一下,咱们构造这个链表的目的,就是构造完以后,可以按主观地拿一个CRuntimeClass对象和链表中的元素做比较,看看其中一个对象是否属于你指定的类。这样,咱们须要有一个函数,一个能返回自身类型名的函数GetRuntimeClass()。
上面简单地说了一下链表的过程,但单纯有这个链表是没有任何意义。回到MFC中来,咱们要实现的是在每一个须要有RTTI能力的类中构造一个CRuntimeClass对象,比较一个类是否属于某个CRuntimeClass对象的时候,实际上只是比较CRuntimeClass对象。
如何在各个类之中插入CRuntimeClass对象,而且指定CRuntimeClass对象的内容及CRuntimeClass对象的连接,这里起码有十行的代码才能完成。在每一个须要有RTTI能力的类设计中都要重复那十多行代码是一件乏味的事情,也容易出错,因此MFC用了两个宏代替这些工做,即DECLARE_DYNAMIC(类名)和IMPLEMENT_DYNAMIC(类名,基类名)。从这两个宏咱们能够看出在MFC名类中的CRuntimeClass对象构造链接只有类名及基类名的不一样!
到此,可能会有朋友问:为何要用两个宏,用一个宏不能够代换CRuntimeClass对象构造链接吗?我的认为确定能够,由于宏只是文字代换的游戏而已。但咱们在编程之中,头文件与源文件是分开的,咱们要在头文件头声明变量及方法,在源文件里实具体实现。便是说咱们要在头文件中声明:
C++代码
21 public:
22 static CRuntimeClass classXXX //XXX为类名
23 virtual CRuntime* GetRuntimeClass() const;
而后在源文件里实现:
C++代码
24 CRuntimeClass* XXX::classXXX={……};
25 CRuntime* GetRuntimeClass() const;
26 { return &XXX:: classXXX;}//这里不能直接返回&classXXX,由于static变量是类拥有而不是对象拥有。
咱们一眼能够看出MFC中的DECLARE_DYNAMIC(类名)宏应该这样定义:
C++代码
27 #define DECLARE_DYNAMIC(class_name) public: static CRuntimeClass class##class_name; virtual CRuntimeClass* GetRuntimeClass() const;
其中##为链接符,可让咱们传入的类名前面加上class,不然跟原类同名,你们会知道产生什么后果。
有了上面的DECLARE_DYNAMIC(类名)宏以后,咱们在头文件里写上一句
DECLARE_DYNAMIC(XXX)
宏展开后就有了咱们想要的:
public:
static CRuntimeClass classXXX //XXX为类名
virtual CRuntime* GetRuntimeClass() const;
对于IMPLEMENT_DYNAMIC(类名,基类名),看来也不值得在这里代换文字了,你们知道它是知道回事,宏展开后为咱们作了什么,再深究真是一点意义都没有!
有了此链表以后,就像有了一张存放各种型的网,咱们能够垂手可得地RTTI。
IsKindOf函数
CObject有一个函数BOOL IsKindOf(const CRuntimeClass* pClass) const;,被它如下全部派生类继承。
此函数实现以下:
C++代码
28 BOOL CObject::IsKindOf(const CRuntimeClass* pClass) const
29 {
30 CRuntimeClass* pClassThis=GetRuntimeClass();//得到本身的CRuntimeClass对象指针。
31 while(pClassThis!=NULL)
32 {
33 if(pClassThis==pClass) return TRUE;
34 pClassThis=pClassThis->m_pBaseClass;//这句最关键,指向本身基类,再回头比较,一直到尽头m_pBaseClass为NULL结束。
35 }
36 return FALSE;
37 }
说到这里,运行时类型识别(RTTI)算是完成了。
MFC六大核心机制之三:动态建立
不少地方都使用了动态建立技术。动态建立就是在程序运行时建立指定类的对象。例如MFC的单文档程序中,文档模板类的对象就动态建立了框架窗口对象、文档对象和视图对象。动态建立技术对于但愿了解MFC底层运行机制的朋友来讲,很是有必要弄清楚。
不须要手动实例化对象的疑惑
MFC编程入门时,通常人都会有这样的疑惑:MFC中几个主要的类不须要咱们设计也就罢了,为何连实例化对象都不用咱们来作?咱们认为本该是:须要框架的时候,亲手写上CFrameWnd myFrame;须要视的时候,亲自打上CView myView;……。
但MFC不给咱们这个机会,导致咱们错觉窗口没有实例化就弹出来了!但大伙想了一下,可能会一拍脑门,认为简单不过:MFC自动帮咱们完成CView myView之流的代码不就好了么!其实否则,写MFC程序的时候,咱们几乎要对每一个大类进行派生改写。换句话说,MFC并不知道咱们打算怎样去改写这些类,固然也不打算所有为咱们“静态”建立这些类了。即便静态了建立这些类也没有用,由于咱们历来也不会直接利用这些类的实例干什么事情。咱们只知道,想作什么事情就往各大类里塞,无论什么变量、方法照塞,塞完以后,咱们彷佛并未实例化对象,程序就能够运行!
CRuntimeClass链表
要作到把本身的类交给MFC,MFC就用同同样的方法,把不一样的类一一准确建立,咱们要作些什么事情呢?一样地,咱们要创建链表,记录各种的关键信息,在动态建立的时候找出这些信息,就象上一节RTTI那样!咱们能够设计一个类:
C++代码
1 struct CRuntimeClass{
2 LPCSTR m_lpszClassName; //类名指针
3 CObject* (PASCAL *m_pfnCreateObject)(); //建立对象的函数的指针
4 CRuntimeClass* m_pBaseClass; //讲RTTI时介绍过
5 CRuntimeClass* m_pNextClass; //指向链表的下一个元素(许多朋友说上一节讲RTTI时并无用到这个指针,我本来觉得这样更好理解一些,由于没有这个指针,这个链表是没法连起来,而m_pBaseClass仅仅是向基类走,在MFC的树型层次结构中m_pBaseClass是不能遍历的)
6 CObject* CreateObject(); //建立对象
7 static CRuntimeClass* PASCAL Load(); //遍历整个类型链表,返回符合动态建立的对象。
8 static CRuntimeClass* pFirstClass; //类型链表的头指针
9 };
一会儿往结构里面塞了那么多的东西,你们能够以为有点头晕。至于CObject* (PASCAL *m_pfnCreateObject)();,这定义函数指针的方法,你们可能有点陌生。函数指针在C++书籍里通常被定为选学章节,但MFC仍是常常用到此类的函数,好比咱们所熟悉的回调函数。简单地说m_pfnCreateObject便是保存了一个函数的地址,它将会建立一个对象。便是说,之后,m_pfnCreateObject指向不一样的函数,咱们就会建立不一样类型的对象。
有函数指针,咱们要实现一个与原定义参数及返回值都相同一个函数,在MFC中定义为:
static CObject* PASCAL CreateObject(){return new XXX};//XXX为类名。类名不一样,咱们就建立不一样的对象。
由此,咱们能够以下构造CRuntimeClass到链表(伪代码):
CRuntimeClass classXXX={
类名,
……,
XXX::CreateObject(), //m_pfnCreateObject指向的函数
RUNTIME_CLASS(基类名), // RUNTIME_CLASS宏能够返回CRuntimeClass对象指针。
NULL //m_pNextClass暂时为空,最后会咱们再设法让它指向旧链表表头。
};
这样,咱们用函数指针m_pfnCreateObject(指向CreateObject函数),就随时可new新对象了。而且你们留意到,咱们在设计CRuntimeClass类对时候,只有类名(和基类名)的不一样(咱们用XXX代替的地方),其它的地方同样,这正是咱们想要的,由于咱们动态建立也象RTTI那样用到两个宏,只要传入类名和基类做宏参数,就能够知足条件。
便是说,咱们类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏和在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏来为咱们加入链表,至于这两个宏怎么为咱们创建一个链表,咱们本身能够玩玩文字代换的游戏,在此不一一累赘。但要说明的一点就是:动态建立宏xxx_DYNCREATE包含了RTTI宏,便是说, xxx_DYNCREATE是xxx_DYNAMIC的“加强版”。
到此,咱们有必要了解一下上节课没有明讲的m_pNextClass指针。由于MFC层次结构是树状的,并非直线的。若是咱们只有一个m_pBaseClass指针,它只会沿着基类上去,会漏掉其它分支。在动态建立时,必须要检查整个链表,看有多少个要动态建立的对象,便是说要从表头(pFirstClass)开始一直遍历到表尾(m_pNextClass=NULL),不能漏掉一个CRuntimeClass对象。
因此每当有一个新的链表元素要加入链表的时候,咱们要作的就是使新的链表元素成为表头,而且m_pNextClass指向原来链表的表头,即像下面那样(固然,这些不须要咱们操心,是RTTI宏帮助咱们完成的):
C++代码
10 pNewClass->m_pNextClass=CRuntimeClass::pFirstClass;//新元素的m_pNextClass指针指向想加入的链表的表头。
11 CRuntimeClass::pFirstClass=pNewClass;//链表的头指针指向刚插入的新元素。
好了,有了上面的链表,咱们就能够分析动态建立了。
动态建立的步骤
有了一个包含类名,函数指针,动态建立函数的链表,咱们就能够知道应该按什么步骤去动态建立了:
一、得到一要动态建立的类的类名(假设为A)。
二、将A跟链表里面每一个元素的m_lpszClassName指向的类名做比较。
三、若找到跟A相同的类名就返回A所属的CRuntimeClass元素的指针。
四、判断m_pfnCreateObject是否有指向建立函数,有则建立对象,并返回该对象。
代码演示以下(如下两个函数都是CRuntimeClass类函数):
C++代码
12 ///////////////如下为根据类名从表头向表尾查找所属的CRuntimeClass对象////////////
13
14 CRuntimeClass* PASCAL CRuntimeClass::Load()
15 {
16 char szClassXXX[64];
17 CRuntimeClass* pClass;
18 cin>>szClassXXX; //假定这是咱们但愿动态建立的类名
19 for(pClass=pFirstClass;pClass!=NULL;pClass=pClass->m_pNextClass)
20 {
21 if(strcmp(szClassXXX,pClass->m_lpszClassName)==0)
22 return pClass;
23 }
24 return NULL;
25 }
26
27 ///////////根据CRuntimeClass建立对象///////////
28 CObject* CRuntimeClass::CreateObject()
29 {
30 if(m_pfnCreateObject==NULL) return NULL;
31 CObject *pObject;
32 pObject=(* m_pfnCreateObject)(); //函数指针调用
33 return pObject;
34 }
有了上面两个函数,咱们在程序执行的时候调用,就能够动态建立对象了。
简单实现动态建立
咱们还能够更简单地实现动态建立,你们注意到,就是在咱们的程序类里面有一个RUNTIME_CLASS(class_name)宏,这个宏在MFC里定义为:
RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))
做用就是获得类的RunTime信息,即返回class_name所属CRuntimeClass的对象。在咱们的应用程序类(CMyWinApp)的InitInstance()函数下面的CSingleDocTemplate函数中,有:
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CMyView)
构造文档模板的时候就用这个宏获得文档、框架和视的RunTime信息。有了RunTime信息,咱们只要一条语句就能够动态建立了,如:
classMyView->CreateObject(); //对象直接调用用CRuntimeClass自己的CreateObject()
总结
最后再总结和明确下动态建立的具体步骤:
一、定义一个不带参数的构造函数(默认构造函数);由于咱们是用CreateObject()动态建立,它只有一条语句就是return new XXX,不带任何参数。因此咱们要有一个无参构造函数。
二、类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;和在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;这个宏完成构造CRuntimeClass对象,并加入到链表中。
三、使用时先经过宏RUNTIME_CLASS获得类的RunTime信息,而后使用CRuntimeClass的成员函数CreateObject建立一个该类的实例。
四、CObject* pObject = pRuntimeClass->CreateObject();//完成动态建立。
MFC六大核心机制之四:永久保存(串行化)
永久保存(串行化)是MFC的重要内容,能够用一句简明直白的话来形容其重要性:弄懂它之后,你就愈来愈像个程序员了!
若是咱们的程序不须要永久保存,那几乎能够确定是一个小玩儿。那怕咱们的记事本、画图等小程序,也须要保存才有真正的意义。
对于MFC的不少地方我不甚满意,总以为它喜欢拿一组低能而神秘的宏来故弄玄虚,但对于它的连续存储(serialize)机制,倒是我十分钟爱的地方。在此,可以让你们感觉到面向对象的幸福。
MFC的连续存储(serialize)机制俗称串行化。“在你的程序中尽管有着各类各样的数据,serialize机制会象流水同样按顺序存储到单一的文件中,而又能按顺序地取出,变成各类不一样的对象数据。”不知我在说上面这一句话的时候,你们有什么反应,可能不少朋友直觉是一件很简单的事情,只是说了一个“爽”字就没有下文了。
串行化原理的讨论
要实现象流水同样存储实际上是一个很大的难题。试想,在咱们的程序里有各式各样的对象数据。如画图程序中,里面设计了点类,矩形类,圆形类等等,它们的绘图方式及对数据的处理各不相同,用它们实现了成百上千的对象以后,如何存储起来?不想由可,一想头都大了:咱们要在程序中设计函数store(),在咱们单击“文件/保存”时能把各对象往里存储。那么这个store()函数要神通广大,它能清楚地知道咱们设计的是什么样的类,产生什么样的对象。你们可能并不以为这是一件很困难的事情,程序有能力知道咱们的类的样子,对象也不过是一块初始化了存储区域罢了。就把一大堆对象“转换”成磁盘文件就好了。
即便上面的存储能成立,但当咱们单击“文件/打开”时,程序固然不能预测用户想打开哪一个文件,而且当打开文件的时候,要根据你那一大堆垃圾数据new出数百个对象,还原为你原来存储时的样子,你又该怎么作呢?
试想,要是咱们有一个能容纳各类不一样对象的容器,这样,用户用咱们的应用程序打开一个磁盘文件时,就能够把文件的内容读进咱们程序的容器中。把磁盘文件读进内存,而后识别它“是什么对象”是一件很难的事情。首先,保存过程不像电影的胶片,把景物直接映射进去,而后,看一下胶片就知道那是什么内容。可能有朋友说它象录像磁带,拿着录像带咱们看不出里面变化的磁场信号,但通过录像机就能把它还原出来。
其实不是这样的,好比保存一个矩形,程序并非把矩形自己按点阵存储到磁盘中,由于咱们绘制矩形的整个过程只不过是调用一个GDI函数罢了。它保存只是坐标值、线宽和某些标记等。程序面对“00 FF”这样的东西,固然不知道它是一个圆或是一个字符!
拿刚才录像带的例子,咱们之因此能最后放映出来,前提咱们知道这对象是“录像带”,即肯定了它是什么类对象。若是咱们事先只知道它“里面保存有东西,但不知道它是什么类型的东西”,这就致使咱们没法把它读出来。拿录像带到录音机去放,对录音机来讲,那彻底是垃圾数据。便是说,要了解永久保存,要对动态建立有深入的认识。
如今你们能够知道困难的根源了吧。咱们在写程序的时候,会不断创造新的类,构造新的对象。这些对象,固然是旧的类对象(如MyDocument)从未见过的。那么,咱们如何才能使文档对象能够保存本身新对象呢,又能动态建立本身新的类对象呢?
许多朋友在这个时候想起了CObject这个类,也想到了虚函数的概念。因而觉得本身“大体了解”串行化的概念。他们设想:“咱们设计的MyClass(咱们想用于串行化的对象)所有从CObject类派生,CObject类对象固然是MyDocument能认识的。”这样就实现了一个目的:原本MyDocument不能识别咱们建立的MyClass对象,但它能识别CObject类对象。因为MyClass从CObject类派生,构造的新类对象“是一个CObject”,因此MyDocument能把咱们的新对象看成CObiect对象读出。或者根据书本上所说的:打开或保存文件的时候,MyDocument会调用Serialize(),MyDocument的Serialize()函会呼叫咱们建立类的Serialize函数[便是在MyDocument Serialize()中调用:m_pObject->Serialize(),注意:在此m_pObject是CObject类指针,它能够指向咱们设计的类对象]。最终结果是MyDocument的读出和保存变成了咱们建立的类对象的读出和保存,这种认识是不明朗的。
有意思还有,在网上我遇到几位自觉得懂了Serialize的朋友,竟然不约而同的犯了一个很低级得让人难以想象的错误。他们说:Serialize太简单了!Serialize()是一个虚函数,虚函数的做用就是“优先派生类的操做”。因此MyDocument不实现Serialize()函数,留给咱们本身的MyClass对象去调用Serialize()……真是啼笑皆非,咱们建立的类MyClass并非由MyDocument类派生,Serialize()函数为虚在MyDocument和MyClass之间没有任何意义。MyClass产生的MyObject对象仅仅是MyDocument的一个成员变量罢了。
话说回来,因为MyClass从CObject派生,因此CObject类型指针能指向MyClass对象,而且可以让MyClass对象执行某些函数(特指重载的CObject虚函数),但前提必须在MyClass对象实例化了,即在内存中占领了一块存储区域以后。不过,咱们的问题偏偏就是在应用程序随便打开一个文件,面对的是它不认识的MyClass类,固然实例化不了对象。
幸亏咱们在上一节课中懂得了动态建立。即想要从CObject派生的MyClass成为能够动态建立的对象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏就能够了(注意:最终能够Serialize的对象仅仅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL宏,这是由于DECLARE_SERIAL/IMPLEMENT_SERIAL包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏)。
整理思路,深刻理解串行化
从解决上面的问题中,咱们能够分步理解了:
一、Serialize的目的:让MyDocument对象在执行打开/保存操做时,能读出(构造)和保存它不认的MyClass类对象。
二、MyDocument对象在执行打开/保存操做时会调用它自己的Serialize()函数。但不要期望它会自动保存和读出咱们的MyClass类对象。这个问题很容易解决,以下便可:
C++代码
1 MyDocument:: Serialize(){
2 // 在此函数调用MyClass类的Serialize()就好了!即
3 MyObject. Serialize();
4 }
三、咱们但愿MyClass对象为能够动态建立的对象,因此要求在MyClass类中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。
但目前的Serialize机制还很抽象。咱们仅仅知道了表面上的东西,实际又是如何的呢?下面做一个简单深入的详解。
先看一下咱们文档类的Serialize():
C++代码
5 void CMyDoc::Serialize(CArchive& ar)
6 {
7 if (ar.IsStoring())
8 {
9 // TODO: add storing code here
10 }
11 else
12 {
13 // TODO: add loading code here
14 }
15 }
目前这个子数什么也没作(没有数据的读出和写入),CMyDoc类正等待着咱们去改写这个函数。如今假设CMyDoc有一个MFC可识别的成员变量m_MyVar,那么函数就可改写成以下形式:
C++代码
16 void CMyDoc::Serialize(CArchive& ar)
17 {
18 if (ar.IsStoring()) //读写判断
19 {
20 ar<<m_MyVar; //写
21 }
22 else
23 {
24 ar>>m_MyVar; //读
25 }
26 }
许多网友问:本身写的类(即MFC未包含的类)为何不行?咱们在CMyDoc里包含自写类的头文件MyClass.h,这样CMyDoc就认识MyDoc类对象了。这是通常常识性的错误,MyDoc类认识MyClass类对象与否并无用,关键是CArchive类,即对象ar不认识MyClass(固然你梦想重写CArchive类当别论)。“>>”、“<<”都是CArchive重载的操做符。上面ar>>m_MyVar说白便是在执行一个以ar和m_MyVar 为参数的函数,相似于function(ar,m_MyVar)罢了。咱们固然不能传递一个它不认识的参数类型,也所以不会执行function(ar,m_MyObject)了。
【注:这里咱们能够用指针。让MyClass从Cobject派生,一切又起了质的变化,假设咱们定义了:MyClass *pMyClass = new MyClass;由于MyClass从CObject派生,根据虚函数原理,pMyClass也是一个CObject*,即pMyClass指针是CArchive类可认识的。因此执行上述function(ar, pMyClass),即ar << pMyClass是没有太多的问题(在保证了MyClass对象能够动态建立的前提下)。】
回过头来,若是想让MyClass类对象能Serialize,就得让MyClass从CObject派生,Serialize()函数在CObject里为虚,MyClass从CObject派生以后就能够根据本身的要求去改写它,像上面改写CMyDoc::Serialize()方法同样。这样MyClass就获得了属于MyClass本身特有的Serialize()函数。
如今,程序就能够这样写:
C++代码
27 ……
28
29 #include “MyClass.h”
30
31 ……
32
33 void CMyDoc::Serialize(CArchive& ar)
34 {
35 //在此调用MyClass重写过的Serialize()
36 m_MyObject.Serialize(ar); // m_MyObject为MyClass实例
37 }
至此,串行化工做就算完成了,简单直观的讲:从CObject派生本身的类,重写Serialize()。在此过程当中,我刻意安排:在没有用到DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也没有用到CArray等模板类的前提下就完成了串行化的工做。我看过某些书,老是一开始就讲DECLARE_SERIAL/IMPLEMENT_SERIAL宏或立刻用CArray模板,让读者以为串行化就是这两个东西,致使许多朋友所以找不着北。
你们看到了,没有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等数据结构模板也依然能够完成串行化工做。
CArchive
最后再补充讲解一下有些抽象的CArchive。咱们先看如下程序(注:如下程序包含动态建立等,请包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)
C++代码
38 void MyClass::Serialize(CArchive& ar)
39 {
40 if (ar.IsStoring()) //读写判断
41 {
42 ar<< m_pMyVar; //问题:ar 如何把m_pMyVar所指的对象变量保存到磁盘?
43 }
44 else
45 {
46 pMyClass = new MyClass; //准备存储空间
47 ar>> m_pMyVar;
48 }
49 }
为回答上面的问题,即“ar<<XXX”的问题,这里给出一段模拟CArchive的代码。
“ar<<XXX”是执行CArchive对运算符“<<”的重载动做。ar和XXX都是该重载函数中的一参数而已。函数大体以下:
C++代码
50 CArchive& operator<<( CArchive& ar, const CObject* pOb)
51 {
52 …………
53 //如下为CRuntimeClass链表中找到、识别pOb资料。
54 CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
55 //保存pClassRef即类信息(略)
56
57 ((CObject*)pOb)->Serialize();//保存MyClass数据
58 …………
59 }
从上面能够看出,由于Serialize()为虚函数,即“ar<<XXX”的结果是执行了XXX所指向对象自己的Serialize()。对于“ar>>XXX”,虽然不是“ar<<XXX”逆过程,你们可能根据动态建立和虚函数的原理料想到它。
至此,永久保存算是写完了。在此过程当中,我一直努力用最少的代码,详尽的解释来讲明问题。之前我为本课题写过一个版本,并在几个论坛上发表过,但不知怎么在网上遗失(可能被删除)。因此这篇文章是我重写的版本。记得第一个版本中,我是对DECLARE_SERIAL/IMPLEMENT_SERIAL和可串行化的数组及链表对象说了许多。这个版本中我对DECLARE_SERIAL/IMPLEMENT_SERIAL其中奥秘几乎一句不提,目的是让你们能找到中心,有更简洁的永久保存的概念,我以为这种感受很好!
MFC六大核心机制之5、六:消息映射和命令传递
做为C++程序员,咱们老是但愿本身程序的全部代码都是本身写出来的,若是使用了其余的一些库,也老是想方设法想弄清楚其中的类和函数的原理,不然就会感受不踏实。因此,咱们对于在进行MFC视窗程序设计时常常要用到的消息机制也不知足于会使用,而是但愿能理解个中道理。本文就为你们剖析MFC消息映射和命令传递的原理。
理解MFC消息机制的必要性
说到消息,在MFC中,“最熟悉的神秘”能够说是消息映射了,那是咱们刚开始接触MFC时就要面对的东西。有过SDK编程经验的朋友转到MFC编程的时候,一会儿以为什么都变了样。特别是窗口消息及对消息的处理跟之前相比,更是风马牛不相及的。如文档不是窗口,是怎样响应命令消息的呢?
初次用MFC编程,咱们只会用MFC ClassWizard为咱们作大量的东西,最主要的是添加消息响应。记忆中,若是是自已添加消息响应,咱们应何等的当心翼翼,对BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神灵。它就是一个魔盒子,把咱们的咒语放入恰当的地方,就会发生神奇的力量,放错了,本身的程序就连“命”都没有。
听说,知道得太多未必是好事。我也曾经打算不去理解这神秘的区域,以为编程的时候知道本身想作什么就好了。MFC外表上给咱们提供了东西,直观地说,不但给了我个一个程序的外壳,更给咱们许多方便。微软的出发点多是但愿达到“傻瓜编程”的结果,试想,谁不会用ClassWizard?你们知道,Windows是基于消息的,有了ClassWizard,你又会添加类,又会添加消息,那么你所学的东西彷佛学到头了。因而许多程序员认为“咱们没有必要走SDK的老路,直接用MFC编程,新的东西一般是简单、直观、易学……”。
到你真正想用MFC编程的时候,你会发觉光会ClassWizard的你是多么的愚蠢。MFC不是一个普通的类库,普通的类库咱们彻底能够不理解里面的细节,只要知道这些类库能干什么,接口参数如何就万事大吉。如string类,操做顺序是定义一个string对象,而后修改属性,调用方法。但对于MFC,并非在你的程序中写上一句“#include MFC.h”,而后就使用MFC类库的。
MFC是一块包着糖衣的牛骨头。你很轻松地写出一个单文档窗口,在窗口中间打印一句“I love MFC!”,而后,恶梦开始了……想逃避,打算永远不去理解MFC内幕?门都没有!在MFC这个黑暗神秘的洞中,即便你打算摸着石头前行,也注定找不到出口。对着MFC这块牛骨头,微软温和、民主地告诉你“你固然能够选择不啃掉它,咳咳……但你必然会所以而饿死!”
MFC消息机制与SDK的不一样
消息映射与命令传递体现了MFC与SDK的不一样。在SDK编程中,没有消息映射的概念,它有明确的回调函数,经过一个switch语句去判断收到了何种消息,而后对这个消息进行处理。因此,在SDK编程中,会发送消息和在回调函数中处理消息就差很少能够写SDK程序了。
在MFC中,看上去发送消息和处理消息比SDK更简单、直接,但惋惜不直观。举个简单的例子,若是咱们想自定义一个消息,SDK是很是简单直观的,用一条语句:SendMessage(hwnd,message/*一个大于或等于WM_USER的数字*/,wparam,lparam),以后就能够在回调函数中处理了。但MFC就不一样了,由于你一般不直接去改写窗口的回调函数,因此只能亦步亦趋对照原来的MFC代码,把消息放到恰当的地方。这确实是同样很痛苦的劳动。
要了解MFC消息映射原理并非一件轻松的事情。咱们能够逆向思惟,想象一下消息映射为咱们作了什么工做。MFC在自动化给咱们提供了很大的方便,好比,全部的MFC窗口都使用同一窗口过程,即全部的MFC窗口都有一个默认的窗口过程。不像在SDK编程中,要为每一个窗口类写一个窗口过程。
MFC消息映射原理
对于消息映射,最直截了当地猜测是:消息映射就是用一个数据结构把“消息”与“响应消息函数名”串联起来。这样,当窗口感知消息发生时,就对结构查找,找到相应的消息响应函数执行。其实这个想法也不能简单地实现:咱们每一个不一样的MFC窗口类,对同一种消息,有不一样的响应方式。便是说,对同一种消息,不一样的MFC窗口会有不一样的消息响应函数。
这时,你们又想了一个可行的方法。咱们设计窗口基类(CWnd)时,咱们让它对每种不一样的消息都来一个消息响应,并把这个消息响应函数定义为虚函数。这样,从CWnd派生的窗口类对全部消息都有了一个空响应,咱们要响应一个特定的消息就重载这个消息响应函数就能够了。但这样作的结果,一个几乎什么也不作的CWnd类要有几百个“多余”的函数,哪怕这些消息响应函数都为纯虚函数,每一个CWnd对象也要背负着一个巨大的虚拟表,这也是得不偿失的。
许多朋友在学习消息映射时苦无突破,其缘由是一开始就认为MFC的消息映射的目的是为了替代SDK窗口过程的编写——这原本没有理解错。但他们还有多一层的理解,认为既然是替代“旧”的东西,那么MFC消息映身应该是更高层次的抽象、更简单、更容易认识。但结果是,若是咱们不经过ClassWizard工具,手动添加消息是至关迷茫的一件事。
因此,咱们在学习MFC消息映射时,首先要弄清楚:消息映射的目的,不是为是更加快捷地向窗口过程添加代码,而是一种机制的改变。若是不想改变窗口过程函数,那么应该在哪里进行消息响应呢?许多朋友只知其一;不知其二地认为:咱们能够用HOOK技术,抢在消息队列前把消息抓取,把消息响应提到窗口过程的外面。再者,不一样的窗口,会有不一样的感兴趣的消息,因此每一个MFC窗口都应该有一个表把感兴趣的消息和相应消息响应函数连系起来。而后得出——消息映射机制执行步骤是:当消息发生,咱们用HOOK技术把原本要发送到窗口过程的消息抓获,而后对照一下MFC窗口的消息映射表,若是是表里面有的消息,就执行其对应的函数。
固然,用HOOK技术,咱们理论上能够在不改变窗口过程函数的状况下,能够完成消息响应。MFC确实是这样作的,但实际操做起来可能跟你的想象差异很大。
如今咱们来编写消息映射表,咱们先定义一个结构,这个结构至少有两个项:一是消息ID,二是响应该消息的函数。以下:
C++代码
1 struct AFX_MSGMAP_ENTRY
2 {
3 UINT nMessage; //感兴趣的消息
4 AFX_PMSG pfn; //响应以上消息的函数指针
5 }
固然,只有两个成员的结构链接起来的消息映射表是不成熟的。Windows消息分为标准消息、控件消息和命令消息,每类型的消息都是包含数百不一样ID、不一样意义、不一样参数的消息。咱们要准确地判别发生了何种消息,必须再增长几个成员。还有,对于AFX_PMSG pfn,实际上等于做如下声明:
void (CCmdTarget::*pfn)(); // 提示:AFX_PMSG为类型标识,具体声明是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
pfn是一个不带参数和返回值的CCmdTarget类型函数指针,只能指向CCmdTarget类中不带参数和返回值的成员函数,这样pfn更为通用,但咱们响应消息的函数许多须要传入参数的。为了解决这个矛盾,咱们还要增长一个表示参数类型的成员。固然,还有其它……
最后,MFC咱们消息映射表成员结构以下定义:
C++代码
6 struct AFX_MSGMAP_ENTRY
7 {
8 UINT nMessage; //Windows 消息ID
9 UINT nCode; // 控制消息的通知码
10 UINT nID; //命令消息ID范围的起始值
11 UINT nLastID; //命令消息ID范围的终点
12 UINT nSig; // 消息的动做标识
13 AFX_PMSG pfn;
14 };
有了以上消息映射表成员结构,咱们就能够定义一个AFX_MSGMAP_ENTRY类型的数组,用来容纳消息映射项。定义以下:
AFX_MSGMAP_ENTRY _messageEntries[];
但这样还不够,每一个AFX_MSGMAP_ENTRY数组,只能保存着当前类感兴趣的消息,而这仅仅是咱们想处理的消息中的一部分。对于一个MFC程序,通常有多个窗口类,里面都应该有一个AFX_MSGMAP_ENTRY数组。
咱们知道,MFC还有一个消息传递机制,能够把本身不处理的消息传送给别的类进行处理。为了能查找各下MFC对象的消息映射表,咱们还要增长一个结构,把全部的AFX_MSGMAP_ENTRY数组串联起来。因而,咱们定义了一个新结构体:
C++代码
15 struct AFX_MSGMAP
16 {
17 const AFX_MSGMAP* pBaseMap; //指向别的类的AFX_MSGMAP对象
18 const AFX_MSGMAP_ENTRY* lpEntries; //指向自身的消息表
19 };
以后,在每一个打算响应消息的类中声明这样一个变量:AFX_MSGMAP messageMap,让其中的pBaseMap指向基类或另外一个类的messageMap,那么将获得一个AFX_MSGMAP元素的单向链表。这样,全部的消息映射信息造成了一张消息网。
固然,仅有消息映射表还不够,它只能把各个MFC对象的消息、参数与相应的消息响应函数连成一张网。为了方便查找,MFC在上面的类中插入了两个函数(其中theClass表明当前类):
一个是_GetBaseMessageMap(),用来获得基类消息映射的函数。函数原型以下:
C++代码
20 const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() /
21 { return &baseClass::messageMap; } /
另外一个是GetMessageMap() ,用来获得自身消息映射的函数。函数原型以下:
C++代码
22 const AFX_MSGMAP* theClass::GetMessageMap() const /
23 { return &theClass::messageMap; } /
有了消息映射表以后,咱们得讨论到问题的关键,那就是消息发生之后,其对应的响应函数如何被调用。你们知道,全部的MFC窗口,都有一个一样的窗口过程——AfxWndProc(…)。在这里顺便要提一下的是,看过MFC源代码的朋友都得,从AfxWndProc函数进去,会遇到一大堆曲折与迷团,由于对于这个庞大的消息映射机制,MFC要作的事情不少,如优化消息,加强兼容性等,这一大量的工做,有些甚至用汇编语言来完成,对此,咱们很难深究它。因此咱们要省略大量代码,理性地分析它。
对已定型的AfxWndProc来讲,对全部消息,最多只能提供一种默认的处理方式。这固然不是咱们想要的。咱们想经过AfxWndProc最终执行消息映射网中对应的函数。那么,这个执行路线是怎么样的呢?
从AfxWndProc下去,最终会调用到一个函数OnWndMsg。请看代码:
C++代码
24 LRESULT CALLBACK AfxWndProc(HWND hWnd,UINT nMsg,WPARAM wParam, LPARAM lParam)
25 {
26 ……
27 CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //把对句柄的操做转换成对CWnd对象。
28 Return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
29 }
把对句柄的操做转换成对CWnd对象是很重要的一件事,由于AfxWndProc只是一个全局函数,固然不知怎么样去处理各类windows窗口消息,因此它聪明地把处理权交给windows窗口所关联的MFC窗口对象。
如今,你们几乎能够想象获得AfxCallWndProc要作的事情,不错,它当中有一句:
pWnd->WindowProc(nMsg,wParam,lParam);
到此,MFC窗口过程函数变成了本身的一个成员函数。WindowProc是一个虚函数,咱们甚至能够经过改写这个函数去响应不一样的消息,固然,这是题外话。
WindowProc会调用到CWnd对象的另外一个成员函数OnWndMsg,下面看看大概的函数原型是怎么样的:
C++代码
30 BOOL CWnd::OnWndMsg(UINT message,WPARAM wParam,LPARAM lParam,LRESULT* pResult)
31 {
32 if(message==WM_COMMAND)
33 {
34 OnCommand(wParam,lParam);
35 ……
36 }
37 if(message==WM_NOTIFY)
38 {
39 OnCommand(wParam,lParam,&lResult);
40 ……
41 }
42 const AFX_MSGMAP* pMessageMap; pMessageMap=GetMessageMap();
43 const AFX_MSGMAP_ENTRY* lpEntry;
44 /*如下代码做用为:用AfxFindMessageEntry函数从消息入口pMessageMap处查找指定消息,若是找到,返回指定消息映射表成员的指针给lpEntry。而后执行该结构成员的pfn所指向的函数*/
45 if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
46 {
47 lpEntry->pfn();/*注意:真正MFC代码中没有用这一条语句。上面提到,不一样的消息参数表明不一样的意义和不一样的消息响应函数有不一样类型的返回值。而pfn是一个不带参数的函数指针,因此真正的MFC代码中,要根据对象lpEntry的消息的动做标识nSig给消息处理函数传递参数类型。这个过程包含很复杂的宏代换,你们在此知道:找到匹配消息,执行相应函数就行!*/
48 }
49 }
MFC命令传递
在上面的代码中,你们看到了OnWndMsg能根据传进来的消息参数,查找到匹配的消息和执行相应的消息响应。但这还不够,咱们日常响应菜单命令消息的时候,本来属于框架窗口(CFrameWnd)的WM_COMMAND消息,却能够放到视对象或文档对象中去响应。其原理以下:
咱们看上面函数OnWndMsg原型中看到如下代码:
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
即对于命令消息,其实是交给OnCommand函数处理。而OnCommand是一个虚函数,即WM_COMMAND消息发生时,最终是发生该消息所对应的MFC对象去执行OnCommand。好比点框架窗口菜单,即向CFrameWnd发送一个WM_COMMAND,将会致使CFrameWnd::OnCommand(wParam,lParam)的执行。且看该函数原型:
C++代码
50 BOOL CFrameWnd::OnCommand(WPARAM wParam,LPARAM lParam)
51 {
52 ……
53 return CWnd:: OnCommand(wParam,lParam);
54 }
能够看出,它最后把该消息交给CWnd:: OnCommand处理。再看:
C++代码
55 BOOL CWnd::OnCommand(WPARAM wParam,LPARAM lParam)
56 {
57 ……
58 return OnCmdMsg(nID,nCode,NULL,NULL);
59 }
这里包含了一个C++多态性很经典的问题。在这里,虽然是执行CWnd类的函数,但因为这个函数在CFrameWnd:: OnCmdMsg里执行,即当前指针是CFrameWnd类指针,再有OnCmdMsg是一个虚函数,因此若是CFrameWnd改写了OnCommand,程序会执行CFrameWnd::OnCmdMsg(…)。
对CFrameWnd::OnCmdMsg(…)函数的原理扼要分析以下:
C++代码
60 BOOL CFrameWnd:: OnCmdMsg(…)
61 {
62 CView pView = GetActiveView();//获得活动视指针。
63 if(pView-> OnCmdMsg(…))
64 return TRUE; //若是CView类对象或其派生类对象已经处理该消息,则返回。
65 ……//不然,同理向下执行,交给文档、框架、及应用程序执行自身的OnCmdMsg。
66 }
到此,CFrameWnd:: OnCmdMsg完成了把WM_COMMAND消息传递到视对象、文档对象及应用程序对象实现消息响应。
写了这么多,咱们已经清楚了MFC消息映射与命令传递的大体过程。
MFC消息映射宏
如今,咱们来看MFC“神秘代码”,会发觉好看多了。
先看DECLARE_MESSAGE_MAP()宏,它在MFC中定义以下:
C++代码
67 #define DECLARE_MESSAGE_MAP() /
68 private: /
69 static const AFX_MSGMAP_ENTRY _messageEntries[]; /
70 protected: /
71 static AFX_DATA const AFX_MSGMAP messageMap; /
72 virtual const AFX_MSGMAP* GetMessageMap() const; /
能够看出DECLARE_MESSAGE_MAP()定义了咱们熟悉的两个结构和一个函数,显而易见,这个宏为每一个须要实现消息映射的类提供了相关变量和函数。
如今集中精力来看一下BEGIN_MESSAGE_MAP,END_MESSAGE_MAP和ON_COMMAND三个宏,它们在MFC中定义以下(其中ON_COMMAND与另外两个宏并无定义在同一个文件中,把它放到一块儿是为了好看):
C++代码
73 #define BEGIN_MESSAGE_MAP(theClass, baseClass) /
74 const AFX_MSGMAP* theClass::GetMessageMap() const /
75 { return &theClass::messageMap; } /
76 AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = /
77 { &baseClass::messageMap, &theClass::_messageEntries[0] }; /
78 AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = /
79 { /
80
81 #define ON_COMMAND(id, memberFxn) /
82 { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&memberFxn },
83
84 #define END_MESSAGE_MAP() /
85 {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } /
86 }; /
一会儿看三个宏以为有点复杂,但这仅仅是复杂,公式性的文字代换并非很难。且看下面例子,假设咱们框架中有一菜单项为“Test”,即定义了以下宏:
C++代码
87 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
88 ON_COMMAND(ID_TEST, OnTest)
89 END_MESSAGE_MAP()
那么宏展开以后获得以下代码:
C++代码
90 const AFX_MSGMAP* CMainFrame::GetMessageMap() const
91
92 { return &CMainFrame::messageMap; }
93
94 ///如下填入消息表映射信息
95
96 const AFX_MSGMAP CMainFrame::messageMap =
97
98 { &CFrameWnd::messageMap, &CMainFrame::_messageEntries[0] };
99
100 //下面填入保存着当前类感兴趣的消息,可填入多个AFX_MSGMAP_ENTRY对象
101
102 const AFX_MSGMAP_ENTRY CMainFrame::_messageEntries[] =
103
104 {
105
106 { WM_COMMAND, CN_COMMAND, (WORD)ID_TEST, (WORD)ID_TEST, AfxSig_vv, (AFX_PMSG)&OnTest }, // 加入的ID_TEST消息参数
107
108 {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } //本类的消息映射的结束项
109
110 };
你们知道,要完成ID_TEST消息映射,还要定义和实现OnTest函数。即在头文件中写afx_msg void OnTest()并在源文件中实现它。根据以上所学的东西,咱们知道了当ID为ID_TEST的命令消息发生,最终会执行到咱们写的OnTest函数。
至此,MFC六大关键技术写完了。其中写得最难的是消息映射与命令传递,除了技术复杂以外,最难的是有许多避不开的代码。为了你们看得轻松一点,我把那繁杂的宏放在文章最后,但愿能给你阅读带来方便。