MFC六大关键技术 – 动态创建

原文地址 : http://blog.csdn.net/netanimals/article/details/6595467

 

动态创建就是运行时创建指定类的对象,在MFC中大量使用。如框架窗口对象、视对象,还有文档对象都需要由文档模板类对象来动态的创建。我觉得这是每个MFC的学习者很希望理解的问题。

 

初次接触MFC的时候,很容易有这样的迷惘。MFC的几大类不用我们设计也就罢了,但最疑惑的是不用我们实例化对象。本来最直观的理解就是,我们需要框架的时候,亲手写上CFrameWnd myFrame;需要视的时候,亲自打上CView myView;……

 

但MFC不给我们这个机会,致使我们错觉窗口没有实例化就弹出来了!就象画了张电视机的电路图就可以看电视一样令人难以置信。但大伙想了一下,可能会一拍脑门,认为简单不过:MFC自动帮我们完成CView myView之流的代码不就行了么!!!其实不然,写MFC程序的时候,我们几乎要对每个大类进行派生改写。换句话说,MFC并不知道我们打算怎样去改写这些类,当然也不打算全部为我们“静态”创建这些类了。即使静态了创建这些类也没有用,因为我们从来也不会直接利用这些类的实例干什么事情。我们只知道,想做什么事情就往各大类里塞,不管什么变量、方法照塞,塞完之后,我们似乎并未实例化对象,程序就可以运行!

 

要做到把自己的类交给MFC,MFC就用同一样的方法,把不同的类一一准确创建,我们要做些什么事情呢?同样地,我们要建立链表,记录各类的关键信息,在动态创建的时候找出这些信息,就象上一节RTTI那样!我们可以设计一个类:

 

struct CRuntimeClass{

 

LPCSTR m_lpszClassName; //类名指针

 

CObject* (PASCAL *m_pfnCreateObject)(); //创建对象的函数的指针

 

CRuntimeClass* m_pBaseClass; //讲RTTI时介绍过

 

CRuntimeClass* m_pNextClass; //指向链表的下一个元素(许多朋友说上一节讲RTTI时并没有用到这个指针,我原本以为这样更好理解一些,因为没有这个指针,这个链表是无法连起来,而m_pBaseClass仅仅是向基类走,在MFC的树型层次结构中m_pBaseClass是不能遍历的)

 

CObject* CreateObject(); //创建对象

 

static CRuntimeClass* PASCAL Load(); //遍历整个类型链表,返回符合动态创建的对象。

 

static CRuntimeClass* pFirstClass; //类型链表的头指针

 

};

 

一下子往结构里面塞了那么多的东西,大家可以觉得有点头晕。至于CObject* (PASCAL *m_pfnCreateObject)();,这定义函数指针的方法,大家可能有点陌生。函数指针在C++书籍里一般被定为选学章节,但MFC还是经常用到此类的函数,比如我们所熟悉的回调函数。简单地说m_pfnCreateObject即是保存了一个函数的地址,它将会创建一个对象。即是说,以后,m_pfnCreateObject指向不同的函数,我们就会创建不同类型的对象。

 

有函数指针,我们要实现一个与原定义参数及返回值都相同一个函数,在MFC中定义为:

 

static CObject* PASCAL CreateObject(){return new XXX};//XXX为类名。类名不同,我们就创建不同的对象。

 

由此,我们可以如下构造CRuntimeClass到链表:

 

CRuntimeClass classXXX={

 

类名,

 

……,

 

XXX::CreateObject(), //m_pfnCreateObject指向的函数

 

RUNTIME_CLASS(基类名) // RUNTIME_CLASS宏可以返回CRuntimeClass对象指针。

 

NULL //m_pNextClass暂时为空,最后会我们再设法让它指向旧链表表头。

 

 

};

 

这样,我们用函数指针m_pfnCreateObject(指向CreateObject函数),就随时可new新对象了。并且大家留意到,我们在设计CRuntimeClass类对时候,只有类名(和基类名)的不同(我们用XXX代替的地方),其它的地方一样,这正是我们想要的,因为我们动态创建也象RTTI那样用到两个宏,只要传入类名和基类作宏参数,就可以满足条件。

 

即是说,我们类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏和在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏来为我们加入链表,至于这两个宏怎么为我们建立一个链表,我们自己可以玩玩文字代换的游戏,在此不一一累赘。但要说明的一点就是:动态创建宏xxx_DYNCREATE包含了RTTI宏,即是说, xxx_DYNCREATE是xxx_DYNAMIC的“增强版”。

 

到此,我们有必要了解一下上节课没有明讲的m_pNextClass指针。因为MFC层次结构是树状的,并不是直线的。如果我们只有一个m_pBaseClass指针,它只会沿着基类上去,会漏掉其它分支。在动态创建时,必需要检查整个链表,看有多少个要动态创建的对象,即是说要从表头(pFirstClass)开始一直遍历到表尾(m_pNextClass=NULL),不能漏掉一个CRuntimeClass对象。

 

所以每当有一个新的链表元素要加入链表的时候,我们要做的就是使新的链表元素成为表头,并且m_pNextClass指向原来链表的表头,即像下面那样(当然,这些不需要我们操心,是RTTI宏帮助我们完成的):

 

pNewClass->m_pNextClass=CRuntimeClass::pFirstClass;//新元素的m_pNextClass指针指向想加入的链表的表头。

 

CRuntimeClass::pFirstClass=pNewClass;//链表的头指针指向刚插入的新元素。

 

好了,有了上面的链表,我们就可以分析动态创建了。

 

有一了张有类名,函数指针,动态创建函数的链表,我们就可以知道应该按什么步骤去动态创建了:1、获得一要动态创建的类的类名(假设为A)。2、将A跟链表里面每个元素的m_lpszClassName指向的类名作比较。3、若找到跟A相同的类名就返回A所属的CRuntimeClass元素的指针。4、判断m_pfnCreateObject是否有指向创建函数,有则创建对象,并返回该对象。代码演示如下(以下两个函数都是CRuntimeClass类函数):

 

///////////////以下为根据类名从表头向表尾查找所属的CRuntimeClass对象////////////

 

CRuntimeClass* PASCAL CRuntimeClass::Load()

 

{

 

char szClassXXX[64];

 

CRuntimeClass* pClass;

 

cin>>szClassXXX; //假定这是我们希望动态创建的类名

 

for(pClass=pFirstClass;pClass!=NULL;pClass=pClass->m_pNextClass)

 

{

 

if(strcmp(szClassXXX,pClass->m_lpszClassName)==0)

 

return pClass;

 

}

 

return NULL

 

}

 

///////////根据CRuntimeClass创建对象///////////

 

CObject* CRuntimeClass::CreateObject()

 

{

 

if(m_pfnCreateObject==NULL) return NULL;

 

CObject *pObject;

 

pObject=(* m_pfnCreateObject)(); //函数指针调用

 

return pObject;

 

}

 

有了上面两个函数,我们在程序执行的时候调用,就可以动态创建对象了。

 

我们还可以更简单地实现动态创建,大家注意到,就是在我们的程序类里面有一个RUNTIME_CLASS(class_name)宏,这个宏在MFC里定义为:

 

RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))

 

作用就是得到类的RunTime信息,即返回class_name所属CRuntimeClass的对象。在我们的应用程序员类(CMyWinApp)的InitInstance()函数下面的CSingleDocTemplate函数中,有:

 

RUNTIME_CLASS(CMyDoc),

 

RUNTIME_CLASS(CMainFrame), // main SDI frame window

 

RUNTIME_CLASS(CMyView)

 

构造文档模板的时候就用这个宏得到文档、框架和视的RunTime信息。有了RunTime信息,我们只要一条语句就可以动态创建了,如:

 

classMyView->CreateObject(); //对象直接调用用CRuntimeClass本身的CreateObject()

现在,细心的朋友已经能清楚动态创建需要的步骤:

 

1、定义一个不带参数的构造函数(默认构造函数);因为我们是用CreateObject()动态创建,它只有一条语句就是return new XXX,不带任何参数。所以我们要有一个无参构造函数。

 

2、类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;和在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;这个宏完成构造CRuntimeClass对象,并加入到链表中。

 

3、使用时先通过宏RUNTIME_CLASS得到类的RunTime信息,然后使用CRuntimeClass的成员函数CreateObject创建一个该类的实例。

 

4、CObject* pObject = pRuntimeClass->CreateObject();//完成动态创建。

MFC六大关键技术 – 运行时类型识别(RTTI)

原文地址 : http://blog.csdn.net/netanimals/article/details/6595467

 

运行时类型识别(RTTI)即是程序执行过程中知道某个对象属于某个类,我们平时用C++编程接触的RTTI一般是编译器的RTTI,即是在新版本的VC++编译器里面选用“使能RTTI”,然后载入typeinfo.h文件,就可以使用一个叫typeid的运算子,它的地位与在C++编程中的sizeof运算子类似的地方(包含一个头文件,然后就有一个熟悉好用的函数)。typdid关键的地方是可以接受两个类型的参数:一个是类名称,一个是对象指针。所以我们判别一个对象是否属于某个类就可以象下面那样:

if (typeid (ClassName)== typeid(*ObjectName)){

((ClassName*)ObjectName)->Fun;

}

象上面所说的那样,一个typeid运算子就可以轻松地识别一个对象是否属于某一个类,但MFC并不是用typeid的运算子来进行动态类型识别,而是用一大堆令人费解的宏。很多学员在这里很疑惑,好象MFC在大部分地方都是故作神秘。使们大家编程时很迷惘,只知道在这里加入一组宏,又在那儿加入一个映射,而不知道我们为什么要加入这些东东。

其实,早期的MFC并没有typeid运算子,所以只能沿用一个老办法。我们甚至可以想象一下,如果MFC早期就有template(模板)的概念,可能更容易解决RTTI问题。

所以,我们要回到“古老”的年代,想象一下,要完成RTTI要做些什么事情。就好像我们在一个新型(新型到我们还不认识)电器公司里面,我们要识别哪个是电饭锅,哪个是电磁炉等等,我们要查看登记的各电器一系列的信息,我们才可以比较、鉴别,那个东西是什么!

要登记一系列的消息并不是一件简单的事情,大家可能首先想到用数组登记对象。但如果用数组,我们要定义多大的数组才好呢,大了浪费空间,小了更加不行。所以我们要用另一种数据结构——链表。因为链表理论上可大可小,可以无限扩展。

链表是一种常用的数据结构,简单地说,它是在一个对象里面保存了指向下一个同类型对象的指针。我们大体可以这样设计我们的类:

struct CRuntimeClass

{

……类的名称等一切信息……

CRuntimeClass * m_pNextClass;//指向链表中下一CRuntimeClass对象的指针

};

链表还应该有一个表头和一个表尾,这样我们在查链表中各对象元素的信息的时候才知道从哪里查起,到哪儿结束。我们还要注明本身是由哪能个类派生。所以我们的链表类要这样设计:

struct CRuntimeClass

{

……类的名称等一切信息……

CRuntimeClass * m_PBaseClass;//指向所属的基类。

CRuntimeClass * m_pNextClass;//定义表尾的时候只要定义此指针为空就可以 了。

static CRuntimeClass* pFirstClass;//这里表头指针属于静态变量,因为我们知道static变量在内存中只初始化一次,就可以为各对象所用!保证了各对象只有一个表头。

};

