MFC的本质
MFC的本质就是对Win32的封装
MFC介绍
MFC(Microsoft Foundation Classes,微软基础类)是通过 WIN API 提供面向对象的精简包装,MFC6.0中大约封装了200个类,分别封装了WIN API和WIN SDK中的结构和过程;另外MFC还提供了一个应用程序框架,例如程序向导和类向导自动生成的代码,这样大大减少了程序语言的工作量,提高了开发效率。
VC6创建MFC项目
新建项目,选择MFC AppWizard:
点击OK选择Dialog Based:
点击Finish然后点击OK:
成功创建MFC项目(MFC画窗口是可视化拖动控件,直接点击右边的Controls然后在窗口中创建对应的控件即可):
看似这是一个窗口实际上,在其背后已经替我们写了很多的代码:
我们可以双击某个控件然后创建控件事件函数:
MFC与WIN32
MFC的本质就是Win32的封装,那么使用MFC实际上会更加方便、效率更高;
其缺点就是根据项目引导生成的代码繁杂冗余,对初学者来说不利用学习和驾驭。
第一个MFC程序
由于根据项目引导生成的代码繁杂冗余,所以我们可以自己来手写MFC来创建Windows窗口程序,这样就可以避免一些不必要的代码。
本节需要掌握的知识点
1、本节必须掌握的知识点:
-
CWinApp可以覆盖的虚函数 InitInstance
-
CWinApp成员变量m_pMainWnd
-
CFramWnd的成员函数create以及参数
2、需要简单了解的内容:
-
通过MSDN去看MFC的层次结构图
-
对CWinApp有个初步的认识
-
对CFramWnd有个初步的认识
MFC的层次结构图
在MSDN Library中搜索hierarchy chart即可获得MFC的层次结构图:
本章我们只需要了解CWinApp、CFramWnd
CWinApp
CWinApp类是Windows应用程序对象基类(父类)的派生类(子类),应用程序对象提供了用于初始化应用程序和运行应用程序的成员函数;使用MFC的每个应用程序只能(也必须)包含一个CWinApp类的派生类(子类)的对象;当你从CWinApp派生应用程序类时,需要覆盖InitInstance成员函数以创建应用程序的主窗口对象;它还有一个成员变量m_pMainWnd用来记录创建的主窗口的对象。
CFrameWnd
CFrameWnd类提供了Windows单文档界面(SDI)重叠或弹出框架窗口的功能,以及用于管理窗口的成员;要为应用程序创建有用的框架窗口,请从CFrameWnd派生类(子类);向派生类(子类)添加成员变量以存储特定于您的应用程序的数据;在派生类(子类)中实现消息处理程序成员函数和消息映射,以指定在将消息定向到窗口时会发生什么。
有三种方法来构造框架窗口:
1.使用Create直接构造它 (本节需要掌握的内容)
2.使用LoadFrame直接构造它(后续课程讲解)
3.使用文档模板间接构建它 (后续课程讲解)
注:我们可以认为CFrameWnd类取代了窗口过程函数。
Create 成员函数
CFrameWnd :: Create 成员函数语法格式如下:
BOOL Create(LPCTSTR lpszClassName, // 如果类名为NULL,则以MFC内建的窗口类产生一个标准的外框窗口
LPCTSTR lpszWindowName,
DWORD dwStyle = WS_OVERLAPPEDWINDOW,
const RECT& rect = rectDefault,
CWnd* pParentWnd = NULL, // != NULL for popups
LPCTSTR lpszMenuName = NULL,
DWORD dwExStyle = 0,
CCreateContext* pContext = NULL);
// 返回值: 非零表示初始化成功,否则为0
通过两个步骤构造一个CFrameWnd对象:
-
首先调用构造函数,它构造CFrameWnd类的对象,然后调用Create成员方法,创建Windows框架窗口并将其附加到CFrameWnd类的对象;
-
创建初始化窗口的类名和窗口名称,并注册其样式,父级和关联菜单的默认值。
手动编写MFC程序
基于VC6手动编写MFC程序需要注意的事项:
-
使用Win32 Application去创建项目
-
项目需要包含MFC运行库,VC6设置:Project → Setting → General → Use MFC In Static Library
-
使用头文件afxwin.h
头文件:stdafx.h
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//
#if !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_)
#define AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <afxwin.h> // 包含需要的头文件
class CMyWinApp:public CWinApp
{ // 初始化
public:
virtual BOOL InitInstance();
};
class CMainWindow:public CFrameWnd
{ // 初始化
public:
CMainWindow();
};
// TODO: reference additional headers your program requires here
//{
{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_)
源代码:HelloCode.cpp
#include "stdafx.h"
CMyWinApp theApp;
BOOL CMyWinApp::InitInstance()
{
m_pMainWnd = new CMainWindow; // 成员变量m_pMainWnd用来记录创建的主窗口的对象
m_pMainWnd -> ShowWindow(m_nCmdShow); // 展示窗口,当调用ShowWindow时,你应该把m_nCmdShow作为一个参数传给它
m_pMainWnd -> UpdateWindow(); // 更新窗口
return TRUE; // 返回值
}
CMainWindow::CMainWindow()
{
//Create成员函数创建一个框架窗口,需要注意的是这个函数前两个成员需要我们定义,但是后面的几个成员变量都有其默认参数,我们可以选择不写,第一个参数为NULL它会创建一个默认窗口类
Create(NULL, TEXT("HELLO MFC"));
}
为什么代码里没有WinMain?MFC没有WinMain函数吗?其实MFC是在内部接管了WinMain,我们可以认为CWinApp就是WinMain,只不过我们没法很直观的看见WinMain函数。
总结
-
基于MFC的窗口程序必须也只能有一个由从CWinApp派生的对象;
-
我们必须覆盖CWinApp的虚函数InitInstance在里面创建窗口,并把窗口对象保存在它的成员变量m_pMainWnd;
-
通过CFrameWnd类的派生类(子类)的对象,在它的构造函数里面调用成员函数Create来创建窗口。
课后作业
创建一个带滚动条的,300x300的窗口:
CMainWindow::CMainWindow()
{
RECT rect = {0, 0, 300, 300};
Create(NULL, TEXT("HELLO MFC"), WS_VSCROLL, rect); // Create成员函数创建一个框架窗口
}
MFC的初始化过程(一)
本章通过代码来模拟MFC的初始化过程
本节需要掌握的知识点
1、本节必须掌握的知识点
-
为什么要声明全局的应用程序对象(CWinApp)
-
学会使用类视图快速添加类
2、需要简单了解的内容
-
CWinApp的层次结构
-
CFramWnd的层次结构
代码模拟
用代码模拟MFC的初始化过程,我们基于上一章中手动编写的MFC代码来模拟。
我们继承两个类CWinApp、CFrameWnd,这两个类的层次结构如下:
所以在这里我们需要重写CObject、CCmdTarget、CWinThread、CWnd、CWinApp、CFrameWnd这几个类...
仅仅是模拟代码,不用写实际功能,写上构造、析构函数即可,这里使用VC6的类视图来创建,教程如下所示:
按照这样的层级结构我们创建了这些类,并使用上一章的代码去继承模拟实现一个MFC程序:
最后执行,我们就可以很清晰的看见执行流程了:
总结
全局对象的建构会比程序入口点更早,所以CWinApp类的对象构造函数将早于WinMain函数,而WinMain函数又广泛使用了应用程序对象,这就是为什么应该程序对象必须做全局声明的原因。
MFC的初始化过程(二)
本节需要掌握的知识点
1、本节必须掌握的知识点
-
MFC是如何使用应用程序对象
2、需要简单了解的内容
-
CWinApp类的二个可以覆盖的虚函数
-
virtual BOOL InitInstance();
-
virtual int Run();
-
代码模拟
在上一章节中我们是将InitInstance这个虚函数删除的,在本章中我们可以基于上一章节的代码重新定义一下该虚函数,在原MFC中(这里我们是模拟)这个虚函数在三个类中都存在:
我们已经了解层次结构所以直接在最高一层去定义,也就是CWinThread这个类中去定义即可:
在这我们在当前类中不想具体实现,所以使用纯虚函数去表示,而后在CMyWinApp类中去实现:
除此之外我们还有一个成员变量m_pMainWnd,这个也是在CWinThread类中定义:
同样我们需要在CMainWindow类中定义Create函数,然后做一个简单的输出即可:
因为需要完美的模拟,所以Create函数需要在构造函数中调用,InitInstance函数需要在main函数中调用(实际上是通过指针去调用的):
在这里我们一个简化的模拟代码就完成了,执行顺序如下图:
简单理解:CWinApp的Run函数就是用于消息循环的。
MFC运行时类型识别(RTTI)
什么是RTTI
MFC运行时类型识别(英文:Runtime Type Information,缩写:RTTI),能够使用基类(父类)的指针或引用来检查这些指针或引用所指的对象的实际派生类(子类),简单的意思就是它可以帮助我们在程序运行的过程中了解到某个对象所属类。
本节需要掌握的知识点
1、本节必须掌握的知识点
-
MFC为什么要自己去构建RTTI
-
关键的宏
-
DECLARE_DYNAMIC
-
IMPLEMENT_DYNAMIC
-
RUNTIME_CLASS
-
-
关键的结构体 CRuntimeClass
2、需要简单了解的内容
-
static关键字的作用
-
const关键字的作用
-
C++ RTTI typeid操作符
使用VC6中自带的RTTI
在编译器(VC6)中有自带的RTTI,我们可以在Project-Setting中选择C/C++标签按如下图选择即可:
在源代码中我们还需要引入一个头文件typeinfo.h,而后就可以使用typeid这个函数来进行动态识别,该函数只有一个传参,改参数可以为类名或已经创建的对象名。
如下图所示简单用一下typeid,我们定一个了一个类CAnimal并创建了一个对象pAnimal,使用typeid进行比较发现两者都属于同一个类:
static关键词的作用
static关键词之前课程中也有了解到,这里我们重新温故一下,当用这个关键词定义一个变量,该变量则存储在全局数据区而不是局部的,如果static关键词的变量为某类的成员,则该成员与类进行关联,但并不会与类创建的对象进行关联,也就表示我们不需要创建对象就可以使用这个成员,所以我们想要使用的话就要通过类名::成员名的方式去使用,并且我们不可以在类的内部去赋值初始化,只可以在外部。
const关键词的作用
使用const关键词定义的成员,同样没办法直接初始化,需要在初始化列表中进行初始化:
跟static不同的是,我们想要使用const关键词的成员时应创建对象后根据对象名来使用:
static、const双关键词
如果一个成员使用了static const双关键词,我们还是按照static关键词的方法去使用,但是在初始化的时候需要加上const关键词:
MFC为什么要自己去构建RTTI
在MFC出来的时候C++并没有RTTI这个概念,所以MFC自己设计了这样一套东西,其依靠的就是两个宏:DECLARE_DYNAMIC、IMPLEMENT_DYNAMIC,其中IMPLEMENT_DYNAMIC宏也包含了一个关键的宏RUNTIME_CLASS以及关键结构体CRuntime Class。
使用宏在自己的类中构建
我们要在自己的类中构建RTTI就需要使用这两个宏(注意:宏单独使用的时候,结尾不加分号):DECLARE_DYNAMIC、IMPLEMENT_DYNAMIC
首先在CWinApp类派生的CMyWinApp类中使用DECLARE_DYNAMIC这个宏(个人理解:声明这个类可以使用RTTI),其用法跟函数是一样的,传参为当前类名:
其次在WinMain函数之前使用IMPLEMENT_DYNAMIC宏(个人理解:要在当前使用RTTI,又像建立一个父类和子类的关联),其用法跟函数是一样的,传参为类名、父类名:
最后使用IsKindOf函数去判断当前是否继承某个类,其语法格式如下所示:
传递的参数使用结构体指针,我们可以通过RUNTIME_CLASS这个宏来返回该格式:
如上图所示我们通过判断当前类是否是基于CWinApp类派生的,不是则返回FALSE。
RUNTIME_CLASS
RUNTIME_CLASS这个宏就是返回处理传入的类名,返回一个CRuntimeClass的指针,其本质我们在VC6中鼠标点击按F12即可看见:
就是 → ((CRuntimeClass*)(&class_name::class##class_name)),其中两个#号则代表拼接符(一个#号则表示转为字符串),也就是说这一段代码可以转换为:
((CRuntimeClass*)(&CWinApp::classCWinApp))
所以我们可以在判断中去替换一下使用:
那这个也就很好理解了:这一段就表示返回的是CWinApp类中的classCWinApp的地址然后强转为了CRuntimeClass指针。
因此我们需要来看一下CRuntimeClass这个结构体。
CRuntimeClass结构体
CRuntimeClass结构体,中文名称叫类型记录链表结构,我们可以使用F12跟进看一下定义:
由于这里结构体的内容比较多,我们可以简化一下,整理出本章需要学到的东西:
struct CRuntimeClass
{
LPCSTR m_lpszClassName; // 类名称
int m_nObjectSize; // 类的大小
UINT m_wSchema; // 加载类的模式编号
...
CRuntimeClass* m_pBaseClass; // 父类指针
// 判断函数,判断是否父类
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
...
CRuntimeClass* m_pNextClass; // 指向下一个CRuntimeClass结构体指针
};
这个是一个链表结构体,是用于记录类的结构,其中包含了很多类的信息。
转换宏了解本质
我们可以将使用到的几个宏转换为原来的代码然后看一下本质,首先是DECLARE_DYNAMIC:
#define DECLARE_DYNAMIC(class_name) \
public: \
static const AFX_DATA CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
在代码中改写为:
static const CRuntimeClass classCMyWinApp; // 全局可读的变量,类型记录信息结构体CRuntimeClass
virtual CRuntimeClass* GetRuntimeClass() const; // 最后的const表示对该成员无法更改
这里既然定义了一个成员为全局可读的变量,那么就会需要在一个地方进行初始化,而初始化的地方就在IMPLEMENT_DYNAMIC宏中,我们来看下IMPLEMENT_DYNAMIC的背后是什么:
#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) \
AFX_COMDAT const AFX_DATADEF CRuntimeClass class_name::class##class_name = { \
#class_name, sizeof(class class_name), wSchema, pfnNew, \
RUNTIME_CLASS(base_class_name), NULL }; \
CRuntimeClass* class_name::GetRuntimeClass() const \
{ return RUNTIME_CLASS(class_name); } \
在代码中改写为:
const CRuntimeClass CMyWinApp::classCMyWinApp =
{
"CMyWinApp", sizeof(class CMyWinApp), 0xFFFF, NULL, // CRuntimeClass结构体,初始化类信息
((CRuntimeClass*)(&CWinApp::classCWinApp)), NULL
};
CRuntimeClass* CMyWinApp::GetRuntimeClass() const
{
return ((CRuntimeClass*)(&CMyWinApp::classCMyWinApp)); // 返回
}
整个代码转换下来,流程也清楚了,最后就是IsKindOf函数的原理了,我们可以下断点跟进:
首先是获取类的CRuntimeClass结构体指针,然后根据这个指针调用IsDerivedFrom方法,传递的参数也是一个结构体指针,继续跟进该函数:
前面的可以不用管,都是一些容错代码,进到这个while循环,我们可以很清晰的看见其会判断当前类和传递进来的类是否一样,如果一样则返回TRUE。
课后作业
通过拆分宏,让CMainWindow类也支持RTTI:
分别自写函数打印出它父类的CRuntimeClass结构体信息:
class CMyWinApp : public CWinApp
{
private:
void PrintCRuntimeClass (CRuntimeClass* className)
{
char szOutBuff[0x80];
sprintf(szOutBuff, "%s \n", className->m_lpszClassName);
OutputDebugString(szOutBuff);
CRuntimeClass* baseRuntimeClass = className->m_pBaseClass;
if (baseRuntimeClass != NULL)
{
PrintCRuntimeClass(baseRuntimeClass);
}
}
public:
virtual BOOL InitInstance();
CMyWinApp();
~CMyWinApp();
static const CRuntimeClass classCMyWinApp; // 全局可读的变量,类型记录信息结构体CRuntimeClass
virtual CRuntimeClass* GetRuntimeClass() const; // 最后的const表示对该成员无法更改
void RunIt ()
{
CRuntimeClass* thisRuntimeClass = this->GetRuntimeClass();
PrintCRuntimeClass(thisRuntimeClass);
}
};
动态创建
关于MFC的动态创建
MFC的动态创建基本和C++的new运算符创建没有区别,但是他弥补了C++语言中不让如下语句执行的缺点:
char* className = "MyClass";
CObject* obj = new className;
如上代码我们的本意就是创建一个MyClass类的对象,但是C++是无法创建的。
什么时候需要动态创建
MFC有一个永久保存机制,就是将内存中的东西写入到文件中,写入的数据可能是对象中的成员,所以我们需要根据文件中记载的信息去创建对象,才能将写入的数据读取保存。
本节需要掌握的知识点
1、本节必须掌握的知识点
-
动态创建的作用
-
二个关键的宏:
DECLARE_DYNCREATE IMPLEMENT_DYNCREATE
2、需要简单了解的内容
CRuntimeClass::CreateObject(动态创建函数)
//类型记录链表结构
struct CRuntimeClass
{
LPCSTR m_lpszClassName;// 类名称
int m_nObjectSize;// 类的大小
UINT m_wSchema; // 加载类的模式编号
CObject* (PASCAL* m_pfnCreateObject)(); // 函数指针,定义了一个函数指针m_pfnCreateObject用来存放需要支持动态创建类的CreateObject函数
m_pBaseClass; // 父类指针
CObject* CreateObject(); // 动态创建函数
// 判断函数
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const; …
CRuntimeClass* m_pNextClass; // 指向下一个CRuntimeClass
};
使用动态创建
我们可以跟进CFramWnd类、CWinApp类来看一下谁支持动态创建,也就是谁使用了相关的宏:
如上图所示我们可以很清晰的看见CFrameWnd类使用了DECLARE_DYNCREATE宏,也就表示其支持动态创建。
所以我们可以在其派生的CMainWindow类也使用动态创建,这里宏的使用和RTTI宏的使用是一样的:
</