第3章 MFC六大关键技术之模拟

演化(evolution)永远在进行,

这个世界却不是每天都有革命(revolution)发生。

Application Framework 在软件界确实称得上具有革命精神。

整个 MFC 4.0 多达 189 个类,原始代码达 252 个实现文件,58 个头文件,共 10 MB 之多。MFC 4.2 又多加了 29 个类。这么庞大的对象,当然不是每一个类每一个数据结构都是我的仿真目标。我只挑选最神秘又最重要,与应用程序主干息息相关的题目,包括:

  • MFC 程序的初始化过程

  • RTTI(Runtime Type Information)运行时类型信息

  • Dynamic Creation 动态生成

  • Persistence 永续留存

  • Message Mapping 消息映射

  • Message Routing 消息循环

MFC本身的设计在Application Framework之中不见得最好,敌视者甚至认为它是个Minotaur(注)!但无论如何,这是当今软件霸主微软公司的产品,从探究application framework 设计的角度来说,实为一个重要参考;而如果从选择一套 application framework作为软件开发工具的角度来说,单就就业市场的需求,我对 MFC 的推荐再加 10 分!

注:Minotaur 是希腊神话中的牛头人身怪物,居住在迷宫之中。进入迷宫的人如果走不出来,就会被牠一口吃掉。

以下所有程序的类阶层架构、类名称、变量名称、结构名称、函数名称、函数行为,都以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 范例程序

MFC.H

#0001  #include <iostream.h>
#0003  class CObject
#0004  {
#0005   public:
#0006       CObject::CObject()  {  cout << "CObject Constructor \n";  }
#0007       CObject::~CObject() {  cout << "CObject Destructor \n";   }
#0008  };
#0010  class CCmdTarget : public CObject
#0011  {
#0012   public:
#0013   CCmdTarget::CCmdTarget(){cout << "CCmdTarget Constructor \n"; }
#0014   CCmdTarget::~CCmdTarget(){cout << "CCmdTarget Destructor\n";}
#0015  };
#0017  class CWinThread : public CCmdTarget
#0018  {
#0019   public:
#0020   CWinThread::CWinThread(){cout<<"CWinThread Constructor\n"; }
#0021   CWinThread::~CWinThread(){cout<<"CWinThread Destructor\n";  }
#0022  };
#0024  class CWinApp : public CWinThread
#0025  {
#0026   public:
#0027       CWinApp* m_pCurrentWinApp;
#0029   public:
#0030       CWinApp::CWinApp()  { m_pCurrentWinApp = this;
               cout << "CWinApp Constructor \n";  }
#0031       CWinApp::~CWinApp() { cout << "CWinApp Destructor \n";   }
#0032  };
#0034  class CDocument : public CCmdTarget
#0035  {
#0036   public:
#0037       CDocument::CDocument(){cout<< "CDocument Constructor \n"; }
#0038       CDocument::~CDocument(){cout<<"CDocument Destructor \n";}
#0039  };
#0042  class CWnd : public CCmdTarget
#0043  {
#0044   public:
#0045       CWnd::CWnd(){cout << "CWnd Constructor \n"; }
#0046       CWnd::~CWnd(){cout<< "CWnd Destructor \n";  }
#0047  };
#0049  class CFrameWnd : public CWnd
#0050  {
#0051   public:
#0052       CFrameWnd::CFrameWnd(){cout<< "CFrameWnd Constructor \n"; }
#0053       CFrameWnd::~CFrameWnd(){cout<<"CFrameWnd Destructor \n";}
#0054  };
#0056  class CView : public CWnd
#0057  {
#0058   public:
#0059       CView::CView(){  cout << "CView Constructor \n"; }
#0060       CView::~CView()  {  cout << "CView Destructor \n";  }
#0061  };
#0064  // global function
#0066  CWinApp* AfxGetApp();

MFC.CPP

#0001  #include "my.h"//原本含入mfc.h就好,但为了CMyWinApp的定义,所以...
#0003  extern CMyWinApp theApp;
#0005  CWinApp* AfxGetApp()
#0006  {
#0007      return theApp.m_pCurrentWinApp;
#0008  }

MY.H

#0001  #include <iostream.h>
#0002  #include "mfc.h"
#0003
#0004  class CMyWinApp : public CWinApp
#0005  {
#0006   public:
#0007      CMyWinApp::CMyWinApp(){ cout<< "CMyWinApp Constructor \n"; }
#0008      CMyWinApp::~CMyWinApp(){cout<< "CMyWinApp Destructor \n";}
#0009  };
#0010
#0011  class CMyFrameWnd : public CFrameWnd
#0012  {
#0013   public:
#0014       CMyFrameWnd(){  cout << "CMyFrameWnd Constructor \n";}
#0015       ~CMyFrameWnd(){ cout << "CMyFrameWnd Destructor \n";}
#0016  };

MY.CPP

#0001  #include "my.h"
#0002
#0003  CMyWinApp theApp;  // global object
#0006  // main
#0008  void main()
#0009  {
#0011      CWinApp* pApp = AfxGetApp();
#0013  }

Frame1 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

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 程序的初始化过程的简化。

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

// MY.CPP
CMyWinApp theApp;
void main()
{
CWinApp* pApp = AfxGetApp();
pApp->InitApplication();
pApp->InitInstance();
pApp->Run();
}

其中pApp指向theApp全局对象。在这里我们开始看到了虚函数的妙用(还不熟练者请快复习第2章):

pApp->InitApplication()调用的是CWinApp::InitApplication, pApp->InitInstance()调用的是CMyWinApp::InitInstance(因为CMyWinApp改写它了),pApp->Run()调用的是CWinApp::Run,好,请注意以下 CMyWinApp::InitInstance 的动作,以及它所引发的行为:

你看到了,这些函数什么正经事儿也没做,光只输出一个标识符串。我主要的目的是在让你先熟悉MFC程序的执行流程。

Frame2 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

以下就是 Frame2 的执行结果:

CWinApp::InitApplication
CMyWinApp::InitInstance
CMyFrameWnd::CMyFrameWnd
CFrameWnd::Create
CWnd::CreateEx
CFrameWnd::PreCreateWindow
CWinApp::Run
CWinThread::Run

Frame2 范例程序

MFC.H

#0001  #define BOOL int
#0002  #define TRUE 1
#0003  #define FALSE 0
#0004
#0005  #include <iostream.h>
#0006
#0007  class CObject
#0008  {
#0009  public:
#0010      CObject::CObject()  { }
#0011      CObject::~CObject() { }
#0012  };
#0013
#0014  class CCmdTarget : public CObject
#0015  {
#0016   public:
#0017       CCmdTarget::CCmdTarget()  {  }
#0018       CCmdTarget::~CCmdTarget() {  }
#0019  };
#0020
#0021  class CWinThread : public CCmdTarget
#0022  {
#0023  public:
#0024  CWinThread::CWinThread()  {  }
#0025  CWinThread::~CWinThread() {  }
#0026
#0027  virtual BOOL InitInstance() {
#0028                           cout << "CWinThread::InitInstance \n";
#0029                           return TRUE;
#0030                           }
#0031  virtual int Run() {
#0032                     cout << "CWinThread::Run \n";
#0033                     return 1;
#0034                     }
#0035  };
#0036
#0037  class CWnd;
#0038
#0039  class CWinApp : public CWinThread
#0040  {
#0041  public:
#0042      CWinApp* m_pCurrentWinApp;
#0043      CWnd* m_pMainWnd;
#0044
#0045  public:
#0046      CWinApp::CWinApp()  { m_pCurrentWinApp = this; }
#0047      CWinApp::~CWinApp() {   }
#0048
#0049  virtual BOOL InitApplication() {
#0050                       cout << "CWinApp::InitApplication \n";
#0051                       return TRUE;
#0052                       }
#0053  virtual BOOL InitInstance(){
#0054                       cout << "CWinApp::InitInstance \n";
#0055                       return TRUE;
#0056                       }
#0057  virtual int Run() {
#0058                     cout << "CWinApp::Run \n";
#0059                     return CWinThread::Run();
#0060                   }
#0061  };
#0062
#0063
#0064  class CDocument : public CCmdTarget
#0065  {
#0066  public:
#0067      CDocument::CDocument(){  }
#0068      CDocument::~CDocument()  {  }
#0069  };
#0070
#0071
#0072  class CWnd : public CCmdTarget
#0073  {
#0074  public:
#0075      CWnd::CWnd(){  }
#0076      CWnd::~CWnd() {  }
#0077
#0078  virtual BOOL Create();
#0079  BOOL CreateEx();
#0080  virtual BOOL PreCreateWindow();
#0081  };
#0082
#0083  class CFrameWnd : public CWnd
#0084  {
#0085  public:
#0086      CFrameWnd::CFrameWnd(){  }
#0087      CFrameWnd::~CFrameWnd() {  }
#0088  BOOL Create();
#0089  virtual BOOL PreCreateWindow();
#0090  };
#0091
#0092  class CView : public CWnd
#0093  {
#0094  public:
#0095      CView::CView(){  }
#0096      CView::~CView() {  }
#0097  };
#0098
#0099
#0100  // global function
#0101  CWinApp* AfxGetApp();