有了CRuntimeClass结构后,我们就可以定义链表了:

static CRuntimeClass classCObject={NULL,NULL};//这里定义了一个CRuntimeClass对象,因为classCObject无基类,所以m_pBaseClass为NULL。因为目前只有一个元素(即目前没有下一元素),所以m_pNextClass为NULL(表尾)。

至于pFirstClass(表头),大家可能有点想不通,它到什么地方去了。因为我们这里并不想把classCObject作为链表表头,我们还要在前面插入很多的CRuntimeClass对象,并且因为pFirstClass为static指针,即是说它不是属于某个对象,所以我们在用它之前要先初始化:CRuntimeClass* CRuntimeClass::pFirstClass=NULL;

现在我们可以在前面插入一个CRuntimeClass对象,插入之前我得重要申明一下:如果单纯为了运行时类型识别,我们未必用到m_pNextClass指针(更多是在运行时创建时用),我们关心的是类本身和它的基类。这样,查找一个对象是否属于一个类时,主要关心的是类本身及它的基类,

CRuntimeClass classCCmdTarget={ &classCObject, NULL};

CRuntimeClass classCWnd={ &classCCmdTarget ,NULL };

CRuntimeClass classCView={ &classCWnd , NULL };

好了,上面只是仅仅为一个指针m_pBaseClass赋值(MFC中真正CRuntimeClass有多个成员变量和方法),就连接成了链表。假设我们现在已全部构造完成自己需要的CRuntimeClass对象,那么,这时候应该定义表头。即要用pFirstClass指针指向我们最后构造的CRuntimeClass对象——classCView。

CRuntimeClass::pFirstClass=&classCView;

现在链表有了,表头表尾都完善了,问题又出现了,我们应该怎样访问每一个CRuntimeClass对象?要判断一个对象属于某类,我们要从表头开始,一直向表尾查找到表尾,然后才能比较得出结果吗。肯定不是这样!

大家可以这样想一下,我们构造这个链表的目的,就是构造完之后,能够按主观地拿一个CRuntimeClass对象和链表中的元素作比较,看看其中一个对象中否属于你指定的类。这样,我们需要有一个函数,一个能返回自身类型名的函数GetRuntimeClass。

上面简单地说一下链表的过程,但单纯有这个链表是没有任何意义。回到MFC中来,我们要实现的是在每个需要有RTTI能力的类中构造一个CRuntimeClass对象,比较一个类是否属于某个对象的时候,实际上只是比较CRuntimeClass对象。

如何在各个类之中插入CRuntimeClass对象,并且指定CRuntimeClass对象的内容及CRuntimeClass对象的链接,这里起码有十行的代码才能完成。在每个需要有RTTI能力的类设计中都要重复那十多行代码是一件乏味的事情,也容易出错,所以MFC用了两个宏代替这些工作,即DECLARE_DYNAMIC(类名)和IMPLEMENT_DYNAMIC(类名,基类名)。从这两个宏我们可以看出在MFC名类中的CRuntimeClass对象构造连接只有类名及基类名的不同!

到此,可能会有朋友问:为什么要用两个宏,用一个宏不可以代换CRuntimeClass对象构造连接吗?个人认为肯定可以,因为宏只是文字代换的游戏而已。但我们在编程之中,头文件与源文件是分开的,我们要在头文件头声明变量及方法,在源文件里实具体实现。即是说我们要在头文件中声明:

public:

static CRuntimeClass classXXX  //XXX为类名

virtual CRuntime* GetRuntimeClass const;

然后在源文件里实现:

CRuntimeClass* XXX::classXXX={……};

CRuntime* GetRuntimeClass const;

{ return &XXX:: classXXX;}//这里不能直接返回&classXXX,因为static变量是类拥有而不是对象拥有。

我们一眼可以看出MFC中的DECLARE_DYNAMIC(类名)宏应该这样定义:

#define DECLARE_DYNAMIC(class_name) public: static CRuntimeClass class##class_name; virtual CRuntimeClass* GetRuntimeClass const;

其中##为连接符,可以让我们传入的类名前面加上class,否则跟原类同名,大家会知道产生什么后果。

有了上面的DECLARE_DYNAMIC(类名)宏之后,我们在头文件里写上一句

DECLARE_DYNAMIC(XXX)

宏展开后就有了我们想要的:

public:

static CRuntimeClass classXXX  //XXX为类名

virtual CRuntime* GetRuntimeClass const;

对于IMPLEMENT_DYNAMIC(类名,基类名),看来也不值得在这里代换文字了,大家知道它是知道回事,宏展开后为我们做了什么,再深究真是一点意义都没有!

有了此链表之后,就像有了一张存放各类型的网,我们可以轻而易举地RTTI。CObject有一个函数BOOL IsKindOf(const CRuntimeClass* pClass) const;,被它以下所有派生员继承。

此函数实现如下:

BOOL CObject::IsKindOf(const CRuntimeClass* pClass) const

{

CRuntimeClass* pClassThis=GetRuntimeClass;//获得自己的CRuntimeClass对象指针。

while(pClassThis!=NULL)

{

if(pClassThis==pClass) return TRUE;

pClassThis=pClassThis->m_pBaseClass;//这句最关键,指向自己基类,再回头比较,一直到尽头m_pBaseClass为NULL结束。

}

return FALSE;

}

 

说到这里,运行时类型识别(RTTI)算是完成了。

MFC六大关键技术 – MFC程序的初始化过程

原文地址 : http://blog.csdn.net/netanimals/article/details/6595467

 

