[转贴] Windows编程和面向对象技术 chap8

第八课 多文档界面(MDI

  这一讲主要介绍多文档界面的窗口管理、多文档编程技术、分割视图、多个文档类型程序设计。在介绍多文档编程时,还结合绘图程序介绍了GDI(图形设备接口)的使用。

多文档界面窗口

图形设备接口(GDI

绘图程序

访问当前活动视图和活动文档

分隔视图

打印和打印预览

支持多个文档类型的文档视结构程序

防止应用程序运行时创建空白窗口

小结

8.1多文档界面窗口

  MDI 应用程序是另一类重要的文档视结构程序。它的特点是:用户一次可以打开多个文档,每个文档对应不同的窗口;主窗口的菜单会自动随着当前活动的子窗口的变化而变化;可以对子窗口进行层叠、平铺等各种操作;子窗口可以在MDI 主窗口区域内定位、改变大小、最大化和最小化,当最大化子窗口时,它将占满MDI 主窗口的全部客户区。MDI 不仅可以在同一时间内同时打开多个文档,还可以为同一文档打开多个视图。在Windows 菜单下选择New ,就为当前活动文档打开一个新的子窗口。

  从程序员角度看,每个MDI 应用程序必须有一个CMDIFrameWnd 或其派生类的实例,这个窗口称作MDI 框架窗口。CMDIFrameWndCFrameWnd 的派生类,它除了拥有CFrameWnd 框架窗口类的全部特性外,还具有以下与MDI 相关的特性:

SDI 不同,主框架窗口并不直接与一个文档和视图相关联。MDI 框架窗口拥有MDICLIENTMDI 客户窗口),在显示或隐藏控制条(包括工具条、状态栏、对话条)时,重新定位该子窗口。

MDI 客户窗口是MDI 子窗口的直接父窗口,它负责管理主框架窗口的客户区以及创建子窗口。每个MDI 主框架窗口都有且只有一个MDI 客户窗口。

MDI 主框架窗口、客户窗口和子窗口的关系如下图所示:

8-1 MDI 主框架窗口、客户窗口和子窗口

MDI 子窗口是CMDIChildWnd 或其派生类的实例,CMDIChildWndCFrameWnd 的派生类,用于容纳视图和文档,它相当于SDI 下的主框架窗口。每打开一个文档,框架就自动为文档创建一个MDI 子窗口。一个MDI 应用程序负责动态的创建和删除MDI 子窗口。在任何时刻,最多只有一个子窗口是活动的( 窗口标题栏颜色呈高亮显示)MDI 框架窗口始终与当前活动子窗口相关联,命令消息在传给MDI 框架窗口之前首先分派给当前活动子窗口。

在没有任何活动的MDI 子窗口时,MDI 框架窗口可以拥有自己的缺省菜单。当有活动子窗口时,MDI 框架窗口的菜单条会自动被子窗口的菜单所替代。框架会自动监视当前活动的子窗口类型,并相应的改变主窗口的菜单。比如,在Visual Studio 中,当选择对话框资源编辑窗口或源程序窗口时,菜单会有所不同。但是,对于程序员来说,只需要在InitInstance 中注册文档时指定每一类子窗口(严格的讲是文档)所使用的菜单,而不必显式的通过调用函数去改变主框架窗口的菜单,因为框架会自动完成这一任务。

MDI 框架窗口为层叠、平铺、排列子窗口和新建子窗口等一些标准窗口操作提供了缺省的菜单响应。在响应新建子窗口命令时,框架调用CDocTemplate::CreateNewFrame() 为当前活动文档创建一个子窗口。CreateNewFrame() 不仅创建子窗口,还创建与文档相对应的视图。

  下面,我们结合一个绘图程序例子,介绍多文档界面技术。在此之前,我们首先要介绍一下如何在Windows 中绘图以及Windows 的图形设备接口(GDI )。

 

8.2   图形设备接口(GDI)

图形设备接口(GDI)是一个可执行程序,它接受Windows应用程序的绘图请求(表现为GDI函数调用),并将它们传给相应的设备驱动程序,完成特定于硬件的输出,象打印机输出和屏幕输出。

GDI负责Windows的所有图形输出,包括屏幕上输出像素、在打印机上输出硬拷贝以及绘制Windows用户界面。

8.2.1 三种图形输出类型

应用程序可以使用GDI创建三种类型的图形输出:矢量输出、光栅图形输出和文本输出。

矢量图形输出

矢量图形输出指的是创建线条和填充图形,包括点、直线、曲线、多边形、扇形和矩形的绘制。

光栅输出

光栅图形的输出是指光栅图形函数对以位图形式存储的数据进行操作,它包括各种位图和图标的输出。在屏幕上表现为对若干行和列的像素的操作,在打印机上则是若干行和列的点阵的输出。

光栅图形输出的优点是速度很快,它是直接从内存到显存的拷贝操作。缺点是需要额外的内存空间。Windows在绘制界面时使用了大量的光栅输出。

文本输出

与DOS字符方式下的输出不同,Windows是按图形方式输出的。这样,在输出文本时,必须以逻辑坐标为单位计算文本的输出位置,而不是象DOS下以文本行为单位输出文本。这比DOS下的文本输出要难一些。

但是,按图形方式输出文本也给文本输出带来很大的灵活性。用户可以通过调用各种GDI函数,制造出各种文本输出效果,包括加粗、斜体、设置颜色等。

Windows还提供了一种TrueType(写真字体)。TrueType字体用一组直线和曲线命令及一些参数来描述字体的轮廓。Windows可以通过参数来调整直线的长度和曲线的形状,从而实现对字体的自由缩放。

8.2.2 MFC中与GDI有关的类

为了支持GDI绘图,MFC提供了两种重要的类:设备上下文类,用于设置绘图属性和绘制图形;绘图对象类,封装了各种GDI绘图对象,包括画笔、刷子、字体、位图、调色板和区域。

设备上下文类

设备上下文类包括CDC和它的派生类CClientDC、CPaintDC、CWindowDC、CMetaFileDC。

CDC是设备上下文类的基类,除了一般的窗口显示外,还用于基于桌面的全屏幕绘制和非屏幕显示的打印机输出。CDC类封装了所有图形输出函数,包括矢量、光栅和文本输出。

CClientDC(客户区设备上下文)用于客户区的输出,它在构造函数中封装了GetDC(),在析构函数中封装了ReleaseDC()函数。一般在响应非窗口重画消息(如键盘输入时绘制文本、鼠标绘图)绘图时要用到它。用法是:

CClientDC dc(this);//this一般指向本窗口或当前活动视图

dc.TextOut(10,10,str,str.GetLength());

//利用dc输出文本,如果是在CScrollView中使用,还要注意调

//用OnPrepareDC(&dc)调整设备上下文的坐标。

CPaintDC用于响应窗口重绘消息(WM_PAINT)是的绘图输出。CPaintDC在构造函数中调用BeginPaint()取得设备上下文,在析构函数中调用EndPaint()释放设备上下文。EndPaint()除了释放设备上下文外,还负责从消息队列中清除WM_PAINT消息。因此,在处理窗口重画时,必须使用CPaintDC,否则WM_PAINT消息无法从消息队列中清除,将引起不断的窗口重画。CPaintDC也只能用在WM_PAINT消息处理之中。

CWindowDC用于窗口客户区和非客户区(包括窗口边框、标题栏、控制按钮等)的绘制。除非要自己绘制窗口边框和按钮(如一些CD播放程序等),否则一般不用它。

CMetaFileDC专门用于图元文件的绘制。图元文件记录一组GDI命令,可以通过这一组GDI命令重建图形输出。使用CMetaFileDC时,所有的图形输出命令会自动记录到一个与CMetaFileDC相关的图元文件中。

图形对象类

图形对象类包括CGdiObject、画笔、刷子、字体、位图、调色板、区域等。CGdiObject是图形对象类的基类,但该类不能直接为应用程序所使用。要使用GDI对象,必须使用它的派生类:画笔、刷子、字体、位图、区域等等。

使用图形对象要注意两点:

1.同其他MFC对象一样,GDI对象的创建也要分为两步:第一步,是定义一个GDI绘图对象类的实例;第二步调用该对象的创建方法真正创建对象。

2.创建对象:使用该对象,首先要调用CDC::SelectObject()将它选入到设备上下文中,同时保存原来的设置到一个GDI对象指针比如说pOldObject中。在使用完后,再用SelectObject(pOldObject)恢复原来的设置。但是,如果该设备上下文是用户自己创建的,则不必恢复原来设置,因为框架会在该设备上下文生存期结束时删除该设备上下文,同时也就删除了原来存放于该设备上下文中的绘图对象设置。

下面介绍各种对象的用法:

画笔(CPen):封装GDI画笔,可被选中设备上下文中当前所用得笔。画笔用于绘制对象的边框以及直线和曲线。缺省画笔画一条与一个像素等宽的黑色实线。

要使用画笔,首先要定义一个画笔:

CPen pen;

然后创建画笔。创建画笔有两种方法:

一是使用CPen::CreatePen(int nPenStyle,int nWidth,DWORD crColor)进行初始化。第一个参数是笔的风格。nPenStyle可选值有:

PS_SOLID 实线

PS_DOT 虚线

PS_INSIDEFRAME 在一个封闭形状的框架内画线,若设定的颜色不能在调色板种找到且线宽大于1,Windows会使用一种混色。

PS_NULL 空的画笔,什么也不画

第二个参数是线的宽度,按逻辑单位。若线宽设为0,则不管是什么映射模式下,线宽始终为一个像素。第三个参数是线的颜色,可以选16种VGA颜色中的一种。颜色的设置用一个RGB宏来指定。

RGB宏形式如下

COLORREF RGB(cRed,cGreen,cBlue)

cRed、cGreen、cBlue分别代表颜色的RGB三个分量,它们的取值在0-255之间。可以使用RGB组合成各种色彩。但是,这种表示法并不是很直观,因此我们把常见的RGB组合定义成新的宏并放在一个colors.h中,如清单8.1。

清单8.1 常见色彩定义

/*COLORS.H -常见色彩定义 */

#ifndef _COLORS_H

#define _COLORS_H

//Main Colors

#define WHITE RGB(255,255,255)

#define BLACK RGB(0,0,0)

#define DK_GRAY RGB(128,128,128)

#define LT_GRAY RGB(192,192,192)

//dark colors

#define DK_RED RGB(128,0,0)

#define DK_GREEN RGB(0,128,0)

#define DK_BLUE RGB(0,0,128)

#define DK_PURPLE RGB(128,0,128)

#define DK_YELLOW RGB(128,128,0)

#define DK_CYAN RGB(0,128,128)

//bright colors

#define BR_RED RGB(255,0,0)

#define BR_GREEN RGB(0,255,0)

#define BR_BLUE RGB(0,0,255)

#define BR_PURPLE RGB(255,0,255)

#define BR_YELLOW RGB(255,255,0)

#define BR_CYAN RGB(0,255,255)

#endif

这样,要指定一个淡黄色的宽度为逻辑单位1的实心笔,可以调用:

pen.CreatePen(PS_SOLID,1,BR_YELLOW);

创建笔的另一个方法是使用库存对象。SelectStockObject可从以下库存笔中选择一个:

BLACK_PEN 黑笔

NULL_PEN 空笔(不画线或边框)

WHITE_PEN 白笔

注:库存对象是由操作系统维护的用于绘制屏幕的常用对象,包括库存画笔、刷子、字体等。

刷子(CBrush):封装GDI刷子,可用作设备上下文中当前刷子。刷子用来填充一个封闭图形对象(如矩形、椭圆)的内部区域。缺省的刷子将封闭图形的内部填充成全白色。我们以前所创建的窗口内部都是白色就是窗口使用缺省刷子填充的结果。

可以用以下几种方法创建刷子:

(1)CreateSolidBrush(DWORD crColor)创建一个实心刷子,用一种颜色填充一个内部区域。

(2)CreateHatchBrush(int nIndex,DWORD crColor);创建一个带阴影的刷子,nIndex代表一种影线模式:

图8-2刷子的各种影线效果

(3)用CreatePatternBrush(CBitmap* pBitmap)

用一个位图作刷子,一般采用8X8的位图,因为刷子可以看作8X8的小位图。当Windows桌面背景采用图案(如weave)填充时,使用的就是这种位图刷子。

(4)同样可以使用SelectStockObject()从库存刷子中选取一个:

BLACK_BRUSH 黑色刷子

WHITE_BRUSH 白色刷子

DKGRAY_BRUSH 暗灰刷子

GRAY_BRUSH 灰色刷子

LTGRAY_BRUSH 淡灰色刷子

NULL_BRUSH 空刷子,内部不填充

字体(CFont):封装了GDI字体对象,用户可以建立一种GDI字体,并使用CFont的成员函数来访问它。关于CFont类,我们在前面已经作了一些介绍,这里不再赘述,读者可以参见前一章内容。

位图(CBitmap):封装一GDI位图,它提供成员函数装载和操作位图。

调色板(CPalette):封装GDI调色板,它保存着系统可用的色彩信息,是应用程序和彩色输出设备上下文的接口。

区域CRgn类:封装GDI区域。区域是窗口内的一块多边形或椭圆形的区域。CRgn用于设备环境(通常是窗口)内的区域操作。CRgn通常与CDC的有关剪裁(clipping)的成员函数配合使用。

有关位图和调色板的使用在第十一章“多媒体编程”还要再作详细阐述。

 

8.2.3常见的绘图任务

输出文本

字体大小计算:通过调用GetTextMetrics()返回当前使用字体的尺寸描述,如前面文 本编辑所演示的那样。

字体颜色设置:

设置前景色:CDC::SetTextColor(int nColor);

设置背景色:CDC::SetBkColor(int nColor);

例如:

dc.SetTextColor(WHITE);

dc.SetBkColor(DK_BLUE);

dc.TextOut(10,10,“White Text on blue background”,30);

文字输出:

除了我们前面介绍的文本输出函数TextOut()之外,还有其他几个函数可用于文本输 出:

TabbedTextOut:象TextOut一样显示正文,但用指定的制表间隔扩充制表键Tab。 在前面的文本编辑器中,当输入一个Tab时,TextOut在屏幕上输出一个黑色方块。

ExtTextOut:在指定的矩形中显示正文。可以用该函数删去超出矩形的正文,用正 文背景填充矩形,调整字符间隔。

DrawText:在指定矩形种显示正文,可以用这个函数扩展制表键Tab。在格式化矩 形时调整正文左对齐、右对齐或居中;还可以在一个词中断开以适应矩形边界。

画点

SetPixel在指定坐标处按指定色彩画一点。

画线

MoveToEx将直线起点移动到指定坐标处,LineTo从起点开始画直线到终点处。使 用的线型由当前所用画笔指定。

画弧

Arc(int x1,int y1,int x2,int y2,int x3,int y3,int x4,int y4);

 

 

图8-3 弧线的坐标定位

封闭图形

矩形Rectangle

圆角矩形RoundRect()

Ellipse在一个矩形内画椭圆

Chord弦形图

Pie画饼形图

Polygon生成封闭的多边形

PolyPolygon画完整的一组多边形

 

其它常用的绘图函数还有:

FillRect:用指定颜色填充矩形且不画边线

Draw3dRect:这是一个非常实用的函数,用于绘制各种3D边框。它的函数原型如下:

void Draw3dRect( LPCRECT lpRect, COLORREF clrTopLeft, COLORREF clrBottomRight );

void Draw3dRect( int x, int y, int cx, int cy, COLORREF clrTopLeft, COLORREF clrBottomRight );

通过设置上下边框的颜色clrTopLeft和clrBottomRight,可以绘制出凸出或 凹陷等各种效果的3D边框。

图8-4 Draw3dRect绘制3D边框

DrawFocusRect:用点线画一个矩形框,内部不填充,边线是用于屏幕上当前色的相反色画出来的,故第二次画时,会擦除原来所画的线。

ExtFloodFill:用给定的颜色,利用当前刷子填充表面被一个边线包围的区域,用户可以有选择地填充一个由指定颜色组成的区域。

FloodFill:用给定的颜色,利用当前所选的刷子填充显示的底面被一个边线所包围的区域,如多边形区域的填充。

FrameRect:绘制矩形边框时内部不填充。

InvertRect:在某一矩形区域内反显现有颜色。

 

8.3 绘图程序

在了解GDI的一些基本知识之后,我们就可以着手编写绘图程序了。这个绘图程序可以让读者用鼠标器在窗口内任意涂写,并可以保存所画的内容。这里我们参考了Visual C++的例子Scribble,并作了一些修改和简化。

8.3.1 MDI应用程序框架

首先用AppWizard生成绘图程序的基本框架:

选择File->New,弹出New对话框,选择MFC AppWizard(exe),并指定项目文件名为Draw。

在MFC AppWizard-Step1对话框中指定框架类型为Multiple Document(多文档,这是缺省设置)。

Step2,3按缺省值。在MFC AppWizard Step 4 of 6对话框中,点“Advanced...”按钮,弹出Advanced Options对话框。在File Extension编辑框中指定文件名后缀为.drw,按OK关闭Advanced Options对话框。

Step5按缺省设置。在MFC AppWizard Step 6 of 6中,在应用程序所包含的类列表中选择CDrawView,并为其指定基类为CScrollView,因为绘图程序需要卷滚文档。现在点Finish按钮生成绘图所需的应用程序框架。

在往框架里添加代码实现绘图程序之前,先看看多文档框架与单文档框架的差别。

AppWizard为多文档框架创建了以下类:

CAboutDlg:“关于”对话框

CChildFrame:子框架窗口,用于容纳视图

CDrawApp:应用程序类

CDrawDoc:绘图程序视图类

CDrawView:绘图视图类

CMainFrame:主框架窗口,用来容纳子窗口,它是多文档应用程序的主窗口。

在生成的类上,MDI比SDI多了一个CChildFrame子框架窗口类,而且CMainFrame的职责也不同了。

另外,MDI和SDI在初始化应用程序实例上也有所不同。MDI应用程序InitInstance函数如清单8.2定义。

清单8.2 多文档程序的InitInstance成员函数定义

BOOL CDrawApp::InitInstance()

{

//一些初始化工作......

// Register the application's document templates. Document templates

// serve as the connection between documents, frame windows and views.

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_DRAWTYPE,

RUNTIME_CLASS(CDrawDoc),

RUNTIME_CLASS(CChildFrame), // custom MDI child frame

RUNTIME_CLASS(CDrawView));

AddDocTemplate(pDocTemplate);

// create main MDI Frame window

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

// Enable drag/drop open

m_pMainWnd->DragAcceptFiles();

// Enable DDE Execute open

EnableShellOpen();

RegisterShellFileTypes(TRUE);

// Parse command line for standard shell commands, DDE, file open

CCommandLineInfo cmdInfo;

ParseCommandLine(cmdInfo);

// Dispatch commands specified on the command line

if (!ProcessShellCommand(cmdInfo))

return FALSE;

// The main window has been initialized, so show and update it.

pMainFrame->ShowWindow(m_nCmdShow);

pMainFrame->UpdateWindow();

return TRUE;

}

在注册文档模板时,首先创建一个CMultiDocTemplate类型(在SDI下是CSingleDocTemplate)的模板对象,然后用AddDocTemplate()把它加入到文档模板链表中去。

CMultiDocTemplate构造函数带四个参数,第一个参数是文档使用的资源ID定义。第二个是文档类型,第三个是子窗口类型,第四个是视图类型。

与SDI不同,由于MDI的主框架窗口并不直接与文档相对应,因此无法通过创建文档来创建主框架窗口,而需要自己去创建。

//定义一个主窗口类指针,并创建一个窗口的空的实例

CMainFrame* pMainFrame = new CMainFrame;

//从资源文件中载入菜单、图标等信息,并创建窗口

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

//将应用程序对象的主窗口指针数据成员设为当前创建的窗口

m_pMainWnd = pMainFrame;

 

8.3.2 设计绘图程序的文档类

Draw需要保存用户在屏幕上涂写的每一个笔划。一副画由许多笔划组成,可以把它看作是笔划组成的链表。每一个笔划可以看作一个对象,它由许多点组成。这样,我们可以把绘图文档的数据看作是笔划对象CStroke组成的链表。另外,我们还需要一些数据成员表示当前画图所使用的画笔和画笔的宽度。

修改后的文档类声明文件如清单8-1:

清单8.3文档类声明

// DrawDoc.h : interface of the CDrawDoc class

//

/

#if !defined(AFX_DRAWDOC_H__143330AE_85BC_11D1_9304_444553540000__INCLUDED_)

#define AFX_DRAWDOC_H__143330AE_85BC_11D1_9304_444553540000__INCLUDED_

#if _MSC_VER >= 1000

#pragma once

#endif // _MSC_VER >= 1000

 

class CDrawDoc : public CDocument

{

protected: // create from serialization only

CDrawDoc();

DECLARE_DYNCREATE(CDrawDoc)

// Attributes

 

public:

UINT m_nPenWidth; // current user-selected pen width

CPen m_penCur; // pen created according to

// user-selected pen style (width)

public:

CTypedPtrList m_strokeList;

//获取当前使用的画笔,为视图所使用

CPen* GetCurrentPen() { return &m_penCur; }

protected:

CSize m_sizeDoc;

public:

CSize GetDocSize() { return m_sizeDoc; }

// Operations

public:

//往链表里增加一个笔划

CStroke* NewStroke();

// Operations

//用于初始化文档

protected:

void InitDocument();

// Overrides

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CDrawDoc)

public:

virtual BOOL OnNewDocument();

virtual void Serialize(CArchive& ar);

//}}AFX_VIRTUAL

// Implementation

public:

virtual ~CDrawDoc();

#ifdef _DEBUG

virtual void AssertValid() const;

virtual void Dump(CDumpContext& dc) const;

#endif

protected:

// Generated message map functions

protected:

//{{AFX_MSG(CDrawDoc)

// NOTE - the ClassWizard will add and remove member functions here.

// DO NOT EDIT what you see in these blocks of generated code !

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

};

 

这里我们使用 指针链表模板来保存指向每个笔划的指针:

CTypedPtrList m_strokeList;

其中“<>”第一个参数表示链表基本类型,第二个参数代表链表中所存放的元素的类型。

为了使用模板,还要修改stdafx.h,在其中加入afxtempl..h头文件,它包含了使用模板时所需的类型定义和宏:

//.........

#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers

#include // MFC core and standard components

#include // MFC extensions

 

#include // MFC templates

#include // MFC OLE automation classes

#ifndef _AFX_NO_AFXCMN_SUPPORT

#include // MFC support for Windows Common Controls

#endif // _AFX_NO_AFXCMN_SUPPORT

//......

由于绘图程序需要卷滚文档,因此象前面的编辑那样,增加一个m_sizeDoc数据成员存放文档的大小。另外,还需要提供一个GetDocSize()来访问它。NewStroke()用于往链表里增加一个笔划。

现在,开始设计CStroke类。笔划可以看作由一系列点组成,这样CStroke可以用一个点的数组来表示。另外,还需要一些成员函数来访问这个数组。我们还希望笔划能够自己绘制自己,并用串行化机制保存自己的数据。

CStroke类定义清单如8.4,我们把它在CDrawDoc类定义之前。

清单8.4 CStroke类定义

class CStroke : public CObject

{

public:

CStroke(UINT nPenWidth);//用笔的宽度构造一个画笔

//用于串行化笔划对象

protected:

CStroke(); //串行化对象所需的不带参数的构造函数

DECLARE_SERIAL(CStroke)

// Attributes

protected:

UINT m_nPenWidth; // one pen width applies to entire stroke

public:

//用数组模板类保存笔划的所有点

CArray m_pointArray; // series of connected points

//包围笔划所有的点的一个最小矩形,关于它的作用以后会提到

CRect m_rectBounding; // smallest rect that surrounds all

// of the points in the stroke

// measured in MM_LOENGLISH units

// (0.01 inches, with Y-axis inverted)

public:

CRect& GetBoundingRect() { return m_rectBounding; }

//结束笔划,计算最小矩形

void FinishStroke();

// Operations

public:

//绘制笔划

BOOL DrawStroke(CDC* pDC);

public:

virtual void Serialize(CArchive& ar);

};

文档的初始化

文档的初始化在OnNewDocument()和OnOpenDocument()中完成。对于Draw程序来说,两者的初始化相同,因此设计一个InitDocument()函数用于文档初始化:

void CDrawDoc::InitDocument()

{

m_nPenWidth=2;

m_nPenCur.CreatePen(PS_SOLID,m_nPenWidth,RGB(0,0,0));

//缺省文档大小设置为800X900个逻辑单位

m_sizeDoc = CSize(800,900);

}

InitDocument()函数将笔的宽度初值设为2,然后创建一个画笔对象。该对象在以后绘图是要用到。最后将文档尺寸大小设置为800X900个逻辑单位。

然后在OnNewDocument()和OnOpenDocument()中调用它:

void CDrawDoc::OnNewDocument()

{

if (!CDocument::OnNewDocument())

return FALSE;

// TODO: add reinitialization code here

// (SDI documents will reuse this document)

 

InitDocument();

return TRUE;

}

AppWizard并没有生成OnOpenDocument()的代码,因此要用ClassWizard来生成OnOpenDocument()的框架。生成框架后,在其中加入代码:

BOOL CDrawDoc::OnOpenDocument(LPCTSTR lpszPathName)

{

if (!CDocument::OnOpenDocument(lpszPathName))

return FALSE;

 

// TODO: Add your specialized creation code here

 

InitDocument();

return TRUE;

}

文档的清理

在关闭文档的最后一个子窗口时,框架要求文档清理数据。文档清理在文档类的DeleteContents()中完成。同样需要用ClassWizard生成DeleteContents的框架。

void CDrawDoc::DeleteContents()

{

// TODO: Add your specialized code here and/or call the base class

 

while (!m_strokeList.IsEmpty())

{

delete m_strokeList.RemoveHead();

}

CDocument::DeleteContents();

}

DeleteContents()从头到尾遍里链表中的所有对象指针,并通过指针删除对象,然后用RemoveHead()删除该指针。

文档的串行化

现在设计文档的Serialize函数,实现文档数据的保存和载入:

void CDrawDoc::Serialize(CArchive& ar)

{

if (ar.IsStoring())

{

 

ar << m_sizeDoc;

}

else

{

 

ar >> m_sizeDoc;

}

 

m_strokeList.Serialize(ar);

}

文档的Serialize()函数首先分别保存和载入文档大小,然后调用m_strokeList的Serialize()方法。m_strokeList.Serialize()又会自动调用存放在m_strokeList中的每一个元素CStroke的串行化方法CStroke.Serialize()最终实现文档的串行化即文档所包含的对象的存储和载入。

在DrawDoc.cpp的末尾加上CStroke::Serialize()函数的定义:

void CStroke::Serialize(CArchive& ar)

{

if (ar.IsStoring())

{

ar << m_rectBounding;

ar << (WORD)m_nPenWidth;

m_pointArray.Serialize(ar);

}

else

{

ar >> m_rectBounding;

WORD w;

ar >> w;

m_nPenWidth = w;

m_pointArray.Serialize(ar);

}

}

CStroke的Serialize()依次保存(载入)笔划的矩形边界、线宽度以及点数组。注意m_nPenWidth是UINT类型的,>>和<<操作符并不支持UINT类型但却支持WORD,因此要作UINT和DWORD之间的类型转换。点数组的串行化通过调用数组的每个CPoint类元素的Serialize()完成,CPoint类是MFC类,它本身支持串行化。

8.3.3 设计绘图程序的视图类

视图类数据成员

现在着手设计绘图程序的视图类。首先,需要在视图中增加两个数据成员:

class CDrawView : public CScrollView

{

protected: // create from serialization only

CDrawView();

DECLARE_DYNCREATE(CDrawView)

// Attributes

public:

CDrawDoc* GetDocument();

 

protected:

CStroke* m_pStrokeCur; // the stroke in progress

CPoint m_ptPrev; // the last mouse pt in the stroke in progress

// 其它数据成员和成员函数......

};

m_pStrokeCur代表正在画的那一个笔划。m_ptPrev保存鼠标上次移动位置。画图时,LineTo从这个点到当前鼠标位置画一条直线。

视图初始化

接下去,要初始化视图。由于是卷滚视图,因此要在OnInitialUpdate()中设置卷滚范围。在用户选择File->New菜单或File->Open菜单时,框架调用OnInitialUpdate函数。

void CDrawView::OnInitialUpdate()

{

 

SetScrollSizes(MM_LOENGLISH, GetDocument()->GetDocSize());

CScrollView::OnInitialUpdate();

}

注意我们这里将映射模式设置为MM_LOENGLISH,MM_LOENGLISH以0.01英寸为逻辑单位,y轴方向向上递增,同MM_TEXT的y轴递增方向相反。

视图绘制

在CDrawView::OnDraw()内完成视图绘制工作。在以前的文档视结构程序中,在需要绘图的时侯都是绘制整个窗口。如果窗口只有很小的一部分被覆盖,是否可以只绘制那些需要重画的部分?

回答是肯定的,而且大部分程序都这么做了。

比如,象下图这种情况:

 

图8-5 窗口的重绘

当窗口2从窗口1上移开后,只需要重画阴影线所包围的区域就够了。

当Windows通知窗口要重绘用户区时,并非整个用户区都需要重绘,需要重绘的区域称为“无效矩形区”,如上图中的阴影区域。用户区中出现一个无效矩形提示Windows在应用程序队列中放置WM_PAINT消息。由于WM_PAINT消息优先级最低,可调用UpdateWindows直接立即向窗口发送WM_PAINT消息,从而立即重绘。无效矩形区限制程序只能在该区域中绘图,越界的绘图将被裁剪掉。下面三个函数与无效矩形有关:

InvalidateRect 产生一个无效矩形,并生成WM_PAINT消息

ValidateRect 使无效矩形区有效

GetUpdateRect 获得无效矩形坐标(逻辑)

Windows为每个窗口保留一个PAINTSTRUCT结构,其中包含无效矩形区域的坐标值。

要想在自己的程序高效绘图、只绘制无效矩形,首先需要重载视图的OnUpdate成员函数。

virtual void CView::OnUpdate( CView* pSender, LPARAM lHint, CObject* pHint );

当调用文档的UpdateAllViews时,框架会自动调用OnUpdate函数,也可在视图类中直接调用该函数。OnUpdate函数一般是这样处理的:访问文档,读取文档的数据,然后对视图的数据成员或控制进行更新,以反映文档的改动。可以用OnUpdate函数使视图的某部分无效。以便触发视的OnDraw,利用文档数据重绘窗口。缺省的OnUpdate使窗口整个客户区都无效,在重新设计时,要利用提示信息lHint和pHint定义一个较小的无效矩形。修改后的OnUpdate成员函数如清单8.5。

清单8.5 修改后的OnUpdate成员函数

void CDrawView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

{

// TODO: Add your specialized code here and/or call the base class

// The document has informed this view that some data has changed.

 

if (pHint != NULL)

{

if (pHint->IsKindOf(RUNTIME_CLASS(CStroke)))

{

// The hint is that a stroke as been added (or changed).

// So, invalidate its rectangle.

CStroke* pStroke = (CStroke*)pHint;

CClientDC dc(this);

OnPrepareDC(&dc);

CRect rectInvalid = pStroke->GetBoundingRect();

dc.LPtoDP(&rectInvalid);

InvalidateRect(&rectInvalid);

return;

}

}

// We can't interpret the hint, so assume that anything might

// have been updated.

Invalidate(TRUE);

return;

}

这里,传给pHint指针的内容是指向需要绘制的笔画对象的指针。采用强制类型转换将它转换为笔划指针,然后取得包围该笔划的最小矩形。OnPrepareDC用于调整视图坐标原点。由于InvalidateRect需要设备坐标,因此调用LPToDP(&rectInvalid)将逻辑坐标转换为设备坐标。最后,调用InvalidateRect是窗口部分区域“无效”,也就是视图在收到WM_PAINT消息后需要重绘这一区域。

InvalidateRect函数原型为:

void InvalidateRect( LPCRECT lpRect, BOOL bErase = TRUE );

第一个参数是指向要重绘的矩形的指针,第二个参数告诉视图是否要删除区域内的背景。

这样,当需要重画某一笔划时,只需要重画包围笔划的最小矩形部分就可以了,其他部分就不再重绘。这也是为什么在笔划对象中提供最小矩形信息的原因。

如果pHint为空,则表明是一般的重绘,此时需要重绘整个客户区。

现在,在OnDraw中,根据无效矩形绘制图形,而不是重绘全部笔划,见清单8.6。

清单8.6 根据无效矩形绘制图形的OnDraw成员函数

void CDrawView::OnDraw(CDC* pDC)

{

CDrawDoc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

 

// Get the invalidated rectangle of the view, or in the case

// of printing, the clipping region of the printer dc.

CRect rectClip;

CRect rectStroke;

pDC->GetClipBox(&rectClip);

pDC->LPtoDP(&rectClip);

rectClip.InflateRect(1, 1); // avoid rounding to nothing

// Note: CScrollView::OnPaint() will have already adjusted the

// viewport origin before calling OnDraw(), to reflect the

// currently scrolled position.

// The view delegates the drawing of individual strokes to

// CStroke::DrawStroke().

CTypedPtrList& strokeList = pDoc->m_strokeList;

POSITION pos = strokeList.GetHeadPosition();

while (pos != NULL)

{

CStroke* pStroke = strokeList.GetNext(pos);

rectStroke = pStroke->GetBoundingRect();

pDC->LPtoDP(&rectStroke);

rectStroke.InflateRect(1, 1); // avoid rounding to nothing

if (!rectStroke.IntersectRect(&rectStroke, &rectClip))

continue;

pStroke->DrawStroke(pDC);

}

// TODO: add draw code for native data here

}

OnDraw首先调用GetClipBox取得当前被剪裁区域(无效矩形区域),它把矩形复制导GetClipBox的参数rectClip中。然后将rectClip的坐标由逻辑坐标转换为设备坐标。为了防止该矩形太小而无法包围其他内容,上下各放大一个单位。然后OnDraw遍历笔划链表中的所有笔划,获取它们的最小矩形,用IntersectRect看它是否与无效矩形相交。如果相交,说明笔划的部分或全部落在无效矩形中,此时调用笔划的DrawStroke方法画出该笔划。

图8-6 根据包围笔划 的矩形是否与无效

矩形相交 ,判断笔划是否落入无效矩形中

为了获得笔划的最小包围矩形,需要在结束笔划时计算出包围笔划的最小矩形。因此为笔划提供两个方法:一个是FinishStroke(),用于在笔划结束时计算最小矩形,见清单8.7。

清单8.7 CStroke::FinishStroke()成员函数

void CStroke::FinishStroke()

{

// Calculate the bounding rectangle. It's needed for smart

// repainting.

if (m_pointArray.GetSize()==0)

{

m_rectBounding.SetRectEmpty();

return;

}

CPoint pt = m_pointArray[0];

m_rectBounding = CRect(pt.x, pt.y, pt.x, pt.y);

for (int i=1; i < m_pointArray.GetSize(); i++)

{

// If the point lies outside of the accumulated bounding

// rectangle, then inflate the bounding rect to include it.

pt = m_pointArray[i];

m_rectBounding.left = min(m_rectBounding.left, pt.x);

m_rectBounding.right = max(m_rectBounding.right, pt.x);

m_rectBounding.top = max(m_rectBounding.top, pt.y);

m_rectBounding.bottom = min(m_rectBounding.bottom, pt.y);

}

// Add the pen width to the bounding rectangle. This is necessary

// to account for the width of the stroke when invalidating

// the screen.

m_rectBounding.InflateRect(CSize(m_nPenWidth, -(int)m_nPenWidth));

return;

}

另一个是DrawStroke(),用于绘制笔划:

BOOL CStroke::DrawStroke(CDC* pDC)

{

CPen penStroke;

if (!penStroke.CreatePen(PS_SOLID, m_nPenWidth, RGB(0,0,0)))

return FALSE;

CPen* pOldPen = pDC->SelectObject(&penStroke);

pDC->MoveTo(m_pointArray[0]);

for (int i=1; i < m_pointArray.GetSize(); i++)

{

pDC->LineTo(m_pointArray[i]);

}

pDC->SelectObject(pOldPen);

return TRUE;

}

鼠标绘图

鼠标绘图基本过程是:用户按下鼠标左键时开始绘图,在鼠标左键按下且移动过程中不断画线跟踪鼠标位置,当松开鼠标左键结束绘图。因此,需要处理三个消息:WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_LBUTTONUP。用ClassWizard为上述三个消息生成消息处理函数,并在其中手工加入代码,修改后的成员函数如下:

 

清单8.8 鼠标消息处理函数OnLButtonDown()

void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

// Pressing the mouse button in the view window starts a new stroke

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

CClientDC dc(this);

OnPrepareDC(&dc);

dc.DPtoLP(&point);

m_pStrokeCur = GetDocument()->NewStroke();

// Add first point to the new stroke

m_pStrokeCur->m_pointArray.Add(point);

SetCapture(); // Capture the mouse until button up.

m_ptPrev = point; // Serves as the MoveTo() anchor point for the

// LineTo() the next point, as the user drags the

// mouse.

return;

}

 

在鼠标左键按下,首先获得鼠标按下的位置坐标。由于它是设备坐标,因此先用DPToLP将它转换为逻辑坐标。在此之前,要用OnPrepareDC()对视图坐标原点进行调整。然后用CDrawDoc的NewStroke()成员函数创建一个笔划对象,并将笔划对象加入到笔划链表中。然后,将当前点坐标加入道笔划对象内部的点数组中。以后,当鼠标移动时,OnMouseMove就不断修改该笔划对象的内部数据成员(加入新的点到笔划对象的数组中)。另外,为了用LineTo画出线条,需要将当前鼠标位置保存到m_ptPrev中,以便出现一个新的点时,画一条从m_ptPrev到新的点的直线。

但是,由于用户的鼠标可以在屏幕上任意移动。当鼠标移出窗口外时,窗口无法收到鼠标消息。此时,如果松开了鼠标左键,应用程序由于无法接受到该条消息而不会终止当前笔划,这样就造成了错误。如何避免这种情况发生呢?解决的办法是要让窗口在鼠标移出窗口外时仍然能接受到鼠标消息。幸好,Windows提供了一个API函数SetCapture()解决了这一问题。

CWnd::SetCapture()用于捕获鼠标:无论鼠标光标位置在何处,都会将鼠标消息送给调用它的那一个窗口。在用完后,需要用ReleaseCapture()释放窗口对鼠标的控制,否则其他窗口将无法接收到鼠标消息。这一工作当然最好在鼠标左键松开OnLButtonUp()时来做。

清单8.9 OnLButtonUp消息处理函数

void CDrawView::OnLButtonUp(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

 

// Mouse button up is interesting in the draw application

// only if the user is currently drawing a new stroke by dragging

// the captured mouse.

if (GetCapture() != this)

return; // If this window (view) didn't capture the mouse,

// then the user isn't drawing in this window.

CDrawDoc* pDoc = GetDocument();

CClientDC dc(this);

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

OnPrepareDC(&dc); // set up mapping mode and viewport origin

dc.DPtoLP(&point);

CPen* pOldPen = dc.SelectObject(pDoc->GetCurrentPen());

dc.MoveTo(m_ptPrev);

dc.LineTo(point);

dc.SelectObject(pOldPen);

m_pStrokeCur->m_pointArray.Add(point);

// Tell the stroke item that we're done adding points to it.

// This is so it can finish computing its bounding rectangle.

m_pStrokeCur->FinishStroke();

// Tell the other views that this stroke has been added

// so that they can invalidate this stroke's area in their

// client area.

pDoc->UpdateAllViews(this, 0L, m_pStrokeCur);

ReleaseCapture(); // Release the mouse capture established at

// the beginning of the mouse drag.

return;

}

 

OnLButtonUp首先检查鼠标是否被当前窗口所捕获,如果不是则返回。然后画出笔划最后两点之间的极短的直线段。接着,调用CStroke::FinishStroke(),请求CStroke对象计算它的最小矩形。然后调用pDoc->UpdateAllViews(this, 0L, m_pStrokeCur)通知其他视图更新显示。

当一个视图修改了文档内容并更新显示时,一般的其它的对应于同一文档的视图也需要相应更新,这通过调用文档的成员函数UpdateAllViews完成。

void UpdateAllViews( CView* pSender, LPARAM lHint = 0L, CObject* pHint =

NULL );

UpdateAllViews带三个参数:pSender指向修改文档的视图。由于该视图已经作了更新,所以不再需要更新。比如,在上面的例子中,OnLButtonUp已经绘制了视图,因此不需要再次更新。如果为NULL,则文档对应的所有视图都被更新。

lHint和pHint包含了更新视图时所需的附加信息。在本例中,其他视图只需要重画当前绘制中的笔划,因此OnLButtonUp把当前笔划指针传给UpdateAllViews函数。该函数调用文档所对应的除pSender外的所有视图的OnUpdate函数,并将lHint和pHint传给OnUpdate函数通知更新附加信息。

OnLButtonUp最后释放对鼠标的控制,这样别的应用程序窗口就可以获得鼠标消息了。

结合上面讲到的知识,读者不难自行理解下面的OnMouseMove函数。

void CDrawView::OnMouseMove(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

// Mouse movement is interesting in the Scribble application

// only if the user is currently drawing a new stroke by dragging

// the captured mouse.

if (GetCapture() != this)

return; // If this window (view) didn't capture the mouse,

// then the user isn't drawing in this window.

CClientDC dc(this);

// CScrollView changes the viewport origin and mapping mode.

// It's necessary to convert the point from device coordinates

// to logical coordinates, such as are stored in the document.

OnPrepareDC(&dc);

dc.DPtoLP(&point);

m_pStrokeCur->m_pointArray.Add(point);

// Draw a line from the previous detected point in the mouse

// drag to the current point.

CPen* pOldPen = dc.SelectObject(GetDocument()->GetCurrentPen());

dc.MoveTo(m_ptPrev);

dc.LineTo(point);

dc.SelectObject(pOldPen);

m_ptPrev = point;

return;

}

至此,绘图程序的文档、视图全部设计完了,现在编译运行程序。程序启动后,在空白窗口中徒手绘图,如图8-7所示。

图8-7 多文档绘图程序窗口

8.4访问当前活动视图和活动文档

对于SDI程序,主框架窗口就是文档框窗,可以采用以下方法取得当前文档和视图:

取得活动文档:

CMyDocument* pDoc;

pDoc=(CMyDocument*)((CFrameWnd*)AfxGetApp()->m_pMainWnd)

->GetActiveDocument();

pDoc=(CMyDocument*)((CFrameWnd*)AfxGetMainWnd());

这两者是等效的。

取得活动视图:

CMyView* pView;

pView=(CMyView*)((CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveView();

对于MDI程序,由于子窗口才是文档框窗,因此首先要用GetActiveFrame()取得活动子框架窗口,然后通过该子窗口获取活动文档和视图:

CMDIChildWnd* pChild=(CMDIChildWnd*)((CFrameWnd*)AfxGetApp()->m_pMainWnd)->GetActiveFrame();

取得活动文档:

CMyDocument* pDoc=pChild->GetActiveDocument();

CMyView* pView=(CMyView*)pChild->GetActiveView();

可以把上述函数片段做成静态成员函数,比如:

static CMyDocument::GetCurrentDoc()

{

CMDIChildWnd* pChild=(CMDIChildWnd*)((CFrameWnd*)AfxGetApp()- >m_pMainWnd)->GetActiveFrame();

CMyDocument* pDoc=pChild->GetActiveDocument();

}

这样就可以通过以下方式取得活动文档(或视图):

CMyDocument::GetCurrentDoc();

注:静态成员函数调用时不需要一个具体的对象与之相关联。

8.5分割视图

  分割窗口将窗口分成几个部分,每个部分通常代表一个视图(但也可以是具有子窗口标识的CWnd对象),又称窗格。如图8-8所示。如果想在一个窗口里面观察文档的不同部分,或者是在一个窗口里用不同类型的视图(比如用图表和表格)观察同一个文档,那么采用分割窗口是非常方便的。许多优秀的软件都采用了分割窗口技术,因此我们有必要掌握分割窗口的用法。

图8-8 分割窗口

分割窗口分为两类:动态分割窗口和静态分割窗口。

动态分割窗口是指:用户可以动态的分割和除去分割窗口,分割窗口会创建和删除相应的窗格。Microsoft Word就是使用动态分割窗口的例子,这是一种最常用的分割窗口。动态分割窗口最多可以有2行´ 2列个窗格。

静态分割窗口是指:在窗口创建时,分割窗口的窗格就已经创建好了,且窗格的数量和顺序不会改变。窗格为一个分割条所分割,用户可以拖动分割条调整相应的窗格的大小。如图8-9,Visual Studio的图标编辑器就是静态分割窗口的例子。在编辑器的左边窗格,显示图标的缩微图像,在右边显示图标的编辑窗口,可以拖动中间的分割条调整两个窗格的大小。静态分割窗口最多可以有16行´ 16列的窗格。

图8-9 图标编辑器—静态分割窗口的例子

这里我们只介绍动态分割窗口的使用,有关静态分割窗口的用法,读者可以参考Visual C++的例子VIEWEX,它在SAMPLES/MFC/GENERAL /VIEWEX目录下。

要使文档视结构程序支持动态分割窗口,可以有三种方法:

1.在用AppWizard创建窗口时指定分割窗口风格:

在MFC AppWizard Step 4 of 6对话框中,点Advanced按钮。弹出Advanced Options对话框,选择Window Styles标签页。如图8-10,选中该页的Use Split Window检查框。这样生成的应用程序就自动支持分割窗口功能。

图8-10 Window Styles设置

如果应用程序已经生成,采用这种方法就不合适了。此时,可以使用下面的两种方法:

2.使用Component Gallery为已经生成的应用程序增加分割窗口功能:

打开相应的工程文件。选择Project-Add To Project-Components and controls菜单,弹出

Components and controls Gallery对话框。双击Developer Studio Components目录,从该目录下选择split Bars控件。Visual C++提示split Bar对话框,对话框内有三个选项:Horizontal,Vertical和Both,用于指定在水平方向、垂直方向还是两个方向都使用分割窗口。选择Both,点OK关闭Split Bar对话框,此时Component Gallery就将分割窗口功能添加到了Draw程序中。再点OK关闭Components and controls Gallery对话框。然后浏览应用程序类,看有什么变化。

在childfrm.h中,增加了以下内容:

// Generated message map functions

protected:

CSplitterWnd m_wndSplitter;

virtual BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext);

m_wndSplitter是一个CSplitterWnd类的对象。CSplitterWnd是MFC提供的一个类,它提供了窗格分割控制,以及能被所有同一行或列上的窗格共享的滚动条。这些行和列的值都是从0开始的整数,第一个窗格的行数和列数都为0。

另外还重载了子框架窗口的OnCreateClient方法。在该函数内部,创建了分割窗口控制:

BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)

{

// CG: The following block was added by the Split Bars component.

{

if (!m_wndSplitter.Create(this,

2, 2, // TODO: adjust the number of rows, columns

CSize(10, 10), // TODO: adjust the minimum pane size

pContext))

{

TRACE0("Failed to create split bar ");

return FALSE; // failed to create

}

return TRUE;

}

}

CSplitterWnd::Create方法带五个参数,第一个参数代表父窗口指针,第二个参数和第三个参数告诉CSplitterWnd要多少行、多少列的窗格,第四个参数是一个CSize类型的数据,用于指定窗格的最小大小。

第三种方法是手工加入代码:

在需要分割视图的框架窗口中加入一个CSplitterWnd类型的数据成员;用ClassWizard重载框架窗口的OnCreateClient方法,在OnCreateClient方法的实现中,加入上面的代码.

8.6 打印和打印预览

  最后,我们要给绘图程序增加打印和打印预览功能。我们希望文档分两页打印,第一页为封面,打印文档名字。第二页输出文档内容,并在页眉上打印文档名字。虽然AppWizard已经自动生成了打印和打印预览的代码,但是许多情况下,并不能符合要求。

这是因为:

1.打印机和窗口(屏幕)显示的分辨率不同:打印机的分辨率用每英寸多少个点来描述,屏幕分辨率用单位面积的像素点来表示。对于同样的Arial字体下的一个字符,在屏幕上用20个像素表示,而在打印机上则需要50点。在编辑器程序中,使用的映射模式为MM_TEXT,在这种模式下,一个逻辑单位对应于一个像素点。我们已经知道,Windows是按照逻辑单位来绘图的。这样,根据MM_TEXT模式的逻辑单位(实际上也就是像素数目)决定比例的原则打印出来得内容自然要比屏幕上看到的要小的多。因此,前面在初始化视图OnInitialUpdate时候,在选择绘图的映射模式上,没有采用以前使用的缺省的MM_TEXT模式,而是采用了MM_LOENGLISH。

2.窗口和打印机对边界的处理不同:窗口可以看作是无边界的,可以在窗口外面画,而不会引起错误,窗口会自动剪裁超出边界的图形。但打印机却不同,它是按页打印的。打印输出时必须自己处理分页和换页,如果不作这样的处理的话,行和行之间就会叠加起来。

  要正确打印输出屏幕上的内容,就必须解决以上两个问题。对于第一个问题,有两种方法:一是利用SetMapMode(int nMode) 设置别的映射模式,比如采用MM_LOENGLISH,不用像素而是采用0.01inch来衡量。

  要处理打印分页、换页,就必须修改框架处理打印消息的缺省行为,在其中计算和换页。此外,我们还希望在打印时在页眉处能够输出标题(使用文件名作为标题)、在页脚处输出页码。

  为了实现打印和打印预览功能,首先需要了解MFC的打印体系结构,即框架是如何处理打印文档的要求的。

  MFC的打印工作大致上是这样进行的:

1.显示Print对话框

2.创建一个与当前打印机设置相匹配的设备上下文(CDC)对象。

3.设置要打印的页数

4.调用CDC::StartDoc开始打印

5.用CDC::StartPage开始打印一页

6.调用视图的OnDraw()方法打印输出一页内容

7.用CDC::EndPage结束一页的打印

8.循环输出全部内容

9.用CDC::EndDoc结束打印

10.视图作打印的清理工作

  框架的打印文档功能是从OnPreparePrinting(CPrintInfo* pInfo)开始的,在缺省的情况下,它只是简单的调用视图的DoPreparePrinting()函数。DoPreparePrinting()显示Print对话框,并创建与打印机相匹配的设备上下文。如果要想改变打印机初始设置,可以在这里改。缺省设置下,使用1作为第一页编号(注意:打印的页号是从1开始编号而不是0),用0xFFFF作为文档的最后一页编号。因为Draw要求分两页打印输出,因此要在这里设置打印页数。要设置打印页数,可以调用CPrintInfo::SetMaxPage(nMaxPage)。同时还将预览页数也设置为两页。

BOOL CDrawView::OnPreparePrinting(CPrintInfo* pInfo)

{

 

pInfo->SetMaxPage(2); // the document is two pages long:

// the first page is the title page

// the second is the drawing

BOOL bRet = DoPreparePrinting(pInfo); // default preparation

pInfo->m_nNumPreviewPages = 2; // Preview 2 pages at a time

// Set this value after calling DoPreparePrinting to override

// value read from .INI file

return bRet;

}

  DoPreparePrinting显示Print对话框。返回时,CPrintInfo结构包含了用户所指定的值,包括起止页号、最大页号、最小页号等。

  OnBeginPrinting()在OnPreparePrinting()被调用之后实际打印之前调用。OnBeginPrinting()用于分配GDI资源,这里使用缺省行为。

  OnPrepareDC用作屏幕显示时,在绘图前调整DC。在用于打印时,OnPrepareDC也完成类似功能。

  OnPrint完成真正的打印一页文档的工作。它把一个打印机设备上下文传给OnDraw,由OnDraw负责打印输出。可以把那些适合于打印但是不适合于屏幕输出的工作,如打印页眉和页脚,放在OnPrint()的重载中完成,然后再调用OnDraw完成打印和显示都需要的工作。现在,我们就在OnPrint中加入打印页眉和页脚的代码。OnPrint不是由AppWizard自动生成的,首先要用ClassWizard为CDrawView增加OnPrint()方法。然后添加绘图程序的特殊打印代码,见清单8.10。

 

清单8.10 OnPrint()成员函数

void CDrawView::OnPrint(CDC* pDC, CPrintInfo* pInfo)

{

 

// TODO: Add your specialized code here and/or call the base class

if (pInfo->m_nCurPage == 1) // page no. 1 is the title page

{

PrintTitlePage(pDC, pInfo);

return; // nothing else to print on page 1 but the page title

}

CString strHeader = GetDocument()->GetTitle();

PrintPageHeader(pDC, pInfo, strHeader);

// PrintPageHeader() subtracts out from the pInfo->m_rectDraw the

// amount of the page used for the header.

pDC->SetWindowOrg(pInfo->m_rectDraw.left,-pInfo->m_rectDraw.top);

// Now print the rest of the page

OnDraw(pDC);

}

  OnPrint()首先根据CPrintInfo类型的pInfo中m_nCurPage(保存当前打印页号信息)判断当前打印的是不是第一页。如果是第一页,就打印输出封面。否则,首先调用PrintPageHeader打印页眉。然后用SetWindowOrg调整打印输出原点位置。m_rectDraw又是CPrintInfo结构的一个重要数据成员,它保存的是打印输出的矩形边界。最后将与打印机匹配的设备上下文传给OnDraw,由OnDraw在打印机上输出。注意这里使用的映射模式为MM_LOENGLISH,它的y轴方向是向上递增的。

  PrintTitlePage打印输出文档的封面。它首先定义一种逻辑字体,设置逻辑字体属性,然后由调用CreateFontIndirect由逻辑字体创建字体。SetTextAlign(TA_CENTER)将文本设置为居中输出。然后调用TextOut在打印矩形m_rectDraw上输出封面。PrintTitlePage函数定义见清单8.11。

 

