【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 10956|回复: 54

MFC 教程

[复制链接]
发表于 2006-12-8 09:56:30 | 显示全部楼层 |阅读模式
<><STRONG><FONT size=2>作者序<BR></FONT><FONT size=3>----------- </FONT><BR></STRONG><FONT size=2>我曾写一本关于MFC的书,分析了MFC的结构和设计(《MFC深入浅出》),华中理工大学出版。书的内容建立在对MSDN和MFC SOURCE CODE的大量分析上,该书对于希望学习MFC的人来说,是较有价值的。可惜出版社好象只在几个城市针对学生作销售,虽然学生反应不错,但是外界知道不多。 </FONT></P>
<OL>
< align=justify>
<LI><B>MFC概述</B>
<p>
<OL>
< align=justify>
<LI><B>MFC是一个编程框架</B>
<p>
< align=justify>MFC (Microsoft Foundation Class Library)中的各种类结合起来构成了一个应用程序框架,它的目的就是让程序员在此基础上来建立Windows下的应用程序,这是一种相对SDK来说更为简单的方法。因为总体上,MFC框架定义了应用程序的轮廓,并提供了用户接口的标准实现方法,程序员所要做的就是通过预定义的接口把具体应用程序特有的东西填入这个轮廓。Microsoft Visual C++提供了相应的工具来完成这个工作:AppWizard可以用来生成初步的框架文件(代码和资源等);资源编辑器用于帮助直观地设计用户接口;ClassWizard用来协助添加代码到框架文件;最后,编译,则通过类库实现了应用程序特定的逻辑。</P>
<OL>
< align=justify>
<LI><B>封装</B>
<p>
< align=justify>构成MFC框架的是MFC类库。MFC类库是C++类库。这些类或者封装了Win32应用程序编程接口,或者封装了应用程序的概念,或者封装了OLE特性,或者封装了ODBC和DAO数据访问的功能,等等,分述如下。</P>
< align=justify>(1)对Win32应用程序编程接口的封装</P>
< align=justify>用一个C++ Object来包装一个Windows Object。例如:class CWnd是一个C++ window object,它把Windows window(HWND)和Windows window有关的API函数封装在C++ window object的成员函数内,后者的成员变量m_hWnd就是前者的窗口句柄。</P>
< align=justify>(2)对应用程序概念的封装</P>
< align=justify>使用SDK编写Windows应用程序时,总要定义窗口过程,登记Windows Class,创建窗口,等等。MFC把许多类似的处理封装起来,替程序员完成这些工作。另外,MFC提出了以文档-视图为中心的编程模式,MFC类库封装了对它的支持。文档是用户操作的数据对象,视图是数据操作的窗口,用户通过它处理、查看数据。</P>
<P align=justify>(3)对COM/OLE特性的封装</P>
<P align=justify>OLE建立在COM模型之上,由于支持OLE的应用程序必须实现一系列的接口(Interface),因而相当繁琐。MFC的OLE类封装了OLE API大量的复杂工作,这些类提供了实现OLE的更高级接口。</P>
<P align=justify>(4)对ODBC功能的封装</P>
<P align=justify>以少量的能提供与ODBC之间更高级接口的C++类,封装了ODBC API的大量的复杂的工作,提供了一种数据库编程模式。</P>
<P align=justify></P>
<LI><B>继承</B>
<p>
<P align=justify>首先,MFC抽象出众多类的共同特性,设计出一些基类作为实现其他类的基础。这些类中,最重要的类是CObject和CCmdTarget。CObject是MFC的根类,绝大多数MFC类是其派生的,包括CCmdTarget。CObject 实现了一些重要的特性,包括动态类信息、动态创建、对象序列化、对程序调试的支持,等等。所有从CObject派生的类都将具备或者可以具备CObject所拥有的特性。CCmdTarget通过封装一些属性和方法,提供了消息处理的架构。MFC中,任何可以处理消息的类都从CCmdTarget派生。</P>
<P align=justify>针对每种不同的对象,MFC都设计了一组类对这些对象进行封装,每一组类都有一个基类,从基类派生出众多更具体的类。这些对象包括以下种类:窗口对象,基类是CWnd;应用程序对象,基类是CwinThread;文档对象,基类是Cdocument,等等。</P>
<P align=justify>程序员将结合自己的实际,从适当的MFC类中派生出自己的类,实现特定的功能,达到自己的编程目的。</P>
<P align=justify></P>
<LI><B>虚拟函数和动态约束</B>
<p>
<P align=justify>MFC以“C++”为基础,自然支持虚拟函数和动态约束。但是作为一个编程框架,有一个问题必须解决:如果仅仅通过虚拟函数来支持动态约束,必然导致虚拟函数表过于臃肿,消耗内存,效率低下。例如,CWnd封装 Windows窗口对象时,每一条Windows消息对应一个成员函数,这些成员函数为派生类所继承。如果这些函数都设计成虚拟函数,由于数量太多,实现起来不现实。于是,MFC建立了消息映射机制,以一种富有效率、便于使用的手段解决消息处理函数的动态约束问题。</P>
<P align=justify>这样,通过虚拟函数和消息映射,MFC类提供了丰富的编程接口。程序员继承基类的同时,把自己实现的虚拟函数和消息处理函数嵌入MFC的编程框架。MFC编程框架将在适当的时候、适当的地方来调用程序的代码。本书将充分的展示MFC调用虚拟函数和消息处理函数的内幕,让读者对MFC的编程接口有清晰的理解。</P>
<P align=justify></P>
<LI><B>MFC的宏观框架体系</B>
<p></LI></OL>
<P align=justify>如前所述,MFC实现了对应用程序概念的封装,把类、类的继承、动态约束、类的关系和相互作用等封装起来。这样封装的结果对程序员来说,是一套开发模板(或者说模式)。针对不同的应用和目的,程序员采用不同的模板。例如,SDI应用程序的模板,MDI应用程序的模板,规则DLL应用程序的模板,扩展DLL应用程序的模板,OLE/ACTIVEX应用程序的模板,等等。</P>
<P align=justify>这些模板都采用了以文档-视为中心的思想,每一个模板都包含一组特定的类。典型的MDI应用程序的构成将在下一节具体讨论。</P>
<P align=justify>为了支持对应用程序概念的封装,MFC内部必须作大量的工作。例如,为了实现消息映射机制,MFC编程框架必须要保证首先得到消息,然后按既定的方法进行处理。又如,为了实现对DLL编程的支持和多线程编程的支持,MFC内部使用了特别的处理方法,使用模块状态、线程状态等来管理一些重要信息。虽然,这些内部处理对程序员来说是透明的,但是,懂得和理解MFC内部机制有助于写出功能灵活而强大的程序。</P>
<P align=justify></P>
<P align=justify>总之,MFC封装了Win32 API,OLE API,ODBC API等底层函数的功能,并提供更高一层的接口,简化了Windows编程。同时,MFC支持对底层API的直接调用。</P>
<P align=justify>MFC提供了一个Windows应用程序开发模式,对程序的控制主要是由MFC框架完成的,而且MFC也完成了大部分的功能,预定义或实现了许多事件和消息处理,等等。框架或者由其本身处理事件,不依赖程序员的代码;或者调用程序员的代码来处理应用程序特定的事件。</P>
<P align=justify>MFC是C++类库,程序员就是通过使用、继承和扩展适当的类来实现特定的目的。例如,继承时,应用程序特定的事件由程序员的派生类来处理,不感兴趣的由基类处理。实现这种功能的基础是C++对继承的支持,对虚拟函数的支持,以及MFC实现的消息映射机制。</P>
<P align=justify></P>
<LI><B>MDI应用程序的构成</B>
<p>
<P align=justify>本节解释一个典型的MDI应用程序的构成。</P>
<P align=justify>用AppWizard产生一个MDI工程t(无OLE等支持),AppWizard创建了一系列文件,构成了一个应用程序框架。这些文件分四类:头文件(.h),实现文件(.cpp),资源文件(.rc),模块定义文件(.def),等。</P>
<OL>
<P align=justify>
<LI><B>构成应用程序的对象</B>
<p>
<P align=justify>图1-1解释了该应用程序的结构,箭头表示信息流向。</P>
<P align=justify></P><IMG src="http://www.vczx.com/tutorial/mfc/image104.gif" align=left>
<P align=justify>从CWinApp、CDocument、CView、CMDIFrameWnd、CMDIChildWnd类对应地派生出CTApp、CTDoc、CTView、CMainFrame、CChildFrame五个类,这五个类的实例分别是应用程序对象、文档对象、视对象、主框架窗口对象和文档边框窗口对象。主框架窗口包含了视窗口、工具条和状态栏。对这些类或者对象解释如下。</P>
<P align=justify>(1)应用程序</P>
<P align=justify>应用程序类派生于CWinApp。基于框架的应用程序必须有且只有一个应用程序对象,它负责应用程序的初始化、运行和结束。</P>
<P align=justify>(2)边框窗口</P>
<P align=justify>如果是SDI应用程序,从CFrameWnd类派生边框窗口类,边框窗口的客户子窗口(MDIClient)直接包含视窗口;如果是MDI应用程序,从CMDIFrameWnd类派生边框窗口类,边框窗口的客户子窗口(MDIClient)直接包含文档边框窗口。</P>
<P align=justify>如果要支持工具条、状态栏,则派生的边框窗口类还要添加CToolBar和CStatusBar类型的成员变量,以及在一个OnCreate消息处理函数中初始化这两个控制窗口。</P>
<P align=justify>边框窗口用来管理文档边框窗口、视窗口、工具条、菜单、加速键等,协调半模式状态(如上下文的帮助(SHIFT+F1模式)和打印预览)。</P>
<P align=justify>(3)文档边框窗口</P>
<P align=justify>文档边框窗口类从CMDIChildWnd类派生,MDI应用程序使用文档边框窗口来包含视窗口。</P>
<P align=justify>(4)文档</P>
<P align=justify>文档类从CDocument类派生,用来管理数据,数据的变化、存取都是通过文档实现的。视窗口通过文档对象来访问和更新数据。</P>
<P align=justify>(5)视</P>
<P align=justify>视类从CView或它的派生类派生。视和文档联系在一起,在文档和用户之间起中介作用,即视在屏幕上显示文档的内容,并把用户输入转换成对文档的操作。</P>
<P align=justify>(6)文档模板</P>
<P align=justify>文档模板类一般不需要派生。MDI应用程序使用多文档模板类CMultiDocTemplate;SDI应用程序使用单文档模板类CSingleDocTemplate。</P>
<P align=justify>应用程序通过文档模板类对象来管理上述对象(应用程序对象、文档对象、主边框窗口对象、文档边框窗口对象、视对象)的创建。</P>
<P align=justify></P>
<LI><B>构成应用程序的对象之间的关系</B>
<p><B><IMG src="http://www.vczx.com/tutorial/mfc/image105.gif" align=left> </B>
<P align=justify>这里,用图的形式可直观地表示所涉及的MFC类的继承或者派生关系,如图1-2所示意。</P>
<P align=justify>图1-2所示的类都是从CObject类派生出来的;所有处理消息的类都是从CCmdTarget类派生的。如果是多文档应用程序,文档模板使用CMultiDocTemplae,主框架窗口从CMdiFarmeWnd派生,它包含工具条、状态栏和文档框架窗口。文档框架窗口从CMdiChildWnd派生,文档框架窗口包含视,视从CView或其派生类派生。</P>
<P align=justify></P>
<LI><B>构成应用程序的文件</B>
<p></LI></OL></LI></OL></LI></OL>
<P align=justify>通过上述分析,可知AppWizard产生的MDI框架程序的内容,所定义和实现的类。下面,从文件的角度来考察AppWizard生成了哪些源码文件,这些文件的作用是什么。表1-1列出了AppWizard所生成的头文件,表1-2列出了了AppWizard所生成的实现文件及其对头文件的包含关系。</P>
<P align=justify> </P>
<P align=center>表1-1 AppWizard所生成的头文件</P>
<P align=justify></P>
<P align=left>
<TABLE cellSpacing=1 cellPadding=7 width=457 border=1>

<TR>
<TD vAlign=top width="18%">
<P>头文件 </P></TD>
<TD vAlign=top width="82%">
<P align=justify>用途 </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>stdafx.h </P></TD>
<TD vAlign=top width="82%">
<P align=justify>标准AFX头文件 </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>resource.h </P></TD>
<TD vAlign=top width="82%">
<P align=justify>定义了各种资源ID </P></TD></TR>
<TR>
<TD vAlign=top width="18%" rowSpan=2>
<P align=justify>t.h </P></TD>
<TD vAlign=top width="82%" rowSpan=2>
<P align=justify>#include "resource.h"</P>
<P align=justify>定义了从CWinApp派生的应用程序对象CTApp </P></TD></TR>
<TR></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>childfrm.h </P></TD>
<TD vAlign=top width="82%">
<P align=justify>定义了从CMDIChildWnd派生的文档框架窗口对象CTChildFrame </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>mainfrm.h </P></TD>
<TD vAlign=top width="82%">
<P align=justify>定义了从CMDIFrameWnd派生的框架窗口对象CMainFrame </P></TD></TR>
<TR>
<TD vAlign=top width="18%" height=13>
<P align=justify>tdoc.h </P></TD>
<TD vAlign=top width="82%" height=13>
<P align=justify>定义了从CDocument派生的文档对象CTDoc </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>tview.h </P></TD>
<TD vAlign=top width="82%">
<P align=justify>定义了从CView派生的视图对象CTView </P></TD></TR></TABLE>
<p>
<P> </P>
<P align=center>表1-2 AppWizard所生成的实现文件</P>
<P align=center></P>
<P align=left>
<TABLE cellSpacing=1 cellPadding=7 width=653 border=1>

<TR>
<TD vAlign=top width="21%">
<P align=justify>实现文件 </P></TD>
<TD vAlign=top width="41%">
<P align=justify>所包含的头文件 </P></TD>
<TD vAlign=top width="38%">
<P align=justify>实现的内容和功能 </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>stdafx.cpp </P></TD>
<TD vAlign=top width="41%">
<P align=justify>#include "stdafx.h" </P></TD>
<TD vAlign=top width="38%">
<P align=justify>用来产生预编译的类型信息。 </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>t.cpp </P></TD>
<TD vAlign=top width="41%">
<P align=justify># include "stdafx.h"</P>
<P align=justify># include "t.h"</P>
<P align=justify></P>
<P align=justify># include "MainFrm.h"</P>
<P align=justify># include "childfrm.h"</P>
<P align=justify>#include "tdoc.h"</P>
<P align=justify>#include "tview.h"</P>
<P align=justify></P></TD>
<TD vAlign=top width="38%">
<P align=justify>定义CTApp的实现,并定义CTApp类型的全局变量theApp。 </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>childfrm.cpp </P></TD>
<TD vAlign=top width="41%">
<P align=justify>#inlcude "stdafx.h"</P>
<P align=justify>#include "t.h"</P>
<P align=justify></P>
<P align=justify>#include “childfrm.h” </P></TD>
<TD vAlign=top width="38%">
<P align=justify>实现了类CChildFrame </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>childfrm.cpp </P></TD>
<TD vAlign=top width="41%">
<P align=justify>#inlcude "stdafx.h"</P>
<P align=justify>#include "t.h"</P>
<P align=justify></P>
<P align=justify>#include "childfrm.h" </P></TD>
<TD vAlign=top width="38%">
<P align=justify>实现了类CMainFrame </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>tdoc.cpp </P></TD>
<TD vAlign=top width="41%">
<P align=justify># include "stdafx.h"</P>
<P align=justify># include "t.h"</P>
<P align=justify></P>
<P align=justify># include "tdoc.h" </P></TD>
<TD vAlign=top width="38%">
<P align=justify>实现了类CTDoc </P></TD></TR>
<TR>
<TD vAlign=top width="21%">
<P align=justify>tview.cpp</P>
<P align=justify></P></TD>
<TD vAlign=top width="41%">
<P align=justify># include "stdafx.h"</P>
<P align=justify># include "t.h"</P>
<P align=justify></P>
<P align=justify># include "tdoc.h"</P>
<P align=justify># include "tview.h"</P>
<P align=justify></P></TD>
<TD vAlign=top width="38%">
<P align=justify>实现了类CTview</P>
<P align=justify></P></TD></TR></TABLE>
<p>
<P> </P>
<P align=center> </P>
<P align=justify>从表1-2中的包含关系一栏可以看出:</P>
<DIR>
<P align=justify>CTApp 的实现用到所有的用户定义对象,包含了他们的定义;CView 的实现用到CTdoc;其他对象的实现只涉及自己的定义;</P>
<P align=justify>当然,如果增加其他操作,引用其他对象,则要包含相应的类的定义文件。</P></DIR>
<P align=justify>对预编译头文件说明如下:</P>
<P align=justify>所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头文件(如Windows.H、Afxwin.H)预先编译,以后该工程编译时,不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。</P>
<P align=justify>预编译头文件通过编译stdafx.cpp生成,以工程名命名,由于预编译的头文件的后缀是“pch”,所以编译结果文件是projectname.pch。</P>
<P align=justify>编译器通过一个头文件stdafx.h来使用预编译头文件。stdafx.h这个头文件名是可以在project的编译设置里指定的。编译器认为,所有在指令#include "stdafx.h"前的代码都是预编译的,它跳过#include "stdafx. h"指令,使用projectname.pch编译这条指令之后的所有代码。</P>
<P align=justify>因此,所有的CPP实现文件第一条语句都是:#include "stdafx.h"。</P>
<P align=justify>另外,每一个实现文件CPP都包含了如下语句:</P>
<P align=justify>#ifdef _DEBUG</P>
<P align=justify>#undef THIS_FILE</P>
<P align=justify>static char BASED_CODE THIS_FILE[] = __FILE__;</P>
<P align=justify>#endif</P>
<P align=justify>这是表示,如果生成调试版本,要指示当前文件的名称。__FILE__是一个宏,在编译器编译过程中给它赋值为当前正在编译的文件名称。</P>
<HR>
 楼主| 发表于 2006-12-8 09:57:18 | 显示全部楼层
<OL start=2>
< align=justify>
<LI><B>MFC和Win32</B>
<p>
<OL>
< align=justify>
<LI><B>MFC Object和Windows Object的关系</B>
<p></LI></OL></LI></OL>
< align=justify>MFC中最重要的封装是对Win32 API的封装,因此,理解Windows Object和MFC Object (C++对象,一个C++类的实例)之间的关系是理解MFC的关键之一。所谓Windows Object(Windows对象)是Win32下用句柄表示的Windows操作系统对象;所谓MFC Object (MFC对象)是C++对象,是一个C++类的实例,这里(本书范围内)MFC Object是有特定含义的,指封装Windows Object的C++ Object,并非指任意的C++ Object。</P>
< align=justify>MFC Object 和Windows Object是不一样的,但两者紧密联系。以窗口对象为例:</P><IMG src="http://www.vczx.com/tutorial/mfc/image106.gif" align=left>
< align=justify>一个MFC窗口对象是一个C++ CWnd类(或派生类)的实例,是程序直接创建的。在程序执行中它随着窗口类构造函数的调用而生成,随着析构函数的调用而消失。而Windows窗口则是Windows系统的一个内部数据结构的实例,由一个“窗口句柄”标识,Windows系统创建它并给它分配系统资源。Windows窗口在MFC窗口对象创建之后,由CWnd类的Create成员函数创建,“窗口句柄”保存在窗口对象的m_hWnd成员变量中。Windows窗口可以被一个程序销毁,也可以被用户的动作销毁。MFC窗口对象和Windows窗口对象的关系如图2-1所示。其他的Windows Object和对应的MFC Object也有类似的关系。</P>
< align=justify></P>
< align=justify>下面,对MFC Object和Windows Object作一个比较。有些论断对设备描述表(MFC类是CDC,句柄是HDC)可能不适用,但具体涉及到时会指出。</P>
<OL>
< align=justify>
<LI>从数据结构上比较
<p>
< align=justify>MFC Object是相应C++类的实例,这些类是MFC或者程序员定义的;</P>
< align=justify>Windows Object是Windows系统的内部结构,通过一个句柄来引用;</P>
<P align=justify>MFC给这些类定义了一个成员变量来保存MFC Object对应的Windows Object的句柄。对于设备描述表CDC类,将保存两个HDC句柄。</P>
<P align=justify></P>
<LI>从层次上讲比较
<p>
<P align=justify>MFC Object是高层的,Windows Object是低层的;</P>
<P align=justify>MFC Object封装了Windows Object的大部分或全部功能,MFC Object的使用者不需要直接应用Windows Object的HANDLE(句柄)使用Win32 API,代替它的是引用相应的MFC Object的成员函数。</P>
<P align=justify></P>
<LI>从创建上比较
<p>
<P align=justify>MFC Object通过构造函数由程序直接创建;Windows Object由相应的SDK函数创建。</P>
<P align=justify>MFC中,使用这些MFC Object,一般分两步:</P>
<P align=justify>首先,创建一个MFC Object,或者在STACK中创建,或者在HEAP中创建,这时,MFC Object的句柄实例变量为空,或者说不是一个有效的句柄。</P>
<P align=justify>然后,调用MFC Object的成员函数创建相应的Windows Object,MFC的句柄变量存储一个有效句柄。</P>
<P align=justify>CDC(设备描述表类)的创建有所不同,在后面的2.3节会具体说明CDC及其派生类的创建和使用。</P>
<P align=justify>当然,可以在MFC Object的构造函数中创建相应的Windows对象,MFC的GDI类就是如此实现的,但从实质上讲,MFC Object的创建和Windows Object的创建是两回事。</P>
<P align=justify></P>
<LI>从转换上比较
<p>
<P align=justify>可以从一个MFC Object得到对应的Windows Object的句柄;一般使用MFC Object的成员函数GetSafeHandle得到对应的句柄。</P>
<P align=justify>可以从一个已存在的Windows Object创建一个对应的MFC Object; 一般使用MFC Object的成员函数Attach或者FromHandle来创建,前者得到一个永久性对象,后者得到的可能是一个临时对象。</P>
<P align=justify></P>
<LI>从使用范围上比较
<p>
<P align=justify>MFC Object对系统的其他进程来说是不可见、不可用的;而Windows Object一旦创建,其句柄是整个Windows系统全局的。一些句柄可以被其他进程使用。典型地,一个进程可以获得另一进程的窗口句柄,并给该窗口发送消息。</P>
<P align=justify>对同一个进程的线程来说,只可以使用本线程创建的MFC Object,不能使用其他线程的MFC Object。</P>
<P align=justify></P>
<LI>从销毁上比较
<p></LI></OL>
<P align=justify>MFC Object随着析构函数的调用而消失;但Windows Object必须由相应的Windows系统函数销毁。</P>
<P align=justify>设备描述表CDC类的对象有所不同,它对应的HDC句柄对象可能不是被销毁,而是被释放。</P>
<P align=justify>当然,可以在MFC Object的析构函数中完成Windows Object的销毁,MFC Object的GDI类等就是如此实现的,但是,应该看到:两者的销毁是不同的。</P>
<P align=justify>每类Windows Object都有对应的MFC Object,下面用表格的形式列出它们之间的对应关系,如表2-1所示:</P>
<P align=center>表2-1 MFC Object和Windows Object的对应关系</P>
<P align=left>
<TABLE cellSpacing=1 cellPadding=7 width=480 border=1>