我并不认为MFC减轻了程序员们的负担,MFC出现的目的虽然似乎是为了让程序员不用懂得太多就可以进行视窗编程,但本人在MFC里徘徊了很久很久(因为那时没有书本详细介绍MFC的原理),毫无收获。可能朋友们会说,怎么一定要了解MFC的具体呢,“黑箱”作业不行吗?这不是微软的初衷吗?

 

不行!!!如果这样,我宁愿永远不选择MFC!在学电脑之前,本人学习的东西大都与艺术不无关系,小学时参加过全国书画比赛获银奖。儿时的爱好就是在一张纸上随心所欲地画画!MFC“黑箱”就象一幅硕大的抽象画(抽象到你不能理解),它用铅笔勾画好线条,然后请你填颜色。

 

我们怎么能忍受“黑箱”作业?我们选择C++,就是因为它够自由,够艺术,我们可以在此放飞幻想。所以,我们要攻克MFC。

 

伟大孙老师在剖析MFC的时候虽然尽心尽力,但可能由于篇幅所限,说得并不大清楚(我相信许多学员都有这方面的感受)。在此,我突发奇想,想与大家一同分享一下著名的MFC六大关键技术。

 

从什么地方开始讲起好呢?我觉得回到最初摸索MFC的时候,从基本谈起最好。

 

因为我知道,一个走过来程序员,总是忘记了当初自己是怎么走过来的,忘记了一个学员最想知道的是什么。一个小小的问题(一两句话就可以解释的),足学以令手无寸铁的学员头大半个月,所以,我努力回忆当初是怎么让自己豁然开朗的。

 

转入正题,MFC的六大关键技术包括:

 

  •   MFC程序的初始化过程
  •   运行时类型识别(RTTI)
  •   动态创建
  •   永久保存
  •   消息映射
  •   消息传递

 

MFC程序的初始化过程

 

1、设计一个简单完整MFC程序,产生一个窗口。当然这不能让AppWizard自动为我们生成。我们可以在Win32 Application工程下面那样写:

 

 

#include <afxwin.h>

class MyApp : public CWinApp

{

public:

BOOL InitInstance() //②程序入点

{

CFrameWnd *Frame=new CFrameWnd();//构造框架

m_pMainWnd=Frame; //将m_pMainWnd设定为Frame;

Frame->Create(NULL,”最简单的窗口”);//建立框架

Frame->ShowWindow(SW_SHOW); //显示框架

return true; //返回

}

};

MyApp theApp; //①建立应用程序。

 

设定链接MFC库,运行,即可看见一个窗口。

 

从上面,大家可以看到建立一个MFC窗口很容易,只用两步:一是从CWinApp派生一个应用程序类(这里是MyApp),,然后建立应用程序对象(theApp),就可以产生一个自己需要的窗口(即需要什么样就在InitInstance()里创建就行了)。

 

整个程序,就改写一个InitInstance()函数,创建那么一个对象(theApp),就是一个完整的窗口程序。这就是“黑箱”作业的魅力!!!!

 

在我们正想为微软鼓掌的时候,我们突然觉得心里空荡荡的,我们想知道微软帮我们做了什么事情,而我们想编自己的程序时又需要做什么事情,那怕在上面几行的程序里面,我们还有不清楚的地方,比如,干嘛有一个m_pMainWnd指针变量,它从哪里来,又要到哪里去呢?想一想在DOS下编程是多么美妙的一件事呵,我们需要什么变量,就声明什么变量,需要什么样的函数,就编写什么样的函数,或者引用函数库……但是现在我们怎么办!!!

 

我们可以逆向思维一下,MFC要达到这种效果,它是怎么做的呢?首先我们要弄明白,VC不是一种语言,它就象我们学c语言的时候的一个类似记事本的编辑器(请原谅我的不贴切的比喻),所以,在VC里面我们用的是C++语言编程,C++才是根本(初学者总是以为VC是一门什么新的什么语言,一门比C++先进很多的复杂语言,汗)。说了那么多,我想用一句简单的话概括“MFC‘黑箱’就是帮助我们插入了‘C++代码’的东西”。

 

既然MFC黑箱帮我们插入了代码,那么大家想想它会帮我们插入什么样的代码呢?他会帮我们插入求解一元二次方程的代码吗?当然不会,所以它插入的实际上是每次编写窗口程序必须的,通用的代码。

 

再往下想,什么才是通用的呢?我们每次视窗编程都要写WinMain()函数,都要有注册窗口,产生窗口,消息循环,回调函数……即然每次都要的东西,就让它们从我们眼前消失,让MFC帮忙写入!

 

要知道MFC初始化过程,大家当然可以跟踪执行程序。孙老师的第三课跟踪了很长一段时间,我相信大家都有点晕头转向。本人觉得那怕你理解了MFC代码,也很容易让人找不着北,我们完全不懂的时候,在成千上万行程序的迷宫中如何能找到出口?

 

我们要换一种方法,不如就来重新编写个MFC库吧,哗!大家不要笑,小心你的大牙,我不是疯子(虽然疯子也说自己不疯)。我们要写的就是最简单的MFC类库,就是把MFC宏观上的,理论上的东西写出来。我们要用最简化的代码,简化到刚好能运行。

 

既然,我们这一节写的是MFC程序的初始化过程,上面我们还有了一个可执行的MFC程序。程序中只是用了两个MFC类,一个是CWinApp,另一个是CFrameWnd。当然,还有很多同样重要MFC类如视图类,文档类等等。但在上面的程序可以不用到,所以暂时省去了它(总之是为了简单)。

 

好,现在开始写MFC类库吧……唉,面前又有一个大难题,就是让大家背一下MFC层次结构图。天,那张鱼网怎么记得住,但既然我们要理解他,总得知道它是从那里派生出来的吧。

 

考虑到大家都很辛苦,那我们看一下上面两个类的父子关系(箭头代表派生):

 

 

CObject->CCmdTarget->CWinThread->CWinApp->自己的重写了InitInstance()的应用程序类。

CObject(同上)->CCmdTarget(同上)->CWnd->CFrameWnd

 

看到层次关系图之后,终于可以开始写MFC类库了。按照上面层次结构,我们可以写以下六个类(为了直观,省去了构造函数和析构函数)。

 

 

/////////////////////////////////////////////////////////

class CObiect{};//MFC类的基类。

class CCmdTarget : public CObject{};

————————————————

class CWinThread : public CCmdTarget{};

class CWinApp : public CWinThread{};

————————————————

class CWnd : public CCmdTarget{};

class CFrameWnd : public CWnd{};

/////////////////////////////////////////////////////////

 

大家再想一下,在上面的类里面,应该有什么?大家马上会想到,CWinApp类或者它的基类CCmdTarget里面应该有一个虚函数virtual BOOL InitInstance(),是的,因为那里是程序的入口点,初始化程序的地方,那自然少不了的。可能有些朋友会说,反正InitInstance()在派生类中一定要重载,我不在CCmdTarget或CWinApp类里定义,留待CWinApp的派生类去增加这个函数可不可以。扯到这个问题可能有点越说越远,但我想信C++的朋友对虚函数应该是没有太多的问题的。总的来说,作为程序员如果清楚知道基类的某个函数要被派生类用到,那定义为虚函数要方便很多。也有很多朋友问,C++为什么不自动把基类的所有函数定义为虚函数呢,这样可以省了很多麻烦,这样所有函数都遵照派生类有定义的函数就调用派生类的,没定义的就调用基类的,不用写virtual的麻烦多好!其实,很多面向对象的语言都这样做了。但定义一个虚函数要生成一个虚函数表,要占用系统空间,虚函数越多,表就越大,有时得不偿失!这里哆嗦几句,是因为往后要说明的消息映射中大家更加会体验到这一点,好了,就此打往。

 

上面我们自己解决了一个问题,就是在CCmdTarge写一个virtual BOOL InitInstance()。

 

大家再下想,我们还要我们MFC“隐藏”更多的东西:WinMain()函数,设计窗口类,窗口注册,消息循环,回调函数……我们马上想到封装想封装他们。大家似乎隐约地感觉到封装WinMain()不容易, 觉得WinMain()是一个特殊的函数,许多时候它代表了一个程序的起始和终结。所以在以前写程序的时候,我们写程序习惯从WinMain()的左大括写起,到右大括弧返回、结束程序。

 

我们换一个角度去想,有什么东西可以拿到WinMain()外面去做,许多初学者们,总觉得WinMain()函数天大的函数,什么函数都好象要在它里面才能真正运行。其实这样了解很片面,甚至错误。我们可以写一个这样的C++程序:

 

 

////////////////////////////////////////////////////

#include <iostream.h>

class test{

public:

test(){cout<<”请改变你对main()函数的看法!”<<endl;}

};

test test1;

/**************************/

void main(){}

////////////////////////////////////////////////////

 

在上面的程序里,入口的main()函数表面上什么也不做,但程序执行了(注:实际入口函数做了一些我们可以不了解的事情),并输出了一句话(注:全局对象比main()首先运行)。现在大家可以知道我们的WinMain()函数可以什么都不做,程序依然可以运行,但没有这个入口函数程序会报错。

 

那么WinMain()函数会放哪个类上面呢,请看下面程序:

 

 

#include <afxwin.h>

class MyApp : public CWinApp

{

public:

BOOL InitInstance() //②程序入点

{

AfxMessageBox(“程序依然可以运行!”);

return true;

}

};

 

MyApp theApp; //①建立应用程序。

 

大家可以看到,我并没有构造框架,而程序却可以运行了——弹出一个对话框(如果没有WinMain()函数程序会报错)。上面我这样写还是为了直观起见,其实我们只要写两行程序:

 

 

#include <afxwin.h>

CWinApp theApp;

//整个程序只构造一个CWinApp类对象,任可事情,程序就可以运行!

 

所以说,只要我们构造了CWinApp对象,就可以执行WinMain()函数。我们马上相信WinMain()函数是在CWinApp类或它的基类中,而不是在其他类中。其实这种看法是错误的,我们知道编写C++程序的时候,不可能让你在一个类中包含入口函数,WinMain()是由系统调用,跟我们的平时程序自身调用的函数有着本质的区别。我们可以暂时简单想象成,当CWinApp对象构造完的时候,WinMain()跟着执行。

 

现在大家明白了,大部分的“通用代码(我们想封装隐藏的东西)”都可以放到CWinApp类中,那么它又是怎样运行起来的呢?为什么构造了CWinApp类对象就“自动”执行那么多东西。

 

大家再仔细想一下,CWinApp类对象构造之后,它会“自动”执行自己的构造函数。那么我们可以把想要“自动”执行的代码放到CWinApp类的构造函数中。

 

那么CWinApp类可能打算这样设计(先不计较正确与否):

 

 

class CWinApp : public CWinThead{

public:

virtual BOOL InitInstance(); //解释过的程序的入点

CWinApp ::CWinApp(){ //构造函数

////////////////////////

WinMain(); //这个是大家一眼看出的错误

Create(); //设计、创建、更新显示窗口

Run(); //消息循环

//////////////////////

}

};

 

写完后,大家又马上感觉到似乎不对,WinMain()函数在这里好象真的一点用处都没有,并且能这样被调用吗(请允许我把手按在圣经上声明一下:WinMain()不是普通的函数,它要肩负着初始化应用程序,包括全局变量的初始化,是由系统而不是程序本身调用的,WinMain()返回之后,程序就结束了,进程撤消)。再看Create()函数,它能确定设计什么样的窗口,创建什么样的窗口吗?如果能在CWinApp的构造函数里确定的话,我们以后设计MFC程序时窗口就一个样,变得写程序变有必要。再看Run()函数,它能在WinMain()函数外面运行吗?

 

