Visual C++ MFC图形编程基础


第一章MFC MFC MFC MFC 图形编程基础 1.11.11.11.1 MFC MFC MFC MFC 概述 MFC(Microsoft Foundation Classes)库是Microsoft 为利用VC++开发 Windows 应用程序而提供的应用程序框架。在这个框架的支持下,对于不同的 应用程序 ,编程的主要任务是填写各自特殊部分的代码 。MFC 类库由 130 多个 类组成,封装了两千多个 API 函数。 使用 MFC 编程具有很多优点 :用类编程 ,将代码和数据封装在类中 ,大大 减少了编程的复杂性 ;通过继承实现了基本的代码重用 ,在开发应用程序的过程 中,我们可以通过继承来使用 MFC 中已经定义了的大量类 ,在保有它们原有的 特性的同时 ,可以根据我们的需要来修改它们 ,使其具有新的特性 ;在使用 MFC 进行编程的过程中,可以利用其提供的各种工具,提高编程的效率;借助 MFC 类库中设计良好的类资源可以减少代码规模 。使用 MFC 类库 ,编程者只需要注 意应用程序与通用的Windows 程序所不同的特性,而几乎所有的应用程序与 Windows 之间的接口都是由应用程序框架来实现的 ,这大大降低了编程的难度 。 Windows 区别于DOS 的一个重要的特征就是它的图形界面,所以,在 Windows 的程序中绘图就成为了 Windows 编程的一个非常重要的内容 。为了满 足 编程 者在 程序 中绘 图的 需要 , MFC 提 供了 许多 与绘 图相 关的 类和 函数 ,使 用 它们用户可以绘制出想要的各种图形。 1.21.21.21.2 创建 MFC MFC MFC MFC 项目 使用 MFC 框架编程 ,首先需要创建一个 MFC 项目 ,MFC 的应用程序开发 都是在一个 MFC 项目中完成的 。在本教程中 ,我们使用的开发平台是 Microsoft Visual Studio 6.0 中的 Visual C++ 6.0。 1.2.11.2.11.2.11.2.1 使用 MFCMFCMFCMFC AppWizard AppWizard AppWizard AppWizard 创建 MFC MFC MFC MFC 项目 在Microsoft Visual Studio 6.0 中,我们可以利用 MFC AppWizard(MFC 应用程序向导)来建立 MFC 项目,它提供了一个应用程序所必需的框架文件 , 如源文件 、头文件 、资源文件 、项目文件等等 。创建 MFC 项目由以下几个步骤 完成: (1)启动 Visual C++ 6.0,如图 1.1 所示。 (2)从File 菜单中选择 New 选项,出现 New 对话框(新建对话框 ),如 图1.2 所示。该对话框包括Files、Projects、Workspace 和OtherDocuments 四个分页。选中Projects 分页,在Projects 面板中左侧的项目类型列表框中单 击选中要创建的项目类型。这里我们选择MFC AppWizard[exe]项,表示要创 建 一个 MFC 应 用程 序。 在 Project Name 文 本输 入框 中输 入要 创建 的项 目的 名 称。我们在这里输入项目名称为 “DrawTest”,本章的演示程序将在该项目中来 完成。在Location 文本输入框中输入用户希望存放项目的目录名,用户也可以 点击输入框右侧的按钮,调用“Choose Directory”对话框来选择目录。系 统会在用户选择的目录下创建一个与项目名称相同的目录来存放项目文件 。系统 的默认目录是 Microsoft Visual Studio 6.0 安装目录下的 MyProjects 目录 。下 面的一组单选按钮: Create new workspace,代表创建一个新的项目; Add to current workspace,代表要加入到当前的项目中。这里我们选择创建一个新的 项目。因为 Visual C++ 6.0 在32位的操作系统平台上运行,所以在 Platform 编辑框中必须选中 Win32复选框。 (3)单击 OK按钮。出现 MFC AppWizard – Step 1对话框,如图 1.3 所示 。在这一步中 ,用户要选择应用程序的类型和资源文件所使用的语言 。用户 可以创建基于Single document(单文档),Multiple document(多文档)和 Dialog based(对话框 )的应用程序 。这里我们选择创建基于单文档的应用程序 。 在“What type of application would you like to create?”下拉框中,选择 “中 文[中国 ]”作为资源文本语言。选中 “Document/View architecture support” 复选框 ,表示应用程序将支持文档 /视图体系 。关于文档 /视图体系 ,将在后面进 行介绍。 (4)单击Next 按钮,出现MFC AppWizard – Step 2 of 6 对话框, 如图1.4 所 示。 在 “What database support would you like to include?”中, 选择 None 单选钮,表示不需要数据库支持。 (5)单击Next 按钮,出现MFC AppWizard – Step 3 of 6 对话框, 如图1.5 所示。在“What compound document support would you like to include?”中,选择 None 单选钮 ,表示不需要复合文档的支持 。同时在 “What other support would you like to include?”中 ,选 择 ActiveX Controls 复 选框 , 表示包括 ActiveX。 (6)单击Next 按钮,出现MFC AppWizard – Step 4 of 6 对话框, 如图1.6 所示。在这一步,用户将对应用程序的界面特征进行设定。在“What features would you like to include?”中,选中 3D controls(三维图形控件 ) 选项 ,使控件具有立体感 。如果用户选择了 Docking Tools 选项 ,那么 “How do you want your toolbars to look?”被激活,这里有两个单选按钮:Normal 表 示生成传统意义上横跨在窗口顶端的工具条; Internet Explorer Rebars 表示生 成IE4风格的附加工具条 。选中 Initial status bar 复选框 ,表示窗口具有状态条 。 选中Printing and print preview 复选框,表示应用程序提供打印和打印预览。 在“How many files would you like on your recent filelist?”中 使用 默认 值 4, 表示生成的应用程序的 File 菜单中显示 4个最近打开过的文件。 (7)单击Next 按钮,出现MFC AppWizard – Step 5 of 6 对话框, 如图 1.7 所示。在 “What style of project would you like?”有两个单选按钮 : MFC Standard 表示提供一个标准的MFC 应用程序架构;Windows Explorer 表示建立一个像 Windows Explorer 那样使用分割窗口的应用程序,左边的成列 窗口是 CTreeView 类,右边的成列窗口是 CListView 类。在“Would you like to generate source file comments?”中,选择 Yes, Please,表示在源代码中加 上注释 。在“How would you like to use the MFC library?”中,选择 As a shared DLL,表示使用动态链接库,当应用程序仅使用 MFC 类库时,动态链接有利于 减少占用的磁盘空间。 (8)单击Next 按钮,出现MFC AppWizard – Step 6 of 6 对话框, 如图 1.8 所示 。在这里可以修改所创建的类的类名和文件名 ,修改应用程序视类 的基类。在列表框中单击选择CDrawTestView 类,可以单击Base class 下拉 框 的箭 头, 从中 选择 要使 用的 基类 。我 们这 里使 用默 认的 CView 作 为基 类。 除 了修改应用程序视类的基类外,我们不建议修改这里各个类的类名和文件名。 (9)单击 Finish 按钮,出现 New Project Information 对话框,如图 1.9 所示 。这里将显示用户前面几步所做的所有设置 。如果检查设置无误 ,可以单 击 OK按钮,完成MFC 项目的创建,系统在指定的目录下生成了应用程序的框架 文件。 至此 ,MFC 项目创建完毕 。实际上我们在第 (3)步之后各步的设置中采用 的都是系统的默认选项 ,所以可以在第 (3)步选择完单文档应用程序后 ,直接 单击Finish 按钮,这时将直接出现第(9)步的New Project Information 对话 框 ,这 里表 示第 ( 3) 步之 后的 各项 设置 都采 用系 统的 默认 设置 。在 第( 3)步 之后的各步中随时可以选择 Finish 按钮来直接完成设置。 1.2.21.2.21.2.21.2.2 MFC MFC MFC MFC 项目工作区窗口 MFC 创 建完 成之 后, 用户 可以 通过 项目 工作 区窗 口进 行文 件组 织, 项目 管 理和项目设置更改。如图 1.10 所示。 在项目工作区窗口中一共有三个分页,分别是ClassView 面板、 ResourceView 面板和 FileView 面板 。在每个面板中都有一个树型结构 ,用户可 以用鼠标单击树型节点左侧的 “+”字展开节点或者单击 “-”字层叠节点。 (1)ClassView 面板(类面板) 类面板显示了当前项目中所包含的类和类成员的树型结构,如图 1.11 所示 。 展开 “DrawTest classes”节点 ,该节点的下层节点就是当前项目中所包含 的所有类 ;展开每个类节点 ,所显示的就是该类中的所有类成员 ,包括成员函数 和成员变量 。在类的每个成员节点的左边都有一个小图标 ,该图标给出了成员函 数或成员变量的类型以及存取类别: 钥匙图标,表示该类成员为保护成员; 红色方块,表示该类成员为成员函数; 蓝色方块,表示该类成员为成员变量; 锁,表示该类成员为私有成员变量; “Globals”节点下是项目中的全局成员。 通过使用类面板,用户可以很方便的查看和编辑源代码文件。双击类节点 , 就会打开对应该类的头文件 ;双击类成员节点 ,就会打开相应的源码文件 ,并定 位到相应的位置 :双击成员函数 ,打开类文件 (.cpp 文件 ),并定位到函数方法 名最左端 ;双击成员变量 ,打开头文件 (.h文件 ),并定位到变量声明的最左端 。 如图 1.12,就是双击了 CDrawTestView 类的 OnDraw(CDC*pDC)成员函数节 点。开发平台打开 DrawTestView.cpp 类文件 ,并将光标定位在 OnDraw 函数名 的最左端。用户可以在源代码编辑窗口中进行代码编写。 在类节点或类成员节点上用鼠标右键单击,会弹出快捷菜单,如图1.13 所 示。利用快捷菜单 ,用户可以方便的创建新类 ;给已有类增减成员函数和成员变 量;直接跳转到类 、成员函数 、成员变量的定义和声明处等等 。具体的使用方法 将在后面进行介绍。 (2)ResourceView 面板(资源面板) 资源面板用于管理项目资源,应用程序中所使用的对话框、图标、菜单 、工 具条等都属于项目资源。 如图1.14 所 示。 根节 点 “DrawTest resources”的 下层 节点 是各 种项 目资 源文件夹 。展开项目资源文件夹 ,就可以看到项目中包含的资源 。双击资源节点 , 就可以对该资源进行查看和编辑。例如用户想要查看当前应用程序的工具条 ,就 可以先展开Toolbar 项目文件夹节点,然后双击IDR_MAINFRAME(我们所创 建的应用程序的工具条 ),就可以对该工具条进行查看和编辑。 (3)FileView 面板(文件面板) 文件面板显示了项目所包含的各种文件及其之间的关系,如图 1.15 所示。 用鼠标双击文件面板中的文件名 ,源代码编辑窗口会自动以合适的编辑器打 开相应的文件 。在文件面板中我们看到在 “Source Files”中有一个 DrawTest.rc 文件。 .rc 文件就是项目的资源文件,双击该文件,将自动打开资源面板。 虽然我们在文件面板中也可以打开类的源代码文件,对类及其成员进行编 辑。但是这里还是强烈建议用户使用类面板对类及其成员进行操作 。在面向对象 程序设计中 ,程序员操作的对象是类 ,代码的设计和编写始终都应该是基于类对 象的 。通过类面板 ,用户可以时刻了解当前应用程序的类组成 ,而且每个类的成 员构成一目了然 。尽管 Visual C++ 6.0 中每个类仍然是由两个源代码文件组成 , 但是我们在编程过程中应该将这两个文件当成一个整体来看 。利用类面板及其提 供的快捷菜单,也可以加快开发,减少错误。 1.2.31.2.31.2.31.2.3 文档 ////视图体系 前面在创建MFC 项目的时候,我们曾经设置过要应用程序支持文档/视图 体系。这里我们说明一下什么是文档 /视图体系。 在说明之前 ,先看看我们创建的这个单文档应用程序都包含了哪些类 。通过 类面板,我们可以看到在当前项目中包含了五个类:CAboutDlg 类, CDrawTestApp 类,CDrawTestDoc 类,CDrawTestView 类和CMainFrame 类。 其中CAboutDlg 类是一个对话框类,其基类是CDialog。该类是应用程序中的 “关于 ”对话框。运行应用程序,我们可以看到该对话框,这里不详细说明了 。 CDrawTestApp 类是应用程序对象类 ,其基类是 CWinApp。CWinApp 类是每 个 Windows 应用程序对象的基类,它完成应用程序的初始化、运行和终止功能。 所以 CDrawTestApp 类在整个应用程序中完成程序初始化 、运行及终止的工作 。 CMainFrame 类是主框架窗口类 ,其基类是 CFrameWnd。CFrameWnd 类是单 文档应用程序主框架窗口的基类 。在应用程序中 ,工具条和状态条都属于主框架 窗口 ,所以它们都在该类中进行加载 。还有两个类 ,就是我们要介绍的文档和视 图。CDrawTestDoc 类是文档类,其基类是文档基类CDocument 。 CDrawTestView 类是视图类,在本项目中,选择的基类是视图基类 CView。 MFC 应用程序通过建立文档 /视图体系 ,将数据 、数据的显示和用户对数据 的操作区分开来 ,使多视图 、多文档类型 、拆分窗口等有用的功能特性更加容易 实现 。在文档 /视图体系中 ,数据被移植到文档 (Document)数据对象中 ,输出 功能由视图承担,而框架窗口仅仅是一个容器,负责包含和管理视图。 文档( Document)就是数据对象的集合。通过它,用户可以定义、存储和 管理应用程序的数据,并可以将数据写入或读出永久存储介质(如硬盘等 )。 视图(View)就是窗口对象,负责向输出设备(如显示屏幕、打印机等) 输出应用程序中的数据(在本书中主要是向显示屏幕输出图形数据 ),提供文档 和用户的交互界面 。通过它 ,可以观察 、选择和编辑文档中的数据 ,所有用户和 文 档的 交互 都必 须通 过与 文档 相对 应的 视图 进行 。 MFC 中 的绘 图基 本就 是在 视 图中进行绘图。 文档和视图是一个不可分割的统一体!文档基类CDocument 和视图基类 CView 是文档/视图体系中最重要的两个类。所有的文档类都是由CDocument 派生而来的。 CDocument 包容了用户应用程序中的数据,定义了框架窗口如何 对文档起作用。CView 类及其派生视图类是用户的数据窗口,它控制着应用程 序 文档 内容 的显 示以 及用 户和 应用 程序 文档 的交 互( MFC 的 绘图 及相 关的 用户 交互都是在视图中完成的)。不同的视图派生类分别支持不同的功能。如 CSrollView 类是具有滚动功能的视图类 ,从它派生的类可以自动实现滚动功能 ; CFormView 类 是对 话框 资源 视图 类, 从它 派生 的类 可快 速实 现基 于对 话框 资源 的用户界面;CEditView 类为文档提供一个基于文本编辑的用户界面; CDaoRecordView 类提供直接链接到 DAO 记录集的表单视图; CRichEditView 类比CEditView 类封装了更多的文本编辑功能,如支持字体、颜色、段落格式 化等等 ;CListView 类是列表控件视图类 ,用于显示图标和字符串 ;CTreeView 类是树形图视图类,等等。 1.31.31.31.3 设备环境 DC DC DC DC 和OnDraw OnDraw OnDraw OnDraw 函数 通过前面的介绍,我们已经创建了一个MFC 项目DrawTest,并且了解了 MFC 的文档 /视图体系,知道 MFC 绘图绝大多数都是在视图中绘图。而在视图 中绘图必须使用设备环境 DC。现在我们先完成一个简单的在视图中的绘图。 通过类面板 ,选择 CDrawTestView 类的 OnDraw 方法 。在OnDraw 方法中 的“//TODO: add draw code for native data here”注释下输入代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->LineTo(200,200);//用户编写的代码 } 然 后编 译运 行应 用程 序。 这里 简单 介绍 一下 Visual C++ 6.0 的Build 工具 条。如图 1.16 所示。 工具条中的各按钮功能如下表: 图标命令 功能 Compile 编译文件 Build 建立项目 Stop Build 停止项目的建立 Execute Program 执行文件 Go 启动或继续执行程序 Insert/Remove Breakpoint 插入 /删除断点 我们可以先用Compile 或者Build 来编译链接应用程序,然后用Execute Program 来运行应用程序 。因为 MFC 项目涉及的类较多 ,有时候因为编译顺序 问题,在使用Compile 命令时会出错,实际此时程序不一定有错误,所以建议 使用 Build 命令来进行编译和链接 。我们也可以直接运行程序 ,如果此时程序创 建或更改后没有进行编译,系统会先询问是否需要编译。如果选择 “是”,则系 统会重新编译链接应用程序并运行;选择 “否”,如果程序以前编译过,则运行 以前编译过的程序 ;也可以选择 “取消 ”来撤销这次程序运行 。在程序中设置了 断点,需要进行跟踪时使用 Go命令,用来继续执行程序。 运 行我 们编 写完 OnDraw 方 法的 应用 程序 ,得 到结 果如 图 1.17 所 示。 我们 看到在应用程序窗口的视图区部分 (应用程序窗口中间的白色部分 )绘制了一条 直线。我们所编写的OnDraw 函数是在视图内绘图最常用的函数。该函数是 CView 类中的一个虚函数,每次当视图需要重新绘制时,应用程序框架都会自 动调用该函数 (关于视图重画将在后面详细介绍 ),例如用户改变了窗口的大小 , 或者窗口恢复先前被覆盖的部分时。在系统自动生成的OnDraw 函数中已有两 行代码: CDrawTestDoc* pDoc = GetDocument();//获得与视图对应的文档类的 指针 ASSERT_VALID(pDoc);//检查该指针是否为空 OnDraw 函数传入的参数是CDC 类对象指针,这里的CDC 类就是设备环 境类的基类。我们编写的代码: pDC->LineTo(200,200); 就是调用了 CDC 的成员函数 LineTo 来绘制了一条直线 。函数 LineTo 可以 称为绘图函数。 设备环境 DC(Device Context,又称设备上下文或设备描述表)是一个关 于如何绘制图形的方法的集合 。它可以绘制各种图形 ,也可以确定在应用窗口中 绘制图形的方式 ,即确定绘图模式和映射模式 。在MFC 中包含了一些设备环境 类,CDC 是设备环境类基类 ,还有从其派生的一些设备环境类 。基类 CDC 包含 了绘图所需要的所有成员函数,而且除了 CMetaFileDC 派生类之外,其他所有 的派生类都只有构造函数和析构函数的定义有所不同 。这些设备环境派生类的目 的是为了在不同的显示设备上进行显示。CDC 类的派生类有CClientDC 类, CPaintDC 类,CMetaFileDC 类和CWindowDC 类。用显示屏幕进行显示常用 的派生类是 CClientDC 和CWindowDC。下表列出 CDC 类派生类的功能简介 : 派生类名称 说明 CClientDC 提供对窗口视图区域的图形绘制 ,在窗口中绘图时可以 使用此类 DC,但对 WM_PAINT Windows 消息除外 CPaintDC 响应WM_PAINT Windows 消息的设备环境类,可以 使用此DC 更新Windows 显示,通常在MFC 应用程 序中的 OnPaint()函数中使用 CWindowDC 提供在整个窗口 (包括视图区和非视图区 )中进行绘图 的DC CMetaFileDC 代表 Windows 元文件 。想要创建独立于设备的文件时 可以使用此类 DC,用户可以回放这种文件来创建图像 CWindowDC 类和CPaintDC 及CClientDC 类的区别一方面是 用CPaintDC 类和CClientDC 类的对象绘制图形时,绘制区只能是视图区,而不能是非视图 区 ,而 CWindowDC 可 以在 非视 图区 进行 绘图 。如 图 1.17 所 示的 应用 程序 窗口 中,中间的白色部分都属于视图区(直线就绘制在视图区中 ),而窗口标题、菜 单栏,工具条和状态条都属于非视图区。CWindowDC 一般在框架窗口类 (CMainFrame)中引用 。在视图窗口中引用 CWindowDC 类时 ,因为视图类只 能管理视图区 ,所以并不能在非视图区进行绘图 。它们之间的另一个区别是 :在 CWindowDC 设备环境类下 ,坐标系是建立在整个屏幕上的 ,在像素坐标方式下 , 坐标原点在屏幕的左上角;而在CPaintDC 和CClientDC 下,坐标系是建立在 视图区的,在像素坐标方式下,坐标原点在视图区的左上角。 CPaintDC 和CClientDC 的区别在于它们的绘制机制。CPaintDC 应用在 OnPaint 函数中,响应Windows 的WM_PAINT 消息,而CClientDC 应用在非 响应 WM_PAINT 消息的情况下 。所以我们在视图中进行绘图的时候一般都采 用 CClientDC,而 CPaintDC 一般只在 OnPaint 函数中使用。 用户在绘图之前 ,必须获取绘图窗口区域的一个设备环境 DC,接着才能够 调用其绘图函数进行绘图。因为在OnDraw 函数中,系统已经自动传入了一个 设备环境类指针 ,用户可以直接通过该指针调用绘图函数进行绘图 。如果要在视 图类的其它函数中进行绘图就必须首先获取设备环境 DC,常用的获取方法有以 下几种: (1)如果要绘制图形的函数由 OnDraw 函数调用,则可以将 OnDraw 函 数中的 CDC 对象指针作为该函数的一个参数传入; (2)可以构造 CClientDC 对象,使用该对象进行绘图,其构造函数为 CClientDC(CWndCWndCWndCWnd**** pWnd) 因为 CView 类由 CWnd 类(所有窗口的基类 )派生而来 ,所以构造的时候 传入当前视图类的指针即可。 (3)可以通 过GetDC()函数来获得设备环境对象指针 ,其原型声明如下 : CDC* CWnd::GetDC() 该函数没有任何参数 ,用于获取一个窗口视图区指针 。调用的时候通过当前 视图类指针进行调用。 下面举例来说明如何通过上面三种方法来获得设备环境对象。 首先在CDrawTestView 类中添加一个函数,用鼠标右键单击类面板中的 CDrawTestView 类节点 ,在弹出的快捷菜单中选择 “Add Member Function…”, 出现增加成员函数对话框,如图 1.18 所示。 在Functio Type(函数返回类型)文本输入框中输入void,表示函数没有 返回值。在Function Declaration(函数声明)文本输入框中输入Draw(CDC *pDC),表示创建一个名称为Draw 的函数,参数为CDC 类指针。在Access 中可以选择 Public(公有 ),Protected(保护)或者 Private(私有 ),我们选 择 Public,表示Draw 函数是一个公有函数。下面的复选框为Static(静态函数) 和Virtual(虚函数)。然后点击OK按钮,系统自动在CDrawTestView 类中添 加Draw 函数,并在源代码编辑窗口中定位到该函数处,同时在头文件中添加 Draw 函数的声明。建议用户使用这种方法来增加类的成员函数,在减少工作量 的同时可以避免写了函数体之后忘记在头文件中进行声明的问题。同时,尽管 Visual C++ 6.0 允许写不属于任何类的函数,在调用上也没有问题,但是我们 仍然建议不要写这种函数 ,因为它与面向对象程序设计的思想不符 。另外 ,因为 不属于类的函数无法通过类面板看到 ,如果这样的函数多的话 ,会让程序员不清 楚自己的应用程序项目中都有哪些函数,更容易出错。 在我们新增的 Draw 函数中输入如下代码 。为了举例 ,该函数中三种获得设 备环境对象的方法都使用了,在实际应用中,使用其中一种即可。 void CDrawTestView::Draw(CDC*pDC) { pDC->LineTo(200,100);//使用传入的 CDC 对象指针绘图 CClientDC dc(this);//构造 CClientDC 设备环境对象 dc.LineTo(200,200);//利用构造的 CClientDC 设备环境对象绘图 CDC* p=this->GetDC();//通过 GetDC 函数获得设备环境对象指针 p->LineTo(100,200);//利用 GetDC 函数获得设备环境对象指针绘图 this->ReleaseDC(p);//由GetDC 函数获得的设备环境对象必须用 ReleaseDC 函数释放 } 修改 OnDraw 函数,在 OnDraw 函数中调用 Draw 函数。 void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here Draw(pDC);//调用 Draw 函数 } 运行应用程序,结果如图1.19 所示。从运行结果可以看出,三种方法获得 的设备环境对象都可以进行绘图 。在实际应用中可以根据需要进行选择 。如果要 进行绘图的函数是通过OnDraw 函数进行调用的,建议采用第一种方法;而在 该函数不通过 OnDraw 函数调用的时候使用另外两种方法。 1.41.41.41.4 CDC CDC CDC CDC 类常用绘图函数 在获得设备环境对象后 ,具体的绘图工作是由绘图函数完成的 ,CDC 类提 供了一些基本的绘图函数供用户使用 ,如果进行复杂绘图的话 ,就需要通过算法 来组织这些基本绘图函数来完成绘图。本节介绍一下这些基本绘图函数。 1.4.11.4.11.4.11.4.1 SetPixel SetPixel SetPixel SetPixel 函数和 GetPixel GetPixel GetPixel GetPixel 函数 � SetPixel SetPixel SetPixel SetPixel 函数用于绘制一个点,其函数声明如下: COLORREF SetPixel(int x, int y,COLORREF crColor); COLORREF SetPixel(POINT point,COLORREF crColor); 参数 x和y是逻辑坐标系下要绘制的点的坐标 ,参数 point 也是用于指定要 绘制的点的坐标。结构体 POINT 是用于定义逻辑坐标下一个点的坐标位置的, 其结构定义如下: typedef struct tagPOINT { LONG x; LONG y; }POINT; 参数crColor 指定了要绘制的点的颜色,其类型是COLORREF,即32位 的颜色单位,定义这个颜色最简单的方法是使用 RGB 函数: COLORREFRGB(BYTE bRed,BYTE bGreen,BYTE bBlue); 其中的 bRed,bGreen 和bBlue 参数分别代表红 、绿、蓝三色的相对密度 , 每个参数的取值范围是 0-255,0代表没有该颜色分量 ,255 代表最大 。函数 返回实际绘制点的颜色值。 � GetPixel GetPixel GetPixel GetPixel 函数用于获得指定点的颜色,其函数声明如下: COLORREF GetPixel(int x, int y); COLORREF GetPixel(POINT point); 参数意义与 SetPixel 相同。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here for (int i=0;i<255;i=i+5) { //绘制一系列红颜色程度不同的点 pDC->SetPixel(i+10,100,RGB(i,0,0)); } for (int j=0;j<255;j=j+5) { //用与要绘制的点对应的点的颜色来绘制 pDC->SetPixel(j+10,200,pDC->GetPixel(j+10,100)); } } 运行程序,结果如图 1.20 所示。 程序先绘制一系列红颜色分量从0开始变化的点,然后再分别获得这些点 的颜色值,绘制了一系列新的点。因为SetPixel 函数只能绘制大小为一个像素 的点,所以效果看起来不是非常明显。 1.4.21.4.21.4.21.4.2 LineTo LineTo LineTo LineTo 函数和 MoveTo MoveTo MoveTo MoveTo 函数 � LineTo LineTo LineTo LineTo 函数用于绘制一条从当前绘图位置到指定坐标点的直线段 ,其函 数声明如下: BOOL LineTo(int x, int y); BOOL LineTo(POINT point); 参数x和y, 及参 数 point 指 定了 坐标 点。 这里 我们 注意 到, 一条 直线 段应 该有两个端点 ,可是这里只指定了一个点 。这是因为在使用设备环境对象进行绘 图的时候有一个当前绘图位置的概念,该位置是逻辑坐标系下的一个坐标点。 LineTo 函数是在当前绘图位置和指定的点之间绘制一条直线段。初始的当前绘 图位置是视图区逻辑坐标系的原点 ,该原点在默认的映射模式下是在视图区的左 上角 。所以在我们的第一个例子中 ,绘制了一条从视图区左上角到 (200, 200) 点的直线段 (如图 1.17 所示 )。LineTo 函数在绘制完直线段后 ,将当前绘图位置 移到参数所指定的新的坐标点处 。如果画直线段成功 ,函数返回 TRUE,否则返 回FALSE。 � MoveTo MoveTo MoveTo MoveTo 函数用于将当前绘图位置移到指定的坐标点处 ,函数声明如下 : CPoint MoveTo(int x, int y); CPoint MoveTo(POINT point); 参数 x和y,及参数 point 指定了新的当前绘图位置坐标 。返回值是 CPoint 对象实例 ,它包含了新的当前绘图位置坐标 。CPoint 类是 MFC 中定义的一个点 对象的类 ,它有两个成员变量 x和y,用于存放点的坐标位置 ,类型为 int。在绘 图函数参数中所有使用 POINT 结构的地方都可以使用 CPoint 类。MoveTo 函数 通常与 LineTo 函数同时使用,来绘制一条指定了端点的直线段。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->MoveTo(10,10); POINT p1;p1.x = 100;p1.y = 100;//使用 POINT 结构 pDC->LineTo(p1); CPoint p2;p2.x = 200;p2.y = 100;//使用 CPoint 类 pDC->LineTo(p2); } 运行程序,结果如图 1.21 所示。 程序先把当前绘图位置移到 (10, 10)点,然后绘制了从 (10, 10)点到 (100, 100)点的直线段 。此时当前绘图位置移到了 (100, 100)点,所以下一个 LineTo 函数绘制了(100, 100)点到(200, 100)点的直线段。程序中两个画线函数分 别使用了POINT 结构和CPoint 类作为函数的参数。其中,CPoint 类有构造函 数CPoint(int x, int y),我们可以使用该构造函数直接构造 CPoint 对象作为绘 图函数的参数。OnDraw 函数中最后两行代码可以用此代码代替: pDC->LineTo(CPoint(200, 100));。 需要注意的是 :当前绘图位置是针对设备环境对象的 ,不同的设备环境对象 的当前绘图位置并不互相影响。修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here CDC* pDC2=this->GetDC();//获得另一个设备环境对象 pDC->LineTo(100,100); pDC2->LineTo(200,100); pDC->LineTo(300,200); pDC2->LineTo(300,200); } 运行程序,结果如图 1.22 所示。 程序中获得了另一个设备环境对象pDC2。用pDC 绘制了从原点到(100, 100)点的直线段后 ,pDC 的当前绘图位置移到了 (100, 100)点。而对于 pDC2 来说,当前绘图位置并没有变化,仍然是默认的原点,所以pDC2绘制了从原 点到 (200, 100)点的直线段,此时 pDC2的当前绘图位置移到了 (200, 100) 点。所以后面 pDC 绘制了从 (100, 100)点到 (300, 200)点的直线段 ,而pDC2 绘制了从 (200, 100)点到 (300, 200)点的直线段。 1.4.31.4.31.4.31.4.3 Polyline Polyline Polyline Polyline 函数和 PolylineTo PolylineTo PolylineTo PolylineTo 函数 � Polyline Polyline Polyline Polyline 函数用于绘制一条由一系列指定的点连接而成的折线,其函数 声明如下: BOOL Polyline(LPPOINT lpPoints, int nCount); 参数lpPoints 指向POINT 结构或者CPoint 对象数组,该数组按顺序存放 折 线连 接点 的坐 标, 而 nCount 是 连接 点的 个数 ,该 参数 必须 要大 于 1。 如果 画 线成功,函数返回 TRUE;否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here POINT p[5]; p[0].x = 10;p[0].y = 10; p[1].x = 100;p[1].y = 100; p[2].x = 200;p[2].y = 100; p[3].x = 200;p[3].y = 200; p[4].x = 100;p[4].y = 50; pDC->Polyline(p,5); } 运行程序,结果如图 1.23 所示。 � PolylineTo PolylineTo PolylineTo PolylineTo 函数也用于绘制折线,其函数声明如下: BOOL PolylineTo(const POINT* lpPoints, int nCount); 该函数参数意义与 Polyline 函数相同。 PolylineTo 与Polyline 有两点区别 : 一是PolylineTo 函数绘制的折线的起始点并不是输入的点数组中的第一个点, 而是当前绘图位置 ;二是 Polyline 函数执行完毕后 ,将当前绘图位置移动到所绘 制折线的最后一点处。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here CPoint p[5]; p[0].x = 10;p[0].y = 10; p[1].x = 100;p[1].y = 100; p[2].x = 200;p[2].y = 100; p[3].x = 200;p[3].y = 200; p[4].x = 100;p[4].y = 50; pDC->PolylineTo(p,5); pDC->LineTo(300,100); } 运行程序 ,结果如图 1.24(1)所示 。然后将 PolylineTo 函数改成 Polyline 函数,再次运行程序,结果如图 1.24(2)所示。 从运行结果可以看出 ,PolylineTo 函数绘制的折线起始点在原点 (左上角 ), 因为在默认情况下,设备环境的当前绘图位置是原点。如果想用PolylineTo 函 数绘制出与Polyline 函数所绘制的折线相同的折线,就需要在执行PolylineTo 之前把当前绘图位置移动到点数组的第一个点。在本程序中就需要在 pDC->PolylineTO(p, 5)代 码之 前加 上 pDC->MoveTo(10, 10)。PolylineTo 函 数执行完后 ,将当前绘图位置移动到了折线的最后一点 ,所以后面的画线函数绘 制的是 (100, 50)到(300, 100)的直线段。而 Polyline 函数并不改变当前绘图 位置,所以绘制的是从默认当前绘图位置到 (300, 100)的直线段。 1.4.41.4.41.4.41.4.4 Arc Arc Arc Arc 函数和 ArcTo ArcTo ArcTo ArcTo 函数 � Arc Arc Arc Arc 函 数用 于绘 制一 个椭 圆形 的弧 线, 它是 一个 指定 的矩 形的 内切 椭圆 的一段 。所绘制弧线的实际起点是该内切椭圆与一条直线的交点 ,而该直线是从 椭圆中心到指定点的连线。弧线的实际终点也是如此。如图 1.25 所示: 函数声明如下: BOOL Arc(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL Arc(LPCRECT lpRect,POINT ptStart,POINT ptEnd); 第一个函数参数给出了四对坐标 :第一对坐标是要绘制的圆弧所在椭圆的外 接矩形的左上角坐标 ;第二对坐标是要绘制的圆弧所在椭圆的外接矩形的右下角 坐标 ;第三对坐标指定了圆弧的起点 ;第四对坐标指定了圆弧的终点 。在画圆弧 时,默认的圆弧方向是逆时针 ,即圆弧是按逆时针方向从起点到终点的 。第二个 函数给出的参数中,后两个使用 POINT 结构来存放起点和终点坐标。而第一个 参数 LPCRECT 是指向 MFC 中定义矩形区域的数据结构的指针 ,该矩形区域结 构定义如下: typedef struct tagRECT { LONG left; LONG top; LONG right; LONG bottom; }RECT; 其中left,top,right,bottom 分别代表矩形区域的左上角的x坐标,左上 角的y坐标,右下角的x坐标,右下角的y坐标。我们可以使用RECT 来定一 个结构体做为函数的参数 ,也可以使用 MFC 中定义的用于存放矩形区域坐标的 类CRect, 该类 也具 有 RECT 结 构的 四个 参数 。在 使用 RECT 结 构的 地方 也可 以使用CRect 类,其关系与POINT 结构和CPoint 类的关系相同。当函数绘图 成功,返回 TRUE,否则返回 FALSE。 � ArcTo ArcTo ArcTo ArcTo 函数也用于绘制一个椭圆形的弧线 。它与 Arc 函数的区别类似 于 Polyline 函数与 PolylineTo 函数的区别 。ArcTo 函数绘制弧线后将当前绘图位置 移动到终止点 ,并且该函数起始绘图位置并不是设定的起始点 ,而是当前绘图位 置。其函数声明如下: BOOL ArcTo(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL ArcTo(LPCRECT plRect,POINT ptStart,POINT ptEnd); 函数参数的含义与 Arc 函数相同。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->Arc(100,100,300,300,100,150,250,150); CRect r; r.left = 350;r.top = 100; r.right = 650;r.bottom = 300; POINT p1,p2; p1.x = 500;p1.y = 100; p2.x = 650;p2.y = 150; pDC->MoveTo(p1); pDC->ArcTo(r,p1,p2); } 运行程序,结果如图 1.26 所示: 该段程序分别用 Arc 函数和 ArcTo 函数绘制了两个椭圆形弧线 。在用 ArcTo 绘制弧线之前,先将当前绘图位置移动到了 p1点处。如果将此句代码去掉,程 序将绘制一条从默认当前绘图位置原点到 p1点的直线段,读者可以自行试验一 下。 1.4.51.4.51.4.51.4.5 AngleArc AngleArc AngleArc AngleArc 函数 � AngleArc AngleArc AngleArc AngleArc 函数用于绘制一条直线段和一段圆弧线。它在当前绘图位置 和圆弧的起点之间绘制一条直线段 ,并根据给定的圆的圆心坐标 ,半径 ,和起始 和终止角度绘制一个圆弧。其函数声明如下: BOOL AngleArc(int x, int y, int nRadius, float fStartAngle, float fSweepAngle); 该函数的前两个参数定义了圆心的位置 ;第三个参数是圆的半径 ;第四和第 五个参数分别定义了要绘制的圆弧相对于 x轴的起始角和终止角 ,该参数值为角 度值。如果绘图成功函数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->MoveTo(100,100); pDC->AngleArc(200,200,50,90,270); } 运行程序,结果如图 1.27 所示: 该段程序先将当前绘图位置移动到 (100, 100)点。所绘制圆弧所在圆的圆 心为 (200, 200),半径为 50,起始角为 90度,终止角为 270 度。在当前绘 图位置与由起始角所确定的圆弧的起始点之间绘制了一条直线段 。这里 x轴正向 为0度,绘制圆弧方向为逆时针方向。 1.4.61.4.61.4.61.4.6 PolyBezier PolyBezier PolyBezier PolyBezier 函数和 PolyBezierTo PolyBezierTo PolyBezierTo PolyBezierTo 函数 � PolyBezier PolyBezier PolyBezier PolyBezier 函数用于绘制一条 Bezier 参数曲线,其函数声明如下: BOOL PolyBezier(const POINT* lpPoints, int nCount); 其中参数lpPoints 与Polyline 函数中相同,指向POINT 结构或者CPoint 对象数组 ,在数组中存放的是 Bezier 参数曲线的控制点 。nCount 是控制点的个 数。关于Bezier 参数曲线的知识后面将会详细的介绍,这里只简单说明一下该 函数的参数要求 :首先控制点的个数要求是 3n+1 个,其中 n是Bezier 参数曲线 的段数 ,比如要绘制两段 Bezier 参数曲线 ,控制点的个数应为 7,其中第 1到第 4的四个点控制第一段曲线,第4到第7的四个点控制第二段曲线。当要绘制 的曲线是两段或者两段以上时 ,为了保证曲线各段之间光滑连接 ,要求相接的两 段曲线连接处的三个控制点要共线,如曲线是由两段构成时,控制点数为 7,那 么要求第3、第4和第5个点三点共线。如果绘图成功,函数返回TRUE,否 则返回 FALSE。 � PolyBezierTo PolyBezierTo PolyBezierTo PolyBezierTo 函数也用于绘制 Bezier 参数曲线。其函数声明如下: BOOL PolyBezierTo(const POINT* lpPoints, int nCount); 其参数含义与 PolyBezier 函数相同。该函数与 PolyBezier 函数的区别是: PolyBezierTo 函 数默 认第 一个 控制 点为 当前 绘图 位置 ,所 以 PolyBezier 函 数的 控制点个数要求是 3n个;PolyBezierTo 函数绘制完曲线后 ,将当前绘图位置移 动到最后一个控制点处。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here POINT p[7]; p[0].x = 200;p[0].y = 100; p[1].x = 200;p[1].y = 200; p[2].x = 300;p[2].y = 200; p[3].x = 400;p[3].y = 300; p[4].x = 500;p[4].y = 400; p[5].x = 300;p[5].y = 400; p[6].x = 100;p[6].y = 450; pDC->PolyBezier(p,7); } 运行程序,结果如图 1.28 所示。 再修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here POINT p[6]; pDC->MoveTo(200,100); p[0].x = 200;p[0].y = 200; p[1].x = 300;p[1].y = 200; p[2].x = 400;p[2].y = 300; p[3].x = 500;p[3].y = 400; p[4].x = 300;p[4].y = 400; p[5].x = 100;p[5].y = 450; pDC->PolyBezierTo(p,6); } 运行程序,其结果与 1.28 所示相同。 两段程序分别使用PolyBezier 函数和PolyBezierTo 函数绘制了相同的 Bezier 曲线,我们从代码上可以很容易看出两个函数的差别。 1.4.71.4.71.4.71.4.7 Rectangle Rectangle Rectangle Rectangle 函数 � Rectangle Rectangle Rectangle Rectangle 函数用于绘制矩形,其函数声明如下: BOOL Rectangle(int x1, int y1, int x2, int y2); BOOL Rectangle(LPCRECT lpRect); 其中第一个函数参数给出了两组点坐标,第一组为矩形的左上角点坐标 ,第 二组为矩形的右下角点坐标 。第二个函数使用了指向矩形区域结构的指针作为参 数,也可以使用 CRect 类。如果绘图成功 ,函数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->Rectangle(50,50,200,80); CRect r; r.left = 100;r.right=200; r.top = 100;r.bottom = 200; pDC->Rectangle(r); } 运行程序,结果如图 1.29 所示: 1.4.81.4.81.4.81.4.8 RoundRect RoundRect RoundRect RoundRect 函数 � RoundRect RoundRect RoundRect RoundRect 函数用于绘制圆角矩形 ,即四个角被椭圆化的矩形 。如果将 圆角矩形的四个圆角移动到一起 ,就可以拼成一个完整的椭圆 。其函数声明如下 : BOOL RoundRect(int x1, int y1, int x2, int y2, int x3, int y3); BOOL RoundRect(LPCRECT lpRect,POINT point); 其中,第一个函数的参数 x1和y1指定了矩形的左上角点坐标,参数 x2和 y2指定了矩形的右下角点坐标,参数 x3指定了用来绘制圆角的椭圆的宽度 ,参 数y3指定了用来绘制圆角的椭圆的高度。如图1.30 所示。第二个函数用矩形 区域结构来存放矩形的左上角和右下角坐标,用 POINT 结构的 x来存放圆角椭 圆的宽度 ,y来存放圆角椭圆的高度 。如果绘图成功 ,函数返回 TRUE,否则返 回FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->RoundRect(50,50,200,80,10,10); CRect r; r.left = 100; r.right=200; r.top = 100; r.bottom = 200; POINT p; p.x = 50; p.y = 30; pDC->RoundRect(r,p); } 运行程序,结果如图 1.31 所示。 1.4.91.4.91.4.91.4.9 Ellipse Ellipse Ellipse Ellipse 函数 � Ellipse Ellipse Ellipse Ellipse 函数用于绘制椭圆或者圆,其函数声明如下: BOOL Ellipse(int x1, int y1, int x2, int y2); BOOL Ellipse(LPCRECT lpRect); 其中 ,第一个函数的参数 x1和y1指定了要绘制的椭圆或者圆的外接矩形的 左上角点坐标,参数x2和y2指定了要绘制的椭圆或者圆的外接矩形的右下角 点坐标 。第二个函数的参数使用了矩形区域结构来存放外接矩形的左上角和右下 角坐标。如果绘图成功,函数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->Ellipse(50,50,200,80); CRect r; r.left = 100; r.right=200; r.top = 100; r.bottom = 200; pDC->Ellipse(r); } 运行程序,结果如图 1.32 所示。 因为程序中 CRect 类对象 r定义的是一个正方形区域,所以 Ellipse 函数绘 制的是一个圆。 1.4.101.4.101.4.101.4.10Pie Pie Pie Pie 函数 � Pie Pie Pie Pie 函数用于绘制扇形,它是由椭圆弧与其起始点和终止点所对应的椭 圆的半径所围成的图形,其函数声明如下: BOOL Pie(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL Pie(LPCRECT lpRect,POINT ptStart,POINT ptEnd); 其参数含义与 Arc 函数相同 ,差别在于 Arc 函数只绘制了椭圆弧 ,而Pie 函 数将椭圆弧所确定的扇形绘制出来 。如果绘图成功 ,函数返回 TRUE,否则返 回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->Pie(100,100,300,300,100,150,250,150); } 运行程序,结果如图 1.33 所示。 程序中使用的参数值与 Arc 函数示例中的参数值相同 ,通过对比两个函数的 运行结果,就可以知道两个函数的差别。 1.4.111.4.111.4.111.4.11 Chord Chord Chord Chord 函数 � Chord Chord Chord Chord 函数用于绘制一个椭圆与一条直线相交所得到的图形 ,其函数声 明如下: BOOL Chord(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL Chord(LPCRECT lpRect,POINT ptStart,POINT ptEnd); 其参数含义与Arc 函数和Pie 函数相同,差别在于Chord 函数绘制的是指 定的起始点和终止点所在的直线与椭圆相交所得到的图形 。如果绘图成功 ,函数 返回 TRUE,否则返回 FALSE。 我们仍然采用Arc 函数和Pie 函数示例时所使用的参数值,修改OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here pDC->Chord(100,100,300,300,100,150,250,150); } 运行程序,结果如图 1.34 所示。 对比运行结果 ,我们可以看出 ,Arc 函数 、Pie 函数和 Chord 函数都是由给 定的参数决定了一段椭圆弧 ,差别在于 Arc 函数只绘制了该段椭圆弧 ,Pie 函数 绘制了该段椭圆弧所对应的扇形,而Chord 函数则在椭圆弧的起始点和终止点 之间用直线段连接。 1.4.121.4.121.4.121.4.12Polygon Polygon Polygon Polygon 函数 � Polygon Polygon Polygon Polygon 函数用于绘制一个封闭的多边形,其函数声明如下: BOOL Polygon(LPPOINT lpPoints, int nCount); 参数 lpPoints 存放的是多边形的顶点坐标, nCount 是多边形的顶点数目, 该参数必须大于 2。如果绘图成功,函数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here POINT p[5]; p[0].x = 10;p[0].y = 10; p[1].x = 100;p[1].y = 100; p[2].x = 200;p[2].y = 100; p[3].x = 200;p[3].y = 200; p[4].x = 100;p[4].y = 50; pDC->Polygon(p,5); } 运行程序,结果如图 1.35 所示: 1.4.131.4.131.4.131.4.13PolyPolygon PolyPolygon PolyPolygon PolyPolygon 函数 � PolyPolygon PolyPolygon PolyPolygon PolyPolygon 函数用于绘制多个多边形,其函数声明如下: BOOL PolyPolygon(LPPOINT lpPoints,LPINT lpPolyCounts, int nCount); 其 中, 参数 lpPoints 存 放了 所有 多边 形的 顶点 ,参 数 lpPolyCounts 指 向一 个整数的数组,该数组中的每一个数定义了相应的多边形的顶点的数量,参数 nCount 则 定义 了所 要绘 制的 多边 形的 数量 ,它 的值 必须 大于 等于 2。 如果 绘图 成功,函数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here POINT p[6]; p[0].x = 10;p[0].y = 10; p[1].x = 100;p[1].y = 100; p[2].x = 200;p[2].y = 100; p[3].x = 200;p[3].y = 200; p[4].x = 100;p[4].y = 50; p[5].x = 150;p[5].y = 20; int d[2]; d[0] = 3;d[1] = 3; pDC->PolyPolygon(p,d,2); } 运行程序,结果如图 1.36 所示: 以上就是CDC 中比较常用的绘图函数。在CDC 中还定义了其它一些绘图 函数,比如FillRect 函数,FillSolidRect 函数,FillRgn 函数,PaintRgn 函数等 填充封闭区域的绘图函数 ,这些函数将在后面介绍绘图属性时加以介绍 。还有另 外一些绘图函数,如DrawEdge 函数,它可以给一个绘制的矩形添加一个立体 的方框 ;Draw3dRect 函数 ,它可以绘制一个立体的矩形等等 ,这类函数相对来 说并不常用到,这里就不做详细的介绍了。 1.51.51.51.5 视图重画 在前面介绍OnDraw 函数的时候,曾经提到过当视图需要进行重新绘制, 也就是视图重画时, OnDraw 函数可以进行调用。视图重画在 MFC 绘图中是非 常重要的概念,现在我们就来详细说明一下什么是视图重画。 在上一节介绍绘图函数的时候 ,给出了很多演示程序 ,我们执行任意一个演 示程序 ,当应用程序执行后 ,我们可以在应用程序窗口的视图区看到我们编写的 代码所绘制的图形 ,这时我们选择把应用程序窗口最小化 ,或者让其它的应用程 序窗口遮挡该窗口,然后我们恢复原来的应用程序窗口(如果窗口是被遮挡的 , 就让应用程序窗口不再被遮挡 ),这时我们可以看到原来所绘制的图形仍然存在 。 这是因为前面我们所有编写的绘图代码都是写在OnDraw 函数或者是由 OnDraw 函数所调用的函数当中的 。当应用程序窗口从最小化状态恢复 ,或者改 变窗口尺寸大小 ,或者窗口恢复原来被遮挡的部分等等情况下 ,应用程序窗口都 需要进行重画,这时应用程序框架就会自动调用OnDraw 函数,所以写在 OnDraw 函数或者 OnDraw 函数所调用的函数当中的绘图代码就会被自动执行 , 这样我们就仍然能够看到这些代码所绘制的图形。如果绘图代码没有写在 OnDraw 函数或者由 OnDraw 函数调用的函数中 ,情况会是如何呢?下面我们通 过一个演示程序来看一下。 前面介绍绘图函数时 ,每演示一个绘图函数都需要重新修改 OnDraw 函数 , 这样无疑是非常不方便的 。现在我们将所创建的应用程序修改一下 ,给它添加菜 单项 ,每个菜单项执行不同的绘图函数 。这样该应用程序就可以同时演示我们前 面所介绍的所有绘图函数了。同时菜单项所调用的处理函数是不会被OnDraw 函数自动调用到的,我们也可以观察此时视图重画的情况。 1.5.11.5.11.5.11.5.1 编辑应用程序菜单 选择资源面板 ,在Menu(菜单 )节点下 ,双击 IDR_MAINFRAME 页节点 , 在编辑区就会将当前应用程序的菜单打开。如图1.37 所示。该菜单是由MFC 应用程序框架所提供的初始菜单 。我们要修改此菜单 ,添加我们所需要的菜单项 。 在修改菜单之前,我们先了解一些菜单编辑的相关知识。我们都知道, Windows 的菜单是一种分级菜单,我们在菜单编辑区中用鼠标选择 “文件 (F)” 菜单项 ,会打开该菜单项的下级菜单 ,如图 1.38 所示 。我们可以称 “文件 (F)” 菜单项为第一级菜单 ,而其下级菜单为第二级菜单 ,如果第二级菜单还有下级菜 单,选择该第二级菜单,其下级菜单会自动打开,这些菜单就是第三级菜单 ,以 此类推 。选中菜单项后可以按 “Delete”键将该菜单项删除 ,如果该菜单项有下 级菜单,这些下级菜单将同时被删除,所以在删除具有下级菜单的菜单项之前 , 系统会进行讯问是否删除 ,选择 “确定 ”将会进行删除 。初始菜单中的 “文件 (F)” 和“编辑 (E)”两个第一级菜单在我们的应用程序中是不需要的 ,我们可以将这 两个一级菜单删除。 看图 1.38,我们注意到在一级菜单项 “帮助 (H)”右侧有一个虚线矩形框 , 此处就是用来添加菜单项的,因为它在一级菜单栏上,所以添加的是一级菜单 。 同 样的 ,在 “文件(F)”菜 单项 下的 二级 菜单 “退出(X)”下 ,也 有一 个虚 线矩 形框 ,此处也可以添加菜单项 ,只是此处添加的是二级菜单 。我们可以用鼠标左 键选择虚线矩形框,并按住鼠标左键不放,就可以挪动菜单项的位置。比如 ,我 们可以将一级菜单的虚线矩形框移到所有一级菜单的最左边 ,这样我们创建的菜 单就会出现在最左边。如图1.39 所示,该菜单已经删除了“文件(F)”和“编 辑(E)”两个一级菜单 。其它菜单项的位置也可以如此进行移动 ,这样我们就可 以自由的调整菜单项的显示顺序。 现在我们来添加菜单 。在添加菜单之前 ,我们需要首先确定我们希望添加什 么样的菜单项 ,菜单的结构是什么样的 。前面所介绍的绘图函数按照所绘制的图 形可以大致分为两类 ,一类绘制的是线形的图形 ,另一类绘制的是封闭区域图形 。 按照此分类,我们将创建下表中所列结构的菜单。其中MoveTo 函数因为本身 并不绘制图形,所以单独作为一个二级菜单项,它没有下级菜单。ID是菜单项 的标志符,应用程序框架通过 ID来区分各种资源。 一级菜单 二级菜单 三级菜单 IDIDIDID 绘图函数 线形绘图函数 LineTo ID_DRAW_LINETO Polyline ID_DRAW_POLYLINE Arc ID_DRAW_ARC AngleArc ID_DRAW_ANGLEARC PolyBezier ID_DRAW_POLYBEZIER 区域绘图函数 Rectangle ID_DRAW_RECTANGLE RoundRect ID_DRAW_ROUNDRECT Ellipse ID_DRAW_ELLIPSE Pie ID_DRAW_PIE Chord ID_DRAW_CHORD Polygon ID_DRAW_POLYGON 用 鼠标 双击 虚线 矩形 框, 会出 现 “Menu Item Properties”( 菜单 项属 性) 对话框。该对话框用于设置菜单项属性,如果双击的是一个已经存在的菜单项 , 则该对话框显示该菜单项的各种属性。该对话框有两个分页 :“General”(通用 属性)和 “Extended Styles”(扩展类型 )。在 “Extended Styles”页中只有一 个“Right-to-left order and alignment”复选框,选择该复选框,菜单项标题 将由右到左显示,并向右对齐。通常情况我们都不会选择它。 在“General”页中可以设置菜单的各种属性。 “CaptionCaptionCaptionCaption”输 入框 。用于输入菜单标题 ,应用程序执行时 ,该标题是我们 所看到的菜单项的文字。 “PopPopPopPop----upupupup”复 选框 。选中此复选框表示此菜单项具有下级菜单 ,此时 “ID” 下拉输入框 ,“Prompt”(说明)输入框及 “Separator”复选框将处于灰色无效 状态。 “IDIDIDID”下 拉输 入框 。不选择 “Pop-up”复选框 ,“ID”下拉输入框可以输入 , 在此输入此菜单项的 ID。下拉列表中会列出当前应用程序中所有已经使用的 ID。 同时,如果此 ID可以输入,则必须输入,否则系统将自动生成一个 ID。 “SeparatorSeparatorSeparatorSeparator”复 选框 。 选中该复选框,菜单项将变成一条菜单分隔线 ,此 时除 “Caption”外,其它属性将都不能进行设置 ,而“Caption”虽然可以输入 , 但是一旦在 “Caption”中输入值,则 “Separator”复选框的选择将自动取消 。 “CheckedCheckedCheckedChecked”复选框。选中该复选框,应用程序运行时将在该菜单项前预 先设置一个检查符号。 “InactiveInactiveInactiveInactive”复选框。 选中该复选框,在应用程序运行时该菜单项不出现 。 “GrayedGrayedGrayedGrayed”复选框。选 中该 复选 框, 该菜 单项 以灰 色显 示, 表示 此菜 单项 不可用。 “HelpHelpHelpHelp”复选框。选中该复选框,应用程序运行时将自动检查菜单项的合 法性。 “PromptPromptPromptPrompt”输 入框 。 用于输入菜单项的说明,当应用程序运行时,如果鼠 标悬停在菜单项上,该说明将在状态栏中显示。 “BreakBreakBreakBreak”下 拉框 。在此下拉框中有三项可选 :“None”,“Column”,“Bar”。 它们决定菜单的显示样式 ,默认是 “None”。选择 “Column”,菜单项将以列的 方式进行排列 ;选择 “Bar”,菜单项以列的方式进行排列的同时将由竖线分隔开 。 读者可以自行设置来观察效果 ,本应用程序的菜单都采用默认的 “None”。同时 要注意的是 ,一个菜单项选择了非默认的菜单样式 ,所有的同级菜单项都将受到 影响 ,如果同级菜单项中既有选择 “Column”的,也有选择 “Bar”的,则菜单 样式将为 “Bar”所指定的样式。 我们在当前打开的菜单项属性对话框中选择“Pop-up”复选框,并在 “Caption”输入框输入 “绘图函数 ”,关闭对话框,我们将看到 “绘图函数 ”一 级菜单已经创建 。同时在该菜单项下有一个虚线矩形框 ,我们可以双击它来输入 下级菜单。如图 1.41 所示。 双击图1.41 中“绘图函数”菜单项下的虚线矩形框,在打开的菜单项属性 对话框中 ,选择 “Pop-up”复选框 ,并在 “Caption”输入框输入 “线形绘图函 数”,关闭对话框 ,这样就创建了 “线形绘图函数 ”二级菜单 。如图 1.42 所示 。 双击图 1.42 中“线形绘图函数 ”菜单项右侧的虚线矩形框 ,在打开的菜单 项属性对话框中 ,不选择 “Pop-up”复选框 ,在“Caption”输入框输入 “LineTo”, 在“ID”下拉输入框中输入“ID_DRAW_LINETO”,然后关闭对话框,这样就 创建了 “LineTo”三级菜单。如图 1.43 所示。 按照上述方法,可以将我们所需要的菜单项创建出来,二级菜单项输入 “Caption”,并且选择 “Pop-up”复选框 ,三级菜单项不选择 “Pop-up”复选 框,输入 “Caption”和“ID”,具体值在菜单结构表中已经列出 。创建完的菜单 如图 1.44 所示。 我们可以在 “线形绘图函数 ”菜单项和 “区域绘图函数 ”菜单项之间加上一 条分隔线 。双击 “区域绘图函数 ”菜单项下的虚线矩形框 ,在打开的菜单项属性 对话框中选择 “Separator”复选框 。然后关闭对话框 ,并将该菜单项移动到 “区 域绘图函数 ”菜单项上面。其结果如图 1.45 所示。 此时我们运行应用程序,可以看到应用程序菜单已经是我们所创建的了 。但 是各菜单项都处于灰色不可用状态 ,这是因为我们还没有为各菜单项连接处理函 数。下面我们将用 ClassWizard(类向导)为菜单项来连接处理函数。 1.5.21.5.21.5.21.5.2 使用 ClassWizard ClassWizard ClassWizard ClassWizard 为菜单项连接处理函数 在Visual Studio C++ 6.0 开发环境中 ,选中 “View”菜单下的 “ClassWizard” 菜单项 ,或者按 “Ctrl+W”的快捷键组合 ,将出现 “MFC ClassWizard”(MFC 类向导)对话框。如图 1.46 所示。 MFC 类向导的功能很多 ,在进行 MFC 编程的时候经常会用到 。我们现在只 介绍 MFC 类向导中我们将用到的功能 ,其它功能等到后面用到的时候再详细介 绍。MFC 类向导对话框有 “Message Maps”等五个分页 ,其中 “Message Maps” 分页是我们最常用到的。为菜单项连接处理函数也是在这一分页中。 在MFC 类向导对话框的 “Message Maps”分页中 ,“Project:”下拉框中 是当前正在编辑的项目名称 ,图1.46 中显示的就是我们现在所编辑的项目的名 称DrawTest;“Class name:”下拉框中列出了当前项目中所包含的类 ,图1.46 中当前正在显示的是CMainFrame 类,我们可以通过下拉列表来选择其它类; “Object IDs:”列表框中显示的是当前选择的类以及应用程序中所有的资源 ID; “Messages:”列表框中显示的是 “Object IDs:”列表框中所选中的类或者 ID 所代表的资源所支持的 MFC 系统消息,图 1.46 中“Object IDs:”列表框中选 中了CMainFrame 类,所以“Messages:”列表框中显示的CMainFrame 类所 支持的 MFC 系统消息 ;“Member functions:”列表框中显示的是选中的类当前 已有的成员函数。 我们在 “Class name:”下拉框中选择 CDrawTestView 类,然后在 “Object IDs:”列表框中选中ID_DRAW_LINETO,这是为我们刚才创建的菜单中的 “LineTo”三级菜单所设置的 ID。在 “Object IDs:”列表框中我们可以看到所 设置的所有ID。选中ID_DRAW_LINETO 后,我们会看到在“Messages:”列 表框中列出了它所支持的MFC 系统消息,也就是菜单项所支持的MFC 系统消 息。而在“Member functions:”列表框中列出了CDrawTestView 类当前所有 的成员函数。如图 1.47 所示。 从图1.47 中可以看到,菜单项支持两种消息:COMMAND 消息和 UPDATE_COMMAND_UI 消息。COMMAND 消息是鼠标单击菜单项时应用程 序发出的消息 ,UPDATE_COMMAND_UI 消息是菜单项形成或者发生改变时应 用程序发出的消息。我们是要为菜单项连接用户点击时的处理函数,所以选择 COMMAND 消息。此时,右侧的 “Add Function”按钮变为可用,我们可以点 击 此按 钮, 或者 双击 COMMAND 消 息, 都可 以打 开 “Add Member Function” (添加成员函数)对话框,如图 1.48 所示。 在对话框的 “Member function name:”输入框中,显示的是 MFC 类向导 为新的成员函数所自动生成的一个函数名 ,我们可以修改函数名 ,但是建议还是 采用生成的函数名称。在对话框下端,列出了这个处理函数是针对ID 为 ID_DRAW_LINETO 资源 的COMMAND 消息所创建的处理函数 。点击OK 按钮 , 则此成员函数就被加入到CDrawTestView 类中。如图1.49 所示。我们前面选 择CDrawTestView 类,就是为了将菜单项的处理函数加入到该类中 ,当然这个 处理函数也可以加入到其它类中 ,但是 CDrawTestView 类是视图类 ,用于进行 图形绘制的处理函数最好还是放在该类中。 在“Member functions:”列表框中我们可以看到刚才添加的成员函数,并 且处于选中状态 ,我们可以双击该成员函数 ,或者点击右侧 “Edit Code”按钮 , 系统会自动定位到 OnDrawLineto 函数处 ,让用户进行代码编写 。当应用程序运 行时 ,如果用户点击 “LineTo”菜单项 ,则应用程序框架会自动调 用OnDrawLineto 函 数。 我们 编写 OnDrawLineto 函 数, 用它 来演 示 LineTo 绘 图函 数, 输入 如下 代码: void CDrawTestView::OnDrawLineto() { //TODO: Add your command handler code here CClientDC dc(this); dc.MoveTo(300,300); dc.LineTo(400,400); } 按照上面的方法 ,添加其它菜单项的处理函数 ,我们可以在 MFC 类向导中 一次把所有要添加的处理函数都添加完,再对其进行编辑。 编写 “Polyline”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawPolyline() { //TODO: Add your command handler code here CClientDC dc(this); POINT p[5]; p[0].x = 300;p[0].y = 200; p[1].x = 400;p[1].y = 200; p[2].x = 400;p[2].y = 300; p[3].x = 350;p[3].y = 350; p[4].x = 320;p[4].y = 220; dc.Polyline(p,5); } 编写 “Arc”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawArc() { //TODO: Add your command handler code here CClientDC dc(this); dc.Arc(100,200,300,300,280,120,120,280); } 编写 “AngleArc”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawAnglearc() { //TODO: Add your command handler code here CClientDC dc(this); dc.MoveTo(450,100); dc.AngleArc(550,200,50,90,270); } 编写 “PolyBezier”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawPolybezier() { //TODO: Add your command handler code here CClientDC dc(this); POINT p[7]; p[0].x = 200;p[0].y = 100; p[1].x = 200;p[1].y = 200; p[2].x = 300;p[2].y = 200; p[3].x = 400;p[3].y = 300; p[4].x = 500;p[4].y = 400; p[5].x = 300;p[5].y = 400; p[6].x = 100;p[6].y = 450; dc.PolyBezier(p,7); } 编写 “Rectangle”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawRectangle() { //TODO: Add your command handler code here CClientDC dc(this); dc.Rectangle(450,100,650,250); } 编写 “RoundRect”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawRoundrect() { //TODO: Add your command handler code here CClientDC dc(this); dc.RoundRect(650,250,850,400,20,20); } 编写 “Ellipse”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawEllipse() { //TODO: Add your command handler code here CClientDC dc(this); dc.Ellipse(450,400,650,500); } 编写 “Pie”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawPie() { //TODO: Add your command handler code here CClientDC dc(this); dc.Pie(100,300,300,400,280,220,120,380); } 编写 “Chord”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawChord() { //TODO: Add your command handler code here CClientDC dc(this); dc.Chord(100,400,300,500,280,320,120,480); } 编写 “Polygon”菜单项处理函数,输入如下代码: void CDrawTestView::OnDrawPolygon() { //TODO: Add your command handler code here CClientDC dc(this); POINT p[4]; p[0].x = 300;p[0].y = 150; p[1].x = 350;p[1].y = 1; p[2].x = 420;p[2].y = 200; p[3].x = 350;p[3].y = 150; dc.Polygon(p,4); } 那么应用程序是如何将菜单项和处理函数实际连接起来的呢?我们可以看 一下CDrawTestView 类的类文件的类声明之前有一个 BEGIN_MESSAGE_MAP 宏,在这个宏中 ,应用程序完成了资源 ID与其处理函 数的实际连接,现在该宏内容如下: BEGIN_MESSAGE_MAP(CDrawTestView, CView) //{{AFX_MSG_MAP(CDrawTestView) ON_COMMAND(ID_DRAW_LINETO, OnDrawLineto) ON_COMMAND(ID_DRAW_POLYLINE, OnDrawPolyline) ON_COMMAND(ID_DRAW_ARC, OnDrawArc) ON_COMMAND(ID_DRAW_ANGLEARC, OnDrawAnglearc) ON_COMMAND(ID_DRAW_POLYBEZIER, OnDrawPolybezier) ON_COMMAND(ID_DRAW_RECTANGLE, OnDrawRectangle) ON_COMMAND(ID_DRAW_ROUNDRECT, OnDrawRoundrect) ON_COMMAND(ID_DRAW_ELLIPSE, OnDrawEllipse) ON_COMMAND(ID_DRAW_PIE, OnDrawPie) ON_COMMAND(ID_DRAW_CHORD, OnDrawChord) ON_COMMAND(ID_DRAW_POLYGON, OnDrawPolygon) //}}AFX_MSG_MAP // Standard printing commands ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint) ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview) END_MESSAGE_MAP() 可见 ,正是在这个宏中完成了菜单项和处理函数的实际连接 。这部分代码是 由系统自动添加的 ,用户并不需要自己编写 ,用户只需要知道实际连接在代码上 是如何实现的就可以了 。同时 ,我们可以看一下这些处理函数在头文件中的声明 。 打开 CDrawTestView 类的头文件,可以看到处理函数的声明如下: protected: //{{AFX_MSG(CDrawTestView) afx_msg void OnDrawLineto(); afx_msg void OnDrawPolyline(); afx_msg void OnDrawArc(); afx_msg void OnDrawAnglearc(); afx_msg void OnDrawPolybezier(); afx_msg void OnDrawRectangle(); afx_msg void OnDrawRoundrect(); afx_msg void OnDrawEllipse(); afx_msg void OnDrawPie(); afx_msg void OnDrawChord(); afx_msg void OnDrawPolygon(); //}}AFX_MSG 同样,这些声明也是由系统自动添加的,不需要用户自己编写。 运行应用程序,我们可以通过选择菜单项来看对应的绘图函数的执行结果 。 此时 ,如果把应用程序窗口最小化 ,然后再恢复该窗口 ,我们就会看到原来在视 图区所绘制的图形已经没有了 ,这是因为我们现在所编写的绘图代码是 OnDraw 函数所调用不到的 。因此在窗口重画的时候 ,并不能把原来已有的图形重新绘制 出来 。为了能使绘制的图形在视图重画的时候能够正确显示 ,就需要将图形的相 关数据先存储起来,然后在OnDraw 函数中将这些图形重新绘制一遍。具体的 做法我们将在下一章中详细介绍。 1.5.31.5.31.5.31.5.3 主动视图重画 前面我们介绍了OnDraw 函数是在应用程序窗口需要重新绘制的时候,由 应用程序框架来自动调用的。有时候,我们需要通过程序代码来引起视图重画 , 即引起 OnDraw 函数的调用。 MFC 提供了相应的函数,使我们可以主动引发视 图重画。 主动视图重画可以分为两种类型:全部重画和局部重画。 1.1.1.11.1.1.11.1.1.11.1.1.1 全部重画 全部重画可以通过调用窗口类的 Invalidate Invalidate Invalidate Invalidate 函数来实现 ,其函数声明如下 : void CWnd::Invalidate(BOOL bErase = TRUE); 参数 bErase 为TRUE 时,应用程序在调用 OnDraw 函数之前会先清空视图 区;如果为 FALSE,则不清空。 Invalidate 函数在调用的时候可以不传入参数 , 此时默认 bErase 为TRUE。 这里需要注意的是 ,调用 Invalidate 函数后 ,应用程序框架并不是马上调 用 OnDraw 函数进行重画,而是等到调用 Invalidate 函数的函数执行完毕之后 ,才 会调用 OnDraw 函数,所以调用 Invalidate 函数通常写在函数执行的最后一句 。 1.1.1.21.1.1.21.1.1.21.1.1.2 局部重画 全部重画是针对整个视图区进行重画 ,而有时候 ,绘图工作只是在视图区中 的一个局部区域进行的,此时进行全部重画,会造成一些不必要的资源浪费 ,有 时还会使屏幕产生闪烁的现象 。针对这种问题 ,可以采用局部重画的方法 ——使 指定的区域无效,而不是全部视图区都失效。局部重画通过调用 InvalidateRect 函数或者 InvalidateRgn 函数来实现。 � InvalidateRect InvalidateRect InvalidateRect InvalidateRect 的函数声明如下: void CWnd::InvalidateRect(LPCRECT lpRect,BOOL bErase = TRUE); 其中参数lpRect 指定了一个矩形区域,如果该参数传入NULL 的话,代表 的 是整 个视 图区 。参 数 bErase 的 含义 与 Invalidate 函 数中 的相 同, 只是 此处 清 空的是指定区域的内部。 � InvalidateRgn InvalidateRgn InvalidateRgn InvalidateRgn 的函数声明如下: void CWnd::InvalidateRgn(CRgn* pRgn,BOOL bErase = TRUE); 其中参数 pRgn 是一个指向 CRgn 类的指针 ,如果该指针为 NULL,则代表 无效区域为整个视图区。参数 bErase 的含义与 InvalidateRect 函数中的相同。 CRgn 类是MFC 中提供的一个区域对象类 ,它不同 于CRect 类的地方在于 : CRect 类只能够定义一个矩形区域,而 CRgn 类可以定义任意类型的封闭区域 , 它可以是矩形,也可以是椭圆形或者多边形,所以 CRgn 类应用起来更加灵活 。 1.1.1.31.1.1.31.1.1.31.1.1.3 CRgn CRgn CRgn CRgn 类 我们使用 CRgn 类的构造函数来创建一个没有被初始化的区域对象: CRgn(); 创建的 CRgn 对象必须进行初始化才能够与实际的区域进行关联 ,CRgn 类 提供了一系列的初始化函数,用来初始化不同的区域。 � CreateRectRgn CreateRectRgn CreateRectRgn CreateRectRgn 函数,用于初始化一个矩形区域,其函数声明如下: BOOL CreateRectRgn(int x1, int y1, int x2, int y2); 其中的参数给出了矩形区域的左上角点和右下角点坐标。如果区域创建成 功,函数返回 TRUE,否则返回 FALSE。 � CreateRectRgnIndirect CreateRectRgnIndirect CreateRectRgnIndirect CreateRectRgnIndirect 函数,用于间接初始化一个矩形区域,其函数 声明如下: BOOL CreateRectRgnIndirect(LPCRECT lpRect); 该函数是通过一个已有的矩形区域对象来创建矩形区域的 CRgn 对象 ,传入 的参数可以是RECT 结构或者是CRect 对象。如果区域创建成功,函数返回 TRUE,否则返回 FALSE。 � CreateEllipticRgn CreateEllipticRgn CreateEllipticRgn CreateEllipticRgn 函数 ,用于初始化一个椭圆形区域 ,其函数声明如下 : BOOL CreateEllipticRgn(int x1, int y1, int x2, int y2); 其中参数给出了椭圆区域的外接矩形的左上角点和右下角点坐标 。如果区域 创建成功,函数返回 TRUE,否则返回 FALSE。 � CreateEllipticRgnIndirect CreateEllipticRgnIndirect CreateEllipticRgnIndirect CreateEllipticRgnIndirect 函数,用于间接初始化一个椭圆型区域 ,其 函数声明如下: BOOL CreateEllipticRgnIndirect(LPCRECT lpRect); 该函数参数将椭圆区域的外接矩形的左上角点和右下角点坐标存放在一个 矩形区域对象中,该参数可以是一个 RECT 结构或是一个 CRect 对象。如果区 域创建成功,函数返回 TRUE,否则返回 FALSE。 � CreatePolygonRgn CreatePolygonRgn CreatePolygonRgn CreatePolygonRgn 函数 ,用于初始化一个多边形区域 ,其函数声明如 下: BOOL CreatePolygonRgn(LPPOINT lpPoints, int nCount, int nMode); 其中参数lpPoints 执行一个POINT 结构数组或者一个CPoint 对象数组, 该数组中存放了多边形区域的顶点坐标;参数 nCount 为多边形区域的顶点数 ; 参数 nMode 为该区域的填充模式,取值可以为 ALTERNATE 或WINDING。 以上为CRgn 类中常用的初始化方法。需要注意的是CRgn 类的析构函数 并不能删除初始化后的CRgn 对象,要删除已经初始化的CRgn 对象,需要调 用其基类的成员函数DeleteObject 来完成,该函数没有任何参数,直接调用即 可。因为CRgn 类对象要占用系统资源,如果创建了过多CRgn 类对象而不删 除的话 ,其占用的资源就不会释放 ,这将造成应用程序因为系统资源不足而无法 运行的情况,所以编程时,创建的CRgn 对象在使用完毕之后一定要调用 DeleteObject 函数来将其删除。 CRgn 类同时提供了一些区域对象操作函数: � OffsetRgn OffsetRgn OffsetRgn OffsetRgn 函数,用于按照给定的偏移量移动一个区域,其函数声明如 下: int OffsetRgn(int x, int y); int OffsetRgn(POINT point); 其中参数x表示区域对象沿着X轴方向向左或向右的移动量,参数y表示 区域对象沿着 Y轴方向向上或向下的移动量 。参数 point 的两个分量 x和y的含 义与上面的参数 x和y相同 。该函数返回一个新的区域类型 ,下表中介绍了几种 区域类型: 返回值 说明 COMPLEXREGION 表示一个具有重叠边界的区域 ERROR 表示区域无效 NULLREGION 表示区域为空 SIMPLEREGION 表示一个不具有重叠边界的区域 � PtInRegion PtInRegion PtInRegion PtInRegion 函数 ,用于判断给定的点是否在区域内部 ,其函数声明如下 : BOOL PtInRegion(int x, int y) const; BOOL PtInRegion(POINT point) const; 该函数判断传入的点坐标是否在当前区域的内部,如果在内部,则返回 TRUE,否则返回 FALSE。 � RectInRegion RectInRegion RectInRegion RectInRegion 函数,用于判断给定的矩形的任意部分是否在区域边界 的内部,其函数声明如下: BOOL RectInRegion(LPCRECT lpRect) const; 函数判断传入的矩形区域是否有任意部分在当前区域边界的内部,如果有 , 函数返回 TRUE,否则返回 FALSE。 � CombineRgn CombineRgn CombineRgn CombineRgn 函数 ,用于初始化一个区域 ,该区域由两个已经存在的区 域之间的相互关系来确定,其函数声明如下: int CombineRgn(CRgn* pRgn1, CRgn* pRgn2, int nCombineMode); 其中参数pRgn1和pRgn2为指向两个已经存在的区域对象的指针; nCombineMode 是对这两个区域对象操作所采用的模式 ,可采用的模式如下表 : 参数值 操作模式 RGN_AN D 用两个区域的相交部分初始化新的区域对象 RGN_CO PY 初始化和第一个区域相同的区域对象 RGN_DIF F 用第一个区域中没有与第二个区域相交的部分初始化新的区域对象 RGN_OR 初始化包含两个区域的新的区域对象 RGN_XO R 初始化包含两个区域但不包含两个区域相交部分的新的区域对象 根据不同的操作结果,函数返回下表中所列的值: 返回值 说明 COMPLEXREGION 表示区域覆盖边界 ERROR 表示区域无效 NULLREGION 表示区域为空 SIMPLEREGION 表示区域没有覆盖边界 以上是 CRgn 类中比较常用的成员函数。 现在我们可以实际看一下主动视图重画的效果 ,我们在当前应用程序中添加 如下菜单,并将其移动到 “绘图函数 ”右侧,创建完的菜单如图 1.50 所示。 一级菜单 二级菜单 IDIDIDID 视图重画 清空后全部重画 ID_DRAW_INVALIDATE 不清空全部重画 ID_DRAW_INVALIDATEF 矩形区域部分重画 ID_DRAW_INVALIDATERECT Rgn 区域部分重画 ID_DRAW_INVALIDATERGN 然后为菜单项连接处理函数 (注意 :函数要添加到 CDrawTestView 类中 ), 并输入如下代码: //清空后全部重画 void CDrawTestView::OnDrawInvalidate() { //TODO: Add your command handler code here this->Invalidate(); } //不清空全部重画 void CDrawTestView::OnDrawInvalidatef() { //TODO: Add your command handler code here this->Invalidate(false); } //矩形区域部分重画 void CDrawTestView::OnDrawInvalidaterect() { //TODO: Add your command handler code here CRect r; r.left = 400; r.right = 600; r.top = 200; r.bottom = 400; this->InvalidateRect(r,true); } //Rgn 区域部分重画 void CDrawTestView::OnDrawInvalidatergn() { //TODO: Add your command handler code here CRgn rgn; rgn.CreateEllipticRgn(250,250,350,350); this->InvalidateRgn(&rgn,true); rgn.DeleteObject(); } 以上代码分别演示了先清空视图区再全部重画,不清空视图区全部重画 ,矩 形区域的部分重画 。调用 InvalidateRgn 函数进行部分重画时 ,创建了一个椭圆 型区域。可以运行应用程序,来看一下视图重画的具体效果。 1.61.61.61.6 绘图工具 前面介绍的绘图函数只是确定了要绘制什么样的图形 ,比如直线段 ,椭圆弧 线,矩形等等 ,但是实际绘图时 ,我们往往希望能够绘制出不同线宽 ,不同线型 (如虚线,点划线等等 ),不同色彩的图形,在矩形、椭圆形等封闭区域内部填 充各种颜色或者不同的图形 。要完成以上要求 ,我们就必须使用绘图工具 。正如 实际画画时需要使用画笔和画刷一样, Windows 也提供相应的 GDI 对象类:画 笔类CPen 和画刷类CBrush。CPen 类用于控制绘图时的线的类型,宽度,颜 色等信息 ,它既应用 LineTo,Arc 等线形绘图函数 ,也影响区域绘图函数所绘制 的区域图形的边框。 CBrush 类用于控制区域图形的内部填充方式。 读者此时会注意到 ,我们前面编写绘图函数的演示程序时 ,并没有使用到画 笔和画刷 ,这是因为设备环境对象在没有指定画笔和画刷的情况下将使用默认的 画笔和画刷 。默认画笔画出的线是宽度为 1像素的黑色实线 。默认的画刷是白色 的实心画刷 ,该画刷在封闭区域内部用白色进行填充 。如果不想使用默认的画笔 和 画刷 ,可 以有 两种 选择 方法 :一 是使 用库 存的 绘图 工具 , CDC 提 供了 一些 库 存的定义好的画笔和画刷 ,二是根据需要创建 CPen 类和 CBrush 类对象 ,即自 定义绘图工具。下面将分别加以介绍。 1.6.11.6.11.6.11.6.1 使用库存的绘图工具 我们调 用CDC 的成员函 数SelectStockObject 函数来选择库存的绘图工具 。 � SelectStockObject SelectStockObject SelectStockObject SelectStockObject 函数声明如下: CGdiObject* SelectStockObject(int nIndex); 其中参数 nIndex 定义了可以使用的库存画笔和画刷,下表列出了库存画笔 和画刷所对应的 nIndex 值: nIndex nIndex nIndex nIndex 参数值 库存对象 BLACK_PEN 黑色画笔(设备环境对象默认画笔) NULL_PEN 空画笔(不画) WHITE_PEN 白色画笔 WHITE_BRUSH 白色画刷(设备环境对象默认画刷) BLACK_BRUSH 黑色画刷 DKGRAY_BRUSH 深灰色画刷 LTGRAY_BRUSH 浅灰色画刷 GRAY_BRUSH 灰色画刷 HOLLOW_BRUSH 空画刷(内部不填充) NULL_BRUSH 空画刷(没有刷子填充) 库存画笔绘制的线都是宽度为 1像素的实线 ,库存画刷都是用指定的颜色进 行实心填充。 该函数的返回值为指向CGidObject 对象的指针。CGidObject 类是GDI 对 象的基类 ,即它是 CPen 类和 CBrush 类的基类 。其具体返回值视该函数传入的 参数而定。如果传入的是库存的画笔,则返回的是原来正在使用的画笔的指针 ; 如果传入的是库存的画刷 ,则返回的是原来正在使用的画刷的指针 。如果设备环 境对象在选用库存绘图工具之前没有选择其它的画笔和画刷 ,则返回的是设备环 境对象默认的黑色画笔和白色画刷。 下面我们演示一下如何使用库存的绘图工具进行绘图 ,在应用程序中添加如 下菜单: 一级菜单 二级菜单 三级菜单 IDIDIDID 库存 绘图工具 库存画笔 BLACK_PENID_STOCK_BLACKPEN NULL_PENID_STOCK_NULLPEN WHITE_PENID_STOCK_WHITEPEN 库存画刷 WHITE_BRUSHID_STOCK_WHITEBRUS H BLACK_BRUSHID_STOCK_BLACKBRUS H DKGRAY_BRUS H ID_STOCK_DKGRAYBR USH GRAY_BRUSHID_STOCK_GRAYBRUS H LTGRAY_BRUS H ID_STOCK_LTGRAYBRU SH HOLLOW_BRUS H ID_STOCK_HOLLOWBU RSH NULL_BRUSHID_STOCK_NULLBRUSH 使用库存绘图 工具画矩形 ID_STOCK_DRAWRECT ANGLE 使用库存绘图 工具画椭圆 ID_STOCK_DRAWELLIP SE 其中“BLACK_PEN”和“WHITE_BRUSH”两个三级菜单在菜单项属性 对话框中把 “Checked”复选框选上。创建完的菜单如图 1.51 所示。 我们计划用户通过菜单选择要使用的库存绘图工具 ,然后在 “使用库存绘图 工具画矩形 ”和“使用库存绘图工具画椭圆 ”两个菜单项的处理函数中使用用户 选择的库存绘图工具来画矩形和椭圆 (这里我们只选择绘制这两种图形的绘图函 数来看效果,读者有兴趣的话也可以添加其它的绘图函数 )。因为每个菜单项的 处理函数都是单独的,所以为了记载用户选择的库存对象,需要在 CDrawTestView 类 中添 加两 个成 员变 量( 因为 我们 的处 理函 数要 添加 到这 个类 中)m_StockPen 和m_StockBrush,其类型为 int。我们可以直接在头文件中声 明这两个变量,也可以采用如下办法:在类面板中,用鼠标右键单击 CDrawTestView 节点,在弹出的快捷菜单中选择 “Add Member Variable…”, 出现添加成员变量对话框,如图 1.52 所示。 按图 1.52 所示,在对话框中的 “Variable Type:”(变量类型)输入框中输 入int,在“Variable Name:”(变量名称 )输入框中输入 m_StockPen,在“Access” (访问权限 )单选钮组中选择 “Public”(公有 ),然后单击 OK按钮 ,则系统会 在CDrawTestView 类中创建int 类型的公有变量m_StockPen。用同样方法添 加变量m_StockBrush。在类面板的CDrawTestView 节点下可以看到我们所创 建的成员变量。 我们需要给这两个变量赋上初值 ,在CDrawTestView 类的构造函数中添加 如下代码: m_StockPen = BLACK_PEN; m_StockBrush = WHITE_BRUSH; 我们希望用户选择了相应的菜单项之后,在该菜单项前面加上选中标记 ,如 图1.51 中“WHITE_BRUSH”菜单项 。该菜单项的选中标记是在设置菜单项属 性时 ,选择 “Checked”复选框所产生的 。但是这样设置的选中标记并不能满足 我们的要求 ,应用程序运行时 ,无论用户是否选中该菜单项 ,该菜单项都会有这 个选中标记 ,而其它没有选中 “Checked”复选框的菜单项都不会出现选中标记 。 此时就需要通过编写代码来实现我们的要求。 我们首先给刚才创建的菜单项都连接上处理函数,然后在 “BLACK_PEN” 菜单项的处理函数中输入如下代码: void CDrawTestView::OnStockBlackpen() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent();//获得父窗口指针 CMenu* pMenu = pParent->GetMenu();//获得窗口的系统菜单 //设置相应的菜单项的选中状态 pMenu->CheckMenuItem(ID_STOCK_BLACKPEN,MF_CHECKED); pMenu->CheckMenuItem(ID_STOCK_NULLPEN,MF_UNCHECKED) ; pMenu->CheckMenuItem(ID_STOCK_WHITEPEN,MF_UNCHECKE D); //设置当前选中的库存画笔为黑画笔 m_StockPen = BLACK_PEN; } 在代码中相应的语句上都加上了注释,现在来详细说明一下:视图类的 GetParent 函数用于获得应用程序的主框架窗口指针 pParent,它是一个 CWnd 窗口类的指针 。因为我们现在编辑的菜单是主框架窗口的菜单 ,所以必须首先获 得主框架窗口。然后通过窗口类的GetMenu 方法,获得窗口的系统菜单指针 pMenu,它是一个 CMenu 类指针 ,CMenu 类是 MFC 类库中的菜单类 ,它封装 了菜单的相应属性和成员函数 。最后调 用CMenu 类的成员函 数CheckMenuItem 来设置菜单项的选中状态。 � CheckMenuItem CheckMenuItem CheckMenuItem CheckMenuItem 函数声明如下: UINT CheckMenuItem(UINT nIDCheckItem,UINT nCheck); 其中参数nIDCheckItem 表示用ID(菜单项的ID)或者位置序号(0对应 当前菜单的第一个下级菜单 )选择菜单项 。如果用位置序号来选择菜单 ,则第一 个菜单项为 0,第二个为 1,依次类推。参数 nCheck 表示上述两种方式的一种 以及需要执行的动作,其取值如下表所示: 取值 说明 MF_BYCOMMA ND 通过 ID来选择菜单项(默认的选择方式) MF_BYPOSITIO N 通过位置序号来选择菜单项 MF_CHECKED 在菜单项前加上选中标识 MF_UNCHECKE D 去除菜单项前的选中标识 在编写代码中,我们三次调用了CheckMenuItem 函数,使ID 为 ID_STOCK_BLACKPEN 的“BLACK_PEN”菜单项之前加上选中标识,使ID 为ID_STOCK_NULLPEN 的菜单项“NULL_PEN ”和ID 为 ID_STOCK_WHITEPEN 的菜单项 “WHITE_PEN”之前没有选中标识 。最后把 应用程序中当前选中的库存画笔设置为用户要选择的黑画笔。 通过 CheckMenuItem 函数的介绍 ,我们知道可以通过菜单的位置序号来选 择菜单项,下面我们就举例说明如何通过位置序号来选择菜单项。在 “NULL_PEN”菜单项的处理函数中输入如下代码: void CDrawTestView::OnStockNullpen() { //TODO: Add your command handler code here //获得父窗口指针 CWnd* pParent = this->GetParent(); //获得窗口的系统菜单 CMenu* pMenu = pParent->GetMenu(); //获得 “库存绘图工具 ”菜单 CMenu* subMenu1 = pMenu->GetSubMenu(2); //获得 “库存画笔 ”菜单 CMenu* subMenu2 = subMenu1->GetSubMenu(0); //设置相应的菜单项的选中状态 subMenu2->CheckMenuItem(0,MF_BYPOSITION|MF_UNCHECKE D); subMenu2->CheckMenuItem(1,MF_BYPOSITION|MF_CHECKED); subMenu2->CheckMenuItem(2,MF_BYPOSITION|MF_UNCHECKE D); //设置当前选中的库存画笔为空画笔 m_StockPen = NULL_PEN; } 我们发现相比 “BLACK_PEN”菜单项处理函数中的代码多了两行代码 ,分 别用于获得 “库存绘图工具 ”菜单和 “库存画笔菜单 ”。这是因为 ID是唯一的 , 我们使用 ID在应用程序主框架窗口的系统菜单中就可以正确获得我们要选择的 菜单项 ,而位置序号只能是当前菜单的下级菜单的相对位置顺序 。所以为了选择 到作为三级菜单的 “BLACK_PEN”等菜单项,就必须先获得它们的上级菜单 。 我们调用CMenu 类的成员函数GetSubMenu 来获得菜单的下级菜单,该函数 传入的是下级菜单的位置序号 ,该序号从 0开始 。从图 1.51 中可以看到 ,“库存 绘图工具 ”菜单在系统菜单上排在第三位 ,所以此处参数传 入2。返回 的subMenu 指针就指向 “库存绘图工具 ”菜单 ,因为这也是一个菜单 ,所以 subMenu 指针 也是一个CMenu 类指针。同样的,再通过subMenu1指针调用GetSubMenu 函数 ,传入参数 0(因为 “库存画笔 ”菜单是 “库存绘图工具 ”菜单的下级菜单 中的第一个 ),返回了指向 “库存画笔 ”菜单的指针 subMenu2。通过 subMenu2 指针,我们可以通过位置序号设置“BLACK_PEN”等菜单项的选中状态了。 “BLACK_PEN”菜单项的位置序号为 0,“NULL_PEN”菜单项的位置序号 为 1,“WHITE_PEN”菜单项的位置序号为2。这里还要注意,CheckMenuItem 函数默认的选择菜单项方式是通过 ID选择,所以在参数中要指明选择方式为按 位置序号选择,所以参数中传入的是 “MF_BYPOSITION|MF_UNCHECKED” 或“MF_BYPOSITION|MF_CHECKED”。当参数要同时传入多个值时 ,值之间 用“|”连接。 因为对于一个菜单而言, ID是不会重复的,所以通常我们都会采用通过 ID 选择菜单项的方法 。一般只有在菜单项非常多的时候才会采用通过位置序号来选 择菜单项的方法,因为通过位置序号选择,我们可以使用循环语句来编写代码 , 而不需要每个菜单项都写代码。 现在我们在 “WHITE_PEN”菜单项以及 “库存画刷 ”下的菜单项的处理函 数中输入如下代码: //WHITE_PEN 菜单项处理函数 void CDrawTestView::OnStockWhitepen() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_BLACKPEN,MF_UNCHECKE D); pMenu->CheckMenuItem(ID_STOCK_NULLPEN,MF_UNCHECKED) ; pMenu->CheckMenuItem(ID_STOCK_WHITEPEN,MF_CHECKED); //设置当前选中的库存画笔为白画笔 m_StockPen = WHITE_PEN; } //WHITE_BRUSH 菜单项处理函数 void CDrawTestView::OnStockWhitebrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_CHECKE D); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); //设置当前选中的库存画刷为白画刷 m_StockBrush = WHITE_BRUSH; } //BLACK_BRUSH 菜单项处理函数 void CDrawTestView::OnStockBlackbrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_CHECKE D); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); //设置当前选中的库存画刷为黑画刷 m_StockBrush = BLACK_BRUSH; } //DKGRAY_BRUSH 菜单项处理函数 void CDrawTestView::OnStockDkgraybrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_CHECK ED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); ////设置当前选中的库存画刷为深灰画刷 m_StockBrush = DKGRAY_BRUSH; } //GRAY_BRUSH 菜单项处理函数 void CDrawTestView::OnStockGraybrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_CHECKED) ; pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); //设置当前选中的库存画刷为灰画刷 m_StockBrush = GRAY_BRUSH; } //LTGRAY_BRUSH 菜单项处理函数 void CDrawTestView::OnStockLtgraybrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_CHECK ED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); //设置当前选中的库存画刷为浅灰画刷 m_StockBrush = LTGRAY_BRUSH; } //HOLLOW_BRUSH 菜单项处理函数 void CDrawTestView::OnStockHollowbrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_CHEC KED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_UNCHECK ED); //设置当前选中的库存画刷为空画刷 m_StockBrush = HOLLOW_BRUSH; } //NULL_BRUSH 菜单项处理函数 void CDrawTestView::OnStockNullbrush() { //TODO: Add your command handler code here CWnd* pParent = this->GetParent(); CMenu* pMenu = pParent->GetMenu(); pMenu->CheckMenuItem(ID_STOCK_WHITEBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_BLACKBRUSH,MF_UNCHEC KED); pMenu->CheckMenuItem(ID_STOCK_DKGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_GRAYBRUSH,MF_UNCHECK ED); pMenu->CheckMenuItem(ID_STOCK_LTGRAYBRUSH,MF_UNCHE CKED); pMenu->CheckMenuItem(ID_STOCK_HOLLOWBRUSH,MF_UNCH ECKED); pMenu->CheckMenuItem(ID_STOCK_NULLBRUSH,MF_CHECKED) ; //设置当前选中的库存画刷为空画刷 m_StockBrush = NULL_BRUSH; } 下面我们来编写两个绘图菜单项的处理函数 ,首先在 “使用库存绘图工具画 矩形 ”菜单项处理函数中输入如下代码: void CDrawTestView::OnStockDrawrectangle() { //TODO: Add your command handler code here CClientDC dc(this);//构造设备环境对象 dc.SelectStockObject(m_StockPen);//选择库存画笔 dc.SelectStockObject(m_StockBrush);//选择库存画刷 dc.Rectangle(200,200,400,400);//绘制矩形 } 代码中首先调用 SelectStockObject 函数来选择用户选中的库存绘图工具 , 然后调用 Rectangle 函数绘制矩形 。库存画笔影响矩形的边框 ,而库存画刷影响 矩形的内部填充 。为了演示一下 SelectStockObject 函数的返回值 ,我们在 “使 用库存绘图工具画椭圆 ”菜单项处理函数中输入如下代码: void CDrawTestView::OnStockDrawellipse() { //TODO: Add your command handler code here CClientDC dc(this); CGdiObject* p; CGdiObject* b; //选择库存画笔并返回原画笔 p = dc.SelectStockObject(m_StockPen); //选择库存画刷并返回原画刷 b = dc.SelectStockObject(m_StockBrush); //使用库存绘图工具画椭圆 dc.Ellipse(100,100,200,200); dc.SelectObject(p);//选择原画笔 dc.SelectObject(b);//选择原画刷 //使用原有的绘图工具画椭圆 dc.Ellipse(200,200,300,300); } 运行应用程序,首先选择库存画刷为 “LTGRAY_BRUSH”,然后选择 “使 用库存绘图工具画矩形 ”菜单项绘制一个矩形 ,再选择 “使用库存绘图工具画椭 圆”菜单项绘制椭圆,结果如图 1.53 所示。 我们使用了库存的黑画笔和浅灰色画刷绘制了窗口视图区中的矩形 ,该矩形 的边框为宽度 1像素的黑色实线 ,而内部是浅灰色的实心填充 。绘制的第一个椭 圆是同样的情况 ,因为在绘制椭圆的处理函数中 ,设备环境对象在选择库存画笔 和画刷之前并没有选择其它的画笔和画刷 ,那么画笔和画刷就是设备环境对象的 默认画笔黑画笔和默认画刷白画刷,因此绘制的第二个椭圆的内部填充是白色 。 同时因为画椭圆是后执行的 ,所以第二个椭圆覆盖了所画矩形的一部分 。如果此 时再执行一次画矩形,则矩形将完全覆盖第二个椭圆。 需要注意的是绘图工具和设备环境的当前绘图位置一样 ,也是针对每个设备 环境对象的 。不同设备环境对象选择的画笔和画刷相互并不影响 。所以此应用程 序不论选择什么样的库存画笔和画刷 ,在执行 “使用库存绘图工具画椭圆 ”菜单 项时 ,绘制的第一个椭圆受选择的画笔和画刷影响 ,而绘制的第二个椭圆一直都 是使用设备环境对象的默认画笔和画刷进行绘制 。这是因为在 “使用库存绘图工 具画椭圆 ”菜单项处理函数中 ,每次都获得一个与前不同的设备环境对象 ,而每 个设备环境对象的初始默认画笔和画刷都是黑画笔和白画刷。 显然 ,仅使用库存的绘图工具并不能够满足我们的需要 ,此时就需要使用自 定义绘图工具。 1.6.21.6.21.6.21.6.2 使用自定义绘图工具 使用自定义绘图工具的基本步骤是: (1)生成画笔 CPen 类和画刷 CBrush 类的对象实例; (2) 初始化画笔和画刷; (3) 将初始化后的画笔和画刷选入设备环境对象 (4) 调用绘图函数进行绘图。 生成的画笔或画刷对象实例必须初始化后才能够使用 ,我们可以通过类的构 造函数直接初始化画笔或画刷 ,也可以声明一个未初始化的画笔或画刷 ,然后调 用初始化函数进行初始化。下面分别介绍一下如何使用自定义的画笔和画刷。 1.1.1.41.1.1.41.1.1.41.1.1.4 自定义 CPen CPen CPen CPen 画笔类 CPen 类是一个 Windows 的GDI 对象类,该类重载了三个构造函数,其函 数声明如下: CPen(); CPen(int nPenStyle,int nWidth,COLORREF crColor); CPen(int nPenStyle,int nWidth, const LOGBRUSH* pLogBrush, int nStyleCount = 0, const DWORD* lpStyle = NULL); throw(CResourceException); 第一个构造函数没有任何参数,它构造了一个没有初始化的 CPen 对象 ,必 须调用 CPen 类的初始化函数对其进行初始化才能够使用 。CPen 类的初始化函 数将在后面进行介绍。 第二个构造函数有三个参数 ,分别对画笔的线型 ,线宽以及颜色进行了初始 化。下面分别介绍这三个参数。 参数nPenStyle nPenStyle nPenStyle nPenStyle 指定了画笔画线的线型,即画笔的风格样式,该参数的可 取值如下表所示: 参数值 说明 线型 PS_SOLID 创建一个实线画笔 PS_DASH 创建一个虚线画笔 ,该值只有在线宽为 1 时才有效 PS_DOT 创建一个点线画笔 ,该值只有在线宽为 1 时才有效 PS_DASHDOT 创建一个点划线画笔,该值只有在线宽 为1时才有效 PS_DASHDOTD OT 创建一个点点划线画笔,该值只有在线 宽为 1时才有效 PS_NULL 创建一个空线画笔 PS_INSIDEFRA ME 创建一个内框线画笔 当线宽大 于1 的时候 ,即使nPenStyle 参数设置 为PS_DASH、或PS_DOT, 或PS_DASHDOT,或PS_DASHDOTDOT,画笔仍然只能以实线进行绘制。 PS_INSIDEFRAME 画笔与PS_SOLID 画笔的区别在于:当绘图函数绘制的是 有矩形包围盒的封闭区域图形 (如Ellipse,Rectangle,RoundRect,Pie,Chord) 时,如果画笔的线宽大于 1(其它线型当线宽大于 1的时候等同于 PS_SOLID), 则在绘制封闭区域的边界框的时候,PS_SOLID 画笔将居中对齐边界框,所以 实际绘制出的边界框线有一部分处于边界框外,而PS_INSIDEFRAME 画笔把 整个边界框线都画在边界框内。 参数 nWidth nWidth nWidth nWidth 指定了画笔所绘制线的线宽 ,如果该值为 0,则无论是什么映 射模式,线宽总是 1个像素。 参数 crColor crColor crColor crColor 指定了画笔画线的颜色。 第三个构造函数有五个参数,下面分别加以介绍。 参数nPenStyle nPenStyle nPenStyle nPenStyle 指定了画笔画线的线型,该参数除了可以具有上一个构造 函数中所介绍的参数值之外,还可以赋予下表中所列的参数值: 参数值 说明 PS_GEOMETRIC 创建一个几何画笔 PS_COSMETIC 创建一个装饰画笔 PS_ALTERNATE 创建一个设置其它像素的画笔(只对装饰画笔可用) PS_USERSTYLE 创建一个使用由用户提供风格矩阵的画笔 PS_ENDCAP_ROU ND 端点为圆形的 PS_ENDCAP_SQU ARE 端点为方形的 PS_ENDCAP_FLA T 端点为平坦的 PS_JOIN_BEVEL 成尖角连接 PS_JOIN_MITER 在SetMiterLimit 函数设置的当前极限范围内斜接 ;否则 , 成尖角连接 PS_JOIN_ROUND 成圆角连接 参数 nWidth nWidth nWidth nWidth 的含义与上一个构造函数中同一参数相同 ,区别在于如果参 数 nPenStyle 的值为PS_COSMETIC,那么该参数值是针对逻辑单位而言的,且 该参数值只能为 1。 参数 pLogBrush pLogBrush pLogBrush pLogBrush 为指向 LOGBRUSH 结构的指针,该结构的声明如下: typedef struct tagLOGBRUSH {/* lb */ UINT lbStyle; COLORREF lbColor; LONG lbHatch; }LOGBRUSH; 该结构定义了一个画刷的风格,颜色和阴影线类型。成员 lbStyle 的取值如 下表所列: 参数值 说明 BS_DIBPATTER N 设 备无 关位 图 (DIB)定 义的 图形 画刷 ,如 果参 数值 为该 值, 则成员 lbHatch 包含了一个被压缩的 DIB 句柄 BS_DIBPATTER NPT 设 备无 关位 图 (DIB)定 义的 图形 画刷 ,如 果参 数值 为该 值, 则成员 lbHatch 包含了一个被压缩的 DIB 句柄 BS_HATCHED 阴影线画刷 BS_HOLLOW 空画刷 BS_NULL 空画刷 BS_PATTERN 由内存位图定义的图形画刷 BS_SOLID 实心画刷 成员 lbColor 指定了画刷的颜色。 成员 lbHatch 指定了阴影线画刷的阴影线样式 ,其取值将在介绍自定义画刷 的时候进行介绍。 如果参数 nPenStyle 的值为 PS_COSMETIC,那么 LOGBRUSH 结构的成 员lbColor 指定画笔的颜色,而成员lbStyle 必须设为BS_SOLID;如果参数 nPenStyle 的值为PS_GEOMETRIC,那么LOGBRUSH 结构的所有成员用于 指定画笔的刷子属性,是画笔也可以使用画刷的填充模式来画线。 参数nStyleCount nStyleCount nStyleCount nStyleCount 指定了一个以两个字为一个单元的矩阵的长度。如果 nPenStyle 的值不是 PS_USERSTYLE,则该参数值必须为 0。 参数 lpStyle lpStyle lpStyle lpStyle 指向一个以两个字为一个单元的矩阵 。这个两个字单元的第一 个值指定了第一个直线段的长度,第二个值指定了第一个空隙的长度。如果 nPenStyle 的值不是 PS_USERSTYLE,则该参数值必须为 NULL。 使用第二个和第三个构造函数构造的 CPen 对象不需要再进行初始化了 。如 果这两个构造函数在调用过程中构造失败 ,则会产生一个异常 。其中第二个构造 函数的构造方式比较简单 ,在实际应用中使用的频率最高 。第三个构造函数使用 起来比较复杂 ,不过相对的 ,它的功能也更为强大 。稍后我们将会进一步介绍如 何使用第三个构造函数来创建画笔并举例 ,现在我们接着介绍如何使用自定义画 笔。 如果我们使用第一个构造函数来创建画笔对象,那么就需要对它进行初始 化。我们可以使用 CPen 类的 CreatePen 函数来初始化画笔对象。 � CreatePen CreatePen CreatePen CreatePen 函数声明如下: BOOL CreatePen(int nPenStyle,int nWidth,COLORREF crColor); BOOL CreatePen(int nPenStyle,int nWidth, const LOGBRUSH* pLogBrush, int nStyleCount = 0, const DWORD* lpStyle = NULL); 这两个初始化函数分别对应了前面介绍的第二个和第三个构造函数 ,其参数 意义与之完全相同,这里就不再重复了。 也可以使用 CreatePenIndirect 函数来初始化画笔对象 � CreatePenIndirect CreatePenIndirect CreatePenIndirect CreatePenIndirect 函数声明如下: BOOL CreatePenIndirect(LPLOGPEN lpLogPen); 其中参数 lpLogPen 是指向 LOGPEN 结构的指针,其结构声明如下: typedef struct tagLOGPEN{/* lgpn */ UINT lopnStyle; POINT lopnWidth; COLORREF lopnColor; }LOGPEN; 该结构的三个成员变量分别指定了画笔的线型,宽度和颜色。其中成员 lopnWidth 虽然用于指定画笔的线宽,但类型却是 POINT 结构,其中用 POINT 结构的 x成员来表示线宽,而 y成员不起任何作用。 画笔对象初始化后就可以被设备环境对象选中了,调用设备环境对象的 SelectObject 函数来选择自定义的画笔。 � SelectObject SelectObject SelectObject SelectObject 函数声明如下: CPen* SelectObject(CPen* pPen); 该函数传入指向初始化完毕的画笔的指针,并将原来使用的画笔的指针返 回。我们可以保存返回的指针 ,以便在使用完创建的自定义画笔后 ,可以将原有 的画笔选择回来。 选择完自定义画笔之后 ,就可以调用绘图函数来进行绘图了 ,此时设备环境 对象将使用用户自定义的画笔进行绘图 。画笔将对 LineTo,Polyline,PolylineTo, Arc,ArcTo,AngleArc,PolyBezier,PolyBezierTo 等绘图函数绘制的图形产 生影响 ,同时也将对 Rectangle,RoundRect,Ellipse,Pie,Chord,Polygon, PolyPolygon 等绘图函数所绘制的封闭区域图形的边界产生影响 。具体举例我们 将在介绍完自定义画刷之后来统一完成。 1.1.1.51.1.1.51.1.1.51.1.1.5 使用第三个构造函数来创建自定义画笔 首先来看参数nPenStyle 的取值,该参数在第二个构造函数的nPenStyle 参数的 7个取值基础上新增加了 10 个参数值 ,共17 个。这17 个参数值其实可 以大致分成四组 :第一组是 PS_GEOMETRIC 和PS_COSMETIC,它们决定了 画笔的类型(几何画笔或者装饰画笔);第二组是PS_ALTERNATE 和 PS_USERSTYLE,再加上第二个构造函数 中nPenStyle可以设置 的7 个参数值 , 它们决定了画线的样式;第三组是PS_ENDCAP_ROUND , PS_ENDCAP_SQUARE 和PS_ENDCAP_FLAT,它们决定了直线段的端点样 式;第四组是 PS_JOIN_BEVEL,PS_JOIN_MITER 和PS_JOIN_ROUND,它 们决定了折线连接处的样式 。参数 nPenStyle 可以是多个参数值的组合 ,不同组 的参数值可以组合使用,参数值之间用 “|”符号连接。比如 nPenStyle 可以设 置为如下值: PS_GEOMETRICPS_GEOMETRICPS_GEOMETRICPS_GEOMETRIC||||PS_SOLIDPS_SOLIDPS_SOLIDPS_SOLID||||PS_ENDCAP_ROUNDPS_ENDCAP_ROUNDPS_ENDCAP_ROUNDPS_ENDCAP_ROUND||||PS_JOIN_BEVEPS_JOIN_BEVEPS_JOIN_BEVEPS_JOIN_BEVE LLLL 该值表示创建的画笔是几何画笔 ,画线的类型是实心线 ,直线段的端点是圆 形,折线的连接处样式为尖角连接 。同一组中的参数值因为决定的画笔的属性相 同,所以组合在一起没有意义 。如果同一组的参数值组合在一起 ,画笔会按照同 一组中参数值的优先级来决定究竟采用哪一个参数值。比如,如果nPenStyle 设置如下的取值: PS_GEOMETRICPS_GEOMETRICPS_GEOMETRICPS_GEOMETRIC||||PS_COSMETICPS_COSMETICPS_COSMETICPS_COSMETIC 则实际创建的画笔是几何画笔 。对于画笔类来说 ,每一组参数值都有一个取 值是默认值 ,当设置 nPenStyle 参数时没有设置该组的任何参数值时 ,画笔会采 用这个默认值 。第一组的默认值是 PS_COSMETIC(装饰画笔 ),第二组的默认 值是PS_SOLID(实心线),第三组的默认值是PS_ENDCAP_ROUND(端点 为圆形 ),第四组的默认值是 PS_JOIN_ROUND(圆角连接 )。当nPenStyle 设 置为上面的取值时,相当于设置为如下取值: PS_GEOMETRICPS_GEOMETRICPS_GEOMETRICPS_GEOMETRIC||||PS_SOLIDPS_SOLIDPS_SOLIDPS_SOLID||||PS_ENDCAP_ROUNDPS_ENDCAP_ROUNDPS_ENDCAP_ROUNDPS_ENDCAP_ROUND||||PS_JOIN_PS_JOIN_PS_JOIN_PS_JOIN_ROUNROUNROUNROUN DDDD 第三个参数pLogBrush 所指向的LOGBRUSH 结构的三个成员变量中, lbColor指定了画笔的颜色 。当画笔类型 为PS_COSMETIC(装饰画笔 )时,lbStyle 必须为BS_SOLID,此时lbHatch 无意义,可以不用设置。而当画笔类型为 PS_GEOMETRIC(几何画笔)时,lbStyle 可以设置为画刷的风格取值,此时 如果 lbStyle 的取值为 BS_HATCHED(阴影线画刷 ),则lbHatch 可以设置为各 种阴影线样式 。所以当画笔类型为几何画笔的时候 ,我们可以让绘制的线型具有 画刷的填充风格。下面我们来看一个例子,修改应用程序的OnDraw 函数,输 入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造第一个 LOGBRUSH 结构 LOGBRUSH lb1; //设置画线颜色为红色 lb1.lbColor = RGB(255,0,0); //因为第一个画笔要创建装饰画笔,所以此处取值必须为 BS_SOLID //并且不用设置 lbHatch 的取值 lb1.lbStyle = BS_SOLID; //创建装饰画笔,绘制线型为点线,因为是装饰画笔,所以线宽为 1 CPen pen1(PS_COSMETIC|PS_DOT,1,&lb1,0,NULL); //选择创建的画笔 pDC->SelectObject(&pen1); //绘制一条直线段 pDC->MoveTo(100,100); pDC->LineTo(500,100); //构造第二个 LOGBRUSH 结构 LOGBRUSH lb2; //设置画线颜色为蓝色 lb2.lbColor = RGB(0,0,255); //因为第二个画笔要创建几何画笔 ,所以可以设置 lbStyle 为阴影线画刷 lb2.lbStyle = BS_HATCHED; //设置阴影线的样式为水平线和垂直线的十字交叉线 lb2.lbHatch = HS_CROSS; //创建几何画笔,设置线宽为 20,线型为虚线 CPen pen2(PS_GEOMETRIC|PS_DASH,20,&lb2,0,NULL); //选择创建的画笔 pDC->SelectObject(&pen2); //绘制一条直线段 pDC->MoveTo(100,200); pDC->LineTo(500,200); //构造第三个 LOGBRUSH 结构 LOGBRUSH lb3; //设置画线颜色为绿色 lb3.lbColor = RGB(0,255,0); //第三个画笔的设置与第二个画笔相同 lb3.lbStyle = BS_HATCHED; lb3.lbHatch = HS_CROSS; //创建几何画笔,设置线宽为 20,线型为虚线 //设置直线段的端点为方形 CPen pen3(PS_GEOMETRIC|PS_DASH|PS_ENDCAP_SQUARE, 20,&lb3,0,NULL); //选择创建的画笔 pDC->SelectObject(&pen3); //绘制一条直线段 pDC->MoveTo(100,300); pDC->LineTo(500,300); pen1.DeleteObject(); pen2.DeleteObject(); pen3.DeleteObject(); } 此段代码利用画笔类的第三个构造函数创建了三个画笔 ,pen1是装饰画笔 , pen2和pen3是几何画笔 ,pen2和pen3的差别在于 pen2的直线段的端点是 默认的PS_ENDCAP_ROUND (圆形),而pen3的直线段端点设置为 PS_ENDCAP_SQUARE(方形 )。运行程序结果如图 1.54 所示 。从图中我们可 以看到 pen2和pen3所绘制的直线段使用了阴影线画刷的填充风格 ,同时可以 看到它们在端点处的区别。因为画笔类也是GDI 对象类,要占用系统资源,所 以在最后需要调用 DeleteObject 函数将其删除。 在nPenStyle 的第二组参数值中,我们可以设置画线类型为 PS_USERSTYLE(用户定义类型),该参数值允许用户自定义线型。当然这种 自定义的线型是一种简单的线型 ,这种线型可以归类为虚线 ,用户只是可以自由 定义虚线中直线段和空隙的长度 。如果设置画线类型为 PS_USERSTYLE,在创 建画笔对象的时候就需要用到第四个和第五个参数。第五个参数 lpStyle 指向一 个矩阵,该矩阵要求以双字为单元,实际上就是要求矩阵中的值的个数是2的 倍数 。每两个值为一组 ,第一组中的第一个值指定了虚线的第一段直线段的长度 , 第二个值指定了接下来的空隙的长度 ,然后下一个直线段的长度由第二组的第一 个值指定 ,下一个空隙的长度由第二组的第二个值指定 ,如此类推 ,如果所有的 组都使用完了 ,而要绘制的虚线仍然没有绘制完 ,则从第一组重新开始 ,如此循 环,直到绘制完所要绘制的虚线为止 。第四个参数指定了该矩阵的长度 。我们看 下面的例子,修改 OnDraw 函数,输入如下的代码。 void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造用户自定义线型风格矩阵 DWORD dwdStyles[] = {20,20, 10, 10}; //构造 LOGBRUSH 结构 LOGBRUSH lb; lb.lbColor = RGB(0,0,0); lb.lbStyle = BS_SOLID; //创建几何画笔,使用用户自定义线型风格 CPen pen(PS_GEOMETRIC|PS_USERSTYLE,3,&lb,4,dwdStyles); //选择画笔 pDC->SelectObject(&pen); //绘制一条折线 POINT p[4]; p[0].x = 10;p[0].y = 10; p[1].x = 300;p[1].y = 250; p[2].x = 400;p[2].y = 100; p[3].x = 100;p[3].y = 100; pDC->Polyline(p,4); //删除画笔 pen.DeleteObject(); } 运行程序,结果如图 1.55 所示。 从运行结果可以看出,使用自定义的画笔,绘制出了用户定义格式的虚线 。 相对于第二个构造函数所创建的画笔 ,第三个构造函数所创建的画笔不仅可以绘 制用户自定义格式的线型,而且可以绘制出任意线宽的各种线型。 画笔类的第三个构造函数的最重要的功能就是允许用户使用画刷填充的方 式来绘制线形 ,丰富了绘制线型的样式 。实际上画刷的主要功能是定义如何在封 闭区域内部进行填充的样式 ,下面就来看一下如何使用自定义画刷来填充封闭区 域的内部。 1.1.1.61.1.1.61.1.1.61.1.1.6 自定义 CBrush CBrush CBrush CBrush 画刷类 同CPen 类一样,CBrush 类也是Windows 的GDI 对象类,该类重载了四 个构造函数,其函数声明如下: CBrush(); CBrush(COLORREF crColor); throw(CResourceException); CBrush(int nIndex,COLORREF crColor); throw(CResourceException); CBrush(CBitmpa* pBitmap); throw(CResourceException); 第一个构造函数构造了一个没有初始化的画刷 ,用该构造函数创建的画刷必 须调用画刷类的初始化函数进行初始化后,才能够使用。 第二个构造函数用于构造一个实心填充画刷 ,传入的参数 crColor 指定了填 充的颜色 ,使用该画刷绘制封闭区域图形 ,将对封闭区域内部用 crColor 指定的 颜色进行完全填充。 第三个构造函数用于构造一个阴影线画刷 ,其中参数 crColor 指定了填充的 前景色,即阴影线的颜色;参数 nIndex 指定了阴影线的样式,其取值如下表所 示: 参数值 说明 HS_BDIAGONAL 从左到右向下成 45 度的对角线 HS_CROSS 水平线和垂直线相交的十字交叉线 HS_DIAGCROS S 夹角为 45 度的斜十字交叉线 HS_FDIAGONAL 从左到右向上成 45 度的对角线 HS_HORIZONT AL 水平线 HS_VERTICAL 垂直线 第四个构造函数用于构造一个使用位图填充的画刷,参数pBitmap 指向一 个CBitmap 对象,该对象指定了一幅位图。 这四个构造函数除了第一个之外 ,后三个在构造画刷对象的同时都将其初始 化了。如果是使用第一个构造函数构造的画刷对象,那么还需要进行初始化, CBrush 类提供了以下初始化函数: � CreateSolidBrush CreateSolidBrush CreateSolidBrush CreateSolidBrush 函数,用于初始化一个实心填充画刷,其函数声明 如下: BOOL CreateSolidBrush(COLORREF crColor); 参数 crColor 指定了填充的颜色 。使用该函数进行初始化的画刷等同于使用 第二个构造函数所创建的画刷。画刷初始化成功,函数返回TRUE,否则返回 FALSE。 � CreateHatchBrush CreateHatchBrush CreateHatchBrush CreateHatchBrush 函 数, 用于 初始 化一 个阴 影线 画刷 ,其 函数 声明 如 下: BOOL CreateHatchBrush(int nIndex,COLORREF crColor); 其参数含义与第三个构造函数完全相同 。使用该函数进行初始化的画刷等同 于使用第三个构造函数所创建的画刷 。画刷初始化成功 ,函数返回 TRUE,否则 返回 FALSE。 � CreatePatternBrush CreatePatternBrush CreatePatternBrush CreatePatternBrush 函数,用于初始化一个图形画刷,使用该画刷填 充封闭区域内部时 ,将使用指定的位图进行一个接一个的填充 ,其函数声明如下 : BOOL CreatePatternBrush(CBitmap* pBitmap); 使用该函数进行初始化的画刷等同于使用第四个构造函数所创建的画刷 。画 刷 初始 化成 功, 函数 返回 TRUE, 否则 返回 FALSE。 如何 使用 此画 刷进 行填 充 将在下一章进行介绍。 � CreateBrushIndirect CreateBrushIndirect CreateBrushIndirect CreateBrushIndirect 函数,用于初始化一个由LOGBRUSH 结构决定 风格样式的画刷,其函数声明如下: BOOL CreateBrushIndirect(const LOGBRUSH* plLogBrush); 其中参数 plLogBrush 是指向 LOGBRUSH 结构的指针 。在LOGBRUSH 结 构中包含了画刷的相关信息 ,这在介绍使用画笔类的第三个构造函数构造画笔时 已经做了介绍 ,这里就不再重复了 。画刷初始化成功 ,函数返回 TRUE,否则返 回FALSE。 画刷被初始化后,就可以被设备环境对象选用了,调用设备环境对象的 SelectObject 函数来选择自定义的画笔。 � SelectObject SelectObject SelectObject SelectObject 函数声明如下: CBrush* SelectObject(CBrush* pBrush); 该函数和选中画笔的函数相同 ,是函数的另一种重载 。此函数还有其它的重 载形式 ,我们将在使用到的时候进行介绍 。该函数传入指向初始化完毕的画刷的 指针 ,并将原来使用的画刷指针返回 。我们可以保存返回的指针 ,以便在使用完 创建的自定义画刷后,可以将原有的画刷选择回来。 选择完自定义画刷之后 ,就可以调用绘图函数进行绘图了 ,画刷所影响的绘 图函数是绘制封闭区域图形的绘图函数 ,包括 Rectangle,RoundRect,Ellipse, Pie,Chord,Polygon,PolyPolygon 等绘图函数。在CDC 类中还提供了一些 画填充图形的函数 ,这类函数只填充封闭区域而不绘制区域边界 。下面简单介绍 一下这类绘图函数。 � FillRect FillRect FillRect FillRect 函数,用于使用指定的画刷填充一个矩形区域,其函数声明如 下: void FillRect(LPCRECT lpRect, CBrush* pBrush); 参数 lpRect 指向一个 RECT 结构或者 CRect 类对象 ,它指定了要进行填充 的矩形区域的位置 ;参数 pBrush 指向一个初始化了的画刷对象 ,函数使用该画 刷进行填充 。此函数只对给定的矩形区域进行填充 ,并不绘制区域的边界 ,填充 的范围为整个矩形的内部及矩形的左边界和上边界 ,不包括矩形的右边界和下边 界。修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造一个十字交叉线的阴影线画刷,前景色为红色 CBrush brush(HS_CROSS,RGB(255,0,0)); //构造一个宽度为 1,颜色为蓝色的实线画笔 CPen pen(PS_SOLID,1,RGB(0,0,255)); //选择画笔和画刷 pDC->SelectObject(&pen); pDC->SelectObject(&brush); //定义矩形区域 CRect r; r.top =100;r.left = 100; r.right =200;r.bottom =200; //用指定的画笔绘制矩形区域的边界 pDC->MoveTo(100,100);pDC->LineTo(200,100); pDC->LineTo(200,200);pDC->LineTo(100,200); pDC->LineTo(100,100); pDC->FillRect(r,&brush);//调用 FillRect 函数填充矩形区域 //删除画笔和画刷对象 pen.DeleteObject(); brush.DeleteObject(); } 运行应用程序,结果如图1.56 所示。从运行结果可以看出,因为FillRect 函数填充了矩形区域的左边界和上边界 ,所以后执行的 FillRect 函数所绘制的填 充矩形区域覆盖了用蓝色画笔绘制的矩形区域的左边界和上边界 ,而FillRect 函 数没有填充矩形区域的右边界和下边界 ,所以蓝色画笔绘制的右边界和下边界仍 然可以看到。 � FillSolidRect FillSolidRect FillSolidRect FillSolidRect 函数 ,用于使用指定的颜色填充矩形区域 ,其函数声明如 下: void FillSolidRect(LPCRECT lpRect,COLORREF clr); void FillSolidRect(int x , int y, int cx, int cy,COLORREF clr); 其中第一个函数的参数 lpRect 指定了要进行填充的矩形区域 ;参数 clr 指定 了要填充的颜色 。第二个函数的参数 x和y指定了要填充的矩形区域的左上角点 坐标 ;参数 cx 和cy 指定了要填充的矩形区域的宽度和高度 。这里要特别注意的 是,此函数的第二组参数并不是通常绘图函数中定义矩形区域时所采用的矩形区 域的右下角点坐标,输入参数值时不要弄错了。参数 clr 指定了要填充的颜色。 FillSolidRect 函数与 FillRect 函数相同 ,填充的时候不填充矩形区域的右边界和 下边界。 � FillRgn FillRgn FillRgn FillRgn 函数,用于填充一个由CRgn 类对象定义的封闭区域,其函数 声明如下: BOOL FillRgn(CRgn* pRgn, CBrush* pBrush); 其中参数 pRgn 是指向定义了要填充的封闭区域的 CRgn 类对象的指针 ;参 数pBrush 是指向用于填充的画刷。 � PaintRgn PaintRgn PaintRgn PaintRgn 函数,用于填充一个CRgn 类对象定义的封闭区域,其函数 声明如下: BOOL PaintRgn(CRgn* pRgn); 其中参数 pRgn 是指向定义了要填充的封闭区域的 CRgn 类对象的指针 。此 函数与上一个函数的差别在于此函数的参数中没有指定画刷 ,所以填充时将使用 设备环境对象的当前画刷。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造一个十字交叉线的阴影线画刷,前景色为红色 CBrush brush1(HS_CROSS,RGB(255,0,0)); //构造一个蓝色的实心画刷 CBrush brush2(RGB(0,0,255)); pDC->SelectObject(&brush2); //选择第二个画刷 CRgn rgn;//构造 CRgn 类对象 //将CRgn 类对象初始化为一个椭圆形区域 rgn.CreateEllipticRgn(100,100,300,200); //调用 FillRgn 函数填充 CRgn 对象指定的区域 pDC->FillRgn(&rgn,&brush1); //调用 CRgn 类的 OffsetRgn 函数平移区域 rgn.OffsetRgn(100,100); pDC->PaintRgn(&rgn);//调用 PaintRgn 函数填充 //删除画刷 brush1.DeleteObject(); brush2.DeleteObject(); } 运行应用程序,结果如图 1.57 所示。 在这段代码中 ,创建了两个画刷 ,设备环境对象选用了第二个画刷 。在调 用 FillRgn 函数进行填充时,因为函数参数指定要使用第一个画刷进行填充,所以 尽管设备环境对象已经选用了第二个画刷,封闭区域仍使用指定的画刷进行填 充。而调用 PaintRgn 函数时 ,因为参数中没有指定画刷 ,所以就使用了设备环 境对象所选用的画刷进行填充。 下面我们将添加菜单和相应的代码到应用程序中 ,来演示一下在调用前面介 绍的 LineTo 等绘图函数进行绘图时如何使用自定义画笔和画刷,以及相应的结 果。 1.1.1.71.1.1.71.1.1.71.1.1.7 使用自定义画笔和画刷绘图 在当前应用程序中添加如下菜单: 一级菜单 二级菜单 三级菜单 四级菜单 IDIDIDID 自定义绘 图工具 画笔 线型 PS_SOLIDPS_SOLIDPS_SOLIDPS_SOLIDID_PEN_SOLID PS_DASHID_PEN_DASH PS_DOTID_PEN_DOT PS_DASHDOTID_PEN_DASHDOT PS_DASHDOTD OT ID_PEN_DASHDOTD OT PS_NULLID_PEN_NULL PS_INSIDEFRA ME ID_PEN_INSIDEFRA ME 线宽 1111 ID_PEN_1 3 ID_PEN_3 5 ID_PEN_5 7 ID_PEN_7 颜色 黑色 ID_PEN_BLACK 红色 ID_PEN_RED 绿色 ID_PEN_GREEN 蓝色 ID_PEN_BLUE 画刷 阴影线类 型 NoneNoneNoneNone ID_BRUSH_NONE HS_BDIAGONALID_BRUSH_BDIAGO NAL HS_CROSSID_BRUSH_CROSS HS_DIAGCROSSID_BRUSH_DIAGCR OSS HS_FDIAGONALID_BRUSH_FDIAGO NAL HS_HORIZONTA L ID_BRUSH_HORIZO NTAL HS_VERTICALID_BRUSH_VERTIC AL 前景色 白色 ID_BRUSH_WHITE 红色 ID_BRUSH_RED 绿色 ID_BRUSH_GREEN 蓝色 ID_BRUSH_BLUE 添加完的菜单如图 1.58 所示。将该菜单放在 “库存绘图工具 ”菜单右侧。 设置五组四级菜单中每组的第一个菜单项前有选中标识 (上表中粗体字的五个四 级菜单 )。其中线宽选择了 1,3,5,7四种宽度。阴影线类型中的 “None”菜 单项表示不使用阴影线画刷,而是使用实心画刷。颜色只选用了黑色,白色 ,红 色,绿色 ,蓝色等几种颜色 ,其中黑色和白色分别是设备环境对象默认画笔和画 刷的颜色。为这些菜单项连接处理函数,处理函数放在 CDrawTestView 类中 。 为了记录用户选择的自定义画笔和画刷的相应参数,需要在 CDrawTestView 类中添加如下变量: COLORREF m_BrushColor;//自定义画刷前景色 int m_BrushStyle;//自定义画刷类型 COLORREF m_PenColor;//自定义画笔颜色 int m_PenWidth;//自定义画笔宽度 int m_PenStyle;//自定义画笔类型 同时在 CDrawTestView 类的构造函数中添加如下代码 ,对上述变量进行初 始化: m_PenStyle = PS_SOLID;//初始画笔线型为实线 m_PenWidth = 1;//初始画笔宽度为 1 m_PenColor = RGB(0,0,0);//初始画笔颜色为黑色 m_BrushStyle = -1;//初始画刷类型为 -1,代表使用实心画刷 m_BrushColor = RGB(255,255,255);//初始画刷颜色为白色 现在我们可以为每个菜单项的处理函数编写代码 。在每个菜单项的处理函数 中要为对应的变量设置值 ,同时还要更改菜单项的选中状态 ,因为此时的菜单项 比较多 ,如果采用通过 ID来设置菜单项选中状态的方法 ,无疑要写很多的代码 。 这里我们编写一个专门的函数来设置菜单项的选中状态 ,在函数中通过菜单项的 位置序号来设置菜单项的选中状态。我们在CDrawTestView 类中添加函数 SetMenuItemCheck,其函数代码如下: void CDrawTestView::SetMenuItemCheck(int level2, int level3, int count, int sel) { //获得主框架窗口指针 CWnd* pParent = this->GetParent(); //获得主框架窗口系统菜单 CMenu* pMenu = pParent->GetMenu(); //获得 “自定义绘图工具 ”菜单指针,该菜单在系统菜单中的位置序号 为3 CMenu* subMenu1= pMenu->GetSubMenu(3); //获得指定位置序号的二级菜单指针 CMenu* subMenu2 = subMenu1->GetSubMenu(level2); //获得指定位置序号的三级菜单指针 CMenu* subMenu3 = subMenu2->GetSubMenu(level3); //循环当前三级菜单下的所有四级菜单项 for (int i=0;iCheckMenuItem(i,MF_BYPOSITION|MF_CHECKED); else //不是要选中的菜单项,清除选中标识 subMenu3->CheckMenuItem(i,MF_BYPOSITION|MF_UNCHECKED); } } 该函数有四个参数,其中参数level2指定了要设置选中标识的菜单项所在 的二级菜单的位置序号 ;level3指定了要设置选中标识的菜单项所在的三级菜单 的位置序号 ;count 指定了要设置选中标识的菜单项所在的三级菜单下总共有多 少个四级菜单项; sel 指定了要设置选中标识的菜单项的位置序号。 现在编写“PS_SOLID ”菜单项处理函数,看一下如何使用 SetMenuItemCheck 函数来设置菜单项的选中标识,代码如下: void CDrawTestView::OnPenSolid() { //TODO: Add your command handler code here m_PenStyle = PS_SOLID;//设置画笔线型为 PS_SOLID this->SetMenuItemCheck(0,0,7,0);//设置菜单项选中状态 } 代码中第一句将当前自定义画笔的线型设置为PS_SOLID,第二句调用 SetMenuItemCheck 函数来设置 “PS_SOLID”菜单项为选中状态 。该函数传入 参数为 0、0、7、0,因为 “PS_SOLID”菜单项所处的二级菜单 “画笔 ”在“自 定义绘图工具 ”菜单中的位置序号为 0,所处三级菜单 “线型 ”在二级菜单 “画 笔”中的位置序号也为0,在“线型”菜单下总共有7个四级菜单项,其中 “PS_SOLID”菜单项的位置序号为 0。 同样的,我们可以编写其他菜单项的处理函数代码,其代码如下: //PS_DASH 菜单项处理函数 void CDrawTestView::OnPenDash() { //TODO: Add your command handler code here m_PenStyle = PS_DASH; this->SetMenuItemCheck(0,0,7,1); } //PS_DOT 菜单项处理函数 void CDrawTestView::OnPenDot() { //TODO: Add your command handler code here m_PenStyle = PS_DOT; this->SetMenuItemCheck(0,0,7,2); } //PS_DASHDOT 菜单项处理函数 void CDrawTestView::OnPenDashdot() { //TODO: Add your command handler code here m_PenStyle = PS_DASHDOT; this->SetMenuItemCheck(0,0,7,3); } //PS_DASHDOTDOT 菜单项处理函数 void CDrawTestView::OnPenDashdotdot() { //TODO: Add your command handler code here m_PenStyle = PS_DASHDOTDOT; this->SetMenuItemCheck(0,0,7,4); } //PS_NULL 菜单项处理函数 void CDrawTestView::OnPenNull() { //TODO: Add your command handler code here m_PenStyle = PS_NULL; this->SetMenuItemCheck(0,0,7,5); } //PS_INSIDEFRAME 菜单项处理函数 void CDrawTestView::OnPenInsideframe() { //TODO: Add your command handler code here m_PenStyle = PS_INSIDEFRAME; this->SetMenuItemCheck(0,0,7,6); } //宽度 1菜单项处理函数 void CDrawTestView::OnPen1() { //TODO: Add your command handler code here m_PenWidth = 1; this->SetMenuItemCheck(0,1,4,0); } //宽度 3菜单项处理函数 void CDrawTestView::OnPen3() { //TODO: Add your command handler code here m_PenWidth = 3; this->SetMenuItemCheck(0,1,4,1); } //宽度 5菜单项处理函数 void CDrawTestView::OnPen5() { //TODO: Add your command handler code here m_PenWidth = 5; this->SetMenuItemCheck(0,1,4,2); } //宽度 7菜单项处理函数 void CDrawTestView::OnPen7() { //TODO: Add your command handler code here m_PenWidth = 7; this->SetMenuItemCheck(0,1,4,3); } //画笔颜色下黑色菜单项处理函数 void CDrawTestView::OnPenBlack() { //TODO: Add your command handler code here m_PenColor = RGB(0,0,0); this->SetMenuItemCheck(0,2,4,0); } //画笔颜色下红色菜单项处理函数 void CDrawTestView::OnPenRed() { //TODO: Add your command handler code here m_PenColor = RGB(255,0,0); this->SetMenuItemCheck(0,2,4,1); } //画笔颜色下绿色菜单项处理函数 void CDrawTestView::OnPenGreen() { //TODO: Add your command handler code here m_PenColor = RGB(0,255,0); this->SetMenuItemCheck(0,2,4,2); } //画笔颜色下蓝色菜单项处理函数 void CDrawTestView::OnPenBlue() { //TODO: Add your command handler code here m_PenColor = RGB(0,0,255); this->SetMenuItemCheck(0,2,4,3); } //None 菜单项处理函数 void CDrawTestView::OnBrushNone() { //TODO: Add your command handler code here m_BrushStyle = -1; this->SetMenuItemCheck(1,0,7,0); } //HS_BDIAGONAL 菜单项处理函数 void CDrawTestView::OnBrushBdiagonal() { //TODO: Add your command handler code here m_BrushStyle = HS_BDIAGONAL; this->SetMenuItemCheck(1,0,7,1); } //HS_CROSS 菜单项处理函数 void CDrawTestView::OnBrushCross() { //TODO: Add your command handler code here m_BrushStyle = HS_CROSS; this->SetMenuItemCheck(1,0,7,2); } //HS_DIAGCROSS 菜单项处理函数 void CDrawTestView::OnBrushDiagcross() { //TODO: Add your command handler code here m_BrushStyle = HS_DIAGCROSS; this->SetMenuItemCheck(1,0,7,3); } //HS_FDIAGONAL 菜单项处理函数 void CDrawTestView::OnBrushFdiagonal() { //TODO: Add your command handler code here m_BrushStyle = HS_FDIAGONAL; this->SetMenuItemCheck(1,0,7,4); } //HS_HORIZONTAL 菜单项处理函数 void CDrawTestView::OnBrushHorizontal() { //TODO: Add your command handler code here m_BrushStyle = HS_HORIZONTAL; this->SetMenuItemCheck(1,0,7,5); } //HS_VERITICAL 菜单项处理函数 void CDrawTestView::OnBrushVertical() { //TODO: Add your command handler code here m_BrushStyle = HS_VERTICAL; this->SetMenuItemCheck(1,0,7,6); } //画刷颜色下白色菜单项处理函数 void CDrawTestView::OnBrushWhite() { //TODO: Add your command handler code here m_BrushColor = RGB(255,255,255); this->SetMenuItemCheck(1,1,4,0); } //画刷颜色下红色菜单项处理函数 void CDrawTestView::OnBrushRed() { //TODO: Add your command handler code here m_BrushColor = RGB(255,0,0); this->SetMenuItemCheck(1,1,4,1); } //画刷颜色下绿色菜单项处理函数 void CDrawTestView::OnBrushGreen() { //TODO: Add your command handler code here m_BrushColor = RGB(0,255,0); this->SetMenuItemCheck(1,1,4,2); } //画刷颜色下蓝色菜单项处理函数 void CDrawTestView::OnBrushBlue() { //TODO: Add your command handler code here m_BrushColor = RGB(0,0,255); this->SetMenuItemCheck(1,1,4,3); } 我们在CDrawTestView 类中再添加两个函数GetPen 和GetBrush 来获得 自定义画笔和画刷指针,函数代码如下: //获得自定义画笔指针 CPen* CDrawTestView::GetPen() { return new CPen(m_PenStyle,m_PenWidth,m_PenColor); } //获得自定义画刷指针 CBrush* CDrawTestView::GetBrush() { //判断是否是阴影线画刷 if (m_BrushStyle == -1) //不是阴影线画刷 return new CBrush(m_BrushColor); else //是阴影线画刷 return new CBrush(m_BrushStyle,m_BrushColor); } 编写这两个函数的好处在于:如果以后需要更改获得画笔或者画刷的方式 (比如画笔改用第三个构造函数来构造 ),只需要修改这两个函数即可。如果在 每个绘图函数菜单项的处理函数中写构造画笔和画刷的代码 ,一旦画笔和画刷的 构造方式要发生改变,就必须逐个修改每个绘图函数菜单项的处理函数。 现在修改绘图函数菜单项的处理函数 ,调用 GetPen 和GetBrush 函数获得 画笔和画刷 ,然后选用画笔和画刷 ,并在函数的最后删除画笔和画刷 。这是因为 获得画笔和画刷的函数每次都是构造新的画笔和画刷,而它们将占用系统资源 , 所以在使用完毕后要进行删除。而 LineTo 等绘制线形图形的绘图函数不受画刷 影响 ,所以在这些绘图函数的处理函数中不需要选用画刷 。因为所有线形图形绘 图函数菜单项的处理函数要添加的代码是相同的 ,同样所有区域图形绘图函数菜 单项的处理函数要添加的代码也是相同的,所以这里只列出 “LineTo”(绘制线 形图形 )菜单项和 “Rectangle”(绘制区域图形 )菜单项修改后的处理函数代码 , 读者只需按照相同方法修改其它处理函数即可。修改后的代码如下: //LineTo 菜单项处理函数 void CDrawTestView::OnDrawLineto() { //TODO: Add your command handler code here CClientDCCClientDCCClientDCCClientDC dcdcdcdc((((thisthisthisthis);););); CPenCPenCPenCPen**** penpenpenpen ==== thisthisthisthis->->->->GetPenGetPenGetPenGetPen();();();(); dcdcdcdc....SelectObjectSelectObjectSelectObjectSelectObject((((penpenpenpen);););); dc.MoveTo(300,300); dc.LineTo(400,400); penpenpenpen->->->->DeleteObjectDeleteObjectDeleteObjectDeleteObject();();();(); } //Rectangle 菜单项处理函数 void CDrawTestView::OnDrawRectangle() { //TODO: Add your command handler code here CClientDC dc(this); CPenCPenCPenCPen**** penpenpenpen ==== thisthisthisthis->->->->GetPenGetPenGetPenGetPen();();();(); CBrushCBrushCBrushCBrush**** brushbrushbrushbrush ==== thisthisthisthis->->->->GetBrushGetBrushGetBrushGetBrush();();();(); dcdcdcdc....SelectObjectSelectObjectSelectObjectSelectObject((((penpenpenpen);););); dcdcdcdc....SelectObjectSelectObjectSelectObjectSelectObject((((brushbrushbrushbrush);););); dc.Rectangle(450,100,650,250); penpenpenpen->->->->DeleteObjectDeleteObjectDeleteObjectDeleteObject();();();(); brushbrushbrushbrush->->->->DeleteObjectDeleteObjectDeleteObjectDeleteObject();();();(); } 代码中粗体字部分是后添加的代码,修改原则是在获得设备环境对象之后 , 调用绘图函数之前,先获得画笔或画笔和画刷,然后让设备环境对象选用它们 。 最后在所有绘图函数执行完后,删除前面获得的画笔或画笔和画刷。 图1.59 显示的是选用了不同的自定义画笔和画刷进行绘图后的结果。 1.71.71.71.7 文本输出 除了可以在视图区绘制图形外 ,我们也可以在视图区输出文本 ,设备环境提 供了用于文本输出的函数 。与设备环境提供的绘图函数只是决定绘制什么样形状 的图形,而要由画笔类和画刷类来决定绘制的样式一样, MFC 也提供了 CFont 类(字体类 )来决定以什么样的格式输出文本 。用户除了可以使用预定义的系统 字体输出文本外,还可以自己定义逻辑字体。 1.7.11.7.11.7.11.7.1 文本输出函数 设备环境中常用的文本输出函数有以下几个: � TextOut TextOut TextOut TextOut 函数,其函数声明如下: virtual BOOL TextOut(int x, int y,LPCTSTR lpszString, int nCount); BOOL TextOut(int x, int y, const CString& str); 其中第一个函数的参数 x和y指定了输出文本的起始位置 ;参数 lpszString 指定了要输出的文本,该参数值可以是一个 CString(字符串)类对象,或者直 接是一个用双引号定义的字符串 ;参数 nCount 指定了要输出的字符个数 ,要注 意,中文是占两个字符位的 。第二个函数的参数 x和y指定了输出文本的起始位 置;参数str 指定了要输出的文本,该参数值可以是一个CString(字符串)类 对象,或者直接是一个用双引号定义的字符串。如果输出文本成功,函数返回 TRUE,否则返回 FALSE。 � DrawText DrawText DrawText DrawText 函数,其函数声明如下: virtual int DrawText(LPCTSTR lpszString, int nCount,LPRECT lpRect, UINT nFormat); int DrawText(const CSTring& str,LPCRECT lpRect,UINT nFormat); 第一个函数将指定的字符串按照指定的格式显示在一个矩形区域中 ,其中参 数lpszString 指定了要输出的文本字符串 ;参数 nCount 指定了字符串中要输出 的字符个数;参数 lpRect 指定了要输出的矩形区域;参数 nFormat 指定了文本 输出格式 。第二个函数与第一个函数相比 ,只是没有了 nCount 参数 ,即第二个 函数要输出全部的字符串 。如果输出文本成功 ,函数返回输出文本的高度 。在该 函数中 nFormat 用于指定文本在矩形区域内的输出格式 ,其可选值如下表所示 : 参数值 说明 DT_WORDBREAK 如果行显示超过矩形区域宽度,自动在单词之间换行 DT_VCENTER 垂直方向居中显示文本,必须和DT_SINGLELINE 联 合使用 DT_TOP 区域顶部显示文本,必须和DT_SINGLELINE 联合使 用 DT_TABSTOP 设置 TAB 字符停止位 DT_SINGLELINE 单行显示文本,回车和换行不打断原有的行 DT_RIGHT 靠右显示文本 DT_NOPREFIX 终止对前缀字符的处理 DT_NOCLIP 不加剪切的显示文本 DT_LEFT 靠左显示文本 DT_EXTERNALLEADI NG 行高度内包含外部行间距 DT_EXPANDTABS 扩大 TAB 字符数,默认为 8个字符宽 DT_CENTER 居中显示文本 DT_CALCRECT 多行显示文本时 ,如果文本实际所在矩形区域的宽度与 参数lpRect 指定的用于输出文本的矩形区域的宽度不 一致的话 ,那么该函数使用后者的宽度 ,并向下延长矩 形区域以便可以容纳下所有的文本 。如果是单行显示文 本,该函数将修改矩形区域的右边界以便它可以容纳下 此行中的最后一个字符 。在这两种情况下 ,该函数不会 绘制文本,而只返回格式化的文本的高度 DT_BOTTOM 区域底部显示文本,必须和DT_SINGLELINE 联合使 用 DT_END_ELLIPSIS DT_PATH_ELLIPSIS 如果有必要使用省略号来代替字符串中的一部分 ,以便 使输出结果可以适合于lpRect 所指定的矩形区域,用 户可以使用DT_END_ELLIPSIS 参数来取代字符串尾 部的若干字符 ,或者使用 DT_PATH_ELLIPSIS 来取代 字符串中间的若干字符 。如果字符串中包括 “\”(反斜 杠)字符 ,DT_PATH_ELLIPSIS 参数将尽可能保证最 后一个反斜杠后面的文本。 以上可选值可以联合使用,值与值之间用 “|”连接。比如设置如下值: DT_CENTERDT_CENTERDT_CENTERDT_CENTER||||DT_SINGLELINEDT_SINGLELINEDT_SINGLELINEDT_SINGLELINE||||DT_NOCLIPDT_NOCLIPDT_NOCLIPDT_NOCLIP 表示输出文本将在矩形区域居中单行显示 ,如果文本超出矩形范围也不进行 剪切。下面我们看一个例子,修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造要输出的文本字符串 CString s; s = "DrawTest Function"; //构造文本要输出的矩形区域 CRect r; r.left = 50;r.right =150; r.top =10;r.bottom = 40; //绘制矩形,以便确定输出的文本在矩形区域中的位置 pDC->Rectangle(r); //水平,垂直方向都居中,单行显示,超出区域范围不剪切 pDC->DrawText(s,r,DT_VCENTER|DT_CENTER|DT_SINGLELINE |DT_NOCLIP); //向下平移矩形区域 r.top = r.top + 50;r.bottom = r.bottom +50; pDC->Rectangle(r); //垂直方向居底,水平方向靠右,单行显示 pDC->DrawText(s,r,DT_BOTTOM|DT_RIGHT|DT_SINGLELINE); //向下平移矩形区域 r.top = r.top + 50;r.bottom = r.bottom +50; pDC->Rectangle(r); //垂直方向居顶,水平方向靠左,超出矩形区域时,在单词之间换行 pDC->DrawText(s,r,DT_TOP|DT_LEFT|DT_WORDBREAK); //向下平移矩形区域 r.top = r.top + 50;r.bottom = r.bottom +50; pDC->Rectangle(r); //垂直方向居中,水平方向靠左,单行显示,超出范围时用省略号取代 字符串尾部字符 pDC->DrawText(s,r, DT_VCENTER|DT_LEFT|DT_SINGLELINE|DT_END_E LLIPSIS); } 运行应用程序,结果如图 1.60 所示。 该段程序分别以不同的格式输出了四次 “DrawText Function”字符串,每 次输出的矩形区域位置不同 ,但是矩形区域的大小是相同的 。为了能够看到输出 文本在矩形区域中的位置 ,在输出文本之前将矩形区域用 Rectangle 函数绘制了 出来 。这里要注意 Rectangle 绘制的是填充的矩形 ,所以要放在文本输出函数之 前执行 ,否则绘制的矩形将覆盖输出的文本 。绘制矩形是为了示例的需要 ,正常 使用时由使用者决定是否需要绘制出矩形区域。 可以注意到,因为第一个输出选择了DT_NOCLIP 参数值,所以尽管输出 的文本超出了矩形区域的范围 ,文本仍然完全显示 。而第二个就因为超出了矩形 区域范围,超出的部分被剪切掉。第三个输出因为选择了 DT_WORDBREAK, 所以在文本超出矩形范围时 ,按该字符串的单词结构 ,将“Function”换行到下 一行显示。这里 DrawText 函数将字符串中的空格符认为是单词的分割标识 ,所 以把 “DrawText Function”字符串中被空格符分开的两段字符串 “DrawText” 和“Function”视为两个单词 。第四个因为选择了 DT_END_ELLIPSIS,所以当 输出字符串超出了矩形区域范围时 ,字符串的尾部若干字符被省略号 “...”代替 。 � ExtTextOut ExtTextOut ExtTextOut ExtTextOut 函数,其函数声明如下: virtual BOOL ExtTextOut(int x, int y,UINT nOptions,LPCRECT lpRect, LPCTSTR lpszString,UINT nCount,LPINT lpDxWidths); BOOL ExtTextOut(int x, int y,UINT nOptions,LPCRECT lpRect, const CString& str,LPINT lpDxWidths); 第一个函数中的参数 x和y指定了文本起始输出位置;参数 nOptions 可以 是下列两个值之一,或者是它们的组合: ETO_CLIPPEDETO_CLIPPEDETO_CLIPPEDETO_CLIPPED 文本被矩形剪切 ETO_OPAQUEETO_OPAQUEETO_OPAQUEETO_OPAQUE 使用当前的背景色填充矩形 参数lpRect 指定了显示文本的矩形区域,因为此函数指定了文本起始输出 的位置 ,所以如果指定的起始输出位置导致文本不在指定的矩形区域内 ,则文本 将不作输出 ;如果指定的起始输出位置导致文本不全在矩形区域内 ,且nOptions 选择了 ETO_CLIPPED,则在矩形区域外的部分将被剪切 。如果 nOptions 选择 了ETO_OPAQUE,则使用当前设置的背景色填充指定的矩形区域。 参数 lpszString 指定了要输出的文本字符串 ;参数 nCount 指定了要输出的 字符数;参数 lpDxWidths 指向一个数值数组,该数组元素指定了对应的字符间 的间隔,比如 lpDxWidths 数组的第一个值指定了字符串中第一个字符和第二个 字符之间的间隔宽度,而 lpDxWidths 数组的第二个值指定了字符串中第二个字 符和第三个字符之间的间隔宽度 ,以此类推 。需要注意的是 ,此间隔宽度的计算 是相邻两个字符最左端之间的间隔 ,而不是前一个字符的最右端和当前字符最左 端之间的间隔 ,所以如果这个间隔值设置过小 ,会导致字符重叠在一起 。如果参 数设置为 NULL,表示字符间隔使用系统默认值。 第二个函数与第一个函数相比 ,只是没有指定要输出的字符数 ,表示整个传 入的字符串都要进行输出。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造要输出的字符串 CString str; str = "ExtTextOut Function!!!"; //构造矩形区域对象 CRect r; r.left = 100;r.right = 220; r.top =50;r.bottom = 100; //设定字符间隔数组 int lpDxWidths[] = {15,20,25,30,0}; pDC->SetTextColor(RGB(255,0,0)); //设置文本输出前景色 //设置文本输出背景色 pDC->SetBkColor(RGB(225,225,225)); pDC->Rectangle(r);//绘制矩形区域 //在(110,60)位置开始输出文本,超出部分裁剪掉,输出字符数为 5 pDC->ExtTextOut(110,60,ETO_CLIPPED,r,str,5,lpDxWidths); //矩形区域下移 r.top = r.top + 100;r.bottom = r.bottom + 100; //绘制矩形区域 pDC->Rectangle(r); //在(110,160)位置开始输出文本,超出部分裁剪掉 //字符串全部输出,没有指定字符间隔 pDC->ExtTextOut(110,160,ETO_CLIPPED,r,str,NULL); } 运行应用程序,结果如图 1.61 所示: 该段程序将指定字符串用 ExtTextOut 函数输出了两次,第一次指定了输出 字符个数和字符间隔 。第二次则输出了整个字符串且没有指定字符间隔 。需要注 意的是 ,第一次输出五个字符 ,实际上指定五个字符间的间隔只需要数值数组有 四个值即可,而代码中给出了五个值且最后一个值为0。如果将最后一个0去 掉,我们会发现实际输出的文本在第五个字符 “e”后多出一段空白。在数值数 组中多加一个参数 0,表示输出文本在最后一个字符右端截止 。第二个输出的字 符串 ,由于超出了指定的矩形区域的范围 ,且nOptions 值设为 ETO_CLIPPED, 所以多出的部分被裁剪掉。 在此段代码中我们还调用了设备环境的相应函数设置了文本输出的前景色 和背景色,下面我们介绍一下设置文本输出属性的相关函数。 1.7.21.7.21.7.21.7.2 设置文本输出颜色及文本对齐方式 设置文本输出颜色使用如下函数。 � SetTextColor SetTextColor SetTextColor SetTextColor 函数 ,用于设置文本输出的前景色 ,即文字的颜色 ,其函 数声明如下: virtual COLORREF SetTextColor(COLORREF crColor); 参数 crColor 指定了文本输出的前景色 ,该函数返回设置前景色之前设备环 境对象文本输出的前景色。设备环境对象的默认文本输出前景色为黑色。 � SetBkColor SetBkColor SetBkColor SetBkColor 函数,用于设置文本输出的背景色,即输出的文本实际所 处的矩形区域的填充色(参看图 1.61),其函数声明如下: virtual COLORREF SetBkColor(COLORREF crColor); 参数 crColor 指定了文本输出的背景色 ,该函数返回设置背景色之前设备环 境对象文本输出的背景色。设备环境对象的默认文本输出背景色为白色。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //设置文本输出的前景色和背景色,并获得原来使用的颜色 COLORREF c = pDC->SetTextColor(RGB(255,0,255)); COLORREF b = pDC->SetBkColor(RGB(200,200,200)); //输出文本 pDC->TextOut(100,100,"TextColor and BkColor"); //将原来的前景色和背景色设置回来 pDC->SetTextColor(c); pDC->SetBkColor(b); //输出文本 pDC->TextOut(100,200,"TextColor and BkColor"); } 运行应用程序,结果如图 1.62 所示。 此段代码在设置文本输出的前景色和背景色时 ,保存了函数返回的原有的文 本输出的前景色和背景色 ,并在输出第二个文本的时候将原来的前景色和背景色 选择回来。因为设备环境对象的默认文本输出的前景色和背景色是黑色和白色 , 所以输出的第二个文本的前景色和背景色就是黑色和白色。 设置文本的对齐方式使用下面的函数。 � SetTextAlign SetTextAlign SetTextAlign SetTextAlign 函数,其函数声明如下: UINT SetTextAlign(UINT nFlags); 参数nFlags 指定了文本输出的对齐方式。我们在调用TextOut 函数或 ExtTextOut 函数进行文本输出的时候,需要指定一个点作为文本输出的起始位 置。文本输出的对齐方式就决定了这个指定点和实际输出的文本矩形边界 (文本 输出时 ,背景色填充的矩形区域的边界 ,如图 1.61 和图 1.62 所示 )的相对关系 。 该参数的可选值分为三组 ,其参数值应该是这三组可选值中每组选出一个值之后 的联合取值,取值之间用 “|”连接。 第一组取值影响在 X轴方向上的文本对齐方式: 参数值 说明 TA_CENT ER 文本矩形边界的水平中心与给定点对齐 TA_LEFT 文本矩形边界的左边界与给定点对齐 ,此值为该组可选值的默认值 TA_RIGH T 文本矩形边界的右边界与给定点对齐 第二组取值影响在 Y轴方向上的文本对齐方式: 参数值 说明 TA_BASELIN E 按输出文本所使用字体的基线与给定点对齐 TA_BOTTOM 文本矩形边界的下边界与给定点对齐 TA_TOP 文本矩形边界的上边界与给定点对齐,此值为该组默认值 第三组取值指定文本函数的调用是否会更新文本输出的当前位置: 参数值 说明 TA_NOUPDAT ECP 文本输出函数(TextOut 和ExtTextOut)不更新文本输出的 当前位置,此值为该组可选值的默认值 TA_UPDATEC P 调用文本输出函数后,更新X轴方向上的当前绘图位置。新 位置位于输出文本矩形边界的右边界。当设置此种对齐方式 后, TextOut 函数和 ExtTextOut 中所给定的坐标被忽略 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //使用默认文本对齐方式输出文本 pDC->TextOut(100,100,"TextAlign"); //设置文本对齐方式为文本矩形右边界和下边界和给定点对齐 pDC->SetTextAlign(TA_RIGHT|TA_BOTTOM); //设置文本输出前景色 pDC->SetTextColor(RGB(0,0,255)); //输出文本 pDC->TextOut(100,100,"TextAlign"); //设置文本对齐方式为文本矩形左边界和上边界和给定点对齐 //并且文本输出函数更新文本输出的当前位置 pDC->SetTextAlign(TA_LEFT|TA_TOP|TA_UPDATECP); //移动当前绘图位置到 (10,10)点 pDC->MoveTo(10,10); //输出一系列文本 pDC->TextOut(100,200,"TextOut01 "); pDC->TextOut(100,200,"TextOut02 "); CRect r; r.top = 0;r.right = 400; r.left = 0;r.bottom = 400; pDC->ExtTextOut(100,200,ETO_CLIPPED,r,"TextOut03 ",NULL); pDC->TextOut(100,200,"TextOut04 "); //从当前绘图位置到 (300,100)点画一条直线段 pDC->LineTo(300,100); } 运行应用程序,结果如图 1.63 所示: 输出的前两个文本因为设置的文本输出对齐方式的不同,尽管在调用 TextOut 函数时指定了相同的起始位置 ,但是两个文本并没有重叠 。后面输出的 四个文本 ,因为设置了 TA_UPDATECP,所以 TextOut 函数和 ExtTextOut 函数 中指定的起始位置不再有效,而是从用MoveTo 函数设定的当前绘图位置(10, 10)开始进行输出 。输出完四个文本后 ,当前绘图位置的 x坐标值变为输出的最 后一个文本的文本矩形右边界,而y坐标值没有改变。这里需要注意,使用 ExtTextOut 函数输出的文本需要实际输出的文本在其参数所指定的矩形区域 内,如果不在区域范围内 ,我们将看不到该函数所输出的文本 。但是它仍然对当 前绘图位置产生影响 ,就相当于该文本已经输出了一样 。将上段代码中定义的矩 形区域的 top 值修改为 100,重新运行应用程序,结果如图 1.64 所示,图中输 出的 “TextOut02”和“TextOut04”之间有一段空白 ,这是因为新设置的矩形 区域导致实际输出的 “TextOut03”文本不在矩形区域范围内 ,所以该文本并没 有输出,但是 ExtTextOut 函数执行对当前绘图位置产生的影响仍然像该文本已 经输出了一样。 SetTextAlign 函数返回原来的文本对齐方式。 设备环境提供了函数用于设置输出文本的字符间空隙大小。 � SetTextCharacterExtra SetTextCharacterExtra SetTextCharacterExtra SetTextCharacterExtra 函数,其函数声明如下: int SetTextCharacterExtra(int nCharExtra); 参数 nCharExtra 指定了每个字符间的空隙大小,即字符间距,对于汉字而 言就是每个汉字之间的间距。该函数返回设置新字符间距之前的字符间距。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //设置字符间距为 5,并获得原来的字符间距 int extra = pDC->SetTextCharacterExtra(5); //输出文本 pDC->TextOut(10,10,"设置字符间距 "); //使用原来的字符间距输出文本 pDC->SetTextCharacterExtra(extra); pDC->TextOut(10,60,"设置字符间距 "); } 运行应用程序,结果如图 1.65 所示。 本节前面例子中输出文本都是使用了应用环境的默认字体,用户可以通过 CFont 类来自定义逻辑字体 ,并使用该字体来输出文本 。下面介绍一下如何使 用 CFont 类。 1.7.31.7.31.7.31.7.3 CFont CFont CFont CFont 类 CFone 类是一个 Windows 的GDI 对象类 ,该类封装了 Windows 图形设备 接口字体并提供了操作字体的成员函数,用于构建逻辑字体。使用CFont 类对 象,需要首先构造该对象,然后调用CFont 类提供的初始化函数进行初始化, 再由设备环境对象选用初始化后的字体对象 ,调用文本输出函数 ,利用选用的字 体输出文本 。最后需要将使用完的 CFont 对象删除 ,因为该对象占用系统资源 。 CFont 类只有一个构造函数: CFontCFontCFontCFont()()()() 该构造函数构造一个没有初始化的CFont 类对象,该对象必须被初始化后 才能够使用。 CFont 类提供了如下初始化函数来初始化 CFont 对象。 � CreateFont CreateFont CreateFont CreateFont 函数 ,用于初始化一种具有指定属性的逻辑字体 ,其函数声 明如下: BOOL CreateFont(int nHeight, int nWidth, int nEscapement, int nOrientation, int nWeight,BYTE bItalic,BYTE bUnderline,BYTE cStrikeOut, BYTE nCharSet,BYTE nOutPrecision,BYTE nClipPrecision, BYTE nQuality,BYTE nPitchAndFamily,LPCTSTR lpszFacename); 该函数具有很多参数 ,这些参数用于指定字体的各种属性 ,下面分别加以介 绍: 参数 nHeightnHeightnHeightnHeight:指定了字体的高度 。当此参数值为正数时 ,字体映射机制会 根据指定的高度从字体列表中选择一种最为接近的字体,此时是以字体的单元 (cell)高度作为参考。如果此参数值是一个负数,则字体映射机制会以字体的 字符(character)高度为参考从字体列表中找出一种合适的字体。在匹配字体 的高度时 ,字体映射机制从列表中选取一种比你指定高度要小的最大字体 。单元 (cell)和字符 (character)之间的区别在于 :单元在实际输出字符的上下都有 一些空隙 ,而字符的高度是忽略掉这些空隙之后的单元高度 。如果此参数值为 0, 则字体映射机制会使用默认高度的字体。 参数nWidthnWidthnWidthnWidth:指定了字体的平均宽度(比例间隔字体的字符宽窄不一)。 如果此参数值为 0,此时字体映射机制会自动根据所设定的高度选择一个恰当的 宽度作为缺省值。 参数nEscapementnEscapementnEscapementnEscapement:指定了使用该字体输出文本时的旋转角度(以1/10 度为单位 )。X轴正向的角度为 0度,正角度为从 X轴正向开始逆时针旋转。 参数nOrientationnOrientationnOrientationnOrientation:指定了字体基线 与X 轴正向的夹角 (以1/10 度为单位 )。 当Y坐标值向下为增加时,从X轴正向开始逆时针旋转的角度为正角度;当Y 坐标值向上为增加时,从 X轴正向开始顺时针旋转的角度为正角度。 参数 nWeightnWeightnWeightnWeight:指定了字体的浓度,即字体的粗细程度(像素数 /1000), 下表为该参数的可选值,此处可以选用下列常量,也可以直接输入具体的数值 : 常量 值常量 值 FW_DONTCARE 0 FW_SEMIBOLD 600 FW_THIN 100 FW_DEMIBOLD 600 FW_EXTRALIGH T 200 FW_BOLD 700 FW_ULTRALIGH T 200 FW_EXTRABOL D 800 FW_LIGHT 300 FW_ULTRABOL D 800 FW_NORMAL 400 FW_BLACK 900 FW_REGUNAL 400 FW_HEAVY 900 FW_MEDIUM 500 参数 bItalicbItalicbItalicbItalic:指定字体是否为斜体 。输入 TRUE,表示是斜体 ,输入 FALSE, 表示不是斜体。 参数bUnderlinebUnderlinebUnderlinebUnderline: 指定 字体 是否 有下 划线 。输 入 TRUE, 表示 有下 划线 , 输FALSE,表示没有下划线。 参数cStrikeOutcStrikeOutcStrikeOutcStrikeOut:指定字体是否被穿透,即字符中央有一条横线。输入 TRUE,表示被穿透,输入 FALSE,表示不被穿透。 参数 cCharSetcCharSetcCharSetcCharSet:指定字体的字符集 ,下表列出了预定义的字符集常量和对 应的值: 常量 值 ANSI_CHARSET 0 DEFAULT_CHARSET 1 SYMBOL_CHARSET 2 SHIFTJIS_CHARSET 128 OEM_CHARSET 255(依赖于具体系统) 参数 nOutputPrecisionnOutputPrecisionnOutputPrecisionnOutputPrecision:指定了字体的输出精度 ,该精度确定了实际输出 与所设置的字体高度 、宽度 、旋转 、字符方位及间距的匹配和接近程度 。该参数 可 取下 列值 之一 : OUT_CHARACTER_PRECIS、OUT_DEFAULT_PRECIS、 OUT_DEVICE_PRECIS、OUT_RASTER_PRECIS、OUT_STRING_PRECIS、 OUT_STROKE_PRECIS、OUT_TT_PRECIS。 参数nClipPrecisionnClipPrecisionnClipPrecisionnClipPrecision:指定了字体的裁剪精度。当对一段输出的文本进行 裁剪的时候 ,可能字体正好在裁剪线上 ,此参数决定了如何裁剪位于裁剪区之外 的部分字体。该参数可取下列值之一:CLIP_CHARACTER_PRECIS 、 CLIP_MASK 、CLIP_DEFAULT_PRECIS 、CLIP_STROKE_PRECIS 、 CLIP_ENCAPSULATE、CLIP_TT_ALWAYS、CLIP_LH_ANGLES。 参数nQualitynQualitynQualitynQuality:指定了输出字体的质量。该参数决定了系统在多大程度上 将设置了各种属性的逻辑字体与实际的物理字体进行匹配 。该参数可取下列值之 一:DEFAULT_QUALITY 、DRAFT_QUALITY 、PROOF_QUALITY 。其中 DEFAULT_QUALITY 定义的质量最低。 参数 nPitchAndFamilynPitchAndFamilynPitchAndFamilynPitchAndFamily:指定了字体的间距和所属的族。此参数的低 2位 指定了字体的间距,可以是如下的取值之一:DEFAULT_PITCH 、 FIXED_PITCH、VARIABLE_PITCH。高4位指定了字体族,可以是如下的取 值 之一 :FF_DECORATIVE、FF_DONTCARE、FF_MODERN、FF_ROMAN、 FF_SCRIPT 、FF_SWISS 。参数值之间可以用“+”来连接,例如 DEFAULT_PITCH+ FF_DONTCARE 参数 lpszFacenamelpszFacenamelpszFacenamelpszFacename:指定了目标字体 (实际物理字体 )的字体名 ,该参数 值的长度不能超过 30个字符 。此参数指定的字体不是必须存在的 ,如果指定的 字体不存在,或者输入 NULL,则会使用设备默认字体。 如果初始化字体成功,函数返回 TRUE,否则返回 FALSE。 � CreateFontIndirect CreateFontIndirect CreateFontIndirect CreateFontIndirect 函数 ,用于初始化由 LOGFONT 结构指定相关属性 的逻辑字体,其函数声明如下: BOOL CreateFontIndirect(const LOGFONT* lpLogFont); 其中参数 lpLogFont 指向一个 LOGFONT 结构,该结构定义如下: typedef struct tagLOGFONT {// lf LONG lfHeight; LONG lfWidth; LONG lfEscapement; LONG lfOrientation; LONG lfWeight; BYTE lfItalic; BYTE lfUnderline; BYTE lfStrikeOut; BYTE lfCharSet; BYTE lfOutPrecision; BYTE lfClipPrecision; BYTE lfQuality; BYTE lfPitchAndFamily; TCHAR lfFaceName[LF_FACESIZE]; }LOGFONT; 该结构中的各个成员变量与 CreateFont 函数中对应的参数含义相同,这里 不再重复。如果初始化字体成功,函数返回 TRUE,否则返回 FALSE。 初始化逻辑字体对象后,需要调用设备环境对象的SelectObject 函数选择 该字体 � SelectObject SelectObject SelectObject SelectObject 函数声明如下: CFont* SelectObject(CFont* pFont); 该函数也是SelectObject 函数的一个重载,参数pFont 为指向初始化完的 逻辑字体对象的指针,返回将选择新逻辑字体之前的原有逻辑字体指针。 现在来看一下使用自定义逻辑字体输出文本的示例,修改应用程序的 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造字体属性结构 LOGFONT logFont; logFont.lfHeight = -30;//字体高度 logFont.lfWidth = 0;//字体宽度默认 logFont.lfEscapement = 0;//输出文本旋转角度 logFont.lfOrientation = 0;//字体基线与 X轴角度 logFont.lfWeight = FW_THIN;//字体粗细程度 logFont.lfItalic = FALSE;//非斜体 logFont.lfUnderline = FALSE;//无下划线 logFont.lfStrikeOut = FALSE;//非穿透 logFont.lfCharSet = ANSI_CHARSET;//字符集 logFont.lfOutPrecision = OUT_DEFAULT_PRECIS;//缺省输出精度 logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;//缺省裁剪精度 logFont.lfQuality = DEFAULT_QUALITY;//缺省输出质量 logFont.lfPitchAndFamily = DEFAULT_PITCH+FF_DONTCARE;//缺 省字符间距 //构造逻辑字体 1,并初始化 CFont font1; font1.CreateFontIndirect(&logFont); //选择逻辑字体 1 pDC->SelectObject(&font1); //输出文本 pDC->TextOut(10,10,"CreateFontIndirect!!!"); //修改部分属性 logFont.lfEscapement = 450;//输出文本旋转 45 度 logFont.lfWeight = FW_HEAVY;//粗体字 logFont.lfUnderline = TRUE;//有下划线 //构造逻辑字体 2,并初始化 CFont font2; font2.CreateFontIndirect(&logFont); //选择逻辑字体 2 pDC->SelectObject(&font2); //输出文本 pDC->TextOut(120,210,"CreateFontIndirect!!!"); //再次修改部分属性 logFont.lfEscapement = 900;//输出文本旋转 90度 logFont.lfItalic = TRUE;//斜体字 logFont.lfStrikeOut = TRUE;//穿透 logFont.lfHeight = -20;//修改字体高度 //构造逻辑字体 3,并初始化 CFont font3; font3.CreateFontIndirect(&logFont); //选择逻辑字体 3 pDC->SelectObject(&font3); //输出文本 pDC->TextOut(350,250,"CreateFontIndirect!!!"); //删除创建的逻辑字体 font1.DeleteObject(); font2.DeleteObject(); font3.DeleteObject(); } 运行应用程序,结果如图 1.66 所示。 上面介绍的两个用于初始化逻辑字体的函数使用起来都比较复杂 ,需要设置 很多的参数。 CFont 也提供了可以更简单的进行初始化的函数。 � CreatePointFont CreatePointFont CreatePointFont CreatePointFont 函 数, 用于 快速 初始 化逻 辑字 体对 象, 其函 数声 明如 下: BOOL CreatePointFont(int nPointSize,LPCTSTR lpszFaceName, CDC* pDC = NULL); 其中参数nPointSize 指定了字体的高度,该值是实际显示的字符所占用像 素的10 倍,如输入300,则表示实际输出的字符占用像素为30;参数 lpszFaceName 指定了字体名 ,该参数与前两个初始化函数中的对应参数含义相 同;参数 pDC 指定了用于文本输出的设备环境对象。如果初始化字体成功,函 数返回 TRUE,否则返回 FALSE。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造逻辑字体 1 CFont font1; //初始化逻辑字体,对应物理字体 Impact font1.CreatePointFont(120,"Impact",pDC); //选择逻辑字体 1 pDC->SelectObject(&font1); //输出文本 pDC->TextOut(10,10,"CreatePointFont"); //构造逻辑字体 2 CFont font2; //初始化逻辑字体,对应物理字体 Arial font2.CreatePointFont(220,"Arial",pDC); //选择逻辑字体 2 pDC->SelectObject(&font2); //输出文本 pDC->TextOut(10,50,"CreatePointFont"); //删除逻辑字体对象 font1.DeleteObject(); font2.DeleteObject(); } 运行应用程序,结果如图 1.67 所示。 该段代码中分别使用了两种不同的字体输出文本 。使用 CreatePointFont 函 数初始化字体相比前两种方法更简单 ,但是可以设置的属性也更少 。编程时应该 根据实际需要进行选择。 1.81.81.81.8 绘图属性 设备环境对象中可设置的绘图属性包括映射模式 、绘图方式等 ,下面就集中 介绍一下比较重要的绘图属性及其设置的方法。 1.8.11.8.11.8.11.8.1 映射模式和坐标变换 在平面上或者空间中进行绘图是离不开坐标系的,不管是绘制各种图形 ,还 是输出文本都需要指定它们在屏幕上的位置 。而在 MFC 绘图中 ,实际上存在两 种坐标系 :一个是设备坐标系 ,一个是逻辑坐标系 。初始设备坐标系以视图区的 左上角为原点 ,向右为 X轴正方向 ,向下为 Y轴正方向 ,其度量单位是像素数 , 所以视图区中一点的设备坐标就是该点距视图区左上角的水平和垂直距离的像 素数 。而逻辑坐标系是在内存中虚拟的一个坐标系 ,该坐标系与设备坐标系的对 应关系就由映射方式来决定 。我们在绘图函数和文本输出函数中指定的坐标都是 逻辑坐标 。在MFC 绘图中 ,设备坐标系的 X轴方向和 Y轴方向是固定的 ,像素 的大小取决于具体的屏幕和分辨率。而逻辑坐标系根据设置的映射模式的不同 , 其坐标轴方向和逻辑单位的大小会发生改变 。不同的映射模式下 ,逻辑坐标和设 备坐标之间有不同的换算关系。 在设备环境对象中提供的用于映射方式的函数有两个。 � SetMapMode SetMapMode SetMapMode SetMapMode 函数 ,用于设置设备环境对象使用的映射模式 ,其函数声 明如下: virtual int SetMapMode(int nMapMode); 其中参数 nMapMode 指定了要使用的映射模式,其可选值如下表所示: 映射模式 映射识别码 XXXX轴正向 YYYY轴正向 逻辑单位大小 MM_TEXT 1 向右 向下 pixels(像素) MM_LOMETRIC 2 向右 向上 0.1mm MM_HIMETRIC 3 向右 向上 0.01mm MM_LONGLISH 4 向右 向上 0.01in MM_HIENGLISH 5 向右 向上 0.001in MM_TWIPS 6 向右 向上 1/1440in MM_ISOTROPIC 7 可变可变自定义( x等于 y) MM_ANISOTRO PIC 8 可变可变自定义(x不等于 y) 设置参数时也可以使用映射识别码 。该函数返回设置新的映射模式之前的映 射模式。 � GetMapMode GetMapMode GetMapMode GetMapMode 函数 ,用于返回设备环境对象的当前映射模式 ,其函数声 明如下: int GetMapMode(); 该函数返回值为上面映射模式表中映射模式之一。 在八种映射模式下 ,逻辑坐标系的初始原点都在视图区的左上角 ,但如果视 图类是从CScrollView 类派生而来的,则在用户滚动文档时,MFC 会调整逻辑 坐标系原点的相对位置。 MM_TEXT 映射模式为默认映射模式 ,在该映射模式下 ,逻辑坐标系和设备 坐标系相同。 映射模式MM_LOMETRIC 、MM_HIMETRIC 、MM_LONGLISH 、 MM_HIENGLISH、MM_TWIPS 可以称为“固定比例”映射模式,在固定比例 映射模式下,向右为X轴正向,向上为Y轴正向,即为笛卡尔坐标系。它们的 区别在于逻辑单位大小的不同 ,即由逻辑坐标到设备坐标转换的实际比例因子不 同 。其 中 MM_TWIPS 模 式通 常用 于打 印机 ,一 个 “twip”单 位相 当于 1/20 磅 (Windows 中一磅等于 1/72 英寸 )。 映射模式 MM_ISOTROPIC 和MM_ANISOTROPIC 称为 “可变比例 ”映射 模式。在此种映射模式下,我们可以自定义逻辑单位大小和逻辑坐标轴的方向 。 对于 MM_ISOMETRIC 映射模式 ,X轴方向和 Y轴方向上的逻辑单位大小相同 , 而对于 MM_ANISOMETRIC 映射模式 ,X轴方向和 Y轴方向上的逻辑单位大小 不同 。若要指定逻辑单位的大小 ,可以调用 CDC 的成员函数 SetWindowExt 和 SetViewportExt。 � SetWindowExt SetWindowExt SetWindowExt SetWindowExt 函数 ,用于设置与设备环境对象有关的逻辑窗口 X轴和 Y轴方向的宽度,其函数声明如下: virtual CSize SetWindowExt(int cx, int cy); virtual CSize SetWindowExt(SIZE size); 第一个函数中的参 数cx 和cy 分别指定了逻辑窗 口X 轴和Y 轴方向的宽度 。 第二个函数的参 数size为一 个SIZE结构的变量 ,该结构定义了一个窗口的大小 , 它有两个成员变量 cx 和cy,分别指定了窗口 X方向和 Y方向的宽度 ,其在参数 中的含义与第一个参数中的cx 和cy 相同。此处也可以使用CSize 类对象,该 类也具有 cx 和cy 成员变量。该函数返回设置新的宽度之前的逻辑窗口宽度。 � SetViewportExt SetViewportExt SetViewportExt SetViewportExt 函数,用于设置与设备环境对象相关的设备窗口X轴 和Y轴方向的宽度,其函数声明如下: virtual CSize SetViewportExt(int cx, int cy); virtual CSize SetViewportExt(SIZE size); 其参数含义与 SetWindowExt 函数中对应的参数的意义相同 ,只是该参数设 置的是与设备环境对象相关的设备窗口X轴和Y轴方向的宽度。该函数返回设 置新的宽度之前的设备窗口宽度。 以上两个函数只有在映射模式是 MM_ISOTROPIC 或MM_ANISOTROPIC 时才有效,同时在MM_ISOTROPIC 映射模式下,要求在执行SetViewportExt 函数之前必须首先执行 SetWindowExt 函数。 现在我们来说明一下这两个函数具体是如何指定逻辑单位大小的 。实际上该 逻辑单位的大小是由这两个函数所传入的参数值的比例来决定的。假设 SetWindowExt 函数传入的X方向和Y方向的宽度为cxw 和cyw ,而 SetViewportExt 函数传入的X方向和Y方向的宽度为cxv 和cyv,则映射模式 中的 X轴方向的逻辑单位大小 cxu 为cxv/cxw 个像素 ,而Y方向的逻辑单位大 小cyu 为cyv/cyw 个像素。同时 cxu 和cyu 的正负决定了 X轴和 Y轴的方向 , 其中 cxu 为正时表示向右为 X轴正向 ,为负时相反 ;而cyu 为正时表示向下为 Y 轴 正向 ,为 负时 相反 。假 设在 此设 置下 有一 个点 ,它 的逻 辑坐 标为 (xl, yl),则 它的设备坐标 (xd, yd)可按如下方法求得: xd = xl * cxu; yd = yl * cyu; 需要注意的是 ,在映射模式为 MM_ANISOTROPIC 时,X轴方向和 Y轴方 向上的逻辑单位大小分别为 cxu 和cyu,而在映射模式为 MM_ISOTROPIC 时, 因为映射模式设定X轴方向和Y轴方向上的逻辑单位大小相同,所以设备环境 对 象此 时取 cxu 和cyu 中 较小 的那 个值 作为 X轴 方向 和 Y轴 方向 上的 逻辑 单位 大小。下面我们来看一个具体的例子,修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造红色画笔和蓝色画笔来分别绘制不同映射模式下的直线段 CPen pen1(PS_SOLID,1,RGB(255,0,0)); CPen pen2(PS_SOLID,1,RGB(0,0,255)); //绘制标识线 pDC->MoveTo(25,50); pDC->LineTo(25,100); //设置映射模式为 MM_ANISOTROPIC pDC->SetMapMode(MM_ANISOTROPIC); //设置窗口大小从而影响映射模式下的逻辑单位大小 pDC->SetWindowExt(200,200); pDC->SetViewportExt(50,100); //选择红色画笔 pDC->SelectObject(&pen1); //绘制逻辑坐标为 (0,0)到(100,200)的直线段 pDC->MoveTo(0,0); pDC->LineTo(100,200); //设置映射模式为 MM_ISOTROPIC pDC->SetMapMode(MM_ISOTROPIC); //设置窗口大小从而影响映射模式下的逻辑单位大小 pDC->SetWindowExt(200,200); pDC->SetViewportExt(50,100); //选择蓝色画笔 pDC->SelectObject(&pen2); //绘制逻辑坐标为 (0,0)到(100,200)的直线段 pDC->MoveTo(0,0); pDC->LineTo(100,200); //删除画笔 pen1.DeleteObject(); pen2.DeleteObject(); } 我们先来分析一下这段代码。这段代码分别在MM_ANISOTROPIC 和 MM_ISOTROPIC 两种映射模式下各绘制了一条直线段 。这两条直线段的逻辑坐 标相同,都是从(0, 0)点到(100, 200)点。为了区分这两条直线段各是在哪 种映射模式下绘制的,我们创建了红色和蓝色两个画笔(实线,宽度为1),用 红色画笔绘制MM_ANISOTROPIC 映射模式下的直线段,而用蓝色画笔绘制 MM_ISOTROPIC 映射模式下的直线段。在设置完映射模式为 MM_ANISOTROPIC 后,先调 用SetWindowExt 函数 ,传入参数为 (200, 200), 即cxw = 200,cyw = 200;然后再调用 SetViewportExt 函数 ,传入参数为 (50, 100),即cxv = 50,cyv = 100。这里需要注意的是SetWindowExt 函数和 SetViewportExt 函数需要在设置完映射模式后调用 ,如果在设置映射模式前调用 则不起作用。我们可以计算出 cxu 和cyu 的值: cxu = cxv/cxw = 50/200 = 0.25; cyu = cyv/cyw = 100/200 = 0.5; 所以可以计算出在MM_ANISOTROPIC 映射模式下逻辑坐标(100, 200) 的设备坐标 (xd, yd)(对于逻辑坐标 (0, 0),逻辑单位大小不会产生影响 ): xd = 100 * 0.25 = 25; yd = 200 * 0.5 = 100; 所以 在MM_ANISOTROPIC 映射模式下绘制的红色直线段应该从设备坐标 (0, 0)点(设备坐标系原点)到设备坐标 (25, 100)点。 在设置映射模式为MM_ISOTROPIC 后,调用的SetWindowExt 函数和 SetViewportExt 函数传入了相同的参数,所以也有 cxu = 0.25,cyu = 0.5。 但是因为 MM_ISOTROPIC 映射模式决定 X轴和 Y轴的逻辑单位大小相同,所 以此时取 0.25 和0.5 中小的值 0.25 作为逻辑单位大小 ,这样可以计算出逻辑 坐标 (100, 200)的设备坐标 (xd, yd)为: xd = 100 * 0.25 = 25; yd = 200 * 0.25 = 50; 所以 在MM_ISOTROPIC映射模式下绘制的蓝色直线段应该从设备坐标 (0, 0)点到设备坐标 (25, 50)点。 为了验证我们以上的计算是否正确 ,在代码开始时 ,绘制了一条从 (25, 50) 到(25, 100)的直线段 。因为此时还没有设置其它的映射方式和选择画笔 ,所以 此时使用的映射方式为默认的 MM_TEXT,即逻辑坐标系与设备坐标系相同 。而 默认画笔为黑画笔。所以此时绘制的直线段为从设备坐标 (25, 50)到设备坐标 (25, 100)的黑色直线段 。如果我们计算正确的话 ,最终绘制的红色线段应该从 视图区的左上角 (设备坐标系的原点 )到黑色线段的下面的端点 ;而蓝色线段应 该从视图区的左上角(设备坐标系的原点)到黑色线段的上面的端点。 运行应用程序,结果如图 1.68 所示。此结果正如我们所料。 此处还有几点需要说明:在映射模式为MM_ISOTROPIC 时,如果没有调 用SetViewportExt 函数进行设置,此时窗口宽度采用屏幕的分辨率,即如果此 时系统屏幕分辨率为1280X1024,则cxv = 1280,而cyv = 1024;在映射 模式为 MM_ANISOTROPIC 时,如果没有调用 SetViewportExt 函数,则 cxv = 1,cyv = 1;如果没有调用SetWindowExt 函数,则cxw = 1,cyw = 1;如果 两个函数都没有调用 ,则表示 X轴和 Y轴方向上的逻辑单位大小都为 1,此时映 射模式导致的结果和 MM_TEXT 映射模式相同。 上面详细介绍了可变比例映射模式下的映射关系和坐标换算方法 ,其中 cxu 和cyu 可以称为坐标换算比例因子 。而在固定比例映射模式下 ,坐标变换方法与 之类似 ,只是坐标换算比例因子并不是由用户决定的 ,而是由实际屏幕和分辨率 决定。比如在MM_LOMETRIC 映射模式下,逻辑单位的大小为0.1mm,即表 示在此映射模式下 ,一个逻辑单位要占 0.1mm 的长度 。不同的屏幕和不同的分 辨率会导致 0.1mm的长度占有不同的像素数。所以在固定比例映射模式下 ,会 根据系统使用的显示屏幕的不同以及设置的分辨率的不同而有不同的换算比例 因 子值 。设 逻辑 坐标 为 (xl, yl), 设备 坐标 为 (xd, yd), 则固 定比 例映 射模 式下 的坐标变换公式为: xd = xl * cxu; yd = -yl * cyu; 这里需要注意的是,固定比例映射模式的Y轴正方向为向上,而设备坐标 系的 Y轴正方向为向下 。所以计算 yd 的时候要做正负号变换 ,而在可变比例映 射模式下,比例因子本身就包含了坐标轴方向信息,就不需要再变换正负了。 CDC 本身也提供了用于坐标变换的函数: � DPtoLP DPtoLP DPtoLP DPtoLP 函数,用于将设备坐标变换到逻辑坐标,其函数声明如下: void DPtoLP(LPPOINT lpPoints, int nCount = 1) const; 其中参数lpPoints 指向一个POINT 结构或CPoint 类数组的指针;参数 nCount 指定了要进行变换的点的个数。变换后的结果仍然存在POINT 结构或 CPoint 类中,并将原值替换掉。 � LPtoDP LPtoDP LPtoDP LPtoDP 函数,用于将逻辑坐标变换到设备坐标,其函数声明如下: void LPtoDP(LPPOINT lpPoints, int nCount = 1) const; 此函数的使用方法与 DPtoLP 函数相同 ,差别只在于此函数是从逻辑坐标变 换到设备坐标。当要转化的只为一个点时,也可以采用如下方法调用: CPoint p; pDC->DPtoLP(&p);//将p点坐标转换为逻辑坐标 pDC->LPtoDP(&p);//将p点坐标转换为设备坐标 这两个函数的换算结果受映射模式以及下面我们要介绍的设置坐标原点函 数的影响。 使用固定比例映射模式 ,绘制出的图形大小不会依赖于所使用的设备 ,可以 满足设备无关性的要求 。这些映射模式适用于要求屏幕输出对象的大小与其它输 出设备(比如打印机)上输出的对象大小要看起来一样的程序。 1.8.21.8.21.8.21.8.2 设置坐标系原点 在上面介绍的八种映射模式下 ,逻辑坐标系和设备坐标系的初始原点都在视 图区的左上角 。除了当视图类是从 CScrollView 类派生而来时 ,在用户滚动文档 后,MFC 会自动调整逻辑坐标系原点的相对位置外 。CDC 也提供函数 ,可以设 置逻辑坐标系和设备坐标系的原点位置。 � SetWindowOrg SetWindowOrg SetWindowOrg SetWindowOrg 函数 ,用于设置一个新的逻辑坐标系原点 ,其函数声明 如下: CPoint SetWindowOrg(int x, int y); CPoint SetWindowOrg(POINT point); 函数的参数指定了新的逻辑坐标系原点的位置坐标 。该函数返回设置新的逻 辑坐标系原点之前的原点坐标。 � SetViewportOrg SetViewportOrg SetViewportOrg SetViewportOrg 函数,用于设置一个新的设备坐标系原点,其函数声 明如下: CPoint SetViewportOrg(int x, int y); CPoint SetViewportOrg(POINT point); 函数的参数指定了新的设备坐标系原点的位置坐标 。该函数返回设置新的设 备坐标系原点之前的原点坐标。 假设在逻辑坐标系中有一点 (xl, yl)。调用 SetWindowOrg 函数指定新的逻 辑坐标系原点为 (xlo, ylo),则该点新的逻辑坐标 (xln, yln)为: xln = xl – xlo; yln = yl – ylo; 在MM_TEXT 映射模式下,如果没有更改设备坐标原点,则此时该点的设 备坐标就为 (xln, yln)。如果此时调用 SetViewportOrg 函数将设备坐标系原点设 置为 (xdo, ydo),则该点的设备坐标 (xd, yd)为: xd = xln + xdo; yd = yln + ydo; 如果此时映射模式为其它模式,假设X轴和Y轴的坐标变换比例因子分别 为cxu 和cyu,则该点的设备坐标 (xd, yd)为: xd = xln * cxu + xdo; yd = yln * cyu + ydo; 现在我们来看一个具体的例子,修改 OnDraw 函数,输入如下的代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //绘制标识矩形 pDC->Rectangle(125,150,150,250); //构造红色画笔 CPen pen(PS_SOLID,1,RGB(255,0,0)); //设置映射模式为 MM_ANISOTROPIC pDC->SetMapMode(MM_ANISOTROPIC); //设置窗口大小从而影响映射模式下的逻辑单位大小 pDC->SetWindowExt(200,200); pDC->SetViewportExt(50,100); //选择新的逻辑坐标原点为 (-100, -100) pDC->SetWindowOrg(-100,-100); //选择新的设备坐标原点为 (100, 100) pDC->SetViewportOrg(100,100); //选择画笔 pDC->SelectObject(&pen); //绘制逻辑坐标为 (0,0)到(100,200)的直线段 pDC->MoveTo(0,0); pDC->LineTo(100,200); pen.DeleteObject(); } 同样先来分析这段代码。在代码中设置映射方式为MM_ANISOTROPIC, 调用SetWindowExt 函数和SetViewportExt 函数传入的参数值和可变比例映射 模式示例中传入的参数值相同,可知有: cxu = 0.25; cyu = 0.5; 代码中设置新的逻辑坐标原点为 (-100, -100),即: xlo = -100; ylo = -100; 设置新的设备坐标原点为 (100, 100),即: xdo = 100; ydo = 100; 我们可以计算得到逻辑坐标 (0, 0)的设备坐标为: xd = (xl – xlo)* cxu + xdo = (0 –(-100)) * 0.25 + 100 = 125; yd = (yl – ylo)* cyu + ydo = (0 –(-100)) * 0.5 + 100 = 150; 同样可求得逻辑坐标 (100, 200)的设备坐标为: xd = (100 –(-100)) * 0.25 + 100 = 150; yd = (200 –(-100)) * 0.5 + 100 = 250; 所以要绘制的逻辑坐标为(0, 0)和(100, 200)的直线段对应的设备坐标 为(125, 150)和(150, 250)。为了确定我们计算的是否正确,在设置映射模 式之前,我们先绘制一个左上角点为(125, 150),右下角点为(150, 250)的 矩形。然后直线段使用红色画笔绘制。运行应用程序,结果如图 1.69 所示,其 运行结果证明计算是正确的。 1.8.31.8.31.8.31.8.3 设置背景方式和背景颜色 背景方式决定了使用设备环境对象绘图时 ,后绘制的图形和先绘制的图形如 果产生重叠,此时如何处理重叠部分。CDC 提供了SetBkMode 函数和 GetBkMode 函数分别用于设置背景模式和获得当前背景模式。 � SetBkMode SetBkMode SetBkMode SetBkMode 函数,用于设置背景模式,其函数声明如下: int SetBkMode(int nBkMode); 参数nBkMode 指定了背景模式,其值可以为OPAQUE(不透明)或者 TRANSPARENT(透明 )。设备环境对象的默认背景模式为 OPAQUE(不透明 )。 该函数返回设置新的背景模式之前的背景模式。 � GetBkMode GetBkMode GetBkMode GetBkMode 函数,用于获得当前的背景模式,其函数声明如下: int GetBkMode() const; 该函数返回值为 OPAQUE 和TRANSPARENT 之一。 在OPAQUE 背景模式下,后绘制的图形将在重叠部分覆盖先绘制的图形 。 在TRANSPARENT 背景模式下 ,如果后绘制的图形满足以下绘制条件 ,则我们 在图形的重叠部分可以透过后画的图形而看到先画的图形 。条件是 :后绘制的图 形是输出的文本,用阴影线画笔填充的区域,或使用非实线画笔(线型不是 PS_SOLID)绘制的线形图形 。以上情况中 ,我们可以透过文本矩形区域中没有 文字的空隙 ,阴影线填充的区域内阴影线之间的空隙 ,非实线画笔画出的直线段 中的空隙看到重叠的先绘制的图形。 设置背景颜色的函数 SetBkColor 我们在介绍文本输出函数时已经进行了介 绍,这里不再重复。SetBkColor 函数设置的背景颜色,不但会影响到文本输出 函数 ,也会对绘图函数的使用产生影响 。但是它只对符合要求条件的绘图函数所 绘制的图形产生影响 。它的要求和背景模式为 TRANSPARENT 时对绘图函数的 要求相同 。设备环境对象将使用设置的背景颜色填充文本矩形区域中没有文字的 空隙 ,阴影线填充的区域内阴影线之间的空隙 ,还有非实线画笔画出的直线段中 的空隙。同时背景颜色只有在背景模式为 OPAQUE 的时候才有效。 CDC 也提供了获得背景颜色的函数: � GetBkColor GetBkColor GetBkColor GetBkColor 函数,用于获得当前的背景颜色,其函数声明如下; COLORREF GetBkColor() const; 该函数返回设备环境对象的当前背景颜色。 现在看一个例子,修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //设置背景模式为 TRANSPARENT(透明) pDC->SetBkMode(TRANSPARENT); //设置背景颜色为蓝色 pDC->SetBkColor(RGB(0,0,255)); //构造阴影线画刷 1,阴影线类型为十字交叉线 CBrush brush1(HS_CROSS,RGB(255,0,0)); //构造阴影线画刷 2,阴影线类型为斜十字交叉线 CBrush brush2(HS_DIAGCROSS,RGB(0,255,0)); //构造虚线画笔 CPen pen(PS_DASH,1,RGB(255,0,255)); //选择虚线画笔 pDC->SelectObject(&pen); //选择阴影线画刷 1 pDC->SelectObject(&brush1); //用虚线画笔和阴影线画刷 1绘制椭圆 pDC->Ellipse(100,100,200,200); //选择阴影线画刷 2 pDC->SelectObject(&brush2); //用虚线画笔和阴影线画刷 2绘制矩形 pDC->Rectangle(125,150,300,300); //输出文本 pDC->TextOut(210,210,"SetBkMode"); //使用虚线画笔绘制直线段 pDC->LineTo(100,100); //删除创建的画笔和画刷 pen.DeleteObject(); brush1.DeleteObject(); brush2.DeleteObject(); } 该段代码创建了两种不同阴影线类型的阴影线画刷 ,分别使用它们先后绘制 了一个椭圆和一个矩形 ,两个图形有部分重叠 ,然后输出了一段文本 ,并绘制了 一条直线段。运行应用程序,结果如图 1.70 所示。 从结果可以看出 ,在TRANSPARENT 背景模式下 ,在椭圆和矩形的重叠部 分仍然可以看到前面的椭圆,而输出的文本也可以透过文字的空隙部分看到矩 形。但是在 TRANSPARENT 背景模式下,设置的背景颜色没有效果。 将上面代码中的设置背景模式的语句去掉 ,即在默认的 OPAQUE 背景模式 下执行相同的绘图代码。运行应用程序,结果如图 1.71 所示。 从结果可以看出 ,在OPAQUE 背景模式下 ,矩形覆盖了和椭圆的重叠部分 , 而输出的文本也以文本所处矩形区域覆盖了部分矩形 。但是此时设置的背景颜色 产生了效果 。输出文本的文本矩形区域被背景色蓝色填充 ,而椭圆和矩形区域中 阴影线的间隙部分也被蓝色填充。同时绘制的虚线段的空隙部分也被蓝色所填 充。 1.8.41.8.41.8.41.8.4 设置画弧方向 画弧方向是指在Arc,Pie,Ellipse 等函数中,绘制椭圆弧的方向是顺时针 方向还是逆时针方向。 � SetArcDirection SetArcDirection SetArcDirection SetArcDirection 函数,用于设置画弧方向,其函数声明如下: int SetArcDirection(int nArcDirection); 参数nArcDirection 指定了画弧方向,可取如下值之一: AD_COUNTERCLOCKWISE(逆时针方向 )和AD_CLOCKWISE(顺时针方向 )。 其中AD_COUNTERCLOCKWISE 为默认值。该函数返回设置新的画弧方向之 前的画弧方向。 � GetArcDirection GetArcDirection GetArcDirection GetArcDirection 函数,用于获得当前画弧方向,其函数声明如下: int GetArcDirection() const; 受画弧方向影响的绘图函数有 Arc、ArcTo、Pie、Chord、Ellipse、Rectangle、 RoundRect。在Ellipse,Rectangle 和RoundRect 函数中,画弧方向指定了边 界线的绘制方向 。在Arc、ArcTo、Pie、Chord 等函数中 ,画弧方向指定了截取 的椭圆弧线段是从起始点逆时针方向到终止点还是顺时针方向。 修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //设置画弧方向为逆时针方向 pDC->SetArcDirection(AD_COUNTERCLOCKWISE); //绘制椭圆弧 pDC->Arc(100,100,300,200,150,100,100,150); //设置画弧方向为顺时针方向 pDC->SetArcDirection(AD_CLOCKWISE); //构造红色画笔 CPen pen(PS_SOLID,1,RGB(255,0,0)); //选择画笔 pDC->SelectObject(&pen); //绘制椭圆弧 pDC->Arc(100,100,300,200,150,100,100,150); //删除画笔 pen.DeleteObject(); } 运行应用程序,结果如图 1.72 所示。 代码在两种画弧方向下 ,调用的 Arc 函数传入了相同的参数 ,结果正好绘制 出完整的椭圆。为了区分两个椭圆弧,我们使用了不同颜色的画笔进行绘制。 1.8.51.8.51.8.51.8.5 设置绘图模式 绘图模式指定了画笔颜色以及要填充封闭区域内部的颜色和已经显示的颜 色是如何进行混和的 。假设在视图区中一像素点已经具有了颜色 oldColor(该颜 色由绘图函数绘制图形导致 ),此时还要在该像素点绘制颜色 sColor(该绘制由 绘图函数引起),则绘图模式决定了在这个像素点最终要显示的颜色newColor 是如何得来的。 � SetROPSetROPSetROPSetROP2222函数,用于设置绘图模式,其函数声明如下: int SetROP2(int nDrawMode); 该函数返回设置新的绘图模式之前的绘图模式 。参数 nDrawMode 指定了新 的绘图模式,可以取下列值之一: 参数值 说明 R2_BLACK 像素总为黑色, newColor = 黑色 R2_WHITE 像素总为白色, newColor = 白色 R2_NOP 像素颜色不变, newColor = oldColor R2_NOT 像素为原有颜色反色, newColor = NOT oldColor R2_COPYPEN 像素为要绘制颜色, newColor = sColor R2_NOTCOPYPEN 像素为要绘制颜色的反色, newColor = NOT sColor R2_MERGEPENNO T 像素为要绘制颜色和原有颜色的反色的或运算, newColor = (NOT oldColor)OR sColor R2_MASKPENNOT 像素为要绘制颜色和原有颜色的反色的与运算, newColor = (NOT oldColor)AND sColor R2_MERGENOTPE N 像素为原有颜色和要绘制颜色的反色的或运算, newColor = (NOT sColor)OR oldColor R2_MASKNOTPEN 像素为原有颜色和要绘制颜色的反色的与运算, newColor = (NOT sColor)AND oldColor R2_MERGEPEN 像素为原有颜色和要绘制颜色的或运算,newColor = oldColor OR sColor R2_NOTMERGEPE N 像素为原有颜色和要绘制颜色的或运算颜色的反色, newColor = NOT(oldColor OR sColor) R2_MASKPEN 像素为原有颜色和要绘制颜色的与运算,newColor = oldColor AND sColor R2_NOTMASKPEN 像素为原有颜色和要绘制颜色的与运算颜色的反色, newColor = NOT(oldColor AND sColor) R2_XORPEN 像素为原有颜色和要绘制颜色的异或运算, newColor = oldColor XOR sColor R2_NOTXORPEN 像素为原有颜色和要绘制颜色的异或运算颜色的反色, newColor = NOT(oldColor XOR sColor) 其中R2_COPYPEN 为默认值,即像素颜色为新要绘制的颜色。在使用设 备环境对象进行绘图的时候 ,绘制的图形最终显示的颜色由选用的绘图工具和绘 图模式共同控制。 现在我们看一个简单的例子,修改 OnDraw 函数,输入如下代码: void CDrawTestView::OnDraw(CDC* pDC) { CDrawTestDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //TODO: add draw code for native data here //构造红色画笔 CPen pen(PS_SOLID,1,RGB(255,0,0)); //选择红色画笔 pDC->SelectObject(&pen); //设置绘图模式为 R2_BLACK pDC->SetROP2(R2_BLACK); //绘制椭圆 pDC->Ellipse(100,100,200,200); //设置绘图模式为 R2_NOT pDC->SetROP2(R2_NOT); //绘制直线段 pDC->MoveTo(150,50); pDC->LineTo(200,50); //删除画笔 pen.DeleteObject(); } 运行应用程序,结果如图 1.73 所示。 在代码中,虽然构造了红色画笔并且选用了,但是因为在绘制椭圆之前 ,设 置了绘图模式为 R2_BLACK(像素颜色为黑色 ),所以绘制的椭圆为黑色实心圆 ; 而在绘制直线段之前设置绘图模式为 R2_NOT(原有颜色的反色 ),所以没有绘 制出红色直线段,而是绘制了原有颜色白色的反色黑色的直线段。 � GetROPGetROPGetROPGetROP2222函数 ,用于获得设备环境对象当前绘图模式 ,其函数声明为 : int GetROP2() const; 该函数返回设备环境对象当前正在使用的绘图模式。 1.8.61.8.61.8.61.8.6 其它绘图属性函数及较常用的 CDC CDC CDC CDC 成员函数 � SetPolyFillMode SetPolyFillMode SetPolyFillMode SetPolyFillMode 函数 ,用于设置多边形的填充方式 ,其函数声明如下 : int SetPolyFillMode(int nPolyFillMode); 参数 nPolyFillMode 指定了多边形的填充方式 ,可选值为 ALTERNATE(默 认值 )或WINDING。在ALTERNATE 填充方式下 ,系统将填充每个扫描线上的 奇数和偶数边之间的区域 ;在WINDING 填充方式下 ,系统将按照多边形的绘制 方向判断区域是否进行填充。该函数返回设置新的填充方式之前的填充方式。 � GetPolyFillMode GetPolyFillMode GetPolyFillMode GetPolyFillMode 函数 ,用于获得设备环境对象的当前多边形填充方式 , 其函数声明如下: int GetPolyFillMode() const; 函数返回值为 ALTERNATE 或WINDING 之一。 � GetCurrentPosition GetCurrentPosition GetCurrentPosition GetCurrentPosition 函数,用于获得设备环境对象的当前绘图位置 ,其 函数声明如下: CPoint GetCurrentPosition() const; 返回的 CPoint 类对象中存放设备环境对象的当前绘图位置坐标。 � GetCurrentPen GetCurrentPen GetCurrentPen GetCurrentPen 函数 ,用于获得设备环境对象的当前画笔的指针 ,其函 数声明如下: CPen* GetCurrentPen() const; � GetCurrentBrush GetCurrentBrush GetCurrentBrush GetCurrentBrush 函数,用于获得设备环境对象的当前画刷的指针 ,其 函数声明如下: CBrush* GetCurrentBrush() const; � GetCurrentFont GetCurrentFont GetCurrentFont GetCurrentFont 函数,用于获得设备环境对象的当前字体的指针,其 函数声明如下: CFont* GetCurrentFont() const; � GetTextColor GetTextColor GetTextColor GetTextColor 函数,用于获得设备环境对象的当前文本输出的颜色 ,其 函数声明如下: COLORREF GetTextColor() const; � GetTextExtent GetTextExtent GetTextExtent GetTextExtent 函数,用于获得指定文本的宽度和长度,其函数声明如 下: CSize GetTextExtent(LPCTSTR lpszString, int nCount) const; CSize GetTextExtent(const CString& str) const; 第一个函数的参数 lpszString 指定了一个文本字符串 ,参数 nCount 指定了 要获取宽度和长度的文本字符串中的字符数 。第二个函数参数指定了要获取宽度 和长度的文本。获取的文本的宽度和长度存储在函数返回的 CSize 类对象中。 1.91.91.91.9 小结 本章介绍了 MFC 绘图的基础知识 ,包括设备对象 ,视图类的 OnDraw 函数 , 以及视图重画 。希望读者能学会如何使用绘图函数和文本输出函数绘制图形和输 出文本信息 ;懂得如何使用画笔类 CPen,画刷类 CBrush 以及字体类 CFont 等 绘图工具 ;掌握如何设置绘图属性 ;同时了解如何创建 MFC 项目以及如何编辑 菜单和为菜单连接处理函数。 本章的演示程序只创建了一个 MFC 项目 ,除了添加的菜单和编写的处理函 数,以及在视图类中添加的几个成员函数外 ,其它的示例程序都是通过修改视图 类的 OnDraw 函数完成的,而没有每一个示例都创建一个 MFC 项目。另外 ,为 了使读者能够更清楚地看到视图区中绘制的图形 ,绝大多数示例中的应用程序运 行结果图示中的窗口都修改了大小。
还剩158页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 13 人已下载

下载pdf

pdf贡献者

hlf880217

贡献于2011-03-03

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf