MFC的六大机制
- 程序的初始化过程
- 运行时类型识别
- 动态创建
- 永久保存
- 消息映射
- 命令传递
运行时类型识别
MFC的运行时类型识别就是在程序运行过程中判断某个对象是否属于某个类,MFC通过为需要进行运行时类型识别的类添加一个静态CRuntimeClass类对象,其属于此类而不是属于某一特定对象,其在没有实例化对象的时候就已经存在并且可以被使用。
struct CRuntimeClass
{
//省略其他无关的成员
LPCSTR m_lpszClassName; //类的名称
int m_nObjectSize; //类的大小
CRuntimeClass* m_pBaseClass; //指向基类的CRuntimeClass对象
CRuntimeClass* m_pNextClass; //指向同类型对象的CRuntimeClass对象
};
然后提供了一个能返回类自身CRuntimeClass对象地址的函数GetRuntimeClass()
CRuntimeClass* class_name::GetRuntimeClass() const/
{ return &class_name::class##class_name;}
CObject类提供一个IsKindof虚拟函数,此函数通过从对象自己的CRuntimeClass成员对象开始往基类遍历,判断是否有CRuntimeClass成员对象等于待判断类的CRuntimeClass成员对象,有则说明此对象属于此类(也就是说此对象是此类或其派生类的实例),无则说明不属于此类。
BOOL CObject::IsKindof(const CRuntimeClass *pClass) const
{
CRuntimeClass* pClassThis=GetRuntimeClass();
while(pClassThis != NULL)
{
if(pClassThis==pClass)
return TRUE;
pClassThis=pClassThis->m_pBaseClass;
}
return FALSE;
}
例如:IsKindof()最后就会返回TRUE,因为MFC自己定义的类都实现了运行时类型识别,而所有的类都是从CObject中派生的,所以CWnd类的对象实例属于CObject类。注意RUNTIME_CLASS()宏,其直接返回对应类的CRuntimeClass对象指针。
CWnd cwnd;
cwnd.IsKindof(RUNTIME_CLASS(CObject));
一般MFC预定义的类都支持运行时类型识别,要想在我们从MFC预定义类中继承的类支持运行时类识别需要将DECLARE_DYNAMIC()写在类定义中,将IMPLEMENT_DYNAMIC()宏写在源文件中。
DECLARE_DYNAMIC()宏参数为类名称,其就是为类添加一个CRuntimeClass类对象,覆盖GetRuntimeClass()。
#define DECLARE_DYNAMIC(class_name)/
public:/
static CRuntimeClass class##class_name;/
virtual CRuntimeClass* GetRuntimeClass() const;
IMPLEMENT_DYNAMIC()宏是将CRuntimeClass对象的一些数据成员初始化,然后实现GetRuntimeClass()
#define IMPLEMENT_DYNAMIC(class_name,bass_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;}
动态创建
MFC能够在我们自己没有实例化类的情况下使用这个类的对象,实际是MFC在运行时创建这个类的对象。
struct CRuntimeClass
{
LPCSTR m_lpszClassName; //类的名称
int m_nObjectSize; //类的大小
CRuntimeClass* m_pBaseClass; //指向基类的CRuntimeClass对象
CRuntimeClass* m_pNextClass; //指向同类型链表的下一个类的CRuntimeClass对象
CObject* (PASCAL* m_pfnCreateObject)(); //函数指针,指向创建对象的函数
CObject* CreateObject(); //利用m_pfnCreateObject创建对象
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum); //遍历整个同类型链表(沿着m_pNextClass遍历),查询符合条件的类并创建对象
};
值得注意的是CRuntimeClass类的m_pNextClass成员,当我们从同一个类中派生出两个类时此成员就会排上用场。
CClass1:public CWnd
{
}
//此时同类型链表的头指针pFirstClass指向CClass1类,而此链表中只有CClass1一个类,所以pNextClass为NULL。
CClass2:public CWnd
{
}
//当又从同一类中继承另一个类后执行如下操作(前提加入IMPLEMENT_DYNCREATE宏),即链表头指向新的类,在链表中添加一个新的元素(头插法)。
pNextClass = pFirstClass;
pFirstClass = CClass2;
CObject* CreateObject()成员函数是利用m_pfnCreateObject函数指针创建的对象。
CObject* CRuntimeClass::CreateObject()
{
CObject* pObject = NULL;
pObject = (*m_pfnCreateObject)();
return pObject;
}
要想使我们的类能够动态创建,需要在类定义中使用DECLARE_DYNCREATE()宏,在源文件中使用IMPLEMENT_DYNCREATE()宏。
DECLARE_DYNCREATE()宏包含了DECLARE_DYNAMIC()宏,声明了静态的CreateObject()函数(此函数与CRuntimeClass类中的CreateObject()成员函数不一样,此函数实际就是CRuntimeClass的成员m_pfnCreateObject函数指针指向的函数)。
#define DECLARE_DYNCREATE (class_name)
DECLARE_DYNAMIC (class_name)
static CObject* PASCAL CreateObject();
IMPLEMENT_DYNCREATE宏是包含了CreateObject()函数的实现,可以看到其就是new了一个指定的类对象并将其返回。以及IMPLEMENT_RUNTIMECLASS宏是对CRuntimeClass的成员的定义和初始化。
#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, NULL)
所以我们可以得知在MFC中动态创建对象的大致过程为,以下面实例代码为例,先得到CWnd类的CRuntimeClass成员对象,然后调用CRuntimeClass类的CreateObject成员函数,此成员函数通过m_pfnCreateObject函数指针调用Cwnd类的静态成员函数CreateObject()。从而new了一个对象,实现了动态创建。
RUNTIME_CLASS(CWnd)->CreateObject();
//因为类(CWnd)的CRuntimeClass成员对象是静态的所以直接可以通过类(CWnd)得到并使用,此处调用了CRuntimeClass类的成员函数CreateObject(),因为类(CWnd)的CreateObject()函数也是静态的所以可以直接调用而不用先实例化对象。
所以要想让我们的类能够动态创建需要满足以下条件
- 定义一个不带参数的构造函数(默认构造函数)。因为我们是用CreateObject()动态创建,它只有一条语句就是return new XXX,不带任何参数。所以我们要有一个无参构造函数。
- 在类定义中加入DECLARE_DYNCREATE(),在源文件中加入IMPLEMENT_DYNCREATE()宏其还会在链表中加入一个新的元素(头插法)。
例如我们在MDI多文档和多视图程序设计时,我们在使用文档模板对象进行动态创建对象时。文档对象就会字内部调用对应类的CRuntimeClass类对象的CreateObject函数来动态创建对应的类对象(CMyDoc,CMainFrame,CMyView)。
RUNTIME_CLASS(CMyDoc)
RUNTIME_CLASS(CMainFrame)
RUNTIME_CLASS(CMyView)
永久保存
MFC的永久数据保存也称为序列化(串行化和并行化)
MFC利用档案类CArchive进行串行化和并行化,对于基本的数据类型()可以直接进行串行化和并行化。
//ar 为一个CArchive类对象
int a;
DWORD b;
ar << a << b;
ar >> a >> b;
对于一些MFC预定义的数据类MFC为其重载了<< 和 >> 运算符所以也可以直接串行化和并行化
CString string;
ar << string;
ar >> string;
对于自定义类可以通过类指针进行串行化和并行化,当然必须对自定义类必须满足一些条件才可以。
那么一个满足条件的自定义类是如何进行串行化和并行化的呢?首先说串行化。
CMyClass * pMyclass;
ar << pMyclass;
首先会先调用CArchive::WriteObject()函数,并将自定义类对象的指针作为CObject类对象的指针传递给它。接着会调用GetRuntimeClass()获得自身的CRuntimeClass对象,然后条用WriteClass()将类信息写入档案中。最后调用虚拟函数Serialize()进行类基本数据的串行化。
_AFX_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb)
{ ar.WriteObject(pOb); return ar; }
void CArchive::WriteObject(const CObject* pOb)
{
//省略无关代码
CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
WriteClass(pClassRef);
((CObject*)pOb)->Serialize(*this);
}
接着看看并行化过程。
CMyClass * pMyclass;
ar >> pMyclass;
首先其会先调用CArchive::ReadObject()函数,接着ReadObject()函数会调用ReadClass()函数读取档案中的类信息,并判断版本号是否等于对应pOb类的版本号,如果相等返回对应类的CRuntimeClass,否则返回空说明模式号不匹配。如果模式号不匹配就会判断pOb类是否包含VERSIONABLE_SCHEMA标志从而支持多版本号,如果支持则将模式编号保存到GetObjectSchema()可以检索到的地方,否则产生异常。如果一开始版本号就匹配的话,则会调用CRuntimeClass的CreateObject()进行动态创建对象。接着将调用动态创建对象的Serialize()函数进行并行化,从而将对象的成员进行初始化。
_AFX_INLINE CArchive& AFXAPI operator>>(CArchive& ar, const CObject*& pOb)
{ pOb = ar.ReadObject(NULL); return ar; }
CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
{
//省略部分代码
UINT nSchema;
DWORD obTag;
CRuntimeClass* pClassRef = ReadClass(pClassRefRequested, &nSchema, &obTag);
CObject* pOb=NULL;
if (pClassRef == NULL)
{
if (obTag > (DWORD)m_pLoadArray->GetUpperBound())
AfxThrowArchiveException(CArchiveException::badIndex, m_strFileName);
//此处将模式编号保存到GetObjectSchema()可以检索到的地方
}
else
{
pOb = pClassRef->CreateObject();
pOb->Serialize(*this);
}
}
根据上面自定义类串行化和并行化过程,我们可以得知MFC中自定义的类指针要能进行串行化和并行化需要满足以下条件。
- 直接或间接得到CObject的派生类(因为在调用WriteObject和ReadObject函数的时候都是将自定义类指针转换为CObject类指针使用)
- 在类说明中写入MFC的DECLARE_SERIAL()宏。在类实现中要写入IMPLEMENT_SERIAL()宏
- 覆盖CObject类的虚函数Serialize,并串行化/并行化派生类的数据成员。(因为最后会调用Serialize函数进行并行化或并行化)
- 如果派生类没有默认的构造函数(无参数),则应该添加一个。(因为在并行化时,其会在得到对应类的CRuntimeClass后通过调用CreateObject函数动态创建一个对象,我们知道动态创建对象的时候是通过CRuntimeClass的函数指针成员m_pfnCreateObjcet调用对应类中的静态成员函数CreateObject,而此函数是通过new XXX创建的对象,其new没有传任何参数所以需要一个无参的构造函数供其调用)
最后你可能会疑惑为什么我们自定义的对象并没有使用在定义能动态创建对象需要使用的宏,但是其却能动态创建对象呢?
实际是DECLARE_SERIAL()包含了DECLARE_DYNCREATE(),并且其将 >> 运算符进行了重载(声明为此自定义类的友元)
#define DECLARE_SERIAL(class_name)
_DECLARE_DYNCREATE(class_name)
AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
同样 IMPLEMENT_SERIAL()也是同样的道理,并且其包含了 << 运算符重载函数的实现。
#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema)
CObject* PASCAL class_name::CreateObject()
{ return new class_name; }
extern AFX_CLASSINIT _init_##class_name;
_IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema,
class_name::CreateObject, &_init_##class_name)
AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name));
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
{ pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name));
return ar; }
命令传递
命令传递实现将“命令消息”和“菜单更新命令”通过命令的形式传递到其他类中处理。MFC中的命令消息指的是WM_COMMAND(当选择菜单项,加速键,工具栏时将产生此消息),MFC菜单更新命令(MFC自定义)也可以进行命令传递。
WM_COMMAND消息的命令传递
消息经过MFC的消息处理流程先传递到对应窗口的Cwnd基类的集中消息处理函数WindowProc处,然后WindowProc调用OnWndProc,Cwnd的此函数在进行消息的处理,WM_COMMAND消息就会调用OnCommand函数。
而此函数一般CFrameWnd已经覆盖了,所以WM_COMMAND消息应该是被传送到框架窗口的消息处理函数为OnCommand()处理。
BOOL CFrameWnd::OnCommand(WPARAM wParam,LPARAM lParam)
{
//省略部分代码
return CWnd:: OnCommand(wParam,lParam);
}
我们看到CFrameWnd的OnCommand函数最后会调用CWnd::OnCommand函数,我们继续查看此函数。发现CWnd::OnCommand函数,此函数会调用虚函数OnCmdMsg,因为一般都会在CFrameWmd中覆盖OnCmdMsg函数,所以实际调用的是CFrameWnd::OnCmdMsg函数。
BOOL CWnd::OnCommand(WPARAM wParam,LPARAM lParam)
{
//省略部分代码
return OnCmdMsg(nID,nCode,NULL,NULL);
}
我们查看CFrame::OnCmdMsg函数,发现其会先让活动视图先处理,如果活动视图处理不了或不存在活动视图就交给框架窗口自己处理,如果框架窗口自己处理不了就交给应用程序对象处理。应用程序对象最后会调用消息默认处理函数::DefWindowProc()处理。
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
{
CPushRoutingFrame push(this);
// pump through current view FIRST
CView* pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// then pump through frame
if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
// last but not least, pump through app
CWinApp* pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return TRUE;
return FALSE;
}
注意活动视图在处理WM_COMMAND消息的时候如果不能处理,就会先交给文档对象处理,如果文档对象处理不了就会交给文档模板处理。如果也处理不了就会返回。所以MFC的命令传递让处理WM_COMMAND消息十分自由,可以在框架窗口,视图窗口,文档中任何一个地方处理。最后得出MFC命令传递路径如下图所示。
MFC菜单更新命令的命令传递
MFC通过ON_UPDATE_COMMAND_UI宏实现菜单的更新处理。其原理是当一个从框架窗口类(CFrameWnd)派生出来的类的窗口菜单被点击且并没有显示时其会接收到WM_INITMENUPOPUP消息,如果其没有处理WM_INITMENUPOPUP消息,那么消息就会交给CFrame::OnInitMenuPopup()处理。此函数会为所有菜单调用DoUpdate()函数来更新菜单。
DoUpdate()函数会利用CCmdTarget*指针调用OnCmdMsg虚函数,因为我们类继承了CFrameWnd类,而此类重写了虚函数OnCmdMsg(),所以会进而去调用CFrameWnd::OnCmdMsg。从而回到了WM_COMMAND消息一样的处理过程。