MFC.CPP

MY.H

MY.CPP

RTTI(运行时类型辨识)

你已经在第2章看到,Visual C++ 4.0 支持RTTI,重点不外乎是:

1. 编译时需选用/GR 选项(/GR 的意思是enable C++ RTTI)

2. 含入typeinfo.h

3. 使用新的typeid 运算符。

RTTI 亦有称为 Runtime Type Identification 者。

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)

类型录网与 CRuntimeClass

怎么设计RTTI呢?让我们想想,当你手上握有一种色泽,想知道它的RGB成份比,不查色表行吗?当你持有一种产品,想知道它的型号,不查型录行吗?要达到RTTI 的能力,我们(类库的设计者)一定要在类构造起来的时候,记录必要的信息,以建立型录。型录中的类信息,最好以串列(linked list)方式串接起来,将来方便一一比对。

我们这份「类型录」的串列元素将以CRuntimeClass描述之,那是一个结构,内中至少需有类名称、串列的 Next 指针,以及串列的 First 指针。由于 First 指针属于全局变量,一份就好,所以它应该以 static 修饰之。除此之外你所看到的其它 CRuntimeClass成员都是为了其它目的而准备,陆陆续续我会介绍出来。

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

DECLARE_DYNAMIC / IMPLEMENT_DYNAMIC 宏

为了神不知鬼不觉把 CRuntimeClass 对象塞到类之中,并声明一个可以抓到该对象位址的函数,我们定义 DECLARE_DYNAMIC 宏如下:

出现在宏定义之中的 ##,用来告诉编译器,把两个字符串系在一起。如果你这么使用此宏:

DECLARE_DYNAMIC(CView)

编译器前置处理器为你做出的代码是:

这下子,只要在声明类时放入 DECLARE_DYNAMIC宏即万事OK喽。

不,还没有 OK,类型录(也就是各个 CRuntimeClass 对象)的内容指定以及串接工作最好也能够神不知鬼不觉,我们于是再定义 IMPLEMENT_DYNAMIC 宏:

其中的 _IMPLEMENT_RUNTIMECLASS 又是一个宏。这样区分是为了此一宏在「动态生成」(下一节主题)时还会用到。

其中又有 RUNTIME_CLASS 宏,定义如下:

看起来整个 IMPLEMENT_DYNAMIC 内容好像只是指定初值,不然,其曼妙处在于它所使用的一个 struct AFX_CLASSINIT,定义如下:

这表示它有一个构造函数(别惊讶,C++ 的 struct 与 class 都有构造函数),定义如下:

很明显,此构造函数负责linked list的串接工作。整组宏看起来有点吓人,其实也没有什么,文字代换而已。现在看看这个实例:

上述的代码展开来成为:

于是乎,程序中只需要简简单单的两个宏DECLARE_DYNAMIC(Cxxx)和IMPLEMENT_DYNAMIC(Cxxx, Cxxxbase) ,就完成了构造数据并加入串列的工作:

可是你知道,串列的头,总是需要特别费心处理,不能够套用一般的串列行为模式。我们的类根源CObject,不能套用现成的宏DECLARE_DYNAMIC 和IMPLEMENT_DYNAMIC,必须特别设计如下:

并且,CRuntimeClass 中的static 成员变量应该要初始化(如果你忘记了,赶快复习第2章的「静态成员(变量与函数)」一节):

// in implementation file
CRuntimeClass* CRuntimeClass::pFirstClass = NULL;

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

范例程序Frame3在 .h文件中有这些类声明:

范例程序Frame3在.cpp文件中有这些动作:

于是组织出图 3-1 这样一个大网。

图3-1 CRuntimeClass 对象构成的类型录网。

本图只列出与RTTI 有关系的成员。

为了实证整个类型录网的存在,我在main 函数中调用PrintAllClasses,把串列中的每一个元素的类名称、对象大小、以及schema no. 印出来:

Frame3 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

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

Frame3 范例程序

MFC.H

MFC.CPP

MY.H

MY.CPP

IsKindOf(类型辨识)

有了图 3-1 这张「类型录」网,要实现IsKindOf 功能,再轻松不过了:

1. 为CObject加上一个IsKindOf 函数,于是此函数将被所有类继承。它将把参数所指定的某个CRuntimeClass对象拿来与类型录中的元素一一比对。比对成功(在型录中有发现),就传回TRUE,否则传回FALSE:

注意,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 的使用方式如下:

IsKindOf 的完整范例放在Frame4中。

Frame4 范例程序

Frame4 与 Frame3 大同小异,唯一不同的就是前面所说的,在 CObject 中加上 IsKindOf 函数的声明与定义,并将私有类(non-MFC 类)也挂到「类型录网」中:

我不在此列出Frame4的原始代码,你可以在书附光盘片中找到完整的文件。Frame4 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

以下即是 Frame4 的执行结果:

Dynamic Creation(动态生成)

基础有了,做什么都好。同样地,有了上述的「类型录网」,各种应用纷至沓来。其中一个应用就是解决棘手的动态生成问题。

我已经在第二章描述过动态生成的困难点:你没有办法在程序执行期间,根据动态获得的一个类名称(通常来自读文件,但我将以屏幕输入为例),要求程序产生一个对象。

上述的「类型录网」虽然透露出解决此一问题的些微曙光,但是技术上还得加把劲儿。

如果我能够把类的大小记录在类型录中,把构造函数(注意,这里并非指 C++ 构造函数,而是指即将出现的 CRuntimeClass::CreateObject)也记录在类型录中,当程序在执行时期获得一个类名称,它就可以在「类型录网」中找出对应的元素,然后调用其构造函数(这里并非指 C++ 构造函数),产生出对象。

类型录网的元素型式CRuntimeClass 于是有了变化:

DECLARE_DYNCREATE / IMPLEMENT_DYNCREATE 宏

为了因 应CRuntimeClass 中新增的成员变量 ,我们再添两个宏 ,DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE:

于是,以 CFrameWnd 为例,下列程序代码:

// in header file
class CFrameWnd : public CWnd
{
    DECLARE_DYNCREATE(CFrameWnd)
    ...
};
// in implementation file
IMPLEMENT_DYNCREATE(CFrameWnd, CWnd)

就被展开如下(注意,编译器选项 /P可得前置处理结果):

图示如下:

「对象生成器」CreateObject 函数很简单,只要说new 就好。

从宏的定义我们很清楚可以看出,拥有动态生成(Dynamic Creation)能力的类库,必然亦拥有运行时类型识别(RTTI)能力,因为_DYNCREATE宏涵盖了_DYNAMIC宏。

范例程序 Frame6 在 .h 文件中有这些类声明:

在 .cpp文件中又有这些动作:

IMPLEMENT_DYNAMIC(CCmdTarget, CObject)
IMPLEMENT_DYNAMIC(CWinThread, CCmdTarget)
IMPLEMENT_DYNAMIC(CWinApp, CWinThread)
IMPLEMENT_DYNCREATE(CWnd, CCmdTarget)
IMPLEMENT_DYNCREATE(CFrameWnd, CWnd)
IMPLEMENT_DYNAMIC(CDocument, CCmdTarget)
IMPLEMENT_DYNAMIC(CView, CWnd)
IMPLEMENT_DYNCREATE(CMyFrameWnd, CFrameWnd)
IMPLEMENT_DYNCREATE(CMyDoc, CDocument)
IMPLEMENT_DYNCREATE(CMyView, CView)

于是组织出图 3-2 这样一个大网。

图3-2 以 CRuntimeClass 对象构成的「类型录网」。

本图只列出与动态生成(Dynamic Creation)有关系的成员。

凡是m_pfnCreateObject不为NULL 者,即可动态生成。

现在,我们开始仿真动态生成。首先在main函数中加上这一段代码:

并设计CRuntimeClass::CreateObject和CRuntimeClass::Load如下:

然后,为了验证这样的动态生成机制的确有效(也就是对象的确被产生了),我让许多个类的构造函数都输出一段文字,而且在取得对象指针后,真的去调用该对象的一个成员函数SayHello。我把SayHello设计为虚函数,所以根据不同的对象类型,会调用到不同的SayHello函数,出现不同的输出字符串。

请注意,main 函数中的while循环必须等到CRuntimeClass::Load传回 NULL才会停止,而CRuntimeClass::Load 是在它从整个「类型录网」中找不到它要找的那个类名称时,才传回NULL。这些都是我为了模拟与示范,所采取的权宜设计。

Frame6 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

下面是 Frame6 的执行结果。粗体表示我(程序执行者)在屏幕上输入的类名称:

Frame6 范例程序

MFC.H

MFC.CPP

MY.H

MY.CPP

Persistence(永续生存)机制

面向对象有一个术语:Persistence,意思就是把对象永久保留下来。Power 一关,啥都没有,对象又如何能够永续存留?当然是写到文件去啰。

把数据写到文件,很简单。在Document/View架构中,数据都放在一份 document(文件)里头,我们只要把其中的成员变量依续写进文件即可。成员变量很可能是个对象,而面对对象,我们首先应该记载其类名称,然后才是对象中的数据。

读文件就有点麻烦了。当程序从文件中读到一个类名称,它如何实现(instantiate)一个对象?呵,这不就是动态生成的技术吗?我们在前一章已经解决掉了。

MFC 有一套Serialize机制,目的在于把文件名的选择、文件的开关、缓冲区的建立、数据的读写、提取运算符(>>)和嵌入运算符(<<)的重载(overload)、对象的动态生成...都包装起来。

上述Serialize 的各部分工作,除了数据的读写和对象的动态生成,其余都是支节。动态生成的技术已经解决,让我们集中火力,分析数据的读写动作。

Serialize (数据读写)

假设我有一份文件,用以记录一张图形。图形只有三种基本元素:线条(Stroke)、圆形、 矩形。我打算用以下类,组织这份文件:

其中CObList和CDWordArray是MFC提供的类,前者是一个串列,可放置任何从CObject 派生下来的对象,后者是一个数组,每一个元素都是 "double word"。另外三个类:CStroke 和 CRectangle 和 CCircle,是我从 CObject 中派生下来的类。

假设现有一份文件,内容如图 3-3,如果你是Serialize 机制的设计者,你希望怎么做呢?

把图 3-3 写成这样的文件内容好吗:

还算堪用。但如果考虑到屏幕卷动的问题,以及印表输出的问题,应该在最前端增加「文件大小」。另外,如果这份文件有 100 条线条,50 个圆形,80 个矩形,难不成我们要记录 230 个类名称?应该有更好的方法才是。

图3-3 一个串列,内含三种基本图形:线条、圆形、矩形。

我们可以在每次记录对象内容的时候,先写入一个代码,表示此对象之类是否曾在文件中记录过了。如果是新类,乖乖地记录其类名称;如果是旧类,则以代码表示。

这样可以节省文件大小以及程序用于解析的时间。啊,不要看到文件大小就想到硬盘很便宜,桌上的一切都将被带到网上,你得想想网络频宽这回事。

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

下面是新的构想,也就是Serialization 的目标:

我希望有一个专门负责 Serialization的函数,就叫作 Serialize好了。假设现在我的 Document类名称为 CScribDoc,我希望有这么便利的程序方法(请仔细琢磨琢磨其便利性):

每一个可写到文件或可从文件中读出的类,都应该有它自己的 Serailize 函数,负责它自己的数据读写文件动作。此类并且应该改写 << 运算符和 >> 运算符,把数据导流到archive中。archive 是什么?是一个与文件息息相关的缓冲区,暂时你可以想象它就是文件的化身。当图 3-3 的文件写入文件时,Serialize 函数的调用次序如图 3-4。

图3-4 图3-3的文件内容写入文件时,Serialize 函数的调用次序。

DECLARE_SERIAL / IMPLEMENT_SERIAL 宏

要将<<>>两个运算符重载化,还要让 Serialize 函数神不知鬼不觉地放入类声明之中,最好的作法仍然是使用宏。

类之能够进行文件读写动作,前提是拥有动态生成的能力,所以,MFC 设计了两个宏DECLARE_SERIAL和IMPLEMENT_SERIAL:

为了在每一个对象被处理(读或写)之前,能够处理琐屑的工作,诸如判断是否第一次出现、记录版本号代码、记录文件名等工作,CRuntimeClass 需要两个函数Load 和Store:

你已经在上一节看过Load 函数,当时为了简化,我把它的参数拿掉,改为由屏幕上获得类名称,事实上它应该是从文件中读一个类名称。至于Store函数,是把类名称写入文件中:

// Runtime class serialization code

图 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);
    ...
}

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

AfxSig_xx 用来描述消息处理例程 memberFxn 的类型(参数与回返值)。本例纯为模拟与简化,所以不在这上面作文章。真正讲到MFC时(第四篇p580),我会再解释它。

于是,以CView为例,下面的原始代码:

就被展开成为:

以图表示则为:

我们还可以定义各种类似 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一个项目):

// in implementation files
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)。

图3-5 Frame7程序所架构起来的消息流动网(也就是Message Map)

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

ON_COMMAND(Classid, 0)

这样就可以把 Classid 嵌到映射表中当作记号。正式用途(于 MFC 中)当然不是这样,这只不过是权宜之计。

在main函数中,我先产生四个对象(分别是CMyWinApp、CMyFrameWnd、CMyDoc、CMyView 对象):

然后分别取其消息映射表,一路追踪上去,把每一个消息映射表中的类记号打印出来:

下面这个函数追踪并打印消息映射表中的classid记号:

Frame7 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

Frame7 的执行结果是:

Frame7 范例程序

MFC.H

AFXMSG_.H

MFC.CPP

MY.H

MY.CPP

Command Routing(命令循环)

我们已经在上一节把整个消息流动网架设起来了。当消息进来,会有一个捕获推动它前进。消息如何进来,以及捕获函数如何推动,都是属于Windows程序设计的范畴,暂时不管。我现在要仿真出消息的流动循环路线 -- 我常喜欢称之为消息的「二万五千里长征」。