<TR>
<TD vAlign=top width="18%">
<P align=justify>描述 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>Windows句柄 </P></TD>
<TD vAlign=top width="60%">
<P align=justify>MFC Object </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>窗口 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HWND </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CWnd and CWnd-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>设备上下文 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HDC </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CDC and CDC-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>菜单 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HMENU </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CMenu </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>笔 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HPEN </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CPen和CPen-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>刷子 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HBRUSH </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CBrush和CBrush-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>字体 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HFONT </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CFont和CFont-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>位图 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HBITMAP </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CBitmap和CBitmap-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>调色板 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HPALETTE </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CPalette和CPalette-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>区域 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HRGN </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CGdiObject类,CRgn和CRgn-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>图像列表 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>HimageLIST </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CimageList和CimageList-derived classes </P></TD></TR>
<TR>
<TD vAlign=top width="18%">
<P align=justify>套接字 </P></TD>
<TD vAlign=top width="22%">
<P align=justify>SOCKET </P></TD>
<TD vAlign=top width="60%">
<P align=justify>CSocket,CAsynSocket及其派生类 </P></TD></TR></TABLE>
<p>
<P align=justify></P>
<P align=justify> </P>
<P align=justify>
<HR width="0%" SIZE=1>
表2-1中的OBJECT分以下几类:
<p>
<P align=justify>Windows对象,</P>
<P align=justify>设备上下文对象,</P>
<P align=justify>GDI对象(BITMAP,BRUSH,FONT,PALETTE,PEN,RGN),</P>
<P align=justify>菜单,</P>
<P align=justify>图像列表,</P>
<P align=justify>网络套接字接口。</P>
<P align=justify>从广义上来看,文档对象和文件可以看作一对MFC Object和Windows Object,分别用CDocument类和文件句柄描述。</P>
 楼主| 发表于 2006-12-8 09:58:45 | 显示全部楼层
< align=justify>后续几节分别对前四类作一个简明扼要的论述。</P>
<OL>
<OL>
< align=justify>
<LI><B>Windows Object</B>
<>
< align=justify>用SDK的Win32 API编写各种Windows应用程序,有其共同的规律:首先是编写WinMain函数,编写处理消息和事件的窗口过程WndProc,在WinMain里头注册窗口(Register Window),创建窗口,然后开始应用程序的消息循环。</P>
< align=justify>MFC应用程序也不例外,因为MFC是一个建立在SDK API基础上的编程框架。对程序员来说所不同的是:一般情况下,MFC框架自动完成了Windows登记、创建等工作。</P>
< align=justify>下面,简要介绍MFC Window对Windows Window的封装。</P>
<OL>
< align=justify>
<LI><B>Windows的注册</B>
<p></LI></OL></LI></OL></OL>
< align=justify>一个应用程序在创建某个类型的窗口前,必须首先注册该“窗口类”(Windows Class)。注意,这里不是C++类的类。Register Window把窗口过程、窗口类型以及其他类型信息和要登记的窗口类关联起来。</P>
<OL>
< align=justify>
<LI>“窗口类”的数据结构
<>
<P align=justify>“窗口类”是Windows系统的数据结构,可以把它理解为Windows系统的类型定义,而Windows窗口则是相应“窗口类”的实例。Windows使用一个结构来描述“窗口类”,其定义如下:</P>
<P align=justify>typedef struct _WNDCLASSEX { </P>
<P align=justify>UINT cbSize; //该结构的字节数</P>
<P align=justify>UINT style; //窗口类的风格</P>
<P align=justify>WNDPROC lpfnWndProc; //窗口过程</P>
<P align=justify>int cbClsExtra; </P>
<P align=justify>int cbWndExtra; </P>
<P align=justify>HANDLE hInstance; //该窗口类的窗口过程所属的应用实例</P>
<P align=justify>HICON hIcon; //该窗口类所用的像标</P>
<P align=justify>HCURSOR hCursor; //该窗口类所用的光标</P>
<P align=justify>HBRUSH hbrBackground; //该窗口类所用的背景刷</P>
<P align=justify>LPCTSTR lpszMenuName; //该窗口类所用的菜单资源</P>
<P align=justify>LPCTSTR lpszClassName; //该窗口类的名称</P>
<P align=justify>HICON hIconSm; //该窗口类所用的小像标</P>
<P align=justify>} WNDCLASSEX; </P>
<P align=justify>从“窗口类”的定义可以看出,它包含了一个窗口的重要信息,如窗口风格、窗口过程、显示和绘制窗口所需要的信息,等等。关于窗口过程,将在后面消息映射等有关章节作详细论述。</P>
<P align=justify>Windows系统在初始化时,会注册(Register)一些全局的“窗口类”,例如通用控制窗口类。应用程序在创建自己的窗口时,首先必须注册自己的窗口类。在MFC环境下,有几种方法可以用来注册“窗口类”,下面分别予以讨论。</P>
<P align=justify></P>
<LI>调用AfxRegisterClass注册
<P>
<P align=justify>AfxRegisterClass函数是MFC全局函数。AfxRegisterClass的函数原型:</P>
<P align=justify>BOOL AFXAPI AfxRegisterClass(WNDCLASS *lpWndClass);</P>
<P align=justify>参数lpWndClass是指向WNDCLASS结构的指针,表示一个“窗口类”。</P>
<P align=justify>首先,AfxRegisterClass检查希望注册的“窗口类”是否已经注册,如果是则表示已注册,返回TRUE,否则,继续处理。</P>
<P align=justify>接着,调用::RegisterClass(lpWndClass)注册窗口类;</P>
<P align=justify>然后,如果当前模块是DLL模块,则把注册“窗口类”的名字加入到模块状态的域m_szUnregisterList中。该域是一个固定长度的缓冲区,依次存放模块注册的“窗口类”的名字(每个名字是以“\n\0”结尾的字符串)。之所以这样做,是为了DLL退出时能自动取消(Unregister)它注册的窗口类。至于模块状态将在后面第9章详细的讨论。</P>
<P align=justify>最后,返回TRUE表示成功注册。</P>
<P align=justify></P>
<LI>调用AfxRegisterWndClass注册
<P>
<P align=justify>AfxRegisterWndClass函数也是MFC全局函数。AfxRegisterWndClass的函数原型:</P>
<P align=justify>LPCTSTR AFXAPI AfxRegisterWndClass(UINT nClassStyle,</P>
<P align=justify>HCURSOR hCursor, HBRUSH hbrBackground, HICON hIcon)</P>
<P align=justify>参数1指定窗口类风格;</P>
<P align=justify>参数2、3、4分别指定该窗口类使用的光标、背景刷、像标的句柄,缺省值是0。</P>
<P align=justify>此函数根据窗口类属性动态地产生窗口类的名字,然后,判断是否该类已经注册,是则返回窗口类名;否则用指定窗口类的属性(窗口过程指定为缺省窗口过程),调用AfxRegisterCalss注册窗口类,返回类名。</P>
<P align=justify>动态产生的窗口类名字由以下几部分组成(包括冒号分隔符):</P>
<P align=justify>如果参数2、3、4全部为NULL,则由三部分组成。</P>
<P align=justify>“Afx”+“:”+模块实例句柄”+“:”+“窗口类风格”</P>
<P align=justify>否则,由六部分组成:</P>
<P align=justify>“Afx”+“:”+模块实例句柄+“:”+“窗口类风格”+“:”+光标句柄+“:”+背景刷句柄+“:”+像标句柄。比如:“Afx:400000:b:13de:6:32cf”。</P>
<P align=justify>该函数在MFC注册主边框或者文档边框“窗口类”时被调用。具体怎样用在5.3.3.3节会指出。</P>
<P align=justify></P>
<LI>隐含的使用MFC预定义的的窗口类
<P>
<P align=justify>MFC4.0以前的版本提供了一些预定义的窗口类,4.0以后不再预定义这些窗口类。但是,MFC仍然沿用了这些窗口类,例如:</P>
<P align=justify>用于子窗口的“AfxWnd”;</P>
<P align=justify>用于边框窗口(SDI主窗口或MDI子窗口)或视的“AfxFrameOrView”;</P>
<P align=justify>用于MDI主窗口的“AfxMDIFrame”;</P>
<P align=justify>用于标准控制条的“AfxControlBar”。</P>
<P align=justify>这些类的名字就 是“AfxWnd”、“AfxFrameOrView”、“AfxMdiFrame”、 “AfxControlBar”加上前缀和后缀(用来标识版本号或是否调试版等)。它们使用标准应用程序像标、标准文档像标、标准光标等标准资源。为了使用这些“窗口类”,MFC会在适当的时候注册这些类:或者要创建该类的窗口时,或者创建应用程序的主窗口时,等等。</P>
<P align=justify>MFC内部使用了函数</P>
<P align=justify>BOOL AFXAPI AfxEndDeferRegisterClass(short fClass)</P>
<P align=justify>来帮助注册上述原MFC版本的预定义“窗口类”。参数fClass区分了那些预定义窗口的类型。根据不同的类型,使用不同的窗口类风格、窗口类名字等填充WndClass的域,然后调用AfxRegisterClass注册窗口类。并且注册成功之后,通过模块状态的m_fRegisteredClasses记录该窗口类已经注册,这样该模块在再次需要注册这些窗口类之前可以查一下m_fRegisteredClasses,如果已经注册就不必浪费时间了。为此,MFC内部使用宏</P>
<P align=justify>AfxDeferRegisterClass(short fClass)</P>
<P align=justify>来注册“窗口类”,如果m_fRegisteredClasses记录了注册的窗口类,返回TRUE,否则,调用AfxEndDeferRegisterClass注册。</P>
<P align=justify>注册这些窗口类的例子:</P>
<P align=justify>MFC在加载边框窗口时,会自动地注册“AfxFrameOrView”窗口类。在创建视时,就会使用该“窗口类”创建视窗口。当然,如果创建视窗口时,该“窗口类”还没有注册,MFC将先注册它然后使用它创建视窗口。</P>
<P align=justify>不过,MFC并不使用”AfxMDIFrame”来创建MDI主窗口,因为在加载主窗口时一般都指定了主窗口的资源,MFC使用指定的像标注册新的MDI主窗口类(通过函数AfxRegisterWndClass完成,因此“窗口类”的名字是动态产生的)。</P>
<P align=justify>MDI子窗口类似于上述MDI主窗口的处理。</P>
<P align=justify>在MFC创建控制窗口时,如工具栏窗口,如果“AfxControlBar”类还没有注册,则注册它。注册过程很简单,就是调用::InitCommonControl加载通用控制动态连接库。</P>
<P align=justify></P>
<LI>调用::RegisterWndClass。
<P>
<P align=justify>直接调用Win32的窗口注册函数::RegisterWndClass注册“窗口类”,这样做有一个缺点:如果是DLL模块,这样注册的“窗口类”在程序退出时不会自动的被取消注册(Unregister)。所以必须记得在DLL模块退出时取消它所注册的窗口类。</P>
<P align=justify></P>
<LI>子类化
<p></LI></OL>
<P align=justify>子类化(Subclass)一个“窗口类”,可自动地得到它的“窗口类”属性。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>MFC窗口类CWnd</B>
<p></LI></OL></OL></OL>
<P align=justify>在Windows系统里,一个窗口的属性分两个地方存放:一部分放在“窗口类”里头,如上所述的在注册窗口时指定;另一部分放在Windows Object本身,如:窗口的尺寸,窗口的位置(X,Y轴),窗口的Z轴顺序,窗口的状态(ACTIVE,MINIMIZED,MAXMIZED,RESTORED…),和其他窗口的关系(父窗口,子窗口…),窗口是否可以接收键盘或鼠标消息,等等。</P>
<P align=justify>为了表达所有这些窗口的共性,MFC设计了一个窗口基类CWnd。有一点非常重要,那就是CWnd提供了一个标准而通用的MFC窗口过程,MFC下所有的窗口都使用这个窗口过程。至于通用的窗口过程却能为各个窗口实现不同的操作,那就是MFC消息映射机制的奥秘和作用了。这些,将在后面有关章节详细论述。</P>
<P align=justify>CWnd提供了一系列成员函数,或者是对Win32相关函数的封装,或者是CWnd新设计的一些函数。这些函数大致如下。</P>
<P align=justify>(1)窗口创建函数</P>
<P align=justify>这里主要讨论函数Create和CreateEx。它们封装了Win32窗口创建函数::CreateWindowEx。Create的原型如下:</P>
<P align=justify>BOOL CWnd::Create(LPCTSTR lpszClassName,</P>
<DIR>
<P align=justify>LPCTSTR lpszWindowName, DWORD dwStyle,</P>
<P align=justify>const RECT&amp; rect,</P>
<P align=justify>CWnd* pParentWnd, UINT nID,</P>
<P align=justify>CCreateContext* pContext)</P></DIR>
<P align=justify>Create是一个虚拟函数,用来创建子窗口(不能创建桌面窗口和POP UP窗口)。CWnd的基类可以覆盖该函数,例如边框窗口类等覆盖了该函数以实现边框窗口的创建,视类则使用它来创建视窗口。</P>
<P align=justify></P>
<P align=justify>Create调用了成员函数CreateEx。CWnd::CreateEx的原型如下:</P>
<P align=justify>BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,</P>
<DIR>
<P align=justify>LPCTSTR lpszWindowName, DWORD dwStyle,</P>
<P align=justify>int x, int y, int nWidth, int nHeight,</P>
<P align=justify>HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)</P></DIR>
<P align=justify>CreateEx有11个参数,它将调用::CreateWindowEx完成窗口的创建,这11个参数对应地传递给::CreateWindowEx。参数指定了窗口扩展风格、“窗口类”、窗口名、窗口大小和位置、父窗口句柄、窗口菜单和窗口创建参数。</P>
<P align=justify>CreateEx的处理流程将在后面4.4.1节讨论窗口过程时分析。</P>
<P align=justify>窗口创建时发送WM_CREATE消息,消息参数lParam指向一个CreateStruct结构的变量,该结构有11个域,其描述见后面4.4.1节对窗口过程的分析,Windows使用和CreateEx参数一样的内容填充该变量。</P>
<P align=justify>(2)窗口销毁函数</P>
<DIR>
<P align=justify>例如:</P></DIR>
<P align=justify>DestroyWindow函数 销毁窗口</P>
<P align=justify>PostNcDestroy( ),销毁窗口后调用,虚拟函数</P>
<P align=justify></P>
<P align=justify>(3)用于设定、获取、改变窗口属性的函数,例如:</P>
<P align=justify>SetWindowText(CString tiltle) 设置窗口标题</P>
<P align=justify>GetWindowText() 得到窗口标题</P>
<P align=justify>SetIcon(HICON hIcon, BOOL bBigIcon);设置窗口像标</P>
<P align=justify>GetIcon( BOOL bBigIcon ) ;得到窗口像标</P>
<P align=justify>GetDlgItem( int nID);得到窗口类指定ID的控制子窗口</P>
<P align=justify>GetDC(); 得到窗口的设备上下文</P>
<P align=justify>SetMenu(CMenu *pMenu); 设置窗口菜单</P>
<P align=justify>GetMenu();得到窗口菜单</P>
<P align=justify>…</P>
<P align=justify></P>
<P align=justify>(4)用于完成窗口动作的函数</P>
<P align=justify>用于更新窗口,滚动窗口,等等。一部分成员函数设计成或可重载(Overloaded)函数,或虚拟(Overridden)函数,或MFC消息处理函数。这些函数或者实现了一部分功能,或者仅仅是一个空函数。如:</P>
<UL>
<P align=justify>
<LI>有关消息发送的函数:
<p></LI></UL>
<P align=justify>SendMessage( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 );</P>
<P align=justify>给窗口发送发送消息,立即调用方式</P>
<P align=justify>PostMessage(( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 );</P>
<P align=justify>给窗口发送消息,放进消息队列</P>
<P align=justify>…</P>
<UL>
<P align=justify>
<LI>有关改变窗口状态的函数
<p></LI></UL>
<P align=justify>MoveWindow( LPCRECT lpRect, BOOL bRepaint = TRUE );</P>
<P align=justify>移动窗口到指定位置</P>
<P align=justify>ShowWindow(BOOL );显示窗口,使之可见或不可见</P>
<P align=justify>….</P>
<UL>
<P align=justify>
<LI>实现MFC消息处理机制的函数:
<p></LI></UL>
<P align=justify>virtual LRESULT WindowProc( UINT message, WPARAM wParam, LPARAM lParam ); 窗口过程,虚拟函数</P>
<P align=justify></P>
<P align=justify>virtual BOOL OnCommand( WPARAM wParam, LPARAM lParam );处理命令消息</P>
<P align=justify>…</P>
<UL>
<P align=justify>
<LI>消息处理函数:
<p></LI></UL>
<P align=justify>OnCreate( LPCREATESTRUCT lpCreateStruct );MFC窗口消息处理函数,窗口创建时由MFC框架调用</P>
<P align=justify>OnClose();MFC窗口消息处理函数,窗口创建时由MFC框架调用</P>
<P align=justify>…</P>
<UL>
<P align=justify>
<LI>其他功能的函数
<p></LI></UL>
<P align=justify>CWnd的导出类是类型更具体、功能更完善的窗口类,它们继承了CWnd的属性和方法,并提供了新的成员函数(消息处理函数、虚拟函数、等等)。</P>
<P align=justify>常用的窗口类及其层次关系见图1-1。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>在MFC下创建一个窗口对象</B>
<p></LI></OL></OL></OL>
<P align=justify>MFC下创建一个窗口对象分两步,首先创建MFC窗口对象,然后创建对应的Windows窗口。在内存使用上,MFC窗口对象可以在栈或者堆(使用new创建)中创建。具体表述如下:</P>
<UL>
<P align=justify>
<LI>创建MFC窗口对象。通过定义一个CWnd或其派生类的实例变量或者动态创建一个MFC窗口的实例,前者在栈空间创建一个MFC窗口对象,后者在堆空间创建一个MFC窗口对象。
<P>
<P align=justify></P>
<LI>调用相应的窗口创建函数,创建Windows窗口对象。
<p></LI></UL>
<P align=justify>例如:在前面提到的AppWizard产生的源码中,有CMainFrame(派生于CMDIFrame(SDI)或者CMDIFrameWnd(MDI))类。它有两个成员变量定义如下:</P>
<DIR>
<P align=justify>CToolBar m_wndToolBar;</P>
<P align=justify>CStatusBar m_wndStatusBar;</P></DIR>
<P align=justify>当创建CMainFrame类对象时,上面两个MFC Object也被构造。</P>
<DIR>
<P align=justify>CMainFrame还有一个成员函数</P>
<DIR>
<P align=justify>OnCreate(LPCREATESTRUCT lpCreateStruct),</P></DIR></DIR>
<P align=justify>它的实现包含如下一段代码,调用CToolBar和CStatusBar的成员函数Create来创建上述两个MFC对象对应的工具栏HWND窗口和状态栏HWND窗口:</P>
<P align=justify>int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)</P>
<P align=justify>{</P>
<P align=justify>…</P>
<DIR>
<P align=justify>if (!m_wndToolBar.Create(this) ||</P>
<DIR>
<P align=justify>!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))</P></DIR>
<P align=justify>{</P>
<DIR>
<P align=justify>TRACE0("Failed to create toolbar\n");</P>
<P align=justify>return -1; // fail to create</P>
<P align=justify>}</P>
<P align=justify></P></DIR>
<P align=justify>if (!m_wndStatusBar.Create(this) ||</P>
<DIR>
<P align=justify>!m_wndStatusBar.SetIndicators(indicators,</P>
<P align=justify>sizeof(indicators)/sizeof(UINT)))</P></DIR>
<P align=justify>{</P>
<DIR>
<P align=justify>TRACE0("Failed to create status bar\n");</P>
<P align=justify>return -1; // fail to create</P></DIR>
<P align=justify>}</P></DIR>
<P align=justify>…</P>
<DIR>
<P align=justify>}</P></DIR>
<P align=justify>关于工具栏、状态栏将在后续有关章节作详细讨论。</P>
<P align=justify>在MFC中,还提供了一种动态创建技术。动态创建的过程实际上也如上所述分两步,只不过MFC使用这个技术是由框架自动地完成整个过程的。通常框架窗口、文档框架窗口、视使用了动态创建。介于MFC的结构,CFrameWnd和CView及其派生类的实例即使不使用动态创建,也要用new在堆中分配。理由见窗口的销毁(2.2.5节)。</P>
<P align=justify>至于动态创建技术,将在下一章具体讨论。</P>
<P align=justify>在Windows窗口的创建过程中,将发送一些消息,如:</P>
<P align=justify>在创建了窗口的非客户区(Nonclient area)之后,发送消息WM_NCCREATE;</P>
<P align=justify>在创建了窗口的客户区(client area)之后,发送消息WM_CREATE;</P>
<P align=justify>窗口的窗口过程在窗口显示之前收到这两个消息。</P>
 楼主| 发表于 2006-12-8 09:59:28 | 显示全部楼层
< align=justify>如果是子窗口,在发送了上述两个消息之后,还给父窗口发送WM_PARENATNOTIFY消息。其他类或风格的窗口可能发送更多的消息,具体参见SDK开发文档。</P>
<OL>
<OL>
<OL>
< align=justify>
<LI><B>MFC窗口的使用</B>
<>
< align=justify>MFC提供了大量的窗口类,其功能和用途各异。程序员应该选择哪些类来使用,以及怎么使用他们呢?</P>
< align=justify>直接使用MFC提供的窗口类或者先从MFC窗口类派生一个新的C++类然后使用它,这些在通常情况下都不需要程序员提供窗口注册的代码。是否需要派生新的C++类,视MFC已有的窗口类是否能满足使用要求而定。派生的C++类继承了基类的特性并改变或扩展了它的功能,例如增加或者改变对消息、事件的特殊处理等。</P>
< align=justify>主要使用或继承以下一些MFC窗口类(其层次关系图见图1-1):</P>
< align=justify>框架类CFrameWnd,CMdiFrameWnd;</P>
< align=justify>文档框架CMdiChildWnd;</P>
< align=justify>视图CView和CView派生的有特殊功能的视图如:列表CListView,编辑CEditView,树形列表CTreeView,支持RTF的CRichEditView,基于对话框的视CFormView等等。</P>
< align=justify>对话框CDialog。</P>
<P align=justify>通常,都要从这些类派生应用程序的框架窗口和视窗口或者对话框。</P>
<P align=justify></P>
<P align=justify>工具条CToolBar</P>
<P align=justify>状态条CStatusBar</P>
<P align=justify>其他各类控制窗口,如列表框CList,编辑框CEdit,组合框CComboBox,按钮Cbutton等。</P>
<P align=justify>通常,直接使用这些类。</P>
<P align=justify></P>
<LI><B>在MFC下窗口的销毁</B>
<p></LI></OL></OL></OL>
<P align=justify>窗口对象使用完毕,应该销毁。在MFC下,一个窗口对象的销毁包括HWND窗口对象的销毁和MFC窗口对象的销毁。一般情况下,MFC编程框架自动地处理了这些。</P>
<P align=justify>(1)对CFrameWnd和CView的派生类</P>
<P align=justify>这些窗口的关闭导致销毁窗口的函数DestroyWindow被调用。销毁Windows窗口时,MFC框架调用的最后一个成员函数是OnNcDestroy函数,该函数负责Windows清理工作,并在最后调用虚拟成员函数PostNcDestroy。CFrameWnd和CView的PostNcDestroy调用delete this删除自身这个MFC窗口对象。</P>
<P align=justify>所以,对这些窗口,如前所述,应在堆(Heap)中分配,而且,不要对这些对象使用delete操作。</P>
<P align=justify></P>
<P align=justify>(2)对Windows Control窗口</P>
<P align=justify>在它们的析构函数中,将调用DestroyWidnow来销毁窗口。如果在栈中分配这样的窗口对象,则在超出作用范围的时候,随着析构函数的调用,MFC窗口对象和它的Windows window对象都被销毁。如果在堆(Heap)中分配,则显式调用delete操作符,导致析构函数的调用和窗口的销毁。</P>
<P align=justify>所以,这种类型的窗口应尽可能在栈中分配,避免用额外的代码来销毁窗口。如前所述的CMainFrame的成员变量m_wndStatusBar和m_wndToolBar就是这样的例子。</P>
<P align=justify>(3)对于程序员直接从CWnd派生的窗口</P>
<P align=justify>程序员可以在派生类中实现上述两种机制之一,然后,在相应的规范下使用。</P>
<P align=justify>后面章节将详细的讨论应用程序退出时关闭、清理窗口的过程。</P>
<OL>
<OL>
<P align=justify>
<LI><B>设备描述表</B>
<p>
<OL>
<P align=justify>
<LI><B>设备描述表概述</B>
<p></LI></OL></LI></OL></OL>
<P align=justify>当一个应用程序使用GDI函数时,必须先装入特定的设备驱动程序,然后为绘制窗口准备设备描述表,比如指定线的宽度和颜色、刷子的样式和颜色、字体、剪裁区域等等。不像其他Win32结构,设备描述表不能被直接访问,只能通过系列Win32函数来间接地操作。</P>
<P align=justify>如同Windows“窗口类”一样,设备描述表也是一种Windows数据结构,用来描述绘制窗口所需要的信息。它定义了一个坐标映射模式、一组GDI图形对象及其属性。这些GDI对象包括用于画线的笔,绘图、填图的刷子,位图,调色板,剪裁区域,及路径(Path)。</P>
<P align=justify>表2-2列出了设备描述表的结构和各项缺省值,表2-3列出了设备描述表的类型,表2-4显示设备描述表的类型。</P>
<P align=center>表2-2 设备描述表的结构</P>
<P align=justify>
<TABLE cellSpacing=1 cellPadding=7 width=497 border=1>

<TR>
<TD vAlign=top width="27%">
<P align=justify>属性 </P></TD>
<TD vAlign=top width="73%">
<P align=justify>缺省值 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Background color </P></TD>
<TD vAlign=top width="73%">
<P align=justify>Background color setting from Windows Control Panel (typically, white) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Background mode </P></TD>
<TD vAlign=top width="73%">
<P align=justify>OPAQUE </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Bitmap </P></TD>
<TD vAlign=top width="73%">
<P align=justify>None </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Brush </P></TD>
<TD vAlign=top width="73%">
<P align=justify>WHITE_BRUSH </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Brush origin </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(0,0) </P></TD></TR>
<TR>
<TD vAlign=top width="27%" rowSpan=3>
<P align=justify>Clipping region </P></TD>
<TD vAlign=top width="73%" rowSpan=3>
<P align=justify>Entire window or client area with the update region clipped, as appropriate. Child and pop-up windows in the client area may also be clipped </P></TD></TR>
<TR></TR>
<TR></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Palette </P></TD>
<TD vAlign=top width="73%">
<P align=justify>DEFAULT_PALETTE </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Current pen position </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(0,0) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Device origin </P></TD>
<TD vAlign=top width="73%">
<P align=justify>Upper left corner of the window or the client area </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Drawing mode </P></TD>
<TD vAlign=top width="73%">
<P align=justify>R2_COPYPEN </P></TD></TR>
<TR>
<TD vAlign=top width="27%" rowSpan=2>
<P align=justify>Font </P></TD>
<TD vAlign=top width="73%" rowSpan=2>
<P align=justify>SYSTEM_FONT (SYSTEM_FIXED_FONT for applications written to run with Windows versions 3.0 and earlier) </P></TD></TR>
<TR></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Intercharacter spacing </P></TD>
<TD vAlign=top width="73%">
<P align=justify>0 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Mapping mode </P></TD>
<TD vAlign=top width="73%">
<P align=justify>MM_TEXT </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Pen </P></TD>
<TD vAlign=top width="73%">
<P align=justify>BLACK_PEN </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Polygon-fill mode </P></TD>
<TD vAlign=top width="73%">
<P align=justify>ALTERNATE </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Stretch mode </P></TD>
<TD vAlign=top width="73%">
<P align=justify>BLACKONWHITE </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Text color </P></TD>
<TD vAlign=top width="73%">
<P align=justify>Text color setting from Control Panel (typically, black) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Viewport extent </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(1,1) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Viewport origin </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(0,0) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Window extent </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(1,1) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>Window origin </P></TD>
<TD vAlign=top width="73%">
<P align=justify>(0,0) </P></TD></TR></TABLE></P>
<P align=justify> </P>
<P align=center>表2-3 设备描述表的分类</P>
<P align=justify>
<TABLE cellSpacing=1 cellPadding=7 width=497 border=1>

<TR>
<TD vAlign=top width="17%">
<P align=justify>Display </P></TD>
<TD vAlign=top width="83%">
<P align=justify>显示设备描述表,提供对视频显示设备上的绘制操作的支持 </P></TD></TR>
<TR>
<TD vAlign=top width="17%">
<P align=justify>Printer </P></TD>
<TD vAlign=top width="83%">
<P align=justify>打印设备描述表,提供对打印机、绘图仪设备上的绘制操作的支持 </P></TD></TR>
<TR>
<TD vAlign=top width="17%">
<P align=justify>Memory </P></TD>
<TD vAlign=top width="83%">
<P align=justify>内存设备描述表,提供对位图操作的支持 </P></TD></TR>
<TR>
<TD vAlign=top width="17%">
<P align=justify>Information </P></TD>
<TD vAlign=top width="83%">
<P align=justify>信息设备描述表,提供对操作设备信息获取的支持 </P></TD></TR></TABLE></P>
<P align=justify>表2-3中的显示设备描述表又分三种类型,如表2-4所示。</P>
<P align=center>表2-4 显示设备描述表的分类</P>
<P align=left>
<TABLE cellSpacing=1 cellPadding=7 width=499 border=1>

<TR>
<TD vAlign=top width="19%">
<P align=justify>名称 </P></TD>
<TD vAlign=top width="27%">
<P align=justify>特点 </P></TD>
<TD vAlign=top width="55%">
<P align=justify>功能 </P></TD></TR>
<TR>
<TD vAlign=top width="19%">
<P align=justify>Class Device</P>
<P align=justify>Contexts </P></TD>
<TD vAlign=top width="27%">
<P align=justify>提供对Win16的向后兼容 </P></TD>
<TD vAlign=top width="55%"> </TD></TR>
<TR>
<TD vAlign=top width="19%">
<P align=justify>Common</P>
<P align=justify>Device</P>
<P align=justify>Contexts </P></TD>
<TD vAlign=top width="27%">
<P align=justify>在Windows系统的高速缓冲区,数量有限 </P></TD>
<TD vAlign=top width="55%">
<P align=justify>Applicaion获取设备描述表时,Windows用缺省值初始化该设备描述表,Application使用它完成绘制操作,然后释放 </P></TD></TR>
<TR>
<TD vAlign=top width="19%">
<P align=justify>Private</P>
<P align=justify>Device</P>
<P align=justify>Contexts </P></TD>
<TD vAlign=top width="27%">
<P align=justify>没有数量限制,用完不需释放一次获取,多次使用 </P></TD>
<TD vAlign=top width="55%">
<P align=justify>多次使用过程中,每次设备描述表属性的任何修改或变化都会被保存,以支持快速绘制 </P></TD></TR></TABLE>
<p>
<P align=justify></P>
<P align=justify>(1)使用设备描述表的步骤</P>
<P align=justify>要使用设备描述表,一般有如下步骤:</P>
<UL>
<P align=justify>
<LI>获取或者创建设备描述表;
<p>
<P align=justify></P>
<LI>必要的话,改变设备描述表的属性;
<p>
<P align=justify></P>
<LI>使用设备描述表完成绘制操作;
<p>
<P align=justify></P>
<LI>释放或删除设备描述表。
<p></LI></UL>
<P align=justify>Common设备描述表通过::GetDC,::GetDCEx,::BeginPaint来获得一个设备描述表,用毕,用::ReleaseDC或::EndPaint释放设备描述表;</P>
<P align=justify>Printer设备描述表通过::CreateDC创建设备描述表,用:eleteDC删除设备描述表。</P>
<P align=justify>Memory设备描述表通过::CreateCompatibleDC创建设备描述表,用:eleteDC删除。</P>
<P align=justify>Information设备描述表通过::CreateIC创建设备描述表,用:eleteDC删除。</P>
<P align=justify>(2)改变设备描述表属性的途径</P>
<P align=justify>要改变设备描述表的属性,可通过以下途径:</P>
<P align=justify>用::SelectObject选入新的除调色板以外的GDI Object到设备描述表中;</P>
<P align=justify>对于调色板,使用::SelectPalette函数选入逻辑调色板,并使用::RealizePalette把逻辑调色板的入口映射到物理调色板中。</P>
<P align=justify>用其他API函数改变其他属性,如::SetMapMode改变映射模式。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>设备描述表在MFC中的实现</B>
<p></LI></OL></OL></OL>
<P align=justify>MFC提供了CDC类作为设备描述表类的基类,它封装了Windows的HDC设备描述表对象和相关函数。</P>
<OL>
<P align=justify>
<LI>CDC类
<p>
<P align=justify>CDC类包含了各种类型的Windows设备描述表的全部功能,封装了所有的Win32 GDI 函数和设备描述表相关的SDK函数。在MFC下,使用CDC的成员函数来完成所有的窗口绘制工作。</P>
<P align=justify>CDC 类的结构示意图2-2所示。</P><IMG src="http://www.vczx.com/tutorial/mfc/image107.gif" align=left>
<P align=justify></P>
<P align=justify>CDC类有两个成员变量:m_hDC,m_hAttribDC,它们都是Windows设备描述表句柄。CDC的成员函数作输出操作时,使用m_Hdc;要获取设备描述表的属性时,使用m_hAttribDC。</P>
<P align=justify>在创建一个CDC类实例时,缺省的m_hDC等于m_hAttribDC。如果需要的话,程序员可以分别指定它们。例如,MFC框架实现CMetaFileDC类时,就是如此:CMetaFileDC从物理设备上读取设备信息,输出则送到元文件(metafile)上,所以m_hDC和m_hAttribDC是不同的,各司其责。还有一个类似的例子:打印预览的实现,一个代表打印机模拟输出,一个代表屏幕显示。</P>
<P align=justify>CDC封装::SelectObject(HDC hdc,HGDIOBJECT hgdiobject)函数时,采用了重载技术,即它针对不同的GDI对象,提供了名同而参数不同的成员函数:</P>
<P align=justify>SelectObject(CPen *pen)用于选入笔;</P>
<P align=justify>SelectObject(CBitmap* pBitmap)用于选入位图;</P>
<P align=justify>SelectObject(CRgn *pRgn)用于选入剪裁区域;</P>
<P align=justify>SelectObject(CBrush *pBrush)用于选入刷子;</P>
<P align=justify>SelectObject(CFont *pFont)用于选入字体;</P>
<P align=justify>至于调色板,使用SelectPalette(CPalette *pPalette,BOOL bForceBackground )选入调色板到设备描述表,使用RealizePalletter()实现逻辑调色板到物理调色板的映射。</P>
<P align=justify></P>
<LI>从CDC派生出功能更具体的设备描述表
<p></LI></OL>
<P align=justify>从CDC 派生出四个功能更具体的设备描述表类。层次如图2-3所示。</P>
<P align=justify></P>
<P align=justify><IMG src="http://www.vczx.com/tutorial/mfc/image108.gif"></P>
<P align=justify></P>
<P align=justify>下面,分别讨论派生出的四种设备描述表。</P>
<UL>
<P align=justify>
<LI>CCientDC
<p></LI></UL>
<P align=justify>代表窗口客户区的设备描述表。其构造函数CClientDC(CWnd *pWin)通过::GetDC获取指定窗口的客户区的设备描述表HDC,并且使用成员函数Attach把它和CClientDC对象捆绑在一起;其析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::ReleaseDC释放设备描述表HDC。</P>
<UL>
<P align=justify>
<LI>CPaintDC
<p></LI></UL>
<P align=justify>仅仅用于响应WM_PAINT消息时绘制窗口,因为它的构造函数调用了::BeginPaint获取设备描述表HDC,并且使用成员函数Attach把它和CPaintDC对象捆绑在一起;析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::EndPaint释放设备描述表HDC,而::BeginPaint和::EndPaint仅仅在响应WM_PAINT时使用。</P>
<UL>
<P align=justify>
<LI>CMetaFileDC
<p></LI></UL>
<P align=justify>用于生成元文件。</P>
<UL>
<P align=justify>
<LI>CWindowDC
<p></LI></UL>
<P align=justify>代表整个窗口区(包括非客户区)的设备描述表。其构造函数CWindowDC(CWnd *pWin)通过::GetWindowDC获取指定窗口的客户区的设备描述表HDC,并使用Attach把它和CWindowDC对象捆绑在一起;其析构函数使用Detach把设备描述表HDC分离出来,调用::ReleaseDC释放设备描述表HDC。</P>
 楼主| 发表于 2006-12-8 09:59:54 | 显示全部楼层
<OL>
<OL>
<OL>
< align=justify>
<LI><B>MFC设备描述表类的使用</B>
<p></LI></OL></OL></OL>
<OL>
< align=justify>
<LI>使用CPaintDC、CClientDC、CWindowDC的方法
<p>
< align=justify>首先,定义一个这些类的实例变量,通常在栈中定义。然后,使用它。</P>
< align=justify>例如,MFC中CView对WM_PAINT消息的实现方法如下:</P>
< align=justify>void CView::OnPaint()</P>
< align=justify>{</P>
< align=justify>// standard paint routine</P>
< align=justify>CPaintDC dc(this);</P>
< align=justify>OnPrepareDC(&amp;dc);</P>
< align=justify>OnDraw(&amp;dc);</P>
<P align=justify>}</P>
<P align=justify>在栈中定义了CPaintDC类型的变量dc,随着构造函数的调用获取了设备描述表;设备描述表使用完毕,超出其有效范围就被自动地清除,随着析构函数的调用,其获取的设备描述表被释放。</P>
<P align=justify>如果希望在堆中创建,例如</P>
<P align=justify>CPaintDC *pDC;</P>
<P align=justify>pDC = new CPaintDC(this)</P>
<P align=justify>则在使用完毕时,用delete删除pDC:</P>
<P align=justify>delete pDC;</P>
<P align=justify></P>
<P align=justify></P>
<LI>直接使用CDC
<p></LI></OL>
<P align=justify>需要注意的是:在生成CDC对象的时候,并不像它的派生类那样,在构造函数里获取相应的Windows设备描述表。最好不要使用::GetDC等函数来获取一个设备描述表,而是创建一个设备描述表。其构造函数如下:</P>
<P align=justify>CDC::CDC()</P>
<P align=justify>{</P>
<DIR>
<P align=justify>m_hDC = NULL;</P>
<P align=justify>m_hAttribDC = NULL;</P>
<P align=justify>m_bPrinting = FALSE;</P></DIR>
<P align=justify>}</P>
<P align=justify>其析构函数如下:</P>
<P align=justify>CDC::~CDC()</P>
<P align=justify>{</P>
<DIR>
<P align=justify>if (m_hDC != NULL)</P>
<P align=justify>:eleteDC(Detach());</P></DIR>
<P align=justify>}</P>
<P align=justify>在CDC析构函数中,如果设备描述表句柄不空,则调用DeleteDC删除它。这是直接使用CDC时最好创建Windows设备描述表的理由。如果设备描述表不是创建的,则应该在析构函数被调用前分离出设备描述表句柄并用::RealeaseDC释放它,释放后m_hDC为空,则在析构函数调用时不会执行:eleteDC。当然,不用担心CDC的派生类的析构函数调用CDC的析构函数,因为CDC::~CDC()不是虚拟析构函数。</P>
<P align=justify>直接使用CDC的例子是内存设备上下文,例如:</P>
<P align=justify>CDC dcMem; //声明一个CDC对象</P>
<P align=justify>dcMem.CreateCompatibleDC(&amp;dc); //创建设备描述表</P>
<P align=justify>pbmOld = dcMem.SelectObject(&amp;m_bmBall);//更改设备描述表属性</P>
<P align=justify>…//作一些绘制操作</P>
<P align=justify></P>
<P align=justify>dcMem.SelectObject(pbmOld);//恢复设备描述表的属性</P>
<P align=justify>dcMem.DeleteDC(); //可以不调用,而让析构函数去删除设备描述表</P>
<OL>
<OL>
<P align=justify>
<LI><B>GDI对象</B>
<p></LI></OL></OL>
<P align=justify>在讨论设备描述表时,已经多次涉及到GDI对象。这里,需强调一下:GDI对象要选入Windows 设备描述表后才能使用;用毕,要恢复设备描述表的原GDI对象,并删除该GDI对象。</P>
<P align=justify>一般按如下步骤使用GDI对象:</P>
<P align=justify>Create or get a GDI OBJECT hNewGdi;</P>
<P align=justify></P>
<P align=justify>hOldGdi = ::SelectObject(hdc, hNewGdi)</P>
<P align=justify>……</P>
<P align=justify>::SelectObject(hdc, hOldGdi)</P>
<P align=justify>:eleteObject(hNewGdi)</P>
<P align=justify>先创建或得到一个GDI对象,然后把它选入设备描述表并保存它原来的GDI对象;用毕恢复设备描述表原来的GDI对象并删除新创建的GDI对象。</P>
<P align=justify>需要指出的是,如果hNewGdi是一个Stock GDI对象,可以不删除(删除也可以)。通过</P>
<DIR>
<DIR>
<P>HGDIOBJ GetStockObject(</P></DIR>
<P>int fnObject // type of stock object </P>
<P align=justify>);</P></DIR>
<P align=justify>来获取Stock GDI对象。</P>
<P align=justify></P>
<OL>
<P align=justify>
<LI>MFC GDI对象
<p>
<P align=justify>MFC用一些类封装了Windows GDI对象和相关函数,层次结构如图2-4所示:</P>
<P align=justify></P><IMG src="http://www.vczx.com/tutorial/mfc/image109.gif" align=left>
<P align=justify></P>
<P align=justify>CGdiObject封装了Windows GDI Object共有的特性。其派生类在继承的基础上,主要封装了各类GDI的创建函数以及和具体GDI对象相关的操作。</P>
<P align=justify></P><IMG src="http://www.vczx.com/tutorial/mfc/image110.gif" align=left>
<P align=justify>CGdiObject的构造函数仅仅让m_hObject为空。如果m_hObject不空,其析构函数将删除对应的Windows GDI对象。MFC GDI对象和Windows GDI对象的关系如图2-5所示。</P>
<P align=justify></P>
<LI>使用MFC GDI类的使用
<p></LI></OL>
<P align=justify>首先创建GDI对象,可分一步或两步创建。一步创建就是构造MFC对象和Windows GDI对象一步完成;两步创建则先构造MFC对象,接着创建Windows GDI对象。然后,把新创建的GDI对象选进设备描述表,取代原GDI对象并保存。最后,恢复原GDI对象。例如:</P>
<P align=justify>void CMyView::OnDraw(CDC *pDC)</P>
<P align=justify>{</P>
<P align=justify>CPen penBlack; //构造MFC CPen对象</P>
<P align=justify>if (penBlack.CreatePen(PS_SOLID, RGB(0, 0, 0)))</P>
<P align=justify>{</P>
<DIR>
<P align=justify>CPen *pOldPen = pDC-&gt;SelectObject(&amp;penBlack)); //选进设备表,保存原笔</P>
<P align=justify>…</P>
<P align=justify>pDC-&gt;SelectObject(pOldPen); //恢复原笔</P></DIR>
<P align=justify>}else</P>
<P align=justify>{</P>
<DIR>
<P align=justify>…</P></DIR>
<P align=justify>}</P>
<P align=justify>}</P>
<P align=justify>和在SDK下有一点不同的是:这里没有DeleteObject。因为执行完OnDraw后,栈中的penBlack被销毁,它的析构函数被调用,导致DeleteObject的调用。</P>
<P align=justify>还有一点要说明:</P>
<P align=justify>pDC-&gt;SelectObject(&amp;penBlack)返回了一个CPen *指针,也就是说,它根据原来PEN的句柄创建了一个MFC CPen对象。这个对象是否需要删除呢?不必要,因为它是一个临时对象,MFC框架会自动地删除它。当然,在本函数执行完毕把控制权返回给主消息循环之前,该对象是有效的。</P>
<P align=justify>关于临时对象及MFC处理它们的内部机制,将在后续章节详细讨论。</P>
<P align=justify>至此,Windows编程的核心概念:窗口、GDI界面(设备描述表、GDI对象等)已经陈述清楚,特别揭示了MFC对这些概念的封装机制,并简明讲述了与这些Windows Object对应的MFC类的使用方法。还有其他Windows概念,可以参见SDK开发文档。在MFC的实现上,基本上仅仅是对和这些概念相关的Win32函数的封装。如果明白了MFC的窗口、GDI界面的封装机制,其他就不难了。</P>
<P>
  1. &lt;SCRIPT language=JavaScript&gt;<br>write_tail();<br>&lt;/script&gt;
复制代码

</P>
<HR>
 楼主| 发表于 2006-12-8 10:00:13 | 显示全部楼层
