« 看完了深入浅出C++第三章(MFC六大关键技术之模拟),thinking in java第六章的3/4! | Home | thinking in java 第六、七章 »
深入浅出MFC-MFC六大关键技术笔记
By robinhesky | 01月 20, 2008
上图为MFC各主要类的关系图
1.RTTI(执行时期类型识别)
以shape 为例,希望达到的技术效果如下,再程序执行期间可以识别对象的类型:
CSquare* pSquare = new CSquare;
cout << pSquare->IsKindOf(CSquare); // 应该获得1(TRUE)
cout << pSquare->IsKindOf(CRect); // 应该获得1(TRUE)
cout << pSquare->IsKindOf(CShape); // 应该获得1(TRUE)
以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)
要达到RTTI 的能力,我们(类别库的设计者)一定要在类别构造起来的时候,记录必要的信息,以建立型录。型录中的类别信息,最好以串行(linked list)方式串接起来,将来方便一一比对。我们这份「类别型录」的串行元素将以CRuntimeClass 描述之,那是一个结构。结构如下:
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” 作为它的名称),然后,经由某种手段将整个类别库构造好之后,「类别型录」能呈现类似这样的风貌:
上图的实现, 是通过以系列宏的声明实现的:
#define DECLARE_DYNAMIC(class_name) \
public: \
static CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const
但这样并没有把类型表连接起来,还要通通过下面的宏定义来实现:
#define IMPLEMENT_DYNAMIC(class_name, base_class_name) \
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL)
#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; } \
#define RUNTIME_CLASS(class_name) \
(&class_name::class##class_name)
而其中负责把各个类别连接起来的,主要靠下面这段宏程序:
struct AFX_CLASSINIT
{ AFX_CLASSINIT(CRuntimeClass* pNewClass); }
AFX_CLASSINIT::AFX_CLASSINIT(CRuntimeClass* pNewClass)
{
pNewClass->m_pNextClass = CRuntimeClass::pFirstClass;
CRuntimeClass::pFirstClass = pNewClass;
}
这样利用静态指针PFIRSTCLASS,就可以把各个类型别录表连接起来,每当创建一个CRUNTIMECLASS后,pfirstclass都指向当前结构体。
对于classcobject这个头部,因为它的基类为Null,故要特别设计
// 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 成员变量应该要初始化
CRuntimeClass* CRuntimeClass::pFirstClass = NULL
这样,整个类型别录表就建立起来了
那么类型识别就可以很容易实现:
为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
}
2.Dynamic Creatiion(动态生成)
如果能够把类别的大小记录在类别型录中,把构造函数(注意,这里并非指C++ 构造式,而是指即将出现CRuntimeClass::CreateObject)也记录在类别型录中,当程序在执行时期获得一个类别名称,它就可以在「类别型录网」中找出对应的元素,然后调用其构造函数(这里并非指C++ 构造式),产生出对象。
于是CRuntimeClass有了变化
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
}
为了适应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)
从宏的定义可以很清楚看出,拥有动态生成(Dynamic Creation)能力的类别库,
必然亦拥有执行时期类型识别(RTTI)能力,因为_DYNCREATE 宏涵盖了_DYNAMIC
宏。
现在,我们开始仿真动态生成。首先在main 函数中加上这一段码:
void main()
{
…
//Test Dynamic Creation
CRuntimeClass* pClassRef;
CObject* pOb;
while(1)
{
if ((pClassRef = CRuntimeClass::Load()) == NULL)
break;
pOb = pClassRef->CreateObject();
if (pOb != NULL)
pOb->SayHello();
}
}
CRuntimeClass::CreateObject 和 CRuntimeClass::Load 如㆘:
// in implementation file
CObject* CRuntimeClass::CreateObject()
{
if (m_pfnCreateObject == NULL)
{
TRACE1(”Error: Trying to create object which is not ”
“DECLARE_DYNCREATE \nor DECLARE_SERIAL: %hs.\n”,
m_lpszClassName);
return NULL;
}
CObject* pObject = NULL;
pObject = (*m_pfnCreateObject)();
return pObject;
}
CRuntimeClass* PASCAL CRuntimeClass::Load()
{
char szClassName[64];
CRuntimeClass* pClass;
// JJHOU : instead of Load from file, we Load from cin.
cout << “enter a class name… “;
cin >> szClassName;
for (pClass = pFirstClass; pClass != NULL; pClass = pClass->m_pNextClass)
{
if (strcmp(szClassName, pClass->m_lpszClassName) == 0)
return pClass;
}
TRACE1(”Error: Class not found: %s \n”, szClassName);
return NULL; // not found
}
3.Persistence(永续生存机制)
对象导向有一个术语:Persistence,意思就是把对象永久保留下来,一般是把对象写到文件里去。
MFC 有一套Serialize 机制,目的在于把档名的选择、文件的开关、缓冲区的建立、资
料的读写、萃取运算子(>>)和嵌入运算子(<<)的多载(overload)、对象的动态生成
…
都包装起来。
上述Serialize 的各部份工作,除了资料的读写和对象的动态生成,其余都是支节。动态
生成的技术已经解决,让我们集中火力,分析资料的读写动作。
06 00 ;CObList elements count
07 00 ;class name string length
43 53 74 72 6F 6B 65 ;”CStroke”
02 00 ;DWordArray size
28 00 13 00 ;point
28 00 13 00 ;point
0A 00 ;class name string length
43 52 65 63 74 61 6E 67 6C 65 ;”CRectangle”
11 00 22 00 33 00 44 00 ;CRect
07 00 ;class name string length
43 43 69 72 63 6C 65 ;”CCircle”
55 00 66 00 77 00 ;CPoint & radius
07 00 ;class name string length
43 53 74 72 6F 6B 65 ;”CStroke”
02 00 ;DWordArray size
28 00 35 00 ;point
28 00 35 00 ;point
0A 00 ;class name string length
43 52 65 63 74 61 6E 67 6C 65 ;”CRectangle”
11 00 22 00 33 00 44 00 ;CRect
07 00 ;class name string length
43 43 69 72 63 6C 65 ;”CCircle”
55 00 66 00 77 00 ;CPoint & radius
我们可以在每次记录对象内容的时候,先写入一个代码,表示此对象之类别是否曾在档案中记录过了。如果是新类别,乖乖地记录其类别名称;如果是旧类别,则以代码表示。这样可以节省文件大小以及程序用于解析的时间。
还有一个问题。文件的「版本」如何控制?旧版程序读取新版文件,新版程序读取旧版文件,都可能出状况。为了防弊,最好把版本号码记录上去。最好是每个类别有自己的版本号码
20 03 84 03 ;Document Size
06 00 ;CObList elements count
FF FF ;new class tag
02 00 ;schema
07 00 ;class name string length
43 53 74 72 6F 6B 65 ;”CStroke”
02 00 ;DWordArray size
28 00 13 00 ;point
28 00 13 00 ;point
FF FF ;new class tag
01 00 ;schema
0A 00 ;class name string length
43 52 65 63 74 61 6E 67 6C 65 ;”CRectangle”
11 00 22 00 33 00 44 00 ;CRect
FF FF ;new class tag
01 00 ;schema
07 00 ;class name string length
43 43 69 72 63 6C 65 ;”CCircle”
55 00 66 00 77 00 ;CPoint & radius
01 80 ;old class tag
02 00 ;DWordArray size
28 00 35 00 ;point
28 00 35 00 ;point
03 80 ;old class tag
11 00 22 00 33 00 44 00 ;CRect
05 80 ;old class tag
55 00 66 00 77 00 ;CPoint & radius
类别之能够进行文件读写动作,前提是拥有动态生成的能力,所以,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
};
// 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] = ”;
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));
}
4.Message Mapping(消息映射)
但是,MFC 之中用来处理消息的C++ 类别,并不呈单鞭发展。作为application framework
的重要架构之一的document/view,也具有处理消息的能力(你现在可能还不清楚什么是
document/view,没有关系)。因此,消息藉以攀爬的路线应该有横流的机会,如下图所示:
为了尽量降低对正常(一般)类别声明和定义的影响,我们希望,最好能够像RTTI 和
Dynamic Creation 一样,用一两个宏就完成这巨大蜘蛛网的构造。最好能够像
DECLARE_DYNAMIC 和IMPLEMENT_DYNAMIC 宏那么方便。
struct AFX_MSGMAP
{
AFX_MSGMAP* pBaseMessageMap;
AFX_MSGMAP_ENTRY* lpEntries;
};
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)
};
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 就相当于声明了这样一个数据结构:
于是,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 } \
};
enum AfxSig
{
AfxSig_end = 0, // [marks end of message map]
AfxSig_vv,
};
5.Command Routing(命令绕行)
如果是一般的Windows 消息(WM_xxx),一定是由衍生类别流向基础类别,
没有旁流的可能。
如果是命令消息WM_COMMAND,就有奇特的路线了:
书上给出了个仿真例子,要理解它,必须理解虚函数!代码就不写了。
这并是我对这一章重要的地方的笔记,有些部分还不是很懂(特别是串行化部分) 。希望在以后几章的学习中,能加深对这几个重要技术的理解。
Tags: VC | No Comments »