项目目录:
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 消息绕行
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
好,你看到了,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
RTTI(执行时期型别辨识)(通过运行时类型识别)
Runtime Type Identification
你已经在第2章看到,Visual C++ 4.0 支持RTTI,重点不外乎是:
1. 编译时需选用/GR选项(/GR 的意思是enable C++ RTTI
)
2. 包含typeinfo.h
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;
出现在宏定义之中的##,用来告诉编译器,把两个字符串系在一起。如果你这么使用此宏:
编译器前置处理器为你做出的码是:
public:
static CRuntimeClass classCView;
virtual CRuntimeClass* GetRuntimeClass() const;
这下子,只要在声明类别时放入DECLARE_DYNAMIC 宏即万事OK喽。
不,还没有OK,类别型录(也就是各个CRuntimeClass对象)的内容指定以及串接工作最好也能够神不知鬼不觉,我们于是再定义IMPLEMENT_DYNAMIC 宏:
#define IMPLEMENT_DYNAMIC(class_name, base_class_name) \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL)
其中的_IMPLEMENT _RUNTIMECLASS 又是一个宏。这样区分是为了此一宏在「动态生成」(下一节主题)时还会用到。
#define _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name,wSchema,pfnNew) \
static char _lpsz##class_name[] = #class_name; \
CRuntimeClass class_name::class##class_name = { \
_lpsz##class_name, sizeof(class_name), wSchema, pfnNew, \
RUNTIME_CLASS (base_class_name), NULL }; \
static AFX_CLASSINIT _init_##class_name(&class_name::class##class_name); \
CRuntimeClass* class_name::GetRuntimeClass() const \
{ return &class_name::class##class_name; } \
其中又有RUNTIME_CLASS宏,定义如下:
#define RUNTIME_CLASS (class_name) \
(&class_name::class##class_name)
看起来整个IMPLEMENT_DYNAMIC 内容好象只是指定初值,不然,其曼妙处在于它所使用的一个struct AFX_CLASSINIT,定义如下:
struct AFX_CLASSINIT
{ AFX_CLASSINIT(CRuntimeClass* pNewClass); };
这表示它有一个构造式(别惊讶,C++ 的struct与class都有构造式),定义如下:
AFX_CLASSINIT::AFX_CLASSINIT (CRuntimeClass* pNewClass)
{
pNewClass->m_pNextClass = CRuntimeClass::pFirstClass;
CRuntimeClass::pFirstClass = pNewClass;
}
很明显,此构造式负责linked list的串接工作。
整组宏看起来有点吓人,其实也没有什么,文字代换而已。现在看看这个实例:
// in header file
class CView : public CWnd
{
DECLARE_DYNAMIC(CView)
...
};
// in implementation file
IMPLEMENT_DYNAMIC(CView, CWnd)
上述的码展开来成为:
// in header file class CView : public CWnd { public: static CRuntimeClass classCView ; \ virtual CRuntimeClass* GetRuntimeClass() const; ... }; // in implementation file static char _lpszCView[] = "CView"; CRuntimeClass CView::classCView = { _lpszCView, sizeof(CView), 0xFFFF, NULL, &CWnd::classCWnd, NULL }; static AFX_CLASSINIT _init_CView(&CView::classCView); CRuntimeClass* CView::GetRuntimeClass() const { return &CView::classCView; }
于是乎,程序中只需要简简单单的两个宏DECLARE_DYNAMIC(Cxxx) 和IMPLEMENT_DYNAMIC(Cxxx, Cxxxbase) ,就完成了构造资料并加入串行的工作:
可是你知道,串行的头,总是需要特别费心处理,不能够套用一般的串行行为模式。我们的类别根源CObject ,不能套用现成的宏DECLARE_DYNAMIC和IMPLEMENT _DYNAMIC,必须特别设计如下:
// in header file
class CObject
{
public:
virtual CRuntimeClass* GetRuntimeClass() const;
...
public:
static CRuntimeClass classCObject ;
};
// in implementation file
static char szCObject[] = "CObject";
struct CRuntimeClass CObject::classCObject =
{ szCObject, sizeof(CObject), 0xffff, NULL, NULL };
static AFX_CLASSINIT _init_CObject (&CObject::classCObject);
CRuntimeClass* CObject::GetRuntimeClass() const
{
return &CObject::classCObject;
}
并且,CRuntimeClass 中的static 成员变量应该要初始化(如果你忘记了,赶快复习第2章的「静态成员(变量与函数)」一节):
// in implementation file
CRuntimeClass* CRuntimeClass::pFirstClass = NULL;
终于,整个「类别型录」串行的头部就这样形成了:
范例程序Frame3 在.h 档中有这些类别声明:
class CObject
{
...
};
class CCmdTarget : public CObject
{
DECLARE_DYNAMIC(CCmdTarget)
...
};
class CWinThread : public CCmdTarget
{
DECLARE_DYNAMIC(CWinThread)
...
};
class CWinApp : public CWinThread
{
DECLARE_DYNAMIC(CWinApp)
...
};
class CDocument : public CCmdTarget
{
DECLARE_DYNAMIC(CDocument)
...
};
class CWnd : public CCmdTarget
{
DECLARE_DYNAMIC(CWnd)
...
};
class CFrameWnd : public CWnd
{
DECLARE_DYNAMIC(CFrameWnd)
...
};
class CView : public CWnd
{
DECLARE_DYNAMIC(CView)
...
};
class CMyWinApp : public CWinApp
{
...
};
class CMyFrameWnd : public CFrameWnd
{
};
class CMyDoc : public CDocument
{
};
// 其实在MFC 中是DECLARE_DYNCREATE() ,见下节。
// 其实在MFC 中是DECLARE_DYNCREATE() ,见下节。
... // 其实在MFC 应用程序中这里也有DECLARE_DYNCREATE(),见下节。
... // 其实在MFC 应用程序中这里也有DECLARE_DYNCREATE(),见下节。
class CMyView : public CView
{... // 其实在MFC 应用程序中这里也有DECLARE_DYNCREATE(),见下节。
};
范例程序Frame3 在.cpp 档中有这些动作:
IMPLEMENT_DYNAMIC(CCmdTarget, CObject)
IMPLEMENT_DYNAMIC(CWinThread, CCmdTarget)
IMPLEMENT_DYNAMIC(CWinApp, CWinThread)
IMPLEMENT_DYNAMIC(CWnd, CCmdTarget) // 其实在MFC 中它是IMPLEMENT_DYNCREATE() ,见下节。
IMPLEMENT_DYNAMIC(CFrameWnd, CWnd) // 其实在MFC 中它是IMPLEMENT_DYNCREATE() ,见下节。
IMPLEMENT_DYNAMIC(CDocument, CCmdTarget)
IMPLEMENT_DYNAMIC(CView, CWnd)
于是组织出图3-1 这样一个大网。
为了实证整个类别型录网的存在,我在main 函数中调用PrintAllClasses,把串行中的每一个元素的类别名称、对象大小、以及schemano. 印出来:
void PrintAllClasses()
{
CRuntimeClass* pClass;
// just walk through the simple list of registered classes
for (pClass = CRuntimeClass::pFirstClass; pClass != NULL;
pClass = pClass->m_pNextClass)
{
cout << pClass->m_lpszClassName << "\n";
cout << pClass->m_nObjectSize << "\n";
cout << pClass->m_wSchema << "\n";
}
}
Frame3 的执行结果如下:
CView
4
65535
CDocument
4
65535
CFrameWnd
4
65535
CWnd
4
65535
CWinApp
12
65535
CWinThread
4
65535
CCmdTarget
4
65535
CObject
4
65535
代码见项目mfc33
IsKindOf(型别辨识)
有了图3-1这张「类别型录」网,要实现IsKindOf功能,再轻松不过了:
1. 为CObject 加上一个IsKindOf函数,于是此函数将被所有类别继承。它将把参数所指定的某个CRuntimeClass对象拿来与类别型录中的元素一一比对。比对成功(在型录中有发现),就传回TRUE,否则传回FALSE :
// in header file
class CObject
{
public:
...
BOOL IsKindOf(const CRuntimeClass* pClass) const;
};
// in implementation file
BOOL CObject::IsKindOf(const CRuntimeClass* pClass) const
{
CRuntimeClass* pClassThis = GetRuntimeClass();
while (pClassThis != NULL)
{
if (pClassThis == pClass)
return TRUE;
pClassThis = pClassThis->m_pBaseClass;
}
return FALSE; // walked to the top, no match
}
注意,while 循环中所追踪的是「同宗」路线,也就是凭借着m_pBaseClass 而非m_pNextClass。假设我们的调用是:
CView* pView = new CView;
pView->IsKindOf(RUNTIME_CLASS(CWinApp));
IsKindOf 的参数其实就是&CWinApp :: classCWinApp 。函数内利用GetRuntimeClass先取得&CView:: classCView ,然后循线而上(从图3-1 来看,所谓循线分别是指CView、CWnd、CCmdTarget、CObject),每获得一个CRuntimeClass 对象指针,就拿来和CView:: classCView 的指针比对。靠这个土方法,完成了IsKindOf 能力。
2. IsKindOf 的使用方式如下:
CMyDoc* pMyDoc = new CMyDoc;
CMyView* pMyView = new CMyView;
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CMyDoc)); // 應該獲得 TRUE
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CDocument)); // 應該獲得 TRUE
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CCmdTarget)); // 應該獲得 TRUE
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CObject)); // 應該獲得 TRUE
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CWinApp)); // 應該獲得 FALSE
cout << pMyDoc->IsKindOf(RUNTIME_CLASS(CView)); // 應該獲得 FALSE
cout << pMyView->IsKindOf(RUNTIME_CLASS(CView)); // 應該獲得 TRUE
cout << pMyView->IsKindOf(RUNTIME_CLASS(CObject)); // 應該獲得 TRUE
cout << pMyView->IsKindOf(RUNTIME_CLASS(CWnd)); // 應該獲得 TRUE
cout << pMyView->IsKindOf(RUNTIME_CLASS(CFrameWnd)); // 应该获得FALSE
IsKindOf 的完整范例放在Frame4 中。
Frame4 范例程序 mfc34
Dynamic Creation ( 动态生成)
基础有了,做什么都好。同样地,有了上述的「类别型录网」,各种应用纷至沓来。其中一个应用就是解决棘手的动态生成问题。
我已经在第二章描述过动态生成的困难点:你没有办法在程序执行期间,根据动态获得的一个类别名称(通常来自读档,但我将以屏幕输入为例),要求程序产生一个对象。上述的「类别型录网」虽然透露出解决此一问题的些微曙光,但是技术上还得加把劲儿。
如果我能够把类别的大小记录在类别型录中,把构造函数(注意,这里并非指C++ 构造式,而是指即将出现的CRuntimeClass::CreateObject也记录在类别型录中,当程序在执行时期获得一个类别名称,它就可以在「类别型录网」中找出对应的元素,然后调用其构造函数(这里并非指C++ 构造式),产生出对象。
好主意!
类别型录网的元素型式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;
CObject* CreateObject();
static CRuntimeClass* PASCAL Load();
// CRuntimeClass objects linked together in simple list
static CRuntimeClass* pFirstClass; // start of class list
CRuntimeClass* m_pNextClass; // linked list of registered classes
};
DECLARE_DYNCREATE/IMPLEMENT_DYNCREATE宏
为了因应CRuntimeClass 中新增的成员变量,我们再添两个宏,
#define DECLARE_DYNCREATE(class_name) \
DECLARE_DYNAMIC(class_name) \
static CObject* PASCAL CreateObject();
#define IMPLEMENT_DYNCREATE(class_name, base_class_name) \
CObject* PASCAL class_name::CreateObject() \
{ return new class_name; } \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, \
class_name::CreateObject)
DECLARE_DYNCREATE 和IMPLEMENT _DYNCREATE:
于是,以CFrameWnd 为例,下列程序代码:
// in header file
class CFrameWnd : public CWnd
{
DECLARE_DYNCREATE(CFrameWnd)
...
};
// in implementation file
IMPLEMENT_DYNCREATE(CFrameWnd, CWnd)
就被展开如下(注意,编译器选项/P 可得前置处理结果):
// in header file
class CFrameWnd : public CWnd
{
public:
static CRuntimeClass classCFrameWnd;
virtual CRuntimeClass* GetRuntimeClass() const;
static CObject* PASCAL CreateObject();
...
};
// in implementation file
CObject* PASCAL CFrameWnd::CreateObject()
{ return new CFrameWnd; }
static char _lpszCFrameWnd[] = "CFrameWnd";
CRuntimeClass CFrameWnd::classCFrameWnd = {
_lpszCFrameWnd, sizeof(CFrameWnd), 0xFFFF, CFrameWnd::CreateObject,
RUNTIME_CLASS(CWnd), NULL };
static AFX_CLASSINIT _init_CFrameWnd(&CFrameWnd::classCFrameWnd);
CRuntimeClass* CFrameWnd::GetRuntimeClass() const
{ return &CFrameWnd::classCFrameWnd; }
图示如下:
「对象生成器」CreateObject 函数很简单,只要说new 就好。
从宏的定义我们很清楚可以看出,拥有动态生成(Dynamic Creation)能力的类别库,必然亦拥有执行时期类型识别(RTTI)能力,因为_DYNCREATE 宏涵盖了_DYNAMIC宏。
下面代码见mfc36
然后,为了验证这样的动态生成机制的确有效(也就是对象的确被产生了),我让许多个类别的构造式都输出一段文字,而且在取得对象指针后,真的去调用该对象的一个成员函数SayHello 。我把SayHello 设计为虚拟函数,所以根据不同的对象类型,会调用到不同的SayHello 函数,出现不同的输出字符串。请注意,main 函数中的while 循环必须等到CRuntimeClass:: Load 传回NULL 才会停止,而CRuntimeClass:: Load 是在它从整个「类别型录网」中找不到它要找的那个类别名称时,才传回NULL。这些都是我为了仿真与示范,所采取的权宜设计。
enter a class name...
CObject
Error: Trying to create object which is not DECLARE_DYNCREATE
or DECLARE_SERIAL: CObject.
enter a class name...
CView
Error: Trying to create object which is not DECLARE_DYNCREATE
or DECLARE_SERIAL: CView.
enter a class name...
CMyView
CWnd Constructor
CMyView Constructor
Hello CMyView
enter a class name...
CMyFrameWnd
CWnd Constructor
CFrameWnd Constructor
CMyFrameWnd Constructor
Hello CMyFrameWnd
enter a class name...
CMyDoc
CMyDoc Constructor
Hello CMyDoc
enter a class name...
CWinApp
Error: Trying to create object which is not DECLARE_DYNCREATE
or DECLARE_SERIAL: CWinApp.
enter a class name... CJjhou (故意输入一个不在「类别型录网」中的类别名称)
Error: Class not found: CJjhou (程序结束)
Persistence(永续生存)机制
对象导向有一个术语:Persistence,意思就是把对象永久保留下来。
Power 一关,啥都没有,对象又如何能够永续存留?当然是写到文件去!
把资料写到文件,很简单。在Document/View架构中,资料都放在一份document(文件)里头,我们只要把其中的成员变量依续写进文件即可。成员变量很可能是个对象,而面对对象,我们首先应该记载其类别名称,然后才是对象中的资料。
读档就有点麻烦了。当程序从文件中读到一个类别名称,它如何实现(instantiate)一个对象?呵,这不就是动态生成的技术吗?我们在前一章已经解决掉了。
MFC 有一套Serialize机制,目的在于把档名的选择、文件的开关、缓冲区的建立、资料的读写、萃取运算子(>>)和嵌入运算子(<<)的多载(overload)、对象的动态生成
都包装起来。
上述Serialize的各部份工作,除了资料的读写和对象的动态生成,其余都是支节。动态生成的技术已经解决,让我们集中火力,分析资料的读写动作。
Serialize(资料读写)
假设我有一份文件,用以记录一张图形。图形只有三种基本元素:线条(Stroke )、圆形、矩形。我打算用以下类别,组织这份文件:
其中CObList 和CDWordArray是MFC 提供的类别,前者是一个串行,可放置任何从CObject 衍生下来的对象,后者是一个数组,每一个元素都是"double word" 。另外三个类别:CStroke和CRectangle和CCircle,是我从CObject 中衍生下来的类别。
class CMyDoc : public CDocument
{
CObList m_graphList;
CSize m_sizeDoc;
...
};
class CStroke : public CObject
{
CDWordArray m_ptArray; // series of connected points
...
};
class CRectangle : public CObject
{
CRect m_rect;
...
};
class CCircle : public CObject
{
CPoint m_center;
UINT m_radius;
...
};
假设现有一份文件,内容如图3-3,如果你是Serialize 机制的设计者,你希望怎么做呢?把图3-3 写成这样的文件内容好吗:
还算堪用。但如果考虑到屏幕卷动的问题,以及打印输出的问题,应该在最前端增加「文件大小」。另外,如果这份文件有100条线条,50个圆形,80个矩形,难不成我们要记录230 个类别名称?应该有更好的方法才是。
我们可以在每次记录对象内容的时候,先写入一个代码,表示此对象之类别是否曾在档案中记录过了。如果是新类别,乖乖地记录其类别名称;如果是旧类别,则以代码表示。这样可以节省文件大小以及程序用于解析的时间。啊,不要看到文件大小就想到硬盘很便宜,桌上的一切都将被带到网上,你得想想网络频宽这回事。
还有一个问题。文件的「版本」如何控制?旧版程序读取新版文件,新版程序读取旧版文件,都可能出状况。为了防弊,最好把版本号码记录上去。最好是每个类别有自己的版本号码。
下面是新的构想,也就是Serialization的目标:
我希望有一个专门负责Serialization 的函数,就叫作Serialize好了。假设现在我的Document类别名称为CScribDoc,我希望有这么便利的程序方法(请仔细琢磨琢磨其便利性):
void CScribDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
ar << m_sizeDoc;
else
ar >> m_sizeDoc;
m_graphList.Serialize(ar);
}
void CObList::Serialize(CArchive& ar)
{
if (ar.IsStoring()) {
ar << (WORD) m_nCount;
for (CNode* pNode = m_pNodeHead; pNode != NULL; pNode = pNode->pNext)
ar << pNode->data;
}
else {
WORD nNewCount;
ar >> nNewCount;
while (nNewCount--) {
CObject* newData;
ar >> newData;
AddTail(newData);
}
}
}
void CStroke::Serialize(CArchive& ar)
{
m_ptArray.Serialize(ar);
}
void CDWordArray::Serialize(CArchive& ar)
{
if (ar.IsStoring()) {
ar << (WORD) m_nSize;
for (int i = 0; i < m_nSize; i++)
ar << m_pData[i];
}
else {
WORD nOldSize;
ar >> nOldSize;
for (int i = 0; i < m_nSize; i++)
ar >> m_pData[i];
}
}
void CRectangle::Serialize(CArchive& ar)
{
if (ar.IsStoring())
ar << m_rect;
else
ar >> m_rect;
}
void CCircle::Serialize(CArchive& ar)
{
if (ar.IsStoring()) {
ar << (WORD)m_center.x;
ar << (WORD)m_center.y;
ar << (WORD)m_radius;
}
else {
ar >> (WORD&)m_center.x;
ar >> (WORD&)m_center.y;
ar >> (WORD&)m_radius;
}
}
每一个可写到文件或可从文件中读出的类别,都应该有它自己的Serailize 函数,负责它自己的资料读写文件动作。此类别并且应该改写<< 运算子和>> 运算子,把资料导流到archive 中。archive 是什么?是一个与文件息息相关的缓冲区,暂时你可以想象它就是文件的化身。当图3-3 的文件写入文件时,Serialize 函数的调用次序如图3-4。
DECLARE_SERIAL / IMPLEMENT_SERIAL宏
要将<< 和>> 两个运算子多载化,还要让Serialize 函数神不知鬼不觉地放入类别声明之中,最好的作法仍然是使用宏。
类别之能够进行文件读写动作,前提是拥有动态生成的能力,所以,MFC 设计了两个宏
DECLARE_SERIAL和IMPLEMENT _SERIAL:
#define DECLARE_SERIAL(class_name) \
DECLARE_DYNCREATE(class_name) \
friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) \
CObject* PASCAL class_name::CreateObject() \
{ return new class_name; } \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \
class_name::CreateObject) \
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \
{ pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \
return ar; } \
为了在每一个对象被处理(读或写)之前,能够处理琐屑的工作,诸如判断是否第一次出现、记录版本号码、记录文件名等工作,CRuntimeClass 需要两个函数Load 和Store
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;
CObject* CreateObject();
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);
// CRuntimeClass objects linked together in simple list
static CRuntimeClass* pFirstClass; // start of class list
CRuntimeClass* m_pNextClass; // linked list of registered classes
};
你已经在上一节看过Load函数,当时为了简化,我把它的参数拿掉,改为由屏幕上获得类别名称,事实上它应该是从文件中读一个类别名称。至于Store 函数,是把类别名称写入文件中:
// Runtime class serialization code
CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum)
{
WORD nLen;
char szClassName[64];
CRuntimeClass* pClass;
ar >> (WORD&)(*pwSchemaNum) >> nLen;
if (nLen >= sizeof(szClassName) || ar.Read(szClassName, nLen) != nLen)
return NULL;
szClassName[nLen] = '\0';
for (pClass = pFirstClass; pClass != NULL; pClass = pClass->m_pNextClass)
{
if (lstrcmp(szClassName, pClass->m_lpszClassName) == 0)
return pClass;
}
return NULL; // not found
}
void CRuntimeClass::Store(CArchive& ar) const
// stores a runtime class description
{
WORD nLen = (WORD)lstrlenA(m_lpszClassName);
ar << (WORD)m_wSchema << nLen;
ar.Write(m_lpszClassName, nLen*sizeof(char));
}
图3-4 的例子中,为了让整个Serialization 机制运作起来,我们必须做这样的类别声明:
class CScribDoc : public CDocument
{
DECLARE_DYNCREATE(CScribDoc)
...
};
class CStroke : public CObject
{
DECLARE_SERIAL(CStroke)
public:
void Serialize(CArchive&);
...
};
class CRectangle : public CObject
{
DECLARE_SERIAL(CRectangle)
public:
void Serialize(CArchive&);
...
};
class CCircle : public CObject
{
DECLARE_SERIAL(CCircle)
public:
void Serialize(CArchive&);
...
};
以及在.CPP 档中做这样的动作:
IMPLEMENT_DYNCREATE(CScribDoc, CDocument)
IMPLEMENT_SERIAL(CStroke, CObject, 2)
IMPLEMENT_SERIAL(CRectangle, CObject, 1)
IMPLEMENT_SERIAL(CCircle, CObject, 1)
然后呢?分头设计CStroke、CRectangle 和CCircle 的Serialize 函数吧。当然,毫不令人意外地,MFC 源代码中的CObList 和CDWordArray 有这样的内容:
// in header files
class CDWordArray : public CObject
{
DECLARE_SERIAL(CDWordArray)
public:
void Serialize(CArchive&);
...
};
class CObList : public CObject
{
DECLARE_SERIAL(CObList)
public:
void Serialize(CArchive&);
...
};
// in implementation files
IMPLEMENT_SERIAL(CObList, CObject, 0)
IMPLEMENT_SERIAL(CDWordArray, CObject, 0)
而CObject 也多了一个虚拟函数Serialize:
class CObject
{
public:
virtual void Serialize(CArchive& ar);
...
}
没有范例程序
抱歉,我没有准备DOS 版的Serialization 范例程序给你。你看到了,很多东西需要仿真:CFil e、CArchive、CObList、CDWordArray 、CRect、CPoint 、运算子多载、Serialize函数... 。我干脆在本书第8章直接为你解释MFC 的作法,更好。
Message Mapping(消息映射)
Windows程序靠消息的流动而维护生命。你已经在第一章看过了消息的一般处理方式,也就是在窗口函数中借着一个大大的switch /case 比对动作,判别消息再调用对应的处理例程。为了让大大的switch /case比对动作简化,也让程序代码更模块化一些,我在第1章提供了一个简易的消息映射表作法,把消息和其处理例程关联起来。
当我们的类别库成立,如果其中与消息有关的类别(姑且叫作「消息标的类别」好了,在MFC 之中就是CCmdTarget)都是一条鞭式地继承,我们应该为每一个「消息标的类别」准备一个消息映射表,并且将基础类别与衍生类别之消息映射表串接起来。然后,当窗口函数做消息的比对时,我们就可以想办法导引它沿着这条路走过去:
但是,MFC 之中用来处理消息的C++ 类别,并不呈单鞭发展。作为application framework的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是document/view,没有关系)。因此,消息藉以攀爬的路线应该有横流的机会:
消息如何流动,我们暂时先不管。是直线前进,或是中途换跑道,我们都暂时不管,本节先把这个攀爬路线网建立起来再说。这整个攀爬路线网就是所谓的消息映射表(Message Map);说它是一张地图,当然也没有错。将消息与表格中的元素比对,然后调用对应的处理例程,这种动作我们也称之为消息映射(Message Mapping)。
为了尽量降低对正常(一般)类别声明和定义的影响,我们希望,最好能够像RTTI 和Dynamic Creation 一样,用一两个宏就完成这巨大蜘蛛网的构造。最好能够像DECLARE_DYNAMIC 和IMPLEMENT _DYNAMIC 宏那么方便。首先定义一个数据结构:
struct AFX_MSGMAP
{
AFX_MSGMAP* pBaseMessageMap;
AFX_MSGMAP_ENTRY* lpEntries;
};
其中的AFX_MSGMAP_ENTRY 又是另一个数据结构:
struct AFX_MSGMAP_ENTRY // MFC 4.0 format
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
其中的AFX_PMSG 定义为函数指针:
typedef void (CCmdTarget::*AFX_PMSG )(void);
然后我们定义一个宏:
#define DECLARE_MESSAGE_MAP() \
static AFX_MSGMAP_ENTRY _messageEntries[]; \
static AFX_MSGMAP messageMap; \
virtual AFX_MSGMAP* GetMessageMap() const;
于是,DECLARE_MESSAGE_MAP 就相当于声明了这样一个数据结构:
这个数据结构的内容填塞工作由三个宏完成:
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
AFX_MSGMAP* theClass::GetMessageMap() const \
{ return &theClass::messageMap; } \
AFX_MSGMAP theClass::messageMap = \
{ &(baseClass::messageMap), \
(AFX_MSGMAP_ENTRY*) &(theClass::_messageEntries) }; \
AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \
{
#define ON_COMMAND (id, memberFxn) \
{ WM_COMMAND, 0, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)memberFxn },
#define END_MESSAGE_MAP () \
{ 0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
};
其中的AfxSig_end 定义为:
enum AfxSig
{
AfxSig_end = 0, // [marks end of message map]
AfxSig_vv,
};
于是,以CView 为例,下面的源代码:
// in header file class CView : public CWnd { public: ... DECLARE_MESSAGE_MAP() }; // in implementation file #define CViewid 122 ... BEGIN_MESSAGE_MAP(CView, CWnd) ON_COMMAND(CViewid, 0) END_MESSAGE_MAP()
就被展开成为:
// in header file class CView : public CWnd { public: ... static AFX_MSGMAP_ENTRY _messageEntries[]; static AFX_MSGMAP messageMap; virtual AFX_MSGMAP* GetMessageMap() const; }; // in implementation file AFX_MSGMAP* CView::GetMessageMap() const { return &CView::messageMap; } AFX_MSGMAP CView::messageMap = { &(CWnd::messageMap), (AFX_MSGMAP_ENTRY*) &(CView::_messageEntries) }; AFX_MSGMAP_ENTRY CView::_messageEntries[] = { { WM_COMMAND, 0, (WORD)122, (WORD)122, 1, (AFX_PMSG)0 }, { 0, 0, 0, 0, 0, (AFX_PMSG)0 } };
以图表示则为:
我们还可以定义各种类似ON_COMMAND 这样的宏,把各式各样的消息与特定的处理例程关联起来。MFC 里头就有名为ON_WM_PAINT 、ON_WM_CREATE、ON_WM_SIZE...等等的宏。
我在Frame7 范例程序中为CCmdTarget 的每一衍生类别都产生类似上图的消息映射表:
// in header files
class CObject
{
... // 注意:CObject 并不属于消息流动网的一份子。
};class CCmdTarget : public CObject{...DECLARE_MESSAGE_MAP()};class CWinThread : public CCmdTarget{
... // 注意:CWinThread 并不属于消息流动网的一份子。
};class CWinApp : public CWinThread{...
DECLARE_MESSAGE_MAP()};class CDocument : public CCmdTarget{...
DECLARE_MESSAGE_MAP()};class CWnd : public CCmdTarget{...
DECLARE_MESSAGE_MAP()};class CFrameWnd : public CWnd{...
DECLARE_MESSAGE_MAP()};class CView : public CWnd{...
DECLARE_MESSAGE_MAP()};class CMyWinApp : public CWinApp{...
DECLARE_MESSAGE_MAP()};class CMyFrameWnd : public CFrameWnd{...
DECLARE_MESSAGE_MAP()};class CMyDoc : public CDocument{...
DECLARE_MESSAGE_MAP()};class CMyView : public CView{...
DECLARE_MESSAGE_MAP()};
并且把各消息映射表的关联性架设起来,给予初值(每一个映射表都只有ON_COMMAND一个项目):
BEGIN_MESSAGE_MAP(CWnd, CCmdTarget)
ON_COMMAND(CWndid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CFrameWnd, CWnd)
ON_COMMAND(CFrameWndid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CDocument, CCmdTarget)
ON_COMMAND(CDocumentid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CView, CWnd)
ON_COMMAND(CViewid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CWinApp, CCmdTarget)
ON_COMMAND(CWinAppid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMyWinApp, CWinApp)
ON_COMMAND(CMyWinAppid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMyFrameWnd, CFrameWnd)
ON_COMMAND(CMyFrameWndid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMyDoc, CDocument)
ON_COMMAND(CMyDocid, 0)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMyView, CView)
ON_COMMAND(CMyViewid, 0)
END_MESSAGE_MAP()
同时也设定了消息的终极镖靶CCmdTarget 的映射表内容:
AFX_MSGMAP CCmdTarget::messageMap =
{
NULL,
&CCmdTarget::_messageEntries[0]
};
AFX_MSGMAP_ENTRY CCmdTarget::_messageEntries[] =
{
{ 0, 0, CCmdTargetid, 0, AfxSig_end, 0 }
};
于是,整个消息流动网就隐然成形了(图3- 5 )。
为了验证整个消息映射表,我必须在映射表中做点记号,等全部构造完成之后,再一一追踪把记号显示出来。我将为每一个类别的消息映射表加上这个项目:
ON_COMMAND(Classid, 0)
这样就可以把Classid 嵌到映射表中当作记号。正式用途(于MFC 中)当然不是这样,这只不过是权宜之计。
在main 函数中,我先产生四个对象(分别是CMyWinApp、CMyFrameWnd、CMyDoc、CMyView 对象):
CMyWinApp theApp; // theApp 是CMyWinApp 对象
void main()
{
CWinApp* pApp = AfxGetApp();
pApp->InitApplication();
pApp->InitInstance(); // 产生CMyFrameWnd 对象
pApp->Run();
CMyDoc* pMyDoc = new CMyDoc; // 产生CMyDoc 对象
CMyView* pMyView = new CMyView; // 产生CMyView 对象
CFrameWnd* pMyFrame = (CFrameWnd*)pApp->m_pMainWnd;
...
}
然后分别取其消息映射表,一路追踪上去,把每一个消息映射表中的类别记号打印出来:
void main()
{
...
AFX_MSGMAP* pMessageMap = pMyView->GetMessageMap();
cout << endl << "CMyView Message Map : " << endl;
MsgMapPrinting(pMessageMap);
pMessageMap = pMyDoc->GetMessageMap();
cout << endl << "CMyDoc Message Map : " << endl;
MsgMapPrinting(pMessageMap);
pMessageMap = pMyFrame->GetMessageMap();
cout << endl << "CMyFrameWnd Message Map : " << endl;
MsgMapPrinting(pMessageMap);
pMessageMap = pApp->GetMessageMap();
cout << endl << "CMyWinApp Message Map : " << endl;
MsgMapPrinting(pMessageMap);
}
下面这个函数追踪并打印消息映射表中的classid 记号:
void MsgMapPrinting(AFX_MSGMAP* pMessageMap)
{
for(; pMessageMap != NULL; pMessageMap = pMessageMap->pBaseMessageMap)
{
AFX_MSGMAP_ENTRY* lpEntry = pMessageMap->lpEntries;
printlpEntries(lpEntry);
}
}
void printlpEntries(AFX_MSGMAP_ENTRY* lpEntry)
{
struct {
int classid;
char* classname;
} classinfo[] = {
CCmdTargetid, "CCmdTarget",
CWinThreadid, "CWinThread",
CWinAppid, "CWinApp",
CMyWinAppid, "CMyWinApp",
CWndid, "CWnd",
CFrameWndid, "CFrameWnd",
CMyFrameWndid, "CMyFrameWnd",
CViewid, "CView",
CMyViewid, "CMyView",
CDocumentid, "CDocument",
CMyDocid, "CMyDoc",
0, " "
};
for (int i=0; classinfo[i].classid != 0; i++)
{
if (classinfo[i].classid == lpEntry->nID)
{
cout << lpEntry->nID << " ";
cout << classinfo[i].classname << endl;
break;
}
}
}
Frame7 的执行结果是:
CMyView Message Map :
1221 CMyView
122 CView
12 CWnd
1 CCmdTarget
CMyDoc Message Map :
131 CMyDoc
13 CDocument
1 CCmdTarget
CMyFrameWnd Message Map :
1211 CMyFrameWnd
121 CFrameWnd
12 CWnd
1 CCmdTarget
CMyWinApp Message Map :
1111 CMyWinApp
111 CWinApp
1 CCmdTarget
Command Routing(命令绕行)
我们已经在上一节把整个消息流动网架设起来了。当消息进来,会有一个邦浦推动它前进。消息如何进来,以及邦浦函数如何推动,都是属于Windows 程序设计的范畴,暂时不管。我现在要仿真出消息的流动绕行路线--我常喜欢称之为消息的「二万五千里长征」。
消息如果是从子类别流向父类别(纵向流动),那么事情再简单不过,整个Message Map消息映射表已规划出十分明确的路线。但是正如上一节一开始我说的,MFC 之中用来处理消息的C++ 类别并不呈单鞭发展,作为application framework 的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是document/view,没有关系);因此,消息应该有横向流动的机会。MFC 对于消息绕行的规定是:
1、如果是一般的Windows 消息(WM_xxx),一定是由衍生类别流向基础类别,没有旁流的可能。
„
2、 如果是命令消息WM_COMMAND,就有奇特的路线了:
„
不管这个规则是怎么定下来的,现在我要设计一个推动引擎,把它仿真出来。以下这些函数名称以及函数内容,完全仿真MFC 内部。有些函数似乎赘余,那是因为我删掉了许多主题以外的动作。不把看似赘余的函数拿掉或合并,是为了留下MFC 的足迹。此外,为了追踪调用过程(call stack ),我在各函数的第一行输出一串识别文字。
首先我把新增加的一些成员函数做个列表:
全域函数AfxWndProc 就是我所谓的推动引擎的起始点。它本来应该是在CWinThread:: Run 中被调用,但为了实验目的,我在main 中调用它,每调用一次便推送一个消息。这个函数在MFC 中有四个参数,为了方便,我加上第五个,用以表示是谁获得消息(成为绕行的起点)。例如:
AfxWndProc(0, WM_CREATE, 0, 0, pMyFrame);
表示pMyFrame 获得了一个WM_CREATE,而:
AfxWndProc(0, WM_COMMAND, 0, 0, pMyView);
表示pMyView 获得了一个WM_COMMAND。
下面是消息流动的过程:
LRESULT AfxWndProc (HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam,
CWnd *pWnd) // last param. pWnd is added by JJHou.
{
cout << "AfxWndProc()" << endl;
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
LRESULT AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
WPARAM wParam, LPARAM lParam)
{
cout << "AfxCallWndProc()" << endl;
LRESULT lResult = pWnd->WindowProc(nMsg, wParam, lParam);
return lResult;
}
pWnd-> WindowProc 究竟是调用哪一个函数?不一定,得视pWnd到底指向何种类别之对象而定-- 别忘了WindowProc是虚拟函数。这正是虚拟函数发挥它功效的地方呀:
***如果pWnd 指向CMyFrameWnd 对象,那么调用的是CFrameWnd:: WindowProc。而因为CFrameWnd 并没有改写WindowProc ,所以调用的其实是CWnd:: WindowProc。
***如果pWnd 指向CMyView 对象,那么调用的是CView:: WindowProc。而因为CView并没有改写WindowProc,所以调用的其实是CWnd:: WindowProc。虽然殊途同归,意义上是不相同的。切记!切记!
CWnd:: WindowProc 首先判断消息是否为WM_COMMAND。如果不是,事情最单纯,就把消息往父类别推去,父类别再往祖父类别推去。每到一个类别的消息映射表,原本应该比对AFX_MSGMAP_ENTRY 的每一个元素,比对成功就调用对应的处理例程。不过在这里我不作比对,只是把AFX_MSGMAP_ENTRY 中的类别识别代码印出来(就像上一节的Frame7 程序一样),以表示「到此一游」:
LRESULT CWnd::WindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
{
AFX_MSGMAP* pMessageMap;
AFX_MSGMAP_ENTRY* lpEntry;
if (nMsg == WM_COMMAND) // special case for commands
{
if (OnCommand(wParam, lParam))
return 1L; // command handled
else
return (LRESULT) DefWindowProc (nMsg, wParam, lParam);
}
pMessageMap = GetMessageMap();
for (; pMessageMap != NULL;
pMessageMap = pMessageMap->pBaseMessageMap)
{
lpEntry = pMessageMap->lpEntries;
printlpEntries(lpEntry);
}
return 0; // J.J.Hou: if find, should call lpEntry->pfn,
// otherwise should call DefWindowProc.
// for simplification, we just return 0.
}
如果消息是WM_COMMAND,CWnd:: WindowProc 调用 OnCommand。好,注意了,这又是一个CWnd 的虚拟函数:
1. 如果this 指向CMyFrameWnd 对象,那么调用的是CFrameWnd:: OnCommand。
2. 如果this 指向CMyView 对象,那么调用的是CView:: OnCommand。而因为CView
并没有改写OnCommand,所以调用的其实是CWnd:: OnCommand。这次可就没有殊途同归了。
我们以第一种情况为例,再往下看:
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
cout << "CFrameWnd::OnCommand()" << endl;
// ...
// route as normal command
return CWnd::OnCommand(wParam, lParam);
}
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{
cout << "CWnd::OnCommand()" << endl;
// ...
return OnCmdMsg(0, 0);
}
又一次遭遇虚拟函数。经过前两次的分析,相信你对此很有经验了。 OnCmdMsg 是CCmdTarget 的虚拟函数,所以:
1. 如果this 指向CMyFrameWnd 对象,那么调用的是CFrameWnd:: OnCmdMsg 。
2. 如果this 指向CMyView 对象,那么调用的是CView:: OnCmdMsg 。
3. 如果this 指向CMyDoc 对象,那么调用的是CDocument :: OnCmdMsg 。
4. 如果this 指向CMyWinApp 对象,那么调用的是CWinApp :: OnCmdMsg 。而因为CWinApp 并没有改写OnCmdMsg , 所以调用的其实是CCmdTarget:: OnCmdMsg 。
目前的情况是第一种,于是调用CFrameWnd::OnCmdMsg:
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode)
{
cout << "CFrameWnd::OnCmdMsg()" << endl;
// pump through current view FIRST
CView* pView = GetActiveView();
if (pView->OnCmdMsg(nID, nCode))
return TRUE;
// then pump through frame
if (CWnd::OnCmdMsg(nID, nCode))
return TRUE;
// last but not least, pump through app
CWinApp* pApp = AfxGetApp();
if (pApp->OnCmdMsg(nID, nCode))
return TRUE;
return FALSE;
}
这个函数反应出图3-6 Frame 窗口处理WM_COMMAND 的次序。最先调用的是
pView-> OnCmdMsg ,于是:
BOOL CView::OnCmdMsg(UINT nID, int nCode)
{
cout << "CView::OnCmdMsg()" << endl;
if (CWnd::OnCmdMsg(nID, nCode))
return TRUE;
BOOL bHandled = FALSE;
bHandled = m_pDocument->OnCmdMsg(nID, nCode);
return bHandled;
}
这又反应出图3-6 View 窗口处理WM_COMMAND 的次序。最先调用的是CWnd::OnCmdMsg ,而CWnd 并未改写OnCmdMsg ,所以其实就是调用
CCmdTarget:: OnCmdMsg:
{BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode)}
cout << "CCmdTarget::OnCmdMsg()" << endl;
// Now look through message map to see if it applies to us
AFX_MSGMAP* pMessageMap;
AFX_MSGMAP_ENTRY* lpEntry;
for (pMessageMap = GetMessageMap() ; pMessageMap != NULL;
pMessageMap = pMessageMap->pBaseMessageMap)
{
lpEntry = pMessageMap->lpEntries;
printlpEntries(lpEntry);
} return FALSE; // not handled
{
这是一个走访消息映射表的动作。注意,GetMessageMap 也是个虚拟函数(隐藏在DECLARE_MESSAGE_MAP 宏定义中),所以它所得到的消息映射表将是this (以目前而言是pMyView)所指对象的映射表。于是我们得到了这个结果:
pMyFrame received a WM_COMMAND, routing path and call stack:
AfxWndProc()
AfxCallWndProc()
CWnd::WindowProc()
CFrameWnd::OnCommand()
CWnd::OnCommand()
CFrameWnd::OnCmdMsg()
CFrameWnd::GetActiveView()
CView::OnCmdMsg()
CCmdTarget::OnCmdMsg()
1221 CMyView
122 CView
12 CWnd
1 CCmdTarget
如果在映射表中找到了对应的消息,就调用对应的处理例程,然后也就结束了二万五千里长征。如果没找到,长征还没有结束,这时候退守回到CView:: OnCmdMsg ,调用
CDocument:: OnCmdMsg:
BOOL CDocument::OnCmdMsg(UINT nID, int nCode)
{
cout << "CDocument::OnCmdMsg()" << endl;
if (CCmdTarget::OnCmdMsg(nID, nCode))
return TRUE;
return FALSE;
}
于是得到这个结果:
CDocument::OnCmdMsg()
CCmdTarget::OnCmdMsg()
131 CMyDoc
13 CDocument
1 CCmdTarget
如果在映射表中还是没找到对应消息,二万五千里长征还是未能结束,这时候退守回到CFrameWnd:: OnCmdMsg ,调用 CWnd::OnCmdMsg(也就是CCmdTarget:: OnCmdMsg ),得到这个结果:
CCmdTarget::OnCmdMsg()
1211 CMyFrameWnd
121 CFrameWnd
12 CWnd
1 CCmdTarget
如果在映射表中还是没找到对应消息,二万五千里长征还是未能结束,再退回到CFrameWnd:: OnCmdMsg ,调用 CWinApp :: OnCmdMsg (亦即CCmdTarge t:: OnCmdMsg ),得到这个结果:
1111 CMyWinApp
111 CWinApp
1 CCmdTarget
万一还是没找到对应的消息,二万五千里长征可也穷途末路了,退回到CWnd::WindowPro c,调用 CWnd:: DefWindowProc。你可以想象,在真正的MFC 中这个成员函数必是调用Windows API 函数:: DefWindowProc。为了简化,我让它在Frame8 中是个空函数。
故事结束!
我以图3-7 表示这二万五千里长征的调用次序(call stack ),图3-8 表示这二万五千里长征的消息流动路线。
Frame8 测试四种情况:分别从frame对象和view 对象中推动消息,消息分一般Windows 消息和WM_COMMAND 两种:
// test Message Routing
AfxWndProc(0, WM_CREATE, 0, 0, pMyFrame);
AfxWndProc(0, WM_PAINT, 0, 0, pMyView);
AfxWndProc(0, WM_COMMAND, 0, 0, pMyView);
AfxWndProc(0, WM_COMMAND, 0, 0, pMyFrame);
结果:
本章回顾
像外科手术一样精准,我们拿起锋利的刀子,划开MFC 坚轫的皮肤,再一刀下去,剖开它的肌理。掏出它的内脏,反复观察研究。终于,借着从MFC 掏挖出来的源代码清洗整理后完成的几个小小的C++ console 程序,我们彻底了解了所谓Runtime Class 、Runtime Time Information 、Dynamic Creation、Message Mapping、Command Routing 的内部机制。
咱们并不是要学着做一套application framework,但是这样的学习过程确实有必要。因为,「只用一样东西,不明白它的道理,实在不高明」。况且,有什么比光靠三五个一两百行小程序,就搞定对象导向领域中的高明技术,更值得的事?有什么比欣赏那些由Runtime Class 所构成的「类别型录网」示意图、消息的实际流动图、消息映射表的架构图,更令人心旷神怡?
把Frame1~Frame8好好研究一遍,你已经对MFC 的架构成竹在胸。再来,就是MFC 类别的实际运用,以及Visual C++ 工具的熟练!
这章终于过去了, 用了5天的时间仔细研究了这几个小console程序,简单的代码暴露出了MFC复杂的内部运行规律,这样的分析方式让我受益匪浅!