<OL start=3>
< align=justify>
<LI><B>CObject类</B>
<p>
< align=justify>CObject是大多数MFC类的根类或基类。CObject类有很多有用的特性:对运行时类信息的支持,对动态创建的支持,对串行化的支持,对象诊断输出,等等。MFC从CObject派生出许多类,具备其中的一个或者多个特性。程序员也可以从CObject类派生出自己的类,利用CObject类的这些特性。</P>
< align=justify>本章将讨论MFC如何设计CObject类的这些特性。首先,考察CObject类的定义,分析其结构和方法(成员变量和成员函数)对CObject特性的支持。然后,讨论CObject特性及其实现机制。</P>
<OL>
< align=justify>
<LI><B>CObject的结构</B>
<p>
< align=justify>以下是CObject类的定义:</P>
< align=justify>class CObject</P>
< align=justify>{</P>
< align=justify>public:</P>
< align=justify></P>
< align=justify>//与动态创建相关的函数</P>
<P align=justify>virtual CRuntimeClass* GetRuntimeClass() const;</P>
<P align=justify>析构函数</P>
<P align=justify>virtual ~CObject(); // virtual destructors are necessary</P>
<P align=justify></P>
<P align=justify>//与构造函数相关的内存分配函数,可以用于DEBUG下输出诊断信息</P>
<P align=justify>void* PASCAL operator new(size_t nSize);</P>
<P align=justify>void* PASCAL operator new(size_t, void* p);</P>
<P align=justify>void PASCAL operator delete(void* p);</P>
<P align=justify>#if defined(_DEBUG) &amp;&amp; !defined(_AFX_NO_DEBUG_CRT)</P>
<P align=justify>void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);</P>
<P align=justify>#endif</P>
<P align=justify></P>
<P align=justify>//缺省情况下,复制构造函数和赋值构造函数是不可用的</P>
<P align=justify>//如果程序员通过传值或者赋值来传递对象,将得到一个编译错误</P>
<P align=justify>protected:</P>
<P align=justify>//缺省构造函数</P>
<P align=justify>CObject();</P>
<P align=justify>private:</P>
<P align=justify>//复制构造函数,私有</P>
<P align=justify>CObject(const CObject&amp; objectSrc); // no implementation</P>
<P align=justify>//赋值构造函数,私有</P>
<P align=justify>void operator=(const CObject&amp; objectSrc); // no implementation</P>
<P align=justify></P>
<P align=justify>// Attributes</P>
<P align=justify>public:</P>
<P align=justify>//与运行时类信息、串行化相关的函数</P>
<P align=justify>BOOL IsSerializable() const;</P>
<P align=justify>BOOL IsKindOf(const CRuntimeClass* pClass) const;</P>
<P align=justify>// Overridables</P>
<P align=justify>virtual void Serialize(CArchive&amp; ar);</P>
<P align=justify>// 诊断函数</P>
<P align=justify>virtual void AssertValid() const;</P>
<P align=justify>virtual void Dump(CDumpContext&amp; dc) const;</P>
<P align=justify></P>
<P align=justify>// Implementation</P>
<P align=justify>public:</P>
<P align=justify>//与动态创建对象相关的函数</P>
<P align=justify>static const AFX_DATA CRuntimeClass classCObject;</P>
<P align=justify>#ifdef _AFXDLL</P>
<P align=justify>static CRuntimeClass* PASCAL _GetBaseClass();</P>
<P align=justify>#endif</P>
<P align=justify>};</P>
<P align=justify></P>
<P align=justify>由上可以看出,CObject定义了一个CRuntimeClass类型的静态成员变量:</P>
<P align=justify>CRuntimeClass classCObject</P>
<P align=justify>还定义了几组函数:</P>
<P align=justify>构造函数析构函数类,</P>
<P align=justify>诊断函数,</P>
<P align=justify>与运行时类信息相关的函数,</P>
<P align=justify>与串行化相关的函数。</P>
<P align=justify>其中,一个静态函数:_GetBaseClass;五个虚拟函数:析构函数、GetRuntimeClass、Serialize、AssertValid、Dump。这些虚拟函数,在CObject的派生类中应该有更具体的实现。必要的话,派生类实现它们时可能要求先调用基类的实现,例如Serialize和Dump就要求这样。</P>
<P align=justify>静态成员变量classCObject和相关函数实现了对CObjet特性的支持。</P>
<P align=justify></P>
<LI><B>CObject类的特性</B>
<p></LI></OL></LI></OL>
<P align=justify>下面,对三种特性分别描述,并说明程序员在派生类中支持这些特性的方法。</P>
<OL>
<P align=justify>
<LI>对运行时类信息的支持
<p></LI></OL>
<P align=justify>该特性用于在运行时确定一个对象是否属于一特定类(是该类的实例),或者从一个特定类派生来的。CObject提供IsKindOf函数来实现这个功能。</P>
<P align=justify>从CObject派生的类要具有这样的特性,需要:</P>
<UL>
<P align=justify>
<LI>定义该类时,在类说明中使用DECLARE_DYNAMIC(CLASSNMAE)宏;
<p>
<P align=justify></P>
<LI>在类的实现文件中使用IMPLEMENT_DYNAMIC(CLASSNAME,BASECLASS)宏。
<p></LI></UL>
<P align=justify></P>
<OL>
<P align=justify>
<LI>对动态创建的支持
<p></LI></OL>
<P align=justify>前面提到了动态创建的概念,就是运行时创建指定类的实例。在MFC中大量使用,如前所述框架窗口对象、视对象,还有文档对象都需要由文档模板类(CDocTemplate)对象来动态的创建。</P>
<P align=justify>从CObject派生的类要具有动态创建的功能,需要:</P>
<UL>
<P align=justify>
<LI>定义该类时,在类说明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;
<p>
<P align=justify></P>
<LI>定义一个不带参数的构造函数(默认构造函数);
<p>
<P align=justify></P>
<LI>在类的实现文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;
<p>
<P align=justify></P>
<LI>使用时先通过宏RUNTIME_CLASS得到类的RunTime信息,然后使用CRuntimeClass的成员函数CreateObject创建一个该类的实例。
<p></LI></UL>
<P align=justify>例如:</P>
<P align=justify>CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CNname)</P>
<P align=justify>//CName必须有一个缺省构造函数</P>
<P align=justify>CObject* pObject = pRuntimeClass-&gt;CreateObject();</P>
<P align=justify>//用IsKindOf检测是否是CName类的实例</P>
<P align=justify>Assert( pObject-&gt;IsKindOf(RUNTIME_CLASS(CName));</P>
<P align=justify></P>
<OL>
<P align=justify>
<LI>对序列化的支持
<p></LI></OL>
<DIR>
<P align=justify>“序列化”就是把对象内容存入一个文件或从一个文件中读取对象内容的过程。从CObject派生的类要具有序列化的功能,需要:</P></DIR>
<UL>
<P align=justify>
<LI>定义该类时,在类说明中使用DECLARE_SERIAL(CLASSNMAE)宏;
<p>
<P align=justify></P>
<LI>定义一个不带参数的构造函数(默认构造函数);
<p>
<P align=justify></P>
<LI>在类的实现文件中使用IMPLEMENT_SERIAL(CLASSNAME,BASECLASS)宏;
<p>
<P align=justify></P>
<LI>覆盖Serialize成员函数。(如果直接调用Serialize函数进行序列化读写,可以省略前面三步。)
<p></LI></UL>
<P align=justify></P>
<P align=justify>对运行时类信息的支持、动态创建的支持、串行化的支持层(不包括直接调用Serailize实现序列化),这三种功能的层次依次升高。如果对后面的功能支持,必定对前面的功能支持。支持动态创建的话,必定支持运行时类信息;支持序列化,必定支持前面的两个功能,因为它们的声明和实现都是后者包含前者。</P>
<OL>
<P align=justify>
<LI>综合示例:
<p></LI></OL>
<P align=justify>定义一个支持串行化的类CPerson:</P>
<P align=justify>class CPerson : public CObject</P>
<P align=justify>{</P>
<DIR>
<P align=justify>public:</P></DIR>
<P align=justify>DECLARE_SERIAL( CPerson )</P>
<P align=justify>// 缺省构造函数</P>
<P align=justify>CPerson(){}{};</P>
<P align=justify></P>
<P align=justify>CString m_name;</P>
<P align=justify>WORD m_number;</P>
<P align=justify></P>
<P align=justify>void Serialize( CArchive&amp; archive );</P>
<P align=justify></P>
<P align=justify>// rest of class declaration</P>
<P align=justify>};</P>
<P align=justify></P>
<P align=justify>实现该类的成员函数Serialize,覆盖CObject的该函数:</P>
<P align=justify>void CPerson::Serialize( CArchive&amp; archive )</P>
<P align=justify>{</P>
<P align=justify>// 先调用基类函数的实现</P>
<P align=justify>CObject::Serialize( archive );</P>
<P align=justify></P>
<P align=justify>// now do the stuff for our specific class</P>
<P align=justify>if( archive.IsStoring() )</P>
<P align=justify>archive &lt;&lt; m_name &lt;&lt; m_number;</P>
<P align=justify>else</P>
<P align=justify>archive &gt;&gt; m_name &gt;&gt; m_number;</P>
<P align=justify>}</P>
<P align=justify></P>
<P align=justify>使用运行时类信息:</P>
<P align=justify>CPerson a; </P>
<P align=justify>ASSERT( a.IsKindOf( RUNTIME_CLASS( CPerson ) ) );</P>
<P align=justify>ASSERT( a.IsKindOf( RUNTIME_CLASS( CObject ) ) );</P>
<P align=justify>动态创建:</P>
<P align=justify>CRuntimeClass* pRuntimeClass = RUNTIME_CLASS(CPerson)</P>
<P align=justify>//Cperson有一个缺省构造函数</P>
<P align=justify>CObject* pObject = pRuntimeClass-&gt;CreateObject();</P>
<P align=justify>Assert( pObject-&gt;IsKindOf(RUNTIME_CLASS(CPerson)); </P>
<OL>
<OL>
<P align=justify>
<LI><B>实现CObject特性的机制</B>
<p>
<P align=justify>由上,清楚了CObject的结构,也清楚了从CObject派生新类时程序员使用CObject特性的方法。现在来考察这些方法如何利用CObjet的结构,CObject结构如何支持这些方法。</P>
<P align=justify>首先,要揭示DECLARE_DYNAMIC等宏的内容,然后,分析这些宏的作用。</P>
<OL>
<P align=justify>
<LI><B>DECLARE_DYNAMIC等宏的定义</B>
<p></LI></OL></LI></OL></OL>
<P align=justify>MFC提供了DECLARE_DYNAMIC、DECLARE_DYNCREATE、DECLARE_SERIAL声明宏的两种定义,分别用于静态链接到MFC DLL和动态链接到MFC DLL。对应的实现宏IMPLEMNET_XXXX也有两种定义,但是,这里实现宏就不列举了。</P>
<P align=justify></P>
<P align=justify>MFC对这些宏的定义如下:</P>
<P align=justify>#ifdef _AFXDLL //动态链接到MFC DLL</P>
<P align=justify>#define <B>DECLARE_DYNAMIC</B>(class_name) \</P>
<P align=justify>protected: \</P>
<P align=justify>static CRuntimeClass* PASCAL _GetBaseClass(); \</P>
<P align=justify>public: \</P>
<P align=justify>static const AFX_DATA CRuntimeClass class##class_name; \</P>
<P align=justify>virtual CRuntimeClass* GetRuntimeClass() const; \</P>
<P align=justify></P>
<P align=justify>#define _<B>DECLARE_DYNAMIC</B>(class_name) \</P>
<P align=justify>protected: \</P>
<P align=justify>static CRuntimeClass* PASCAL _GetBaseClass(); \</P>
<P align=justify>public: \</P>
<P align=justify>static AFX_DATA CRuntimeClass class##class_name; \</P>
<P align=justify>virtual CRuntimeClass* GetRuntimeClass() const; \</P>
<P align=justify></P>
<P align=justify>#else</P>
<P align=justify>#define <B>DECLARE_DYNAMIC</B>(class_name) \</P>
<P align=justify>public: \</P>
<P align=justify>static const AFX_DATA CRuntimeClass class##class_name; \</P>
<P align=justify>virtual CRuntimeClass* GetRuntimeClass() const; \</P>
<P align=justify></P>
<P align=justify>#define <B>_DECLARE_DYNAMIC</B>(class_name) \</P>
<P align=justify>public: \</P>
<P align=justify>static AFX_DATA CRuntimeClass class##class_name; \</P>
<P align=justify>virtual CRuntimeClass* GetRuntimeClass() const; \</P>
<P align=justify></P>
<P align=justify>#endif</P>
<P align=justify></P>
<P align=justify>// not serializable, but dynamically constructable</P>
<P align=justify>#define <B>DECLARE_DYNCREATE</B>(class_name) \</P>
<P align=justify>DECLARE_DYNAMIC(class_name) \</P>
<P align=justify>static CObject* PASCAL CreateObject();</P>
<P align=justify></P>
<P align=justify>#define <B>DECLARE_SERIAL</B>(class_name) \</P>
<P align=justify>_DECLARE_DYNCREATE(class_name) \</P>
<P align=justify>friend CArchive&amp; AFXAPI operator&gt;&gt;(CArchive&amp; ar, class_name* &amp;pOb);</P>
 楼主| 发表于 2006-12-8 10:00:30 | 显示全部楼层
< align=justify>由于这些声明宏都是在CObect派生类的定义中被使用的,所以从这些宏的上述定义中可以看出,DECLARE_DYNAMIC宏给所在类添加了一个CRuntimeClass类型的静态数据成员class##class_name(类名加前缀class,例如,若类名是CPerson,则该变量名称是classCPerson),且指定为const;两个(使用MFC DLL时,否则,一个)成员函数:虚拟函数GetRuntimeClass和静态函数_GetBaseClass(使用MFC DLL时)。</P>
< align=justify>DECLARE_DYNCREATE宏包含了DECLARE_DYNAMIC,在此基础上,还定义了一个静态成员函数CreateObject。</P>
< align=justify>DECLARE_SERIAL宏则包含了_DECLARE_DYNCREATE,并重载了操作符“&gt;&gt;”(友员函数)。它和前两个宏有所不同的是CRuntimeClass数据成员class##class_name没有被指定为const。</P>
< align=justify></P>
< align=justify>对应地,MFC使用三个宏初始化DECLARE宏所定义的静态变量并实现DECLARE宏所声明的函数:IMPLEMNET_DYNAMIC,IMPLEMNET_DYNCREATE,IMPLEMENT_SERIAL。</P>
< align=justify>首先,这三个宏初始化CRuntimeClass类型的静态成员变量class#class_name。IMPLEMENT_SERIAL不同于其他两个宏,没有指定该变量为const。初始化内容在下节讨论CRuntimeClass时给出。</P>
< align=justify>其次,它实现了DECLARE宏声明的成员函数:</P>
<UL>
< align=justify>
<LI>_GetBaseClass()
<p></LI></UL>
< align=justify>返回基类的运行时类信息,即基类的CRuntimeClass类型的静态成员变量。这是静态成员函数。</P>
<UL>
< align=justify>
<LI>GetRuntimeClass()
<p></LI></UL>
<P align=justify>返回类自己的运行类信息,即其CRuntimeClass类型的静态成员变量。这是虚拟成员函数。</P>
<P align=justify>对于动态创建宏,还有一个静态成员函数CreateObject,它使用C++操作符和类的缺省构造函数创建本类的一个动态对象。</P>
<UL>
<P align=justify>
<LI>操作符的重载
<p></LI></UL>
<P align=justify>对于序列化的实现宏IMPLEMENT_SERIAL,还重载了操作符&lt;&lt;和定义了一个静态成员变量</P>
<P align=justify>static const AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); </P>
<P align=justify>比如,对CPerson来说,该变量是_init_Cperson,其目的在于静态成员在应用程序启动之前被初始化,使得AFX_CLASSINIT类的构造函数被调用,从而通过AFX_CLASSINIT类的构造函数在模块状态的CRuntimeClass链表中插入构造函数参数表示的CRuntimeClass类信息。至于模块状态,在后文有详细的讨论。</P>
<P align=justify>重载的操作符函数用来在序列化时从文档中读入该类对象的内容,是一个友员函数。定义如下:</P>
<P align=justify>CArchive&amp; AFXAPI operator&gt;&gt;(CArchive&amp; ar, class_name* &amp;pOb) </P>
<P align=justify>{</P>
<P align=justify>pOb = (class_name*) ar.ReadObject(</P>
<P align=justify>RUNTIME_CLASS(class_name));</P>
<P align=justify>return ar;</P>
<P align=justify>}</P>
<P align=justify>回顾CObject的定义,它也有一个CRuntimeClass类型的静态成员变量classCObject,因为它本身也支持三个特性。</P>
<P align=justify>以CObject及其派生类的静态成员变量classCObject为基础,IsKindOf和动态创建等函数才可以起到作用。</P>
<P align=justify>这个变量为什么能有这样的用处,这就要分析CRuntimeClass类型变量的结构和内容了。下面,在讨论了CRuntimeClass的结构之后,考察该类型的静态变量被不同的宏初始化之后的内容。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>CruntimeClass类的结构与功能</B>
<p></LI></OL></OL></OL>
<P align=justify>从上面的讨论可以看出,在对CObject特性的支持上,CRuntimeClass类起到了关键作用。下面,考查它的结构和功能。</P>
<OL>
<P align=justify>
<LI>CRuntimeClass的结构
<p>
<P align=justify>CruntimeClass的结构如下:</P>
<P align=justify>Struct CRuntimeClass</P>
<P align=justify>{</P>
<P align=justify>LPCSTR m_lpszClassName;//类的名字</P>
<P align=justify>int m_nObjectSize;//类的大小</P>
<P align=justify>UINT m_wSchema;</P>
<P align=justify>CObject* (PASCAL* m_pfnCreateObject)();</P>
<P align=justify>//pointer to function, equal to newclass.CreateObject() </P>
<P align=justify>//after IMPLEMENT</P>
<P align=justify>CRuntimeClass* (PASCAL* m_pfnGetBaseClass)();</P>
<P align=justify>CRumtieClass* m_pBaseClass;</P>
<P align=justify></P>
<P align=justify>//operator:</P>
<P align=justify>CObject *CreateObject();</P>
<P align=justify>BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;</P>
<P align=justify>...</P>
<P align=justify>}</P>
<P align=justify>CRuntimeClass成员变量中有两个是函数指针,还有几个用来保存所在CruntimeClass对象所在类的名字、类的大小(字节数)等。</P>
<P align=justify>这些成员变量被三个实现宏初始化,例如:</P>
<P align=justify>m_pfnCreateObject,将被初始化指向所在类的静态成员函数CreateObject。CreateObject函数在初始化时由实现宏定义,见上文的说明。</P>
<P align=justify>m_pfnGetBaseClass,如果定义了_AFXDLL,则该变量将被初始化指向所在类的成员函数_GetBaseClass。_GetBaseClass在声明宏中声明,在初始化时由实现宏定义,见上文的说明。</P>
<P align=justify>下面,分析三个宏对CObject及其派生类的CRuntimeClass类型的成员变量class##class_name初始化的情况,然后讨论CRuntimeClass成员函数的实现。</P>
<P align=justify></P>
<LI>成员变量class##class_name的内容
<p>
<P align=justify>IMPLEMENT_DYNCREATE等宏将初始化类的CRuntimeClass类型静态成员变量的各个域,表3-1列出了在动态类信息、动态创建、序列化这三个不同层次下对该静态成员变量的初始化情况:</P>
<P align=center>表3-1 静态成员变量class##class_name的初始化</P>
<TABLE cellSpacing=1 cellPadding=7 width=497 border=1>

<TR>
<TD vAlign=top width="27%">
<P align=justify>CRuntimeClass成员变量 </P></TD>
<TD vAlign=top width="24%">
<P align=justify>动态类信息 </P></TD>
<TD vAlign=top width="25%">
<P align=justify>动态创建 </P></TD>
<TD vAlign=top width="24%">
<P align=justify>序列化 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_lpszClassName </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类名字符串 </P></TD>
<TD vAlign=top width="25%">
<P align=justify>类名字符串 </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类名字符串 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_nObjectSize </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类的大小(字节数) </P></TD>
<TD vAlign=top width="25%">
<P align=justify>类的大小(字节数) </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类的大小(字节数) </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_wShema </P></TD>
<TD vAlign=top width="24%">
<P align=justify>0xFFFF </P></TD>
<TD vAlign=top width="25%">
<P align=justify>0xFFFF </P></TD>
<TD vAlign=top width="24%">
<P align=justify>1、2等,非0 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_pfnCreateObject </P></TD>
<TD vAlign=top width="24%">
<P align=justify>NULL </P></TD>
<TD vAlign=top width="25%">
<P align=justify>类的成员函数</P>
<P align=justify>CreateObject </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类的成员函数</P>
<P align=justify>CreateObject </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_pBaseClass </P></TD>
<TD vAlign=top width="24%">
<P align=justify>基类的CRuntimeClass变量 </P></TD>
<TD vAlign=top width="25%">
<P align=justify>基类的CRuntimeClass变量 </P></TD>
<TD vAlign=top width="24%">
<P align=justify>基类的CRuntimeClass变量 </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_pfnGetBaseClass </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类的成员函数</P>
<P align=justify>_GetBaseClass </P></TD>
<TD vAlign=top width="25%">
<P align=justify>类的成员函数</P>
<P align=justify>_GetBaseClass </P></TD>
<TD vAlign=top width="24%">
<P align=justify>类的成员函数</P>
<P align=justify>_GetBaseClass </P></TD></TR>
<TR>
<TD vAlign=top width="27%">
<P align=justify>m_pNextClass </P></TD>
<TD vAlign=top width="24%">
<P align=justify>NULL </P></TD>
<TD vAlign=top width="25%">
<P align=justify>NULL </P></TD>
<TD vAlign=top width="24%">
<P align=justify>NULL </P></TD></TR></TABLE>
<P align=center></P>
<P align=justify>m_wSchema类型是UINT,定义了序列化中保存对象到文档的程序的版本。如果不要求支持序列化特性,该域为0XFFFF,否则,不能为0。</P>
<P align=justify>Cobject类本身的静态成员变量classCObject被初始化为:</P>
<P align=justify>{ "CObject", sizeof(CObject), 0xffff, NULL, &amp;CObject::_GetBaseClass, NULL };</P>
<P align=justify>对初始化内容解释如下:</P>
<P align=justify>类名字符串是“CObject”,类的大小是sizeof(CObject),不要求支持序列化,不支持动态创建。</P></LI></OL>
 楼主| 发表于 2006-12-8 10:00:46 | 显示全部楼层
<OL>
<LI>
<>成员函数CreateObject </P>
<p>
< align=justify>回顾3.2节,动态创建对象是通过语句pRuntimeClass-&gt;CreateObject完成的,即调用了CRuntimeClass自己的成员函数,CreateObject函数又调用m_pfnCreateObject指向的函数来完成动态创建任务,如下所示:</P>
< align=justify>CObject* CRuntimeClass::CreateObject()</P>
< align=justify>{</P>
< align=justify>if (m_pfnCreateObject == NULL) //判断函数指针是否空</P>
< align=justify>{</P>
< align=justify>TRACE(_T("Error: Trying to create object which is not ")</P>
< align=justify>_T("DECLARE_DYNCREATE \nor DECLARE_SERIAL: %hs.\n"),</P>
< align=justify>m_lpszClassName);</P>
< align=justify>return NULL;</P>
<P align=justify>}</P>
<P align=justify>//函数指针非空,继续处理</P>
<P align=justify>CObject* pObject = NULL;</P>
<P align=justify>TRY</P>
<P align=justify>{</P>
<P align=justify>pObject = (*m_pfnCreateObject)(); //动态创建对象</P>
<P align=justify>}</P>
<P align=justify>END_TRY</P>
<P align=justify>return pObject;</P>
<P align=justify>}</P>
<P align=justify></P>
<P align=justify></P>
<LI>成员函数IsDerivedFrom
<p>
<P align=justify>该函数用来帮助运行时判定一个类是否派生于另一个类,被CObject的成员函数IsKindOf函数所调用。其实现描述如下:</P>
<P align=justify>如果定义了_AFXDLL则,成员函数IsDerivedFrom调用成员函数m_pfnGetBaseClass指向的函数来向上逐层得到基类的CRuntimeClass类型的静态成员变量,直到某个基类的CRuntimeClass类型的静态成员变量和参数指定的CRuntimeClass变量一致或者追寻到最上层为止。</P>
<P align=justify>如果没有定义_AFXDLL,则使用成员变量m_pBaseClass基类的CRuntimeClass类型的静态成员变量。</P>
<P align=justify>程序如下所示:</P>
<P align=justify>BOOL CRuntimeClass::IsDerivedFrom(</P>
<P align=justify>const CRuntimeClass* pBaseClass) const</P>
<P align=justify>{</P>
<P align=justify>ASSERT(this != NULL);</P>
<P align=justify>ASSERT(AfxIsValidAddress(this, sizeof(CRuntimeClass), FALSE));</P>
<P align=justify>ASSERT(pBaseClass != NULL);</P>
<P align=justify>ASSERT(AfxIsValidAddress(pBaseClass, sizeof(CRuntimeClass), FALSE));</P>
<P align=justify></P>
<P align=justify>// simple SI case</P>
<P align=justify>const CRuntimeClass* pClassThis = this;</P>
<P align=justify>while (pClassThis != NULL)//从本类开始向上逐个基类搜索</P>
<P align=justify>{</P>
<P align=justify>if (pClassThis == pBaseClass)//若是参数指定的类信息</P>
<P align=justify>return TRUE;</P>
<P align=justify>//类信息不符合,继续向基类搜索</P>
<P align=justify>#ifdef _AFXDLL</P>
<P align=justify>pClassThis = (*pClassThis-&gt;m_pfnGetBaseClass)();</P>
<P align=justify>#else</P>
<P align=justify>pClassThis = pClassThis-&gt;m_pBaseClass;</P>
<P align=justify>#endif</P>
<P align=justify>}</P>
<P align=justify>return FALSE; // 搜索完毕,没有匹配,返回FALSE。</P>
<P align=justify>}</P>
<P align=justify>由于CRuntimeClass类型的成员变量是静态成员变量,所以如果两个类的CruntimeClass成员变量相同,必定是同一个类。这就是IsDerivedFrom和IsKindOf的实现基础。</P>
<P align=justify></P>
<LI>RUNTIME_CLASS宏
<p></LI>
<LI>
<DIV align=justify>RUNTIME_CLASS宏定义如下:</DIV></LI></OL>
<DIR>
<P>#define RUNTIME_CLASS(class_name) (&amp;class_name::class##class_name)</P></DIR>
<P align=justify>为了方便地得到每个类(Cobject或其派生类)的CRuntimeClass类型的静态成员变量,MFC定义了这个宏。它返回对类class_name的CRuntimeClass类型成员变量的引用,该成员变量的名称是“class”加上class_name(类的名字)。例如:</P>
<P align=justify>RUNTIME_CLASS(CObject)得到对classCObject的引用;</P>
<P align=justify>RUNTIME_CLASS(CPerson)得到对class CPerson的引用。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>动态类信息、动态创建的原理</B>
<p></LI></OL></OL></OL>
<DIR>
<P align=justify>MFC对Cobject动态类信息、动态创建的实现原理:</P></DIR>
<P align=justify>动态类信息、动态创建都建立在给类添加的CRuntimeClass类型的静态成员变量基础上,总结如下。</P>
<P align=justify>C++不支持动态创建,但是支持动态对象的创建。动态创建归根到底是创建动态对象,因为从一个类名创建一个该类的实例最终是创建一个以该类为类型的动态对象。其中的关键是从一个类名可以得到创建其动态对象的代码。</P>
<P align=justify>在一个类没有任何实例之前,怎么可以得到该类的创建动态对象的代码?借助于C++的静态成员数据技术可达到这个目的:</P>
<UL>
<P align=justify>
<LI>静态成员数据在程序的入口(main或WinMain)之前初始化。因此,在一个静态成员数据里存放有关类型信息、动态创建函数等,需要的时候,得到这个成员数据就可以了。
<p>
<P align=justify></P>
<LI>不论一个类创建多少实例,静态成员数据只有一份。所有的类的实例共享一个静态成员数据,要判断一个类是否是一个类的实例,只须确认它是否使用了该类的这个静态数据。
<p></LI></UL>
<P align=justify>从前两节的讨论知道,DECLARE_CREATE等宏定义了一个这样的静态成员变量:类型是CRuntimeClass,命名约定是“calss”加上类名;IMPLEMENT_CREATE等宏初始化该变量;RUNTIME_CLASS宏用来得到该成员变量。</P>
<P align=justify>动态类信息的原理在分析CRuntimeClass的成员函数IsDerivedFrom时已经作了解释。</P>
<P align=justify>动态创建的过程和原理了,用图表示其过程如下:</P>
<P align=justify></P><IMG src="http://www.vczx.com/tutorial/mfc/image111.gif" align=left>
<P align=justify>注:下面一个方框内的逐级缩进表示逐层调用关系。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>序列化的机制</B>
<p></LI></OL></OL></OL>
<P align=justify>由上所述可知,一个类要支持实现序列化,使得它的对象可以保存到文档中或者可以从文档中读入到内存中并生成对象,需要使用动态类信息,而且,需要覆盖基类的Serialize虚拟函数来完成其对象的序列化。</P>
<P align=justify>仅仅有类的支持是不够的,MFC还提供了一个归档类CArchive来支持简单类型的数据和复杂对象的读写。</P>
<P align=justify>CArchive 在文件和内存对象之间充当一个代理者的角色。它负责按一定的顺序和格式把内存对象写到文件中,或者读出来,可以被看作是一个二进制的流。它和文件类CFile的关系如图3-2所示:</P><IMG src="http://www.vczx.com/tutorial/mfc/image112.gif" align=left>
<P align=justify></P>
<P align=justify>一个CArchive对象在要序列化的对象和存储媒体(storage medium,可以是一个文件或者一个Socket)之间起了中介作用。它提供了系列方法来完成序列化,不仅能够把int、float等简单类型数据进行序列化,而且能够把复杂的数据如string等进行序列化,更重要的是它能把复杂的对象(包括复合对象)进行序列化。这些方法就是重载的操作符&gt;&gt;和&lt;&lt;。对于简单类型,它针对不同类型直接实现不同的读写操作;对于复杂的对象,其每一个支持序列化的类都重载了操作符&gt;&gt;,从前几节可以清楚地看到这点:IMPLEMENT_SERIAL给所在类重载了操作符&gt;&gt;。至于&lt;&lt;操作,就不必每个序列化类都重载了。</P>
<P align=justify>复杂对象的“&lt;&lt;”操作,先搜索本模块状态的CRuntimeClass链表看是否有“&lt;&lt;”第二个参数指定的对象类的运行类信息(搜索过程涉及到模块状态,将在9.5.2节描述),如果有(无,则返回),则先使用这些信息动态的创建对象(这就是是序列化类必须提供动态类信息、支持动态创建的原因),然后对该对象调用Serilize函数从存储媒体读入对象内容。</P>
<P align=justify>复杂对象的“&gt;&gt;”操作先把对象类的运行类信息写入存储媒体,然后对该对象调用Serilize函数把对象内容写入存储媒体。</P>
<P align=justify>在创建CArchive对象时,必须有一个CFile对象,它代表了存储媒介。通常,程序员不必做这个工作,打开或保存文档时MFC将自动的创建CFile对象和CArchive对象并在适当的时候调用序列化类的Serialize函数。在后面讨论打开(5.3.3.2节)或者关闭(6.1节)文档时将会看到这样的流程。</P>
<P align=justify>CArchive对象被创建时,需要指定它是用来读还是用来写,即指定序列化操作的方向。Serialize函数适用CArchive的函数IsStoring判定CArchive是用于读出数据还是写入数据。</P>
<P align=justify></P>
<P align=justify>在解释实现序列化的方法时,曾经提到如果程序员直接调用Serilize函数完成序列化,而不借助CArchive的&gt;&gt;和&lt;&lt;操作,则可以不需要动态类信息和动态创建。从上文的论述可以看出,没有CArchive的&gt;&gt;和&lt;&lt;操作,的确不需要动态类信息和动态创建特性。</P>
 楼主| 发表于 2006-12-8 10:01:11 | 显示全部楼层
<OL start=4>
< align=justify>
<LI><B>消息映射的实现</B>
<p>
<OL>
< align=justify>
<LI><B>Windows消息概述</B>
<p>
< align=justify>Windows应用程序的输入由Windows系统以消息的形式发送给应用程序的窗口。这些窗口通过窗口过程来接收和处理消息,然后把控制返还给Windows。</P>
<OL>
< align=justify>
<LI><B>消息的分类</B>
<p></LI></OL></LI></OL></LI></OL>
<OL>
< align=justify>
<LI>队列消息和非队列消息
<p>
< align=justify>从消息的发送途径上看,消息分两种:队列消息和非队列消息。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。</P>
< align=justify>这里,对消息队列阐述如下:</P>
< align=justify>Windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。</P>
< align=justify>鼠标、键盘事件由鼠标或键盘驱动程序转换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次从系统消息队列移走一个消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。</P>
< align=justify>除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。</P>
<P align=justify>这些队列消息以外的绝大多数消息是非队列消息。</P>
<P align=justify></P>
<LI>系统消息和应用程序消息
<p></LI></OL>
<P align=justify>从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。</P>
<P align=justify>系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>消息结构和消息处理</B>
<p></LI></OL></OL></OL>
<OL>
<P align=justify>
<LI>消息的结构
<p>
<P align=justify>为了从消息队列获取消息信息,需要使用MSG结构。例如,::GetMessage函数(从消息队列得到消息并从队列中移走)和:eekMessage函数(从消息队列得到消息但是可以不移走)都使用了该结构来保存获得的消息信息。</P>
<P align=justify>MSG结构的定义如下:</P>
<P align=justify>typedef struct tagMSG { // msg </P>
<P align=justify>HWND hwnd; </P>
<P align=justify>UINT message; </P>
<P align=justify>WPARAM wParam; </P>
<P align=justify>LPARAM lParam; </P>
<P align=justify>DWORD time; </P>
<P align=justify>POINT pt; </P>
<P align=justify>} MSG; </P>
<P align=justify>该结构包括了六个成员,用来描述消息的有关属性:</P>
<P align=justify>接收消息的窗口句柄、消息标识(ID)、第一个消息参数、第二个消息参数、消息产生的时间、消息产生时鼠标的位置。</P>
<P align=justify></P>
<LI><A><A>应用程序通过窗口过程来处理消息</A></A>
<p>
<P align=justify>如前所述,每个“窗口类”都要登记一个如下形式的窗口过程:</P>
<P align=justify>LRESULT CALLBACK MainWndProc (</P>
<P align=justify>HWND hwnd,// 窗口句柄</P>
<P align=justify>UINT msg,// 消息标识</P>
<P align=justify>WPARAM wParam,//消息参数1</P>
<P align=justify>LPARAM lParam//消息参数2</P>
<P align=justify>)</P>
<P align=justify>应用程序通过窗口过程来处理消息:非队列消息由Windows直接送给目的窗口的窗口过程,队列消息由:ispatchMessage等派发给目的窗口的窗口过程。窗口过程被调用时,接受四个参数:</P>
<P align=justify>a window handle(窗口句柄);</P>
<P align=justify>a message identifier(消息标识);</P>
<P align=justify>two 32-bit values called message parameters(两个32位的消息参数);</P>
<P align=justify>需要的话,窗口过程用::GetMessageTime获取消息产生的时间,用::GetMessagePos获取消息产生时鼠标光标所在的位置。</P>
<P align=justify>在窗口过程里,用switch/case分支处理语句来识别和处理消息。</P>
<P align=justify></P>
<LI><A><A>应用程序通过消息循环来获得对消息的处理</A></A>
<p>
<P align=justify>每个GDI应用程序在主窗口创建之后,都会进入消息循环,接受用户输入、解释和处理消息。</P>
<P align=justify>消息循环的结构如下:</P>
<P align=justify>while (GetMessage(&amp;msg, (HWND) NULL, 0, 0)) {//从消息队列得到消息</P>
<P align=justify>if (hwndDlgModeless == (HWND) NULL || </P>
<P align=justify>!IsDialogMessage(hwndDlgModeless, &amp;msg) &amp;&amp; </P>
<P align=justify>!TranslateAccelerator(hwndMain, haccel, &amp;msg)) { </P>
<P align=justify>TranslateMessage(&amp;msg); </P>
<P align=justify>DispatchMessage(&amp;msg); //发送消息</P>
<P align=justify>} </P>
<P align=justify>}</P>
<P>消息循环从消息队列中得到消息,如果不是快捷键消息或者对话框消息,就进行消息转换和派发,让目的窗口的窗口过程来处理。</P>
<P>当得到消息WM_QUIT,或者::GetMessage出错时,退出消息循环。</P>
<P align=justify></P>
<LI>MFC消息处理
<p></LI></OL>
<P align=justify>使用MFC框架编程时,消息发送和处理的本质也如上所述。但是,有一点需要强调的是,所有的MFC窗口都使用同一窗口过程,程序员不必去设计和实现自己的窗口过程,而是通过MFC提供的一套消息映射机制来处理消息。因此,MFC简化了程序员编程时处理消息的复杂性。</P>
<P align=justify>所谓消息映射,简单地讲,就是让程序员指定要某个MFC类(有消息处理能力的类)处理某个消息。MFC提供了工具ClassWizard来帮助实现消息映射,在处理消息的类中添加一些有关消息映射的内容和处理消息的成员函数。程序员将完成消息处理函数,实现所希望的消息处理能力。</P>
<P>如果派生类要覆盖基类的消息处理函数,就用ClassWizard在派生类中添加一个消息映射条目,用同样的原型定义一个函数,然后实现该函数。这个函数覆盖派生类的任何基类的同名处理函数。</P>
<P align=justify></P>
<P align=justify>下面几节将分析MFC的消息机制的实现原理和消息处理的过程。为此,首先要分析ClassWizard实现消息映射的内幕,然后讨论MFC的窗口过程,分析MFC窗口过程是如何实现消息处理的。</P>
<OL>
<OL>
<P align=justify>
<LI><B>消息映射的定义和实现</B>
<p>
<OL>
<P align=justify>
<LI><B>MFC处理的三类消息</B>
<p></LI></OL></LI></OL></OL>
<P align=justify>根据处理函数和处理过程的不同,MFC主要处理三类消息:</P>
<UL>
<P align=justify>
<LI>Windows消息,前缀以“WM_”打头,WM_COMMAND例外。Windows消息直接送给MFC窗口过程处理,窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。
<p>
<P align=justify></P>
<LI>控制通知消息,是控制子窗口送给父窗口的WM_COMMAND通知消息。窗口过程调用对应的消息处理函数。一般,由窗口对象来处理这类消息,也就是说,这类消息处理函数一般是MFC窗口类的成员函数。
<p></LI></UL>
<P align=justify>需要指出的是,Win32使用新的WM_NOFITY来处理复杂的通知消息。WM_COMMAND类型的通知消息仅仅能传递一个控制窗口句柄(lparam)、控制窗ID和通知代码(wparam)。WM_NOTIFY能传递任意复杂的信息。</P>
<UL>
<P align=justify>
<LI>命令消息,这是来自菜单、工具条按钮、加速键等用户接口对象的WM_COMMAND通知消息,属于应用程序自己定义的消息。通过消息映射机制,MFC框架把命令按一定的路径分发给多种类型的对象(具备消息处理能力)处理,如文档、窗口、应用程序、文档模板等对象。能处理消息映射的类必须从CCmdTarget类派生。
<p></LI></UL>
<P align=justify>在讨论了消息的分类之后,应该是讨论各类消息如何处理的时候了。但是,要知道怎么处理消息,首先要知道如何映射消息。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>MFC消息映射的实现方法</B>
<p>
<P align=justify>MFC使用ClassWizard帮助实现消息映射,它在源码中添加一些消息映射的内容,并声明和实现消息处理函数。现在来分析这些被添加的内容。</P>
<P align=justify>在类的定义(头文件)里,它增加了消息处理函数声明,并添加一行声明消息映射的宏DECLARE_MESSAGE_MAP。</P>
<P align=justify>在类的实现(实现文件)里,实现消息处理函数,并使用IMPLEMENT_MESSAGE_MAP宏实现消息映射。一般情况下,这些声明和实现是由MFC的ClassWizard自动来维护的。看一个例子:</P>
<P align=justify>在AppWizard产生的应用程序类的源码中,应用程序类的定义(头文件)包含了类似如下的代码:</P>
<P align=justify>//{{AFX_MSG(CTttApp)</P>
<P align=justify>afx_msg void OnAppAbout();</P>
<P align=justify>//}}AFX_MSG</P>
<P align=justify>DECLARE_MESSAGE_MAP()</P>
<P align=justify></P>
<P align=justify>应用程序类的实现文件中包含了类似如下的代码:</P>
<P align=justify>BEGIN_MESSAGE_MAP(CTApp, CWinApp)</P>
<P align=justify>//{{AFX_MSG_MAP(CTttApp)</P>
<P align=justify>ON_COMMAND(ID_APP_ABOUT, OnAppAbout)</P>
<P align=justify>//}}AFX_MSG_MAP</P>
<P align=justify>END_MESSAGE_MAP()</P>
<P align=justify></P>
<P align=justify>头文件里是消息映射和消息处理函数的声明,实现文件里是消息映射的实现和消息处理函数的实现。它表示让应用程序对象处理命令消息ID_APP_ABOUT,消息处理函数是OnAppAbout。</P>
<P align=justify>为什么这样做之后就完成了一个消息映射?这些声明和实现到底作了些什么呢?接着,将讨论这些问题。</P>
<P align=justify></P>
<LI><B>在声明与实现的内部</B>
<p></LI></OL></OL></OL>
<OL>
<P align=justify>
<LI>DECLARE_MESSAGE_MAP宏:
<p>
<P align=justify>首先,看DECLARE_MESSAGE_MAP宏的内容:</P>
<P align=justify>#ifdef _AFXDLL</P>
<P align=justify>#define<B> DECLARE_MESSAGE_MAP</B>() \</P>
<P align=justify>private: \</P>
<P align=justify>static const AFX_MSGMAP_ENTRY _messageEntries[]; \</P>
<P align=justify>protected: \</P>
<P align=justify>static AFX_DATA const AFX_MSGMAP messageMap; \</P>
<P align=justify>static const AFX_MSGMAP* PASCAL _GetBaseMessageMap(); \</P>
<P align=justify>virtual const AFX_MSGMAP* GetMessageMap() const; \</P>
<P align=justify></P>
<P align=justify>#else</P>
<P align=justify>#define <B>DECLARE_MESSAGE_MAP</B>() \</P>
<P align=justify>private: \</P>
<P align=justify>static const AFX_MSGMAP_ENTRY _messageEntries[]; \</P>
<P align=justify>protected: \</P>
<P align=justify>static AFX_DATA const AFX_MSGMAP messageMap; \</P>
<P align=justify>virtual const AFX_MSGMAP* GetMessageMap() const; \</P>
<P align=justify></P>
<P align=justify>#endif</P>
<P align=justify>DECLARE_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。</P>
<P align=justify></P>
<LI>BEGIN_MESSAE_MAP宏
<p>
<P align=justify>然后,看BEGIN_MESSAE_MAP宏的内容:</P>
<P align=justify>#ifdef _AFXDLL</P>
<P align=justify>#define BEGIN_MESSAGE_MAP(theClass, baseClass) \</P>
<P align=justify>const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() \</P>
<P align=justify>{ return &amp;baseClass::messageMap; } \</P>
<P align=justify>const AFX_MSGMAP* theClass::GetMessageMap() const \</P>
<P align=justify>{ return &amp;theClass::messageMap; } \</P>
<P align=justify>AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \</P>
<P align=justify>{ &amp;theClass::_GetBaseMessageMap, &amp;theClass::_messageEntries[0] }; \</P>
<P align=justify>const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \</P>
<P align=justify>{ \</P>
<P align=justify></P>
<P align=justify>#else</P>
<P align=justify>#define BEGIN_MESSAGE_MAP(theClass, baseClass) \</P>
<P align=justify>const AFX_MSGMAP* theClass::GetMessageMap() const \</P>
<P align=justify>{ return &amp;theClass::messageMap; } \</P>
<P align=justify>AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \</P>
<P align=justify>{ &amp;baseClass::messageMap, &amp;theClass::_messageEntries[0] }; \</P>
<P align=justify>const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \</P>
<P align=justify>{ \</P>
<P align=justify></P>
<P align=justify>#endif</P>
<P align=justify></P>
<P align=justify>#define END_MESSAGE_MAP() \</P>
<P align=justify>{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \</P>
<P align=justify>}; \</P>
<P align=justify>对应地,BEGIN_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。END_MESSAGE_MAP相对简单,就只有一种定义。</P>
<P align=justify></P></LI></OL>
 楼主| 发表于 2006-12-8 10:01:25 | 显示全部楼层
<OL>
<LI>
<>ON_COMMAND宏 </P>
<p></LI></OL>
< align=justify>最后,看ON_COMMAND宏的内容:</P>
< align=justify>#define ON_COMMAND(id, memberFxn) \</P>
< align=justify>{\</P>
<DIR>
< align=justify>WM_COMMAND,\</P>
< align=justify>CN_COMMAND,\</P>
< align=justify>(WORD)id,\</P>
< align=justify>(WORD)id,\</P>
< align=justify>AfxSig_vv,\</P>
< align=justify>(AFX_PMSG)memberFxn\</P></DIR>
<P align=justify>};</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>消息映射声明的解释</B>
<p></LI></OL></OL></OL></OL>
<P align=justify>在清楚了有关宏的定义之后,现在来分析它们的作用和功能。</P>
<P align=justify>消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。</P>
<OL>
<P align=justify>
<LI>成员变量
<p></LI></OL>
<P align=justify>有两个成员变量被添加,第一个是_messageEntries,第二个是messageMap。</P>
<UL>
<P align=justify>
<LI>第一个成员变量的声明:
<p></LI></UL>
<DIR>
<P align=justify><B>AFX_MSGMAP_ENTRY _messageEntries[]</B></P></DIR>
<P align=justify>这是一个AFX_MSGMAP_ENTRY<B> </B>类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用AFX_MSGMAP_ENTRY结构来描述。</P>
<DIR>
<P align=justify>AFX_MSGMAP_ENTRY结构的定义如下:</P></DIR>
<P align=justify>struct AFX_MSGMAP_ENTRY</P>
<P align=justify>{</P>
<P align=justify>//Windows消息ID</P>
<P align=justify>UINT nMessage;</P>
<P align=justify>//控制消息的通知码</P>
<P align=justify>UINT nCode;</P>
<P align=justify>//Windows Control的ID</P>
<P align=justify>UINT nID;</P>
<P align=justify>//如果是一定范围的消息被映射,则nLastID指定其范围</P>
<P align=justify>UINT nLastID;</P>
<P align=justify></P>
<P align=justify>UINT nSig;//消息的动作标识</P>
<P align=justify>//响应消息时应执行的函数(routine to call (or special value))</P>
<P align=justify>AFX_PMSG pfn; </P>
<P align=justify>};</P>
<DIR>
<P align=justify>从上述结构可以看出,每条映射有两部分的内容:第一部分是关于消息ID的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。</P></DIR>
<P align=justify>在上述结构的六个域中,pfn是一个指向CCmdTarger成员函数的指针。函数指针的类型定义如下:</P>
<P align=justify>typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);</P>
<P align=justify>当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从CCmdTarge派生的,所以可以实现这样的转换。</P>
<P align=justify>nSig是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个不同的nSig。在消息分发时,MFC内部根据nSig把消息派发给对应的成员函数处理,实际上,就是根据nSig的值把pfn还原成相应类型的消息处理函数并执行它。</P>
<P align=justify></P>
<UL>
<P align=justify>
<LI>第二个成员变量的声明
<p></LI></UL>
<P align=justify><B>AFX_MSGMAP messageMap;</B></P>
<P align=justify>这是一个AFX_MSGMAP类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP结构的定义如下:</P>
<P align=justify>struct AFX_MSGMAP</P>
<P align=justify>{</P>
<P align=justify>//得到基类的消息映射入口地址的数据或者函数</P>
<P align=justify>#ifdef _AFXDLL</P>
<P align=justify>//pfnGetBaseMap指向_GetBaseMessageMap函数</P>
<P align=justify>const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();</P>
<P align=justify>#else</P>
<P align=justify>//pBaseMap保存基类消息映射入口_messageEntries的地址</P>
<P align=justify>const AFX_MSGMAP* pBaseMap;</P>
<P align=justify>#endif</P>
<P align=justify>//lpEntries保存消息映射入口_messageEntries的地址</P>
<P align=justify>const AFX_MSGMAP_ENTRY* lpEntries;</P>
<P align=justify>};</P>
<P align=justify>从上面的定义可以看出,通过messageMap可以得到类的消息映射数组_messageEntries和函数_GetBaseMessageMap的地址(不使用MFC DLL时,是基类消息映射数组的地址)。</P>
<P align=justify></P>
<OL>
<P align=justify>
<LI><B>成员函数</B>
<p></LI></OL>
<UL>
<P align=justify>
<LI>_GetBaseMessageMap()
<p></LI></UL>
<P align=justify>用来得到基类消息映射的函数。</P>
<P align=justify></P>
<UL>
<P align=justify>
<LI>GetMessageMap()
<p></LI></UL>
<P align=justify>用来得到自身消息映射的函数。</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>消息映射实现的解释</B>
<p></LI></OL></OL></OL></OL>
<P align=justify>消息映射实现的实质是初始化声明中定义的静态成员函数_messageEntries和messageMap,实现所声明的静态或虚拟函数GetMessageMap、_GetBaseMessageMap。</P>
<P align=justify>这样,在进入WinMain函数之前,每个可以响应消息的MFC类都生成了一个消息映射表,程序运行时通过查询该表判断是否需要响应某条消息。</P>
<OL>
<P align=justify>
<LI>对消息映射入口表(消息映射数组)的初始化
<p>
<P align=justify>如前所述,消息映射数组的元素是消息映射条目,条目的格式符合结构AFX_MESSAGE_ENTRY的描述。所以,要初始化消息映射数组,就必须使用符合该格式的数据来填充:如果指定当前类处理某个消息,则把和该消息有关的信息(四个)和消息处理函数的地址及原型组合成为一个消息映射条目,加入到消息映射数组中。</P>
<P align=justify>显然,这是一个繁琐的工作。为了简化操作,MFC根据消息的不同和消息处理方式的不同,把消息映射划分成若干类别,每一类的消息映射至少有一个共性:消息处理函数的原型相同。对每一类消息映射,MFC定义了一个宏来简化初始化消息数组的工作。例如,前文提到的ON_COMMAND宏用来映射命令消息,只要指定命令ID和消息处理函数即可,因为对这类命令消息映射条目,其他四个属性都是固定的。ON_COMMAND宏的初始化内容如下:</P>
<P align=justify>{WM_COMMAND,</P>
<P align=justify>CN_COMMAND,</P>
<P align=justify>(WORD)ID_APP_ABOUT,</P>
<P align=justify>(WORD)ID_APP_ABOUT,</P>
<P align=justify>AfxSig_vv,</P>
<P align=justify>(AFX_PMSG)OnAppAbout</P>
<P align=justify>}</P>
<P align=justify>这个消息映射条目的含义是:消息ID是ID_APP_ABOUT,OnAppAbout被转换成AFX_PMSG指针类型,AfxSig_vv是MFC预定义的枚举变量,用来标识OnAppAbout的函数类型为参数空(Void)、返回空(Void)。</P>
<P align=justify>在消息映射数组的最后,是宏END_MESSAGE_MAP的内容,它标识消息处理类的消息映射条目的终止。</P>
<P align=justify></P>
<LI>对messageMap的初始化
<p>
<P align=justify>如前所述,messageMap的类型是AFX_MESSMAP。</P>
<P align=justify>经过初始化,域lpEntries保存了消息映射数组_messageEntries的地址;如果动态链接到MFC DLL,则pfnGetBaseMap保存了_GetBaseMessageMap成员函数的地址;否则pBaseMap保存了基类的消息映射数组的地址。</P>
<P align=justify></P>
<LI>对函数的实现
<p></LI></OL>
<P align=justify>_GetBaseMessageMap()</P>
<P align=justify>它返回基类的成员变量messagMap(当使用MFC DLL时),使用该函数得到基类消息映射入口表。</P>
<P align=justify></P>
<P align=justify>GetMessageMap():</P>
<P align=justify>它返回成员变量messageMap,使用该函数得到自身消息映射入口表。</P>
<P align=justify></P>
<P align=justify>顺便说一下,消息映射类的基类CCmdTarget也实现了上述和消息映射相关的函数,不过,它的消息映射数组是空的。</P>
<P align=justify></P>
<P align=justify>既然消息映射宏方便了消息映射的实现,那么有必要详细的讨论消息映射宏。下一节,介绍消息映射宏的分类、用法和用途。</P>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>消息映射宏的种类</B>
<p></LI></OL></OL></OL>
<P align=justify>为了简化程序员的工作,MFC定义了一系列的消息映射宏和像AfxSig_vv这样的枚举变量,以及标准消息处理函数,并且具体地实现这些函数。这里主要讨论消息映射宏,常用的分为以下几类。</P>
<OL>
<P align=justify>
<LI>用于Windows消息的宏,前缀为“ON_WM_”。
<p>
<P align=justify>这样的宏不带参数,因为它对应的消息和消息处理函数的函数名称、函数原型是确定的。MFC提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows消息。</P>
<P align=justify>例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函数,消息映射条目的第一个成员nMessage指定为要处理的Windows消息的ID,第二个成员nCode指定为0。</P>
<P align=justify></P>
<LI>用于命令消息的宏ON_COMMAND
<p></LI></OL>
<P align=justify>这类宏带有参数,需要通过参数指定命令ID和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0)。消息处理函数的原型是void (void),不带参数,不返回值。</P>
<P align=justify>除了单条命令消息的映射,还有把一定范围的命令消息映射到一个消息处理函数的映射宏ON_COMMAND_RANGE。这类宏带有参数,需要指定命令ID的范围和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0),第三个成员nID和第四个成员nLastID指定了映射消息的起止范围。消息处理函数的原型是void (UINT),有一个UINT类型的参数,表示要处理的命令消息ID,不返回值。</P>
<P align=justify>(3)用于控制通知消息的宏</P>
<P align=justify>这类宏可能带有三个参数,如ON_CONTROL,就需要指定控制窗口ID,通知码和消息处理函数;也可能带有两个参数,如具体处理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,需要指定控制窗口ID和消息处理函数。</P>
<P align=justify>控制通知消息也被映射到WM_COMMAND上,也就是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (void),没有参数,不返回值。</P>
<P align=justify>还有一类宏处理通知消息ON_NOTIFY,它类似于ON_CONTROL,但是控制通知消息被映射到WM_NOTIFY。消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (NMHDR*, LRESULT*),参数1是NMHDR指针,参数2是LRESULT指针,用于返回结果,但函数不返回值。</P>
<P align=justify>对应地,还有把一定范围的控制子窗口的某个通知消息映射到一个消息处理函数的映射宏,这类宏包括ON__CONTROL_RANGE和ON_NOTIFY_RANGE。这类宏带有参数,需要指定控制子窗口ID的范围和通知消息,以及消息处理函数。</P>
<P align=justify>对于ON__CONTROL_RANGE,是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID等于指定了控制窗口ID的范围。消息处理函数的原型是void (UINT),参数表示要处理的通知消息是哪个ID的控制子窗口发送的,函数不返回值。</P>
<P align=justify>对于ON__NOTIFY_RANGE,消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID指定了控制窗口ID的范围。消息处理函数的原型是void (UINT, NMHDR*, LRESULT*),参数1表示要处理的通知消息是哪个ID的控制子窗口发送的,参数2是NMHDR指针,参数3是LRESULT指针,用于返回结果,但函数不返回值。</P>
<P align=justify>(4)用于用户界面接口状态更新的ON_UPDATE_COMMAND_UI宏</P>
<P align=justify>这类宏被映射到消息WM_COMMND上,带有两个参数,需要指定用户接口对象ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID都指定为用户接口对象ID。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,不返回值。</P>
<P align=justify>对应地,有更新一定ID范围的用户接口对象的宏ON_UPDATE_COMMAND_UI_RANGE,此宏带有三个参数,用于指定用户接口对象ID的范围和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID用于指定用户接口对象ID的范围。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,函数不返回值。之所以不用当前用户接口对象ID作为参数,是因为CCmdUI对象包含了有关信息。</P>
<P align=justify>(5)用于其他消息的宏</P>
<P align=justify>例如用于用户定义消息的ON_MESSAGE。这类宏带有参数,需要指定消息ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为消息ID,第二个成员nCode被指定为0,第三个成员nID和第四个成员也是0。消息处理的原型是LRESULT (WPARAM, LPARAM),参数1和参数2是消息参数wParam和lParam,返回LRESULT类型的值。</P>
<P align=justify>(6)扩展消息映射宏</P>
<P align=justify>很多普通消息映射宏都有对应的扩展消息映射宏,例如:ON_COMMAND对应的ON_COMMAND_EX,ON_ONTIFY对应的ON_ONTIFY_EX,等等。扩展宏除了具有普通宏的功能,还有特别的用途。关于扩展宏的具体讨论和分析,见4.4.3.2节。</P>
 楼主| 发表于 2006-12-8 10:01:48 | 显示全部楼层
< align=justify>作为一个总结,下表列出了这些常用的消息映射宏。</P>
< align=center>表4-1 常用的消息映射宏</P>
< align=left>
<TABLE cellSpacing=1 cellPadding=7 width=499 border=1>

<TR>
<TD vAlign=top width="41%">
< align=justify>消息映射宏 </P></TD>
<TD vAlign=top width="59%">
< align=justify>用途 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
< align=justify>ON_COMMAND </P></TD>
<TD vAlign=top width="59%">
< align=justify>把command message映射到相应的函数 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
< align=justify>ON_CONTROL </P></TD>
<TD vAlign=top width="59%">
< align=justify>把control notification message映射到相应的函数。MFC根据不同的控制消息,在此基础上定义了更具体的宏,这样用户在使用时就不需要指定通知代码ID,如ON_BN_CLICKED。 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
< align=justify>ON_MESSAGE </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把user-defined message.映射到相应的函数 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
<P align=justify>ON_REGISTERED_MESSAGE </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把registered user-defined message映射到相应的函数,实际上nMessage等于0x0C000,nSig等于宏的消息参数。nSig的真实值为Afxsig_lwl。 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
<P align=justify>ON_UPDATE_COMMAND_UI </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把user interface user update command message映射到相应的函数上。 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
<P align=justify>ON_COMMAND_RANGE </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把一定范围内的command IDs 映射到相应的函数上 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
<P align=justify>ON_UPDATE_COMMAND_UI_RANGE </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把一定范围内的user interface user update command message映射到相应的函数上 </P></TD></TR>
<TR>
<TD vAlign=top width="41%">
<P align=justify>ON_CONTROL_RANGE </P></TD>
<TD vAlign=top width="59%">
<P align=justify>把一定范围内的control notification message映射到相应的函数上 </P></TD></TR></TABLE>
<p>
<P align=justify></P>
<DIR>
<P align=justify>在表4-1中,宏ON_REGISTERED_MESSAGE的定义如下:</P></DIR>
<P align=justify>#define ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn) \</P>
<P align=justify>{ 0xC000, 0, 0, 0,\</P>
<P align=justify>(UINT)(UINT*)(&amp;nMessageVariable), \</P>
<P align=justify>/*implied 'AfxSig_lwl'*/ \</P>
<P align=justify>(AFX_PMSG)(AFX_PMSGW)(LRESULT\</P>
<P align=justify>(AFX_MSG_CALL CWnd::*)\</P>
<P align=justify>(WPARAM, LPARAM))&amp;memberFxn }</P>
<P align=justify>从上面的定义可以看出,实际上,该消息被映射到WM_COMMAND(0XC000),指定的registered消息ID存放在nSig域内,nSig的值在这样的映射条目下隐含地定为AfxSig_lwl。由于ID和正常的nSig域存放的值范围不同,所以MFC可以判断出是否是registered消息映射条目。如果是,则使用AfxSig_lwl把消息处理函数转换成参数1为Word、参数2为long、返回值为long的类型。</P>
<P align=justify>在介绍完了消息映射的内幕之后,应该讨论消息处理过程了。由于CCmdTarge的特殊性和重要性,在4.3节先对其作一个大略的介绍。</P>
<OL>
<OL>
<P align=justify>
<LI><B>CcmdTarget类</B>
<p></LI></OL></OL>
<P align=justify>除了CObject类外,还有一个非常重要的类CCmdTarget。所有响应消息或事件的类都从它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。</P>
<P align=justify>CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,而且,很大一部分是针对OLE设计的。在OLE应用中,CCmdTarget是MFC处理模块状态的重要环节,它起到了传递模块状态的作用:其构造函数获取当前模块状态,并保存在成员变量m_pModuleState里头。关于模块状态,在后面章节讲述。</P>
<P align=justify>CCmdTarget有两个与消息映射有密切关系的成员函数:DispatchCmdMsg和OnCmdMsg。</P>
<OL>
<P align=justify>
<LI>静态成员函数DispatchCmdMsg
<p>
<P align=justify>CCmdTarget的静态成员函数DispatchCmdMsg,用来分发Windows消息。此函数是MFC内部使用的,其原型如下:</P>
<P align=justify>static BOOL DispatchCmdMsg(</P>
<P align=justify>CCmdTarget* pTarget, </P>
<P align=justify>UINT nID,</P>
<P align=justify>int nCode,</P>
<P align=justify>AFX_PMSG pfn,</P>
<P align=justify>void* pExtra,</P>
<P align=justify>UINT nSig,</P>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P>
<P align=justify></P>
<P align=justify>关于此函数将在4.4.3.2章节命令消息的处理中作更详细的描述。</P>
<P align=justify></P>
<LI>虚拟函数OnCmdMsg
<p></LI></OL>
<P align=justify>CCmdTarget的虚拟函数OnCmdMsg,用来传递和发送消息、更新用户界面对象的状态,其原型如下:</P>
<P align=justify>OnCmdMsg(</P>
<DIR>
<P align=justify>UINT nID,</P>
<P align=justify>int nCode,</P>
<P align=justify>void* pExtra,</P>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P></DIR>
<P align=justify>框架的命令消息传递机制主要是通过该函数来实现的。其参数描述参见4.3.3.2章节DispacthCMdMessage的参数描述。</P>
<P align=justify>在本书中,命令目标指希望或者可能处理消息的对象;命令目标类指命令目标的类。</P>
<P align=justify>CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数(标准Windows消息不会送到这里处理)。</P>
<P align=justify>这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配指定的消息映射条目。匹配标准:命令消息ID相同,控制通知代码相同。因为GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。</P>
<P align=justify>如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;</P>
<P align=justify>如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;</P>
<P align=justify>如果最后没有找到,则返回FASLE。</P>
<P align=justify></P>
<P align=justify>每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时,要调用基类的被覆盖的OnCmdMsg。</P>
<P align=justify>在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。具体实现见后续章节。</P>
<P align=justify>必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。</P>
<P align=justify>本节对CCmdTarget的两个成员函数作一些讨论,是为了对MFC的消息处理有一个大致印象。后面4.4.3.2节和4.4.3.3节将作进一步的讨论。</P>
<OL>
<OL>
<P align=justify>
<LI><B>MFC窗口过程</B>
<p>
<P align=justify>前文曾经提到,所有的消息都送给窗口过程处理,MFC的所有窗口都使用同一窗口过程,消息或者直接由窗口过程调用相应的消息处理函数处理,或者按MFC命令消息派发路径送给指定的命令目标处理。</P>
<P align=justify>那么,MFC的窗口过程是什么?怎么处理标准Windows消息?怎么实现命令消息的派发?这些都将是下文要回答的问题。</P>
<OL>
<P align=justify>
<LI><B>MFC窗口过程的指定</B>
<p>
<P align=justify>从前面的讨论可知,每一个“窗口类”都有自己的窗口过程。正常情况下使用该“窗口类”创建的窗口都使用它的窗口过程。</P>
<P align=justify>MFC的窗口对象在创建HWND窗口时,也使用了已经注册的“窗口类”,这些“窗口类”或者使用应用程序提供的窗口过程,或者使用Windows提供的窗口过程(例如Windows控制窗口、对话框等)。那么,为什么说MFC创建的所有HWND窗口使用同一个窗口过程呢?</P>
<P align=justify>在MFC中,的确所有的窗口都使用同一个窗口过程:AfxWndProc或AfxWndProcBase(如果定义了_AFXDLL)。它们的原型如下:</P>
<P align=justify>LRESULT CALLBACK</P>
<P align=justify>AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)</P>
<P align=justify></P>
<P align=justify>LRESULT CALLBACK</P>
<P align=justify>AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)</P>
<P align=justify>这两个函数的原型都如4.1.1节描述的窗口过程一样。</P>
<P align=justify>如果动态链接到MFC DLL(定义了_AFXDLL),则AfxWndProcBase被用作窗口过程,否则AfxWndProc被用作窗口过程。AfxWndProcBase首先使用宏AFX_MANAGE_STATE设置正确的模块状态,然后调用AfxWndProc。</P>
<P align=justify></P>
<P align=justify>下面,假设不使用MFC DLL,讨论MFC如何使用AfxWndProc取代各个窗口的原窗口过程。</P>
<P align=justify>窗口过程的取代发生在窗口创建的过程时,使用了子类化(Subclass)的方法。所以,从窗口的创建过程来考察取代过程。从前面可以知道,窗口创建最终是通过调用CWnd::CreateEx函数完成的,分析该函数的流程,如图4-1所示。</P>
<P align=justify><IMG src="http://www.vczx.com/tutorial/mfc/image113.gif"></P>
<P align=justify>图4-1中的CREATESTRUCT结构类型的变量cs包含了传递给窗口过程的初始化参数。CREATESTRUCT结构描述了创建窗口所需要的信息,定义如下:</P>
<P align=justify>typedef struct tagCREATESTRUCT {</P>
<P align=justify>LPVOID lpCreateParams; //用来创建窗口的数据</P>
<P align=justify>HANDLE hInstance; //创建窗口的实例</P>
<P align=justify>HMENU hMenu; //窗口菜单</P>
<P align=justify>HWND hwndParent; //父窗口</P>
<P align=justify>int cy; //高度</P>
<P align=justify>int cx; //宽度</P>
<P align=justify>int y; //原点Y坐标</P>
<P align=justify>int x;//原点X坐标</P>
<P align=justify>LONG style; //窗口风格</P>
<P align=justify>LPCSTR lpszName; //窗口名</P>
<P align=justify>LPCSTR lpszClass; //窗口类</P>
<P align=justify>DWORD dwExStyle; //窗口扩展风格</P>
<P align=justify>} CREATESTRUCT;</P>
<P align=justify>cs表示的创建参数可以在创建窗口之前被程序员修改,程序员可以覆盖当前窗口类的虚拟成员函数PreCreateWindow,通过该函数来修改cs的style域,改变窗口风格。这里cs的主要作用是保存创建窗口的各种信息,::CreateWindowEx函数使用cs的各个域作为参数来创建窗口,关于该函数见2.2.2节。</P>
<P align=justify>在创建窗口之前,创建了一个WH_CBT类型的钩子(Hook)。这样,创建窗口时所有的消息都会被钩子过程函数_AfxCbtFilterHook截获。</P>
<P align=justify>AfxCbtFilterHook函数首先检查是不是希望处理的Hook──HCBT_CREATEWND。如果是,则先把MFC窗口对象(该对象必须已经创建了)和刚刚创建的Windows窗口对象捆绑在一起,建立它们之间的映射(见后面模块-线程状态);然后,调用::SetWindowLong设置窗口过程为AfxWndProc,并保存原窗口过程在窗口类成员变量m_pfnSuper中,这样形成一个窗口过程链。需要的时候,原窗口过程地址可以通过窗口类成员函数GetSuperWndProcAddr得到。</P>
<P align=justify>这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息,还是非队列消息,都送到AfxWndProc窗口过程来处理(如果使用MFC DLL,则AfxWndProcBase被调用,然后是AfxWndProc)。经过消息分发之后没有被处理的消息,将送给原窗口过程处理。</P>
<P align=justify>最后,有一点可能需要解释:为什么不直接指定窗口过程为AfxWndProc,而要这么大费周折呢?这是因为原窗口过程(“窗口类”指定的窗口过程)常常是必要的,是不可缺少的。</P>
<P align=justify>接下来,讨论AfxWndProc窗口过程如何使用消息映射数据实现消息映射。Windows消息和命令消息的处理不一样,前者没有消息分发的过程。</P>
<P align=justify></P>
<LI><B>对Windows消息的接收和处理</B>
<p>
<P align=justify>Windows消息送给AfxWndProc窗口过程之后,AfxWndProc得到HWND窗口对应的MFC窗口对象,然后,搜索该MFC窗口对象和其基类的消息映射数组,判定它们是否处理当前消息,如果是则调用对应的消息处理函数,否则,进行缺省处理。</P>
<P align=justify>下面,以一个应用程序的视窗口创建时,对WM_CREATE消息的处理为例,详细地讨论Windows消息的分发过程。</P>
<P align=justify>用第一章的例子,类CTview要处理WM_CREATE消息,使用ClassWizard加入消息处理函数CTview::OnCreate。下面,看这个函数怎么被调用:</P>
<P align=justify>视窗口最终调用::CreateEx函数来创建。由Windows系统发送WM_CREATE消息给视的窗口过程AfxWndProc,参数1是创建的视窗口的句柄,参数2是消息ID(WM_CREATE),参数3、4是消息参数。图4-2描述了其余的处理过程。图中函数的类属限制并非源码中所具有的,而是根据处理过程得出的判断。例如,“CWnd::WindowProc”表示CWnd类的虚拟函数WindowProc被调用,并不一定当前对象是CWnd类的实例,事实上,它是CWnd派生类CTview类的实例;而“CTview::OnCreate”表示CTview的消息处理函数OnCreate被调用。下面描述每一步的详细处理。</P>
<P align=justify><IMG src="http://forum.exceedu.com/forum/"></P></LI></OL></LI></OL></OL>
 楼主| 发表于 2006-12-8 10:02:09 | 显示全部楼层
<OL>
< align=justify>
<LI><B>从窗口过程到消息映射</B>
<p></LI></OL>
< align=justify>首先,分析AfxWndProc窗口过程函数。</P>
<UL>
< align=justify>
<LI>AfxWndProc的原型如下:
<p></LI></UL>
< align=justify>LRESULT AfxWndProc(HWND hWnd, </P>
<DIR>
< align=justify>UINT nMsg, WPARAM wParam, LPARAM lParam)</P></DIR>
< align=justify>如果收到的消息nMsg不是WM_QUERYAFXWNDPROC(该消息被MFC内部用来确认窗口过程是否使用AfxWndProc),则从hWnd得到对应的MFC Windows对象(该对象必须已存在,是永久性&ltermanent&gt;对象)指针pWnd。pWnd所指的MFC窗口对象将负责完成消息的处理。这里,pWnd所指示的对象是MFC视窗口对象,即CTview对象。</P>
< align=justify>然后,把pWnd和AfxWndProc接受的四个参数传递给函数AfxCallWndProc执行。</P>
< align=justify></P>
<UL>
< align=justify>
<LI>AfxCallWndProc原型如下:
<p></LI></UL>
<P align=justify>LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, </P>
<DIR>
<P align=justify>UINT nMsg, WPARAM wParam = 0, LPARAM lParam = 0)</P></DIR>
<P align=justify>MFC使用AfxCallWndProc函数把消息送给CWnd类或其派生类的对象。该函数主要是把消息和消息参数(nMsg、wParam、lParam)传递给MFC窗口对象的成员函数WindowProc(pWnd-&gt;WindowProc)作进一步处理。如果是WM_INITDIALOG消息,则在调用WindowProc前后要作一些处理。</P>
<P align=justify></P>
<P align=justify>WindowProc的函数原型如下:</P>
<P align=justify>LRESULT CWnd::WindowProc(UINT message, </P>
<DIR>
<P align=justify>WPARAM wParam, LPARAM lParam)</P></DIR>
<P align=justify>这是一个虚拟函数,程序员可以在CWnd的派生类中覆盖它,改变MFC分发消息的方式。例如,MFC的CControlBar就覆盖了WindowProc,对某些消息作了自己的特别处理,其他消息处理由基类的WindowProc函数完成。</P>
<P align=justify>但是在当前例子中,当前对象的类CTview没有覆盖该函数,所以CWnd的WindowProc被调用。</P>
<P align=justify>这个函数把下一步的工作交给OnWndMsg函数来处理。如果OnWndMsg没有处理,则交给DefWindowProc来处理。</P>
<P align=justify>OnWndMsg和DefWindowProc都是CWnd类的虚拟函数。</P>
<p>
<UL>
<P align=justify>
<LI>OnWndMsg的原型如下:
<p></LI></UL>
<P align=justify>BOOL CWnd::OnWndMsg( UINT message, </P>
<DIR>
<P align=justify>WPARAM wParam, LPARAM lParam,RESULT*pResult );</P></DIR>
<P>该函数是虚拟函数。</P>
<P align=justify>和WindowProc一样,由于当前对象的类CTview没有覆盖该函数,所以CWnd的OnWndMsg被调用。</P>
<P>在CWnd中,MFC使用OnWndMsg来分别处理各类消息:</P>
<P>如果是WM_COMMAND消息,交给OnCommand处理;然后返回。</P>
<P>如果是WM_NOTIFY消息,交给OnNotify处理;然后返回。</P>
<P>如果是WM_ACTIVATE消息,先交给_AfxHandleActivate处理(后面5.3.3.7节会解释它的处理),再继续下面的处理。</P>
<P>如果是WM_SETCURSOR消息,先交给_AfxHandleSetCursor处理;然后返回。</P>
<P>如果是其他的Windows消息(包括WM_ACTIVATE),则</P>
<DIR>
<DIR>
<P>首先在消息缓冲池进行消息匹配,</P>
<P>若匹配成功,则调用相应的消息处理函数;</P>
<DIR>
<DIR>
<P>若不成功,则在消息目标的消息映射数组中进行查找匹配,看它是否处理当前消息。这里,消息目标即CTview对象。</P>
<DIR>
<P>如果消息目标处理了该消息,则会匹配到消息处理函数,调用它进行处理;</P>
<P>否则,该消息没有被应用程序处理,OnWndMsg返回FALSE。</P>
<P align=justify></P></DIR></DIR></DIR></DIR></DIR>
<P align=justify>关于Windows消息和消息处理函数的匹配,见下一节。</P>
<P align=justify></P>
<P align=justify>缺省处理函数DefWindowProc将在讨论对话框等的实现时具体分析。</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>Windows消息的查找和匹配</B>
<p>
<P>CWnd或者派生类的对象调用OnWndMsg搜索本对象或者基类的消息映射数组,寻找当前消息的消息处理函数。如果当前对象或者基类处理了当前消息,则必定在其中一个类的消息映射数组中匹配到当前消息的处理函数。</P>
<P>消息匹配是一个比较耗时的任务,为了提高效率,MFC设计了一个消息缓冲池,把要处理的消息和匹配到的消息映射条目(条目包含了消息处理函数的地址)以及进行消息处理的当前类等信息构成一条缓冲信息,放到缓冲池中。如果以后又有同样的消息需要同一个类处理,则直接从缓冲池查找到对应的消息映射条目就可以了。</P>
<P>MFC用哈希查找来查询消息映射缓冲池。消息缓冲池相当于一个哈希表,它是应用程序的一个全局变量,可以放512条最新用到的消息映射条目的缓冲信息,每一条缓冲信息是哈希表的一个入口。</P>
<P>采用AFX_MSG_CACHE结构描述每条缓冲信息,其定义如下:</P>
<P align=justify>struct AFX_MSG_CACHE</P>
<P align=justify>{</P>
<P align=justify>UINT nMsg;</P>
<P align=justify>const AFX_MSGMAP_ENTRY* lpEntry;</P>
<P align=justify>const AFX_MSGMAP* pMessageMap;</P>
<P align=justify>};</P>
<P>nMsg存放消息ID,每个哈希表入口有不同的nMsg。</P>
<P>lpEnty存放和消息ID匹配的消息映射条目的地址,它可能是this所指对象的类的映射条目,也可能是这个类的某个基类的映射条目,也可能是空。</P>
<P>pMessageMap存放消息处理函数匹配成功时进行消息处理的当前类(this所指对象的类)的静态成员变量messageMap的地址,它唯一的标识了一个类(每个类的messageMap变量都不一样)。</P>
<P>this所指对象是一个CWnd或其派生类的实例,是正在处理消息的MFC窗口对象。</P>
<P>哈希查找:使用消息ID的值作为关键值进行哈希查找,如果成功,即可从lpEntry获得消息映射条目的地址,从而得到消息处理函数及其原型。</P>
<P>如何判断是否成功匹配呢?有两条标准:</P>
<P align=justify>第一,当前要处理的消息message在哈希表(缓冲池)中有入口;第二,当前窗口对象(this所指对象)的类的静态变量messageMap的地址应该等于本条缓冲信息的pMessagMap。MFC通过虚拟函数GetMessagMap得到messageMap的地址。</P>
<P align=justify>如果在消息缓冲池中没有找到匹配,则搜索当前对象的消息映射数组,看是否有合适的消息处理函数。</P>
<P align=justify>如果匹配到一个消息处理函数,则把匹配结果加入到消息缓冲池中,即填写该条消息对应的哈希表入口:</P>
<P align=justify>nMsg=message; </P>
<P align=justify>pMessageMap=this-&gt;GetMessageMap; </P>
<P align=justify>lpEntry=查找结果</P>
<P align=justify>然后,调用匹配到的消息处理函数。否则(没有找到),使用_GetBaseMessageMap得到基类的消息映射数组,查找和匹配;直到匹配成功或搜寻了所有的基类(到CCmdTarget)为止。</P>
<P align=justify>如果最后没有找到,则也把该条消息的匹配结果加入到缓冲池中。和匹配成功不同的是:指定lpEntry为空。这样OnWndMsg返回,把控制权返还给AfxCallWndProc函数,AfxCallWndProc将继续调用DefWndProc进行缺省处理。</P>
<P align=justify></P>
<P align=justify>消息映射数组的搜索在CCmdTarget::OnCmdMsg函数中也用到了,而且算法相同。为了提高速度,MFC把和消息映射数组条目逐一比较、匹配的函数AfxFindMessageEntry用汇编书写。</P>
<P align=justify>const AFX_MSGMAP_ENTRY* AFXAPI</P>
<P align=justify><I>AfxFindMessageEntry</I>(const AFX_MSGMAP_ENTRY* lpEntry,</P>
<P align=justify>UINT nMsg, UINT nCode, UINT nID)</P>
<P>第一个参数是要搜索的映射数组的入口;第二个参数是Windows消息标识;第三个参数是控制通知消息标识;第四个参数是命令消息标识。</P>
<P>对Windows消息来说,nMsg是每条消息不同的,nID和nCode为0。</P>
<P>对命令消息来说,nMsg固定为WM_COMMAND,nID是每条消息不同,nCode都是CN_COMMAND(定义为0)。</P>
<P>对控制通知消息来说,nMsg固定为WM_COMMAND或者WM_NOTIFY,nID和nCode是每条消息不同。</P>
<P>对于Register消息,nMsg指定为0XC000,nID和nCode为0。在使用函数AfxFindMessageEntry得到匹配结果之后,还必须判断nSig是否等于message,只有相等才调用对应的消息处理函数。</P>
<P align=justify></P>
<LI><B>Windows消息处理函数的调用</B>
<p>
<P align=justify>对一个Windows消息,匹配到了一个消息映射条目之后,将调用映射条目所指示的消息处理函数。</P>
<P align=justify>调用处理函数的过程就是转换映射条目的pfn指针为适当的函数类型并执行它:MFC定义了一个成员函数指针mmf,首先把消息处理函数的地址赋值给该函数指针,然后根据消息映射条目的nSig值转换指针的类型。但是,要给函数指针mmf赋值,必须使该指针可以指向所有的消息处理函数,为此则该指针的类型是所有类型的消息处理函数指针的联合体。</P>
<P align=justify>对上述过程,MFC的实现大略如下:</P>
<P align=justify>union MessageMapFunctions mmf;</P>
<P align=justify>mmf.pfn = lpEntry-&gt;pfn;</P>
<P align=justify>swithc (value_of_nsig){</P>
<P align=justify>…</P>
<P align=justify>case AfxSig_is: //OnCreate就是该类型</P>
<P align=justify>lResult = (this-&gt;*mmf.pfn_is)((LPTSTR)lParam);</P>
<P align=justify>break;</P>
<P align=justify>…</P>
<P align=justify>default:</P>
<P align=justify>ASSERT(FALSE); break;</P>
<P align=justify>}</P>
<P align=justify>…</P>
<P align=justify>LDispatchRegistered: // 处理registered windows messages</P>
<P align=justify>ASSERT(message &gt;= 0xC000);</P>
<P align=justify>mmf.pfn = lpEntry-&gt;pfn;</P>
<P align=justify>lResult = (this-&gt;*mmf.pfn_lwl)(wParam, lParam);</P>
<P align=justify>…</P>
<P>如果消息处理函数有返回值,则返回该结果,否则,返回TRUE。</P>
<P align=justify>对于图4-1所示的例子,nSig等于AfxSig_is,所以将执行语句</P>
<P align=justify>(this-&gt;*mmf.pfn_is)((LPTSTR)lParam)</P>
<P align=justify>也就是对CTview::OnCreate的调用。</P>
<P>顺便指出,对于Registered窗口消息,消息处理函数都是同一原型,所以都被转换成lwl型(关于Registered窗口消息的映射,见4.4.2节)。</P>
<P>综上所述,标准Windwos消息和应用程序消息中的Registered消息,由窗口过程直接调用相应的处理函数处理:</P>
<P>如果某个类型的窗口(C++类)处理了某条消息(覆盖了CWnd或直接基类的处理函数),则对应的HWND窗口(Winodws window)收到该消息时就调用该覆盖函数来处理;如果该类窗口没有处理该消息,则调用实现该处理函数最直接的基类(在C++的类层次上接近该类)来处理,上述例子中如果CTview不处理WM_CREATE消息,则调用上一层的CWnd::OnCreate处理;</P>
<P>如果基类都不处理该消息,则调用DefWndProc来处理。</P>
<P align=justify></P>
<LI><B>消息映射机制完成虚拟函数功能的原理</B>
<p></LI></OL></OL></OL></OL>
<P>综合对Windows消息的处理来看,MFC使用消息映射机制完成了C++虚拟函数的功能。这主要基于以下几点:</P>
<UL>
<P align=justify>
<LI>所有处理消息的类从CCmdTarget派生。
<p>
<P align=justify></P>
<LI>使用静态成员变量_messageEntries数组存放消息映射条目,使用静态成员变量messageMap来唯一地区别和得到类的消息映射。
<p>
<P align=justify></P>
<LI>通过GetMessage虚拟函数来获取当前对象的类的messageMap变量,进而得到消息映射入口。
<p>
<P align=justify></P>
<LI>按照先底层,后基层的顺序在类的消息映射数组中搜索消息处理函数。基于这样的机制,一般在覆盖基类的消息处理函数时,应该调用基类的同名函数。
<p></LI></UL>
<P align=justify>以上论断适合于MFC其他消息处理机制,如对命令消息的处理等。不同的是其他消息处理有一个命令派发/分发的过程。</P>
<P align=justify>下一节,讨论命令消息的接受和处理。</P>
 楼主| 发表于 2006-12-8 10:04:27 | 显示全部楼层
