『VC++技术内幕』(第四版)读书笔记
关键字:VC++
原作者姓名:loose_went
文章原出处:vczx.com
写在前面:
站长所看的『VC++技术内幕』版本为--潘爱民和王国印译清华大学出版的第四版,因有时工作忙碌,不能及时更新,请大家见谅!
第一天 Windows的编程模式
Windows程序中必须要有WinMain函数,因为该函数最重要的任务是创建该应用程序的主窗口。Windows程序与基于MS-DOS程序的最大差别就在于:MS-DOS程序是通过调用操作系统的功能来获得用户输入的,而Windows程序是通过操作系统发送的消息来处理用户输入的。Windows消息都是经过严格定义的,并且适用于所有的程序。
WINDOWS提供通用的图形设备接口(GUI),我们通过调用(GDI)函数和硬件打交道,不必理会设备环境,WINDOWS会自动将设备环境结构映射到相应的物理设备。
Windows程序设计中所需要的数据是存储在资源文件中的,这样,连接器就可以把编译好的二进制代码和二进制资源文件结合起来生成可执行程序。资源文件可以包括位图、图标、菜单定义、对话框设计,甚至可以包含用户自己定义的格式。
Windows程序允许动态的连接目标模块,并且多个应用程序可以共享同一个动态连接库。
VC++的源程序浏览器能够使我们从类或函数的角度来了解或编辑程序,而不是直接从文件入手。在看别人的源代码时如果能熟练的使用源代码浏览器将会事半功倍。源程序浏览器主要的查看状态有以下几种:
Definitions and References--选择任何函数、变量、类型、宏定义可以看到它在项目中的定义,并且在何处和什么地方用到它。
Call Graph/Caller Graph--对于所选择的函数,给出它的调用与被调用函数的图示。
Derived Class Graph/Base Class Graph--给出类层次关系的图形表示,可以看到所选择的类的派生类和基类以及成员。
File Outline--对于所选的文件,列出文件中的类、函数和数据成员,同时还显示它们定义的位置和使用位置。
可见Source Brower比起Class View来功能多了很多也更加好用。
对于本章学习loose_went建议大家在VC++6中用AppWizard生成一个空的程序,然后试着看看都有哪些文件,和他们的类层次、函数、宏、结构的定义,我就是这样干的,学编程不动手是不行的。
第二天 MFC应用程序框架
?MFC是C++的Microsoft Windows API
?MFC产生的应用程序使用了标准化的结构。
?MFC产生的应用程序短而运行速度快。
?VC++工具降低了编码的复杂性,这当然了,很多代码都由它代劳了,呵呵。
?MFC库应用程序框架的功能非常丰富。
以上说的都是MFC库的优点,虽然说MFC有着这样多的优点,但我个人认为不能盲目的学习它,要想学好,那么您必须先掌握C++,这是毋庸置疑的。可能刚开始的时候,您觉得收获很大,也很有趣,但要进一步提高,没有C++基础是很难的。所以站长建议大家学习的时候要有先有后,这样才能学好!
应用程序框架是一种类库的超集。
我们现在先来看一个例子,看看MFC有多么强大!您只需加一行代码,甚至一行都不用加只需要点几下鼠标就可以创建一个windows 程序,不信,试一下:
1、打开VC++6从菜单选择NEW,给项目命名为"MyApp "。
2、选择MFC AppWizard[exe] 选项,除STEP 1选择单文档外其他STEP缺省。
3、在Class View选择CMyAppView类的OnDraw()成员函数双击会在C++编译器看到以下内容
void CMyAppView::OnDraw(CDC* pDC)
{
CMyAppDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
}
在 // TODO: add draw code for native data here的位置增加一行代码
void CMyAppView::OnDraw(CDC* pDC)
{
CMyAppDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(10,10,"愿vc在线能成为您学习vc最好的朋友!"); //增加的一行
// TODO: add draw code for native data here
}
完了,就这么简单。编译运行。看到了吗?这个程序具备WINDOWS程序的所有特性,例如有菜单、工具条、状态栏、最大化、关闭、甚至还有关于对话框、打印预览.....全了,这就是AppWizard通过MFC动态创建的一个应用程序。从这个小例子可以看出用VC/MFC设计WINDOWS程序多么方便。
下面我们看看书上的例子,以便更进一步了解应用程序框架。
1、先建立一个Win32 Application的应用程序。
2、选择Project->Add to project->Files,分别创建一个名为MyApp.h和一个名为MyApp.cpp的文件。
3、添加代码:(最好照敲一下代码到编译器,别用Ctrl+C/Ctrl+V)
//***********************************************
// MyApp.h
//
class CMyApp:public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CMyFrame:public CFrameWnd
{
public:
CMyFrame();
protected:
afx_msg void OnLButtonDown(UINT nFlags,CPoint point);
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
};
//*****************************************************
// MyApp.cpp
//
#include "afxwin.h"
#include "myapp.h"
CMyApp theApp;//建立一个CMyAPP对象
BOOL CMyApp::InitInstance ()
{
m_pMainWnd=new CMyFrame();
m_pMainWnd->ShowWindow (m_nCmdShow);
m_pMainWnd->UpdateWindow ();
return TRUE;
}
BEGIN_MESSAGE_MAP(CMyFrame,CFrameWnd)
ON_WM_LBUTTONDOWN()
ON_WM_PAINT()
END_MESSAGE_MAP()
CMyFrame::CMyFrame(){
Create(NULL,"MYAPP Application");
}
void CMyFrame::OnLButtonDown (UINT nFlags,CPoint point)
{
TRACE("Entering CMyFrame::OnLButtonDown - %lx,%d,%d\n",
(long)nFlags,point.x ,point.y);
}
void CMyFrame::OnPaint ()
{
CPaintDC dc(this);
dc.TextOut (0,0,"Hello World!");
}
4、编译运行,报错。为什么呢?原来还没有添加MFC的支持,在Project Setting选项General属性页选择"Use MFC in a Static Library"
5、再按Ctrl+F5,怎么样,简单吧?
让我们看看这个程序中的一些元素。
①WinMain函数:Windows总是要求每个应用程序都要有WinMain函数的,您之所以看不见,是因为它已经隐藏在应用程序框架内部了。
②CMyApp类:CMyApp类的对象代表一个应用程序,CWinApp基类决定它的大部分行为。
③应用程序的启动:当开始运行应用程序时WINDOWS会调用WinMain函数,WinMain会查找该应用程序的全局对象theApp。
④CMyApp::InitInstance成员函数:发现theApp后自动调用重载的虚函数InitInstance来完成主窗口的构造和显示工作。
⑤CWinApp::Run成员函数:WinMain在调用InitInstance之后紧接着调用Run函数,它被隐藏在基类中负责传递应用程序的消息给相映的窗口。
⑥CMyFrame类:此类的对象代表着应用程序的主窗口。它的构造函数调用基类CFrameWnd的Create函数创建具体的窗口结构。
⑦CMyFrame::OnLButtonDown函数:演示消息处理机制,当鼠标坐键被按下这一事件被映射到CMyFrame的OnLButtonDown函数上,如果你选择F5进行编译运行的话可以在调试窗口看到TRACE宏显示的类似下面的信息
Entering CMyFrame::OnLButtonDown - 1,309,119
Entering CMyFrame::OnLButtonDown - 1,408,221
⑧CMyFrame::OnPaint函数:应用程序每次重新绘制窗口都需要调用此函数,将显示"Hello World!"放在这里是因为每次窗口发生变化时保证"Hello World!"被显示,你可以试着将语句:
CPaintDC dc(this);
dc.TextOut (0,0,"Hello World!");
写在别出,例如写在
void CMyFrame::OnLButtonDown (UINT nFlags,CPoint point)
{
TRACE("Entering CMyFrame::OnLButtonDown - %lx,%d,%d\n",
(long)nFlags,point.x ,point.y);
CPaintDC dc(this);
dc.TextOut (0,0,"Hello World!");
}
运行后当点击左键时显示"Hello World!",但当窗口最小化再最大化时"Hello World!"不见了。
⑧关闭应用程序:用户关闭应用程序时会有一系列事件发生。首先CMyFrame对象被删除,然后退出Run,进而退出WinMain,最后删除CMyApp对象。
通过上面的示例我们看见程序的大部分功能包含在基类CWinApp和CFrameWnd中,我们只写了很少的函数,便可以完成很复杂的功能。所以应用程序框架不仅仅是一种类库,它还定义了应用程序的结构,除了基类外还包括WinMain函数,以及用来支持消息处理、诊断、DLL、等都包含在应用程序框架中。
第三天 消息映射和视图类
MFC库应用程序框架没有采用虚函数来处理windows消息,而是通过宏将消息映射到派生类相应的成员函数上。文档-视图结构是应用程序框架的核心,它把数据从用户对数据的观察中分离出来,这样做最大的好处就是同一个数据可以对应多个视图。比如同一个股票报价数据,既可以有报表观察窗口,也可以有图形观察窗口,明白了否?
视图简单来说就是一个普通的窗口,对于程序员来说就是一个从MFC库中Cview类派生出来的类的一个对象。视图类分为两个源文件模块:头文件(H)和源代码文件(CPP)。
用Appwizard创建一个SDI应用程序,产生了如下文件(假设工程名为Exc01):
Exc01.dsp 项目文件,Visual Studio用它来创建应用程序
Exc01.dsw 工作空间文件,包含一个项目Exc01.dsp
Exc01.rc ASCII码资源描述文件
Exc01View.cpp 包含CExc01View类成员函数和视图类文件
Exc01View.h 包含CExc01View类定义的视图类头文件
Exc01.opt 二进制文件,告诉Developer Studio本项目的哪些文件是打开的,又是如何排序的
Readme.txt 用来解释所产生的所有文件的文本文件
Resource.h 包含#define常量定义的头文件
从Exc01View.cpp和Exc01View.h的代码中可以看出,这两个文件已经完全定义了CExc01View类,而该类正是此应用程序的核心。CExc01View类的对象与应用程序的视窗相关联,应用程序的所有"动作"都会在这个视窗中显示出来。
CExc01View类的两个最重要的基类是CWnd和CView类。CWnd类提供了CExc01View的窗口属性,而CView类则提供了它和应用程序框架的其它部分之间的联系,特别是和文档以及框架窗口之间的联系。这一点一定要记住。
下面我们来看一下如何在视窗内绘图。最重要的一个函数是OnDraw()函数,它是一个虚函数,每次窗口被重画时,应用程序都要先调用这个函数。注意:尽管可以随时对窗口绘制,但最好还是等变化内容积累到一定程度后再教给OnDraw()函数处理,这样效率会高一些。
在MFC中,设备环境是由C++的CDC类对象来表示的,该对象被作为参数传给Ondraw()函数,这样,我们就可以调用CDC的许多成员函数来完成各种绘制了。
找到OnDraw()函数,用以下语句替换函数原来的内容:
pDC->TextOut( 0, 0, "Hello World!" );
pDC->Ellipse(CRect(0,20,100,120));
再编译运行,看到了什么?
TextOut和Ellipse都是设备环境类CDC的成员函数,MFC库提供了一个用来表示windows矩形的类CRect,在这里CRect的一个临时对象被作为参数传递给 了Ellipse函数,当外接矩形的宽和高相等时,Ellipse函数就画出个圆。
第四天 资源和编译
资源文件(就是以应用程序名和扩展名是.rc的文件)很大程度上决定了应用程序的用户界面。在VC++中资源文件包括以下内容:
Accelerator //模拟菜单和工具栏选择的键盘定义
Dialog //对话框的布局及内容
Icon //图标有两种一种是16X16一种是32X32。
Menu //应用程序的主菜单及所属的弹出式菜单
String table //一些字符串,不属于C++源代码部分
Toolbar //工具条。
Version //程序的描述、版本号、支持语言信息。
除了以上信息,.rc文件还包含了以下语句: #include "afxres.h" #include "afxres.rc" 它们的作用是把适合于所有应用程序的一些通用MFC库资源包含进来,其中包括字符串、图形按钮以及打印所需的一些元素。
关于资源编辑器的使用就不多说了,因为它的操作很简单,需要注意的是虽然resource.h是一个ASCII码文件可以用文本编辑器进行编辑,但如果使用文本编辑器进行编辑的话,下次再使用资源编辑器时所做的修改有可能丢失,所以我们应该在尽量在资源编辑器中编辑应用程序的资源,新增的资源内容回自动的添加在我们的程序相应位置,例如resource.h而不用我们操心。
编译在VC++中有两种模式,一种是Release Build另一种是Debug Build。它们之间的区别在于,Release Build不对源代码进行调试,不考虑MFC的诊断宏,使用的是MFC Release库,编译十对应用程序的速度进行优化,而Debug Build则正好相反,它允许对源代码进行调试,可以定义和使用MFC的诊断宏,采用MFC Debug库,对速度没有优化。所以我们应该在Debug模式下开发应用程序,然后在Release模式下发布应用程序。在我们的工程文件夹下会有一个Debug文件夹和一个Release文件夹分别存放输出文件和中间文件。
诊断宏是我们编译程序时检测程序状态的有利工具,例如上两篇用到的TRACE宏,可以在Debug窗口获得你需要的诊断信息,而不用设置对话框之类的方法,在发布时Release会自动滤掉此信息。
为了更好的管理项目,最好理解系统是如何处理预编译头文件的。VC++有两个预编译系统:自动的和手工的。这一部分笔者就不多说了,建议读者好好看看。
第五天 基本事件处理
用户在视窗中的任何一个操作,都会引起Windows自动发送一个消息给该视窗。我们以一个例子来说明:比如我们在视窗中按下鼠标左键,Windows就会发送ON_LBUTTONDOWN消息给视窗,那么在视窗类中就必须包含下面的成员函数:
Void CmyView::OnLButtonDown(UINT nFlags, Cpoint point)
{
//event processing code here
}
在类头文件中也要包含相应的函数声明:
afx_msg void OnLButtonDown(UINT nFlags, Cpoint point)
在代码文件中还要有一个消息映射宏,用于将OnLButtonDown函数和应用程序框架联系在一起:
BEGIN_MESSAGE_MAP(CmyView, CView)
ON_WM_LBUTTONDOWN()
// other message map entries
END_MESSAGE_MAP
最后,在类库头文件中包含如下语句:
DECLARE_MESSAGE_MAP()
以上这些步骤,我们都可以借助于ClassWizard来完成。这就是消息映射的过程。
MFC库对140种windows消息直接提供了消息控制函数,并且我们还可以自己定义自己的消息,下面列出的五种消息是我们应该特别注意的(MSDN上有更详细的内容)。
WM_CREATE
该消息是Windows发给视图的第一个消息。当应用程序框架调用create函数时该消息便会被发送,此时窗口还未创建完成,不可见,因此在消息控制函数OnCreate内不能调用那些依赖窗口处于完全激活状态的Windows函数。如果需要可以在重载的OnInitialUpdate函数内调用。不过注意在SDI应用程序OnInitialUpdate函数可能被多次调用。
WM_CLOSE
当用户关闭窗口时,系统会发送WM_CLOSE消息。如果派生类重新定义了OnClose函数,就可以完全控制关闭过程,可以将提醒用户存盘之类的工作放在这里完成。我们可以通过重载CDocument::SaveModified虚函数达到相同的目的。
WM_QUERYENDSESSION
从字面的意思看就可以看出,当用户退出Windows时,或者调用了ExitWindows 函数时。Windows会发送WM_QUERYENDSESSION消息给所有的正在运行的应用程序,由OnQueryEndSession消息映射函数对消息进行处理。在它之后应该是WM_ENDSESSION 消息。
WM_DESTROY
在Windows发送WM_CLOSE消息后,紧接着会发送WM_DESTROY消息,虽然窗口已经Close但实际上并没有完全清除,在任务管理器中还可以看见应用程序的进程(我想很多木马或病毒都是无窗口的程序,它们的做法是生成了已经活动状态的窗口但不显示出来),利用这个消息控制函数便可以对依赖于当前窗口存在的东西做清除工作,不过一定要注意,应该调用基类的OnDestroy函数,而不能在用户自己的视图的OnDestroy函数中终止窗口的析构过程,终止析构过程应该在OnClose函数中。
WM_NCDESTROY
当窗口被取消所发送的最后一个消息就是这个消息。我们可以在OnNcDestroy函数中做一些不依赖该窗口是否处于活动状态的最后的处理工作,(我实在想不出还需要做什么?那位朋友能给个例子),注意一定要调用基类中的OnNcDestroy函数。
MFC库中非静态数据成员的名字以m_为前缀。
一个窗口具有一个矩形的"客户区域",CWnd中的GetClient成员函数可以给出客户区域的大小,只允许在客户区域内绘图。
标准的windows应用程序会首先登记一个窗口类,这不同于C++类,同时在处理过程中,还需要对每个类指定窗口过程。每次应用程序调用CreateWindow建立一个窗口时,都要指定一个窗口类作为参数,这样就把新建立的窗口和窗口过程函数连接起来了,每次windows给窗口发送消息的时候,这个函数就会被调用,以检查用参数传进来的消息码。
第六天 映射模式
所谓映射模式,说白了就是坐标系。在默认情况下,Windows所绘图像单位为像素,这是因为设备环境用了默认的映射模式MM_TEXT,所以如下语句所绘图形为长和宽都为200像素的方块: pDC->Rectangle(CRect(0,0,200,200));
那么我们要绘制一个长和宽都是4厘米的方块该怎么做呢?这就必须改变设备环境的默认映射模式为MM_HIMETRIC,它的图像单位为1/100mm,而不是像素了。它的y轴方向和MM_TEXT的相反,它的向下为递减的,因此用如下语句就可以绘出4×4cm的方块了:
pDC->SetMapMode( MM_HIMETRIC);
pDC->Rectangle(CRect(0,0,4000,-4000));
下面我们再来了解一下Windows都提供了哪些映射模式。
1、MM_TEXT映射模式
这种模式下,绘图单位为像素,x轴向右递增,y轴向下递增,我们可以用CDC的SetViewPortOrg和SetWindowOrg函数来改变坐标原点的位置,下面的代码就是把坐标原点设在了(100,100)处,画了一个200×200像素的方块,此时逻辑坐标点(100,100)被映射到了设备坐标点(0,0)处,下一篇的滚动窗口使用的就是这种变换。
Void CmyView::OnDraw( CDC *pDC ){
pDC->SetMapMode(MM_TEXT);
pDC->SetWindowOrg(Cpoint(100,100));
pDC->Rectangle(CRect(100,100,200,200));
}
2、固定比例映射模式
Windows提供了一组非常重要的固定比例影视模式,所有这种模式都遵循x轴向右递减,y轴向下递减的规则,而且我们无法将其改变。固定比例模式之间唯一的差别就在于实际的比例因子。下表列出了影视模式和比例因子的对应情况:
映射模式 |
逻辑单位 |
MM_LOENGLISH |
0.01英寸 |
MM_HIENGLISH |
0.001英寸 |
MM_LOMETRIC |
0.1mm |
MM_HIMETRIC |
0.01mm |
MM_TWIPS |
1/1440英寸 |
MM_TWIPS模式常用于打印机。
3、可变比例映射模式
Windows还提供了两种映射模式MM_ISOTROPIC和MM_ANISOTROPIC,这两种模式允许我们修改比例因子和坐标原点。在MM_ISOTROPIC模式下,纵横比总是1:1,就像改变图像时锁定比例一样,而MM_ANISOTROPIC模式则可以独立的改变x和y的比例因子,即圆可以变成扁圆。
以上就是常见的映射模式,笔者建议:我们没必要死记住这些模式,只是到用的时候会用就可以了,哪怕查查MSDN,这个东东真好!
在设置了映射模式和相应参数之后,我们可以用CDC的LPtoDP函数将逻辑坐标转换为设备坐标,用DptoLP函数将设备坐标转换为逻辑坐标。那么我们什么时候用什么样的坐标呢?有一些规则如下:
① 可以认为CDC的所有成员函数都以逻辑坐标为参数
② 可以认为CWnd的所有成员函数都以设备坐标为参数
③ 所有选中测试都应该选用设备坐标,区域的定义应采用设备坐标,某些像CRect::PtInRect之类的函数只有采用设备坐标才能有正确的结果
④ 将一些长期使用的值用逻辑坐标来保存,如果用设备坐标,那么只要用户对窗口进行一下滚动,坐标就不再有效了
一般情况下,我们在CView的虚函数OnPrepareDC中设置映射模式,应用程序框架在调用OnDraw函数之前调用这个虚函数。
第七天 滚动视窗
CView类并不直接支持窗口滚动,如要实现窗口滚动,就要用到CView的派生类CScrollView类,CScrollView的成员函数能够处理滚动条并发送给视图WM_HSCROLL和WM_VSCROLL消息,从而实现窗口的滚动。
在文档-视图结构中,视图窗口建立以后,框架最先调用OnInitialUpdate虚函数,在框架第一次调用OnDraw函数前也是先调用OnInitialUpdate函数,因此在OnInitialUpdate函数中设置滚动视窗的初始化最合适。
下面我们就来创建一个滚动示例程序a:
1、 用AppWizard创建一个文档-视图程序a,注意在第六步时设置CAView的基类应为CScrollView而不是CView。
2、 在CAView中加入数据成员m_rectEllipse和m_nColor。
3、 修改OnInitialUpdate函数如下:
void CAView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal( 20000, 30000 ); //逻辑窗口大小20×30cm
CSize sizePage( sizeTotal.cx/2, sizeTotal.cy/2 );
CSize sizeLine( sizeTotal.cx/50, sizeTotal.cy/50 );
SetScrollSizes( MM_HIMETRIC, sizeTotal, sizePage, sizeLine );
}
4、 用ClassWizard产生对消息WM_KEYDOW控制的OnKeyDown函数,并编辑代码如下:
void CAView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Add your message handler code here and/or call default
switch( cChar ){
case VK_HOME:
OnVScroll( SB_TOP, 0, NULL );
OnHScroll( SB_LEFT, 0, NULL );
break;
case VK_END:
OnVScroll( SB_BOTTOM, 0, NULL );
OnHScroll( SB_RIGHT, 0, NULL );
break;
case VK_UP:
OnVScroll( SB_LINEUP, 0, NULL );
break;
case VK_DOWN:
OnVScroll( SB_LINEDOWN, 0, NULL );
break;
case VK_PRIOR:
OnVScroll( SB_PAGEUP, 0, NULL );
break;
case VK_NEXT:
OnVScroll( SB_PAGEDOWN, 0, NULL );
break;
case VK_LEFT:
OnHScroll( SB_LINELEFT, 0, NULL );
break;
case VK_RIGHT( SB_LINERIGHT, 0, NULL );
break;
default:
break;
}
}
5、 编辑构造函数和OnDraw函数如下:
CAView::CAView():m_rectEllipse( 0, 0, 4000, -4000 )
{
// TODO: add construction code here
m_nColor = GRAY_BRUSH;
}
…
void CAView::OnDraw(CDC* pDC)
{
pDC->SelectStockObject( m_nColor );
pDC->Ellipse( m_rectEllipse );
}
6、 映射WM_LBUTTONDOWN消息并编辑消息处理函数OnLButtonDown如下:
void CAView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
CClientDC dc( this );
OnPrepareDC( &dc );
CRect rectDevice = m_rectEllipse;
dc.LPtoDP( rectDevice );
if( rectDevice.PtInRect( point ) ){
if( m_nColor = GRAY_BRUSH )
m_nColor = WHITE_BRUSH;
else
m_nColor = GRAY_BRUSH;
}
InvalidateRect( rectDevice );
}
编译并运行看看结果吧。
另外,我们要特别注意下面五种比较特殊的windows消息:
1、 WM_CREATE消息
该消息是windows发给视图的第一个消息,由于应用程序框架调用Create函数时该消息就会被发送,而此时窗口创建还未完成,因此在Create函数内不能调用那些依赖于窗口处于完全激活状态的windows函数。不过对于SDI应用程序,在视图生存期间,OnInitialUpdate函数可以被调用多次。
2、 WM_CLOSE消息
当用户从系统菜单中关闭窗口或者父窗口被关闭时,windows会发送WM_CLOSE消息。
3、 WM_QUERYENDSESSION消息
当用户退出windows时,windows就会发送WM_QUERYENDSESSION消息给正在运行的程序,处理这个消息的映射函数为OnQueryEndSession。
4、 WM_DESTROY消息
Windows在发送完WM_CLOSE消息后,紧接着就发送WM_DESTROY消息,消息映射函数为OnDestroy。当程序接收到该消息时,它将假定此时视窗已经消失,但仍处于活动状态。利用这个消息控制函数,就可以对依赖于当前窗口的所有东西作清除工作,不过一定要记住,应该用基类的OnDestroy而不能在自己视图中的OnDestroy中"终止"窗口的析构过程,终止析构的处理应该在OnClose函数中。
5、 WM_NCDESTROY消息
当窗口被取消时发送的最后一个消息就是这个消息,由于此时所有的窗口都被关闭,所以我们可以在OnNcDestroy函数中做一些不依赖于窗口是否处于激活状态的最后处理工作,不过一定要调用基类的OnNcDestroy函数。不要在OnNcDestroy中取消动态申请的窗口对象,这一工作是由CWnd的一个特殊虚函数PostNcDestroy来完成的,它是由基类的OnNcDestroy来调用的。何时取消窗口对象最为合适呢,去看MFC的联机文档吧!
第八天 设备环境类
任何程序在画图时都需要调用图形设备接口( GDI )函数, GDI 包含了一些绘制点、线、矩形、椭圆、位图以及文本的函数。 Windows 的设备环境是 GDI 的关键元素,它代表了物理设备,每一个 C++ 设备环境对象都有与之对应的 Windows 设备环境,并通过一个 32 位的 HDC 句柄来标识。
MFC 中的基类 CDC 包含了绘图所需要的所有成员函数,并且除了 CMetaFileDC 类外,所有的派生类都只有构造函数和析构函数不同。对于显示器来说,常用的派生类有 CClientDC 和 CWindowDC 。
显示设备环境的类 CClientDC 和 CWindowDC , CClientDC 类绘图只局限于客户区域内,即不包含边框、菜单栏和标题栏,而 CWindowDC 类可以。简单来说,如果创建 CclientDC 对象,点( 0,0 )指客户区域的左上角,如果创建的是 CWindowDC 对象,则点( 0,0 )指整个屏幕的左上角。
在创建 CDC 对象的时候,不要忘记在合适的时候将它删除,不然程序在退出之前有小部分内存就会丢失。要保证设备环境对象能够被适时的删除,可以有两种方法:
一种是在堆栈中构造对象,比如在 OnLButtonDown 函数中,它的析构函数在函数返回时自动被调用。
void CMyView::OnLButtonDown(UINT nFlags,CPoint point){
CRect rect;
CClientDC dc(this); //constructs dc on the stack
…
} //dc automatically destroyed
另一种是通过调用 CWnd 的成员函数 GetDC 来获得设备环境指针,但此时必须要调用 RleaseDC 来释放设备环境。
void CMyView::OnLButtonDown(UINT nFlags,CPoint point){
CRect rect;
CDC *pDC=GetDC();
pDC->GetClipBox(rect);
ReleaseDC(pDC); // 不要忘了这句
}
注意:千万不要删除作为参数以指针形式传递给 OnDraw 函数的 CDC 对象,应用程序框架会自动控制它的删除。
在绘图时我们离不开设备环境,那么在绘图时我们就要依赖于设备环境的当前状态,这种状态包括:
• 被选中的 GDI 绘图对象,如笔、刷子和字体等
• 绘图时的缩放尺寸的映射模式
• 其他各种细节,如文本的对齐方式,多边形的填充状态
创建设备环境对象时,通常会有些默认的特性,而其他特性都是通过 CDC 类的成员函数来设定的,可以通过重载 SelectObject 函数来将 GDI 对象选进设备环境中。
如果我们要重新编写 OnPaint 函数,就需要使用 CPaintDC 类,这个类是比较特殊的,它的构造函数和析构函数所完成的工作都是针对显示用的,当我们一旦获得一个 CDC 指针,就可以把它当成任何设备环境指针来用。
第九天 GDI对象
所有 GDI 对象类都是由抽象基类 CGdiObject 派生出来的。下面是 GDI 派生类列表:
CBitmap - 位图是一种位矩阵,每一个显示像素都对应一个或多个位,我们可以用位图来表示图像,也可以用它来创建刷子。
CBrush - 刷子定义了一种位图形式的像素,用它可以对区域内部填充颜色。
CFont - 字体是一种具有某种风格和尺寸的所有字符的集合。
CPalette - 调色板是一种颜色映射接口。
CPen - 笔是一种画线和有形边框的工具,可以指定画线的宽度,以及画虚线,实线等。
CRgn - 区域是一种范围,可以用它来填充、裁剪以及鼠标点中测试。
我们只需要构造 CGdiObject 类的派生类对象,而无需构造它的对象,有些 GDI 派生类允许构造函数一步完成创建对象的任务,如 CPen 和 CBrush 。而有些派生类的对象要两步,如 CFont 和 CRgn ,首先要调用默认的构造函数,然后还要调用相应的创建函数,如 CreateFont 、 CreatePolygonRgn 等。
CGdiObject 类有一个虚析构函数,如果构造了一个它的派生类的对象,则在程序退出之前要将其删除,为了删除它,要先将其从设备环境中分离出来。那么如何分离呢?其实, CDC 类的 SelectObject 成员函数在将 GDI 对象选进设备环境的同时,它已经从设备环境中分离出来了,但在未选中新的对象前,还不能将旧的对象分离。所以在选进自己的 GDI 对象时,将原来的 GDI 对象也保存起来,任务完成后,再将其恢复,这样就可以将自己的 GDI 对象分离并删除了。下面看一个例子:
void CMyView::OnDraw( CDC *pDC ){
CPen newPen( PS_DASHDOTDOT, 2, (COLORREF)0); //black 2 pixels wide
CPen * pOldPen = pDC->SelectObject( &newPen );
pDC->MoveTo( 10, 10 );
pDC->LineTo( 110, 10 );
pDC->SelectObject( pOldPen ); //newPen 被分离
} //newPen 在函数退出时自动删除
对于一些库存的 GDI 对象,由于它们是 windows 系统的一部分,因此我没有必要删除它们。 MFC 库函数 SelectStockObject 可以将一个库存对象选进设备环境中,并返回原先被选中对象的指针,同时使该对象被分离。在上例中,我们就可以用库存对象代替“旧”对象:
void CMyView::OnDraw( CDC *pDC ){
CPen newPen( PS_DASHDOTDOT, 2, (COLORREF)0); //black 2 pixels wide
pDC->MoveTo( 10, 10 );
pDC->LineTo( 110, 10 );
pDC->SelectStockObject( BLACK_PEN ); //newPen 被分离
} //newPen 在函数退出时自动删除
对于显示设备环境来说,在每个消息控制函数的入口处,设备环境都是未被初始化的,因此每次都必须从头开始设置设备环境,由于 SelectObject 返回的 GDI 对象指针的临时性,而应用程序框架在函数返回时会删除 C++ 临时对象指针,所以不能简单地将设备环境指针保存在类的数据成员中,而要借助于 GetSafeHandle 成员函数来将它转换为 windows 句柄(唯一能够持久存在的 GDI 标识)。
注意,当删除由 SelectObject 返回的指针所指向的对象时,一定要当心,如果该对象是我们自己申请的,可以删除,如果是临时的,则不能随便删除。 |