三、MFC 六大关键技术之仿真 (学习笔记)

演化(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 消息绕行

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 六大关键技术之仿真 (学习笔记)_第1张图片

这个实例仿真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 范例程序的类别阶层及其成员。对于那些「除了构造式与析构式之外没有其它成员」的类别,我就不在图中展开他们了:
三、MFC 六大关键技术之仿真 (学习笔记)_第2张图片


就如我曾在第1章解释过的,InitApplication 和InitInstance  现在成了MFC 的CWinApp的两个虚拟函数。前者负责「每一个程序只做一次」的动作,后者负责「每一个执行个体都得做一次」的动作。通常,系统会(并且有能力)为你注册一些标准的窗口类别(当然也就准备好了一些标准的窗口函数),你(应用程序设计者)应该在你的CMyWinApp中改写InitInstance ,并在其中把窗口产生出来--  这样你才有机会在标准的窗口类别中指定自己的窗口标题和菜单。下面就是我们新的main 函数:

void main()
{

CWinApp* pApp = AfxGetApp();

pApp->InitApplication();
pApp->InitInstance();
pApp->Run();
}

其中pApp指向theApp全域对象。在这里我们开始看到了虚拟函数的妙用(还不熟练者请快复习第2章):
三、MFC 六大关键技术之仿真 (学习笔记)_第3张图片



好,请注意以下CMyWinApp::InitInstance  的动作,以及它所引发的行为:
三、MFC 六大关键技术之仿真 (学习笔记)_第4张图片
三、MFC 六大关键技术之仿真 (学习笔记)_第5张图片

你看到了,这些函数什么正经事儿也没做,光只输出一个标识符串。我主要的目的是在让你先熟悉MFC程序的执行流程。
三、MFC 六大关键技术之仿真 (学习笔记)_第6张图片

以下就是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
};

三、MFC 六大关键技术之仿真 (学习笔记)_第7张图片

我希望,每一个类别都能拥有这样一个CRuntimeClass 成员变量,并且最好有一定的命名规则(例如在类别名称之前冠以" class"  作为它的名称),然后,经由某种手段将整个类别库构造好之后,「类别型录」能呈现类似这样的风貌:

三、MFC 六大关键技术之仿真 (学习笔记)_第8张图片



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对象)的内容指定以及串接工作最好也能够神不知鬼不觉,我们于是再定义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)  ,就完成了构造资料并加入串行的工作:

三、MFC 六大关键技术之仿真 (学习笔记)_第9张图片

可是你知道,串行的头,总是需要特别费心处理,不能够套用一般的串行行为模式。我们的类别根源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;

终于,整个「类别型录」串行的头部就这样形成了:

三、MFC 六大关键技术之仿真 (学习笔记)_第10张图片


范例程序Frame3 在.h 档中有这些类别声明:
三、MFC 六大关键技术之仿真 (学习笔记)_第11张图片

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  这样一个大网。
三、MFC 六大关键技术之仿真 (学习笔记)_第12张图片

为了实证整个类别型录网的存在,我在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; }

图示如下:
三、MFC 六大关键技术之仿真 (学习笔记)_第13张图片
「对象生成器」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 )、圆形、矩形。我打算用以下类别,组织这份文件:
三、MFC 六大关键技术之仿真 (学习笔记)_第14张图片

其中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  写成这样的文件内容好吗:
三、MFC 六大关键技术之仿真 (学习笔记)_第15张图片

还算堪用。但如果考虑到屏幕卷动的问题,以及打印输出的问题,应该在最前端增加「文件大小」。另外,如果这份文件有100条线条,50个圆形,80个矩形,难不成我们要记录230  个类别名称?应该有更好的方法才是。
三、MFC 六大关键技术之仿真 (学习笔记)_第16张图片
我们可以在每次记录对象内容的时候,先写入一个代码,表示此对象之类别是否曾在档案中记录过了。如果是新类别,乖乖地记录其类别名称;如果是旧类别,则以代码表示。这样可以节省文件大小以及程序用于解析的时间。啊,不要看到文件大小就想到硬盘很便宜,桌上的一切都将被带到网上,你得想想网络频宽这回事。

还有一个问题。文件的「版本」如何控制?旧版程序读取新版文件,新版程序读取旧版文件,都可能出状况。为了防弊,最好把版本号码记录上去。最好是每个类别有自己的版本号码。

下面是新的构想,也就是Serialization的目标:
三、MFC 六大关键技术之仿真 (学习笔记)_第17张图片


我希望有一个专门负责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 六大关键技术之仿真 (学习笔记)_第18张图片

但是,MFC 之中用来处理消息的C++ 类别,并不呈单鞭发展。作为application framework的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是document/view,没有关系)。因此,消息藉以攀爬的路线应该有横流的机会:
三、MFC 六大关键技术之仿真 (学习笔记)_第19张图片

消息如何流动,我们暂时先不管。是直线前进,或是中途换跑道,我们都暂时不管,本节先把这个攀爬路线网建立起来再说。这整个攀爬路线网就是所谓的消息映射表(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  就相当于声明了这样一个数据结构:
三、MFC 六大关键技术之仿真 (学习笔记)_第20张图片
这个数据结构的内容填塞工作由三个宏完成:

#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 } };

以图表示则为:
三、MFC 六大关键技术之仿真 (学习笔记)_第21张图片

我们还可以定义各种类似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 )。
三、MFC 六大关键技术之仿真 (学习笔记)_第22张图片


为了验证整个消息映射表,我必须在映射表中做点记号,等全部构造完成之后,再一一追踪把记号显示出来。我将为每一个类别的消息映射表加上这个项目:

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 六大关键技术之仿真 (学习笔记)_第23张图片

不管这个规则是怎么定下来的,现在我要设计一个推动引擎,把它仿真出来。以下这些函数名称以及函数内容,完全仿真MFC 内部。有些函数似乎赘余,那是因为我删掉了许多主题以外的动作。不把看似赘余的函数拿掉或合并,是为了留下MFC 的足迹。此外,为了追踪调用过程(call stack ),我在各函数的第一行输出一串识别文字。

首先我把新增加的一些成员函数做个列表:
三、MFC 六大关键技术之仿真 (学习笔记)_第24张图片


全域函数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  表示这二万五千里长征的消息流动路线。

三、MFC 六大关键技术之仿真 (学习笔记)_第25张图片


三、MFC 六大关键技术之仿真 (学习笔记)_第26张图片



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 六大关键技术之仿真 (学习笔记)_第27张图片
三、MFC 六大关键技术之仿真 (学习笔记)_第28张图片



本章回顾


像外科手术一样精准,我们拿起锋利的刀子,划开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复杂的内部运行规律,这样的分析方式让我受益匪浅!












你可能感兴趣的:(三、MFC 六大关键技术之仿真 (学习笔记))