演化(evolution)永远在进行,
这个世界却不是每天都有革命(revolution)发生。
Application Framework 在软件界确实称得上具有革命精神。
项目目录:
mfc31:全局对象theApp的构造过程
mfc32:使用cWinApp通过afxgetapp()指向theApp并调用initapplication、initinstance的过程(有图有真相)
mfc33:RTTI宏的初步应用
mfc34:上述例子的扩充,iskindof函数的实现
mfc37:MessageMap的搭建
mfc38:BT MessageMap Route(Requires a strong heart!)
仿真M F C?有必要吗?意义何在?如何仿真?
我已经在序言以及导读开宗明义说过了,这本书除了教导你使用MFC,另一个重要的功能是让你认识一个application framework 的内部运作。以M F C 为教学载具,我既可以让你领略application framework的设计方式,更可以让你熟悉MFC 类别,将来运用时得心应手。呵,双效合一。
整个MFC4. 0多达189个类别,源代码达252个实作档,58个头文件,共10MB 之多。MFC4. 2又多加了29 个类别。这么庞大的对象,当然不是每一个类别每一个数据结构都是我的仿真目标。我只挑选最神秘又最重要,与应用程序主干息息相关的题目,包括:
■ M F C 程序的初始化过程
■ RTTI(Runtime Type Information)执行时期型别信息
■ Dynamic Creation动态生成
■ Persistence 永续留存
■ Message Mapping 消息映射
■ Message Rout ing 消息绕行
■ RTTI(Runtime Type Information)执行时期型别信息
■ Dynamic Creation动态生成
■ Persistence 永续留存
■ Message Mapping 消息映射
■ Message Rout ing 消息绕行
MFC 本身的设计在Application Framework之中不见得最好,敌视者甚至认为它是个Minotaur(注)!但无论如何,这是当今软件霸主微软公司的产品,从探究application framework 设计的角度来说,实为一个重要参考;而如果从选择一application framework作为软件开发工具的角度来说,单就就业市场的需求,我对MFC的推荐再加10 分!
注:Minotaur 是希腊神话中的牛头人身怪物,居住在迷宫之中。进入迷宫的人如果走不出来,就会被一口吃掉!
另一个问题是,为什么要仿真?第三篇第四篇各章节不是还要挖MFC源代码来看吗?原因是MFC太过庞大,我必须撇开枝节,把唯一重点突显出来,才容易收到教育效果。而且,仿真才能实证嘛!
如何仿真?我采用文字模式,也就是所谓的Console 程序,这样可以把程序结构的负荷降到最低。但是像消息映射和消息绕行怎么办?消息的流动是Windows程序才有的特征啊!唔,看了你就知道。
我的最高原则是:简化再简化,简化到不能再简化。
请注意,以下所有程序的类别阶层架构、类别名称、变量名称、结构名称、函数名称、函数行为,都以MFC为仿真对象,具体而微。也可以说,我从数以万行计的MFC原代码中,「偷」了一些出来,砍掉旁枝末节,只露出重点。
在文件的安排上,我把仿真MFC的类别都集中在MFC.H和MFC.CPP 中,把自己衍生的类别集中在MY.H和MY.CPP中。对于自定类别,我的命名方式是在父类别的名称前面加一个" My" ,例如衍生自CWinApp者,名为CMyWinApp,衍生自CDocument者,名为CMyDoc。
MFC 类别阶层
首先我以一个极简单的程序Frame1,把MFC数个最重要类别的阶层关系仿真出来:
这个实例仿真MFC 的类别阶层。后续数节中,我会继续在这个类别阶层上开发新的能力。在这些名为Frame?的各范例中,我以MFC源代码为蓝本,尽量仿真MFC的内部行为,并且使用完全相同的类别名称、函数名称、变量名称。这样的仿真对于我们在第三篇以及第四篇中深入探讨MFC 时将有莫大助益。相信我,这是真的。
Frame1范例程序
程序项目mfc31
Frame1 的执行结果是:
CObject Constructor
CCmdTarget Constructor
CWinThread Constructor
CWinApp Constructor
CMyWinApp Constructor
CMyWinApp Destructor
CWinApp Destructor
CWinThread Destructor
CCmdTarget Destructor
CObject Destructor
CCmdTarget Constructor
CWinThread Constructor
CWinApp Constructor
CMyWinApp Constructor
CMyWinApp Destructor
CWinApp Destructor
CWinThread Destructor
CCmdTarget Destructor
CObject Destructor
好,你看到了,Frame1 并没有new任何对象,反倒是有一个全域对象theApp存在。C++ 规定,全域对象的构造将比程序进入点(在DOS 环境为main,在Windows 环境为WinMain)更早。所以theApp的构造式将更早于main。换句话说你所看到的执行结果中的那些构造式输出动作全都是在main函数之前完成的。
main 函数调用全域函数AfxGetApp以取得theApp的对象指针。这完全是仿真MFC程序的手法。
MFC程序的初始化过程
MFC 程序也是个Windows 程序,它的内部一定也像第1章所述一样,有窗口注册动作,有窗口产生动作,有消息循环动作,也有窗口函数。此刻我并不打算做出Windows 程序,只是想交待给你一个程序流程,这个流程正是任何MFC 程序的初始化过程的简化。以下是Frame2 范例程序的类别阶层及其成员。对于那些「除了构造式与析构式之外没有其它成员」的类别,我就不在图中展开他们了:
就如我曾在第1章解释过的,InitApplication 和InitInstance 现在成了MFC 的CWinApp的两个虚拟函数。前者负责「每一个程序只做一次」的动作,后者负责「每一个执行个体都得做一次」的动作。通常,系统会(并且有能力)为你注册一些标准的窗口类别(当然也就准备好了一些标准的窗口函数),你(应用程序设计者)应该在你的CMyWinApp中改写InitInstance ,并在其中把窗口产生出来-- 这样你才有机会在标准的窗口类别中指定自己的窗口标题和菜单。下面就是我们新的main 函数:
void main()
{
CWinApp* pApp = AfxGetApp();
pApp->InitApplication();
pApp->InitInstance();
pApp->Run();
}
其中pApp指向theApp全域对象。在这里我们开始看到了虚拟函数的妙用(还不熟练者请快复习第2章):
„
好,请注意以下CMyWinApp::InitInstance 的动作,以及它所引发的行为:
你看到了,这些函数什么正经事儿也没做,光只输出一个标识符串。我主要的目的是在让你先熟悉MFC程序的执行流程。
以下就是Frame2 的执行结果:
CWinApp::InitApplication
CMyWinApp::InitInstance
CMyFrameWnd::CMyFrameWnd
CFrameWnd::Create
CWnd::CreateEx
CFrameWnd::PreCreateWindow
CWinApp::Run
CWinThread::Run
CMyWinApp::InitInstance
CMyFrameWnd::CMyFrameWnd
CFrameWnd::Create
CWnd::CreateEx
CFrameWnd::PreCreateWindow
CWinApp::Run
CWinThread::Run
RTTI(执行时期型别辨识)(通过运行时类型识别)
Runtime Type Identification
你已经在第2章看到,Visual C++ 4.0 支持RTTI,重点不外乎是:
1. 编译时需选用/GR选项(/GR 的意思是enable C++ RTTI )
2. 包含typeinfo.h
3. 使用新的typeid运算子。
3. 使用新的typeid运算子。
MFC 早在编译器支持RTTI之前,就有了这项能力。我们现在要以相同的手法,在DOS程序中仿真出来。我希望我的类别库具备IsKindOf的能力,能在执行时期侦测某个对象是否「属于某种类别」,并传回TRUE或FALSE 。以前一章的Shape为例,我希望:
CSquare* pSquare = new CSquare;
cout << pSquare->IsKindOf(CSquare); // 应该获得1 (TRUE)
cout << pSquare->IsKindOf(CRect); // 应该获得1 (TRUE)
cout << pSquare->IsKindOf(CShape); // 应该获得1 (TRUE)
cout << pSquare->IsKindOf(CCircle); // 应该获得0 (FALSE)
以MFC 的类别阶层来说,我希望:
CMyDoc* pMyDoc = new CMyDoc;
cout << pMyDoc->IsKindOf(CMyDoc); // 应该获得1 (TRUE)
cout << pMyDoc->IsKindOf(CDocument); // 应该获得1 (TRUE)
cout << pMyDoc->IsKindOf(CCmdTarget); // 应该获得1 (TRUE)
cout << pMyDoc->IsKindOf(CWnd); // 应该获得0 (FALSE)
注意:真正的IsKindOf 参数其实没能那么单纯
类别型录网与CRuntimeClass
怎么设计RTTI 呢?让我们想想,当你手上握有一种色泽,想知道它的RGB成份比,不查色表行吗?当你持有一种产品,想知道它的型号,不查型录行吗?要达到RTTI 的能力,我们(类别库的设计者)一定要在类别构造起来的时候,记录必要的信息,以建立型录。型录中的类别信息,最好以串行(linked list)方式串接起来,将来方便一一比对。
我们这份「类别型录」的串行元素将以CRuntimeClass 描述之,那是一个结构,内中至少需有类别名称、串行的Next 指针,以及串行的First指针。由于First 指针属于全域变量,一份就好,所以它应该以static修饰之。除此之外你所看到的其它CRuntimeClass成员都是为了其它目的而准备,陆陆续续我会介绍出来。
// in MFC.H
struct CRuntimeClass
{
// Attributes
LPCSTR m_lpszClassName;
int m_nObjectSize;
UINT m_wSchema; // schema number of the loaded class
CObject* (PASCAL* m_pfnCreateObject)(); // NULL => abstract class
CRuntimeClass* m_pBaseClass ;
// CRuntimeClass objects linked together in simple list
static CRuntimeClass* pFirstClass ; // start of class list
CRuntimeClass* m_pNextClass ; // linked list of registered classes
};
我希望,每一个类别都能拥有这样一个CRuntimeClass 成员变量,并且最好有一定的命名规则(例如在类别名称之前冠以" class" 作为它的名称),然后,经由某种手段将整个类别库构造好之后,「类别型录」能呈现类似这样的风貌:
DECLARE_DYNAMIC / IMPLEMENT_DYNAMIC宏
为了神不知鬼不觉把CRuntimeClass对象塞到类别之中,并声明一个可以抓到该对象地址的函数,我们定义DECLARE_DYNAMIC 宏如下:
#define DECLARE_DYNAMIC (class_name) \
public: \
static CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const;
出现在宏定义之中的##,用来告诉编译器,把两个字符串系在一起。如果你这么使用此宏:
DECLARE_DYNAMIC(CView)
编译器前置处理器为你做出的码是:
public:
static CRuntimeClass classCView;
virtual CRuntimeClass* GetRuntimeClass() const;
这下子,只要在声明类别时放入DECLARE_DYNAMIC 宏即万事OK喽。
不,还没有OK,类别型录(也就是各个CRuntimeClass对象