<OL>
<OL>
<OL>
< align=justify>
<LI><B>对命令消息的接收和处理</B>
<>
<OL>
< align=justify>
<LI><B>MFC标准命令消息的发送</B>
<p></LI></OL></LI></OL></OL></OL>
< align=justify>在SDI或者MDI应用程序中,命令消息由用户界面对象(如菜单、工具条等)产生,然后送给主边框窗口。主边框窗口使用标准MFC窗口过程处理命令消息。窗口过程把命令传递给MFC主边框窗口对象,开始命令消息的分发。MFC边框窗口类CFrameWnd提供了消息分发的能力。</P>
< align=justify>下面,还是通过一个例子来说明命令消息的处理过程。</P>
< align=justify>使用AppWizard产生一个单文档应用程序t。从help菜单选择“About”,就会弹出一个ABOUT对话框。下面,讨论从命令消息的发出到对话框弹出的过程。</P>
< align=justify> </P>
< align=justify>首先,选择“ About”菜单项的动作导致一个Windows命令消息ID_APP_ABOUT的产生。Windows系统发送该命令消息到边框窗口,导致它的窗口过程AfxWndProc被调用,参数1是边框窗口的句柄,参数2是消息ID(即WM_COMMAND),参数3、4是消息参数,参数3的值是ID_APP_ABOUT。接着的系列调用如图4-3所示。</P><IMG src="http://forum.exceedu.com/forum/" align=left>
< align=justify></P>
< align=justify>下面分别讲述每一层所调用的函数。</P>
<P align=justify>前4步同对Windows消息的处理。这里接受消息的HWND窗口是主边框窗口,因此,AfxWndProc根据HWND句柄得到的MFC窗口对象是MFC边框窗口对象。</P>
<P align=justify>在4.2.2节谈到,如果CWnd::OnWndMsg判断要处理的消息是命令消息(WM_COMMAND),就调用OnCommand进一步处理。由于OnCommand是虚拟函数,当前MFC窗口对象是边框窗口对象,它的类从CFrameWnd类导出,没有覆盖CWnd的虚拟函数OnCommand,而CFrameWnd覆盖了CWnd的OnCommand,所以,CFrameWnd的OnCommand被调用。换句话说,CFrameWnd的OnCommand被调用是动态约束的结果。接着介绍的本例子的有关调用,也是通过动态约束而实际发生的函数调用。</P>
<P align=justify>接着的有关调用,将不进行为什么调用某个类的虚拟或者消息处理函数的分析。</P>
<P align=justify>(1)CFrameWnd的OnCommand函数</P>
<P align=justify>BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)</P>
<P align=justify>参数wParam的低阶word存放了菜单命令nID或控制子窗口ID;如果消息来自控制窗口,高阶word存放了控制通知消息;如果消息来自加速键,高阶word值为1;如果消息来自菜单,高阶word值为0。</P>
<P align=justify>如果是通知消息,参数lParam存放了控制窗口的句柄hWndCtrl,其他情况下lParam是0。</P>
<P align=justify>在这个例子里,低阶word是ID_APP_ABOUT,高阶word是1;lParam是0。</P>
<P align=justify>MFC对CFrameWnd的缺省实现主要是获得一个机会来检查程序是否运行在HELP状态,需要执行上下文帮助,如果不需要,则调用基类的CWnd::OnCommand实现正常的命令消息发送。</P>
<P align=justify>(2)CWnd的OnCommand函数</P>
<P align=justify>BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)</P>
<P align=justify>它按一定的顺序处理命令或者通知消息,如果发送成功,返回TRUE,否则,FALSE。处理顺序如下:</P>
<P align=justify>如果是命令消息,则调用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &amp;state, NULL)测试nID命令是否已经被禁止,如果这样,返回FALSE;否则,调用OnCmdMsg进行命令发送。关于CN_UPDATE_COMMAND_UI通知消息,见后面用户界面状态的更新处理。</P>
<P align=justify>如果是控制通知消息,则先用ReflectLastMsg反射通知消息到子窗口。如果子窗口处理了该消息,则返回TRUE;否则,调用OnCmdMsg进行命令发送。关于通知消息的反射见后面4.4.4.3节。OnCommand给OnCmdMsg传递四个参数:nID,即命令消息ID;nCode,如果是通知消息则为通知代码,如果是命令消息则为NC_COMMAND(即0);其余两个参数为空。</P>
<P align=justify>(3)CFrameWnd的OnCmdMsg函数</P>
<P align=justify>BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,</P>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P>
<P align=justify>参数1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),则参数2表示通知代码,如果是命令消息,参数2是0;如果是WM_NOTIFY,参数3包含了一些额外的信息;参数4在正常消息处理中应该是空。</P>
<P align=justify>在这个例子里,参数1是命令ID,参数2为0,参数3空。</P>
<P align=justify>OnCmdMsg是虚拟函数,CFrameWnd覆盖了该函数,当前对象(this所指)是MFC单文档的边框窗口对象。故CFrameWnd的OnCmdMsg被调用。CFrameWnd::OnCmdMsg在MFC消息发送中占有非常重要的地位,MFC对该函数的缺省实现确定了MFC的标准命令发送路径:</P>
<OL>
<P align=justify>
<LI>送给活动(Active)视处理,调用活动视的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTview及其基类的消息映射数组,试图得到相应的处理函数。
<P>
<P align=justify></P>
<LI>如果视对象自己不处理,则视得到和它关联的文档,调用关联文档的OnCmdMsg。由于当前对象是MFC视对象,所以,OnCmdMsg将搜索CTdoc及其基类的消息映射数组,试图得到相应的处理函数。
<P>
<P align=justify></P>
<LI>如果文档对象不处理,则它得到管理文档的文档模板对象,调用文档模板的OnCmdMsg。由于当前对象是MFC文档模板对象,所以,OnCmdMsg将搜索文档模板类及其基类的消息映射数组,试图得到相应的处理函数。
<P>
<P align=justify></P>
<LI>如果文档模板不处理,则把没有处理的信息逐级返回:文档模板告诉文档对象,文档对象告诉视对象,视对象告诉边框窗口对象。最后,边框窗口得知,视、文档、文档模板都没有处理消息。
<P>
<P align=justify></P>
<LI>CFrameWnd的OnCmdMsg继续调用<I>CWnd::OnCmdMsg</I>(斜体表示有类属限制)来处理消息。由于CWnd没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC边框窗口对象,所以OnCmdMsg函数将搜索CMainFrame类及其所有基类的消息映射数组,试图得到相应的处理函数。CWnd没有实现OnCmdMsg却指定要执行其OnCmdMsg函数,可能是为了以后MFC给CWnd实现了OnCmdMsg之后其他代码不用改变。
<P>
<P align=justify>这一步是边框窗口自己尝试处理消息。</P>
<P align=justify></P>
<LI value=2>如果边框窗口对象不处理,则送给应用程序对象处理。调用CTApp的OnCmdMsg,由于实际上CTApp及其基类CWinApp没有覆盖OnCmdMsg,故实际上调用了函数CCmdTarget::OnCmdMsg。由于当前对象是MFC应用程序对象,所以OnCmdMsg函数将搜索CTApp类及其所有基类的的消息映射入口数组,试图得到相应的处理函数
<P>
<P align=justify></P>
<LI>如果应用程序对象不处理,则返回FALSE,表明没有命令目标处理当前的命令消息。这样,函数逐级别返回,OnCmdMsg告诉OnCommand消息没有被处理,OnCommand告诉OnWndMsg消息没有被处理,OnWndMsg告诉WindowProc消息没有被处理,于是WindowProc调用DefWindowProc进行缺省处理。
<p></LI></OL>
<P align=justify></P>
<P align=justify>本例子在第六步中,应用程序对ID_APP_ABOUT消息作了处理。它找到处理函数CTApp::OnAbout,使用DispatchCmdMsg派发消息给该函数处理。</P>
<P align=justify>如果是MDI边框窗口,标准发送路径还有一个环节,该环节和第二、三、四步所涉及的OnCmdMsg函数,将在下两节再次具体分析。</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>命令消息的派发和消息的多次处理</B>
<p></LI></OL></OL></OL></OL>
<OL>
<P align=justify>
<LI>命令消息的派发
<P>
<P align=justify>如前3.1所述,CCmdTarget的静态成员函数DispatchCmdMsg用来派发命令消息给指定的命令目标的消息处理函数。</P>
<P align=justify>static BOOL <I>DispatchCmdMsg</I>(CCmdTarget* pTarget,</P>
<P align=justify>UINT nID, int nCode,</P>
<P align=justify>AFX_PMSG pfn, void* pExtra, UINT nSig,</P>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P>
<P align=justify>前面在讲CCmdTarget时,提到了该函数。这里讲述它的实现:</P>
<P align=justify>第一个参数指向处理消息的对象;第二个参数是命令ID;第三个是通知消息等;第四个是消息处理函数地址;第五个参数用于存放一些有用的信息,根据nCode的值表示不同的意义,例如当消息是WM_NOFITY,指向一个NMHDR结构(关于WM_NOTIFY,参见4.4.4.2节通知消息的处理);第六个参数标识消息处理函数原型;第七个参数是一个指针,指向AFX_CMDHANDLERINFO结构。前六个参数(除了第五个外)都是向函数传递信息,第五个和第七个参数是双向的,既向函数传递信息,也可以向调用者返回信息。</P>
<P align=justify>关于AFX_CMDHANDLERINFO结构:</P>
<P align=justify>struct AFX_CMDHANDLERINFO</P>
<P align=justify>{</P>
<P align=justify>CCmdTarget* pTarget;</P>
<P align=justify>void (AFX_MSG_CALL CCmdTarget::*pmf)(void);</P>
<P align=justify>};</P>
<P align=justify>第一个成员是一个指向命令目标对象的指针,第二个成员是一个指向CCmdTarget成员函数的指针。</P>
<P align=justify></P>
<P align=justify>该函数的实现流程可以如下描述:</P>
<P align=justify>首先,它检查参数pHandlerInfo是否空,如果不空,则用pTarget和pfn填写其指向的结构,返回TRUE;通常消息处理时传递来的pHandlerInfo空,而在使用OnCmdMsg来测试某个对象是否处理某条命令时,传递一个非空的pHandlerInfo指针。若返回TRUE,则表示可以处理那条消息。</P>
<P align=justify>如果pHandlerInfo空,则进行消息处理函数的调用。它根据参数nSig的值,把参数pfn的类型转换为要调用的消息处理函数的类型。这种指针转换技术和前面讲述的Windows消息的处理是一样的。</P>
<P align=justify></P>
<LI>消息的多次处理
<p></LI></OL>
<P align=justify>如果消息处理函数不返回值,则DispatchCmdMsg返回TRUE;否则,DispatchCmdMsg返回消息处理函数的返回值。这个返回值沿着消息发送相反的路径逐级向上传递,使得各个环节的OnCmdMsg和OnCommand得到返回的处理结果:TRUE或者FALSE,即成功或者失败。</P>
<P align=justify>这样就产生了一个问题,如果消息处理函数有意返回一个FALSE,那么不就传递了一个错误的信息?例如,OnCmdMsg函数得到FALSE返回值,就认为消息没有被处理,它将继续发送消息到下一环节。的确是这样的,但是这不是MFC的漏洞,而是有意这么设计的,用来处理一些特别的消息映射宏,实现同一个消息的多次处理。</P>
<P align=justify>通常的命令或者通知消息是没有返回值的(见4.4.2节的消息映射宏),仅仅一些特殊的消息处理函数具有返回值,这类消息的消息处理函数是使用扩展消息映射宏映射的,例如:</P>
<P align=justify>ON_COMMAND对应的ON_COMMAND_EX</P>
<P align=justify>扩展映射宏和对应的普通映射宏的参数个数相同,含义一样。但是扩展映射宏的消息处理函数的原型和对应的普通映射宏相比,有两个不同之处:一是多了一个UINT类型的参数,另外就是有返回值(返回BOOL类型)。回顾4.4.2章节,范围映射宏ON_COMMAND_RANGE的消息处理函数也有一个这样的参数,该参数在两处的含义是一样的,例如:命令消息扩展映射宏ON_COMMAND_EX定义的消息处理函数解释该参数是当前要处理的命令消息ID。有返回值的意义在于:如果扩展映射宏的消息处理函数返回FALSE,则导致当前消息被发送给消息路径上的下一个消息目标处理。</P>
<P align=justify>综合来看,ON_COMMAND_EX宏有两个功能:</P>
<P align=justify>一是可以把多个命令消息指定给一个消息处理函数处理。这类似于ON_COMMAND_RANGE宏的作用。不过,这里的多条消息的命令ID或者控制子窗口ID可以不连续,每条消息都需要一个ON_COMMAND_EX宏。</P>
<P align=justify>二是可以让几个消息目标处理同一个命令或者通知或者反射消息。如果消息发送路径上较前的命令目标不处理消息或者处理消息后返回FALSE,则下一个命令目标将继续处理该消息。</P>
<P align=justify></P>
<P align=justify>对于通知消息、反射消息,它们也有扩展映射宏,而且上述论断也适合于它们。例如:</P>
<P align=justify>ON_NOTIFY对应的ON_NOTIFY_EX</P>
<P align=justify>ON_CONTROL对应的ON_CONTROL_EX</P>
<P align=justify>ON_CONTROL_REFLECT对应的ON_CONTROL_REFLECT_EX</P>
<P align=justify>等等。</P>
<P align=justify></P>
<P align=justify>范围消息映射宏也有对应的扩展映射宏,例如:</P>
<P align=justify>ON_NOTIFY_RANGE对应的ON_NOTIFY_EX_RANGE</P>
<P align=justify>ON_COMMAND_RANGE对应的ON_COMMAND_EX_RANGE</P>
<P align=justify>使用这些宏的目的在于利用扩展宏的第二个功能:实现消息的多次处理。</P>
<P align=justify></P>
<P align=justify>关于扩展消息映射宏的例子,参见13.2..4.4节和13.2.4.6节。</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>一些消息处理类的OnCmdMsg的实现</B>
<p></LI></OL></OL></OL></OL>
<P align=justify>从以上论述知道,OnCmdMsg虚拟函数在MFC命令消息的发送中扮演了重要的角色,CFrameWnd的OnCmdMsg实现了MFC的标准命令消息发送路径。</P>
<P align=justify>那么,就产生一个问题:如果命令消息不送给边框窗口对象,那么就不会有按标准命令发送路径发送消息的过程?答案是肯定的。例如一个菜单被一个对话框窗口所拥有,那么,菜单命令将送给MFC对话框窗口对象处理,而不是MFC边框窗口处理,当然不会和CFrameWnd的处理流程相同。</P>
<P align=justify>但是,有一点需要指出,一般标准的SDI和MDI应用程序,只有主边框窗口拥有菜单和工具条等用户接口对象,只有在用户与用户接口对象进行交互时,才产生命令,产生的命令必然是送给SDI或者MDI程序的主边框窗口对象处理。</P>
<P align=justify>下面,讨论几个MFC类覆盖OnCmdMsg虚拟函数时的实现。这些类的OnCmdMsg或者可能是标准MFC命令消息路径的一个环节,或者可能是一个独立的处理过程(对于其中的MFC窗口类)。</P>
<P align=justify>从分析CView的OnCmdMsg实现开始。</P>
<UL>
<P align=justify>
<LI>CView的OnCmdMsg
<p></LI></UL>
<P align=justify>CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,</P>
<DIR>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P></DIR>
<P align=justify>首先,调用<I>CWnd::OnCmdMsg</I>,结果是搜索当前视的类和基类的消息映射数组,搜索顺序是从下层到上层。若某一层实现了对命令消息nID的处理,则调用它的实现函数;否则,调用m_pDocument-&gt;OnCmdMsg,把命令消息送给文档类处理。m_pDocument是和当前视关联的文档对象指针。如果文档对象类实现了OnCmdMsg,则调用它的覆盖函数;否则,调用基类(例如CDocument)的OnCmdMsg。</P>
<P align=justify>接着,讨论CDocument的实现。</P>
<UL>
<P align=justify>
<LI>CDocument的 OnCmdMsg
<p></LI></UL>
<P align=justify>BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,</P>
<DIR>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P></DIR>
<P align=justify>首先,调用<I>CCmdTarget::OnCmdMsg</I>,导致当前对象(this)的类和基类的消息映射数组被搜索,看是否有对应的消息处理函数可用。如果有,就调用它;如果没有,则调用文档模板的OnCmdMsg函数(m_pTemplate-&gt;OnCmdMsg)把消息送给文档模板处理。</P>
<P align=justify>MFC文档模板没有覆盖OnCmdMsg,导致基类CCmdTarget的OnCmdMsg被调用,看是否有文档模板类或基类实现了对消息的处理。是的话,调用对应的消息处理函数,否则,返回FALSE。从前面的分析知道,CCmdTarget类的消息映射数组是空的,所以这里返回FALSE。</P>
<UL>
<P align=justify>
<LI>CDialog的OnCmdMsg
<p></LI></UL>
<P align=justify>BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,</P>
<DIR>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P></DIR>
<OL>
<P align=justify>
<LI>调用<I>CWnd::OnCmdMsg</I>,让对话框或其基类处理消息。
<P>
<P align=justify></P>
<LI>如果还没有处理,而且是控制消息或系统命令或非命令按钮,则返回FALSE,不作进一步处理。否则,调用父窗口的OnCmdmsg(GetParent()-&gt;OnCmdmsg)把消息送给父窗口处理。
<P>
<P align=justify></P>
<LI>如果仍然没有处理,则调用当前线程的OnCmdMsg(GetThread()-&gt;OnCmdMsg)把消息送给线程对象处理。
<P>
<P align=justify></P>
<LI>如果最后没有处理,返回FALSE。
<p></LI></OL>
<UL>
<P align=justify>
<LI>CMDIFrameWnd的OnCmdMsg
<p></LI></UL>
<P align=justify>对于MDI应用程序,MDI主边框窗口首先是把命令消息发送给活动的MDI文档边框窗口进行处理。MDI主边框窗口对OnCmdMsg的实现函数的原型如下:</P>
<P align=justify>BOOL CMDIFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,</P>
<DIR>
<P align=justify>AFX_CMDHANDLERINFO* pHandlerInfo)</P></DIR>
<OL>
<P align=justify>
<LI>如果有激活的文档边框窗口,则调用它的OnCmdMsg(MDIGetActive()-&gt;OnCmdMsg)把消息交给它进行处理。MFC的文档边框窗口类并没有覆盖OnCmdMsg函数,所以基类CFrameWnd的函数被调用,导致文档边框窗口的活动视、文档边框窗口本身、应用程序对象依次来进行消息处理。
<P>
<P align=justify></P>
<LI>如果文档边框窗口没有处理,调用CFrameWnd::OnCmdMsg把消息按标准路径发送,重复第一次的步骤,不过对于MDI边框窗口来说不存在活动视,所以省却了让视处理消息的必要;接着让MDI边框窗口本身来处理消息,如果它还没有处理,则让应用程序对象进行消息处理──虽然这是一个无用的重复。
<p></LI></OL>
<P align=justify></P>
 楼主| 发表于 2006-12-8 10:04:37 | 显示全部楼层