清单8.11 PrintTitlePage成员函数

void CDrawView::PrintTitlePage(CDC* pDC, CPrintInfo* pInfo)

{

// Prepare a font size for displaying the file name

LOGFONT logFont;

memset(&logFont, 0, sizeof(LOGFONT));

logFont.lfHeight = 75; // 3/4th inch high in MM_LOENGLISH

// (1/100th inch)

CFont font;

CFont* pOldFont = NULL;

if (font.CreateFontIndirect(&logFont))

pOldFont = pDC->SelectObject(&font);

// Get the file name, to be displayed on title page

CString strPageTitle = GetDocument()->GetTitle();

// Display the file name 1 inch below top of the page,

// centered horizontally

pDC->SetTextAlign(TA_CENTER);

pDC->TextOut(pInfo->m_rectDraw.right/2, -100, strPageTitle);

if (pOldFont != NULL)

pDC->SelectObject(pOldFont);

}

PrintPageHeader在页眉位置输出文件名,然后从m_rectDraw扣除页眉的大小。

 

void CDrawView::PrintPageHeader(CDC* pDC, CPrintInfo* pInfo,

CString& strHeader)

{

// Print a page header consisting of the name of

// the document and a horizontal line

pDC->SetTextAlign(TA_LEFT);

pDC->TextOut(0,-25, strHeader); // 1/4 inch down

// Draw a line across the page, below the header

TEXTMETRIC textMetric;

pDC->GetTextMetrics(&textMetric);

int y = -35 - textMetric.tmHeight; // line 1/10th inch below text

pDC->MoveTo(0, y); // from left margin

pDC->LineTo(pInfo->m_rectDraw.right, y); // to right margin

// Subtract out from the drawing rectange the space used by the header.

y -= 25; // space 1/4 inch below (top of) line

pInfo->m_rectDraw.top += y;

}

  作为一个练习,读者可以修改OnPrint()并增加一个PrintPageFooter()函数,在每一页的页脚处输出打印的页号。注意调用OnDraw之前,要从m_rectDraw中扣除页脚的高度。

  

8.7 支持多个文档类型的文档视结构程序

  要支持多种文档类型,可以在CWinApp派生类对象中创建和注册附加的CMultiDocTemplate对象。在MFC应用程序中,要增加附加的文档类型,步骤可分为五步。下面我们试着将上一章的文本编辑器加到绘图程序中。这样程序不仅支持绘图,还支持文本编辑功能。

(1)使用ClassWizard创建新的文档类和视图类:

  由于已经有了前面的文本编辑器程序,只需要将其中的文件拷贝过来就可以了,然后用Project->Add To Project->Files命令,将EditorDoc.h、EditorDoc.cpp、EditorView.h、EditorView.cpp加入到工程中。

(2)利用资源编辑器为新的文档类型增加新的字符串。

  先看看绘图程序的文档模板字符串结构。打开字符串编辑器,找到IDR_DRAWTYPE字符串,它是这样定义的:

/nDraw/nDraw/nDraw Files(*.drw)/n.drw/nDraw.Document/nDraw Document

  文档模板字符串包含7个由’/n’结尾并分隔的子串。如果子串不包含则‘/n’作为一个占位字符出现,最后一个字符串后面不需要‘/n’。这些子串描述了文档的类型,它们分别代表:

1.窗口标题:如Microsoft Word,该字符串仅出现在SDI程序中,对于多文档程序为空。因此IDR_DRAWTYPE以/n开头。

2.文档名:在用户从File菜单选取New菜单项时,建立新文档名。新的文档名使用这个文档名字符串作为前缀,后面添加一个数字,用作缺省的新文件名,如“Draw1”、“Draw2”等。如果没有指定,则使用“untitled”作为缺省值。

3.新建文档类型名:当应用程序支持多个文档类型时,该字符串显示在File New对话框中。如果没有指定,则无法用File-New菜单项创建该类型的文档。

4.过滤器名:允许指定与这个文档类型相关的描述。此描述显示在Open对话框中的Type下拉列表中。

5.过滤器后缀:与过滤器名一起使用,指定与文档类型相关的文件的扩展名。对于绘图程序我们在前面已经指定为“.drw”。

6.标注Windows维护的注册数据库中的文档类型Id。应用程序运行时会将该Id加入到注册数据库中。这样File Manager就可以通过Id和下面的注册文档类型名打开相应的应用程序。

7.注册文档类型名:存放在注册数据库中,标识文档类型的名字。

  现在我们要加入文本编辑器的文档模板字符串。在字符串编辑器中增加一个字符串资源,指定ID为IDR_EDITORTYPE,内容为:

/nEditor/nEditor/nEditor Files(*.txt)/n.txt/nEditor.Document/nEditor Document

(3)利用资源编辑器增加附加的图标和菜单资源。注意这些资源的ID必须同第二步中创建文档模板字符串中所用的ID相同。CMultiDocTemplate类利用该ID来识别与附加的文档类型相关的所有资源(包括图标、菜单等)。可以在打开Draw项目工作区文件后,用Project-Insert Project into Workspace将Editor工程文件加入到Draw项目工作区中。然后从Editor中拷贝资源到Draw工程并更名为IDR_EDITORTYPE。

(4)在应用程序类的InitInstance()方法中,创建另一个CMultiDocTemplate对象,并用CWinApp::AddDocTemplate()成员函数注册该模板对象。修改后的代码是这样的:

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_DRAWTYPE,

RUNTIME_CLASS(CDrawDoc),

RUNTIME_CLASS(CChildFrame), // custom MDI child frame

RUNTIME_CLASS(CDrawView));

AddDocTemplate(pDocTemplate);

 

CMultiDocTemplate* pDocTemplate2=new CMultiDocTemplate(IDR_EDITORTYPE,

RUNTIME_CLASS(CEditorDoc),

RUNTIME_CLASS(CMDIChildWnd),RUNTIME_CLASS(CEditorView));

AddDocTemplate(pDocTemplate2);

(5)最后,增加定制的串行化方法和绘图方法到新增的文档和视图类中。

对于CEditorDoc和CEditorView,这一步工作已经在前面做好了。

现在编译并运行程序。

8.8 防止应用程序运行时自动创建空白窗口

  在前面的MDI程序中,当应用程序启动时,都会自动创建一个空白窗口。但有时我们并不希望创建这样的空白窗口。比如,对于一个文件浏览器来说,空白窗口就没有什么意义。

  要防止空白窗口的创建,首先就要明白这个窗口是如何被创建的。在InitInstance()中,有一个命令行的执行过程,当命令行上没有参数时,函数ParseCommandLine(cmdInfo)会将CCommandLineInfo::m_nShellCommand成员置为CCommandLineInfo::FileNew,这将导致ProcessShellCommand调用CWinApp::OnFileNew成员函数。要想防止程序开始时就调用OnFileNew,解决方法之一是去掉与命令行有关的代码,但是这样就没有了命令行处理功能。另一种方法是在ProcessShellCommand调用之前加一句cmdInfo.m_nShellCommand =CCommandLineInfo::FileNothing。具体代码见清单8.12。

清单8.12 不自动创建空白文档窗口的InitInstance成员函数定义

BOOL CDrawApp::InitInstance()

{

// Enable DDE Execute open

EnableShellOpen();

RegisterShellFileTypes(TRUE);

// Parse command line for standard shell commands, DDE, file open

CCommandLineInfo cmdInfo;

// Alter behaviour to not open window immediately

 

cmdInfo.m_nShellCommand = CCommandLineInfo::FileNothing;

ParseCommandLine(cmdInfo);

// Dispatch commands specified on the command line

if (!ProcessShellCommand(cmdInfo))

return FALSE;

//......

}

 

你可能感兴趣的:(技术文章)