消息如果是从子类流向父类(纵向流动),那么事情再简单不过,整个 Message Map消息映射表已规划出十分明确的路线。但是正如上一节一开始我说的,MFC之中用来处理消息的C++ 类并不呈单鞭发展,作为application framework 的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是document/view,没有关系);因此,消息应该有横向流动的机会。MFC 对于消息循环的规定是:

  • 如果是一般的Windows消息(WM_xxx),一定是由派生类流向基类,没有旁流的可能。

  • 如果是命令消息WM_COMMAND,就有奇特的路线了:

图3-6 MFC对于命令消息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。

下面是消息流动的过程:

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 程序一样),以表示「到此一游」:

如果消息是 WM_COMMAND,CWnd::WindowProc 调用①OnCommand。好,注意了,这又是一个CWnd 的虚函数:

1.如果this指向CMyFrameWnd对象,那么调用的是CFrameWnd::OnCommand。

2. 如果this指向CMyView对象,那么调用的是CView::OnCommand。而因为CView并没有改写OnCommand,所以调用的其实是CWnd::OnCommand。

这次可就没有殊途同归了。

我们以第一种情况为例,再往下看:

又一次遭遇虚函数。经过前两次的分析,相信你对此很有经验了。③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:

这个函数反应出图3-6Frame窗口处理WM_COMMAND 的次序。最先调用的是④pView->OnCmdMsg,于是:

这又反应出图3-6 View窗口处理WM_COMMAND的次序。最先调用的是⑤Wnd::OnCmdMsg ,而CWnd并未改写OnCmdMsg , 所以其实就是调用CmdTarget::OnCmdMsg:

这是一个走访消息映射表的动作。注意,GetMessageMap 也是个虚函数(隐藏在DECLARE_MESSAGE_MAP 宏定义中),所以它所得到的消息映射表将是 this(以目前而言是pMyView)所指对象的映射表。于是我们得到了这个结果:

如果在映射表中找到了对应的消息,就调用对应的处理例程,然后也就结束了二万五千里长征。如果没找到,长征还没有结束,这时候退守回到CView::OnCmdMsg,调用⑥CDocument::OnCmdMsg:

于是得到这个结果:

如果在映射表中还是没找到对应消息,二万五千里长征还是未能结束,这时候退守回到CFrameWnd::OnCmdMsg ,调用⑦CWnd::OnCmdMsg(也就是CCmdTarget::OnCmdMsg),得到这个结果:

如果在映射表中还是没找到对应消息,二万五千里长征还是未能结束,再退回到CFrameWnd::OnCmdMsg,调用⑧CWinApp::OnCmdMsg(亦即CCmdTarget::OnCmdMsg),得到这个结果:

万一还是没找到对应的消息 ,二万五千里长征可也穷途末路了,退回到CWnd::WindowProc,调用 ⑨CWnd::DefWindowProc。你可以想象,在真正的MFC中这个成员函数必是调用Windows API函数::DefWindowProc。为了简化,我让它在Frame8 中是个空函数。

故事结束!

我以图 3-7 表示这二万五千里长征的调用次序(call stack),图 3-8 表示这二万五千里长征的消息流动路线。

图3-7 当CMyFrameWnd对象获得一个WM_COMMAND,

所引起的Frame8函数调用次序

图3-8 当CMyFrameWnd对象获得一个WM_COMMAND,

所引起的消息流动路线

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);

Frame8 的命令列编译链接动作是(环境变量必须先设定好,请参考第4章的「安装与设定」一节):

cl my.cpp mfc.cpp  <Enter>

以下是 Frame8 的执行结果:

Frame8 范例程序

MFC.H

AFXMSG_.H

MFC.CPP

#0001  #include "my.h"//原该含入mfc.h就好,但为了extern CMyWinApp所以...

MY.H

#0042 };

MY.CPP

本章回顾

像外科手术一样精准,我们拿起锋利的刀子,划开 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++工具的熟练啰。