< align=justify>除了CView、CDocument和CMDIFrameWnd类,还有几个OLE相关的类覆盖了OnCmdMsg函数。OLE的处理本书暂不涉及,CDialog::OnCmdMsg将在对话框章节专项讨论其具体实现。</P>
<OL>
<OL>
<OL>
<OL>
< align=justify>
<LI><B>一些消息处理类的OnCommand的实现</B> <br>
<p></LI></OL></OL></OL></OL>
< align=justify>除了虚拟函数OnCmdMsg,还有一个虚拟函数OnCommand在命令消息的发送中占有重要地位。在处理命令或者通知消息时,OnCommand被MFC窗口过程调用,然后它调用OnCmdMsg按一定路径传送消息。除了CWnd类和一些OLE相关类外,MFC里主要还有MDI边框窗口实现了OnCommand。</P>
< align=justify>BOOL CMDIFrameWnd::<I>OnCommand</I>(WPARAM wParam, LPARAM lParam)</P>
< align=justify>第一,如果存在活动的文档边框窗口,则使用AfxCallWndProc调用它的窗口过程,把消息送给文档边框窗口来处理。这将导致文档边框窗口的OnCmdMsg作如下的处理:</P>
< align=justify>活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理</P>
< align=justify>任何一个环节如果处理消息,则不再向下发送消息,处理终止。如果消息仍然没有被处理,就只有交给主边框窗口了。</P>
< align=justify>第二,第一步没有处理命令,继续调用<I>CFrameWnd::OnCommand</I>,将导致CMDIFrameWnd的OnCmdMsg被调用。从前面的分析知道,将再次把消息送给MDI边框窗口的活动文档边框窗口,第一步的过程除了文档边框窗口缺省处理外都将被重复。具体的处理过程见前文的CMDIFrameWnd::OnCmdMsg函数。</P>
<OL start=3>
< align=justify>
<LI>对于MDI消息,如果主边框窗口还不处理的话,交给CMDIFrameWnd的DefWindowProc作缺省处理。 <br>
<>
<P align=justify></P>
<LI>消息没有处理,返回FALSE。 <br>
<p></LI></OL>
<P align=justify>上述分析综合了OnCommand和OnCmdMsg的处理,它们是在MFC内部MDI边框窗口处理命令消息的完整的流程和标准的步骤。整个处理过程再次表明了边框窗口在处理命令消息时的中心作用。从程序员的角度来看,可以认为整个标准处理路径如下:</P>
<P align=justify>活动视处理消息→与视关联的文档处理消息→本文档边框窗口处理消息→应用程序对象处理消息→文档边框窗口缺省处理→MDI边框窗口处理消息→MDI边框窗口缺省处理</P>
<DIR>
<DIR>
<DIR>
<P align=justify>任何一个环节如果处理消息,不再向下发送消息,急处理终止。</P></DIR></DIR></DIR>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>对控制通知消息的接收和处理</B> <br>
<P>
<OL>
<P align=justify>
<LI><B>WM_COMMAND控制通知消息的处理</B> <br>
<p></LI></OL></LI></OL></OL></OL>
<P align=justify>WM_COMMAND控制通知消息的处理和WM_COMMAND命令消息的处理类似,但是也有不同之处。</P>
<P align=justify>首先,分析处理WM_COMMAND控制通知消息和命令消息的相似处。如前所述,命令消息和控制通知消息都是由窗口过程给OnCommand处理(参见CWnd::OnWndMsg的实现),OnCommand通过wParam和lParam参数区分是命令消息或通知消息,然后送给OnCmdMsg处理(参见CWnd::OnCommnd的实现)。</P>
<P align=justify>其次,两者的不同之处是:</P>
<UL>
<P align=justify>
<LI>命令消息一般是送给主边框窗口的,这时,边框窗口的OnCmdMsg被调用;而控制通知消息送给控制子窗口的父窗口,这时,父窗口的OnCmdMsg被调用。 <br>
<P>
<P align=justify></P>
<LI>OnCmdMsg处理命令消息时,通过命令分发可以由多种命令目标处理,包括非窗口对象如文档对象等;而处理控制通知消息时,不会有消息分发的过程,控制通知消息最终肯定是由窗口对象处理的。 <br>
<p></LI></UL>
<P align=justify>不过,在某种程度上可以说,控制通知消息由窗口对象处理是一种习惯和约定。当使用ClassWizard进行消息映射时,它不提供把控制通知消息映射到非窗口对象的机会。但是,手工地添加消息映射,让非窗口对象处理控制通知消息的可能是存在的。例如,对于CFormView,一方面它具备接受WM_COMMAND通知消息的条件,另一方面,具备把WM_COMMAND消息派发给关联文档对象处理的能力,所以给CFormView的通知消息是可以让文档对象处理的。</P>
<P align=justify>事实上,BN_CLICKED控制通知消息的处理和命令消息的处理完全一样,因为该消息的通知代码是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。</P>
<P align=justify>此外,MFC的状态更新处理机制就是建立在通知消息可以发送给各种命令目标的基础之上的。关于MFC的状态更新处理机制,见后面4.4.4.4节的讨论。</P>
<UL>
<P align=justify>
<LI>控制通知消息可以反射给子窗口处理。OnCommand判定当前消息是WM_COMAND通知消息之后,首先它把消息反射给控制子窗口处理,如果子窗口处理了反射消息,OnCommand不会继续调用OnCmdMsg让父窗口对象来处理通知消息。 <br>
<p></LI></UL>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>WM_NOTIFY消息及其处理:</B> <br>
<p></LI></OL></OL></OL></OL>
<P align=justify>(1)WM_NOTIFY消息</P>
<P align=justify>还有一种通知消息WM_NOTIFY,在Win32中用来传递信息复杂的通知消息。WM_NOTIFY消息怎么来传递复杂的信息呢?WM_NOTIFY的消息参数wParam包含了发送通知消息的控制窗口ID,另一个参数lParam包含了一个指针。该指针指向一个NMHDR结构,或者更大的结构,只要它的第一个结构成员是NMHDR结构。</P>
<P align=justify>NMHDR结构:</P>
<P align=justify>typedef struct tagNMHDR {</P>
<P align=justify>HWND hwndFrom;</P>
<P align=justify>UINT idFrom;</P>
<P align=justify>UINT code;</P>
<P align=justify>} NMHDR;</P>
<P align=justify>上述结构有三个成员,分别是发送通知消息的控制窗口的句柄、ID和通知消息代码。</P>
<P align=justify>举一个更大、更复杂的结构例子:列表控制窗发送LVN_KEYDOWN控制通知消息,则lParam包含了一个指向LV_KEYDOWN结构的指针。其结构如下:</P>
<P align=justify>typedef struct tagLV_KEYDOWN {</P>
<P align=justify>NMHDR hdr;</P>
<P align=justify>WORD wVKey;</P>
<P align=justify>UINT flags;</P>
<P align=justify>}LV_KEYDOWN;</P>
<P align=justify>它的第一个结构成员hdr就是NMHDR类型。其他成员包含了更多的信息:哪个键被按下,哪些辅助键(SHIFT、CTRL、ALT等)被按下。</P>
 楼主| 发表于 2006-12-8 10:07:52 | 显示全部楼层
<DIR>
< align=justify>(2)WM_NOTIFY消息的处理</P></DIR>
< align=justify>在分析CWnd::OnWndMsg函数时,曾指出当消息是WM_NOTIFY时,它把消息传递给OnNotify虚拟函数处理。这是一个虚拟函数,类似于OnCommand,CWnd和派生类都可以覆盖该函数。OnNotify的函数原型如下:</P>
< align=justify>BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)</P>
< align=justify>参数1是发送通知消息的控制窗口ID,没有被使用;参数2是一个指针;参数3指向一个long类型的数据,用来返回处理结果。</P>
< align=justify>WM_NOTIFY消息的处理过程如下:</P>
<OL>
< align=justify>
<LI>反射消息给控制子窗口处理。
<p>
< align=justify></P>
<LI>如果子窗口不处理反射消息,则交给OnCmdMsg处理。给OnCmdMsg的四个参数分别如下:第一个是命令消息ID,第四个为空;第二个高阶word是WM_NOTIFY,低阶word是通知消息;第三个参数是指向AFX_NOTIFY结构的指针。第二、三个参数有别于OnCommand送给OnCmdMsg的参数。
<p></LI></OL>
< align=justify>AFX_NOTIFY结构:</P>
< align=justify>struct AFX_NOTIFY</P>
< align=justify>{</P>
<P align=justify>LRESULT* pResult;</P>
<P align=justify>NMHDR* pNMHDR;</P>
<P align=justify>};</P>
<P align=justify>pNMHDR的值来源于参数2 lParam,该结构的域pResult用来保存处理结果,域pNMHDR用来传递信息。</P>
<P align=justify></P>
<P align=justify>OnCmdMsg后续的处理和WM_COMMAND通知消息基本相同,只是在派发消息给消息处理函数时,DispatchMsdMsg的第五个参数pExtra指向OnCmdMsg传递给它的AFX_NOTIFY类型的参数,而不是空指针。这样,处理函数就得到了复杂的通知消息信息。</P>
<OL>
<OL>
<OL>
<OL>
<P align=justify>
<LI><B>消息反射</B>
<p></LI></OL></OL></OL></OL>
<P align=justify>(1)消息反射的概念</P>
<P align=justify>前面讨论控制通知消息时,曾经多次提到了消息反射。MFC提供了两种消息反射机制,一种用于OLE控件,一种用于Windows控制窗口。这里只讨论后一种消息反射。</P>
<P align=justify>Windows控制常常发送通知消息给它们的父窗口,通常控制消息由父窗口处理。但是在MFC里头,父窗口在收到这些消息后,或者自己处理,或者反射这些消息给控制窗口自己处理,或者两者都进行处理。如果程序员在父窗口类覆盖了通知消息的处理(假定不调用基类的实现),消息将不会反射给控制子窗口。这种反射机制是MFC实现的,便于程序员创建可重用的控制窗口类。</P>
<P align=justify>MFC的CWnd类处理以下控制通知消息时,必要或者可能的话,把它们反射给子窗口处理:</P>
<P align=justify>WM_CTLCOLOR,</P>
<P align=justify>WM_VSCROLL,WM_HSCROLL,</P>
<P align=justify>WM_DRAWITEM,WM_MEASUREITEM,</P>
<P align=justify>WM_COMPAREITEM,WM_DELETEITEM,</P>
<P align=justify>WM_CHARTOITEM,WM_VKEYTOITEM,</P>
<P align=justify>WM_COMMAND、WM_NOTIFY。</P>
<P align=justify></P>
<P align=justify>例如,对WM_VSCROLL、WM_HSCROLL消息的处理,其消息处理函数如下:</P>
<P align=justify>void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)</P>
<P align=justify>{</P>
<DIR>
<P align=justify>//如果是一个滚动条控制,首先反射消息给它处理</P>
<P align=justify>if (pScrollBar != NULL &amp;&amp; pScrollBar-&gt;SendChildNotifyLastMsg())</P>
<DIR>
<P align=justify>return; //控制窗口成功处理了该消息</P>
<P align=justify></P></DIR>
<P align=justify>Default();</P></DIR>
<P align=justify>}</P>
<P align=justify>又如:在讨论OnCommand和OnNofity函数处理通知消息时,都曾经指出,它们首先调用ReflectLastMsg把消息反射给控制窗口处理。</P>
<P align=justify></P>
<P align=justify>为了利用消息反射的功能,首先需要从适当的MFC窗口派生出一个控制窗口类,然后使用ClassWizard给它添加消息映射条目,指定它处理感兴趣的反射消息。下面,讨论反射消息映射宏。</P>
<P align=justify>上述消息的反射消息映射宏的命名遵循以下格式:“ON”前缀+消息名+“REFLECT”后缀,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分别为ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。状态更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。</P>
<P align=justify>消息处理函数的名字和去掉“WM_”前缀的消息名相同 ,例如WM_HSCROLL反射消息处理函数是Hscroll。</P>
<P align=justify>消息处理函数的原型这里不一一列举了。</P>
<P align=justify>这些消息映射宏和消息处理函数的原型可以借助于ClassWizard自动地添加到程序中。ClassWizard添加消息处理函数时,可以处理的反射消息前面有一个等号,例如处理WM_HSCROLL的反射消息,选择映射消息“=EN_HSC ROLL”。ClassWizard自动的添加消息映射宏和处理函数到框架文件。</P>
<P align=justify>(2)消息反射的处理过程</P>
<P align=justify>如果不考虑有OLE控件的情况,消息反射的处理流程如下图所示:</P>
<P align=justify></P><IMG src="http://forum.exceedu.com/forum/" align=left>
<P align=justify></P>
<P align=justify>首先,调用CWnd的成员函数SendChildNotifyLastMsg,它从线程状态得到本线程最近一次获取的消息(关于线程状态,后面第9章会详细介绍)和消息参数,并且把这些参数传递给函数OnChildNotify。注意,当前的CWnd对象就是MFC控制子窗口对象。</P>
<P align=justify>OnChlidNofify是CWnd定义的虚拟函数,不考虑OLE控制的话,它仅仅只调用ReflectChildNotify。OnChlidNofify可以被覆盖,所以如果程序员希望处理某个控制的通知消息,除了采用消息映射的方法处理通知反射消息以外,还可以覆盖OnChlidNotify虚拟函数,如果成功地处理了通知消息,则返回TRUE。</P>
<P align=justify>ReflectChildNotify是CWnd的成员函数,完成反射消息的派发。对于WM_COMMAND,它直接调用<I>CWnd::OnCmdMsg</I>派发反射消息WM_REFLECT_BASE+WM_COMMAND;对于WM_NOTIFY,它直接调用<I>CWnd::OnCmdMsg</I>派发反射消息WM_REFLECT_BASE+WM_NOFITY;对于其他消息,则直接调用<I>CWnd::OnWndMsg</I>(即<I>CmdTarge::OnWndMsg</I>)派发相应的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。</P>
<P align=justify>注意:ReflectChildNotify直接调用了CWnd的OnCmdMsg或OnWndMsg,这样反射消息被直接派发给控制子窗口,省却了消息发送的过程。</P>
<P align=justify>接着,控制子窗口如果处理了当前的反射消息,则返回反射消息被成员处理的信息。</P>
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-2-6 04:13

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表