回过头来,我们可以让WinMain()函数一条语句都不包含吗?不可以,我们看一下WinMain() 函数的四个参数:

 

 

WinMain(HINSTANCE, HINSTANCE, LPSTR, int)

 

其中第一个参数指向一个实例句柄,我们在设计WNDCLASS的时候一定要指定实例句柄。我们窗口编程,肯定要设计窗口类。所以,WinMain()再简单也要这样写:

 

 

int WinMain(HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{ hInstance=hinst }

 

既然实例句柄要等到程序开始执行才能知道,那么我们用于创建窗口的Create()函数也要在WinMain()内部才能执行[因为如果等到WinMain()执行完毕后,程序结束,进程撤消,当然Create()也不可能创建窗口]

 

那么Run()(消息循环)放在那里执行好呢?众所周知,消息循环就是相同的那么几句代码,但我们也不要企图把它放在WinMain()函数之外执行。

 

所以我们在WinMain()函数里面,我们程序要象以下这样写

 

 

WinMain(……)

{

……窗口类对象执行创建窗口函数……

……程序类对象执行消息循环函数……

}

 

对于WinMain()的问题,得总结一下,我们封装的时候是不可以把它封装到CWinApp类里面,但由于WinMain()的不变性(或者说有规律可循),MFC完全有能力在我们构造CWinApp类对象的时候,帮我们完成那几行代码。

转了一个大圈,我们仿佛又回到了SDK编程的开始。但现在我们现在能清楚地知道,表面上MFC与SDK编程截然不同,但实质上MFC只是用类的形式封装了SDK函数,封装之后,我们在WinMain()函数中只需要几行代码,就可以完成一个窗口程序。我们也由此知道了应如何去封装应用程序类(CWinApp)和主框架窗口类(CFrameWnd)。下面把上开始设计这两个类。

 

为了简单起见,我们忽略这两个类的基类和派生类的编写,可能大家会认为这是一种很不负责任的做法,但本人觉得这既可减轻负担,又免了大家在各类之间穿来穿去,更好理解一些(我们在关键的地方作注明)。还有,我把全部代码写在同一个文件中,让大家看起来不用那么吃力,但这是最不提倡的写代码方法,大家不要学哦!

 

 

#include <windows.h>

HINSTANCE hInstance;

 

class CFrameWnd

{

HWND hwnd;

public:

CFrameWnd(); //也可以在这里调用Create()

virtual ~CFrameWnd();

int Create(); //类就留意这一个函数就行了!

BOOL ShowWnd();

};

class CWinApp1

{

public:

CFrameWnd* m_pMainWnd;//在真正的MFC里面

//它是CWnd指针,但这里由于不写CWnd类

//只要把它写成CFrameWnd指针

CWinApp1* m_pCurrentWinApp;//指向应用程序对象本身

CWinApp1();

virtual ~CWinApp1();

virtual BOOL InitInstance();//MFC原本是必须重载的函数,最重要的函数!!!!

virtual BOOL Run();//消息循环

};

CFrameWnd::CFrameWnd(){}

CFrameWnd::~CFrameWnd(){}

 

int CFrameWnd::Create() //封装创建窗口代码

{

WNDCLASS wndcls;

wndcls.style=0;

wndcls.cbClsExtra=0;

wndcls.cbWndExtra=0;

wndcls.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);

wndcls.hCursor=LoadCursor(NULL,IDC_CROSS);

wndcls.hIcon=LoadIcon(NULL,IDC_ARROW);

wndcls.hInstance=hInstance;

wndcls.lpfnWndProc=DefWindowProc;//默认窗口过程函数。

//大家可以想象成MFC通用的窗口过程。

wndcls.lpszClassName=”窗口类名”;

wndcls.lpszMenuName=NULL;

RegisterClass(&wndcls);

 

hwnd=CreateWindow(“窗口类名”,”窗口实例标题名”,WS_OVERLAPPEDWINDOW,0,0,600,400,NULL,NULL,hInstance,NULL);

return 0;

}

 

BOOL CFrameWnd::ShowWnd()//显示更新窗口

{

ShowWindow(hwnd,SW_SHOWNORMAL);

UpdateWindow(hwnd);

return 0;

}

 

/////////////

CWinApp1::CWinApp1()

{

m_pCurrentWinApp=this;

}

CWinApp1::~CWinApp1(){}

//以下为InitInstance()函数,MFC中要为CWinApp的派生类改写,

//这里为了方便理解,把它放在CWinApp类里面完成!

//你只要记住真正的MFC在派生类改写此函数就行了。

BOOL CWinApp1::InitInstance()

{

m_pMainWnd=new CFrameWnd;

m_pMainWnd->Create();

m_pMainWnd->ShowWnd();

return 0;

}

 

BOOL CWinApp1::Run()//////////////////////封装消息循环

{

MSG msg;

while(GetMessage(&msg,NULL,0,0))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

return 0;

} //////////////////////////////////////////////////////封装消息循环

 

CWinApp1 theApp; //应用程序对象(全局)

 

int WINAPI WinMain( HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

{

hInstance=hinst;

CWinApp1* pApp=theApp.m_pCurrentWinApp;

//真正的MFC要写一个全局函数AfxGetApp,以获取CWinApp指针。

pApp->InitInstance();

pApp->Run();

return 0;

}

 

代码那么长,实际上只是写了三个函数,一是CFrameWnd类的Create(),第二个是CWinApp类的InitInstance()和Run()。在此特别要说明的是InitInstance(),真正的MFC中,那是我们跟据自己构造窗口的需要,自己改写这个函数。

 

大家可以看到,封装了上面两个类以后,在入口函数WinMain中就写几行代码,就可以产生一个窗口程序。在MFC中,因为WinMain函数就是固定的那么几行代码,所以MFC绝对可以帮我们自动完成(MFC的特长就是帮我们完成有规律的代码),所以我们创造MFC应用程序的时候,看不到WinMain函数。

使用MFC实现打印功能

使用MFC实现打印功能

如果程序不是文档视图结构的,我们要使用MFC来进行打印,可以通过建立一个虚拟的文档视图结构来进行打印。MFC的打印的强大功能是在CView里提供的,而CView类的对象是一个子窗口,它必须是某一个框架窗口的子窗口,而在对话框程序中,我们只需要打印,而不需要显示这个框架窗口和视图。我们以按下按钮”打印”来执行打印程序,例如按钮为ID为IDC_PNT,消息相应函数为OnPnt(),即:

ON_BN_CLICKED(IDC_PNT, OnPnt);

  需要在OnPnt中建立一个框架窗口,同时使某个CView类的对象为该窗口的子窗口。因此需要建立两个类,一个为框架窗口类CPrintFrame,另一个为CPrintView。在新建一个用于打印的虚拟框架窗口时,需要将执行打印的对话框的指针传给框架窗口,这以便在对话框来响应WM_BEGIN_PRINTING和WM_END_PRINTING消息,使对话框可以完成打印的初始化和释放操作。在执行一个打印任务时,将打印的流程交给CView来进行,而这个CView是虚拟的,只是用来完成打印一些操作,其它内容则完全不负责处理,而当要执行CView::OnPrint时,则又将处理的具体内容传回到对话框,而对话框则只需要响应WM_MY_PRINT消息即可:

pFrame->m_pCallerDlg->SendMessage(WM_MY_PRINT,(WPARAM) pDC, (LPARAM) pInfo);

  使打印的具体处理又传回到对话框中,使开发人员根据具体的需要写WM_MY_PRINT的处理函数就可以实现打印,而CView::OnPrint(CDC* pDC, CPrintInfo* pInfo)的参数也从WM_MY_PRINT的消息参数传出来,在用户的对话框程序中,需要写的代码就很少,主要有以下几个步骤:

1. 建立一个CPrintFrame的对象,设该对象的指针为pFrame,并将对话框的指针传给该对象的m_pCallerDlg,即pFrame->m_pCallerDlg = this;

2. 调用对象的Create函数创建框架窗口;例如pFrame->Create(NULL,”频谱打印”,WS_OVERLAPPEDWINDOW,CRect(0,0,0,0));

3. 如果要执行打印,则调用pFrame->m_pView->OnMyPrint();

4. 如果要执行打印预览,则调用:

pFrame->m_pView->OnMyPrintPreview();

例如:

void CDlgPrintDlg::OnPrint() //执行打印功能

{

CPrintFrame *pFrame = new CPrintFrame;

pFrame->m_pCallerDlg = this;

pFrame->Create(NULL,”Curve

Print”,WS_OVERLAPPEDWINDOW,CRect(0,0,0,0));

pFrame->m_pView->OnMyPrint();

}

void CDlgPrintDlg::OnPrintPreview() //执行打印预览功能

{

CPrintFrame *pFrame = new CPrintFrame;

pFrame->m_pCallerDlg = this;

pFrame->Create(NULL,”Curve Print

Preview”,WS_OVERLAPPEDWINDOW,CRect(0,0,0,0));

pFrame->m_pView->OnMyPrintPreview();

}

5. 在对话框中响应 WM_BEGIN_PRINTING, WM_END_PRINTING,WM_MY_PRINT消息,分别完成打印的初始化、释放和具体的打印操作

  框架实现了对打印的一些底层支持,直接的打印机制是通过函数StartDoc和EndDoc()来实现的。应用程序要使用打印机时,它首先使用CreateDC或PrintDlg来获取指向打印机设备环境的一个句柄,这就使得打印机设备驱动程序库模块被加载到内存(如果还没有加载到内存的话),并进行初始化。然后,程序调用StartDoc函数,通知一个新文档开始了。StartDoc函数是由GDI模块来处理的。GDI模块调用打印机设备驱动程序中的control函数告诉打印机准备打印。

     打印一个文档的过程以StartDoc调用开始,以EndDoc调用结束。调用StartPage来开始一页,调用EndPage来结束该页。

   

   下面这段代码在对话框中实现了对打印的支持。

   /*

   GetPrinterDC 获取设备环境的句柄。

   HDC GetPrinterDC()const;

   返回值:如果成功则返回一个打印机设备环境的句柄;否则返回null.

   说明:如果CPrintDialog构造函数的参数bPrintSetupOnly是FALSE(表明显示的是Print对话框,则GetPrinterDC返回一个打印机设备环境句柄。当你使用完这个设备环境时,你必须调用Windows DeleteDC函数来删除它。)

   */

   /*

   {

     long cbSize,

     CString lpszDocName,

     CString lpszOutput

   } DOCINFO;

   对文档进行定义的一个结构。

   cbSize:结构的大小

   lpszDocName:文档的名字

   lpszOutput:输出文档的名字

   */

   /*

   StartDoc:开始新的打印作业

   CDC::StartDoc

   int StartDoc(LPDOCINFO lpDocInfo);

   返回值:如果出错,例如存储空间不足或指定端口无效,则返回-1否则返回正值。

   参数: lpDocInfo: DOCINFO结构的指针。该结构包含了文档文件和输出文件的名字。

   说明:通知设备的驱动程序开始一个新的打印作业,其后所有的StartPage和EndPage调用处于假

   脱机状态,直到EndDoc调用出现。这确保了长于一页的文档不被其它作业中断。   

   */

   /*

   EndDoc  结束由StartDoc成员函数启动的打印作业。

   CDC::EndDoc

   int EndDoc();

   返回值:如果成功,则返回值大于零或等于零,出错则返回值小于零。下面列出了一般的错误

   类型:

   SP_ERROR:一般错误。

   SP_OUTOFDISK:假脱机所需的磁盘空间不足,没有其它可用的磁盘空间。

   SP_OUTOFMEMORY:假脱机所需的内存不足。

   SP_USERABORT: 用户在打印管理中中止作业。

   

   说明:中止由StartDoc成员函数调用的打印作业。在成功完成打印作业后应立即调用。

   如果应用遇到打印错误或取消的打印操作,决不可用EndDoc或AbortDoc去中止

   操作,GDI在返回错误值之前自动中止操作。   

   */

   /*

   StartPage:通知设备的驱动程序开始新页。

   CDC::StartPage

   int StartPage

   说明:调用该成员函数使用打印机驱动程序做好准备接收数据。在StartPage和EndPage之间,

   ResetDC成员函数不起作用。

   */

   /*

   EndPage:通知打印机驱动程序打印页结束。

   CDC::EndPage

   int EndPage()

   返回值:如果成功,则返回大于或等于零的值,如果失败则返回如下错误类型:

   SP_ERROR:一般错误

   SP_APPABORT: 作业终止

   SP_USERABORT:用户在打印管理中中止作业。

   SP_OUTOFDISK:假脱机所需的磁盘空间不足。

   SP_OUTOFMEMORY:假脱机所需的内存不足。

   

   说明:通知设备已经写完一页。该成员函数通常用在打印机驱动程序开始新的一页。

   */

   /*

   AbortDoc:终止当前打印任务,擦除自上次调用StartDoc成员函数以业写入设备的任何内容。

   CDC::AbortDoc

   int AbortDoc();

   返回值:如果成功,则返回大于或等于零的值,如果出现错误,则为负值。与EndPage和EndDoc返回的错误类型值一样。

   说明:终止当前打印任务,并擦除自上次StartDoc以后写入设备的任何任务。

   */

   

   /*

   CPrintInfo没有基类。

   CPrintInfo存储有关一次打印或打印预览的信息。每次选择Print或PrintPreview命令,框架就会创建一个CPrintInfo对象。并在命令完成时删除此对象。

   

   CPrintInfo包含打印时的一般信息,例如:要打印页的范围,打印机的状态,当前正在打印的页这些信息存放在CPintInfo的对象中;此对象还包括在CPrint对话框中输入的值。

   

   在打印期间,一个CPrintInfo对象在框架和视图类之间传递,并且用于两者之间交换信息。例如:

   框架通过对CPrintInfo类的m_nCurPgae成员赋值,来通知视图类要打印文档的哪一页,视图类检索此值,并执行指定页的实际打印。

     另一个例子就是文档的长度到打印的时候也不知道多少页。视图类每打印一页都要检测是否到了文档的末尾。当到达文档的末尾时,视图类将CPrintInfo的m_bContinuePrinting成员设置为FALSE,通知框架停止打印循环。

   */

   /*

   Attach:把Windows设备上下文句柄附加在CDC对象上。

   CDC::Attach

   BOOL Attach(HDC hDC);

   返回值:如果成功,返回非零值,否则为0

   参数: hDC:Windows设备上下文。

   说明:使用这个函数把hDC附加到CDC对象上。

   */

   /*

   Detach:从CDC对象中分离出Windows设备上下文。

   CDC::Detach

   HDC Detach()

   返回值:Windows设备上下文句柄。

   说明:调用该函数将m_hDC从CDC对象中分离出来。并将m_hDC与m_bAttribDC设备为NULL。

   */

   void CPrintProj::Print()

   {

    CDC dc;

    CPrintDialog printDlg(FALSE);

       //利用CPrintDialog生成打印机设备环境

    if(printDlg.DoModual() == IDCANCEL) //让用户选择打印纸张等

       return;

    dc.Attach(printDlg.GetPrinterDC());//让Handle连接到dc上.

    dc.m_bPrinting = TRUE;

    CString strTitle;

    strTitle.LoadString(AFX_IDS_APP_TITLE);

    DOCINFO di; //DOCINFO中有相关的打印信息

    ::ZeroMemory(&di,sizeof(DOCINFO));

    di.cbSize = sizeof(DOCINFO);

    di.lpszDocName = strTitle; //设置标题

    BOOL bPrintingOK = dc.StartDoc(&di); //开始打印

    CPrintInfo Info;

    Info.m_rectDraw.SetRect(0,0,dc.GetDeviceCaps(HORZRES),dc.GetDeviceCaps(VERTRES)); //设置范围.

       OnBeginPrinting(&dc,&Info); //调用你自定义的打印功能.

    fo(UINT page = Info.GetMinPage();page < Info.GetMaxPage() && bPrintOK;page++)

    {

     Info.m_nCurPage = page;

     OnPrint(&dc,&Info); //调用你的”Print page”函数

     bPrintOK = dc.EndPage() > 0; //结束页

    }

    OnEndPrinting(&dc,&Info);//结束打印.

    if(bPrintingOK)

     dc.EndDoc();

    else

     dc.AbortDoc();

    dc.Detach();

   }

   

   说明:其实在Windows环境中是设备无关的.只要有了DC,就可以使用各种GDI函数,而不需要理会是在屏幕或是在打印机上绘图.

来自百度文库

本文出自:https://wenku.baidu.com/view/41b5ab41a8956bec0975e355?fr=sogou&_wkts_=1687580026610