版权信息

书名：Visual C++从入门到精通（第5版）
作者：明日科技
出版社：清华大学出版社
出版时间：2019-11-01
ISBN：9787302535867

版权所有·侵权必究

内容简介

《Visual C++从入门到精通（第5版）》从初学者的角度出发，以通俗易懂的语言，配合丰富多彩的实例，详细介绍了使用Visual C++ 6.0进行程序开发需要掌握的知识。全书共分20章，包括Visual C++ 6.0集成开发环境，C++语言基础，语句，面向过程程序设计，面向对象程序设计，对话框应用程序设计，常用控件，菜单，工具栏和状态栏，高级控件，自定义MFC控件，文本、图形、图像处理，文档与视图，打印技术，文件与注册表操作，ADO编程，动态链接库，多线程程序设计，网络套接字编程，图像处理系统。书中所有知识都结合具体实例进行介绍，涉及的程序代码给出了详细的注释，可以使读者轻松领会Visual C++ 6.0程序开发的精髓，快速提高开发技能。另外，本书除了纸质内容之外，配书资源包中还给出了海量开发资源库，主要内容如下。

微课视频讲解：总时长17小时，共97集

实例资源库：881个实例及源码详细分析

模块资源库：15个经典模块开发过程完整展现

项目案例资源库：15个企业项目开发过程完整展现

测试题库系统：616道能力测试题目

面试资源库：371个企业面试真题

PPT电子教案

本书可作为软件开发入门者的自学用书，也可作为高等院校相关专业的教学参考书，还可供开发人员查阅、参考。

付费二维码

如何使用本书开发资源库

在学习《Visual C++从入门到精通（第5版）》时，随书资源包提供了“Visual C++开发资源库”系统，可以帮助读者快速提升编程水平和解决实际问题的能力。本书和Visual C++开发资源库配合学习流程如图1所示。

图1　图书与开发资源库配合学习流程图

打开资源包的“Visual C++开发资源库”文件夹，运行“Visual C++开发资源库.exe”程序，即可进入“Visual C++开发资源库”系统，主界面如图2所示。

图2　Visual C++开发资源库主界面

在学习本书某一章节时，可以配合实例资源库的相应章节，利用大量热点实例和关键实例巩固所学编程技能，提高编程兴趣和信心；也可以配合能力测试题库的对应章节进行测试，检验学习成果，具体流程如图3所示。

图3　使用实例资源库和能力测试题库

对于数学逻辑能力和英语基础较为薄弱的读者，或者想了解个人数学逻辑思维能力和编程英语基础的读者，本书提供了数学及逻辑思维能力测试和编程英语能力测试供练习和测试，如图4所示。

图4　数学及逻辑思维能力测试和编程英语能力测试目录

本书学习完成后，读者可以配合模块资源库和项目资源库的30个模块和项目，全面提升个人综合编程技能和解决实际开发问题的能力，为成为Visual C++软件开发工程师打下坚实基础。具体模块和项目目录如图5所示。

万事俱备，该到软件开发的主战场上接受洗礼了。面试资源库提供了大量国内外软件企业的常见面试真题，同时还提供了程序员职业规划、程序员面试技巧、企业面试真题汇编和虚拟面试系统等精彩内容，是程序员求职面试的绝佳指南。面试资源库具体内容如图6所示。

图5　模块资源库和项目资源库目录

图6　面试资源库具体内容

前言

丛书说明：“软件开发视频大讲堂”丛书（第1版）于2008年8月出版，因其编写细腻，易学实用，配备海量学习资源和全程视频等，在软件开发类图书市场上产生了很大反响，绝大部分品种在全国软件开发零售图书排行榜中名列前茅，2009年多个品种被评为“全国优秀畅销书”。

“软件开发视频大讲堂”丛书（第2版）于2010年8月出版，第3版于2012年8月出版，第4版于2016年10月出版。十年锤炼，打造经典。丛书迄今累计重印500多次，销售200多万册。不仅深受广大程序员的喜爱，还被百余所高校选为计算机、软件等相关专业的教学参考用书。

“软件开发视频大讲堂”丛书（第5版）在继承前4版所有优点的基础上，进一步修正了疏漏，优化了图书内容，更新了开发环境和工具，并根据读者建议替换了部分学习视频。同时，提供了从“入门学习→实例应用→模块开发→项目开发→能力测试→面试”等各个阶段的海量开发资源库，使之更适合读者学习、训练、测试。为了方便教学，还提供了教学课件PPT。

Visual C++ 6.0是由Microsoft公司推出的基于Windows环境的一种面向对象的可视化编程语言。利用Visual C++ 6.0可以开发出具有良好的交互功能、兼容性和扩展性的应用程序。利用Visual C++ 6.0不但可以开发数据库管理系统，还可以开发集声音、动画、视频为一体的多媒体应用程序和网络应用程序，这使得Visual C++ 6.0成为目前应用最广泛的编程语言之一。

本书内容

本书提供了从入门到编程高手所必需的各类知识，共分4篇。

第1篇：基础知识。本篇介绍了Visual C++ 6.0集成开发环境、C++语言基础、语句、面向过程程序设计以及面向对象程序设计，并结合大量的图示、实例、视频等，使读者快速掌握Visual C++语言，为以后的编程奠定坚实的基础。

第2篇：核心技术。本篇介绍对话框应用程序设计，常用控件，菜单，工具栏和状态栏，高级控件，自定义MFC控件，文本、图形、图像处理，文档与视图等内容。学习完本篇，读者将能够开发一些小型应用程序。

第3篇：高级应用。本篇主要介绍打印技术、文件与注册表操作、ADO编程、动态链接库、多线程程序设计和网络套接字编程等内容。学习完本篇，读者将能够开发数据库应用程序、多线程程序和网络程序等。

第4篇：项目实战。本篇通过一个完整、大型的图像处理系统，运用软件工程的设计思想，学习如何进行软件项目的开发。书中按照编写“开发背景→需求分析→系统设计→公共模块设计→主窗体设计→图像旋转模块设计→图像缩放模块设计→图像水印效果模块设计→PSD文件浏览模块设计→照片版式处理模块设计→开发技巧与难点分析”的流程进行介绍，带领读者一步一步亲身体验开发项目的全过程。

本书的大体结构如下图所示。

本书特点

由浅入深，循序渐进：本书以初中级程序员为对象，先从C++语言基础学起，再学习Visual C++的核心技术，然后学习Visual C++的高级应用，最后学习开发一个完整项目。讲解过程中步骤详尽、版式新颖，在操作的内容图片上以❶❷❸……的编号+内容的方式进行标注，使读者在阅读时一目了然，从而快速把握书中内容。

微课视频，讲解详尽：为便于读者直观感受程序开发的全过程，书中大部分章节都配备了教学微视频，可观看学习，能快速引导初学者入门，感受编程的快乐和成就感，进一步增强学习的信心。

实例典型，轻松易学：通过例子学习是最好的学习方式。本书通过“一个知识点、一个例子、一个结果、一段评析、一个综合应用”的模式，透彻详尽地讲述了实际开发中所需的各类知识。另外，为了便于读者阅读程序代码，快速提高编程技能，书中几乎每行代码都提供了注释。

精彩栏目，贴心提醒：本书根据需要在各章使用了很多“注意”“说明”和“技巧”等小栏目，以让读者在学习过程中更轻松地理解相关知识点及概念，更快地掌握个别技术的应用技巧。

应用实践，随时练习：书中几乎每章都提供了“实践与练习”，读者通过独立思考和解决问题，重新回顾、熟悉所学的知识，举一反三，打造超强实战能力。

读者对象

初学编程的自学者

编程爱好者

大中专院校的老师和学生

相关培训机构的老师和学员

毕业设计的学生

初、中级程序开发人员

程序测试及维护人员

参加实习的“菜鸟”程序员

读者服务

本书所附赠的各类学习资源，读者可登录清华大学出版社网站（www.tup.com.cn），在对应图书页面下获取其下载方式。也可扫描下方“文泉云盘”二维码，获取其下载方式。

文泉云盘

获取视频|源码|资源

为了方便解决本书疑难问题，读者朋友可加我们的企业QQ：4006751066（可容纳10万人），也可以登录www.mingrisoft.com留言，我们将竭诚为您服务。

致读者

本书由明日科技Visual C++程序开发团队组织编写，明日科技是一家专业从事软件开发、教育培训以及软件开发教育资源整合的高科技公司，其编写的教材既注重选取软件开发中的必需、常用内容，又注重内容的易学、方便以及相关知识的拓展，深受读者喜爱。其编写的教材多次荣获“全行业优秀畅销品种”“中国大学出版社优秀畅销书”等奖项，多个品种长期位居同类图书销售排行榜的前列。在编写本书的过程中，我们始终本着科学、严谨的态度，力求精益求精，但错误、疏漏之处在所难免，敬请广大读者批评指正。

感谢您购买本书，希望本书能成为您编程路上的领航者。

“零门槛”编程，一切皆有可能。

祝读书快乐！

编　者
2019年6月

资源包“开发资源库”目录

第1大部分　实例资源库

（881个完整实例分析，资源包路径：开发资源库／实例资源库）

第2大部分　模块资源库

（15个经典模块，资源包路径：开发资源库／模块资源库）

第3大部分　项目资源库

（15个企业开发项目，资源包路径：开发资源库／项目资源库）

第4大部分　能力测试资源库

（616 道能力测试题目，资源包路径：开发资源库／能力测试）

第5大部分　面试系统资源库

（371道面试真题，资源包路径：开发资源库／面试系统）

第1篇

基础知识

第1章　Visual C++ 6.0集成开发环境

第2章　C++语言基础

第3章　语句

第4章　面向过程程序设计

第5章　面向对象程序设计

本篇介绍了Visual C++ 6.0集成开发环境、C++语言基础、语句、面向过程程序设计以及面向对象程序设计等基础开发知识，并结合大量的图示、实例、视频等，读者可快速掌握Visual C++语言，为以后的编程奠定坚实的基础。

第1章

Visual C++ 6.0集成开发环境

（视频讲解：30分钟）

Visual C++ 6.0是由Microsoft公司推出的可视化开发环境，是Windows旗下最优秀的程序开发工具之一。利用Visual C++ 6.0可以开发出具有良好交互功能、兼容性和扩展性的应用程序。

本章致力于使读者了解Visual C++ 6.0的开发环境，掌握Visual C++ 6.0集成开发环境中各部分的应用，知道如何设置需要的开发环境，并能编写一个简单的应用程序。

通过阅读本章，您可以：

了解Visual C++ 6.0的特点

掌握Visual C++ 6.0开发环境中各部分的应用

掌握定制Visual C++ 6.0开发环境的方法

掌握应用程序的创建流程

1.1　Visual C++ 6.0概述

Visual C++ 6.0是由Microsoft公司推出的基于Windows系统的可视化集成开发环境。同其他可视化集成开发环境一样，Visual C++ 6.0集程序的代码编辑、编译、连接和调试等功能于一体，再加上Microsoft公司为Visual C++ 6.0开发的功能强大的MFC（Microsoft Foundation Class，微软基础类库），使Visual C++ 6.0成为开发Windows应用程序的最佳选择。

Visual C++ 6.0提供了对面向对象技术的支持，利用类将与用户界面设计有关的Windows API函数封装起来，通过MFC类库的方式提供给开发人员，大大提高了程序代码的可重用性；Visual C++ 6.0还提供了功能强大的应用程序生成向导（App Wizard），能够帮助用户自动生成一个应用程序框架，用户只要在该框架的适当位置添加代码就可以得到一个满意的应用程序。

1.2　Visual C++ 6.0开发环境介绍

在使用Visual C++ 6.0开发应用程序之前，需要了解Visual C++ 6.0的集成开发环境。本节将主要介绍Visual C++ 6.0的集成开发环境。

1.2.1　熟悉Visual C++ 6.0 IDE开发环境

Visual C++ 6.0 IDE（Integrated Development Environment）开发环境拥有友好的可视化界面，并且布局非常紧凑，如图1.1所示。

从图1.1中可以看出，Visual C++ 6.0 IDE开发环境由标题栏、菜单栏、工具栏、工作区窗口、编辑窗口、输出窗口和状态栏7部分组成。下面介绍各部分的具体功能。

标题栏：显示当前项目名称和当前编辑文件的名称。

菜单栏：是Visual C++ 6.0的核心部分，所有的操作命令都可以在这里找到。默认的菜单栏相当于一个工具栏，因为它可以拖曳到开发环境的任意位置。

工具栏：通常包括一些常用的工具按钮。除了在图1.1中可以看到的Standard、WizardBar和Bulid MinBar外，Visual C++ 6.0还提供了Edit、Debug等12个工具栏。右击工具栏，可以弹出相关的工具栏快捷菜单。

工作区窗口：该窗口包括类视图（ClassView）、资源视图（ResourceView）和文件视图（FileView）3个选项卡。

编辑区窗口：用于显示当前编辑的C++程序文件及资源文件。

输出窗口：当编译、链接程序时，输出窗口会显示编译和链接的信息。如果进入程序调试状态，主窗口还将弹出一些调试窗口。

状态栏：用于显示当前的操作状态或所选择命令的提示信息等。

图1.1　Visual C++ 6.0 IDE开发环境

1.2.2　Visual C++ 6.0 IDE菜单介绍

Visual C++ 6.0 IDE集成开发环境的菜单栏中包括File、Edit、View、Insert、Project、Build、Tools、Window和Help 9个菜单，下面对这些菜单进行介绍。

1. File菜单

File菜单中包含用于对文件进行各种操作的命令，如图1.2所示。

图1.2　File菜单

File菜单中各命令的功能说明如表1.1所示。

表1.1　File菜单中各命令的功能说明

技巧

使用Open和Open Workspace命令都可以打开工程，但是使用Open命令打开工程时包括所有的文件类型，没有直接使用Open Workspace命令打开方便。

2. Edit菜单

Edit菜单中包含所有与文件编辑有关的命令（如复制、粘贴等操作），如图1.3所示。

Edit菜单中各命令的功能说明如表1.2所示。

表1.2　Edit菜单中各命令的功能说明

3. View菜单

View菜单用来改变窗口的显示方式，如图1.4所示。

图1.3　Edit菜单

图1.4　View菜单

View菜单中各命令的功能说明如表1.3所示。

表1.3　View菜单中各命令的功能说明

注意

只有在调试状态下才能通过Debug Windows命令激活相应的调试窗口。

4. Insert菜单

Insert菜单用于执行向当前工程中插入类、资源和文件等操作，如图1.5所示。

Insert菜单中各命令的功能说明如表1.4所示。

表1.4　Insert菜单中各命令的功能说明

5. Project菜单

Project菜单用于管理项目和工作区，如图1.6所示。

图1.5　Insert菜单

图1.6　Project菜单

Project菜单中各命令的功能说明如表1.5所示。

表1.5　Project菜单中各命令的功能说明

6. Build菜单

Build菜单中包含的命令用于编译、创建、调试及执行应用程序，如图1.7所示。

Build菜单中各命令的功能说明如表1.6所示。

表1.6　Build菜单中各命令的功能说明

注意

在调试状态下，Build菜单会被替换成Debug菜单。

7. Tools菜单

Tools菜单用于选择或定制集成开发环境中的一些实用工具，如图1.8所示。

图1.7　Build菜单

图1.8　Tools菜单

Tools菜单中各命令的功能说明如表1.7所示。

表1.7　Tools菜单中各命令的功能说明

8. Window菜单

Window菜单用于进行窗口布局的调整、窗口间的跳转、窗口的打开和关闭等操作，如图1.9所示。

Window菜单中各命令的功能说明如表1.8所示。

表1.8　Window菜单中各命令的功能说明

9. Help菜单

Help菜单为Visual C++ 6.0提供了大量详细的帮助信息，如图1.10所示。

图1.9　Window菜单

图1.10　Help菜单

Help菜单中各命令的功能说明如表1.9所示。

表1.9　Help菜单中各命令的功能说明

说明

在用户编辑对话框资源时，还会出现Layout菜单，该菜单中的命令主要用于对控件大小和位置进行操作，在后面章节的应用中会进行介绍。

1.2.3　Visual C++ 6.0 IDE工具栏介绍

工具栏是一种图形化的操作界面，与菜单栏一样也是开发环境的重要组成部分。工具栏中主要列出了在开发过程中经常使用的一些功能，具有直观和快捷的特点，熟练使用这些工具按钮将大大提高工作效率。在Visual C++ 6.0开发环境中包括12个标准工具栏，这些工具栏并不都显示在开发环境中，可以在工具栏上任意位置单击鼠标右键，然后在弹出的快捷菜单中选择要显示的工具栏，如图1.11所示。

下面介绍3个常用的工具栏。

图1.11　工具栏快捷菜单

1. Standard工具栏

Standard工具栏用于维护和编辑工作区的文本和文件，如图1.12所示。

图1.12　Standard工具栏

Standard工具栏中各命令按钮的功能说明如表1.10所示。

表1.10　Standard工具栏中各命令按钮的功能说明

技巧

通过按钮可以调用MSDN，但是搜索时需要输入要查询的内容。还有一种更简单的方法可以达到同样的效果，即在代码编辑器中选择要查询的内容，然后按F1键，即可快速调用MSDN进行搜索。

2. Build MiniBar工具栏

Build MiniBar工具栏用于运行程序和调试程序，如图1.13所示。

Build MiniBar工具栏中各命令按钮的功能说明如表1.11所示。

表1.11　Build MiniBar工具栏中各命令按钮的功能说明

3. Debug工具栏

Debug工具栏用于调试程序，如图1.14所示。

图1.13　Build MiniBar工具栏

图1.14　Debug工具栏

Debug工具栏中各命令按钮的功能说明如表1.12所示。

表1.12　Debug工具栏中各命令按钮的功能说明

1.2.4　Visual C++ 6.0工作区窗口介绍

Visual C++ 6.0工作区窗口包括ClassView（类视图）、ResourceView（资源视图）和FileView（文件视图）3个选项卡，下面分别进行介绍。

1. ClassView选项卡

ClassView选项卡用来显示当前工作区中所有的类、结构和全局变量，如图1.15所示。

ClassView选项卡提供了工程中所有类的层次列表，通过展开各个节点可以显示类中包含的细节。在层次列表的每个项目前面都有一个图标，每个项目对应的图标含义如表1.13所示。

图1.15　ClassView选项卡

表1.13　各项目对应的图标含义

当用户双击类或其成员的图标时，光标自动定位到类或其成员定义的起始位置。在任意类名上单击鼠标右键，将弹出一个快捷菜单，其中各命令的功能如图1.16所示。

注意

当用户在头文件中手动添加函数声明，而没有设置函数的实现代码时，在当前类节点下也会显示函数名，但是双击时无法跳转到指定位置。

2. ResourceView选项卡

ResourceView选项卡在层次列表中列出了工程中用到的资源。图标、位图等都可以作为资源使用，如图1.17所示。

图1.16　快捷菜单

图1.17　Resour ceView选项卡

在ResourceView选项卡中的节点上单击鼠标右键，在弹出的快捷菜单中选择Insert命令，将弹出Insert Resource对话框，如图1.18所示。

在Insert Resource对话框中，可以根据需要对资源进行操作。其中，New按钮用于新建资源，Import按钮用于导入资源，Custom按钮用于定制资源，Cancel按钮用于退出对话框。

3. FileView选项卡

FileView选项卡与ClassView选项卡非常相似，可以显示和编辑源文件和头文件，如图1.19所示。通过FileView选项卡更容易进入类定义的文件，使得打开资源文件和非代码文件更加简单。

图1.18　Insert Resource对话框

图1.19　FileView选项卡

在FileView选项卡中双击某一文件，将在开发环境的编辑窗口中打开对应的文件窗口。

1.2.5　Visual C++6.0控件面板介绍

控件是一个独立的程序模块，用户可以利用对话框编辑器通过交互操作来创建，然后通过控件的ID与程序相连，并进行调用。另外，用户也可以直接编写代码创建控件，但是需要编写大量的代码。使用控件不仅能使界面美观、标准，还可以大大减少编程的工作量。在Visual C++ 6.0的工具栏空白处单击鼠标右键，在弹出的快捷菜单中选择Controls命令将显示控件面板，如图1.20所示。

图1.20　控件面板

1.2.6　Visual C++ 6.0输出窗口介绍

Visual C++ 6.0输出窗口位于开发环境的下部，在执行编译、连接和调试等操作时将显示相关的信息，如图1.21所示。

图1.21　输出窗口

在输出窗口中，数据根据不同的操作显示在不同的选项卡中。各选项卡的功能如表1.14所示。

表1.14　输出窗口中各选项卡的功能

说明

用户在进行编译、调试、查找等操作时，输出窗口会根据操作自动选择相应的选项卡进行显示，如果用户在编译过程中出现错误，只要双击错误信息，代码编辑器就会跳转到相应的错误代码处。

1.3　设置自己的开发环境

1.3.1　自定义工具栏

Visual C++ 6.0为用户提供了12个预定的工具栏，此外用户还可以根据需要自己定义工具栏。自定义工具栏的步骤如下。

（1）在Visual C++ 6.0开发环境中选择Tools/Customize命令，在打开的Customize对话框中选择Toolbars选项卡，如图1.22所示。

（2）单击New按钮，弹出New Toolbar对话框，在Toolbar name编辑框中输入工具栏名称，如图1.23所示。

（3）单击OK按钮，创建一个工具栏，新创建的工具栏名称为“工具栏”，如图1.24所示。

（4）在Customize对话框中选择Commands选项卡，在Category组合框中选择一个目录，如图1.25所示。

图1.22　Customize对话框

图1.23　New Toolbar对话框

图1.24　新建工具栏

（5）在Buttons栏中会显示相应的按钮图标，利用鼠标将其中的按钮拖动到新建的工具栏窗口中。根据需要在不同的目录中选择工具栏按钮，将这些按钮都拖动到工具栏窗口以后，单击Close按钮，就完成了新工具栏的创建。新创建的工具栏如图1.26所示。

图1.25　Commands选项卡

图1.26　新创建的工具栏

说明

如果用户想删除自己创建的工具栏，可以在Customize对话框中选择Toolbars选项卡，在工具栏列表框中选择要删除的工具栏，然后单击右侧的Delete按钮进行删除。

1.3.2　自定义代码编辑窗口

在Tools菜单中包含了许多编辑选项，合理地设置这些选项可以提高程序的编译速度，使程序代码更易于阅读和理解，程序开发更加得心应手。用户可以设置代码编辑器中字体的大小、颜色等信息，其中最主要也是开发人员经常设置的是数字、字符串和注释的颜色。下面就以设置这些信息为例来介绍如何自定义代码编辑窗口，步骤如下。

（1）在Visual C++ 6.0开发环境中选择Tools/Options命令，在打开的Options对话框中选择Format选项卡，如图1.27所示。

图1.27　Options对话框

（2）在Category列表框中选择Source Windows选项；在Colors列表框中选择Comment选项，表示设置注释的颜色。在Foreground组合框中设置注释的字体颜色，用户可以选择自己喜欢的颜色，本例选择绿色。另外，还可以为注释设置背景色，方法是在Background组合框中选择一种颜色，但是通常情况下不要设置背景色，否则代码编辑器会显得很凌乱。

（3）在Colors列表框中选择Number选项，表示设置数字的颜色。同样，在Foreground组合框中设置数字的颜色，本例选择蓝色。

（4）在Colors列表框中选择String选项，表示设置字符串的颜色。在Foreground组合框中为字符串选择一种颜色，本例选择红色。

（5）单击OK按钮完成设置，如图1.28所示。

图1.28　代码编辑窗口

1.4　创建一个简单的程序

【例1.1】一个简单的MFC应用程序。（实例位置：资源包\TM\sl\1\1）

通过前面的学习，读者已经对Visual C++ 6.0的开发环境有了大致的了解。下面将制作一个简单的MFC应用程序，希望通过这个程序使读者了解开发应用程序的一般过程。

1.4.1　创建工程

（1）在Visual C++ 6.0开发环境中选择File/New命令，弹出New对话框。在Projects选项卡中选择MFC AppWizard[exe]（MFC应用程序向导）选项，在Project name编辑框中输入创建的工程名“Hello”，在Location编辑框中设置工程文件存放的位置为“D:\桌面\LIZI\Hello”，如图1.29所示。

（2）单击OK按钮，弹出MFC AppWizard-Step 1窗口，如图1.30所示。

图1.29　New对话框

图1.30　MFC AppWizard-Step 1窗口

在MFC AppWizard-Step 1窗口中可以指定生成框架的类型。

Single document：生成单文档应用程序框架。

Multiple documents：生成多文档应用程序框架。

Dialog based：生成基于对话框的应用程序框架。

Document/View architecture support：选中该复选框，允许生成文档／视图和非文档／视图结构程序。

（3）本例选中Dialog based单选按钮，创建一个基于对话框的应用程序。单击Next按钮，弹出MFC AppWizard-Step 2 of 4窗口，如图1.31所示。

MFC AppWizard-Step 2 of 4窗口中各选项介绍如下。

About box：生成“关于”对话框。

Context-sensitive Help：生成支持上下文的帮助文件。

3D controls：生成具有3D效果的程序界面。

Automation：支持其他应用程序中实现的对象，自己的应用程序也可供Automation客户使用。

ActiveX Controls：支持ActiveX控件。

Windows Sockets：支持基于TCP/IP协议的网络通信。

Please enter a title for your dialog：设置应用程序主窗口的标题。

说明

如果用户没有其他的设置，可以直接单击Finish按钮完成创建。

（4）单击Next按钮，弹出MFC AppWizard-Step 3 of 4窗口，如图1.32所示。

图1.31　MFC AppWizard-Step 2 of 4窗口

图1.32　MFC AppWizard-Step 3 of 4窗口

MFC AppWizard-Step 3 of 4窗口中的各选项含义如下。

MFC Standard：标准MFC项目。

Windows Explorer：“Windows资源管理器”风格项目。

Yes，please：在源文件中添加注释。

No，thank you：不添加注释。

As a shared DLL：共享动态链接库。

As a statically linked library：静态链接库。

（5）单击Next按钮，弹出MFC AppWizard-Step 4 of 4窗口，如图1.33所示。

图1.33　MFC AppWizard-Step 4 of 4窗口

（6）在MFC AppWizard-Step 4 of 4窗口中确定类的名称及所在文件的名称。单击Finish按钮，弹出New Project Information窗口，如图1.34所示。

图1.34　New Project Information窗口

（7）在New Project Information窗口中显示了将要创建工程的文件清单，单击OK按钮完成工程的创建。创建的工程如图1.35所示。

图1.35　创建的工程

1.4.2　编辑程序

工程创建后会自动创建一个对话框，其中有两个“按钮”控件和一个“静态正文”控件，这是程序自动生成的控件。接下来实现新工程的编辑，步骤如下。

（1）调整控件的位置，并修改对话框的大小，如图1.36所示。

（2）单击“确定”按钮，为“确定”按钮处理单击事件，在该事件中为“静态正文”控件设置显示的文本和字体大小。代码如下：

（3）按F7键编译程序，按F5键执行程序，单击“确定”按钮。程序运行结果如图1.37所示。

图1.36　对话框

图1.37　程序运行结果

1.5　小结

本章主要讲解Visual C++ 6.0集成开发环境，对开发环境中的各个部分作了详细讲解，从中读者可以初步了解Visual C++ 6.0。最后通过创建一个简单的应用程序，向读者演示使用Visual C++ 6.0开发应用程序的整个过程。俗话说，“工欲善其事，必先利其器”，只有熟练掌握了开发环境这个利器，才能在以后的程序开发中事半功倍。

第2章

C++语言基础

（视频讲解：1小时40分钟）

C++语言诞生于20世纪80年代初，它在C语言的基础上提供了面向对象功能，可以说C++语言是C语言的扩展。C++语言集面向对象和面向过程于一身，因此它既适合于传统的面向过程程序开发，也适用于面向对象程序开发。本章将详细介绍C++语言的基础知识。

通过阅读本章，您可以：

了解C++文件的结构

了解C++语言的基本要素

掌握C++语言的数据类型

熟悉C++语言的基本运算符

应用C++语言的表达式

2.1　C++文件结构

C++程序主要包含两个文件，即头文件和源文件，本节主要介绍C++头文件和源文件的构成以及一些注意事项。

2.1.1　头文件的构成

在C++语言中，头文件通常以.h为扩展名。在头文件中通常包含3部分内容，即版权与版本的声明、宏定义和函数、类信息的声明。也可以这样理解，头文件就像是一个人的名片一样，名片是对人的介绍，而头文件是对文件所包含内容的介绍。这里单独介绍一些宏定义。在创建MFC应用程序时，类向导会为每个头文件添加宏定义。例如：

宏定义的目的是防止头文件被重复引用。当编译器编译头文件时，判断当前宏是否被定义，如果没有定义，则定义宏，并编译头文件，否则略过当前头文件。

在开发应用程序时，头文件的编写应遵守以下原则。

（1）引用头文件

使用<>格式引用系统的头文件。例如：

使用""格式引用自定义头文件。例如：

对于以<>格式引用的头文件，编译器会在系统库文件目录下搜索头文件，它不会搜索当前工程下的目录。对于以""格式引用的头文件，编译器首先在当前工程目录下搜索头文件，然后再搜索系统库文件目录。采用上述做法的好处是：①可以让其他开发人员知道引用的头文件是系统头文件还是自定义的头文件；②能够提高编译器的性能。

（2）头文件中只提供声明信息

C++语言允许使用内联函数（第4章4.1.6节将详细介绍），将函数的声明和实现放在一起。但是，这样做不容易形成统一风格，建议在头文件中只提供声明信息，在源文件中提供实现信息，使程序的逻辑结构更加清晰。

2.1.2　源文件的构成

C++语言的源文件通常以.cpp为扩展名。源文件中通常包含3部分内容，即源文件版权、版本的声明、对头文件的引用以及系统功能的实现代码。

在开发应用程序时，通常将头文件和源文件分别存储在不同的目录下，这样有利于文件的管理。通常，开发人员习惯将头文件存储在Include目录下，将源文件存储在Source目录下，这是一条约定俗成的规则，但不是必须的。

2.2　C++语言基本要素

程序设计语言的基本要素包括标识符、关键字、常量和变量等。本节将介绍C++语言的基本要素。如果读者熟悉C语言，可以跳过本节。

2.2.1　标识符

在C++语言中，变量、常量、函数、标签和用户定义的各种对象，被称为标识符。标识符由一个或多个字符构成。字符可以是字母、数字或下划线，但是标识符的首字符必须是字母或下划线，而不能是数字。例如，下面的标识符均是合法的。

而下面的标识符是非法的。

在C++语言中，标识符是区分大小写的。例如，“value”和“Value”是两个不同的标识符。此外，标识符不能与C/C++的关键字同名。

注意

C++语言中标识符的长度可以是任意的，但是通常情况下，前1024个字符是有意义的，这与C语言不同。在C语言中，标识符也可以是任意长度，但是在外部链接进程中调用该标识符时，通常前6个字符是有效的，如被多个文件共享的全局函数或变量。如果标识符不用于外部进程链接，通常前31个字符是有效的。

2.2.2　关键字

关键字是C++编译器内置的有特殊意义的标识符，用户不能定义与关键字相同的标识符。C++语言的关键字如表2.1所示。

表2.1　C++语言关键字

2.2.3　常量

常量，顾名思义，其值在运行时是不能改变的，但是在定义常量时可以设置初始值。在C++中，可以使用const关键字来定义一个常量。例如，下面的代码定义了一个MAX_VALUE常量。

对于常量，编译器会将其放置在一个只读的内存区域，其值不能被修改，但是可以应用在各种表达式中。如果用户试图修改常量，编译器将提示错误。

使用常量的最大好处是灵活。当程序中有多处需要使用一个常数值时，可以使用常量代替。当需要改动常数值时，只需要改动常量的值即可。此外，在定义函数时，如果在函数体中不需要修改参数值，建议将参数的类型定义为常量，这样当用户不小心在函数体内修改了参数值，编译器将提示错误信息。

2.2.4　变量

其值可以改变的量称为“变量”。变量提供了一个具有名称（变量名）的存储区域，使得开发人员可以通过名称来对存储区域进行读写。与常量不同的是，变量可以在程序中被随意赋值。对于每一个变量，都具有两个属性，也就是通常所说的左值和右值。所谓左值，是指变量的地址值，即存储变量值的内存地址。右值是指变量的数据值，即内存地址中存储的数据。

在程序中定义变量时，首先是变量的数据类型（2.3节将详细介绍），然后是变量名。如下面的代码定义了两个变量：

在定义变量时，可以对变量进行初始化，即为其设置初始值。例如，上面的代码定义了一个min整型变量，并将其初始化为0。在初始化变量时，可以将变量初始化为自身。例如：

这样做虽然是合法的，但也是“愚蠢”和不明智的。在初始化变量时，可以进行隐式初始化。例如：

当一条语句定义多个变量时，可以为多个变量同时指定初始值，并且后续变量可以利用之前变量作为初始值。例如：

说明

在用一条语句定义多个变量时，变量之间用逗号分隔，在最后一个变量定义结束后，以分号结束语句。

在定义变量时需要指定数据类型，对于C++内置的数据类型（2.3节将详细介绍）编译器会提供特殊的构造函数（第5章将详细介绍）。用户可以利用不同数据类型的构造函数来初始化变量。例如：

2.2.5　变量的存储类型

在C++语言中，变量通常有4种存储类型，分别为extern、static、register和auto，下面分别进行介绍。

1. extern存储类型

在介绍extern存储类型之前，先来澄清一个概念—变量的声明和定义。变量的声明是告知编译器变量的名称和数据类型；变量的定义将为变量分配存储区域。通常情况下，变量的声明也被认为是变量的定义，但是可以使用extern关键字只声明而不定义变量。例如：

那么只声明而不定义变量有何好处呢？通常应用程序可能包含许多文件，如果在一个文件中定义一个全局变量，可能需要在其他文件中进行访问，那么在其他文件中即可使用extern关键字只声明而不定义全局变量，extern关键字将告诉编译器变量的名称和类型，而变量的定义来源于前一个文件，这样即可在其他文件中共享全局变量。例如，在一个文件中定义一个整型的全局变量var。

而在另一个文件中通过使用extern关键字声明全局变量var，在该文件中即可访问全局变量var。

2. static存储类型

static存储类型表示变量在函数或文件内是“持久性”变量，通常也称之为静态变量。静态变量分为局部静态变量和全局静态变量。当使用static关键字标识一个局部变量（在函数内部定义的变量）时，该变量将被分派在一个持久的存储区域，当函数调用结束时，变量并不被释放，依然保留其值。当下一次调用函数时，将应用之前的变量值。这一点类似于全局变量，但是与全局变量不同的是，局部静态变量的作用域为当前的函数，它不能被外界函数或文件访问。可以认为局部静态变量是一个在函数调用后保留其值的局部变量。下面的代码演示了局部静态变量的作用。

【例2.1】局部静态变量的作用。（实例位置：资源包\TM\sl\2\1）

说明

printf函数用于将格式化数据写入标准输出流。其语法格式如下：

int printf ( const char *format [, argument]... );

其中，format表示输出格式；argument表示要输出的变量，以逗号分隔变量。

执行上述代码，结果如图2.1所示。

上述代码3次调用了TestStaticVar函数，在每次函数调用结束后，局部静态变量slocal都将保留，并且对于局部静态变量只初始化一次，因此每次调用TestStaticVar函数输出的值都是不同的。

对于全局静态变量，其作用域仅限于当前定义的文件，不能够被其他文件使用extern关键字访问。可以认为全局静态变量只是半个全局变量（不能够被其他文件共享）。

图2.1　局部静态变量

3. register存储类型

在定义变量时，使用register关键字，表示变量将被放置在CPU的寄存器中。与普通变量不同的是，访问register变量要比访问普通变量快得多。register变量只能用于局部变量或作为函数的形式参数，而不能定义全局的register变量。如下面的代码定义了一个register变量。

4. auto存储类型

变量的存储方式主要有两种，即静态存储和动态存储。全局变量、静态变量均属于静态存储，而普通的局部变量属于动态存储。auto关键字表示变量将被动态存储。默认情况下的局部变量均属于auto变量（也被称为自动变量）。定义一个全局的auto变量是非法的，因为全局变量属于静态存储，与auto变量是相互冲突的。

2.3　数据类型

C++语言中常用的数据类型包括数值类型、字符类型、数组类型、布尔类型、枚举类型、结构体类型、共用体类型、指针类型、引用类型和自定义类型。本节将详细介绍这些数据类型。

2.3.1　数值类型

C++语言中数值类型主要分为整型和实型（浮点类型）两大类。其中，整型按符号划分，可以分为有符号和无符号两大类；按长度划分，可以分为普通整型、短整型和长整型3类，如表2.2所示。

表2.2　整型类型

说明

表格中的[]为可选部分。例如，[signed] long [int]可以简写为long。

实型主要包括单精度型、双精度型和长双精度型，如表2.3所示。

表2.3　实型类型

在程序中使用实型数据时需要注意以下几点。

（1）实数的相加

实型数据的有效数字是有限制的，如单精度float的有效数字是6～7位，如果将数字86041238.78赋值给float类型，显示的数字可能是86041240.00，个位数“8”被四舍五入，小数位被忽略。如果将86041238.78与5相加，输出的结果为86041245.00，而不是86041243.78。

（2）实数与零的比较

在开发程序的过程中，经常会进行两个实数的比较，此时尽量不要使用“==”或“!=”运算符，而应使用“>=”或“<=”之类的运算符，许多程序开发人员在此经常犯错。例如：

上述代码并不是高质量的代码，如果程序要求的精度非常高，可能会产生未知的结果。通常在比较实数时需要定义实数的精度。例如：

【例2.2】利用实数精度进行实数比较。（实例位置：资源包\TM\sl\2\2）

2.3.2　字符类型

在C++语言中，字符数据使用“' '”来表示，如'A''B''C'等。定义字符变量可以使用char关键字。例如：

在计算机中字符是以ASCII码的形式存储的，因此可以直接将整数赋值给字符变量。例如：

输出结果为“a”，因为97对应的ASCII码为“a”。

2.3.3　数组类型

数组是指具有相同数据类型的一组元素的集合，数组中的元素是按顺序存储在内存中的。数组按维数划分，可以分为一维数组、二维数组和多维数组。

1. 一维数组

在C++语言中，一维数组的定义格式如下：

例如，下面的代码定义了一个具有10个元素的整型数组。

在定义数组后，还需要访问数组中的元素。数组元素是通过数组名和下标来访问的，例如：

上面的代码将数组array中的第2个元素值设置为1。

注意

数组的下标是从0开始的。array数组共包含10个元素，下标分别为0、1、2、…、9。如果出现array[10]，将会导致数组访问越界，发生意想不到的后果。编译器并不能识别此类错误，因此在访问数组元素时一定要谨慎，不要发生数组访问越界的情况。

在定义数组时，用户可以直接为数组赋初值。例如：

也可以只对部分元素赋初值。例如：

上面的代码只对array数组的前5个元素设置了初值。注意，不能给数组提供超过数组长度的初始值，例如下面的代码是不能通过编译的，将提示太多的初始值。

如果将数组元素全部初始化为0，用户可以简写为：

但是上述方式不能够将全部数组元素初始化为其他的值，例如将全部数组元素初始化为1。

上面的代码将导致第1个数组元素的值为1，其他数组元素的值为0。

如果需要对数组全部元素进行初始化，可以省略数组长度，但是数组下标符号“[]”不能省略。例如：

2. 二维数组

在C++语言中，二维数组的定义格式如下：

例如：

二维数组元素也是通过数组名和下标来访问的。例如：

可以认为二维数组是一个特殊的一维数组，只是数组中的每一个元素又是一个一维数组。例如，array[3][4]可以认为是一个一维数组array[3]，其中的每一个元素又是一个包含4个元素的一维数组。

在定义二维数组时也可以直接进行初始化。例如：

用户也可以在一个大括号内直接初始化所有的元素。例如：

但是并不提倡该方法，因为如果数组元素过多，将很难界定每一个元素。

与一维数组类似，二维数组也可以只对部分元素进行初始化。例如：

结果是对每一行第1个元素赋值，其他元素为0。

对于二维数组，还可以只对某一个元素或某一行赋值。例如：

在定义二维数组时，如果需要提供全部元素的初始值，可以省略第一维的长度，但是不能省略第二维的长度。例如：

注意

最后一行代码，只提供了11个元素的初始值，但是数组array却包含12个元素，最后一个元素被初始化为0。

2.3.4　布尔类型

在逻辑判断中，结果通常只有真和假两个值。C++语言中提供了布尔类型bool来描述真和假。bool类型共有两个取值，分别为true和false。顾名思义，true表示真，false表示假。在程序中，bool类型被作为整数类型对待，false表示0，true表示1。将bool类型赋值给整型是合法的，反之，将整型赋值给bool类型也是合法的。例如：

2.3.5　枚举类型

在开发程序时，一个对象可能会存在多个状态。例如，Oracle数据库具有关闭、打开、装载、卸载等状态。如果直接在程序中使用0表示关闭状态，1表示打开状态……会使得代码难以阅读。有些用户定义了有意义的常量来表示各个状态，但是在涉及具体函数调用时，无法限制只允许使用“0、1、2、3”。

枚举类型提供了解决上述问题的最好方法。枚举类型提供了一组常量的集合。在定义函数时，将函数参数设置为枚举类型，这样可以限制调用函数必须提供枚举类型中的某个常量，而不能随意输入一个整数。在C++语言中，可以使用enum关键字定义枚举类型。定义格式如下：

使用enum关键字定义一个枚举类型，例如：

在定义枚举类型时，可以为各个常量提供一个整数值，如果没有提供整数值，默认第1个常量值为0，第2个常量值为1，以此类推。例如上面的代码中，CLOSE常量的值为0，OPEN的值为1……

下面为枚举类型设置常量值。例如：

在上面的代码中，将枚举常量CLOSE设置为1，MOUNT设置为4。那么OPEN和UNMOUNT的值是多少呢？由于没有为OPEN和UNMOUNT提供常数值，它们的值应为前一个常量值加1，即OPEN和UNMOUNT的值分别为2和5。下面来演示一下枚举类型的实际应用。

【例2.3】应用枚举类型。（实例位置：资源包\TM\sl\2\3）

2.3.6　结构体类型

结构体是一组变量的集合。与数组不同，结构体中的变量可以有各种类型。通常将一组密切相关的信息组合为一个结构体，以描述一个对象。例如，描述学生信息，包括姓名、性别、年龄、地址等信息，可以定义一个结构体来描述学生的所有信息。

【例2.4】定义结构体类型。

其中，关键字struct用于声明一个结构体类型。结构体中的变量被称为成员，如name、sex等。

注意

在声明结构体时，不要忘记末尾的分号。

在声明一个结构体后，可以定义一个结构变量。在C语言中定义结构变量的语法格式如下：

例如，下面的代码采用C语言的形式定义结构体变量。

在C++语言中定义结构体变量的格式与定义普通变量的格式相同。例如：

当定义一个结构体变量时，编译器将为变量分配足够的空间以容纳结构体中所有的成员。在声明结构体类型时，也可以直接定义结构体变量。例如：

【例2.5】定义结构体类型时直接定义结构体变量。

上述代码在声明结构体Student的同时，定义了一个结构体变量stdnt。此外，在定义结构体时，如果只需要定义一次结构体变量（在其他地方不需要定义该类型的结构体变量），可以不写结构体类型的名称，而只给出结构体变量。例如：

在使用结构体时，需要访问结构体中的各个成员。可以使用“. ”符号来访问结构体中的成员。例如：

两个整型变量可以相互赋值，那么两个结构体变量能否直接赋值呢？答案是可以的。观察如下代码。

【例2.6】结构体变量之间的赋值。（实例位置：资源包\TM\sl\2\4）

执行上述代码，结果如图2.2所示。

从图2.2中可以发现，another变量的age成员与stdnt变量的age成员是相同的；不仅如此，这两个变量的其他成员数据也相同。

在定义结构体变量时，编译器会为变量分配足够的空间以容纳结构体的所有成员。如果定义如下的一个结构体变量，编译器将为其分配多大的空间呢？

图2.2　结构体变量赋值

分析结构体成员，其中memOne类型为double，占用8个字节；memTwo类型为char，占用1个字节；memThree类型为int，占用4个字节。在定义结构体ByteAlign的变量时，应分配13个字节。但实际使用sizeof函数测试时，发现结构体ByteAlign的变量占用了16个字节。究竟是如何多出3个字节的呢？这涉及结构体的字节对齐问题。编译器在为结构体变量分配空间时，保证下一个成员的偏移量应为该成员数据类型长度的整数倍。分析一下ByteAlign结构在内存中的简单布局。首先为memOne成员分配空间，假设起始位置从0开始，memOne成员将占用0、1、2、3、4、5、6、7共8个字节。接下来为成员memTwo分配空间，由于char类型占用1个字节，因此，memTwo将占据8的位置，因为当前位置8与4是整除的。接下来为memThree成员分配空间，该成员为int类型，占用4个字节。当前位置为9，并不是4的整数倍，因此需要空出3个字节（9、10、11），memTree从12的位置开始分配4个字节的空间。这样就导致了实际分配的大小与“理论上”的大小不一致。

在开发应用程序时，有时需要在一个字节中表示多项内容。例如，在描述IP协议的首部时，其首部长度占4位（bit），版本号占4位，在定义描述IP协议首部的结构体时，该如何实现呢？与其他计算机语言不同，C/C++语言提供了位域，允许用户单独访问一位数据。例如，下面的代码定义了一个IP结构体，用于描述首部长度和版本号。

其中，headerlen成员类型为无符号字符型，理应占1个字节（8位），通过使用位域符号“：”和长度4，headerlen成员只占了“半”个字节—4位。在定义位域字段时，也可以不指定成员名称，这样可以预留一些空间。例如：

用户在访问memTwo成员时，将直接从一个字节的第5位开始读取数据。

2.3.7　共用体类型

共用体类型提供了一种机制，使得多个变量（共用体中的成员）可以共享同一个内存地址。下面的代码定义了一个共用体类型unType。

【例2.7】定义共用体类型。

定义共用体与定义结构体类似，只是关键字不同，共用体使用union关键字。在共用体unType中，成员cdata与idata的内存起始位置相同，如图2.3所示。

由于共用体成员共用内存空间，因此如果视图改变了一个共用体成员的值，其他成员的值也会发生改变。但是，对于共用体来说，通常一次只需要使用一个成员。当定义一个共用体变量时，编译器会根据共用体成员中占用最多内存空间的变量分配空间，这样使得共用体中的所有成员都能够获得足够的空间。例如，定义一个unType类型的变量tg，编译器将为其分配4个字节的空间，因为idata需要4个字节的空间，cdata只需要1个字节的空间。

图2.3　共用体成员内部布局示意图

注意

共用体的内存空间可以用来存放数种不同类型的成员，但是在某一时刻只有一个成员起作用，起作用的成员是最后一次存放的成员。

2.3.8　指针类型

在C++语言的数据类型中，指针类型是最难掌握的，也是最重要的数据类型之一。灵活地应用指针，能够提高代码的执行效率。在介绍指针之前，先来回顾一下变量的属性。变量具有左值和右值两个属性，即变量的地址和变量的实际数据。指针是用来存放变量的地址的，即指针的值能够存储变量的地址。直接使用变量时，就称之为直接访问，而通过指针获得变量地址进行使用的方式被称为间接访问。这就像将一个物品放在银行的保险箱里，如果将钥匙带在身上，需要时直接打开保险箱拿东西，这就是直接访问。而为了安全起见，将保险箱的钥匙锁在家中的抽屉里，当你要拿东西时，首先要回家取得保险箱的钥匙，然后再使用钥匙打开保险箱，这就相当于用指针调用变量的地址所进行的间接访问。

如果一个指针存储了变量的地址，那么通过指针就能够访问到变量的值，因为变量的值是存储在变量的地址上的。假设有变量var，指向变量var的指针为pavr，如图2.4所示描述了指针与变量的关系。

图2.4　指针与变量的关系

在C++中，定义指针的语法格式如下：

例如，下面的代码定义了一个整型的指针变量。

只定义一个指针变量是没有意义的，还需要为指针变量赋值。指针变量的值应该是一个有意义的地址（通常是某个变量的地址），而不是数据。如何将变量的地址赋值给指针变量呢？C++中可以使用“&”运算符来获得变量的地址。下面的代码演示了通过“&”运算符获得变量地址，并将其赋值为指针变量。

当指针被赋予一个有效的变量地址后，如何通过指针访问或修改变量的数据呢？在指针变量前使用“*”运算符，即可直接访问变量的数据。

【例2.8】使用“*”运算符访问指针数据。（实例位置：资源包\TM\sl\2\5）

执行代码，结果如图2.5所示。

分析上述代码，首先定义了一个整型变量ivar，将其初始化为10；然后定义一个指针变量pvar，将其初始化为ivar的地址；接着修改指针变量指向的地址上的数据；最后输出变量ivar的值和指针变量pvar的值。从图2.5中可以发现，变量ivar的值为8，表明通过指针变量修改了ivar的值。

指针变量不仅可以指向简单的变量，还可以指向数组变量。例如：

在上面的代码中，&iarray[0]表示获取数组中第1个元素的地址，即数组的首地址。对于数组来说，数组名同样表述数组的首地址，因此下面的代码与上述两行代码是完全相同的。

图2.6描述了当前指针变量pvar与数组iarray之间的关系。

图2.5　使用指针变量修改数据

图2.6　指针变量pvar与数组iarray之间的关系

当一个指针变量指向一个数组后，通过指针变量即可访问该数组中的每一个元素。例如，下面的代码利用指针输出数组中的每一个元素。

【例2.9】使用指针变量输出数组元素。（实例位置：资源包\TM\sl\2\6）

执行上述代码，结果如图2.7所示。

在上述代码中，注意“pvar = pvar + 1;”语句，该语句的作用是使指针指向数组中的下一个元素，而不是简单地将指针地址加1或将指针值加1。语句“pvar = pvar + 1;”是移动指针pvar指向的地址，移动量不是1个字节，而是1个数组元素的大小，更准确地说应该是指针pvar的类型（这里为int类型）的大小。

在开发程序时，如果需要使用一组指针，可以定义一个指针数组。例如，下面的代码定义了一个整型的指针数组。

对于指针数组来说，数组中的每一个变量均是一个指针变量。下面的代码演示了指针数组的应用。

【例2.10】利用指针数组存储数据。（实例位置：资源包\TM\sl\2\7）

执行上述代码，结果如图2.8所示。

图2.7　利用指针遍历数组

图2.8　指针数组应用

在定义指针时，也可以使用const关键字。例如：

对于指针pvar来说，用户不能够修改pvar指向的值，但是可以修改pvar指向的地址。例如：

在使用const关键字定义指针时，也可以将const关键字放置在指针变量的前面，但是指针变量的性质发生了改变。观察如下代码：

上面的代码定义了一个常量指针pvar，使用户不能够修改pvar指向的地址，但是可以修改pvar指向的数据。

在定义指针时也可以同时使用两个const关键字。例如：

在上面的代码中，用户既不能修改pvar指向的地址，也不能修改pvar指向的数据。例如：

2.3.9　引用类型

引用可以认为是一个“别名”，即目标的代名词。当定义一个引用时，需要为其指定一个对象，即目标。此时，引用就成了目标的代名词，操作引用与操作实际的目标对象是相同的。定义引用的格式如下：

下面的代码定义了一个引用对象。

在定义引用对象后，即可使用引用对象操作目标对象。

【例2.11】使用引用对象代替目标对象。（实例位置：资源包\TM\sl\2\8）

执行上述代码，结果如图2.9所示。

从图2.9中可以看到，通过设置引用变量的值，修改了变量ivar的值。在程序中如果对引用对象进行取地址，返回的将是目标的地址，因为引用是目标的代名词。

【例2.12】访问引用对象的地址。（实例位置：资源包\TM\sl\2\9）

执行上述代码，结果如图2.10所示。

图2.9　使用引用操作对象

图2.10　对引用对象进行取地址

说明

从图2.10中可以发现，变量ivar的地址与rvar的地址相同，对引用对象进行取地址运算，其实就是对目标对象进行取地址运算。

2.3.10　自定义类型

在C++语言中，用户可以使用typedef关键字自定义数据类型。自定义数据类型并不是真的创建新的数据类型，而是为已存在的数据类型定义一个新的名称。自定义类型的语法格式如下：

例如，下面的代码定义了一个UINT数据类型。

当定义完新的数据类型后，就可以像定义普通数据类型变量一样定义新数据类型的变量。例如，下面的代码定义了一个UINT类型的变量。

在程序中使用自定义类型的好处是能够提高程序的移植性。同一种数据类型，在不同机器或不同操作系统上，其长度或性质可能是不同的。如果程序中统一使用了自定义类型，在修改程序时，只需要修改自定义类型的基类型即可，代码的其他地方不需要改动。

2.4　运算符

C++语言中常用的运算符有赋值运算符、算术运算符、关系运算符、逻辑运算符、自增自减运算符、位运算符、sizeof运算符、new和delete运算符等。本节将详细介绍C++语言中的运算符。

2.4.1　赋值运算符

赋值运算符是程序开发中最基本的运算符。C++语言中最常用的赋值运算符为“=”。在前面几节的示例中，多处使用了赋值运算符。本节将详细讨论赋值运算符。

首先介绍一下赋值与初始化的区别。赋值和初始化均使用“=”运算符，但是初始化只有在定义变量时进行，并且只有一次，而赋值则可以进行多次。

在使用赋值运算符时，应该保证赋值运算符左右两边的表达式数据类型兼容。例如，下面的赋值语句是错误的。

下面的代码是合法的，因为实型数据将被隐式转换为整型。

在上面的代码中，虽然10.55为实数，但是它能够自动地转换为整数，因此不会出现错误。

此外，在使用赋值运算符时，应保证赋值运算符的左边是一个“可读写地址”的表达式。例如，下面的赋值语句是非法的。

在使用赋值运算符时，可以在一条语句中为多个变量赋值。例如：

在上述代码中，变量jvar和ivar的值均为10。

在C++语言中，赋值运算符除了“=”外，还有“+=”“-=”“*=”“/=”和“%=”等。下面以“+=”赋值运算符为例介绍这些运算符的使用。

“+=”运算符表示将运算符左边的表达式与右边的表达式相加，再赋值给左边的表达式。例如：

上述代码执行后，变量ivar的值将为15。

说明

代码“ivar +=5;”等价于“ivar = ivar + 5;”。

“-=”“*=”“/=”和“%=”等运算符与“+=”运算符用法基本相同，详细描述如表2.4所示。

表2.4　C++赋值运算符

2.4.2　算术运算符

算术运算通常为加、减、乘、除和取模5种，在C++语言中对应的运算符如表2.5所示。

表2.5　C++算术运算符

算术运算符在程序中经常被使用，使用方法也比较简单，下面主要介绍除法运算。当两个整数相除时，其结果仍为整数，小数部分被舍弃。例如：

在上面的代码中，变量ivar与jvar的值将相同，均为2。有些读者可能会认为11/4的结果为实数，只是将其截取了小数赋值给整型变量ivar，这种理解是不正确的。观察如下代码：

【例2.13】两个整数相除的结果。（实例位置：资源包\TM\sl\2\10）

执行上述代码，结果如图2.11所示。

从图2.11中可以发现，变量fvar的值为2.000000，而不是2.75000。因为两个整数相除，结果仍为整数。这是许多初学者编写代码时经常犯的错误。如果对上述代码稍加修改，将整数4修改为实数4.0，结果将发生改变。因为整数与实数相除或两个实数相除，结果将为实数。

【例2.14】整数与实数相除的结果。（实例位置：资源包\TM\sl\2\11）

执行上述代码，结果如图2.12所示。

图2.11　整数相除

图2.12　整数与实数相除

从图2.12中可以发现，变量fvar的值为2.750000。

2.4.3　关系运算符

关系运算属于一种简单的逻辑运算，程序设计语言通常包含6种关系运算，即大于、小于、等于、大于等于、小于等于和不等于。C++中对应的关系运算符如表2.6所示。

表2.6　C++关系运算符

对于关系运算来说，表达式的值只有真和假。若为真，则表达式的值为1；若为假，则表达式的值为0。例如：

执行上述代码，iret的值将为1，因为关系表达式“5 > 4”的值为真，即1。关系表达式可以由多个关系运算符构成。

观察最后一行代码，关系表达式由两个关系运算符“>”构成。对于关系运算符来说，其结合性（有关运算符的结合性可参考2.4.9节）由左到右，因此关系表达式“ivar > jvar > nvar”首先比较“ivar >jvar”，其结果为1，然后进行“1 > nvar”比较，其结果为0，最后将0赋值给变量iret，因此代码“int iret = ivar > jvar > nvar;”执行后，其值为0。

注意

将变量值代入最后一行代码“int iret = 10 > 9 > 8;”，初学者一看“10 > 9 > 8”很容易将表达式的值当成真值，这是初学者经常犯的错误。

2.4.4　逻辑运算符

逻辑运算符用于连接关系表达式，以构成复杂的逻辑表达式。C++中共有3种逻辑运算符，分别为“&&”“||”和“!”。其中“&&”表示逻辑与运算，即当两个表达式同时为真，其结果为真，否则为假。“||”表示逻辑或运算，即当两个表达式中有一个表达式为真，其结果为真，否则为假。“！”表示逻辑非运算，即当表达式的值为真，其结果为假；当表达式的值为假，其结果为真。表2.7描述了逻辑运算符的运算方式。

表2.7　逻辑运算符真值表

技巧

表2.7看起来有些复杂，尤其是“&&”和“||”运算符，记起来非常容易乱，其实可以用一句话来概括这两个运算符，那就是“都真才真，都假才假”，前半句用来形容“&&”运算符，后半句用来形容“||”运算符。

在逻辑表达式中通常可以包含多个逻辑运算符。例如：

分析最后一行代码，首先进行“ivar > jvar”比较，结果为1；然后进行“nvar > mvar”比较，结果为0，接着进行“1”和“0”的与（&&）运算，结果为0；进行“mvar > ivar”比较，结果为1，最后进行“0”和“1”的或（||）运算，结果为1；将1赋值给变量iret。因此，执行最后一行代码后，iret的值为1。

需要说明的是，最后一行代码并不是规范的代码，因为用户要事先熟知各种运算符的结合性和优先级，否则很难确定代码的执行顺序。如果使用“()”对最后一行代码进行分解，则会使代码更加清晰。例如：

通过使用“()”，用户可以非常清楚地了解代码的执行顺序。

2.4.5　自增自减运算符

为了方便进行加1和减1操作，C++提供了自增和自减运算符，即“++”和“--”。自增自减运算符分为前置运算和后置运算。下面通过代码来演示自增和自减运算符的应用。

【例2.15】后置自增运算符。（实例位置：资源包\TM\sl\2\12）

执行上述代码，结果如图2.13所示。

在上述第2行代码中使用了后置自增运算符，使ivar的值加1。对于后置运算符来说，先进行赋值操作，然后才使变量ivar的值自动加1。因此，上述代码运行后，变量ivar的值为11，而变量jvar的值为10。如果将第2行中的后置自增运算符改为前置自增运算符，运行结果就不同了。

【例2.16】前置自增运算符。（实例位置：资源包\TM\sl\2\13）

执行上述代码，结果如图2.14所示。

图2.13　后置自增运算

图2.14　前置自增运算

在上述第2行代码中使用了前置运算符，首先使变量ivar的值加1，然后将其赋值给变量jvar，因此jvar的值为11。

说明

对于自增运算符“++”来说，“变量++”和“++变量”都等价于“变量=变量+1”，区别只在于是先执行加1的运算，还是后进行加1的运算。

自增自减运算符不仅可以应用于数值型变量，还可以应用于指针变量。在开发程序时，通常使用自增运算符来实现指针遍历数组。观察如下代码：

【例2.17】使用指针遍历数组。（实例位置：资源包\TM\sl\2\14）

执行上述代码，结果如图2.15所示。

图2.15　指针遍历数组

在上述代码中，“*pvar++”表达式先输出指针pvar的数据，然后进行“++”运算，使指针指向下一个元素。

2.4.6　位运算符

在计算机系统中，所有数据均以二进制的形式表示。数据存储是以字节为单位，一个字节包含8位，每一位可以表示为0或1。本节介绍的位运算符是指能够对二进制数据中的位进行运算的符号。C++中主要包含6种位运算符，如表2.8所示。

表2.8　C++位运算符

下面分别进行介绍。

1. 按位与运算—&

按位与运算是指两个相应的二进位均为1，则结果为1，否则结果为0。即0&0结果为0，0&1结果为0，1&0结果为0，1&1结果为1。例如，将10和12进行运算，即“10&12”。数字10对应的二进制数为“00001010”，数字12对应的二进制数为“00001100”。10&12运算过程如图2.16所示。

从图2.16中可以发现，10&12运算结果为8。

在开发程序中，按位与运算通常有其特殊的用途。例如，将某个字节数据清零，可以将该字节的数据与0进行按位与运算，其结果将为0。因为任何数与0进行与运算，结果必然为0。此外，与运算还可以将一个字节中的某些位保留下来。例如，对于二进制数00001110，想要保留其1、3、5、7位，则可以将该二进制数与“01010101”二进制数（该二进制数1、3、5、7位为1，其他位为0）进行按位与运算，获得的结果为“00000100”，也就是说当前数的第3位数字为1，其他1、5、7位数字都为0。

2. 按位或运算—|

按位或运算是指两个相应的二进位只要有一个结果为1，则结果为1，否则结果为0。即0|0结果为0，0|1结果为1，1|0结果为1，1|1结果为1。例如，将10和12进行或运算，即“10|12”。其运算过程如图2.17所示。

从图2.17中可以发现，“10 | 12”的运算结果为14。

根据按位或运算的性质可知，如果要将某一个字节数据中的某一位或几位设置为1，通过按位或运算可以非常方便地实现。例如，要将“00001010”后4位设置为1，将其与“00001111”二进制数（后4位全为1）进行按位或运算即可。

3. 按位异或运算—^

按位异或运算是指两个相应的二进位均相同，则结果为0，否则结果为1。即0^0结果为0，0^1结果为1，1^0结果为1，1^1结果为0。例如，将10和12进行异或运算，即“10^12”。其运算过程如图2.18所示。

图2.16　按位与运算

图2.17　按位或运算

图2.18　按位异或运算

从图2.18中可以发现，“10^12”的运算结果为6。在实际应用中，通常利用按位异或运算来实现二进位的反转。例如，将二进制数“00001010”的后4位反转，可以将其与“00001111”二进制数（后4位均为1）进行按位异或运算，所得结果为“00000101”。此外，还可以利用按位异或运算实现两个变量值的互换（不使用中间变量）。首先分析一下异或运算的性质：

任何数据与0进行按位异或运算，结果仍为数据本身。

变量与自身进行按位异或运算，结果为0。

按位异或运算具有交换性，即a^b^c等于a^c^b，还等于b^a^c等。

下面通过一个实例演示如何使用按位异或运算实现两个变量值的互换。

【例2.18】使用按位异或运算实现两个变量值的互换。（实例位置：资源包\TM\sl\2\15）

执行上述代码，结果如图2.19所示。

分析上述黑体部分代码。首先观察黑体部分第2行代码“jvar =jvar ^ ivar;”，根据上一行代码将“ivar”替换为“ivar ^ jvar”，那么第2行代码“jvar = jvar ^ ivar;”转换为“jvar = jvar ^ ivar ^ jvar;”。由于异或运算具有交换性，因此也可以写为“jvar=jvar ^ jvar^ ivar;”。由于变量与自身进行按位异或运算结果为0，因此第2行代码又可以转换为“jvar = 0 ^ ivar;”。根据任何数据与0进行按位异或运算，结果仍为数据本身，最终第2行代码的结果为“jvar = ivar”。接着分析黑体部分第3行代码，根据第1行代码和第2行代码，可以将第3行代码转换为“ivar = ivar ^ jvar ^ jvar^ ivar ^ jvar;”，即“ivar = ivar ^ ivar ^jvar ^ jvar ^ jvar;”，等价于“ivar =0^ jvar;”，最终“ivar = jvar”。

上面的分析对于初学者来说或许有些复杂，不过不要急，还有一种简单的方法可以解释清楚，那就是直接代入数据。虽然不能通过代入数据的结果来证明两个变量交换值的原理，但是，想必读者看过具体的计算以后，可以更好地理解上面的分析。下面就将实例中的数据“00000100”（4）和“00000101”（5）代入计算：

图2.19　交换变量

第一步：ivar = 00000101^ 00000100，结果ivar = 00000001（1）。

第二步：jvar = 00000100^ 00000001，结果jvar = 00000101（5）。

第三步：ivar = 00000001 ^ 00000101，结果ivar = 00000100（4）。

通过以上3步，确实实现了两个变量的数值交换。

4. 按位取反运算—～

取反运算符“～”用于对一个二进制数按位取反，即将0转换为1，将1转换为0。例如，对二进制数“00001010”取反，结果为“11110101”。

5. 左移运算符—<<

左移运算符用于将一个数的二进制位左移若干位，右边补0。例如，将10左移两位，表示为：

其结果iret等于40。因为10的二进制数为“00001010”，左移2位后，表示为“00101000”，即40。实际上，左移一位，表示乘以2；左移两位，表示乘以22，即乘以4，以此类推。

在使用左移运算符时，有时会出现移出界的情况。例如：

上述代码执行后，cvar对应的数值将为4。因为129对应的二进制数为“01000001”，将其左移两位为“00000100”，7、8位“01”溢出，被舍弃，因此cvar的值为4。

6. 右移运算符—>>

右移运算符与左移运算符相反，是将一个数的二进制位右移若干位。例如，将10右移两位，表示为：

其结果iret等于2。因为10对应的二进制数为“00001010”，右移2位后为“00000010”，即十进制数2。

位运算符还可以与赋值运算符“=”组合，形成位运算赋值运算符，如“&=”“|=”“^=”等。以“&=”为例，“ivar &= jvar”实际上等价于“ivar = ivar & jvar”。具体运算过程前文已经介绍，这里不再赘述。

2.4.7　sizeof运算符

sizeof运算符用于返回变量、对象或者数据类型的字节长度。例如：

sizeof运算符还可以用于数组。例如：

在上述代码中，arraysize的值为20，因为数组中共有5个元素，每个元素占4个字节。

使用sizeof运算符在确定数组大小时，需要注意的是对字符串常量的测试。例如，测试字符串“Hello”的长度，其长度为6，因为对于字符串常量来说，系统会自动添加“\0”字符作为结束标记。例如，下面的赋值操作将出现错误。

因为字符串常量“Hello”需要占用6个字节空间，因此在编译时会出现数组溢出的错误。如果改为如下代码，就不会出现问题了。

通常，用户会采用如下方式将字符串常量赋值为数组：

此时，数组carray的长度为6。

如果用户使用的是32位的操作系统，当sizeof运算符测试指针的长度时，无论指针是何种类型及指针指向什么数据，指针的长度均为4，因为指针是按32位寻址的，长度为4个字节。这一点对于C++的初学者很重要。许多公司的面试题中经常会出现类似如下的代码：

要求确定size的值，这里size的值应为4。如果以数组作为函数参数，在函数中测试数组参数的长度时，其值为4，因为调用函数时需要传递数组名，而在传递数组名时实际传递的是数组的首地址，也可以认为是一个指针。

【例2.19】判断参数中数组名的大小。

在上面的代码中，变量size的值为4，因为语句“int size = sizeof(cdata)；”中的cdata被认为是一个指针。

注意

作为参数传递的数组其实是以指针的形式传递的，所以在使用sizeof获得数组参数的长度时是4，而不是数字长度。

2.4.8　new和delete运算符

在介绍new和delete运算符之前，先来介绍一下C++应用程序数据的存储方式。对于C++应用程序来说，数据主要有两种存储方式，即栈存储和堆存储。栈存储通常用于存储占用空间小、生命周期短的数据，如局部变量和函数参数等。本节之前的实例中，除了静态变量和全局变量外，其他的所有变量均属于栈存储方式。堆存储通常用于存储占用空间大、生命周期长的数据，如静态变量和全局变量等。除静态变量和全局变量外，用户可以使用new运算符在堆中开辟一个空间，使变量存储在堆中。例如：

在上述代码中，调用new运算符在堆中开辟了4字节的空间，将地址指向指针pvar，接着设置指针pvar的数据，然后输出数据，最后调用delete运算符释放pvar指向的堆空间。

注意

对于手动分配的堆空间，在使用后一定要释放堆空间，否则会出现内存泄漏。

在使用new运算符分配堆空间时，还可以进行初始化。例如：

此时，pvar指向的数据为10。

使用new运算符，还可以为数组动态分配空间。例如：

使用new运算符为数组分配空间时，不能够对数组进行初始化，除非变量是一个对象，并且对象的类型（类）提供了默认的构造函数。此外，delete []用来释放使用new运算符为数组分配的空间。例如：

说明

在上面的代码中，如果使用“delete pvar;”语句来释放pvar指针指向的数组空间，也是可以的，不会出现内存泄漏。不过在开发程序时并不提倡这么做，因为对于简单的基础数据类型（上述代码为int类型），没有提供析构函数，使用“delete pvar;”语句释放数组时不会出现内存泄漏；但是对于类对象数组来说，这样做是不行的，必须使用“delete []”形式来释放new运算符分配的数组空间。

2.4.9　结合性与优先级

运算符具有结合性和优先级两个属性，这两个属性描述的是语句的执行顺序。所谓结合性是指表达式的整体计算方向，即从左向右或从右向左。以“int iret=x+y+z;”语句为例，由于算术运算符的结合性从左向右，即表达式的整体计算方向从左向右，因此语句中“x+y+z”的计算方式首先计算“x+y”，然后结果再与“z”相加。而赋值运算符的计算方向是从右向左，即将右边的结果赋值给左边，因此将“x+y+z”的结果赋值为iret，而不是将iret赋值给“x+y+z”。优先级表示的是运算符的优先执行顺序。在数学中，表达式“x+y*z”的计算方式是先计算“y*z”，然后将结果与“x”相加。在计算机中，为了符合人们的计算习惯，同样规定了“*”运算符的优先级高于“+”运算符，因此，表达式“x+y*z”在程序中的执行顺序与在数学中的计算方式是相同的。表2.9描述了C++运算符的优先级和结合性。

表2.9　C++运算符的优先级和结合性

说明

同一优先级的运算符，运算次序由结合性决定。

2.5　表达式

将运算符和运算对象连接起来，符合C++语法规则的式子称为C++表达式。根据运算符的不同，表达式可以分为算术表达式、关系表达式、逻辑表达式等。这样的表达式在前面几节已经介绍过，本节主要介绍C++中两个特殊的表达式，即逗号表达式和三目元表达式。

2.5.1　逗号表达式

C++语言中提供了一种特殊的表达式，即逗号表达式。所谓逗号表达式，是指使用逗号运算符将两个表达式连接起来。例如：

对于逗号表达式来说，其运算过程为：先计算表达式1，即“10 * 5”，然后再计算表达式2，即“8 * 9”。整个表达式的值为表达式2的值，也就是最右边表达式的值（如果有多个表达式使用逗号连接）。观察如下代码：

在上面的代码中，iret的值将为15，有些读者可能感到意外，逗号表达式的结果不应该是表达式2的值吗？iret的值应为8。实际上赋值运算符（=）的优先级高于逗号运算符（，），因此，先计算“3 * 5”，将其赋值给iret，再计算iret*4。分析一下下面的代码：

分析变量var的值，var的最终结果为60。因为变量iret的值为15，计算“15*4”即60。

逗号表达式通常应用于for语句中，在for循环语句中修改循环变量，详细内容参见3.4.1节。

2.5.2　三目元表达式

在C++语言中提供了唯一的一个三目元表达式，它是由条件运算符和变量构成的。在开发程序时，经常编写如下形式的if语句（有关if语句的详细介绍参见3.3.1节）。

对于上面的代码，可以使用三目元表达式将其改写为一行代码。

三目元表达式主要由“？”和“：”运算符构成。其中，“？”运算符前面是一个关系表达式，后面的“x：y”表示条件表达式。整个语句的执行过程为：首先判断关系表达式“x > y”的真假，如果为真，则整个三目元表达式的结果为x；如果关系表达式为假，则整个三目元表达式的结果为y。

2.6　程序编码规范

俗话说，“没有规矩，不成方圆”，程序编码也不例外。通常每个软件公司都有一套自己的编码规则，这样既有利于形成自己的风格，又有利于公司内部人员的沟通和交流。本节将介绍程序编码的一些注意事项和通用规则。

2.6.1　合理使用注释

注释是用于帮助程序员阅读程序的一种语言结构，它不会对程序的功能产生任何影响，也不会增加可执行代码的长度，因为编译器在编译代码时会将注释过滤掉。在程序中注释通常有两个作用：一是对代码进行简要解释；二是标注文件的版权、作者、版本号等信息。例如，下面的注释是许多开发人员标注文件采用的通用格式。

在C++语言中，注释有两种，一种是行注释“／／”，另一种是语句注释“／*…*／”。例如：

行注释“／／”只注释当前行，语句注释“／*…*／”能够注释掉“／*”与“*／”之间的语句。

注意

在使用语句注释时，需要注意“／*”与“*／”并不能按层次对应。

例如，下面的注释是非法的。

在上面的代码中，外层注释“／*”是与内层注释“*／”匹配的，而不是与外层注释“*／”匹配。因此，代码“int age = 20；printf（“%d\n”, age）”没有被注释掉，外层注释“*／”成了代码的一部分，代码不能够被编译。

此外，在使用注释时还应注意以下事项。

（1）不可过多地使用注释

注释只是对代码的简要说明，并不是文档，注释过多，会让人感觉眼花缭乱，有些喧宾夺主的“味道”。如果代码简洁、清楚，则不需要使用注释。例如：

上面的代码注释显得多此一举了。但是本书中出现的代码多数都标有注释，其目的一是使页面更加工整，连读多行注释，突然一段代码没有注释，页面显得有些凌乱；二是使读者通过注释中的文字描述来理解代码的作用。实际上，用户在开发程序时没有必要这么做（每行都标有注释）。

（2）代码与注释同时进行

在写代码后马上写注释，甚至在写代码前，应先写注释。此外，修改代码的同时修改注释，使注释与代码的功能对应。

（3）注释要简洁、准确

在写注释时，不要写得过长，将代码的语义描述清楚即可。注释一定要准确，不要产生歧义，错误的注释会适得其反，使其他开发人员产生误解。

（4）注释位置要适当

注释通常放置在代码的上方或右方，不要放置在代码的下方，让人误以为是下行代码的注释。

（5）较长的代码要分段注释

在写某个函数时，如果代码较长，层次结构比较复杂，则要在每层结束后添加注释标识。例如：

2.6.2　统一代码缩进

在编写代码时要统一代码的缩进，如确定每一层的代码缩进为几个空格或一个tab制表位，这样编写出的代码既工整又利于阅读。例如：

技巧

在Visual C++ 6.0开发环境中，选择一段代码，然后按Alt+F8键可以根据缩进情况对齐凌乱的代码。

2.6.3　代码换行

在开发程序时，有时一行代码会很长。例如：

在书写代码时，对于长代码应将其分成多行显示。例如，将上述代码转换为如下格式，让人产生一目了然的感觉。

在分行时，应保证在低优先级运算符处分行，运算符放在新行的开始位置，拆分的新行要按一定规则缩进。

2.6.4　合理使用空格

空格在程序中不起任何作用，但是合理地使用空格会使代码更易于阅读和理解。如下是笔者总结的空格使用规则。

（1）关键字之后使用空格

在程序中使用关键字时，关键字之后建议保留一个空格，以突出显示关键字。例如：

if关键字与“（”之间添加空格以示区分关键字。

（2）逗号之后应添加空格

在定义函数时，如果函数有多个参数，各个参数之间需要使用“，”分隔。例如：

（3）赋值运算符、关系运算符和算术运算符等二目元运算符前后加空格

对于二目元运算符，为了使表达式更清晰，应在运算符前后添加空格。例如：

但是，对于for语句和if语句，为了使表达式更紧凑，可以适当地去掉空格。例如：

（4）“，”“）”和“；”号向前紧跟，与前边的标识符之间不留空格

当程序代码中出现“. ”“）”和“；”等符号时，与前边的标识符之间不留空格。例如：

2.6.5　命名规则

提到命名规则，不得不说的是微软公司的“匈牙利命名规则”。其主导思想是在变量或函数名前加前缀，以增强对程序的理解。例如，定义一个变量int x，如果采用“匈牙利命名规则”，应写为“int ix”，其中前缀i表示整型。

使用“匈牙利命名规则”的最大缺点是烦琐，因此许多开发人员并不赞同使用“匈牙利命名规则”。本节笔者不会定义一套命名规则，因为很难定义适合于所有开发人员的命名规则。这里只介绍一些通用的命名规则。

（1）标识符命名应直观、易懂

标识符的命名应该让人望文知意，而不应该不着边际。标识符的命名应采用英文单词、英文单词的缩写或英文单词的组合，而不要以汉语拼音来命名标识符。例如，“int MaxValue;”不要定义为“int zdz;”，让人难以理解。

（2）标识符的命名不要太长

标识符的名称需要体现其含义，但是名称不能太长。例如，“int LacaoTemporaryValue;”应写为“int TmpValue”。对于一些局部变量，可以使用单个的字母作为标识符的名称，如“int i, j, m, n;”等。

（3）标识符的区分不要以大小写来区分

众所周知，C++语言是区分大小写的。但是，在命名标识符时，不要以此来区分不同的标识符，那样容易让人误解。例如，下面两个函数的命名并不是好的习惯，在调用时很容易误解。

（4）程序中不要出现同名的全局变量和局部变量

当出现全局变量与局部变量同名时，全局变量被隐藏，而不会产生编译错误，但是容易让人误解。

（5）变量的命名应采用名词或形容词+名词的形式

在命名变量时，建议以名词或形容词+名词的形式。例如：

（6）函数的命名应使用动词或动词+名词的形式

对于全局函数或类成员函数，应使用动词或动词+名词的形式。例如：

（7）标识符的命名尽量不使用数字编号

许多程序开发人员在命名标识符时，使用数字编号区分变量。例如：

这并不是好的命名习惯，除非逻辑上确实需要编号，否则不要使用类似的命名习惯。

（8）类和函数名首字母应大写

在命名类和函数名时，首字母应大写。如果类或函数名由多个单词构成，每个单词的首字母都应大写。例如：

（9）局部变量和参数首字母应小写

在命名局部变量或函数参数时，首字母应小写。例如：

（10）常量的所有字母应大写

对于常量，所有字母应大写。如果常量由多个单词构成，单词间用下画线分隔。例如：

2.7　小结

本章详细介绍了C++语言的基本要素、数据类型、运算符和表达式等。希望通过本章的学习，读者能够熟练掌握C++的基础知识，理解书中涉及的C++语言的一些特性和高级用法，积累C++语言的开发经验。

2.8　实践与练习

1. 不使用库函数，复制源字符串到目标字符串中，即实现strcpy函数的功能。（答案位置：资源包\ TM\sl\2\16）

2. 写一个函数，接收一个字符串作为输入参数，将其逆序输出。（答案位置：资源包\TM\sl\2\17）

3. 使用冒泡法对数组元素排序。（答案位置：资源包\TM\sl\2\18）

4. 利用数组输出Fibonacci数列。（答案位置：资源包\TM\sl\2\19）

注意

由于第2章涉及的内容较少，建议读者看完后面第3、4章的内容后再做这些练习。

第3章

语句

（视频讲解：27分钟）

语句是程序执行的最小单位，通常由表达式加一个分号构成。从结构上划分，C++语句主要分为简单语句和复合语句两种；从功能上划分，C++语句可以分为分支语句、循环语句和跳转语句3种。本章将详细介绍C++语句的相关知识。

通过阅读本章，您可以：

了解C++语句的构成

使用分支语句设计逻辑条件判断

掌握各种循环语句在程序开发中的应用

应用跳转语句改变代码的执行顺序

3.1　语句的构成

C++语句通常由表达式和分号构成。例如：

但是，也可以只由分号构成。例如：

上面的语句只有分号，该语句被称为空语句。空语句不执行任何功能，只是在语法格式上要求使用语句时才使用空语句。例如：

在上面的代码中，while语句中已经包含了逻辑功能，但是while语句要求必须有循环体，此时可以使用空语句作为while语句的循环体。在程序中连续地出现空语句（由于不小心）是合法的。例如：

在上述代码中，出现了不必要的空语句，虽然不会对程序产生任何影响，但是会增加代码的长度，不利于用户阅读，因此应该避免出现连续的空语句。

3.2　复合语句

复合语句也被称为语句块，是由“{”“}”符号和多条语句构成的。在开发程序时，单一的一条语句不能完成一项逻辑功能，通常将多条语句组合为复合语句来实现。例如：

【例3.1】使用复合语句。

在使用“{”和“}”符号设计复合语句时，复合语句中也可以不包含代码，此时的复合语句与空语句的作用是相同的。例如：

上述代码定义了一个空复合语句。在程序中可以使用空复合语句来代替空语句，在使用复合语句时，需要注意“}”符号之后没有分号。例如，下面的复合语句是非法的。

在使用复合语句时，需要注意的是在复合语句中定义的变量，其作用域范围为当前定义变量处到复合语句的结束。分析下面的代码：

在上述代码中，在复合语句中定义了变量ivar，但是在复合语句之外访问了变量ivar，因此会出现编译错误。如果将上述代码修改为如下形式，则不会出现编译错误。

在上述代码中，定义了两个同名的整型变量ivar，其中在复合语句中输出的ivar值为1，在复合语句之外输出的ivar值为0。在复合语句内部定义了与外部同名的变量时，其复合语句内部的变量将取代外部变量。

注意

尽量不要在同一段代码中的复合语句内外定义同名的变量，虽然这样是合法的，但是不利于程序的阅读，也容易出现逻辑错误。

对于复合语句来说，复合语句是可以嵌套的。

【例3.2】复合语句中变量的作用域。

在上面的代码中，内层复合语句输出的ivar值为2，而复合语句之外的输出语句输出的ivar值为0。对于内层嵌套的复合语句来说，它访问的变量是外层复合语句中定义的变量ivar（其值初始化为1）。

3.3　分支语句

C++语言中提供了两种分支语句，即if语句和switch语句。其中，if语句多用于单分支条件判断，switch语句用于多分支条件判断。本节将详细介绍这两种分支语句。

3.3.1　if语句

if语句通常用于单分支条件判断。其简单的语法格式如下：

其中，else部分的语句是可选的。if语句的执行过程为：首先判断表达式的真假，如果为真，则执行语句1，否则执行else部分的语句（如果存在else语句）。例如，下面的if语句均是合法的。

注意

在if语句的表达式中，有时在使用“==”运算符时，程序员很容易将“==”运算符写成“=”运算符，由于这两个运算符都是合法的，因此编译器无法检测到这样的错误。在编程时一定要谨慎使用“==”运算符，否则将给您带来无尽的烦恼。

使用上述的if语句只能进行简单的逻辑条件判断。考虑如下的一种情况：学生成绩大于60的为及格、大于80的为良、大于90的为优秀，如何在程序中判断学生成绩呢？分析如下代码。

在上面的代码中存在多个缺点：（1）代码执行效率较低，因为程序会逐一执行每一个if语句，例如，如果ivar的值为59。当程序执行完“if（ivar < 60）”语句后，还需要执行“if (ivar >= 60 && ivar< 80)”“if (ivar >= 80 && ivar < 90)”等if语句。（2）判断条件比较复杂，上述代码中使用了多个区域判断。图3.1描述了上述代码的执行情况。

在C++中，if语句除了提供else子句外，还提供了else if子句。使用else if子句时，if语句的执行过程为：首先判断if条件表达式的真假，如果为真，则执行if部分的语句，然后整个if语句结束。如果为假，则继续判断else if语句中表达式的真假；如果为真，则执行else if部分的语句，if语句结束，否则继续向下判断。下面的代码使用else if子句对之前的学生成绩判断代码做了修改。

【例3.3】成绩等级判断。

从上述代码可以看出，通过使用else if语句，不但提高了代码执行效率，还简化了区域判断的条件，使代码更加简洁。图3.2描述了上述代码的执行情况。

为了描述复杂的逻辑条件，if语句还允许嵌套，即if语句中包含if语句。例如，下面的代码利用嵌套的if语句来进一步判断成绩是否进行了确认。

【例3.4】嵌套的if语句。

图3.1　if语句流程图1

图3.2　if语句流程图2

对于上述代码，还可以修改为如下形式。但是，用户在开发程序时，建议使用上述代码格式。

在上面的代码中注意else子句，这里else子句是与“if (checked)”语句对应的，即采用就近原则，else子句与其之前最近的if语句对应，尽管没有使用复合语句分隔。

3.3.2　switch语句

switch语句用于多分支条件判断，它能够测试一组有序类型（整型、字符型和枚举型等）的数据。当发现匹配的常量时，它将执行与该常量关联的语句。

switch语句的语法格式如下：

其中，表达式必须是有序类型，不能是实数或字符串类型。表达式逐一与case语句部分的常量匹配，如果发现有常量与表达式相匹配，则执行当前case部分的语句，直到遇到break语句位置，或者到达switch语句的末尾（没有遇到break语句）。当表达式没有发现与之匹配的常量，将执行default部分的代码。default语句是可选的，如果代码中没有提供default语句，并且没有常量与表达式匹配，switch语句将不执行任何动作。下面的代码利用switch语句判断数据库的当前状态。

【例3.5】使用switch语句判断数据库的状态。（实例位置：资源包\TM\sl\3\1）

执行上述代码，结果如图3.3所示。

在switch语句中，如果需要对各个常量进行相同的处理，则可以将多个case语句组织在一起，使用一个执行语句。

【例3.6】在switch语句中对多个常量进行相同的处理。（实例位置：资源包\TM\sl\3\2）

switch语句中的break语句属于跳转语句，也可以用于循环语句中。在switch语句中，当遇到break语句时，程序将跳转到switch语句之后的下一行代码处。如果在switch语句中没有使用break语句，当表达式与某个常量匹配时，将执行当前case语句代码，并且继续向下执行case语句的代码，直到遇到break语句或者switch语句结束。观察如下代码：

【例3.7】 switch语句的执行顺序。（实例位置：资源包\TM\sl\3\3）

执行上述代码，结果如图3.4所示。

图3.3　switch语句1

图3.4　switch语句2

由于在switch语句中没有使用break语句，当表达式dbste与close常量匹配时，没有遇到break语句，并且switch语句没有结束，因此继续向下执行，执行了其后的所有switch语句。

注意

在开发程序时，这通常不是预期的结果，因此在编写switch语句时，除非程序需要，否则不要遗忘break语句。

3.4　循环语句

C++语言中主要提供了3种循环语句，分别为for语句、while语句和do while语句。其中，for语句用于根据循环次数来执行循环，而while语句和do while语句用于根据条件的真假来执行循环。本节将详细介绍这3种循环语句。

3.4.1　for语句

for语句是根据预知的循环次数来执行循环。其通用的语法格式如下：

例如，下面的代码利用for循环语句输出数组中的元素。

【例3.8】利用for循环语句输出数组中的元素。

对于for循环语句来说，其中的变量初始赋值、循环结束条件、变量递增每一项均可以省略，但是必须保证循环能够结束，即不能出现死循环。例如，下面的for循环语句省略了变量的初始赋值。

【例3.9】省略for语句中的变量初始赋值。

在上面的代码中，for语句中省略了变量的初始赋值，因为在之前的代码中已经为变量赋值了。但要注意的是，for语句的变量初始赋值后面的分号不能省略。

下面代码中的for语句省略了循环结束条件。

【例3.10】 for语句中省略循环结束条件。

对于for语句来说，可以同时省略变量初始赋值、循环结束条件和变量递增。

【例3.11】 for语句中省略变量递增。

说明

虽然在使用for循环语句时，其中的变量初始赋值、循环结束条件和变量递增每一项均可以省略，但是笔者并不建议读者这样做，因为这种写法在逻辑中很容易出现错误，并且当循环出现问题时，检查错误将更加费时。

在上述代码中使用了break语句来终止循环。该语句还可以应用于switch语句，3.3.2节中曾作了介绍，此处不再赘述。在循环语句中，可以使用break语句来终止循环，也可以使用continue语句来结束本次循环。下面通过两段代码来演示break语句和continue语句的作用及其区别。break语句的应用代码如下。

【例3.12】 break语句在循环中的作用。（实例位置：资源包\TM\sl\3\4）

执行上述代码，结果如图3.5所示。

continue语句的应用代码如下。

【例3.13】 continue语句在循环中的作用。（实例位置：资源包\TM\sl\3\5）

执行上述代码，结果如图3.6所示。

图3.5　break语句应用

图3.6　continue语句应用

分析上述两段代码。当循环语句中遇到break语句时将结束循环，因此第一段代码只执行了两次循环；当循环变量i的值为2时，终止循环，因此只输出了如图3.5所示的两个元素。而continue语句用于结束本次循环，进入下一次循环，因此在第二段代码中，当循环变量i的值为2时，结束了本次循环，进入下一次循环，导致没有输出数组中的第3个元素，因而出现了如图3.6所示的结果。

在2.5.1节中笔者曾经提到逗号表达式的一个主要应用是在for语句中修改循环变量。下面的代码演示了逗号表达式在for循环语句中的应用。

【例3.14】逗号表达式在for循环语句中的应用。

在使用for语句时，应该注意的一个问题是循环变量的作用范围。对于微软的C++编译器来说，循环变量的作用范围为当前定义循环处至当前函数或复合语句的结束。对于其他公司的C++编译器来说，其循环变量的作用范围可能仅限于循环语句内。例如，下面的代码在Visual C++ 6.0中是合法的。

【例3.15】循环变量的作用范围。

而下面的代码在Visual C++ 6.0中是非法的。

【例3.16】循环变量在复合语句中的作用范围。

上述代码中，循环语句位于一个复合语句内，因此循环变量i在复合语句结束时就失效了。因此，在复合语句之外访问循环变量i，将出现标识符没有定义的错误。

3.4.2　while语句

与for循环语句不同的是，while循环语句根据表达式的真假来判断是否进行循环，即当表达式的值为真时执行循环，当表达式的值为假时退出循环。while语句的语法格式如下。

下面利用while语句来计算1～100的累加和。

【例3.17】使用while语句累计求和。

注意

在使用while循环语句时，一定要设置循环结束条件，否则将进入死循环，这是许多初学者经常犯的错误。在上述代码中，如果不执行“i++；”语句使i自加1，程序将进入死循环。

3.4.3　do while语句

do while循环语句与while语句类似，只是它先执行一次循环体，然后才根据表达式的真假来判断循环是否结束。do while语句的语法格式如下。

下面利用do while语句来实现1～100的累加求和。

【例3.18】使用do while语句累计求和。

注意

在程序中使用do while语句时，不要忘记while语句部分在末尾要添加分号。

do while语句与while语句都是根据表达式的真假来判断是否结束循环，那这两个语句之间有什么区别呢？下面笔者举一个例子，假设有甲、乙两个人，甲习惯在吃东西前先看一下食品是否在保质期以内，而乙则是急性子，每次都是先打开食品，吃一口以后才看看食品是否过期。以循环语句来说，甲相当于while语句，乙相当于do while语句，吃东西相当于循环体，查看食品保质期相当于循环条件，通过上面的条件得知，甲在吃东西前先查看保质期，如果没过期则可以放心食用，如果过期则不会吃；而乙不同，乙在查看保质期时已经吃了一口，如果食品没过期则继续食用，但是如果食品已经过期，乙也无法改变已经吃了过期食品的事实。笔者举这个示例不是说while语句比do while语句好，只是要提醒读者，在使用do while语句时，一定要谨慎，以免发生不可挽回的后果。

3.4.4　嵌套循环语句

所谓嵌套循环语句，是指循环语句中还包含有循环语句。例如，在for语句中还可以包含for语句、while语句和do while语句，while语句同样也可以包含for语句和do while语句等。在程序中使用嵌套循环，可以实现复杂的逻辑操作，如开发人员经常使用嵌套循环语句来访问二维数组中的元素。

【例3.19】使用嵌套循环语句访问二维数组。（实例位置：资源包\TM\sl\3\6）

执行上述代码，结果如图3.7所示。

图3.7　嵌套循环语句

3.5　跳转语句

所谓跳转语句，是指能够影响程序执行顺序的语句。C++语言中主要提供了5种跳转语句，分别为break语句、continue语句、goto语句、return语句和exit语句。其中，break语句和continue语句在介绍for循环语句时已经作了介绍，本节主要介绍goto语句、return语句和exit语句。

3.5.1　goto语句

goto语句又称为无条件跳转语句，用于改变语句的执行顺序。goto语句的一般语法格式如下：

其中，标号是用户自定义的一个标识符，以分号结束。下面利用goto语句实现1～100的累加求和。

【例3.20】使用goto语句实现循环。

上述代码中，利用标签实现了原始的循环功能。当语句执行到“if（ivar <100）”时，如果条件为真，则不停地转到标签定义处。

注意

这是一种古老的跳转语句，它会使程序的执行顺序变得混乱，CPU需要不停地进行跳转，效率比较低，因此在开发程序时要慎用goto语句。

在使用goto语句时，应注意标签的定义。在定义标签时，其后不能紧接着出现“}”符号。例如，下面的代码是非法的。

在上述代码中，定义标签时，其后没有执行代码了，所以出现编译错误。如果程序中出现上述情况，可以在标签后添加一个空语句，以解决编译错误。例如：

在使用goto语句时还应注意，goto语句不能越过复合语句之外的变量定义的语句。例如，下面的goto语句是非法的。

在上述代码中，goto语句试图越过变量i的定义，导致编译错误。解决上述问题的方法是将变量的声明放在复合语句中。例如，下面的代码是合法的。

3.5.2　return语句

return语句用于退出当前函数（有关函数的介绍参见4.1节）的执行。当程序在当前函数中执行时，遇到return语句将退出当前函数的执行，返回到函数的调用处继续执行，如果当前函数是主函数（main函数），则退出应用程序。下面的代码演示了return语句的应用。

【例3.21】使用return语句提前结束函数。

在上述代码中，如果价格错误，将通过“return false;”语句提前结束函数。其中，false表示函数ValidateData的返回值，类型为bool。如果函数没有返回值，应只使用return语句。

【例3.22】使用return语句提前结束无返回值函数。

注意

在使用return语句提前结束函数时，需要注意如果代码之前在堆中分配了内存，则在return语句之前应释放内存，以防止产生内存泄漏。

【例3.23】使用return语句时注意内存泄漏问题。

3.5.3　exit语句

exit语句用于终止当前调用的进程，通常用于结束当前的应用程序。实际上，exit是一个退出当前调用进程的函数，它包含一个整型参数，标识进程退出代码。它与return语句不同，return语句只是退出当前调用的函数，除非当前函数是应用程序的主函数，return语句才结束当前调用进程；而exit语句直接结束当前调用进程，无论当前函数是否为应用程序主函数。下面的代码演示了exit语句的作用。

【例3.24】使用exit语句结束应用程序。

当调用ExitDemo函数传递一个0值时，将结束当前应用程序；传递一个非零值时，则ExitDemo函数不会进行任何操作。

3.6　小结

本章介绍了C++语言语句的构成、复合语句、分支语句、循环语句和跳转语句的相关知识，熟练地掌握这些语句可以解决各种复杂的逻辑操作。

3.7　实践与练习

1. 编写一组程序，判断某一年是否为闰年。（答案位置：资源包\TM\sl\3\7）

2. 编写一组程序，描述如下的数学函数，输入x，输出y。（答案位置：资源包\TM\sl\3\8）

3. 编写一组程序，输出1000以内的素数。（答案位置：资源包\TM\sl\3\9）

4. 编写一组程序，对数组中的元素逆序排列。（答案位置：资源包\TM\sl\3\10）

第4章

面向过程程序设计

（视频讲解：49分钟）

C++不属于纯粹的面向对象语言，使用C++语言还可以进行面向过程的程序开发。本章将介绍有关面向过程程序设计的相关知识，包括函数各种形式的定义、调用方法，以及函数模板、异常处理在程序中的使用。

通过阅读本章，您可以：

定义和使用函数

设计递归函数

理解命名空间的使用

定义函数模板

4.1　函数

通常一个应用程序需要包含多个子功能模块，其中每一个子功能都可以由函数来实现。对于C++语言来说，应用程序主要由一个主函数（main函数）和其他函数构成，其中主函数负责调用其他函数。对于函数来说，它可以调用自身（递归调用）或其他函数，也可以被其他函数调用。

如果说将一个程序比做一间公司的话，那么主函数（main函数）就是公司的董事会，也就是领导层，而其他策划、生产、销售等部门就是负责单一功能的函数，各个部门之间相互协调又各司其职，使公司的运作和管理更加流畅。而在程序中，每个函数完成一项功能，当程序出现问题时，只要根据功能就可以判断出是哪个函数出现了问题，使程序更加容易修改；由于函数可以完成指定的功能，因此也增强了可移植性。

读者通过上述的比喻可以了解函数在程序中所处的位置，从而更好地体会函数的存在价值。本节将详细介绍有关函数的相关知识。

4.1.1　定义和调用函数

1. 定义函数

对于C++语言来说，定义函数的一般格式如下。

通常函数都有一个返回值，当函数结束时，将返回值返回给调用该函数的语句。但是，函数也可以没有返回值，即返回值类型为void。函数名可以是任何合法的标识符。函数的参数列表是可选的，如果函数不需要参数，则可以省略参数列表，但是参数列表两边的括号不能省略。函数体描述的是函数的功能，主要由一条或多条语句构成。函数也可以没有函数体，此时的函数称为空函数。空函数不执行任何动作。在开发程序时，当前可能不需要某个功能，但是将来可能需要，此时可以定义一个空函数，在需要时为空函数添加实现代码。如果函数有返回值，通常在函数体的末尾使用return语句返回一个值，其类型必须与函数定义时的返回值类型相同或兼容。下面定义一个简单的求和函数，用于计算两个数的和。

【例4.1】定义简单求和函数。

在上面的代码中，定义了一个临时变量ret，用于记录参数x与y的和，然后返回ret。对于return语句来说，还可以直接返回一个表达式，表达式的结果将作为函数的返回值。例如：

为了更清晰地描述函数的返回值，可以在函数体中将返回的表达式使用括号括起来。例如：

下面再编写一个函数，用于输出十进制数的十六进制形式。

在上面的代码中定义了一个无返回值的Hex函数，用于输出十进制数的十六进制形式。众所周知，在定义同种类型的多个变量时，可以使用一条语句来定义。例如：

但是在定义函数参数时，如果多个参数具有相同的类型，则需要分别定义，不允许整体定义。例如，下面的函数定义是非法的。

注意

在定义函数时，如果函数不是void类型，则一定要在函数中加入return语句。

2. 调用函数

函数调用的一般格式如下：

下面以调用Hex函数为例，介绍函数调用的方法。

在调用函数时，如果函数有参数，注意实际参数的类型应与函数定义时的参数类型相同或兼容。对于没有参数的函数，在调用函数时只需要写函数名和括号即可。下面的代码演示了无参函数的定义及调用。

注意

在调用无参函数时，注意不要遗忘括号，这是许多初学者经常犯的错误。

对于具有返回值的函数，在调用函数时，需要获得函数的返回值。下面的代码演示了如何调用具有返回值的函数以及如何应用函数的返回值。

【例4.2】调用具有返回值的函数。（实例位置：资源包\TM\sl\4\1）

在调用函数时，如果当前函数处于被调用函数的下方，则需要对被调用函数进行前置声明。例如，下面的函数调用将是非法的。

【例4.3】非法的函数调用。

在上述代码中，main函数调用了sum函数，但是sum函数的定义处于main函数的下方，导致了sum标识符没有声明的错误。为什么会出现这种问题呢？其实很简单，假设有甲、乙两人，甲相当于编译器，乙相当于被调用的函数，而你就相当于主函数，这时，上述代码就好比你在和甲先生聊天时说到了乙先生，而甲、乙两人还不认识，甲先生自然会发出疑问，也就是在程序编译中发送的标识符有没有什么错误。为了解决上述问题，只要在你和甲先生聊天前先介绍一下乙先生的信息，这样就不会有问题发生了，如果体现在上述程序中，就是对sum函数进行前置声明，即在main函数的上方声明sum函数。例如：

【例4.4】前置声明函数。（实例位置：资源包\TM\sl\4\2）

注意

在对函数进行声明时，注意不要忘记在括号后面加上分号，这是许多初学者容易忽视的地方。

4.1.2　设置默认值参数

在调用有参函数时，如果经常需要传递同一个值到调用函数，则可以在定义函数时为参数设置一个默认值。这样在调用函数时便可以省略一些参数，此时程序将采用默认值作为函数的实际参数。下面的代码定义了一个具有默认值参数的函数。

【例4.5】创建默认值参数的函数。

下面来调用OutputInfo函数，其中一条语句利用参数的默认值调用函数，另一条语句直接为函数传递实际参数。

运行程序，效果如图4.1所示。

图4.1　默认值参数

从图4.1中可发现，调用函数时如果不设置参数，具有默认值参数的函数会自动按照默认值来运行，而在调用函数时设置了参数，函数则会按照设置的参数值来运行。

在定义函数默认值参数时，如果函数具有多个参数，应保证默认值参数出现在参数列表的右方，没有默认值的参数出现在参数列表的左方，即默认值参数不能出现在非默认值参数的左方。例如，下面的函数定义是非法的。

【例4.6】非法的默认值参数。

在上述代码中，默认值参数y出现在非默认值参数z的左方，导致了编译错误。正确的做法是将默认值参数放置在参数列表的右方。例如：

4.1.3　设置数组参数

在编写函数参数时，可以使用数组作为函数参数。例如，编写一个函数，按从小到大的顺序输出10个整数。如果每一个整数定义一个参数时需要定义10个参数，那么，输出100个整数呢？可以想象编写参数列表多么麻烦。如果使用数组作为函数参数，则可以大大节省编写参数列表的时间。下面的代码演示了使用数组作为函数参数。

【例4.7】使用数组作为函数参数。（实例位置：资源包\TM\sl\4\3）

执行上述代码，结果如图4.2所示。

在定义数组参数时，也可以不指定大小。在调用数组参数的函数时，C++编译器不对数组的长度进行检查，它只是将数组的首地址传递给了函数，因此可以对Sort函数进行修改，使其更具有灵活性。

图4.2　数组参数

说明

参数按引用方式传递，在4.1.4节中将详细介绍参数的传递方式。

【例4.8】使用动态数组作为函数参数。

在上述代码中，函数Sort可以对任意长度的数组进行排序，增强了灵活性。这里不是通过显示指定数组的长度，而是通过另一个参数标识数组的长度。但是有些读者可能会考虑，如何通过数组参数来限制函数调用时必须传递指定长度的数组呢？如函数包含一个10个元素的数组参数，如果用户只传递8个或11个元素的数组，该如何禁止用户的传递呢？换句话说，如何让编译器知道此类错误呢？解决的方法是使用数组的引用作为函数的参数。当数组的引用作为函数参数时，数组的长度将作为参数的一部分。下面仍以Sort函数为例，使用数组的引用作为函数参数。

【例4.9】使用数组的引用作为函数参数。

注意

在上述代码中注意Sort函数的定义，其中“int（&array）[10]”中的括号是不可省略的。如果省略了括号，“[]”运算符的优先级高于“&”运算符，便成了引用数组，而在C++语言中定义引用数组是非法的。

写成“int (&array) [10]”的格式是合法的，表示定义一个引用对象，它可以指向（严格地说应该是取代）具有10个元素的数组。这里回忆一下指针数组的定义。

这里如果对“*parray”使用括号括起来，其性质就变了。例如：

上述代码实际定义了一个整型指针，可以指向具有5个元素的数组。这与“int (&array) [10]”是类似的，“int (&array) [10]”表示定义一个引用，指向具有10个元素的整型数组。因此，以“int（&array）[10]”形式定义函数参数列表，编译器会强制检查数据元素的个数，如果不为10，会显示编译错误。对于上述的Sort函数，如果采用如下方式调用，将是非法的。

正确的调用方式为：

4.1.4　设置指针／引用参数

除了数组可以作为函数参数外，指针和引用也可以作为函数参数。在介绍指针和引用参数之前，先来介绍一下函数参数的传递方式。在C++语言中，函数参数的传递方式主要有两种，分别为值传递和引用传递。所谓值传递，是指在函数调用时，将实际参数的值复制一份传递到调用函数中。这样，如果在调用函数中修改了参数的值，其改变不会影响到实际参数的值。下面的例子演示了函数的按值传递方式。

【例4.10】按值方式传递参数。（实例位置：资源包\TM\sl\4\4）

执行上述代码，结果如图4.3所示。

从图4.3中可以发现，在调用ValuePass函数后，虽然在函数中修改了参数的值，但在函数调用后，变量ivar的值仍为5。

而引用传递则恰恰相反，如果函数按引用方式传递，在调用函数中修改了参数的值，其改变会影响到实际参数。下面的代码演示了使用指针作为函数参数，此时函数采用引用传递方式。

【例4.11】按引用方式传递参数。（实例位置：资源包\TM\sl\4\5）

执行上述代码，结果如图4.4所示。

图4.3　值传递

图4.4　引用传递

从图4.4中可以发现，当调用ValuePass函数修改参数的值为10时，变量ivar的值也随之改变了。之所以发生改变，是因为调用ValuePass函数时将变量ivar的地址传了过去，这样函数的参数与变量ivar的地址是相同的，因此修改了参数的值必然会影响到变量ivar的值。

技巧

函数的值传递和引用传递是如何区分的呢？实际上，通常在定义函数时，如果参数为数组、指针或引用类型，则函数采用引用传递方式，否则采用值传递方式。

下面以引用类型为例来演示函数引用参数的传递。

【例4.12】使用引用类型作为函数参数。（实例位置：资源包\TM\sl\4\6）

运行上述代码，结果与图4.4所示是相同的。使用指针或引用作为函数参数，均采用引用方式传递。那么在开发程序时究竟应该使用指针还是使用引用类型作为函数参数呢？实际上，使用指针和引用类型作为函数参数各有优缺点，视具体环境而定。对于引用类型，引用必须被初始化为一个对象，并且不能使它再指向其他对象，因为对引用赋值实际上是对目标对象赋值。这是引用类型的缺点，但也是引用类型的优点，因为在函数中不用验证引用参数的合法性。例如，下面的函数调用是非法的。

上述代码中，如果ValuePass采用指针作为函数参数，使用“ValuePass（0）；”语句调用是合法的，但是却带来了隐患，因为0被认为是空指针，对空指针操作必然会导致地址访问错误。因此对于指针对象作为函数参数，函数体中需要验证指针参数是否为空。这是使用指针类型作为函数参数的缺点。但是，使用指针对象作为函数参数，用户可以随意修改指针参数指向的对象，这是引用类型参数所不能的。

4.1.5　省略号参数

在本章和其他章节的代码中多次使用了printf函数输出信息。当在程序中调用该函数时，其参数列表会显示省略号，如图4.5所示。

省略号参数代表的含义是函数的参数是不固定的，可以传递一个或多个参数。对于printf函数来说，可以输出一项信息，也可以同时输出多项信息。例如：

那么如何在程序中定义省略号参数函数呢？可以按如下方式定义：

对于上述方式的函数，在编写函数体时需要一一读取用户传递的实际参数。可以使用va_list类型和va_start、va_arg、va_end 3个宏读取传递到函数中的参数值。下面以一个具体的示例介绍省略号参数函数的定义及使用。

【例4.13】定义省略号形式的函数参数。（实例位置：资源包\TM\sl\4\7）

执行上述代码，结果如图4.6所示。

图4.5　省略号参数

图4.6　省略号参数执行结果

说明

要使用va_list类型和va_start、va_arg、va_end 3个宏，需要引用cstdarg头文件。

4.1.6　内联函数

所谓内联函数，是指对于程序中出现函数调用的地方，如果函数是内联函数，编译器则直接将函数代码复制到函数调用的地方，这样省去了跳转到函数定义的地方执行代码，然后再返回到调用函数处的一个过程，提高了程序的执行效率。内联函数最大的一个缺点是增加了程序代码，可以想象一下，如果一个内联函数有上千行的代码，程序中多次出现该函数的调用，执行程序的代码将是多么庞大。但是，对于代码较少、经常需要调用的函数，将其定义为内联函数，则可以显著提高程序执行效率。在C++语言中，可以使用inline关键字定义内联函数。例如：

对于使用inline关键字声明的内联函数，程序不一定将函数作为内联函数对待。可以认为inline关键字只是对编译器的一个建议，编译器是否将其作为内联函数依赖于编译器的优化机制。对于微软的C++编译器来说，可以使用_forceinline关键字强制编译器将该函数作为内联函数。

注意

在调用内联函数时，每次都会将函数代码复制到被调用函数处，这样会导致应用程序变大、执行速度变慢。对于经常使用的、代码较少的函数，可以使用内联函数；对于代码较多的函数，不应该使用内联函数。

4.1.7　重载函数

所谓重载函数，是指多个函数具有相同的函数名称，而参数类型或参数个数不同。函数调用时，编译器以参数的类型及个数来区分调用哪个函数。例如，下面的代码定义了两个重载函数。

【例4.14】定义重载函数。（实例位置：资源包\TM\sl\4\8）

执行上述代码，结果如图4.7所示。

从图4.7中可以发现，语句“int ivar=Add（5,2）；”调用的是第1个Add函数，而语句“float fvar=Add（10.5,11.4）；”调用的是第2个Add函数。考虑这样一种情况，再定义一个Add函数，其参数类型为float类型，那么“float fvar = Add（10.5,11.4）；”语句将调用哪个函数呢？

图4.7　重载函数

答案是调用第2个Add函数，如果改为如下的调用形式，则调用第3个重载函数。

在定义重载函数时，应注意以下几点。

（1）函数的返回值类型不作为区分重载函数的一部分。

下面的函数重载是非法的。

（2）对于普通的函数参数来说，const关键字不作为区分重载函数的标识。

下面的函数重载是非法的。

但是如果参数的类型是指针或引用类型，const关键字则将作为重载函数的标识。因此，下面的函数重载是合法的。

（3）参数的默认值不作为区分重载函数的标识。

下面的函数重载是非法的。

（4）使用typedef自定义类型不作为重载的标识。

当函数使用了typedef自定义类型作为参数类型时，如果另一个函数的参数类型与自定义类型的原始类型相同，则函数的重载是非法的。例如：

上述代码的函数重载是非法的，因为typedef不是创建新的数据类型，所以编译器认为上面的两个函数属于同一个函数，不能区分重载函数。

（5）局部域中声明的函数将隐藏而不是重载全局域中的函数。

下面的代码定义了3个重载函数，但是在主函数内部前置声明了第3个重载函数，此时第3个重载函数将隐藏而不是重载其他函数。下面代码中主函数main中的函数调用是非法的。

上述代码中，在main函数内部（独立域）前置声明了第3个重载函数，这将导致第1个、第2个函数被隐藏，语句“Validate（10.5f）；”试图调用第1个重载函数，导致编译错误。为了能够访问被隐藏的函数，需要使用域运算符“::”。在main函数中将“Validate（10.5f）；”语句修改为“::Validate（10.5f）；”语句即可通过编译。有关域运算符的相关知识参见4.2节。

4.1.8　函数递归调用

函数递归是指函数直接或间接调用其本身。递归的直接调用是指在函数体中再次调用该函数。递归的间接调用是指函数调用另一个函数，而被调用函数又调用了第一个函数。在编写程序时，使用递归可以简化问题的求解方法。例如，下面的代码利用递归求n的阶乘。

【例4.15】利用递归求n的阶乘。

在上述代码中，factorial函数实现了计算n的阶乘。以n等于4为例，4!等于4*3!，3!等于3*2!，…，1!等于1。当计算4的阶乘时，只要知道3的阶乘就可以了，4*3!等于4!。同理，计算3的阶乘，只要知道2的阶乘就可以了，以此类推。1的阶乘为1，知道了1的阶乘，就可以计算2的阶乘，知道2的阶乘就可以计算3的阶乘……

对于递归函数来说，必须具有终止条件，无限制的递归将导致程序崩溃。对于上述factorial函数来说，递归的终止条件是n等于1或等于0。

注意

递归函数会增加系统的额外开销，导致程序性能下降，因此开发程序时应尽量避免使用递归。

在上面的递归函数中，如果传递一个很大的数，会导致堆栈溢出，因为每调用一个函数，系统便会为函数的参数分配堆栈空间。上述的递归函数factorial完全可以用连续乘积的方式实现。

【例4.16】利用循环求n的阶乘。

4.1.9　函数指针

对于C++语言来说，函数名实际上是指向函数的指针。理解这一点，就可以定义一个普通的函数指针，使其指向某一类型的函数。例如，下面的代码定义了一个指向具有两个整型参数的函数指针。

也可以使用typedef定义一个函数指针类型，然后定义该类型的变量。例如：

当定义了一个函数指针后，可以将已经定义的函数（函数的返回值、参数个数、参数类型必须与函数指针定义的形式相同）赋值给函数指针，通过函数指针调用指向的函数。例如：

【例4.17】使用函数指针调用函数。

对于函数指针来说，它指向的函数必须与函数指针定义的函数形式相同，即返回值类型相同、参数类型相同、参数个数相同。这与函数重载是不同的，函数重载不以函数的返回值类型为依据，但是函数指针则不同，如果将上面的sum函数的返回值类型修改为double类型，则上述代码将无法编译。

在程序中使用函数指针的好处是增强程序的灵活性。例如，编写一个通用的函数，实现两个数的加、减、乘、除操作。

【例4.18】使用函数指针指向不同的函数。（实例位置：资源包\TM\sl\4\9）

上述代码的main函数中，同样的“Invoke（20,10,pfun）；”语句，由于pfun指向了不同的函数，因此Invoke函数实现的功能也不相同。如果用户的程序有改动，例如需要实现两个数的求余运算，只需要再编写一个求余函数，将其赋值给pfun函数指针，Invoke函数则无须进行任何改动。

对于指针变量来说，用户可以定义指针数组；对于函数指针，用户也可以定义函数指针数组。例如，下面的代码定义了一个函数指针数组。

下面列举一个示例，通过遍历函数指针数组来调用不同的函数。

【例4.19】通过遍历函数指针数组来调用不同的函数。（实例位置：资源包\TM\sl\4\10）

执行上述代码，结果如图4.8所示。

图4.8　函数指针数组

4.2　作用域和生命期

作用域和生命期描述了常量、变量、函数等对象的适用范围。当程序代码中的这些对象超出了其适用范围时将出现编译错误，这经常出现在面向过程的程序开发中。本节将介绍有关对象的作用域和生命期的相关知识。

4.2.1　局部作用域

局部作用域描述的是函数体中变量、常量等对象的作用范围。每个函数都有一个独立的局部作用域，在函数体中定义的变量、常量，对外部函数是不可见的，因为它处于函数的局部作用域中。对于函数体中出现的复合语句，也有其独立的局部作用域，在其中定义的对象，复合语句之外是不能访问的。例如：

在上述代码中，变量ivar的作用范围处于复合语句的作用域中，因此语句“printf（“%d”,ivar）；”访问变量ivar是非法的。

对于处于同一局部作用域的对象来说，对象是不允许重名的。例如：

在上述代码中，语句“char ivar;”定义的变量与整型变量ivar同名，并且处于同一局部作用域中，因此出现编译错误。同样，语句“int argc;”与参数arge同名，并且处于同一局部作用域中，因此也出现了编译错误。对于函数参数来说，其作用域为函数的局部作用域。复合语句部分定义的两个变量是合法的，它们虽然与变量ivar和函数参数同名，但是处于不同的局部作用域。

注意

在定义变量时，即使不在同一局部作用域，也不要定义同名的变量。在开发程序的过程中，同名的变量很容易被混淆，一旦程序出现错误，阅读起来会很复杂。

下面介绍一下编译器对局部作用域内变量命名解析的过程。当编译器在当前代码处发现变量名时，它将在当前的局部作用域内搜索变量的定义，如果没有发现变量的定义，则向外一层的局部作用域搜索变量的定义，直到搜索完局部作用域。如果还没有发现变量的定义，则会出现标识符没有定义的错误。如果在嵌套局部作用域的外层和内部作用域定义了同名的变量，对于内部的局部作用域变量来说，它将隐藏外层局部作用域中的变量。例如：

【例4.20】隐藏外部局部作用域中的变量。

在上述代码中，复合语句中的printf函数输出的ivar值为10，而外层的printf函数输出的ivar值为5。在复合语句中，定义的变量ivar（值为10）隐藏了外部局部作用域定义的变量ivar（值为5）。

下面对上述代码稍加修改，分析输出结果。

在上面的复合语句中，两次调用了printf函数（黑体部分代码），第1次调用printf函数输出的ivar值为5，因为在当前复合语句的局部作用域内没有发现变量ivar的定义，编译器在外层的局部作用域中发现了ivar的定义。而第2个printf函数输出的ivar值为10，因为编译器在复合语句的局部作用域中直接发现了变量ivar的定义。

4.2.2　全局作用域

全局作用域是指函数、变量、常量等对象的作用范围是整个应用程序，这些对象在整个应用程序中都是可用的。在全局作用域内定义的对象被称为全局对象。例如，在全局作用域内定义的函数被称为全局函数，在全局作用域内定义的变量称为全局变量。

说明

全局对象的生命期开始于应用程序的运行，结束于应用程序的退出。

下面以定义全局变量为例，介绍全局对象的定义。全局变量的定义与局部变量的定义相同，只是在函数外部进行定义。例如：

对于全局变量来说，如果没有进行初始化，其存储区为0。因此，对于整型的全局变量，如果没有进行初始化，其值为0。但是，对于局部变量来说，如果没有进行初始化或赋值，其值是不可预见的。此外，在整个应用程序中，一个全局变量只能定义一次，不能重名。

如果在函数内部定义了一个与全局变量同名的局部变量，则全局变量被隐藏，如果需要访问全局变量，需要使用域运算符“::”。

【例4.21】使用域运算符访问被隐藏的全局变量。（实例位置：资源包\TM\sl\4\11）

执行上述代码，结果如图4.9所示。

图4.9　访问隐藏的全局变量

说明

至于在当前文件中访问其他文件中的全局变量，可以使用extern关键字进行声明，可参考第2.2.5节。

4.2.3　定义和使用命名空间

在一个应用程序的多个文件中可能会存在同名的全局对象，这样会导致应用程序的链接错误。使用命名空间是消除命名冲突的最佳方式。命名空间的定义格式如下。

例如，下面的代码定义了两个命名空间。

【例4.22】定义命名空间。（实例位置：资源包\TM\sl\4\12）

如果使用命名空间中的对象，需要在对象前使用命名空间名作为前缀。例如，下面的函数调用是非法的。

应使用命名空间作为前缀。上述代码应修改为：

如果需要访问同一个命名空间中的多个对象，可以使用using命令引用整个命名空间对象，这样就不必在每个对象前添加命名空间前缀了。例如：

using命令的作用域是从当前引用处到当前作用域的结束。如果将using命令放置在复合语句中，在复合语句结束时，using命令的作用域也随之结束了。例如，下面的函数调用是非法的，因为using命令的作用域已经结束了。

如果在函数中定义的局部变量与命名空间中的变量同名时，命名空间中的变量将被隐藏。例如：

上述代码中局部变量ivar隐藏了命名空间Output中的变量，因此第1个printf函数输出的值为5。为了访问被隐藏的Output命名空间中的变量ivar，需要使用命名空间作为前缀，如“Output::ivar”，尽管前文已经使用using命令引用命名空间Output。

如果程序中使用using命令，同时还引用多个命名空间，并且命名空间中存在相同的函数，将导致歧义，出现编译错误。例如：

解决的方法是必须使用具体命名空间进行区分。例如：

对于同一个命名空间，可以在多个文件中定义。此时，各个文件中的对象将处于同一个命名空间中。例如，在main.cpp头文件中定义了一个命名空间Output。

【例4.23】在多个文件中定义命名空间。

在login.cpp头文件中还可以定义Output命名空间。例如：

此时，命名空间Output中的内容为两个文件Output命名空间内容的“总和”。

注意

如果在login.cpp文件的Output命名空间中定义一个整型变量ivar将是非法的，因为它与main.cpp文件中Output命名空间中的变量同名。

在定义命名空间时，通常在头文件中声明命名空间中的函数，在源文件中定义命名空间中的函数，将程序的声明与实现分开。例如，在头文件中声明命名空间中的函数。

在源文件中定义函数。

在源文件中定义函数时，注意要使用命名空间名作为前缀，表明实现的是命名空间中定义的函数，否则将是定义一个全局函数。

类似于复合语句，命名空间也可以嵌套。例如，下面的代码定义了一个嵌套的命名空间。

【例4.24】定义嵌套的命名空间。

在上述代码中，Windows命名空间中又定义了一个命名空间GDI，如果在程序中要访问GDI命名空间中的对象，可以使用外层的命名空间和内层的命名空间作为前缀。例如：

用户也可以直接使用using命令引用嵌套的GDI命名空间。例如：

在上述代码中，“using namespace Windows::GDI;”语句只是引用了嵌套在Windows命名空间中的GDI命名空间，并没有引用Windows命名空间，因此试图访问Windows命名空间中定义的对象是非法的。例如：

注意

using命令用于引用一个命名空间中所有的命名空间对象，容易和正在编写的程序中的局部变量发生冲突，导致命名空间中的变量被隐藏，所以不建议过多地使用using命令。

在使用namespace关键字定义命名空间时，也可以不指定命名空间的名称，此时的命名空间称为未命名的命名空间。

【例4.25】定义未命名的命名空间。

对于未命名的命名空间来说，其最大特点就是命名空间中的对象只适用于当前文件，各个文件之间不能相互访问定义在各自未命名空间中的对象。这一特点使得未命名空间中的对象与全局静态（static）对象（对于全局静态对象来说，只属于半个全局对象，它只能在当前文件中使用）所起的作用相同。多数C++编译器都支持命名空间，使得越来越多的开发程序更愿意使用未命名的命名空间来代替全局静态对象。对于未命名的命名空间来说，访问其中定义的对象与访问普通的全局对象是相同的。例如：

注意

在定义未命名的命名对象时，注意其中的对象不能与全局对象同名或相同（函数可以重名，表示函数重载，但是参数列表不能完全相同）。

例如，下面的代码是非法的。

在上述代码中，全局函数PutoutText与未命名空间中的PutoutText函数声明格式（函数原型）相同，导致编译器产生了歧义。如果将未命名空间定义为有名称的命名空间（如namespace windows），则不会出现错误。

4.3　函数模板

函数模板提供了一种机制，使函数的返回值、参数类型能够被参数化，而函数体保持不变。这极大地增强了函数的灵活性。例如，编写一个函数，能够实现两个整数相加，返回值为整数；能够实现两个实数相加，返回值为实数……使用函数模板可以轻易地实现这样的一个函数。本节将详细介绍有关函数模板的相关知识。

4.3.1　定义和使用函数模板

C++语言提供了template关键字用于定义模板。下面以编写一个求和函数为例介绍如何使用template定义函数模板。

其中，template为关键字，表示定义一个模板（可以是函数模板或类模板，类模板将在第5章进行介绍）。尖括号<>表示模板参数。模板参数主要有两种，一种是模板类型参数，另一种是模板非类型参数。上述代码中定义的模板使用的是模板类型参数，模板类型参数使用关键字class或typedef开始（本例使用的是class，也可以使用typedef代替。在定义函数模板时，class与typedef关键字的作用是相同的），其后是一个用户定义的合法的标识符（本例为type，也可以是其他合法标识符）。模板非类型参数与普通参数定义相同，它通常为一个常数，如标识数组的长度。

在定义完函数模板后，需要在程序中调用函数模板。下面的代码演示了Sum函数模板的调用。

如果采用如下形式调用Sum函数模板，将会出现错误。

在上述代码中，为函数模板传递了两个类型不同的参数，编译器产生了歧义。如果用户在调用函数模板时显示标识模板类型，就不会出现错误了。例如：

下面再定义一个函数模板，实现获取数组元素的最大值。

【例4.26】定义函数模板，获取数组元素的最大值。

上述代码定义了一个函数模板Max，其中模板参数使用了模板类型参数type和模板非类型参数len。下面的代码演示了函数模板Max的调用。

4.3.2　重载函数模板

函数可以重载，同样，函数模板也可以重载。下面的代码定义了两个重载的函数模板。

【例4.27】定义重载的函数模板。（实例位置：资源包\TM\sl\4\13）

上述代码中定义了两个重载的函数模板Sum，第1个Sum函数模板实现了两个数的求和运算，第2个Sum函数模板实现了数组元素的求和运算。下面的代码演示了重载函数模板的调用。

【例4.28】调用重载的函数模板。（实例位置：资源包\TM\sl\4\13）

执行上述代码，结果如图4.10所示。

图4.10　重载函数模板

4.4　小结

本章详细介绍了有关函数的相关知识，包括各种形式函数的定义及调用，此外还介绍了对象的作用域和生命期、函数模板等知识。在开发面向过程的应用程序时，用户需要熟练地掌握函数的编写及应用，同时需要清楚地了解对象的作用范围，而本章介绍的相关知识正是进行面向过程程序开发必须掌握的基本知识，希望读者能够熟练掌握并灵活运用。

4.5　实践与练习

1. 编写一个动态参数的函数，使函数能够适应不同的参数个数。（答案位置：资源包\TM\sl\4\14）

2. 编写一个函数，利用指针作为参数，实现两个数的互换。（答案位置：资源包\TM\sl\4\15）

3. 用C++语言编写一个递归函数，遍历指定目录下的所有文件。（答案位置：资源包\TM\sl\4\16）

4. 编写一个函数模板，实现对各种数据类型数组进行从小到大排序。（答案位置：资源包\TM\sl\4\17）

第5章

面向对象程序设计

（视频讲解：1小时17分钟）

C++语言在C语言的基础上提供了面向对象的功能，使用C++语言既可以开发面向过程的应用程序，也可以开发面向对象的应用程序。第4章介绍了C++语言面向过程程序开发的相关知识，本章将介绍C++语言提供的面向对象功能。

通过阅读本章，您可以：

理解类和对象的概念

定义并实例化类对象

实现运算符重载

理解多重集成和嵌套类

定义和使用类模板

编写异常处理语句

5.1　类和对象

面向对象最大的特征就是提出了类和对象的概念。在以面向对象的方式开发应用程序时，将遇到的各种事物抽象为类，类中包含数据和操作数据的方法，用户通过实例化类对象来访问类中的数据和方法。举例来说，杯子是一个类，那么茶杯是该类的对象，酒杯也是该类的对象，玻璃杯、塑料杯同样都是杯子类的对象。本节将介绍有关类和对象的相关知识。

5.1.1　类的定义

C++语言中类和结构体类似，其中可以定义数据和方法。C++语言提供了class关键字定义类，其语法格式如下。

类的定义包含两部分，即类头和类体。类头由class关键字和类名构成；类体由一组大括号“{}”和一个分号“；”构成。类体中通常定义类的数据和方法，其中数据描述的是类的特征（也被称为属性）；方法实际上是类中定义的函数，描述的是类的行为。下面的代码定义了一个CUser类。

【例5.1】定义一个CUser类。

上述代码定义了一个CUser类，其中包含两个数据成员和一个方法。对于方法的定义，直接放在了类体中；此外，也可以将方法放在类体的外面进行定义。

【例5.2】将方法放置在类体之外。

当方法的定义放置在类体外时，方法的实现部分首先是方法的返回值，然后是方法名称和参数列表，最后是方法体。

说明

当方法的定义放置在类体外时，方法名称前需要使用类名和域限定符“::”来标记方法属于哪一个类。

注意

在定义类的数据成员时，注意不能像定义不同变量一样进行初始化。

例如，下面的类定义是错误的。

【例5.3】不能直接对类数据成员进行初始化。

注意

在定义类时，不要忘记末尾的分号，这是初学者最容易也最经常犯的一个错误。

5.1.2　类成员的访问

类成员主要是指类中的数据成员和方法（方法也被称为成员函数）。在定义类时，类成员是具有访问限制的。C++语言提供了3个访问限定符用于标识类成员的访问，分别为public、protected和private。public成员也被称为公有成员，该成员可以在程序的任何地方进行访问。protected成员被称为保护成员，该成员只能在该类和该类的派生类（子类）中访问，除此之外，程序的其他地方不能访问保护成员。private成员被称为私有成员，该成员只能在该类中访问，派生类以及程序的其他地方均不能访问私有成员。如果在定义类时没有指定访问限定符，默认为private，如5.1.1节中定义的CUser类。

下面重新定义CUser类，将各个成员设置为不同的访问级别。

【例5.4】设置类成员的不同访问级别。

在上述代码中，读者需要注意的是GetUsername方法的定义，在方法的最后使用了const关键字。在定义类方法时，如果不需要在方法中修改类的数据成员，建议在方法声明的最后使用const关键字，表示用户不能在该方法中修改类的数据成员。例如，如果在GetUsername方法中试图修改m_Password成员或m_Username成员将是非法的。

如果类中包含有指针成员，在const方法中不可以重新为指针赋值，但是可以修改指针所指向地址中的数据。例如：

在定义类后，需要访问类的成员。通常类成员（静态成员除外）的访问是通过对象实现的，对象被称为类的实例化。当程序中定义一个类时，并没有为其分配存储空间，只有当定义类的对象时，才分配存储空间。对象的定义与普通变量的定义是相同的，下面的代码定义了一个CUser类的对象user。

定义了类的对象后，即可访问类的成员。例如：

类成员是具有访问权限的，如果类的外部访问私有或受保护的成员将出现访问错误。例如：

上述代码试图访问CUser类的私有成员m_Password，产生了编译错误。

在定义类对象时，也可以将类对象声明为一个指针。例如：

程序中可以使用new运算符来为指针分配内存。例如：

也可以写成如下形式：

说明

如果类对象被定义为指针，需要使用“->”运算符来访问类的成员，而不能使用“. ”运算符来访问。

例如：

如果将类对象定义为常量指针，则对象只允许调用const方法。例如：

5.1.3　构造函数和析构函数

每个类都具有构造函数和析构函数。其中，构造函数在定义对象时被调用，析构函数在对象释放时被调用。如果用户没有提供构造函数和析构函数，系统将提供默认的构造函数和析构函数。

1. 构造函数

构造函数是一个与类同名的方法，可以没有参数、有一个参数或多个参数，但是构造函数没有返回值。如果构造函数没有参数，该函数被称为类的默认构造函数。下面的代码显式地定义了一个默认的构造函数。

【例5.5】定义默认的构造函数。（实例位置：资源包\TM\sl\5\1）

说明

如果用户为类定义了构造函数，无论是默认构造函数还是非默认构造函数，系统均不会提供默认的构造函数。

下面定义一个CUser类的对象，它将调用用户定义的默认构造函数。

执行上述代码，结果如图5.1所示。

从图5.1中可以发现，在定义user对象时，调用了默认的构造函数对数据成员进行了赋值。下面再定义一个非默认的构造函数，使用户能够在定义CUser类对象时为数据成员赋值。

【例5.6】定义非默认的构造函数。（实例位置：资源包\TM\sl\5\2）

下面定义两个CUser对象，分别采用不同的构造函数。

执行上述代码，效果如图5.2所示。

图5.1　默认构造函数

图5.2　非默认构造函数

说明

一个类可以包含多个构造函数，各个构造函数之间通过参数列表进行区分。

从图5.2所示的输出结果可以发现，语句“CUser Customer（"SK"，"songkun"）；”调用了非默认的构造函数，通过传递两个参数初始化CUser类的数据成员。如果想要定义一个CUser对象的指针，并调用非默认构造函数进行初始化，可以采用如下形式。

在定义常量或引用时，需要同时进行初始化。5.1.1节中已经介绍了在类体中定义数据成员时，为其直接赋值是非法的，那么如果在类中包含常量或引用类型数据成员时该如何初始化呢？

类的构造函数通过使用“：”运算符提供了初始化成员的方法。

【例5.7】在构造函数中初始化数据成员。

上述代码在定义CBook类的构造函数时，对数据成员m_Price和m_ChapterNum进行了初始化。

说明

编译器除了能够提供默认的构造函数外，还可以提供默认的复制构造函数。当函数或方法的参数采用按值传递时，编译器会将实际参数复制一份传递到被调用函数中，如果参数属于某一个类，编译器会调用该类的复制构造函数来复制实际参数到被调用函数。复制构造函数与类的其他构造函数类似，以类名作为函数的名称，但是其参数只有一个，即该类的常量引用类型。因为复制构造函数的目的是为函数复制实际参数，没有必要在复制构造函数中修改参数，因此参数定义为常量类型。

下面的代码为CBook类定义了一个复制构造函数。

【例5.8】定义复制构造函数。（实例位置：资源包\TM\sl\5\3）

下面定义一个函数，以CBook类对象为参数，演示在按值传递函数参数时调用了复制构造函数。

执行上述代码，结果如图5.3所示。

从图5.3中可以发现，执行“CBook book;”语句时调用了构造函数，输出了“构造函数被调用”的信息。执行“OutputBookInfo（book）；”语句首先调用复制构造函数，输出“复制构造函数被调用”的信息，然后执行OutputBookInfo函数，输出m_BookName成员的信息。

注意

如果对OutputBookInfo函数进行修改，以引用类型作为函数参数，将不会执行复制构造函数。

【例5.9】引用类型作为函数参数。（实例位置：资源包\TM\sl\5\4）

执行上述代码，结果如图5.4所示。

图5.3　复制构造函数1

图5.4　复制构造函数2

从图5.4中可以发现，复制构造函数没有被调用。因为OutputBookInfo函数是以引用类型作为参数，函数参数按引用的方式传递，直接将实际参数的地址传递给函数，不涉及复制参数，所以没有调用复制构造函数。

技巧

编写函数时，尽量按引用的方式传递参数，这样可以避免调用复制构造函数，极大地提高了程序的执行效率。

2. 析构函数

在介绍完构造函数后，下面介绍一下析构函数。析构函数在对象超出作用范围或使用delete运算符释放对象时被调用，用于释放对象占用的空间。如果用户没有显式地提供析构函数，系统会提供一个默认的析构函数。析构函数也是以类名作为函数名，与构造函数不同的是，在函数名前添加一个“~”符号，标识该函数是析构函数。析构函数没有返回值，甚至void类型也不可以；析构函数也没有参数，因此不能够重载。这是析构函数与普通函数最大的区别。下面为CBook类添加一个析构函数。

【例5.10】定义析构函数。（实例位置：资源包\TM\sl\5\5）

为了演示析构函数的调用情况，定义一个CBook类对象。

执行上述代码，结果如图5.5所示。

上述代码中定义了一个CBook对象book，当执行“CBook book;”语句时将调用构造函数输出“构造函数被调用”的信息；然后执行“printf（"定义一个CBook类对象\n"）；”语句输出一行信息；最后在函数结束时，也就是book对象超出了作用域时调用析构函数释放book对象，因此输出了“析构函数被调用”的信息。

图5.5　析构函数

5.1.4　内联成员函数

在定义函数时，可以使用inline关键字将函数定义为内联函数。在定义类的成员函数时，也可以使用inline关键字将成员函数定义为内联成员函数。

说明

其实对于成员函数来说，如果其定义是在类体中，即使没有使用inline关键字，该成员函数也被认为是内联成员函数。

例如，定义一个内联成员函数。

【例5.11】定义内联成员函数。

在上述代码中，GetUsername函数即为内联成员函数，因为函数的定义处于类体中。此外，还可以使用inline关键字表示函数为内联成员函数。

【例5.12】使用inline关键字定义内联成员函数。

此外，还可以在类成员函数的实现部分使用inline关键字标识函数为内联成员函数。例如：

说明

对于内联成员函数来说，程序会在函数调用的地方直接插入函数代码，如果函数体语句较多，将会导致程序代码膨胀。因此，将类的析构函数定义为内联成员函数，可能会导致潜在的代码膨胀。

分析下面的代码（假设CBook类的析构函数为内联成员函数）。

【例5.13】将类的析构函数定义为内联成员函数，可能会导致潜在的代码膨胀。

由于CBook类的析构函数是内联成员函数，因此上述代码在每一个return语句之前，析构函数均会被展开。因为return语句表示当前函数调用结束，所以book对象的生命期也就结束了，自然调用其析构函数。

根据上述分析，main函数中switch语句的编写是非常不明智的，下面对其进行修改，将return语句替换为break语句。

【例5.14】 switch语句的注意事项。

通过修改switch语句，避免了可能产生的代码膨胀，这也是使用switch语句应该注意的事项。

5.1.5　静态类成员

本节之前所定义的类成员都是通过对象来访问的，不能通过类名直接访问。如果将类成员定义为静态类成员，则允许使用类名直接访问。静态类成员是在类成员定义前使用static关键字标识。

【例5.15】定义静态数据成员。

在定义静态数据成员时，通常需要在类体外部对静态数据成员进行初始化。例如：

对于静态数据成员来说，不仅可以通过对象访问，还可以直接使用类名访问。例如：

在一个类中，静态数据成员是被所有的类对象所共享的。这就意味着无论定义多少个类对象，类的静态数据成员只有一份；同时，如果某一个对象修改了静态数据成员，其他对象的静态数据成员（实际上是同一个静态数据成员）也将改变。

【例5.16】类对象共享静态数据成员。（实例位置：资源包\TM\sl\5\6）

执行上述代码，结果如图5.6所示。

由于静态数据成员m_Price被所有CBook类对象所共享，因此Book对象修改了m_Price成员，将影响到vcBook对象对m_Price成员的访问。

图5.6　静态数据成员

对于静态数据成员，还需要注意以下几点。

（1）静态数据成员可以是当前类的类型，而其他数据成员只能是当前类的指针或引用类型。

在定义类成员时，对于静态数据成员，其类型可以是当前类的类型，而非静态数据成员则不可以，除非数据成员的类型为当前类的指针或引用类型。

【例5.17】静态数据成员可以是当前类的类型。

（2）静态数据成员可以作为成员函数的默认参数。

在定义类的成员函数时，可以为成员函数指定默认参数，其参数的默认值也可以是类的静态数据成员，但是普通的数据成员则不能作为成员函数的默认参数。

【例5.18】静态数据成员可以作为成员函数的默认参数。

在介绍完类的静态数据成员后，下面介绍类的静态成员函数。定义类的静态成员函数与定义普通的成员函数类似，只是在成员函数前添加static关键字。例如：

类的静态成员函数只能访问类的静态数据成员，而不能访问普通的数据成员。例如：

在上述代码中，语句“printf（"%d\n",m_Pages）；”是错误的，因为m_Pages是非静态数据成员，不能在静态成员函数中访问。

注意

静态成员函数不能定义为const成员函数，即静态成员函数末尾不能使用const关键字。

例如，下面静态成员函数的定义是非法的。

在定义静态成员函数时，如果函数的实现代码处于类体之外，则在函数的实现部分不能再标识static关键字。例如，下面的函数定义是非法的。

上述代码如果去掉static关键字则是正确的。

5.1.6　隐藏的this指针

对于类的非静态成员，每一个对象都有自己的一份备份，即每个对象都有自己的数据成员和成员函数。

【例5.19】每一个对象都有自己的一份备份。（实例位置：资源包\TM\sl\5\7）

执行上述代码，结果如图5.7所示。

从图5.7中可以发现，vbBook和vcBook两个对象均有自己的数据成员m_Pages，在调用OutputPages成员函数时，输出的均是自己的数据成员。在OutputPages成员函数中只是访问了m_Pages数据成员，那么每个对象在调用OutputPages方法时是如何区分自己的数据成员的呢？答案是通过this指针。在每个类的成员函数（非静态成员函数）中都隐含包含一个this指针，指向被调用对象的指针，其类型为当前类类型的指针类型（在const方法中，为当前类类型的const指针类型）。当vbBook对象调用OutputPages成员函数时，this指针指向vbBook对象；当vcBook对象调用OutputPages成员函数时，this指针指向vcBook对象。在OutputPages成员函数中，用户可以显式地使用this指针访问数据成员。例如：

图5.7　访问对象的数据成员

实际上，编译器为了实现this指针，在成员函数中自动添加了this指针用于对数据成员或方法的访问，类似于上面的OutputPages方法。

说明

为了将this指针指向当前调用对象，并在成员函数中能够使用，在每个成员函数中都隐含包含一个this指针作为函数参数，并在函数调用时将对象自身的地址隐含作为实际参数传递。

以OutputPages成员函数为例，编译器将其定义为：

在对象调用成员函数时，传递对象的地址到成员函数中。以“vc.OutputPages()；”语句为例，编译器将其解释为“vbBook.OutputPages（&vbBook）；”。这样就使得this指针合法，并能够在成员函数中使用。

5.1.7　运算符重载

定义两个整型变量后，可以对两个整型变量进行加运算。如果定义两个类对象，它们能否进行加运算呢？答案是不可以。两个类对象是不能直接进行加运算的，因此下面的语句是非法的。

但是，如果对“+”运算符进行重载，则可以实现两个类对象的加运算。

说明

运算符重载是C++语言提供的一个重要特性，允许用户对一些编译器提供的运算符进行重载，以实现特殊的含义。

下面以为CBook类添加运算符重载函数为例来演示如何进行运算符重载。

【例5.20】为类添加运算符重载函数。

在上述代码中，为CBook类实现了“+”运算符的重载。运算符重载需要使用operator关键字，其后是需要重载的运算符，参数及返回值根据实际需要来设置。通过为CBook类实现“+”运算符重载，允许用户实现两个CBook对象的加运算。例如：

如果用户想要实现CBook对象与一个整数相加，可以通过修改重载运算符的参数来实现。例如：

通过修改运算符的参数为整数类型，可以实现CBook对象与整数相加。如下面的代码是合法的：

两个整型变量相加，用户可以调换加数和被加数的顺序，因为加法符合交换律。但是，通过重载运算符实现两个不同类型的对象相加则不可以，因此下面的语句是非法的。

对于“++”和“--”运算符，由于涉及前置运算和后置运算，在重载这类运算符时如何区分是前置运算还是后置运算呢？默认情况下，如果重载运算符没有参数，则表示是前置运算。例如：

如果重载运算符使用了整数作为参数，则表示是后置运算。此时的参数值可以被忽略，它只是一个标识，标识后置运算。

默认情况下，将一个整数赋值给一个对象是非法的，可以通过重载赋值运算符“=”将其变为合法的。例如：

通过重载赋值运算符，可以进行如下形式的赋值。

此外，用户还可以通过重载构造函数将一个整数赋值给一个对象。例如：

【例5.21】通过重载构造函数将一个整数赋值给一个对象。

上述代码定义了一个重载的构造函数，以一个整数作为函数参数，这样同样可以将一个整数赋值给一个CBook类对象。例如：

语句“vbBook = 200；”将调用构造函数CBook（int page）重新构造一个CBook对象，将其赋值给vbBook对象。

说明

无论是重载赋值运算符还是重载构造函数，都无法实现反向赋值，即将一个对象赋值给一个整型变量。

为了实现将一个对象赋值给一个整型变量的功能，C++提供了转换运算符。

【例5.22】转换运算符。

上述代码在定义CBook类时定义了一个转换运算符int，用于实现将CBook类赋值给整型变量。转换运算符由关键字operator开始，其后是转换为的数据类型。在定义转换运算符时，注意operator关键字前没有数据类型，虽然转换运算符实际返回了一个转换后的值，但是不能指定返回值的数据类型。下面的代码演示了转换运算符的应用。

【例5.23】转换运算符的应用。（实例位置：资源包\TM\sl\5\8）

执行上述代码，结果如图5.8所示。

图5.8　转换运算符

从图5.8中可以发现，语句“int page = vbBook;”是将vbBook对象的m_Pages数据成员赋值给了page变量，因此page变量的值为300。

用户在程序中重载运算符时，需要遵守如下规则和注意事项。

（1）并不是所有的C++运算符都可以重载。

C++中的大多数运算符都可以进行重载，但是“::”“？”“：”和“. ”运算符不能够被重载。

（2）运算符重载存在如下限制。

不能构建新的运算符。

不能改变原有运算符操作数的个数。

不能改变原有运算符的优先级。

不能改变原有运算符的结合性。

不能改变原有运算符的语法结构。

（3）运算符重载遵循以下基本准则。

一元操作数可以是不带参数的成员函数，或者是带一个参数的非成员函数。

二元操作数可以是带一个参数的成员函数，或者是带两个参数的非成员函数。

“=”“[]”“->”和“()”运算符只能定义为成员函数。

“->”运算符的返回值必须是指针类型或者能够使用“->”运算符类型的对象。

重载“++”和“--”运算符时，带一个int类型参数，表示后置运算，不带参数表示前置运算。

5.1.8　友元类和友元方法

类的私有方法，只有在该类中允许访问，其他类是不能访问的。在开发程序时，如果两个类的耦合度比较紧密，在一个类中访问另一个类的私有成员会带来很大的方便。C++语言提供了友元类和友元方法（或者称为友元函数）来实现访问其他类的私有成员。当用户希望另一个类能够访问当前类的私有成员时，可以在当前类中将另一个类作为自己的友元类，这样在另一个类中即可访问当前类的私有成员。

【例5.24】定义友元类。

在上述代码中，定义CItem类时使用了friend关键字将CList类定义为CItem类的友元，这样CList类中的所有方法就都可以访问CItem类中的私有成员了。在CList类的OutputItem方法中，语句“m_Item.OutputName()”演示了调用CItem类的私有方法OutputName。

在开发程序时，有时需要控制另一个类对当前类的私有成员的访问。例如，只允许CList类的某个成员访问CItem类的私有成员，而不允许其他成员函数访问CItem类的私有数据，此时可以通过定义友元函数来实现。在定义CItem类时，可以将CList类的某个方法定义为友元方法，这样就限制了只有该方法允许访问CItem类的私有成员。

【例5.25】定义友元方法。（实例位置：资源包\TM\sl\5\9）

在上述代码中，定义CItem类时使用了friend关键字将CList类的OutputItem方法设置为友元函数，在CList类的OutputItem方法中访问了CItem类的私有方法OutputName。执行上述代码，结果如图5.9所示。

注意

对于友元函数来说，不仅可以是类的成员函数，还可以是一个全局函数。

下面的代码在定义CItem类时，将一个全局函数定义为友元函数，这样在全局函数中即可访问CItem类的私有成员。

【例5.26】将全局函数定义为友元函数。（实例位置：资源包\TM\sl\5\10）

执行上述代码，结果如图5.10所示。

图5.9　友元函数

图5.10　全局友元函数

5.1.9　类的继承

继承是面向对象的主要特征（此外还有封装和多态）之一，它使一个类可以从现有类中派生，而不必重新定义一个新类。例如，定义一个员工类，其中包含员工ID、员工姓名、所属部门等信息；再定义一个操作员类，通常操作员属于公司的员工，因此该类也包含员工ID、员工姓名、所属部门等信息，此外还包含密码信息、登录方法等。如果当前已经定义了员工类，则在定义操作员类时可以从员工类派生一个新的员工类，然后向其中添加密码信息、登录方法等即可，而不必重新定义员工ID、员工姓名、所属部门等信息，因为它已经继承了员工类的信息。下面的代码演示了操作员类是如何继承员工类的。

【例5.27】类的继承。（实例位置：资源包\TM\sl\5\11）

上述代码在定义COperator类时使用了“：”运算符，表示该类派生于一个基类；public关键字表示派生的类型为公有型；其后的CEmployee表示COperator类的基类，也就是父类。这样，COperator类将继承CEmployee类的所有非私有成员（private类型成员不能被继承）。

说明

当一个类从另一个类继承时，可以有3种派生类型，即公有型（public）、保护型（protected）和私有型（private）。派生类型为公有型时，基类中的public数据成员和方法在派生类中仍然是public，基类中的protected数据成员和方法在派生类中仍然是protected。派生类型为保护型时，基类中的public、protected数据成员和方法在派生类中均为protected。派生类型为私有型时，基类中的public、protected数据成员和方法在派生类中均为private。

下面定义一个操作员对象，演示通过操作员对象调用操作员类的方法以及调用基类—员工类的方法。

执行上述代码，结果如图5.11所示。

注意

用户在父类中派生子类时，可能存在一种情况，即在子类中定义了一个与父类的方法同名的方法，称之为子类隐藏了父类的方法。

例如，重新定义COperator类，添加一个OutputName方法。

【例5.28】子类隐藏了父类的方法。（实例位置：资源包\TM\sl\5\12）

定义一个COperator类对象，调用OutputName方法。

执行上述代码，结果如图5.12所示。

图5.11　访问父类方法

图5.12　隐藏基类方法

从图5.12中可以发现，语句“optr.OutputName()；”调用的是COperator类的OutputName方法，而不是CEmployee类的OutputName方法。如果用户想要访问父类的OutputName方法，需要显式使用父类名。例如：

如果子类隐藏了父类的方法，则父类中所有同名的方法（重载方法）均被隐藏。因此，例5.29中黑体部分代码的访问是错误的。

【例5.29】如果子类隐藏了父类的方法，则父类中所有同名的方法均被隐藏。

在上述代码中，CEmployee类中定义了重载的OutputName方法，而在COperator类中又定义了一个OutputName方法，导致父类中的所有同名方法被隐藏。因此，语句“optr.OutputName（"MR"）；”是错误的。如果用户想要访问被隐藏的父类方法，依然需要指定父类名称。例如：

在派生完一个子类后，可以定义一个父类的类型指针，通过子类的构造函数为其创建对象。例如：

如果使用pWorder对象调用OutputName方法，如执行“pWorker->OutputName()；”语句，则执行的是CEmployee类的OutputName方法还是COperator类的OutputName方法呢？答案是调用CEmployee类的OutputName方法。编译器对OutputName方法进行的是静态绑定，即根据对象定义时的类型来确定调用哪个类的方法。由于pWorker属于CEmployee类型，因此调用的是CEmployee类的OutputName方法。那么是否有方法将“pWorker->OutputName()；”语句改为执行COperator类的OutputName方法呢？通过定义虚方法可以实现这一点。

说明

在定义方法（成员函数）时，在方法的前面使用virtual关键字，该方法即为虚方法。使用虚方法可以实现类的动态绑定，即根据对象运行时的类型来确定调用哪个类的方法，而不是根据对象定义时的类型来确定调用哪个类的方法。

下面的代码修改了CEmployee类的OutputName方法，使其变为虚方法。

【例5.30】利用虚方法实现动态绑定。（实例位置：资源包\TM\sl\5\13）

在上述代码中，CEmployee类中定义了一个虚方法OutputName，在子类COperator类中改写了OutputName方法，其中COperator类中的OutputName方法仍为虚方法，即使没有使用virtual关键字。下面定义一个CEmployee类型的指针，调用COperator类的构造函数构造对象。

执行上述代码，结果如图5.13所示。

图5.13　虚方法

从图5.13中可以发现，“pWorker->OutputName()；”语句调用的是COperator类的OutputName方法。

注意

在C++语言中，除了能够定义虚方法外，还可以定义纯虚方法，也就是通常所说的抽象方法。一个包含纯虚方法的类被称为抽象类，抽象类是不能够被实例化的，通常用于实现接口的定义。

下面的代码演示了纯虚方法的定义。

在上述代码中，为CEmployee类定义了一个纯虚方法OutputName。纯虚方法的定义是在虚方法定义的基础上在末尾添加“= 0”。包含纯虚方法的类是不能够实例化的，因此下面的语句是错误的。

抽象类通常用于作为其他类的父类，从抽象类派生的子类如果也是抽象类，则子类必须实现父类中的所有纯虚方法。

【例5.31】实现抽象类中的方法。（实例位置：资源包\TM\sl\5\14）

上述代码从CEmployee类派生了两个子类，即COperator和CSystemManager。这两个类分别实现了父类的纯虚方法OutputName。下面编写一段代码演示抽象类的应用。

执行上述代码，结果如图5.14所示。

从图5.14中可以发现，同样的一条语句“pWorker->OutputName()；”，由于pWorker指向的对象不同，其行为也不同。

下面分析一下子类对象的创建和释放过程。当从父类派生一个子类后，定义一个子类的对象时，它将依次调用父类的构造函数、当前类的构造函数来创建对象。在释放子类对象时，先调用的是当前类的析构函数，然后是父类的析构函数。下面的代码说明了这一点。

图5.14　纯虚方法

【例5.32】子类对象的创建和释放过程。（实例位置：资源包\TM\sl\5\15）

执行上述代码，结果如图5.15所示。

从图5.15中可以发现，在定义COperator类对象时，调用的是父类CEmployee的构造函数，然后是COperator类的构造函数。子类对象的释放过程则与其构造过程恰恰相反，先调用自身的析构函数，然后再调用父类的析构函数。

在分析完对象的构建、释放过程后，考虑这样一种情况—定义一个基类类型的指针，调用子类的构造函数为其构建对象，当对象释放时，是先调用父类的析构函数还是先调用子类的析构函数，再调用父类的析构函数呢？答案是如果析构函数是虚函数，则先调用子类的析构函数，然后再调用父类的析构函数；如果析构函数不是虚函数，则只调用父类的析构函数。可以想象，如果在子类中为某个数据成员在堆中分配了空间，父类中的析构函数不是虚方法，上述情况将使子类的析构函数不会被调用，其结果是对象不能被正确地释放，导致内存泄漏的产生。因此，在编写类的析构函数时，析构函数通常是虚函数。

图5.15　构造函数调用顺序

说明

前面所介绍的子类的继承方式属于单继承，即子类只从一个父类继承公有的和受保护的成员。与其他面向对象语言不同，C++语言允许子类从多个父类继承公有的和受保护的成员，称之为多继承。

例如，鸟能够在天空飞翔，鱼能够在水里游，而水鸟既能够在天空飞翔，又能够在水里游，则在定义水鸟类时，可以将鸟和鱼同时作为其基类。下面的代码演示了多继承的应用。

【例5.33】实现多继承。（实例位置：资源包\TM\sl\5\16）

执行上述代码，结果如图5.16所示。

上述代码定义了鸟类CBird和鱼类CFish，然后从鸟类和鱼类派生了一个子类—水鸟类CWaterBird，水鸟类自然继承了鸟类和鱼类所有公有和受保护的成员，因此CWaterBird类对象能够调用FlyInSky和SwimInWater方法。在CBird类中提供了一个Breath方法，在CFish类中同样提供了Breath方法，如果CWaterBird类对象调用Breath方法，将会执行哪个类的Breath方法呢？答案是将会出现编译错误，编译器将产生歧义，不知道具体调用哪个类的Breath方法。为了让CWaterBird类对象能够访问Breath方法，需要在Breath方法前具体指定类名。例如：

图5.16　多继承

在多继承中存在这样一种情况，假如CBird类和CFish类均派生于同一个父类，如CAnimal类，那么当从CBird类和CFish类派生子类CWaterBird时，在CWaterBird类中将存在两个CAnimal类的备份。能否在派生CWaterBird类时，使其只存在一个CAnimal基类？为了解决该问题，C++语言提供了虚继承的机制。下面的代码演示了虚继承的使用。

【例5.34】虚继承。（实例位置：资源包\TM\sl\5\17）

执行上述代码，结果如图5.17所示。

在上述代码中，定义CBird类和CFish类时使用了关键字virtual从基类CAnimal派生而来。实际上，虚继承对于CBird类和CFish类没有多少影响，但是却对CWaterBird类产生了很大影响。CWaterBird类中不再有两个CAnimal类的备份，而只存在一个CAnimal的备份，图5.17充分说明了这一点。

通常在定义一个对象时，先依次调用基类的构造函数，最后才调用自身的构造函数。但是对于虚继承来说，情况有些不同。在定义CWaterBird类对象时，先调用基类CAnimal的构造函数，然后调用CBird类的构造函数，这里CBird类虽然为CAnimal的子类，但是在调用CBird类的构造函数时将不再调用CAnimal类的构造函数，此操作被忽略了，对于CFish类也是同样的道理。

图5.17　虚继承

说明

在程序开发过程中，多继承虽然带来了很多方便，但是很少有人愿意使用它，因为多继承会带来很多复杂的问题，并且多继承能够完成的功能，通过单继承同样也可以实现。如今流行的C#、Delphi和Java等面向对象语言只采用了单继承，而没有提供多继承的功能是经过设计者充分考虑的。因此，读者在开发应用程序时，如果能够使用单继承实现，尽量不要使用多继承。

5.1.10　类域

在定义类时，每个类都存在一个类域，类的所有成员均处于类域中。当程序中使用点运算符（.）和箭头运算符（->）访问类成员时，编译器会根据运算符前面的对象的类型来确定其类域，并在其类域中查找成员。如果使用域运算符（::）访问类成员，编译器将根据运算符前面的类名来确定其类域，查找类成员。当用户通过对象访问一个不属于类成员的“成员”时，编译器将提示其“成员”没有在类中定义，因为在类域中找不到该成员。

在类中如果有自定义的类型，则自定义类型的声明顺序是很重要的。在定义类的成员时如果需要使用自定义类型，通常将自定义类型放置在类成员定义的前方，否则将出现编译错误。例如，下面的类定义将是非法的。

【例5.35】自定义类型应放置在类成员定义的前方。

上述代码中应将自定义类型UINT的定义放置在m_Wage数据成员定义的前方。下面的类定义是正确的。

说明

如果在类中自定义了一个类型，在类域内该类型将被用来解析成员函数参数的类型名。

5.1.11　嵌套类

C++语言允许在一个类中定义另一个类，称之为嵌套类。例如，下面的代码在定义CList类时，在内部又定义了一个嵌套类CNode。

【例5.36】定义嵌套类。

在上述代码中，嵌套类CNode中不仅定义了一个私有成员m_Tag，还定义了一个公有成员m_Name。对于外围类CList来说，通常它不能够访问嵌套类的私有成员，虽然嵌套类是在其内部定义的。但是，上述代码在定义CNode类时将CList类作为自己的友元类，这便使得CList类能够访问CNode类的私有成员。

说明

对于内部的嵌套类来说，只允许其在外围的类域中使用，在其他类域或者作用域中是不可见的。

例如，下面的定义是非法的。

上述代码在main函数的作用域中定义了一个CNode对象，导致CNode没有被声明的错误。对于main函数来说，嵌套类CNode是不可见的，但是可以通过使用外围的类域作为限定符来定义CNode对象。下面的定义是合法的。

上述代码通过使用外围类域作为限定符访问到了CNode类。但是这样做通常是不合理的，也是有限制条件的。因为既然定义了嵌套类，通常不允许在外界访问，这违背了使用嵌套类的原则。其次，在定义嵌套类时，如果将其定义为私有的或受保护的，即使使用外围类域作为限定符，外界也无法访问嵌套类。

5.1.12　局部类

类的定义也可以放置在函数中，这样的类称为局部类。

【例5.37】定义局部类。

上述代码在LocalClass函数中定义了一个CBook类，该类被称为局部类。对于局部类CBook来说，在函数之外是不能够被访问的，因为局部类被封装在了函数的局部作用域中。在局部类中，用户也可以再定义一个嵌套类，其定义形式与5.1.11节介绍的嵌套类完全相同，在此不再赘述。

5.2　类模板

函数模板为函数的参数、返回值等提供了动态参数化的机制，使用户能够动态设置函数的参数类型和返回值类型；而类模板能够为类的数据成员、成员函数的参数、返回值提供动态参数化的机制，使用户可以方便地设计出功能更为灵活的类。本节将介绍有关类模板的相关知识。

5.2.1　类模板的定义及应用

在介绍类模板之前，先来设计一个简单的单向链表。链表的功能包括向尾节点添加数据、遍历链表中的节点、在链表结束时释放所有节点，代码如下。

【例5.38】定义单向链表。（实例位置：资源包\TM\sl\5\18）

下面定义一个链表对象，向其中添加节点，并遍历链表节点。

【例5.39】遍历单向链表。（实例位置：资源包\TM\sl\5\18）

执行上述代码，结果如图5.18所示。

图5.18　简单链表

分析上述代码中定义的链表类CList，一个最大的缺陷就是链表不够灵活，其节点只能是CNode类型。为了让CList能够适应各种类型的节点，一个最简单的方法就是使用类模板。类模板的定义与函数模板类似，以关键字template开始，其后是由尖括号<>构成的模板参数。下面重新修改链表类CList，以类模板的形式进行改写。

【例5.40】利用类模板设计链表类。（实例位置：资源包\TM\sl\5\19）

注意

每个模板参数必须由class或typename标识，不能够利用一个class或typename关键字定义多个模板参数。

上述代码利用类模板对链表类CList进行了修改，实际上是在原来链表的基础上将链表中出现CNode类型的地方替换为模板参数Type。下面再定义一个节点类CNet，演示模板类CList是如何适应不同的节点类型的。

【例5.41】演示类模板适应不同的节点类型。（实例位置：资源包\TM\sl\5\19）

执行上述代码，结果如图5.19所示。

图5.19　类模板

说明

类模板CList虽然能够使用不同类型的节点，但是对节点的类型是有一定要求的。节点类必须包含一个指向自身的指针类型成员m_pNext，因为在CList中访问了m_pNext成员。节点类中必须包含数据成员m_Data，其类型被限制为数字类型或有序类型。实际上m_Data成员可以是任意类型，只是在CList类的PassList方法中，笔者为了演示遍历链表节点，使用了“printf（"%4d",pTmp-> m_Data）；”语句输出节点数据，导致m_Data只能是数字类型或有序类型。

5.2.2　定义类模板的静态数据成员

在类模板中用户也可以定义静态的数据成员，只是类模板中的每个实例都有自己的静态数据成员，而不是所有的类模板实例共享静态数据成员。为了说明这一点，笔者对类模板CList进行简化，向其中添加一个静态数据成员，并初始化静态数据成员。

【例5.42】在类模板中使用静态数据成员。（实例位置：资源包\TM\sl\5\20）

下面定义两个类模板实例，分别设置并访问各自的静态数据成员。代码如下：

执行上述代码，结果如图5.20所示。

从图5.20中可以发现，类模板实例nodelist和netlist均有各自的静态数据成员。但是，对于同一类型的类模板实例，其静态数据成员是共享的。

【例5.43】同一类型的类模板实例，其静态数据成员是共享的。（实例位置：资源包\TM\sl\5\21）

执行上述代码，结果如图5.21所示。

图5.20　类模板静态数据成员

图5.21　静态数据成员

从图5.21中可以发现，类模板实例nodelist和list共享静态数据成员，因为它们的模板类均为CNode。

5.3　异常处理

异常是指程序运行时出现的非正常的情况，如访问指针地址无效、除零错误等。为了防止程序因出现异常而中断，C++语言提供了异常处理语句，使用户有机会对出现的异常进行处理，增强程序的健壮性。本节将介绍有关C++语言异常处理的相关知识。

5.3.1　异常捕捉语句

在C++语言中，为了处理异常，提供了try语句和catch语句。try语句和catch语句实际上是两个语句块，try语句块包含的是可能产生异常的代码，catch语句块包含的是处理异常的代码。下面编写一段代码，演示try语句和catch语句的使用。

【例5.44】编写异常捕捉语句。（实例位置：资源包\TM\sl\5\22）

执行上述代码，结果如图5.22所示。

上述代码有意产生一个除零的错误，使用try语句捕捉可能产生的异常，使用catch语句处理产生的异常。实际上，如果用户不处理异常，系统会进行默认的处理，但是这通常会导致程序的终止。例如，上述代码如果去除try语句和catch语句，将会终止程序，并出现如图5.23所示的错误。

图5.22　除零错误

图5.23　系统错误信息

通过在程序中使用try和catch语句，用户可以截获可能产生的异常并进行处理，这样可以避免系统进行默认处理，防止了由于异常而导致程序的终止。

在try语句中，如果当前代码产生了异常，则try语句中其后的代码将不会执行，程序直接跳转到catch语句中。在上述的代码中，“int ret=x/y;”语句引发了除零的异常，程序将执行catch语句中的“printf（"除法运算错误!\n"）；”语句，而不会再执行try语句中的“printf（"执行除法运算!\n"）；”语句，因此出现了如图5.22所示的结果。

在catch语句中出现了“…”符号，表示处理所有异常。如果try语句中没有产生异常，则不会执行catch语句中的代码。实际上一个try语句可以对应多个catch语句，每一个catch语句可以关联一个异常类，当try语句中产生的异常与catch语句关联的异常匹配时，将执行该catch语句中的代码。

注意

try部分的语句是可能出现错误的语句，该语句必须由大括号包含，即使只有一条语句。catch部分的语句是处理异常的语句，该部分语句也必须由大括号包含。在try程序段后，必须紧跟一个或多个catch程序段，这是C++语法所要求的。每个catch程序段用于捕获一种类型的异常，并列的catch程序段之间不允许插入其他语句。

5.3.2　抛出异常

异常不仅可以由系统触发（代码违规产生的异常），也可以由用户触发。C++语句提供了throw关键字，用于手动触发异常。对于用户触发的异常，其好处是用户能够定义自己的逻辑规则。例如：

在上述代码中，用户定义了自己的一个除法规则，即如果除数为负数，将触发异常，执行catch语句块中的代码。

在程序中，throw语句通常出现在try语句块中。当程序中出现throw语句时，其后的代码将不会执行，跳转到catch语句块中。因此，上述代码中的“int ret=x/y;”语句和“printf（"执行除法运算!\n"）；”语句不会被执行。下面编写一个除法函数，演示如何自定义异常和触发异常。

【例5.45】自定义异常类。（实例位置：资源包\TM\sl\5\23）

下面调用Div函数，将除数设置为负数，使其触发自定义的异常。代码如下：

执行上述代码，结果如图5.24所示。

图5.24　自定义异常

从图5.24中可以发现，语句“Div（20，-5，ret）；”触发了自定义的异常CNegException。

5.4　小结

本章首先详细介绍了面向对象程序设计中关于类的定义及应用，包括各种成员函数、构造函数的定义，以及嵌套类、类模板的定义和使用，最后阐述了C++语言提供的异常处理机制。通过本章的学习，读者应该掌握类的基本定义、类方法的调用以及虚函数的动态绑定，这是面向对象程序设计最基本的知识。

5.5　实践与练习

1. 编写一个类，实现对目录的添加、删除操作。（答案位置：资源包\TM\sl\5\24）

2. 编写一个友元类，实现对另一个类的私有成员的访问。（答案位置：资源包\TM\sl\5\25）

3. 编写一个虚函数，演示面向对象的动态绑定机制。（答案位置：资源包\TM\sl\5\26）

第2篇

核心技术

第6章　对话框应用程序设计

第7章　常用控件

第8章　菜单

第9章　工具栏和状态栏

第10章　高级控件

第11章　自定义MFC控件

第12章　文本、图形、图像处理

第13章　文档与视图

本篇介绍对话框应用程序设计，常用控件，菜单，工具栏和状态栏，高级控件，自定义MFC控件，文本、图形、图像处理，文档与视图等内容。学习完本篇，读者将能够开发一些小型应用程序。

第6章

对话框应用程序设计

（视频讲解：1小时28分钟）

在Windows应用程序中，对话框是重要的组成部分。不论是打开文件，还是查询数据，以及数据交换时都会用到对话框，对话框是实现人机交互的一条重要途径。

在Visual C++中，对话框被封装在CDialog类中，而该类又派生于CWnd类，所以对话框与普通的窗口有很多相似之处，CWnd类中的多数方法都适用于CDialog类。

本章将从基础开始介绍对话框的创建及显示，然后不断地深入介绍对话框，包括属性设置以及控件的操作，并简单介绍Windows通用对话框的应用。讲解过程中为了便于读者理解列举了大量的实例。

通过阅读本章，您可以：

了解如何构建应用程序

掌握对话框的创建和显示方法

掌握对话框属性的设置

掌握在对话框中操作控件

掌握对话框成员及成员函数的添加

掌握消息对话框的使用

掌握Windows通用对话框的使用

6.1　构建应用程序

在Visual C++中，构建应用程序分为3种，分别是对话框应用程序、单文档／视图应用程序和多文档／视图应用程序。本节主要介绍如何构造这3种应用程序。

6.1.1　构建对话框应用程序

在Visual C++ 6.0开发环境中提供了MFC应用程序向导，以帮助用户创建对话框应用程序，步骤如下。

（1）选择“开始”／“所有程序”／Microsoft Visual Studio 6.0/Microsoft Visual C++ 6.0命令，打开Visual C++ 6.0集成开发环境。

（2）在Visual C++ 6.0开发环境中选择File/New命令，弹出New对话框。在Projects选项卡中选择MFC AppWizard[exe]（MFC应用程序向导）选项，如图6.1所示。

（3）在Project name编辑框中输入创建的工程名，在Location编辑框中设置工程文件存放的位置，单击OK按钮，弹出MFC AppWizard-Step 1对话框，如图6.2所示。

图6.1　New对话框

图6.2　MFC AppWizard-Step 1对话框

（4）选中Dialog based单选按钮，创建一个基于对话框的应用程序，因为对程序没有特殊的要求，所以直接单击Finish按钮创建应用程序。

6.1.2　构建单文档／视图应用程序

在Visual C++ 6.0开发环境中提供了MFC应用程序向导帮助用户创建单文档／视图应用程序，步骤如下。

（1）选择“开始”／“所有程序”/Microsoft Visual Studio 6.0/Microsoft Visual C++ 6.0命令，打开Visual C++ 6.0集成开发环境。

（2）在Visual C++ 6.0开发环境中选择File/New命令，弹出New对话框。在Projects选项卡中选择MFC AppWizard[exe]（MFC应用程序向导）选项，如图6.1所示。

（3）在Project name编辑框中输入创建的工程名，在Location编辑框中设置工程文件存放的位置。单击OK按钮，弹出MFC AppWizard-Step 1对话框，如图6.2所示。

（4）选中Single document单选按钮，创建一个单文档应用程序框架。单击Next按钮，弹出MFC AppWizard-Step 2 of 6对话框，如图6.3所示。

在MFC AppWizard-Step 2 of 6对话框中可进行如下设置。

None：代表在程序中不使用数据库。

Header files only：表示在代码框架中加入数据库类的头文件。

Database view without file support：表示在代码框架中加入对具体数据库的支持，但没有对通过菜单打开指定文件进行支持。

Database view with file support：相对Database view without file support增加了通过菜单打开指定文件的支持。

Data Source：设置数据源。

说明

如果用户不需要进行其他设置，可以在选中Single document单选按钮后直接单击Finish按钮创建基于单文档的应用程序。

（5）单击Next按钮，弹出MFC AppWizard-Step 3 of 6对话框，如图6.4所示。

图6.3　MFC AppWizard-Step 2 of 6对话框

图6.4　MFC AppWizard-Step 3 of 6对话框

在MFC AppWizard-Step 3 of 6对话框中可以进行如下设置。

None：表示不使用组件。

Container：表示在代码框架中增加对容器的支持。

Mini-server：表示在代码框架中增加对最小组件服务的支持。

Full-server：表示增加对完整组件服务的支持。

Both container and server：表示在代码框架中增加对容器和组件服务两项的支持。

Automation：支持自动化组件。

ActiveX Controls：支持ActiveX控件。

（6）单击Next按钮，弹出MFC AppWizard-Step 4 of 6对话框，如图6.5所示。

在MFC AppWizard-Step 4 of 6对话框中可以进行如下设置。

Docking toolbar：自动加入浮动工具栏。

Initial status bar：自动加入状态栏。

Printing and print preview：自动加入打印及打印预览命令。

Context-sensitive Help：自动加入帮助按钮。

3D controls：三维外观。

MAPI(Messaging API)：用于创建、操作、传输和存储电子邮件。

Windows Sockets：基于TCP/IP的Windows应用程序接口，用于Internet编程。

Normal：使用默认风格的工具栏。

Internet Explorer ReBars：使用IE风格的工具栏。

Advanced：设置程序中使用的文档模板字符串及窗体的样式。

（7）单击Next按钮，弹出MFC AppWizard-Step 5 of 6对话框，如图6.6所示。

图6.5　MFC AppWizard-Step 4 of 6对话框

图6.6　MFC AppWizard-Step 5 of 6对话框

在MFC AppWizard-Step 5 of 6对话框中可以进行如下设置。

MFC Standard：标准MFC项目。

Windows Explorer：“Windows资源管理器”风格项目。

Yes，please：在源文件中添加注释。

No，thank you：不添加注释。

As a shared DLL：共享动态链接库。

As a statically linked library：静态链接库。

（8）单击Next按钮，弹出MFC AppWizard-Step 6 of 6对话框，如图6.7所示。

（9）在MFC AppWizard-Step 6 of 6对话框中显示了要创建的类、头文件和程序文件的名称信息，并可以在列表框中选择生成视图的基类，单击Finish按钮，即可完成构建单文档／视图应用程序。

图6.7　MFC AppWizard-Step 6 of 6对话框

说明

在Base class组合框中可以选择不同的视图类作为当前视图类的基类。

6.1.3　构建多文档／视图应用程序

多文档／视图应用程序的构建和单文档／视图应用程序的构建步骤是相同的，只是在MFC AppWizard-Step 6 of 6对话框所生成的文件中会多一个子窗口框架CChildFrame类，如图6.8所示。

图6.8　MFC AppWizard-Step 6 of 6对话框

6.2　对话框的创建及显示

对话框可以分为模态对话框和非模态对话框两大类，两种对话框根据需要应用在不同的程序中。本节将分别介绍这两种对话框的创建及显示。

6.2.1　创建对话框

创建对话框指的是在应用程序中创建对话框资源。在Visual C++中，用户可以通过工作区窗口的ResourceView选项卡创建对话框资源，步骤如下。

（1）在工作区窗口中选择ResourceView选项卡，右击Dialog节点，在弹出的快捷菜单中选择Insert Dialog命令，创建一个对话框资源，如图6.9所示。

（2）按Enter键打开对话框的属性窗口，修改对话框资源的ID值，本例为IDD_SHOW_DIALOG。要使用对话框资源，还需要为对话框创建一个窗口类。双击对话框资源或按Ctrl+W键打开类向导，弹出Adding a Class对话框，要求用户为新创建的对话框资源新建或选择一个类，如图6.10所示。

图6.9　对话框资源

图6.10　Adding a Class对话框

（3）选中Create a new class单选按钮，单击OK按钮，打开New Class对话框，在Name编辑框中输入类名，如图6.11所示。

图6.11　New Class对话框

（4）单击OK按钮，完成类的创建。

6.2.2　显示对话框

在Windows系统中，对话框可以分为两类，分别是模态对话框和非模态对话框，两种对话框的显示方法是不同的，下面分别进行介绍。

1. 模态对话框的显示（DoModal方法）

模态对话框的特点是在对话框弹出以后，其他程序会被挂起，只有当前对话框响应用户的操作，在对话框关闭前用户不能在同一应用程序中进行其他操作。

要显示模态对话框，首先要为模态对话框声明一个对象，然后调用该对象的DoModal方法进行显示（DoModal方法用于创建并显示一个模态对话框）。

语法格式如下：

返回值：DoModal方法返回一个整数值，该数值可以应用于EndDialog方法。如果方法返回值为-1，表示没有创建对话框；如果为IDABORT，表示有其他错误发生。

说明

关闭模态对话框时，可以调用CDialog类的OnOK方法或OnCancel方法。用户单击OK按钮（按钮ID为IDOK）时会调用OnOK方法，该方法在内部调用了EndDialog方法，因此单击OK按钮会关闭对话框。通常情况下，用户需要改写该方法，禁止调用基类的OnOK方法。当用户在对话框中单击ID为IDCANCEL的按钮或按Esc键时，程序将自动调用OnCancel方法，默认情况下OnCancel方法在内部调用EndDialog方法。如果用户在一个非模态对话框中实现OnCancel方法，需要在内部调用DestroyWindow方法，而不要调用基类的OnCancel方法，因为它调用EndDialog方法将使对话框不可见，但不销毁对话框。

【例6.1】显示一个模态对话框。

【例6.2】关闭一个模态对话框。

2. 非模态对话框的显示（Create方法）

非模态对话框打开以后，不会影响其他线程处理消息。要显示非模态对话框，首先要调用CDialog类的Create方法进行创建。语法格式如下：

相应参数说明如下。

lpszTemplateName：标识资源模板名称。

pParentWnd：标识父窗口指针。

nIDTemplate：标识对话框资源ID。

返回值：如果对话框创建成功，返回值为非0，否则为0。

然后通过ShowWindow函数进行显示。语法格式如下：

其中，nCmdShow指定了窗口的显示状态。

销毁窗口时，要使用CDialog类的DestroyWindow方法。

说明

如果一个窗口是父窗口，调用该窗口的DestroyWindow方法时，将销毁所有的子窗口。

【例6.3】显示一个非模态对话框。

【例6.4】销毁一个非模态对话框。

6.3　对话框属性设置

前面已经介绍了如何创建并显示对话框，本节将介绍如何设置对话框的属性。右击对话框资源，在弹出的快捷菜单中选择Properties命令（也可以在选中对话框后按Enter键），将弹出Dialog Properties（对话框属性）对话框，在其中可对对话框的属性进行设置。

6.3.1　设置对话框的标题

在Dialog Properties对话框的General选项卡中，用户可以通过Caption属性来设置对话框的标题，如图6.12所示。

图6.12　设置对话框的标题

6.3.2　设置对话框的边框风格

在Dialog Properties对话框的Styles选项卡中，用户可以通过Border列表框来设置对话框的边框风格。在Border组合框中选择None选项，则对话框显示时没有边框，在使用标签控件时这一属性非常适用，对话框可以在标签页切换时显示；在Border组合框中选择Resizing选项后，对话框可以随意调整大小。

在设置对话框的边框风格时，还可以设置对话框标题栏是否显示系统菜单，如“最大化”和“最小化”按钮，如图6.13所示。

图6.13　设置对话框的“最大化”和“最小化”按钮

注意

如果用户只选中“最大化”和“最小化”按钮中的一个，在显示对话框时两个按钮同样会全部显示，但未被选中的按钮会保持不可用的状态。

6.3.3　使用对话框关联菜单

在Dialog Properties对话框的General选项卡中，用户可以通过Menu组合框来设置对话框所关联的菜单资源，如图6.14所示。

图6.14　使用对话框管理菜单

6.3.4　设置对话框字体

在Dialog Properties对话框的General选项卡中包含了一个Font按钮，用户单击Font按钮，在弹出的对话框中可以设置对话框的字体信息，如图6.15所示。

图6.15　设置对话框字体

6.4　在对话框中操作控件

要使用对话框，只创建资源是不够的，还需要为对话框添加控件才行。本节主要介绍如何使用对话框中的控件。

6.4.1　在对话框中使用控件

1. 放置控件

在Visual C++ 6.0开发环境中，系统提供了一个控件面板，用户可以利用鼠标选中其中的控件，将其放置在对话框中。其操作方法是：首先在控件面板中选择要添加的控件，然后将鼠标移动到对话框上，当鼠标指针变成“十”字形时，按住鼠标左键进行绘制，当达到所需要控件的大小时释放鼠标，此时控件将被放置到对话框中，如图6.16所示。

图6.16　放置控件

技巧

在控件面板中选中要选择的控件，不松开鼠标，直接将其拖曳到对话框中，也可以添加控件。此时控件将保持默认的大小。

2. 放置多个控件

开发应用程序时，经常需要在对话框中放置多个相同控件，使用上面的方法需要来回地移动鼠标，很麻烦，也很浪费时间。在Visual C++ 6.0开发环境中，可以通过以下两种方法向对话框中放置多个相同的控件。

按住Ctrl键，用鼠标选择要添加的控件，当鼠标指针变成“十”字形时，按住鼠标左键拖动鼠标，达到所需控件的大小时释放鼠标。重复执行此操作，直到添加完控件为止。

先在对话框中放置一个控件，然后选择该控件，按Ctrl+C快捷键复制控件，再按Ctrl+V快捷键粘贴控件。

上述两种方法都能向对话框中添加多个相同的控件，第一种速度相对较快，第二种则可以添加大小相同的控件。

6.4.2　控件对齐方式

在设置程序的界面时，将同一种类型的控件对齐排列是非常必要的，这样界面看起来会比较整齐、美观。Visual C++ 6.0开发环境中提供了多种控件对齐命令，分别位于Layout菜单的Align和Space Evenly子菜单下。下面分别对这些命令进行介绍。

Left命令：以被选择控件中的当前控件为基准，左边对齐各控件，如图6.17所示。

图6.17　Left命令

Horiz Center命令：以被选择控件中的当前控件为基准，水平方向居中对齐控件，如图6.18所示。

图6.18　Horiz Center命令

Right命令：以被选择控件中的当前控件为基准，右边对齐各控件，如图6.19所示。

图6.19　Right命令

Top命令：以被选择控件中的当前控件为基准，上边对齐各控件，如图6.20所示。

图6.20　Top命令

Vert Center命令：以被选择控件中的当前控件为基准，垂直方向居中对齐各控件，如图6.21所示。

图6.21　Vert Center命令

Bottom命令：以被选择控件中的当前控件为基准，下边对齐各控件，如图6.22所示。

Across命令：使选择的控件水平间距相等，如图6.23所示。

Down命令：使选择的控件垂直间距相等，如图6.24所示。

图6.22　Bottom命令

图6.23　Across命令

图6.24　Down命令

说明

Layout菜单和Dialog工具栏只有在当前窗口是对话框编辑窗口时才会显示出来。

6.4.3　为控件关联变量

在Visual C++开发环境中，要为控件关联变量，可以通过类向导来实现。选择View/ClassWizard命令，在打开的MFC ClassWizard（类向导）对话框中选择Member Variables选项卡，在列表框的Control IDs列中会显示对话框中所有控件的ID值，如图6.25所示。

选择一个控件的ID值，然后单击Add Variable按钮，弹出Add Member Variable对话框，在该对话框中设置变量名称和变量类型，如图6.26所示。

单击OK按钮，即可为控件关联变量，如图6.27所示。

说明

一个控件可以同时关联多个不同名称且类型不同的变量。

图6.25　MFC ClassWizard（类向导）对话框

图6.26　Add Member Variable对话框

图6.27　关联变量

6.5　添加对话框成员及成员函数

在Visual C++开发环境中，要为对话框添加成员及成员函数，可以通过工作区窗口的ClassView选项卡来实现。

6.5.1　添加普通成员及成员函数

1. 添加普通成员

在ClassView选项卡中右击要添加成员的类，在弹出的快捷菜单中选择Add Member Variable命令，弹出Add Member Variable对话框，在该对话框中设置要添加的成员类型、成员名称以及成员的保护权限，如图6.28所示。

单击OK按钮，即在类的头文件中添加了成员。

2. 添加成员函数

为类添加成员函数的方法和添加普通成员相似。在快捷菜单中选择Add Member Function命令，弹出Add Member Function对话框，设置成员函数的返回值类型、函数名称及成员函数的保护权限，如图6.29所示。

图6.28　Add Member Variable对话框

图6.29　Add Member Function对话框

单击OK按钮，即会在类的头文件中添加成员函数的声明，然后在源文件中找到函数的实现部分，为函数添加实现功能的代码。

注意

如果想添加静态成员函数或虚函数，可以选中Static或Virtual复选框。

6.5.2　添加消息处理函数

按Ctrl+W快捷键，打开MFC ClassWizard对话框，选择Message Maps选项卡，在Class name组合框中选择对话框类，在Object IDs列表框中选择资源ID，在Messages列表框中选择要处理的事件，单击Add Function按钮，如图6.30所示。

图6.30　MFC ClassWizard对话框

单击Edit Code按钮，即可跳转到建立的消息响应函数或虚函数中。

6.5.3　手动添加命令消息处理函数

在Visual C++ 6.0开发环境中，除了处理系统的消息外，还可以添加自定义命令消息，并处理自定义消息的处理函数。

选择View/ResourceSymbols命令，弹出ResourceSymbols对话框；单击New按钮，弹出New Symbol对话框，添加一个新命令标识NEWMESSAGE，如图6.31所示。

图6.31　New Symbol对话框

打开对话框的源文件，在BEGIN_MESSAGE_MAP中添加消息映射。代码如下：

然后通过添加成员函数的方法添加OnNewMessage函数，再在需要触发消息的地方调用SendMessage函数来发送消息。

语法格式如下：

参数说明：

message：发送的命令标识。

wParam：指定附加的消息指定信息。

lParam：指定附加的消息指定信息。

技巧

消息ID的添加也可以通过宏常量的形式进行定义。

6.6　消息对话框

消息对话框是一种简单的对话框，不需要用户自己创建即可直接使用。在Visual C++ 6.0中提供了AfxMessageBox函数和MessageBox函数来弹出消息对话框。

语法格式分别如下：

AfxMessageBox函数和MessageBox函数中的参数说明如表6.1所示。

表6.1　AfxMessageBox函数和MessageBox函数中的参数说明

按钮风格如表6.2所示。

表6.2　按钮风格

图标风格如表6.3所示。

表6.3　图标风格

【例6.5】下面在关闭应用程序时使用消息对话框进行确认，程序设计步骤如下。（实例位置：资源包\TM\sl\6\1）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“应用程序”。

（2）删除对话框中自动生成的控件，为对话框处理WM_CLOSE消息，在该消息的响应函数中设置弹出消息对话框的功能，代码如下。

实例的运行结果如图6.32所示。

图6.32　消息对话框

注意

MessageBox只能使用在基于CWin的子类成员方法中，否则必须使用AfxMessageBox函数显示提示框。

6.7　Windows通用对话框

Windows通用对话框是由操作系统提供的任何应用程序都可获得的对话框。在Visual C++中，对这些对话框进行了封装，用户在开发程序时可以方便地调用这些对话框。Windows通用对话框从用户处获取消息，返回相应的消息，但不进行消息处理。如果用户要进行进一步的处理，还需要自行添加代码。下面就来介绍一下通用对话框的应用。

6.7.1　使用“文件”对话框打开和保存文件

“文件”对话框为打开和保存文件提供了一个方便的接口，在MFC中CFileDialog类对“文件”对话框进行了封装。

使用“文件”对话框时要创建一个“文件”对话框对象，通过构造函数进行初始化。

语法格式如下：

CFileDialog构造函数中的参数说明如表6.4所示。

表6.4　CFileDialog构造函数中的参数说明

说明

lpszFilter参数格式：文件类型说明和扩展名间用“|”分隔，每种文件类型间用“|”分隔，末尾用“||”结束。

在使用“文件”对话框时，还需要通过“文件”对话框的一些常用函数来实现用户需要的功能。“文件”对话框的常用函数如表6.5所示。

表6.5　“文件”对话框的常用函数

【例6.6】下面使用“文件”对话框打开和保存文件，程序设计步骤如下。（实例位置：资源包\TM\ sl\6\2）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘文件’对话框打开和保存文件”。

（2）向对话框中添加两个静态文本控件、一个编辑框控件和两个按钮控件。

（3）处理“打开”按钮的单击事件，在静态正文中显示文件路径，在编辑框中显示文件内容，代码如下。

技巧

在上述代码中，使用字符数组存储文件数据。这里数组大小是固定的，如果文件大小超出数组的大小，程序就会出错。要解决这一问题，可以使用new/delete运算符并根据文件大小为字符指针动态分配存储空间，这样就不会出现由于文件过大导致程序出错的问题了。

（4）处理“保存”按钮的单击事件，在静态正文中显示文件路径，将编辑框中的内容保存到文件中，代码如下。

实例的运行结果如图6.33所示。

图6.33　使用“文件”对话框打开和保存文件

6.7.2　使用“字体”对话框设置文本字体

CFontDialog类封装了Windows“字体”对话框。用户可以从系统安装的字体列表中选择要用的字体，同时在“字体”对话框中还可以设置字体大小、颜色、效果和字符集等属性。可以通过构造函数CFontDialog构造“字体”对话框。

语法格式如下：

CFontDialog构造函数中的参数说明如表6.6所示。

表6.6　CFontDialog构造函数中的参数说明

“字体”对话框的常用函数如表6.7所示。

表6.7　“字体”对话框的常用函数

【例6.7】下面使用“字体”对话框设置编辑框控件中显示文本的字体，程序设计步骤如下。（实例位置：资源包\TM\sl\6\3）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘字体’对话框设置文本字体”。

（2）向对话框中添加一个编辑框控件和一个按钮控件。

（3）在对话框头文件中声明一个CFont对象m_Font。

（4）处理“字体”按钮的单击事件，创建“字体”对话框，设置在编辑框中显示文本的字体，代码如下。

实例的运行结果如图6.34所示。

图6.34　使用“字体”对话框设置文本字体

注意

CFont对象m_Font一定要在对话框的头文件中定义，否则无法修改字体。

6.7.3　使用“颜色”对话框设置文本背景颜色

“颜色”对话框也是常用的对话框之一，就像画家的调色板一样，用户可以直观地在“颜色”对话框中选择所需要的颜色，也可以创建自定义颜色。CColorDialog类对“颜色”对话框进行了封装，可以通过构造函数CColorDialog构造“颜色”对话框。

语法格式如下：

参数说明：

clrInit：标识“颜色”对话框默认时的颜色。

dwFlags：一组标记，用于自定义“颜色”对话框。

pParentWnd：标识“颜色”对话框的父窗口。

“颜色”对话框的常用函数如表6.8所示。

表6.8　“颜色”对话框的常用函数

【例6.8】下面使用“颜色”对话框设置静态文本控件中文本的背景颜色，程序设计步骤如下。（实例位置：资源包\TM\sl\6\4）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘颜色’对话框设置文本背景颜色”。

（2）向对话框中添加一个静态文本控件和一个按钮控件。

（3）在对话框头文件中声明一个COLORREF对象m_Color。

（4）处理“颜色”按钮的单击事件，创建“颜色”对话框，获得选择的颜色，代码如下。

说明

获得用户选择的颜色后要使用Invalidate方法重绘窗口，否则程序无法触发WM_CTLCOLOR事件重绘控件的背景颜色。

（5）处理对话框的WM_CTLCOLOR事件，在该事件的处理函数中设置静态文本控件显示文本的背景颜色，代码如下。

实例的运行结果如图6.35所示。

图6.35　使用“颜色”对话框设置文本背景颜色

6.7.4　使用“查找／替换”对话框在文本中替换字符串

使用“查找／替换”对话框可以进行查找和替换操作。CFindReplaceDialog类对“查找／替换”对话框进行了封装，可以通过该类的Create方法进行创建。

语法格式如下：

Create方法中的参数说明如表6.9所示。

表6.9　Create方法中的参数说明

“查找／替换”对话框的常用函数如表6.10所示。

表6.10　“查找／替换”对话框的常用函数

【例6.9】下面使用“查找／替换”对话框在编辑框的文本中替换字符串，程序设计步骤如下。（实例位置：资源包\TM\sl\6\5）

（1）创建一条基于对话框的应用程序，将对话框的Caption属性修改为“使用‘查找／替换’对话框在文本中替换字符串”。

（2）向对话框中添加一个编辑框控件和一个按钮控件。

（3）在对话框头文件中声明变量，代码如下。

（4）定义一条新消息WM_FINDMESSAGE，代码如下。

（5）在对话框的头文件和消息映射部分分别添加函数定义和映射宏，代码如下。

（6）处理WM_FINDMESSAGE消息，在该消息的响应函数中实现查找和替换操作，代码如下。

（7）为“替换”按钮处理单击事件，创建“替换”对话框的代码如下。

说明

如果要创建“查找”对话框，只要将“dlg->Create（FALSE,NULL）；”语句修改为“dlg->Create（TRUE,NULL）；”即可。

实例的运行结果如图6.36所示。

图6.36　使用“查找／替换”对话框在文本中替换字符串

6.7.5　使用“打印”对话框进行打印

“打印”对话框提供了对打印机接口界面的支持，用户可以使用“打印”对话框进行打印。CPrintDialog类对“打印”对话框进行了封装，可以通过构造函数CPrintDialog构造“打印”对话框。

语法格式如下：

参数说明：

bPrintSetupOnly：为TRUE，表示创建“打印设置”对话框；为FALSE，表示创建“打印”对话框。

dwFlags：自定义对话框的一组标记。

pParentWnd：标识“打印”对话框的父窗口指针。

“打印”对话框的常用函数如表6.11所示。

表6.11　“打印”对话框的常用函数

【例6.10】下面使用“打印”对话框进行打印，程序设计步骤如下。（实例位置：资源包\TM\sl\6\6）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘打印’对话框进行打印”。

（2）向对话框中添加一个按钮控件。

（3）在对话框头文件中声明变量，代码如下。

（4）定义一个自定义函数DrawText，用于绘制打印和预览的文本，代码如下。

（5）在OnPaint函数中获取屏幕每逻辑英寸的像素值并进行预览的绘制，代码如下。

（6）处理WM_CTLCOLOR消息，在该消息中将对话框的背景颜色改为白色。

（7）处理“打印”按钮的单击事件，创建“打印”对话框进行打印，代码如下。

说明

在不同设备上下文中使用参数为LOGPIXELSX、LOGPIXELSY的GetDeviceCaps函数，可以获得不同设备上下文的水平和垂直像素数。

实例的运行结果如图6.37所示。

图6.37　使用“打印”对话框进行打印

6.7.6　使用“浏览文件夹”对话框选择文件夹

在进行文件操作时，除了使用“文件”对话框来获得文件路径外，还常常要获得文件夹的路径，这就需要使用“浏览文件夹”对话框。可以通过API函数SHBrowseForFolder来显示“浏览文件夹”对话框。

语法格式如下：

其中，lpbi指BROWSEINFO结构指针。

通过BROWSEINFO结构可以设置“浏览文件夹”对话框的信息。

【例6.11】下面使用“浏览文件夹”对话框选择文件夹，程序设计步骤如下。（实例位置：资源包\TM\sl\6\7）

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘浏览文件夹’对话框选择文件夹”。

（2）向对话框中添加一个群组框控件、一个静态文本控件和一个按钮控件。

（3）处理“选择文件夹”按钮的单击事件，代码如下。

实例的运行结果如图6.38所示。

图6.38　使用“浏览文件夹”对话框选择文件夹

注意

在使用“浏览文件夹”对话框时，SHGetPathFromIDList函数中的路径应使用字符串数组或指针，不能使用CString声明的字符串变量。

6.8　小结

通过本章的学习，读者可以初步了解对话框应用程序的构建及使用，包括对话框的创建、显示、属性设置等方面，还介绍了控件在对话框中如何布局以及成员和成员函数的添加，并通过实例介绍了Windows通用对话框的使用，使读者可以更加全面地了解对话框。

6.9　实践与练习

1. 设计一个可以显示图片预览的“打开”对话框，效果见资源包中的实例。（答案位置：资源包\TM\ sl\6\8）

2. 设计一个能隐藏对话框中控件的“文件”对话框，效果见资源包中的实例。（答案位置：资源包\TM\ sl\6\9）

3. 设计一个“页面设置”对话框，效果见资源包中的实例。（答案位置：资源包\TM\sl\6\10）

第7章

常用控件

（视频讲解：51分钟）

控件是所有应用程序都不可或缺的组成部分，通过控件可以使用户和计算机进行良好的交互。本章将对Visual C++开发环境中的常用控件进行介绍，使读者初步了解控件，为以后使用高级控件打下良好的基础。本章通过设置控件的属性和方法来介绍控件，为了便于读者理解，在讲解过程中列举了大量的实例。

通过阅读本章，您可以：

掌握静态文本控件的应用

掌握编辑框控件的应用

掌握图像控件的应用

掌握按钮控件的应用

掌握复选框控件的应用

掌握单选按钮控件的应用

掌握组合框控件的应用

掌握列表框控件的应用

掌握进度条控件的应用

7.1　静态文本控件

静态文本控件用于显示信息，是一种单向交互的控件，不能接受输入。本节通过几个简单的案例对静态文本控件进行介绍。

7.1.1　设置显示文本

1. 通过caption属性设置显示文本

在对话框中选择静态文本控件，按Enter键打开控件的属性窗口，通过Caption属性修改控件的显示文本，如图7.1所示。

2. 通过函数设置显示文本

控件都是从CWnd类中派生的，所以也可以使用CWnd类中的SetWindowText函数来显示文件。

语法格式如下：

其中，lpszString表示显示的字符串。

【例7.1】通过函数显示文本。

首先为静态文本控件关联一个CStatic类型的变量，然后通过该变量调用SetWindowText函数，在其中设置显示文本。代码如下：

程序运行结果如图7.2所示。

图7.1　通过caption属性设置显示文本

图7.2　通过函数设置显示文本

7.1.2　设置文本颜色

设置静态文本控件的文本颜色，可以使用SetTextColor函数来进行。

语法格式如下：

其中，crColor表示设置的颜色。

【例7.2】设置静态文本控件中的文本颜色。

首先设置静态文本控件显示的文本，然后处理对话框的WM_CTLCOLOR消息，在该消息中调用SetTextColor函数设置文本颜色。代码如下：

程序运行结果如图7.3所示。

图7.3　设置静态文本控件中的文本颜色

7.1.3　模拟按钮控件的单击事件

通过静态文本控件的BN_CLICKED消息，可以模拟按钮控件的单击事件。

【例7.3】使用静态文本控件模拟按钮控件的单击事件。

设置控件的显示文本，然后选择Notify属性，在BN_CLICKED消息的处理函数中添加实现代码，弹出一个消息对话框。代码如下：

程序运行结果如图7.4所示。

图7.4　模拟按钮控件的单击事件

注意

在使用该事件之前，需要选择静态文本控件的Notify属性，否则无法实现单击功能。

7.2　编辑框控件

编辑框既可以进行输出，也可以接受用户的输入，并可以进行复制、剪切、粘贴和删除等操作。本节通过一些常用的属性来介绍编辑框的使用。

7.2.1　设置密码编辑框

通过Password属性可以设置编辑框中的文本密码显示。打开控件的属性窗口，选择Password属性，如图7.5所示。

图7.5　密码编辑框

技巧

在设置密码框的同时，也可以先选中Uppercase或Lowercase复选框，使密码框中的字符全部转换为大写或小写。

7.2.2　设置只读编辑框

还可以设置编辑框控件只读，设置为只读编辑框后，用户不能对控件中的内容进行编辑。打开控件的属性窗口，选择Read-only属性，如图7.6所示。

图7.6　只读编辑框

7.2.3　设置编辑框多行显示

通过编辑框的Multiline属性可以进行多行显示。在使用代码输入字符串，换行时需要输入“\r\n”，如图7.7所示。

图7.7　编辑框多行显示

注意

如果数据很大，还需要编辑框具有Auto HScroll属性。

7.2.4　设置编辑框按Enter键换行

即使选择了编辑框的Multiline属性，在输入时还是会碰到无法输入多行的情况。这是因为在输入时无法进行换行，在编辑框中按Enter键后会直接关闭对话框。解决方法是：选择编辑框的Want return属性，然后再按Enter键时即可换到下一行而不关闭对话框。

7.2.5　使用编辑框控件录入数据

用户可以使用编辑框录入数据，在程序中可以通过GetWindowText函数获得编辑框中的数据。

【例7.4】通过GetWindowText函数获得编辑框中的数据，然后使用消息对话框进行显示。

用户在编辑框中输入数据，单击“获取”按钮弹出消息框，在消息框中显示编辑框中的数据。代码如下：

运行结果如图7.8所示。

注意

对于编辑框控件中字符串的获取，也可通过控件关联的CString类型变量来获取。

图7.8　使用编辑框控件录入数据

7.3　图像控件

图像控件也是常用的控件之一，常用来在对话框中插入位图、图标和指定的矩形区域等图像元素。本节就来介绍图像控件的使用。

7.3.1　通过属性显示位图

图像控件可以通过属性设置来显示位图，其中Type属性可以指定图片的类型，包括Frame（帧）、Rectangle（矩形）、Icon（图标）、Bitmap（位图）和Enhanced Metafile（增强型图元文件）；Image属性则是在Type属性为Icon或Bitmap时，为其指定位图资源。

下面介绍一下通过图像控件属性显示位图的步骤，首先向对话框中导入一个位图资源，然后打开图像控件的属性窗口，设置Type属性为Bitmap，在Image属性中选择位图资源，如图7.9所示。

图7.9　通过图像控件属性显示位图

注意

在选择Bitmap类型时，显示的图片格式只能是BMP。

7.3.2　设置边框颜色和填充颜色

当图像控件的Type属性为Frame或Rectangle时，可以通过控件的Color属性设置边框和矩形区域的填充颜色。颜色包括黑色、灰色和白色等，如图7.10所示是以白色填充的矩形区域。

图7.10　以白色填充的矩形区域

7.3.3　居中显示位图资源

当图像控件的Type属性为Bitmap时，可以在属性窗口的Styles选项卡中选择Center image属性，位图资源将在控件中心显示，并根据位图资源的背景颜色填充空白区域，如图7.11所示。

图7.11　居中显示位图资源

7.4　按钮控件

按钮控件用于执行用户的某种命令，一般通过按钮的单击事件来实现。本节就来介绍按钮的操作。

7.4.1　使用按钮控件显示图标

按钮控件除了显示正常的文本外，还可以显示位图和图标等图像元素。要使用按钮显示图标，首先要向对话框中导入一个图标资源，然后打开按钮控件的属性窗口，选择Icon属性，接下来为按钮控件关联一个CButton类的变量，使用CButton类的SetIcon方法来实现。

语法格式如下：

其中，hIcon表示一个图标句柄。

【例7.5】使用按钮控件显示图标。

属性设置和运行结果如图7.12所示。

图7.12　使用按钮控件显示图标

注意

选中Owner draw复选框，可以在按钮控件上自行绘制图标。

7.4.2　使用按钮控件处理用户操作

使用按钮控件处理用户的操作就是处理按钮控件的单击事件，在按钮单击事件的处理函数中实现用户操作的实现代码。为按钮控件处理单击事件有以下两种方法：

在对话框中选择按钮控件，双击该控件，在弹出的Add Member Function对话框中设置处理函数名，单击OK按钮进行添加（这里以对话框的“确定”按钮为例），如图7.13所示。

在对话框中打开类向导，在“类向导”对话框Message Maps选项卡的Object IDs列表框中选择按钮的ID值，在Messages列表框中选择BN_CLICKED事件，单击Add Function按钮进行添加。

图7.13　Add Member Function对话框

7.5　复选框控件

复选框控件也属于按钮的一种，可以分组使用。使用复选框控件可以简化用户的操作。本节将简单介绍复选框控件的应用。

7.5.1　设置复选框控件的选中状态

在设计程序时，复选框控件的默认初始状态是非选中状态。要设置复选框为选中状态，可以使用SetCheck方法，该方法用于设置复选框是否处于选中状态。

语法格式如下：

其中，nCheck表示复选框的状态。

【例7.6】通过SetCheck方法设置复选框的选中状态。（实例位置：资源包\TM\sl\7\1）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“设置复选框控件的选中状态”。

（2）向对话框中添加2个群组框控件和6个复选框控件。

（3）在对话框的OnInitDialog函数中设置“语文”和“数学”两个复选框被选中，代码如下。

运行结果如图7.14所示。

图7.14　设置复选框控件的选中状态

说明

使用EnableWindow方法可以设置控件是否可用，通过控件的属性也可以实现这一功能。只要选中控件的Disabled属性，就可以使控件不可用，不选中时则控件可用。如果使用属性设置了控件不可用，也可以使用EnableWindow方法重新设置控件为可用。

7.5.2　使用复选框控件统计信息

在应用程序中经常会使用复选框来统计信息，因为复选框操作简单，用户只需选中要选择的信息即可。首先调用GetCheck方法获得控件的选中状态。

语法格式如下：

然后调用GetWindowText函数获得复选框控件的显示信息。

【例7.7】使用复选框控件统计信息。（实例位置：资源包\TM\sl\7\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用复选框控件统计信息”。

（2）向对话框中添加2个静态文本控件、2个编辑框控件、2个群组框控件、8个复选框控件和1个按钮控件。

（3）在对话框的OnInitDialog函数中设置“语文”和“数学”两个复选框被选中，代码如下。

（4）处理“提交”按钮的单击事件，在该事件的处理函数中获得控件中的显示信息，并将获得的信息显示在消息框中，代码如下。

说明

使用GetDlgItem函数可以获得指定控件的窗口指针。

运行结果如图7.15所示。

图7.15　使用复选框控件统计信息

7.6　单选按钮控件

单选按钮控件也属于按钮的一种，可以分组使用。使用单选按钮控件同样可以简化用户的操作。本节将简单介绍单选按钮控件的应用。

7.6.1　为单选按钮控件分组

在使用单选按钮时，有时因为不同的需要会把单选按钮分为几组，使每一组中只有一个处于选中状态。在默认情况下，所有单选按钮都被视为一组。要为单选按钮分组，可以在属性窗口中选择General选项卡中的Group属性，以Tab键顺序为基础。Group属性是设置控件的群组关系的属性，为一个单选按钮选择了Group属性，以Tab键顺序为准，在这个单选按钮以后没有选择该属性的单选按钮都划分为一组，而分为一组的单选按钮可以共用一个成员变量。

7.6.2　获得被选择的单选按钮的文本

要获得单选按钮中的文本，可以使用GetWindowText函数，只是在使用之前需要确定被选择的是哪个单选按钮。可以通过单选按钮的单击事件确定是哪个按钮被选中。

【例7.8】通过GetWindowText函数获得单选按钮中的数据。（实例位置：资源包\TM\sl\7\3）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“获得被选择的单选按钮的文本”。

（2）向对话框中添加4个单选按钮控件和1个按钮控件，为单选按钮分组，并关联一个整型变量m_Radio。

（3）为4个单选按钮处理单击事件，代码如下。

（4）处理“确定”按钮的单击事件，在该事件中获得当前选中的单选按钮的文本，并通过消息框显示出来，代码如下。

实例的运行结果如图7.16所示。

图7.16　获得被选择的单选按钮的文本

说明

使用整型变量记录选中单选按钮的方法，要比遍历窗体中所有单选按钮方便得多。

7.7　组合框控件

组合框控件既可以进行输入，也可以在列表框组成部分中进行选择。本节通过组合框控件的属性和方法对组合框控件进行简单的介绍。

7.7.1　设置控件风格

打开组合框控件的属性窗口，通过Styles选项卡中的Type属性可以设置组合框控件的风格。组合框分为3种风格，即Simple、Dropdown和Drop List。其中，Simple风格的组合框包含一个编辑框和一个总是显示的列表框；Dropdown和Drop List风格的组合框都是在单击下拉箭头后才弹出列表框（这两种风格的组合框的区别：Dropdown风格组合框的编辑框可编辑，而Drop List风格组合框的编辑框是只读的）。

7.7.2　调整列表部分的显示大小

在使用组合框控件时，如果不经过调整，控件的列表框非常小，只能显示一项，操作起来非常麻烦。下面将介绍如何调整组合框列表部分的显示大小，如图7.17所示。

图7.17　调整组合框列表部分的显示大小

注意

在调整组合框控制大小时，需要尺寸适当，不易过小，太小了，否则拖曳滚动条不方便。

7.7.3　通过属性插入数据

在使用组合框控件时，可以不使用代码而直接通过控件的属性窗口为控件添加数据选项。只要在控件的属性窗口中选择Data选项卡，即可在列表框中添加数据（需要注意的是，每添加一个数据后要按Ctrl+Enter快捷键换行，然后才能添加下一个数据），如图7.18所示。

图7.18　通过属性窗口向组合框中插入数据

7.7.4　调整数据显示顺序

组合框控件的默认选中属性中有Sort属性，该属性会使控件中的数据按字母顺序自动排列。但用户有时需要数据按插入顺序排列，此时就要将该属性去掉。选中和去掉Sort属性后程序的运行结果分别如图7.19所示。

图7.19　调整组合框控件中数据的显示顺序

技巧

在使用代码添加数据时，使用insert方法在不去掉Sort属性时也可以不进行自动排序。

7.7.5　获得选择的数据

要获得组合框中列表框部分的数据，首先要获得当前选择的列表项索引，可以使用GetCurSel方法实现。

语法格式如下：

获得当前选择的列表项索引后，还要根据指定的索引获得数据，可以使用GetLBText方法获取列表框中的字符串。

语法格式如下：

参数说明：

nIndex：表示方法返回的项目索引（基于0开始）。

rString：用于接收返回的字符串。

【例7.9】通过GetCurSel方法和GetLBText方法获得列表框中选择的数据。（实例位置：资源包\ TM\sl\7\4）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“获得列表框中选择的数据”。

（2）向对话框中添加一个组合框控件，并通过属性窗口为控件赋初值。

（3）为控件关联一个CComboBox类型变量，并处理组合框的CBN_SELCHANGE消息，当在组合框的列表框部分选择一项时，弹出消息框显示列表项的数据，代码如下。

实例的运行结果如图7.20所示。

图7.20　获得列表框中选择的数据

技巧

因为组合框是由一个编辑框和一个列表框组合而成的，所以也可以使用GetWindowText方法直接获得组合框中当前显示的数据。

7.8　列表框控件

列表框控件显示了一个可选择的列表，可以通过列表框来查看或选择数据项。列表项数是灵活多变的，当列表框中的项数较多时，可以激活滚动条来显示。

7.8.1　在指定位置插入文本

向列表框中指定位置插入文本，需要使用InsertString方法，该方法用于在列表框指定位置插入一个字符串。

语法格式如下：

参数说明：

nIndex：标识插入字符串的位置，如果为-1，字符串将被插入到列表框的末尾。

lpszString：标识一个字符串指针。

【例7.10】使用InsertString方法向列表框中指定位置插入文本。（实例位置：资源包\TM\sl\7\5）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“在列表框控件中指定位置插入文本”。

（2）向对话框中添加2个静态文本控件、2个编辑框控件、1个列表框控件和1个按钮控件。

（3）在对话框初始化时，向列表框中插入数据，代码如下。

注意

使用AddString方法向列表框中添加数据是因为Sort属性会重新进行排序。

（4）处理“插入”按钮的单击事件，获取编辑框中输入的插入数据和位置，将数据插入列表框的指定位置，代码如下。

实例的运行结果如图7.21所示。

图7.21　在列表框控件中指定位置插入文本

7.8.2　避免插入重复数据

使用列表框控件编写程序时，有时由于输入的文本信息过多，容易出现重复输入的情况。要避免这种情况发生，可以通过CListBox类的一些方法来实现。

1. GetText方法

GetText方法用于从列表框中获取一个字符串。

语法格式如下：

参数说明：

nIndex：标识项目索引。

rString：用于接收返回的字符串。

2. GetCount方法

GetCount方法用于获取列表框中的项目数。

语法格式如下：

3. AddString方法

AddString方法用于向列表框中添加字符串。

语法格式如下：

其中，lpszString表示标识字符串指针。

【例7.11】避免向列表框控件中插入重复数据。（实例位置：资源包\TM\sl\7\6）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“避免向列表框控件中插入重复数据”。

（2）向对话框中添加一个编辑框控件、一个列表框控件和一个按钮控件。

（3）处理“插入”按钮的单击事件，获取编辑框中输入的数据，判断数据是否存在，如果存在则弹出提示，反之插入数据，代码如下。

实例的运行结果如图7.22所示。

图7.22　避免向列表框控件中插入重复数据

技巧

在对已添加的项进行查找时，也可以通过FindString方法实现。

7.8.3　实现复选数据功能

可以通过CCheckListBox类在列表框控件中实现复选数据功能。CCheckListBox类是对CListBox类的扩充，使列表框控件具有复选功能，通过GetCheck方法可以判断当前列表项的复选框是否被选中。

语法格式如下：

其中，nIndex表示标识项目索引。

【例7.12】在列表框控件中实现复选数据功能。（实例位置：资源包\TM\sl\7\7）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“在列表框控件中实现复选数据功能”。

（2）向对话框中添加一个列表框控件和一个按钮控件，设置Owner draw属性为Fixed（该属性用于确定控件的所有者如何绘制控件），并选择Has strings属性（该属性用于标识一个owner-draw列表框中的项目由字符串组成）。

（3）处理“确定”按钮的单击事件，获取列表框中的选中项数据，通过消息框将数据显示出来，代码如下。

实例的运行结果如图7.23所示。

图7.23　在列表框控件中实现复选数据功能

7.9　进度条控件

进度条控件用于显示程序的进度，在进行程序安装、文件传输时经常用到。本节将通过进度条控件的属性和方法介绍进度条的使用。

7.9.1　设置显示风格

进度条控件具有两种显示方式，可以通过属性来选择。默认的进度条控件以进度块的方式显示。在进度条控件属性窗口Styles选项卡中选择Smooth属性，进度条控件将以平滑的方式显示，如图7.24所示。

图7.24　进度条显示方式

说明

在选择进度条风格时，也可以通过vertical属性将进度条以垂直的形式显示。

7.9.2　设置进度条的范围

在使用进度条控件时，可以使用SetRange方法来设置进度条控件的范围。

语法格式如下：

参数说明：

nLower：进度条的下界范围。

nUpper：进度条的上界范围。

说明

如果不设置进度条范围，则默认进度条的范围为0～100。

【例7.13】使用SetRange方法设置进度条控件的范围。

首先为进度条控件关联一个CProgressCtrl类型变量m_Prog，通过变量m_Prog调用SetRange方法。代码如下：

7.10　小结

本章介绍了Visual C++开发环境中的常用控件，在介绍的同时列举了大量实例，目的是使读者学习起来更加快捷、方便，掌握常用控件的同时为以后学习高级控件打下坚实的基础。

7.11　实践与练习

1. 设计一组应用程序，利用列表框控件实现标签式数据选择，结果如图7.25所示。（答案位置：资源包\TM\sl\7\8）

2. 设计一组应用程序，使用组合框控件来列举磁盘目录，结果如图7.26所示。（答案位置：资源包\TM\sl\7\9）

图7.25　实现标签式数据选择

图7.26　使用组合框控件列举磁盘目录

第8章

菜单

（视频讲解：19分钟）

在Windows应用程序中，菜单是必不可少的界面元素，几乎每一个应用程序都需要用到菜单。单击菜单可以执行操作命令，简单易用，方便快捷。菜单对Windows应用程序操作进行了简化，使Windows应用程序得到了更好的推广。本章将介绍菜单资源的设计、菜单项命令处理、菜单的创建以及自绘菜单的方法。

通过阅读本章，您可以：

掌握如何设计菜单资源

掌握菜单项的命令处理方式

掌握如何动态创建菜单

掌握如何创建弹出式菜单

掌握如何创建图标菜单

8.1　菜单资源设计

菜单是图形化界面的重要组成部分，用户通过菜单可以方便地与应用程序进行交互。在Visual C++ 6.0中使用菜单前，先要创建菜单资源。创建菜单资源的步骤如下。

（1）打开工作区窗口，选择ResourceView选项卡，右击一个节点，在弹出的快捷菜单中选择Insert命令，打开Insert Resource对话框。选择Menu节点，如图8.1所示。

（2）单击New按钮，创建一个菜单资源，如图8.2所示。

图8.1　Insert Resource对话框

图8.2　菜单资源

（3）在菜单资源设计窗口中按Enter键，打开Menu Item Properties（菜单资源属性）对话框，如图8.3所示。

图8.3　Menu Item Properties对话框

Menu Item Properties对话框中各属性的说明如表8.1所示。

表8.1　菜单资源的属性说明

（4）在Caption文本框中设置菜单标题，然后关闭Menu Item Properties对话框，返回到菜单资源设计窗口中，此时菜单效果如图8.4所示。

（5）在“动漫”菜单下的虚线框上按Enter键，打开Menu Item Properties对话框，从中可以添加子菜单。设置子菜单ID和子菜单的标题，如图8.5所示。

图8.4　菜单资源

图8.5　设置子菜单资源属性

（6）重复步骤（5）可以为“动漫”菜单添加多个子菜单，如图8.6所示。

（7）如果要插入一个分隔线，可以选择Separator属性，并且菜单资源设计窗口支持鼠标拖曳功能，可以在选中菜单项后将其拖曳至适当位置，从而调整各个菜单的位置，如图8.7所示。

图8.6　添加子菜单

图8.7　添加分隔条

（8）如果要为子菜单添加弹出菜单，可以在子菜单的属性对话框中选择Pop-up属性，此时子菜单的右侧会弹出一个新的菜单，如图8.8所示。

注意

选择Pop-up属性后，该菜单项将变成上级菜单，菜单项的ID将不能编辑。

（9）通过设计子菜单的步骤可以设置弹出菜单，如图8.9所示。

图8.8　添加弹出菜单

图8.9　菜单资源

（10）至此，菜单资源的初步设计就结束了。但是，为了方便用户的操作，还要为菜单项设置加速键（或称快捷键），在菜单标题的后面加“&+字母”即可实现加速键的设置。选择要显示菜单的对话框资源，打开对话框资源的属性对话框，在General选项卡的Menu列表框中选择要显示的菜单ID，程序运行时，用户只需同时按下Alt键和该字母键，即可激活并操作该菜单。程序运行结果如图8.10所示。

图8.10　为菜单添加加速键

注意

如果设置加速键的菜单项是子菜单，则还需要为该菜单的上级菜单设置加速键，否则上级菜单没有加速键就不能运行子菜单的加速键。

8.2　菜单项的命令处理

Windows提供了一套非常复杂的消息处理机制，但是在Visual C++中为菜单项处理命令却非常简单，只需要打开MFC classWizard对话框，在其中选择Message Maps选项卡，在Class name组合框中选择关联菜单的对话框类，在Object IDs列表框中选择菜单项ID，在Messages列表框中选择COMMAND和UPDATE_COMMAND_UI，如图8.11所示。

其中，COMMAND事件是菜单的单击触发事件，UPDATE_COMMAND_UI事件用于更新菜单状态。选择要处理的命令（这里以COMMAND消息为例）后，单击Add Function按钮，弹出Add Member Function对话框，并给出默认时的命令处理函数名，如图8.12所示。

图8.11　MFC ClassWizard对话框

图8.12　Add Member Function对话框

单击OK按钮，就添加了菜单项的命令处理函数。在MFC ClassWizard对话框中单击Edit Code按钮，即可打开添加的OnMenumessage命令处理函数，在该函数中可以编辑菜单项的功能。

说明

通过UPDATE_COMMAND_UI事件可以设置菜单项的核对标记。

8.3　动态创建菜单

CMenu类封装了Windows的菜单功能，该类中提供了用于创建、修改、合并菜单的方法，通过这些方法用户可以对菜单进行各种操作。

（1）在动态创建菜单时，首先要通过CreateMenu方法创建一个菜单窗口，并将其关联到菜单对象上。

语法格式如下：

返回值：执行成功，返回值为非零，否则为0。

（2）然后调用CreatePopupMenu方法创建一个弹出式菜单窗口，并将其关联到菜单对象上。

语法格式如下：

返回值：执行成功，返回值为非零，否则为0。

对于弹出式菜单，如果菜单窗口被释放，则菜单对象将被自动释放。

（3）调用AppendMenu方法添加菜单项。

语法格式如下：

参数说明：

nFlags：标识菜单项的状态信息。

nIDNewItem：标识菜单项的ID。

lpszNewItem：标识菜单项的内容。

pBmp：标识关联菜单项的位图对象指针。

（4）调用Detach方法从菜单对象上分离菜单句柄。

语法格式如下：

返回值：分离的菜单句柄。

（5）最后，需要调用SetMenu方法将创建的菜单分配到指定窗口。

语法格式如下：

其中，hMenu指菜单句柄。

【例8.1】动态创建一个菜单。（实例位置：资源包\TM\sl\8\1）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“动态创建菜单”。

（2）在对话框的头文件中声明一个CMenu类对象m_Menu。

（3）在工作区窗口中选择FileView选项卡，打开资源头文件（Resource.h），在该文件中定义命令ID，代码如下。

（4）在对话框的OnInitDialog方法中创建菜单，代码如下。

注意

由于动态创建菜单是通过代码创建菜单项，因此不需要加载菜单资源。

（5）在对话框的头文件中声明菜单的消息处理函数，代码如下。

（6）在对话框的源文件中添加消息映射宏，将命令ID关联到消息处理函数中，代码如下。

（7）在对话框的源文件中添加消息处理函数，实现代码如下。

实例的运行结果如图8.13所示。

图8.13　动态创建菜单

8.4　创建弹出式菜单

创建弹出式菜单的步骤如下。

（1）先创建一个菜单资源，然后调用LoadMenu方法加载菜单资源。

语法格式如下：

参数说明：

lpszResourceName：标识资源名称。

nIDResource：标识资源ID。

返回值：若执行成功，返回值为非零，否则为0。

（2）调用GetSubMenu方法获取弹出式菜单中的一个菜单项。

语法格式如下：

参数说明：

nPos：标识菜单项位置，第1个菜单项对应的位置是0，第2个菜单项对应的位置是1，以此类推。

最后，调用TrackPopupMenu方法显示一个弹出式菜单。

语法格式如下：

TrackPopupMenu方法中的参数说明如表8.2所示。

表8.2　TrackPopupMenu方法中的参数说明

表8.3　nFlags标记中的可选值说明

【例8.2】动态创建一个弹出式菜单。（实例位置：资源包\TM\sl\8\2）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“创建弹出式菜单”。

（2）在工作区窗口中选择RecourceView选项卡，右击一个节点，在弹出的快捷菜单中选择Insert命令，打开Insert Resource对话框，插入一个菜单资源。新建的菜单资源如图8.14所示。

（3）在对话框的头文件中声明一个CMenu类对象m_Menu。

（4）在OnInitDialog方法中调用LoadMenu方法加载菜单资源，代码如下：

（5）在工作区窗口中选择ClassView选项卡，右击CpopupMenuDlg节点，在弹出的快捷菜单中选择Add Windows Message Handler命令，在弹出的New Windows Message and Event Handlers for class CPopupMenuDlg窗口中选择WM_RBUTTONUP事件，该事件在鼠标右键抬起时触发，如图8.15所示。

图8.14　菜单资源

图8.15　添加鼠标右键抬起事件

（6）处理鼠标右键抬起事件的处理函数，在该函数中添加弹出菜单的实现代码，代码如下。

注意

在显示弹出菜单时，使用ClientToScreen函数进行坐标转换，否则弹出菜单的位置可能出错。

（7）按Ctrl+W快捷键打开类向导，在其中为“添加文件”菜单项处理单击事件，代码如下。

实例的运行结果如图8.16所示。

图8.16　创建弹出式菜单

8.5　创建图标菜单

在使用应用程序时，常会看到程序中的菜单带有漂亮的图标，不但美化了程序界面，也可以更好地吸引用户使用。可是在一般情况下，使用CMenu类设计的菜单项并不能显示图标。为了使程序菜单中显示图标，可以通过CMenu类派生一个图标菜单类CMenuIcon，通过CMenuIcon类在菜单项中显示图标。

（1）在CMenuIcon类中，使用GetMenuItemCount方法获得弹出式菜单或顶层菜单的菜单数。

语法格式如下：

返回值：如果菜单项没有子菜单，则函数返回值为-1，否则返回子菜单数。

（2）调用GetMenuString方法设置菜单项的文本。

语法格式如下：

GetMenuString方法中的参数说明如表8.4所示。

表8.4　GetMenuString方法中的参数说明

（3）调用ModifyMenu方法修改菜单项信息。

语法格式如下：

ModifyMenu方法中的参数说明如表8.5所示。

表8.5　ModifyMenu方法中的参数说明

【例8.3】创建一个可以显示图标的菜单。（实例位置：资源包\TM\sl\8\3）

具体操作步骤如下。

（1）创建一组基于对话框的应用程序，将对话框的Caption属性修改为“创建图标菜单”。

（2）在工作区窗口中选择RecourceView选项卡，右击一个节点，在弹出的快捷菜单中选择Insert命令，打开Insert Resource对话框，插入一个菜单资源。新建的菜单资源如图8.17所示。

图8.17　菜单资源

（3）打开类向导，创建一个新类，类名为CMenuIcon，打开CMenuIcon类的头文件，为该类添加一个基类Cmenu，代码如下。

（4）在CMenuIcon的头文件中定义一个数据结构，用来保存菜单项信息，代码如下。

（5）在CMenuIcon的头文件中声明成员变量，代码如下。

注意

MAX_MENUCOUNT的值可以是任意的，用户可以根据实际需要进行设置，但是需要注意的是，MAX_MENUCOUNT的值不能小于菜单项的个数。

（6）向程序中导入6个图标资源，并在CMenuIcon类的构造函数中初始化图像列表，代码如下。

（7）在CMenuIcon类中添加AttatchMenu方法，代码如下。

（8）在CMenuIcon类中添加MenuItem方法，该方法用于修改菜单项信息，使其包含MF_OWNERDRAW风格，并将菜单项信息存储在m_ItemLists成员变量中，代码如下。

（9）重载CMenuIcon类的MeasureItem虚拟方法，计算菜单项的大小，代码如下。

（10）在CMenuIcon类中添加DrawItemText成员函数，用于绘制菜单项文本，代码如下。

注意

在使用DT_VCENTER属性设置文本垂直居中时，一定要设置DT_SINGLELINE属性，否则文本无法垂直居中。

（11）在CMenuIcon类中添加DrawItemIcon成员函数，用于绘制菜单项图标，代码如下。

（12）在CMenuIcon类中添加DrawSeparater成员函数，用于绘制分隔条，代码如下。

（13）在CMenuIcon类中添加DrawTopMenu成员函数，用于绘制顶层菜单，代码如下。

（14）在CMenuIcon类中添加DrawComMenu成员函数，绘制普通菜单项，代码如下。

（15）重载CMenuIcon类的DrawItem虚拟方法，根据菜单项的不同状态绘制菜单项，代码如下。

（16）在对话框头文件中声明CMenuIcon类对象m_Menu。

（17）在对话框的OnInitDialog方法中加载菜单资源，并设置菜单项，代码如下。

（18）重载对话框的OnDrawItem方法，在该方法中调用菜单类的DrawItem方法。

（19）重载对话框的OnMeasureItem方法，在该方法中调用菜单类的MeasureItem方法。

实例的运行结果如图8.18所示。

图8.18　创建图标菜单

8.6　小结

本章介绍了菜单的创建及使用，包括动态创建菜单、弹出式菜单和图标菜单，并且对菜单资源的设计以及菜单项的命令处理也作了简单的介绍，使读者在开发Windows应用程序时可以更好地使用菜单。

8.7　实践与练习

1. 设计一个任务栏托盘弹出菜单，实现效果如图8.19所示。（答案位置：资源包\TM\sl\8\4）

图8.19　任务栏托盘弹出菜单

2. 设计一个浮动的菜单。就像Word中用户可以拖动菜单到任意位置。（答案位置：资源包\TM\sl\ 8\5）

3. 设计一个能根据数据表中的数据动态生成菜单，效果详见资源包中的实例。（答案位置：资源包\ TM\sl\8\6）

第9章

工具栏和状态栏

（视频讲解：22分钟）

工具栏为用户操作提供了更加快捷、方便的途径，其一般实现的是常用的菜单操作。状态栏一般位于程序窗口的最下方，可以显示程序的基本信息。本章将简要介绍工具栏和状态栏，使读者初步了解工具栏和状态栏的使用，为日后编程打下良好的基础。为了便于读者理解，在讲解过程中列举了大量的实例。

通过阅读本章，您可以：

掌握工具栏资源的设计

掌握工具栏的命令处理

掌握创建工具栏的多种方法

掌握提示工具栏的设计方法

掌握状态栏的创建方法

掌握在状态栏中显示控件的方法

9.1　工具栏设计

工具栏中包含了一组用于执行命令的按钮，每个按钮都用一个图标来表示。通常，工具栏按钮都对应一个常用的菜单命令，这样可以简化用户的操作。

9.1.1　工具栏资源设计

要使用工具栏，先要创建工具栏资源。工具栏资源的创建步骤如下。

（1）打开工作区窗口，选择ResourceView选项卡，右击一个节点，在弹出的快捷菜单中选择Insert命令，打开Insert Resource对话框，选择Toolbar节点，如图9.1所示。

（2）单击New按钮，创建一个工具栏资源，如图9.2所示。

图9.1　Insert Resource对话框

图9.2　工具栏资源

（3）创建工具栏资源后，可以通过开发环境右侧的绘图工具（如图9.3所示）对工具栏资源进行编辑。

（4）在工具栏资源中绘制工具栏按钮后，工具栏窗口中会自动创建一个新的工具栏按钮，如图9.4所示。

图9.3　绘图工具

图9.4　创建工具栏按钮

（5）还可以为工具栏资源添加分隔条。选中要加入分隔线位置的按钮，将其向右拖动一点距离，这样就会在两个按钮之间留下一点空隙，如图9.5所示。

说明

拖到工具栏按钮的位置以后，运行程序时空隙就会显示成分隔条了。

（6）按Enter键打开工具栏按钮的属性窗口，如图9.6所示。

图9.5　添加分隔条

图9.6　属性窗口

工具栏资源的属性说明如表9.1所示。

表9.1　工具栏资源的属性说明

说明

如果为一个工具栏按钮设置了大小，那么所有工具栏按钮的大小都会改变为当前设置的大小。

9.1.2　工具栏的命令处理

工具栏的命令处理也要通过类向导来完成。打开MFC class Wizard对话框，选择Message Maps选项卡，在Class name组合框中选择关联菜单的对话框类，在Object IDs列表框中选择菜单项ID，在Messages列表框中选择COMMAND和UPDATE_COMMAND_UI，如图9.7所示。

选择工具栏命令后，单击Add Function按钮，即可添加消息响应函数，并可以通过代码编辑器编辑该消息的实现代码。

技巧

因为工具栏实现的功能通过菜单都可以实现，所以，工具栏按钮可以和菜单共用一个ID。

图9.7　“类向导”对话框

9.1.3　动态创建工具栏

1. 使用位图创建工具栏

使用位图创建工具栏的步骤如下。

（1）通过Create方法创建工具栏窗口。

语法格式如下：

参数说明：

pParentWnd：标识父窗口。

dwStyle：标识工具栏风格。

nID：标识工具栏ID。

（2）调用SetButtons方法向工具栏中添加按钮，并设置按钮的ID和图像索引。

语法格式如下：

参数说明：

lpIDArray：标识一个无符号整型数组，其中包含了按钮ID，如果数组中的某个元素值为ID_SEPARATOR，对应的按钮将是一个分隔条。

nIDCount：标识数组中的元素数量。

（3）调用LoadBitmap方法加载一个位图资源，位图中包含了每个工具栏按钮的图像。

语法格式如下：

参数说明：

lpszResourceName：标识资源名称。

nIDResource：标识资源ID。

返回值：执行成功，返回值是非零，否则为0。

（4）调用SetSizes方法设置工具栏按钮和显示位图的大小。

语法格式如下：

参数说明：

sizeButton：工具栏按钮的大小。

sizeImage：显示位图的大小。

（5）调用RepositionBars方法显示工具栏。

语法格式如下：

RepositionBars方法中的参数说明如表9.2所示。

表9.2　RepositionBars方法中的参数说明

【例9.1】动态创建一个工具栏。（实例位置：资源包\TM\sl\9\1）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“动态创建工具栏”。

（2）在工作区窗口中选择RecourceView选项卡，导入一个位图资源，如图9.8所示。

（3）在对话框的头文件中声明CToolBar类对象m_ToolBar。

（4）在对话框的OnInitDialog函数中创建工具栏，代码如下。

图9.8　位图资源

注意

加载的位图资源中每个图像的大小都是相同的。

（5）在对话框的头文件中声明工具栏按钮的单击事件处理函数，代码如下。

（6）在对话框的源文件中添加消息映射宏，代码如下。

（7）添加工具栏按钮的单击事件处理函数，代码如下。

实例的运行结果如图9.9所示。

图9.9　动态创建工具栏

2. 使用图标创建工具栏

使用图标创建工具栏，首先要创建图像列表，然后用工具栏关联图像列表。图像列表部分的知识将在以后的章节中介绍，这里只介绍创建工具栏的方法。首先调用Create方法创建工具栏窗口，然后调用SetButtons方法设置工具栏按钮的索引，通过GetToolBarCtrl方法调用SetImageList方法关联图像列表，调用SetSizes方法设置工具栏按钮和显示图标的大小，最后调用RepositionBars方法显示工具栏。

【例9.2】动态创建一个工具栏。（实例位置：资源包\TM\sl\9\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“动态创建工具栏”。

（2）在工作区窗口中选择RecourceView选项卡，导入8个图标资源。

（3）在对话框的头文件中声明变量，代码如下。

（4）在对话框的OnInitDialog函数中创建工具栏，代码如下。

实例的运行结果如图9.10所示。

图9.10　动态创建工具栏

注意

为工具栏按钮处理单击事件的方法与例9.1相同，这里就不多作介绍了。

3. 使用工具栏资源创建工具栏

使用工具栏资源创建工具栏的步骤如下。

（1）调用CreateEx方法创建工具栏窗口，并设置工具栏窗口的扩展风格。

语法格式如下：

CreateEx方法中的参数说明如表9.3所示。

表9.3　CreateEx方法中的参数说明

（2）调用LoadToolBar方法加载工具栏资源。

语法格式如下：

参数说明：

lpszResourceName：标识资源名称。

nIDResource：标识资源ID。

返回值：如果函数执行成功，返回值是非零，否则为0。

（3）通过GetToolBarCtrl方法调用SetBitmapSize方法和SetButtonSize方法，设置显示图像的大小和工具栏按钮的大小。

（4）最后调用RepositionBars方法显示工具栏。

【例9.3】动态创建一个工具栏。（实例位置：资源包\TM\sl\9\3）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“动态创建工具栏”。

（2）在工作区窗口中选择RecourceView选项卡，导入一个工具栏资源，如图9.11所示。

（3）在对话框的头文件中声明CToolBar类对象m_ToolBar。

（4）在对话框的OnInitDialog函数中创建工具栏，代码如下。

实例的运行结果如图9.12所示。

图9.11　工具栏资源

图9.12　动态创建工具栏

9.1.4　设置工具栏按钮提示

在使用应用程序时，会发现工具栏具有提示功能，只要将鼠标停留在一个工具栏按钮上，就会显示出这个按钮的功能描述。这在Visual C++中是如何实现的呢？可以通过TTN_NEEDTEXT消息的处理函数OnToolTipNotify来实现这一功能。

（1）首先创建工具栏，然后调用SetButtonText方法设置工具栏按钮的显示文本。

语法格式如下：

参数说明：

nIndex：按钮命令ID。

lpszText：按钮显示文本。

（2）然后在OnToolTipNotify消息处理函数中获得当前按钮文本，并在提示窗口中显示出来。获得按钮文本可以使用GetButtonText方法。

语法格式如下：

参数说明：

nIndex：按钮索引。

rString：用于接收按钮文本。

【例9.4】创建一个具有提示功能的工具栏。（实例位置：资源包\TM\sl\9\4）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“设置工具栏按钮提示”。

（2）在工作区窗口中选择RecourceView选项卡，导入8个图标资源。

（3）在对话框头文件中声明变量，代码如下。

（4）在对话框的OnInitDialog函数中创建工具栏，代码如下。

注意

在创建工具栏后，不要忘记用EnableToolTips方法激活工具栏的提示功能。

（5）在对话框的头文件中声明OnToolTipNotify函数，代码如下。

（6）在对话框的源文件中添加ON_NOTIFY_EX映射宏，代码如下。

（7）添加消息处理函数OnToolTipNotify的实现部分，代码如下。

实例的运行结果如图9.13所示。

图9.13　设置工具栏按钮提示

9.2　状态栏设计

状态栏可以显示应用程序当前的状态、系统时间或其他上下文信息等，也可以显示菜单和工具栏的提示信息。可以根据需要将状态栏分成多个面板，以显示不同的信息。

9.2.1　创建状态栏

状态栏为程序界面提供了一个显示区域，用于显示程序的基本信息。一般来说，状态栏都放置在程序界面的底部。

状态栏和菜单、工具栏不同，它没有资源设计部分。

（1）创建状态栏时直接使用代码进行设计。可以使用Create方法创建状态栏窗口。

语法格式如下：

参数说明：

pParentWnd：状态栏父窗口。

dwStyle：状态栏风格。可选值如下。

CBRS_TOP：状态栏位于框架窗口的顶部。

CBRS_BOTTOM：状态栏位于框架窗口的底部。

CBRS_NOALIGN：当父窗口重新调整尺寸时不重新定位状态栏。

nID：状态栏ID。

说明

虽然可以将状态栏置于窗口的顶部，但是通常情况下不会这么做，默认时将工具栏放置在窗口底部。

使用CreateEx方法也可以创建状态栏窗口，并且该方法支持状态栏的扩展风格。

语法格式如下：

CreateEx方法中的参数说明如表9.4所示。

表9.4　CreateEx方法中的参数说明

（2）调用SetIndicators方法为状态栏窗口添加面板，设置面板ID。

语法格式如下：

参数说明：

lpIDArray：一个无符号整型数组，该数组中包含了面板ID。

nIDCount：数组元素数量。

（3）调用SetPaneInfo方法设置面板宽度。

语法格式如下：

SetPaneInfo方法中的参数说明如表9.5所示。

表9.5　SetPaneInfo方法中的参数说明

（4）调用SetPaneText方法设置面板文本。

语法格式如下：

参数说明：

nIndex：面板ID。

lpszNewText：面板显示文本。

bUpdate：是否立即更新面板。

（5）最后调用RepositionBars方法显示状态栏。

【例9.5】创建一个带有状态栏的程序。（实例位置：资源包\TM\sl\9\5）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“创建状态栏”。

（2）在对话框头文件中声明一个CStatusBar类变量m_StatusBar。

（3）在对话框的OnInitDialog函数中创建工具栏，代码如下。

实例的运行结果如图9.14所示。

图9.14　创建状态栏

注意

如果程序中同时具有工具栏和状态栏，只需要调用一次RepositionBars函数。

9.2.2　在状态栏中显示控件

在使用应用程序时，经常看到在状态栏中显示操作进度，这是怎么做到的呢？可以在状态栏中放置进度条控件，从而实现这一功能。

首先，调用Create方法创建状态栏窗口，调用SetIndicators方法向状态栏窗口中添加面板，调用SetPaneInfo方法设置面板的宽度，调用SetPaneText方法设置面板的显示文本，调用RepositionBars方法显示状态栏。然后调用GetItemRect方法获得要显示控件的面板区域。

语法格式如下：

参数说明：

nIndex：面板ID。

lpRect：面板的显示区域。

最后通过MoveWindow函数和ShowWindow函数移动控件到获得的面板区域中，并显示控件。

【例9.6】在状态栏中显示控件。（实例位置：资源包\TM\sl\9\6）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“在状态栏中显示控件”。

（2）在对话框头文件中声明一个CStatusBar类变量m_StatusBar。

（3）在对话框的OnInitDialog函数中创建工具栏，代码如下。

实例的运行结果如图9.15所示。

图9.15　在状态栏中显示控件

注意

在状态栏上放置其他控件时，如果窗体的大小可以改变，则应注意当前窗口大小发生改变时，改变控件的位置及大小。

9.3　小结

本章介绍了工具栏和状态栏的创建方法，包括使用不同的资源创建工具栏，并且通过实例对工具栏和状态栏的一些应用作了简单的介绍，使读者可以初步了解工具栏和状态栏，为以后进行程序开发打下良好的基础。

9.4　实践与练习

1. 设计一个带下拉菜单的工具栏。设计完成的效果：当用户单击工具栏按钮旁边的下三角按钮时，将弹出一个下拉菜单。（答案位置：资源包\TM\sl\9\7）

2. 设计一个带滚动字幕的状态栏。设计完成的效果：网址“WWW.MRSOFT.COM”在状态栏中来回滚动。（答案位置：资源包\TM\sl\9\8）

第10章

高级控件

（视频讲解：46分钟）

在使用可视化编程工具时，控件的使用是必不可少的。前文中已经介绍了一些常用的控件，但是，只有这些是不够的。本章将介绍几个相对复杂的高级控件，进一步深入了解控件的使用，为日后编程打下良好的基础。为了便于读者理解，在讲解过程中列举了大量的实例。

通过阅读本章，您可以：

掌握图像列表控件的应用

属性列表视图控件的各种风格显示

掌握列表视图控件的使用

掌握树控件的使用

掌握标签控件的使用

掌握工具提示控件的使用

10.1　图像列表控件

图像列表控件与前面介绍的控件有些不同，该控件不能通过控件面板向程序中添加，因为图像列表控件只是一个类CImageList，该控件的创建和设置都是通过代码编辑的。该控件用于存储和管理相同大小的一组图像，其中的每个图像可以通过图像索引访问。

10.1.1　创建图像列表

在前面的章节中已经提到过图像列表控件，下面就来介绍一下图像列表控件的创建。创建图像列表可以使用Create方法，该方法用于创建图像列表。

语法格式如下：

Create方法中的参数说明如表10.1所示。

表10.1　Create方法中的参数说明

【例10.1】通过Create方法创建一个图像列表。

代码如下：

10.1.2　将图像绘制到程序中

通过图像列表控件，可以直接将控件中的图像绘制到程序中。首先调用Create方法创建一个图像列表，然后调用Add方法向图像列表控件中添加图像。

语法格式如下：

Add方法中的参数说明如表10.2所示。

表10.2　Add方法中的参数说明

最后，调用Draw方法将图像列表中的图像绘制在指定的画布上。

语法格式如下：

Draw方法中的参数说明如表10.3所示。

表10.3　Draw方法中的参数说明

【例10.2】将图像列表控件中的图像绘制到程序中。（实例位置：资源包\TM\sl\10\1）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“将图像列表控件中的图像绘制到程序中”。

（2）在工作区窗口中选择RecourceView选项卡，导入一个位图资源。

（3）在对话框头文件中声明一个图像列表对象m_ImageList。

（4）在对话框的OnInitDialog函数中创建图像列表，并向图像列表中加载位图，代码如下。

（5）在对话框的OnPaint函数中绘制图像，代码如下。

实例的运行结果如图10.1所示。

图10.1　将图像列表控件中的图像绘制到程序中

说明

图像列表通常用来存放图标，因此存储的图像都不大。其实，图像列表可以存储大的图像信息，但这些图像的大小都应相同。

10.2　列表视图控件

列表视图控件是用户常用的控件之一，可以看成列表框控件的功能增强版。列表视图控件可以根据用户的需要，以不同的风格来显示数据及图标等内容。本节将对列表视图控件进行简单的介绍。

10.2.1　设置显示风格

列表视图控件可在窗体中管理和显示列表项。可以根据需要为列表视图控件选择不同的显示方式，能够以图标和表格的形式显示数据。打开列表视图控件的属性窗口，在Styles选项卡的View属性中可以设置显示风格。可选值包括如下内容。

Icon：图标视图。

Small Icon：小图标视图。

List：列表视图。

Report：报表视图。

10.2.2　设计登录窗口

使用列表视图控件设计登录窗口很简单，首先创建一个图像列表，并通过SetImageList方法将列表视图控件和图像列表关联到一起。

语法格式如下：

参数说明：

pImageList：标识图像列表指针。

nImageList：标识图像列表类型。可选值如下。

LVSIL_NORMAL：图像列表具有大图标。

LVSIL_SMALL：图像列表具有小图标。

LVSIL_STATE：图像列表具有状态图标。

然后调用InsertItem方法向列表视图控件插入数据。

语法格式如下：

InsertItem方法中的参数说明如表10.4所示。

表10.4　InsertItem方法中的参数说明

【例10.3】利用列表视图控件设计登录窗口。（实例位置：资源包\TM\sl\10\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“利用列表视图控件设计登录窗口”。

（2）在工作区窗口中选择RecourceView选项卡，导入7个图标资源。

（3）向对话框中添加一个列表视图控件、一个静态文本控件、一个编辑框控件和一个按钮控件。

（4）在对话框头文件中声明一个图像列表对象m_ImageList。

（5）在对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，向列表视图中插入数据，代码如下。

实例的运行结果如图10.2所示。

图10.2　利用列表视图控件设计登录窗口

注意

本实例主要介绍的是列表视图控件，所以只有控件的设计，没有编写登录等功能。

10.2.3　将数据加载到列表

10.2.2节已经介绍了列表视图控件的大图标显示风格，本节以报表风格介绍列表视图控件。向列表视图控件中加载数据时，首先调用SetExtendedStyle方法设置列表视图控件的扩展风格。

语法格式如下：

其中，dwNewStyle用于标识列表视图控件的扩展风格。

返回值：函数调用前的扩展风格。

然后调用InsertColumn方法向列表视图控件添加列。

语法格式如下：

InsertColumn方法中的参数说明如表10.5所示。

表10.5　InsertColumn方法中的参数说明

在插入数据时先调用InsertItem方法插入行，接着调用SetItemText方法向列表的每一列插入数据。

语法格式如下：

参数说明：

hItem：标识节点句柄。

lpszItem：标识节点文本。

在使用列表视图控件时，有时会因为数据插入错误需要删除数据。在删除数据前，首先要获得待删除数据的索引，可以调用GetSelectionMark方法实现，该方法用于获取列表视图中当前选中的视图项索引。

语法格式如下：

然后调用DeleteItem方法删除一个指定索引的视图项。

语法格式如下：

其中，nItem用于标识视图项索引。

除了使用DeleteItem方法删除一个视图项外，还可以使用DeleteAllItems方法删除所有的视图项。

语法格式如下：

【例10.4】向列表视图控件中插入数据。（实例位置：资源包\TM\sl\10\3）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“将数据加载到列表视图控件中”。

（2）向对话框中添加1个列表视图控件、2个静态文本控件、2个编辑框控件和3个按钮控件，并使用类向导为控件关联变量。

（3）在对话框的OnInitDialog函数中设置列表视图控件的扩展风格，并设置列信息，代码如下。

注意

列表视图控件在默认状态下是没有网格线等样式的，所以必须在程序中使用代码来指定。

（4）处理“插入”按钮的单击事件，将编辑框控件中的数据插入到列表视图控件中，代码如下。

（5）处理列表视图控件的NM_CLICK消息，在该消息的处理函数中获得当前选中的列表项索引，并将当前项的文本显示在编辑框中，代码如下。

（6）处理“删除”按钮的单击事件，将当前选中的列表项删除，代码如下。

（7）处理“清空”按钮的单击事件，将列表视图中的数据全部删除，代码如下。

实例的运行结果如图10.3所示。

图10.3　将数据加载到列表视图控件中

10.2.4　具有位图背景的控件

设计具有位图背景的列表视图控件需要使用SetBkImage方法，该方法用于设置列表视图控件的当前背景图像。

语法格式如下：

SetBkImage方法中的参数说明如表10.6所示。

表10.6　SetBkImage方法中的参数说明

如果只使用上面的方法为控件添加背景，在显示文字时字体的白色背景会将位图背景覆盖，从而影响界面的美观。要解决这一问题，可以通过SetTextBkColor方法设置文本背景颜色。

语法格式如下：

其中，cr标识设置的文本背景颜色。

【例10.5】设计具有位图背景的列表视图控件。（实例位置：资源包\TM\sl\10\4）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“设计具有位图背景的列表视图控件”。

（2）向对话框中添加一个列表视图控件，并使用类向导为控件关联变量，向工程中导入4个图标资源。

（3）在程序初始化时调用CoInitialize函数初始化COM环境。

（4）在对话框头文件中声明一个图像列表对象m_ImageList。

（5）在对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，向列表视图中插入数据，获得位图文件路径，并为列表视图控件绘制背景，代码如下。

实例的运行结果如图10.4所示。

图10.4　设计具有位图背景的列表视图控件

注意

在给列表视图控件设置背景时，背景图片应尽量选择大小合适的图片。

10.2.5　动态创建列表视图控件

动态创建列表视图控件需要使用Create方法，该方法可以创建一个列表视图控件。

语法格式如下：

Create方法中的参数说明如表10.7所示。

表10.7　Create方法中的参数说明

然后调用MoveWindow函数设置控件的显示位置，最后调用ShowWindow函数显示控件。

【例10.6】动态创建列表视图控件。（实例位置：资源包\TM\sl\10\5）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“动态创建列表视图控件”。

（2）在对话框头文件中声明一个CListCtrl类变量m_List。

（3）在对话框的OnInitDialog函数中创建列表视图控件，设置列表视图控件的扩展风格，设置列信息，并向列表视图控件中添加数据，代码如下。

说明

列表视图的动态创建与设计时期创建不同，必须指定控件在窗体中的位置，并且在窗体发生改变时需要重新指定列表视图控件的位置。

实例的运行结果如图10.5所示。

图10.5　动态创建列表视图控件

10.3　树控件

树控件可以对数据进行分层显示。在树控件中，除根节点外，每个节点都有一个父节点，可以拥有多个兄弟节点和子节点，从而可以使数据以树状结构清晰地显示出来。本节将简单介绍树控件的用法。

10.3.1　动态创建树控件

创建树控件可以使用Create方法，该方法用于动态创建树视图窗口。

语法格式如下：

Create方法中的参数说明如表10.8所示。

表10.8　Create方法中的参数说明

树控件通常都是和图像列表控件一起使用的，在树控件中可以显示图像列表中的图标。使用树控件关联图像列表可以使用SetImageList方法。

语法格式如下：

参数说明：

pImageList：标识图像列表控件指针。

nImageListType：标识图像列表类型。

向树控件中插入数据可以使用InsertItem方法。

语法格式如下：

InsertItem方法中的参数说明如表10.9所示。

表10.9　InsertItem方法中的参数说明

使用Expand方法可以展开或收缩树控件的节点。

语法格式如下：

参数说明：

hItem：展开的节点句柄。

nCode：展开的动作。可选值如下。

TVE_COLLAPSE：收缩所有节点。

TVE_COLLAPSERESET：收缩节点，移除子节点。

TVE_EXPAND：展开所有节点。

TVE_TOGGLE：展开或收缩当前节点。

如果要删除节点，可以使用DeleteItem方法。

语法格式如下：

其中，hItem是指删除节点的句柄。

也可以使用DeleteAllItems方法删除所有节点。

语法格式如下：

【例10.7】动态创建树控件。（实例位置：资源包\TM\sl\10\6）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“动态创建树控件”。

（2）在对话框头文件中声明一个CTreeCtrl类变量m_Tree和一个图像列表对象m_ImageList，向工程中导入3个图标资源。

（3）在对话框的OnInitDialog函数中创建树控件，创建图像列表，向图像列表中添加图标，关联树控件和图像列表控件，并向树控件中添加数据，代码如下。

说明

在使用InsertItem方法插入树节点时，可以将节点的图像索引和选中时的图像索引设置为同一个索引，在节点选中时，节点的图像将不进行变化。

（4）在对话框头文件中声明树控件的双击事件和鼠标右击事件的处理函数，代码如下。

（5）在对话框源文件中添加消息映射宏，代码如下。

（6）添加双击事件的处理函数，在该函数中判断是否删除当前选中的节点，代码如下。

（7）添加鼠标右键单击事件的处理函数，代码如下。

实例的运行结果如图10.6所示。

图10.6　动态创建树控件

10.3.2　带复选功能的树控件

设计带复选功能的树控件，首先要为控件选择Check Boxes属性，该属性被选中后会在树控件的节点前显示复选框，然后使用GetCheck方法获得复选框的状态。

语法格式如下：

其中，hItem表示节点句柄。

在GetCheck方法中需要使用节点句柄，而树控件的节点句柄可以通过以下方法获得。

（1）GetRootItem方法可以获得根节点。

语法格式如下：

（2）GetChildItem方法可以获得指定节点的子节点。

语法格式如下：

其中，hItem指节点句柄。

返回值：子节点句柄。

（3）GetNextSiblingItem方法可以获取下一个兄弟节点。

语法格式如下：

其中，hItem用于标识节点句柄。

返回值：下一个兄弟节点句柄。

（4）GetPrevSiblingItem方法可以获取上一个兄弟节点。

语法格式如下：

其中，hItem用于标识节点句柄。

返回值：上一个兄弟节点句柄。

（5）GetParentItem方法可以获得所标识节点的父节点。

语法格式如下：

其中，hItem用于标识节点句柄。

返回值：父节点句柄。

（6）GetSelectedItem方法用于获得当前被选中的树节点。

语法格式如下：

（7）GetNextItem方法可以根据当前节点获取下一个节点。

语法格式如下：

参数说明：

hItem：是当前节点句柄。

nCode：标识如何查找下一个节点。可选值如下。

TVGN_CARET：返回当前选中的节点。

TVGN_CHILD：返回第一个子节点。

TVGN_DROPHILITE：返回拖动的节点。

TVGN_FIRSTVISIBLE：返回第一个可见的节点。

TVGN_NEXT：返回下一个兄弟节点。

TVGN_NEXTVISIBLE：返回下一个可见节点。

TVGN_PARENT：返回所标识节点的父节点。

TVGN_PREVIOUS：返回上一个兄弟节点。

TVGN_PREVIOUSVISIBLE：返回上一个可见节点。

TVGN_ROOT：返回根节点。

注意

使用GetNextItem方法，通过不同的参数设置，可以实现之前几个函数的功能。

【例10.8】创建一个带复选功能的树控件。（实例位置：资源包\TM\sl\10\7）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“带复选功能的树控件”。

（2）向对话框中添加一个树控件和一个按钮控件，并为树控件关联变量m_Tree，选择树控件的Check Boxes属性，向工程中导入3个图标资源。

（3）在对话框头文件中声明一个图像列表对象m_ImageList和一个CString类型变量m_StrText。

（4）在对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，关联树控件和图像列表，并向树控件中添加数据，代码如下。

（5）添加一个CheckToTree函数，用来判断节点前的复选框是否被选中，代码如下。

（6）处理“确定”按钮的单击事件，在该事件中将调用CheckToTree函数获得选中复选框的节点文本，并通过消息框显示出来，代码如下。

实例的运行结果如图10.7所示。

图10.7　带复选功能的树控件

10.3.3　可编辑节点的树控件

要实现使树控件节点可编辑，需要选择树控件的Edit labels属性，此时将允许用户编辑节点标题。但是只选择Edit labels属性是不够的，因为控件虽然可以被编辑，但是却无法保存修改后的文本，所以还要通过树控件的TVN_ENDLABELEDIT事件来实现保存修改文本的功能。在该事件中，使用SetItemText设置当前修改的节点文本。

语法格式如下：

参数说明：

hItem：标识节点句柄。

lpszItem：标识节点文本。

【例10.9】设计可编辑节点的树控件。（实例位置：资源包\TM\sl\10\8）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“可编辑节点的树控件”。

（2）向对话框中添加一个树控件，并为树控件关联变量m_Tree，选择树控件的Edit labels属性，向工程中导入3个图标资源。

（3）在对话框头文件中声明一个图像列表对象m_ImageList。

（4）在对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，关联树控件和图像列表控件，并向树控件中添加数据，代码如下。

说明

SetImageList方法的LVSIL_NORMAL参数值表示获取常规的图像列表，TVSIL_STATE参数值表示获取状态图像列表。

（5）处理树控件的TVN_ENDLABELEDIT事件，在该事件中设置节点的显示文本，代码如下。

实例的运行结果如图10.8所示。

图10.8　可编辑节点的树控件

10.4　标签控件

标签控件提供了一组标签按钮以及对应标签按钮的显示页面，用户可以单击标签按钮选择不同的显示页面，但是用户不能直接在各个标签页上插入控件，只能在选中不同标签页时显示不同的对话框或控件。

10.4.1　设置显示方式

在使用标签控件时，除了使用默认的层叠显示的方式外，也可以设置其他显示方式，如图10.9所示。可以将标签设置为按钮的形式—打开标签控件的属性窗口，在Styles选项卡中选择Buttons属性，该属性使标签以按钮形状显示，还可以将标签控件的标签设置到控件的底部：打开标签控件的属性窗口，在More Styles选项卡中选择Bottom属性，该属性使标签在控件的底部显示。

图10.9　设置标签控件的显示方式

10.4.2　图标标签控件

带图标的标签控件是通过图像列表和标签控件来实现的。首先，创建一个图像列表，然后调用SetImageList方法使标签控件关联图像列表。

语法格式如下：

其中，pImageList用于标识图像列表指针。

返回值：之前控件关联的图像列表控件。

调用InsertItem方法向标签控件中添加标签。

语法格式如下：

InsertItem方法中的参数说明如表10.10所示。

表10.10　InsertItem方法中的参数说明

【例10.10】设置带图标的标签控件。（实例位置：资源包\TM\sl\10\9）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“设置带图标的标签控件”。

（2）向对话框中添加一个标签控件，并为标签控件关联变量m_Tab。

（3）在对话框头文件中声明一个图像列表对象m_ImageList。

（4）在对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，关联标签控件和图像列表控件，并向图像列表中插入标签，代码如下。

注意

在黑体字的InsertItem方法中，左侧的0、1、2表示的是标签项索引，而右侧的0、1、2表示的是图像索引，两侧的索引没有关联，可以根据需要进行不同的设置，如“m_Tab.InsertItem（0，"员工信息",2）；”语句，可以将第1个标签项的图像索引设置为图像列表中的第3个图像。

实例的运行结果如图10.10所示。

图10.10　设置带图标的标签控件

10.4.3　设计程序模块

在使用标签控件设计程序模块时，最主要的功能就是在选择不同标签时有不同的显示信息。要实现这一功能，需要使用标签控件的TCN_SELCHANGE事件，该事件在选中标签改变后触发。可以在该事件的处理函数中使用GetCurSel方法获得当前被选中的标签索引。

语法格式如下：

返回值：返回当前被选中的标签项索引。

也可以调用SetCurSel方法将某个标签设置为当前选中的标签。

语法格式如下：

其中，nItem用于标识标签索引。

返回值：之前选中的标签索引。

【例10.11】使用标签控件设计程序模块。（实例位置：资源包\TM\sl\10\10）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用标签控件设计程序模块”。

（2）添加3个对话框资源，资源ID分别为IDD_DIALOG_EMP、IDD_DIALOG_CLI和IDD_DIALOG_PRO，并设置对话框资源的Style属性为Child，Border属性为None。为3个对话框资源关联类，分别为CEmployee、CClient和Cprovidedlg，并分别向对话框资源中添加控件。

（3）向主对话框中添加一个标签控件，并为标签控件关联变量m_Tab。

（4）在主对话框头文件中声明一个图像列表对象和3个对话框类对象，代码如下。

（5）在主对话框的OnInitDialog函数中创建图像列表，向图像列表中添加图标，关联标签控件和图像列表控件，并创建对话框，代码如下。

（6）处理对话框的TCN_SELCHANGE事件，在该事件中获得当前选中标签项的索引，根据索引判断显示的对话框，代码如下。

说明

除了显示不同的对话框资源这种方法外，也可以将所有控件都添加到当前的对话框中，然后在TCN_SELCHANGE事件中根据需要隐藏或者显示不同的控件。

（7）处理对话框的WM_CLOSE事件，在主对话框关闭时销毁3个非模态对话框，并释放指针，代码如下。

实例的运行结果如图10.11所示。

图10.11　使用标签控件设计程序模块

10.5　应用工具提示控件

使用工具提示控件可以为控件添加提示。工具提示控件是一个弹出窗口，可以通过一行文本描述应用程序中的一个控件功能。工具提示窗口是隐藏的，只有当鼠标指针停留在一个控件工具上时，如果为该控件设置了提示，则显示提示窗口；当鼠标指针从控件工具上移开时，工具提示窗口自动隐藏。

在使用工具提示控件时，首先要使用Create方法创建工具提示控件窗口。

语法格式如下：

参数说明：

pParentWnd：设置工具提示控件的父窗口。

dwStyle：设置工具提示控件的风格。工具提示控件有以下两种特定类风格。

TTS_ALWAYSTIP：当鼠标指针停留在工具上时，不管工具提示窗口所属的主窗口是否处于活动状态，都显示工具提示窗口。

TTS_NOPREFIX：禁止系统将“&”字符从字符串中去掉。

说明

工具提示控件默认具有的风格有WS_POPUP和WS_EX_TOOLWINDOW两种。

调用SetDelayTime方法为工具提示控件设置延迟时间。

语法格式如下：

参数说明：

nDelay：以毫秒表示延迟时间。

dwDuration：要获取某一段持续时间值的标志。

iTime：以毫秒表示指定延迟时间。

调用SetMaxTipWidth方法设置工具提示窗口的最大宽度。

语法格式如下：

其中，iWidth是指工具提示窗口的宽度。

然后调用AddTool方法向工具提示控件注册一个工具，当鼠标指针停留在该工具上时，工具提示控件中的信息就会显示出来。

语法格式如下：

AddTool方法中的参数说明如表10.11所示。

表10.11　AddTool方法中的参数说明

接着调用RelayEvent方法，该方法可以将鼠标消息传递给工具提示控件。

语法格式如下：

其中，lpMsg表示包含要传递消息的MSG结构指针。

说明

工具提示控件处理的消息包括WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_ LBUTTONUP、WM_RBUTTONDOWN、WM_MBUTTONDOWN、WM_RBUTTONUP和WM_ MBUTTONUP。

在注册工具后，也可以调用UpdateTipText方法为工具设置提示文本。

语法格式如下：

UpdateTipText方法中的参数说明如表10.12所示。

表10.12　UpdateTipText方法中的参数说明

【例10.12】使用工具提示控件为控件添加提示功能。（实例位置：资源包\TM\sl\10\11）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“应用工具提示控件”。

（2）向对话框中添加一个编辑框控件和一个按钮控件。

（3）在对话框头文件中声明一个工具提示控件对象m_ToolTip。

（4）在对话框的OnInitDialog函数中创建工具栏对象，并注册工具信息，代码如下。

（5）为对话框添加PreTranslateMessage虚方法，在该虚方法中设置鼠标消息的传递，并设置编辑框的提示信息，代码如下。

实例的运行结果如图10.12所示。

图10.12　应用工具提示控件

10.6　小结

本章讲解了Visual C++ 6.0集成开发环境中的控件，其中重点介绍了图像列表控件、列表视图控件和树控件等高级控件，讲解过程中为了方便读者理解结合了大量的具体实例。读者一定要认真学习，熟练掌握这些高级控件的使用，才能为日后独立开发项目打下良好的基础。

10.7　实践与练习

1. 设计一个树控件来显示磁盘目录。双击树控件的节点，可以查看该节点下的子目录，效果如图10.13所示。（答案位置：资源包\TM\sl\10\12）

2. 设计一个树控件，实现树形提示框。在文本框的字符上右击，提示框窗体就会显示，效果如图10.14所示。（答案位置：资源包\TM\sl\10\13）

图10.13　利用树控件来显示磁盘目录

图10.14　利用树控件来实现树形提示框

第11章

自定义MFC控件

（视频讲解：41分钟）

Visual C++开发环境中自带的控件非常少，在界面美化程度越来越高的今天，这些控件根本无法满足用户的需求。为了使用户不再为这个问题烦恼，本章将简要介绍控件的绘制方法，使用户可以根据需要自己绘制美观的控件，为日后开发应用程序做好充足的准备。本章将详细介绍编辑框和按钮控件的绘制，为了便于读者理解，在讲解过程中列举了大量的实例。

通过阅读本章，您可以：

掌握如何自定义数字编辑框

掌握改变编辑框文本颜色的方法

掌握为编辑框绘制背景的方法

掌握图标按钮的设计方法

掌握热点按钮的设计方法

掌握圆形按钮的设计方法

掌握为树控件绘制背景的方法

掌握复选框控件的绘制方法

11.1　自定义编辑框控件

前面章节已经介绍了编辑框的基本应用，但是普通的编辑框有时很难满足用户的要求，为此本节介绍几个自定义编辑框控件的创建方法。

11.1.1　数字编辑框

在编辑框控件的属性中有一个Number属性，选中该属性可以使编辑框成为数字编辑框。该属性有很大的局限性，就是该属性被选择后，编辑框中只能输入数字，“. ”和“-”等常用的符号不能输入。因此在实际开发中，程序员很少使用这一属性。可是数字编辑框有时又是非常必要的，通过限制输入数据，可以更好地避免用户操作失误。本节就以CEdit类为基类派生一个CNumberEdit类，实现数字编辑框功能。

【例11.1】以CEdit类为基类派生一个CNumberEdit类，创建数字编辑框。（实例位置：资源包\ TM\sl\11\1）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“数字编辑框”。

（2）向对话框中添加一个编辑框控件。

（3）选择Insert/New Class命令，打开New Class（新建类）对话框，在Name编辑框中输入类名CNumberEdit，在Base class组合框中选择基类CEdit，如图11.1所示。

图11.1　New Class对话框

（4）单击OK按钮创建CNumberEdit类。

（5）为CNumberEdit类处理WM_CHAR消息，在该消息的处理函数中修改控件对用户输入数据的响应，代码如下。

说明

退格键的ASCII码是8，减号的ASCII码是45，小数点的ASCII码是46。

（6）按Ctrl+W快捷键打开类向导，为编辑框控件关联一个CNumberEdit类的变量。

（7）在对话框的头文件中引用NumberEdit.h头文件。

实例的运行结果如图11.2所示。

图11.2　数字编辑框

11.1.2　特殊文本颜色编辑框

编辑框的文本颜色通常使用黑色，可是白色的背景衬托黑色的字体，时间长了会产生视觉疲劳。为了解决这个问题，可以设计一个具有特殊文本颜色的编辑框，在解决视觉疲劳的同时也可以美化程序界面。

在设置文本颜色时需要使用CreateStockObject函数，该函数获取预定义的Windows GDI的画笔、画刷和字体句柄，并将GDI对象与CGdiObject类对象相关联。

语法格式如下：

其中，nIndex表示定义标准对象类型的常量。可选值如下。

BLACK_BRUSH：黑色刷子。

DKGRAY_BRUSH：黑灰色刷子。

GRAY_BRUSH：灰色刷子。

HOLLOW_BRUSH：凹刷子。

LTGRAY_BRUSH：浅灰色刷子。

NULL_BRUSH：空刷子。

WHITE_BRUSH：白色刷子。

BLACK_PEN：黑色画笔。

WHITE_PEN：白色画笔。

ANSI_FIXED_FONT：采用Windows（ANSI）字符集的等宽字体。

ANSI_VAR_FONT：采用Windows（ANSI）字符集的不等宽字体。

DEVICE_DEFAULT_FONT：设备使用的默认字体（NT）。

DEFAULT_GUI_FONT：用户界面的默认字体，包括菜单和对话框字体。

OEM_FIXED_FONT：OEM字符集的固有字体。

SYSTEM_FONT：屏幕系统字体。这是用于菜单、对话框等的默认不等宽字体。

SYSTEM_FIXED_FONT：屏幕系统字体。这是用于菜单、对话框等的默认等宽字体。

DEFAULT_PALETTE：默认调色板。

【例11.2】以CEdit类为基类派生一个CColorEdit类，创建特殊文本颜色编辑框。（实例位置：资源包\TM\sl\11\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“特殊文本颜色编辑框”。

（2）创建一个以CEdit类为基类的派生类CColorEdit。

（3）在CColorEdit类的头文件中声明一个COLORREF类型变量m_Color。

（4）手动添加一个SetColor函数，用来为设置文本颜色的变量赋值，代码如下。

（5）处理CColorEdit类的WM_CTLCOLOR消息，在该消息的处理函数中设置文本颜色，代码如下。

（6）向对话框中添加6个编辑框控件，通过类向导为控件关联CColorEdit类变量。

（7）在对话框的OnInitDialog函数中为编辑框控件设置文本显示颜色，代码如下。

实例的运行结果如图11.3所示。

图11.3　特殊文本颜色编辑框

技巧

为了在同一窗体的不同编辑框中显示不同的文本颜色，需要派生一个类。如果只是将同一窗体中的编辑框设置成一个颜色，可以在当前窗体的WM_CTLCOLOR消息中判断是否为编辑框（nCtlColor==CTLCOLOR_EDIT），然后通过设备上下文指针pDC调用SetTextColor方法，并设置文本颜色。

11.1.3　位图背景编辑框

白色背景的编辑框看的时间长了，会很乏味。为了更好地美化程序，从而吸引用户，可以以CEdit类为基类设计一个CBmpEdit类，通过该类使编辑框显示背景位图。值得注意的是，在设计CBmpEdit类时，绘制背景的部分是在WM_ERASEBKGND消息处理函数中进行的，而不是在WM_PAINT消息处理函数中进行的。这是因为在WM_PAINT消息处理函数中绘制背景位图，会导致编辑框中的文本被位图覆盖。下面就通过实例来看一下具有位图背景的编辑框是如何实现的。

【例11.3】以CEdit类为基类派生一个CBmpEdit类，创建具有位图背景的编辑框。（实例位置：资源包\TM\sl\11\3）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“位图背景编辑框”。

（2）创建一个以CEdit类为基类的派生类CBmpEdit。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入一个位图资源。

（4）在CBmpEdit类的头文件中声明一个CBitmap类对象m_Bitmap。

（5）在CBmpEdit类的构造函数中加载位图资源，代码如下。

（6）处理CBmpEdit类的WM_CTLCOLOR消息，在该消息的处理函数中设置文本的背景透明，代码如下。

（7）处理CBmpEdit类的WM_ERASEBKGND消息，在该消息的处理函数中绘制编辑框背景，代码如下。

（8）处理CBmpEdit类的EN_CHANGE消息，在该消息的处理函数中重绘背景，代码如下。

注意

因为编辑框中的文本是可变的，所以在编辑框内容发生改变（触发EN_CHANGE消息）时，要调用Invalidate函数重绘窗口。

实例的运行结果如图11.4所示。

图11.4　位图背景编辑框

11.2　自定义按钮控件

随着用户对界面美化程度的要求逐渐提高，传统的矩形按钮已经不能满足用户的需要，可以通过自定义的方法绘制漂亮的按钮。

11.2.1　图标按钮

在按钮控件的属性窗口中选择Icon属性，可以使按钮显示图标。不过这种方法并不适用，因为按钮显示图标以后不能显示文本信息。为了解决这个问题，笔者以CButton类为基类派生一个CIconBtn类，通过该类绘制按钮控件，可以实现在按钮控件中同时显示图标和文本的功能。

在绘制按钮时需要改写DrawItem方法，该方法用于绘制控件的外观。当按钮控件包含BS_OWNERDRAW风格时，应用程序将自动调用DrawItem方法绘制按钮。

语法格式如下：

其中，lpDrawItemStruct是一个DRAWITEMSTRUCT结构指针，其结构成员如下。

CtlType：控件的类型。可选值如下。

ODT_BUTTON：按钮。

ODT_COMBOBOX：组合框。

ODT_LISTBOX：列表框。

ODT_MENU：菜单。

ODT_LISTVIEW：列表视图。

ODT_STATIC：静态控件。

ODT_TAB：标签控件。

CtlID：控件ID。

ItemID：菜单项ID或列表框、组合框中的项目索引。

ItemAction：绘画的动作。可选值如下。

ODA_DRAWENTIRE：整个控件需要被绘制时设置该标识。

ODA_FOCUS：控件获得或失去焦点时设置该标识。

ODA_SELECT：表示控件处于选中状态时设置该标识。

ItemState：需要绘画的状态。可选值如下。

ODS_CHECKED：菜单项被选中。

ODS_DISABLED：控件不可用。

ODS_FOCUS：控件获得焦点。

ODS_GRAYED：控件处于灰色状态，只用于菜单控件。

ODS_SELECTED：控件被选中。

ODS_COMBOBOXEDIT：组合框中编辑控件的文本被选中。

ODS_DEFAULT：默认状态。

HwndItem：控件的句柄。

HDC：控件的画布句柄。

RcItem：控件的矩形区域。

ItemData：控件的附加信息。

【例11.4】以CButton类为基类派生一个CIconBtn类，创建图标按钮。（实例位置：资源包\TM\sl\11\4）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“图标按钮”。

（2）创建一个以CButton类为基类的派生类CIconBtn。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入两个图标资源。

（4）在CIconBtn类的头文件中声明变量，代码如下。

（5）添加一个SetImageList函数，用于设置图像列表，代码如下。

（6）添加一个SetImageIndex函数，用于设置显示的图标在图像列表中的索引值，代码如下。

（7）添加DrawItem虚方法，用于绘制图标按钮，代码如下。

注意

在绘制按钮的外观时，按钮突出显示的3D效果是通过DrawEdge方法绘制的。

（8）处理WM_LBUTTONDOWN消息，在按钮被按下时将IsPressed的值设为TRUE。

（9）处理WM_LBUTTONUP消息，在按钮抬起时将IsPressed的值设为FALSE。

（10）向对话框中添加1个群组框控件、4个静态文本控件、4个编辑框控件和2个按钮控件，为按钮控件关联CIconBtn类变量，打开按钮控件的属性窗口，选中Owner draw属性。

注意

通过DrawItem虚方法绘制按钮的外观时，一定要选择按钮控件的Owner draw属性。

（11）在对话框头文件中声明图像列表对象，并引用IconBtn.h头文件。

（12）在对话框的OnInitDialog函数中创建图像列表，并设置按钮的显示图像，代码如下。

实例的运行结果如图11.5所示。

图11.5 　图标按钮

11.2.2　热点按钮

热点按钮不但使界面更美观，而且可以响应鼠标指针滑过时的消息，从而更新控件显示，使程序更加人性化，所以一直都深受用户的欢迎。在此以CButton类为基类派生一个CHotButton类，通过该类绘制按钮控件，可以实现在按钮控件热点时显示不同图片的功能。

在CHotButton类中，使用SetTimer函数设置定时器。

语法格式如下：

参数说明：

nIDEvent：定时器的标识。

nElapse：延迟的时间，单位为毫秒。

lpfnTimer：重复调用函数的地址指针，为NULL时发送WM_TIMER消息。

在定时器中，GetCursorPos函数用于获得鼠标指针的位置。

语法格式如下：

其中，lpPoint表示POINT结构指针，该结构接收鼠标指针的屏幕坐标。

【例11.5】以CButton类为基类派生一个CHotButton类，创建具有热点效果的按钮。（实例位置：资源包\TM\sl\11\5）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“热点按钮”。

（2）创建一个以CButton类为基类的派生类CHotButton。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入4个位图资源。

（4）在CHotButton类的头文件中声明变量，代码如下。

（5）添加SetCBitmap函数，在该函数中设置使用的CBitmap对象，代码如下。

（6）添加PreSubclassWindow虚方法，在该方法中设置定时器，代码如下。

（7）处理WM_TIMER消息，在该消息的处理函数中获得鼠标指针的位置，并判断鼠标指针是否在按钮控件区域中，代码如下。

注意

按钮的热点是在定时器中进行判断的，每间隔10毫秒判断一次鼠标的位置以及鼠标指针和按钮的位置关系，从而判断是否标记为热点效果。

（8）调用DrawItem方法在按钮中绘制图片，代码如下。

（9）处理WM_LBUTTONDOWN消息，在按钮被按下时将IsPressed的值设为TRUE。

（10）处理WM_LBUTTONUP消息，在按钮被按下时将IsPressed的值设为FALSE。

（11）向对话框中添加1个群组框控件、3个静态文本控件、3个编辑框控件和2个按钮控件，为按钮控件关联CHotButton类变量，打开按钮控件的属性对话框，选择Owner draw属性。

（12）在对话框头文件中声明CBitmap类对象，并引用HotButton.h头文件。

（13）在对话框的OnInitDialog函数中加载位图资源，并设置按钮的显示图片，代码如下。

技巧

使用上述方法设计热点按钮时需要许多位图资源，因为一个按钮对应看两个位图资源。其实可以将位图资源设置为不带文字的形式，绘制背景时获得当前按钮的文本，一同绘制出来。这样只要有普通情况和热点效果两个位图资源即可。

实例的运行结果如图11.6所示。

图11.6　热点按钮

11.2.3　圆形按钮

有时根据程序界面的需要会修改按钮的形状，从而使程序界面看起来更自然、协调。在此以CButton类为基类派生一个CCustomButton类，通过该类绘制按钮控件，可以将按钮控件修改为圆形。

在CCustomButton类中调用CreateEllipticRgn方法，该方法用一个椭圆形区域来初始化一个CRgn对象。

语法格式如下：

CreateEllipticRgn方法中的参数说明如表11.1所示。

表11.1　CreateEllipticRgn方法中的参数说明

调用SetWindowRgn函数设置窗口区域。

语法格式如下：

参数说明：

hRgn：HRGN对象句柄。

bRedraw：是否重绘。

【例11.6】以CButton类为基类派生一个CCustomButton类，创建圆形按钮。（实例位置：资源包\ TM\sl\11\6）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“圆形按钮”。

（2）创建一个以CButton类为基类的派生类CCustomButton。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入一个位图资源。

（4）在CCustomButton类的头文件中声明一个BOOL型变量m_IsPressed。

（5）添加DrawItem虚方法，在该方法中绘制按钮，代码如下。

（6）处理WM_LBUTTONDOWN消息，在按钮被按下时将m_IsPressed的值设为TRUE。

（7）处理WM_LBUTTONUP消息，在按钮抬起时将m_IsPressed的值设为FALSE。

（8）处理WM_ERASEBKGND消息，禁止程序重绘按钮控件的背景。

注意

一定要在主窗口的头文件中引用CustomButton.h头文件，因为通过类向导为按钮控件关联的变量是CCustomButton类的。

实例的运行结果如图11.7所示。

图11.7　圆形按钮

11.3　位图背景树控件

为树控件添加背景位图，可以美化程序界面，从而更吸引用户，那么如何才能为树控件绘制背景位图呢？这就需要以CTreeCtrl类为基类，派生一个新类CCustomTree。在绘制树控件的背景位图时，首先要获得树控件的原始图像，再将原始图像绘制在一个内存画布对象上，然后需要重新定义一个画布对象，并将背景位图绘制在该画布对象上，最后将两个画布对象进行与运算绘制在树控件的背景上。

【例11.7】以CTreeCtrl类为基类派生一个CCustomTree类，创建具有位图背景的树控件。（实例位置：资源包\TM\sl\11\7）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“位图背景树控件”。

（2）创建一个以CTreeCtrl类为基类的派生类CCustomTree。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入一个位图资源。

（4）创建一个CMemDC类，该类用来创建一个内存画布，并将背景位图绘制到内存画布上，代码如下。

（5）在CCustomTree类的头文件中声明一个CBitmap对象m_Bitmap。

（6）在CCustomTree类的构造函数中加载位图资源，代码如下。

（7）添加OnPaint函数，在该函数中绘制树控件的背景，代码如下。

（8）处理CCustomTree类的TVN_ITEMEXPANDING消息，在该消息的处理函数中使用SetRedraw函数设置背景不重绘。

（9）处理CCustomTree类的TVN_ITEMEXPANDED消息，在该消息的处理函数中使用SetRedraw函数重绘背景。

说明

SetRedraw函数的参数为TRUE时重绘窗口，参数值为FALSE时不重绘窗口。

（10）重载WM_ERASEBKGND消息的处理函数，代码如下。

实例的运行结果如图11.8所示。

图11.8　位图背景树控件

11.4　三态效果复选框控件

复选框控件可以有3种状态，即未选中状态、选中状态和必选状态。为了使程序界面中的复选框控件看起来更醒目，可以分别使用3个位图代替3种状态。

首先需要调用ModifyStyle函数设置复选框控件的BS_OWNERDRAW，使复选框允许自绘。

语法格式如下：

参数说明：

dwRemove：指定在修改风格时要清除的窗口风格。

dwAdd：指定在修改风格时要加入的窗口风格。

nFlags：要传递给SetWindowPos的标志。如果不调用SetWindowPos，则为0。

【例11.8】以CButton类为基类派生一个CCustomCheck类，创建具有三态效果的复选框控件。（实例位置：资源包\TM\sl\11\8）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“三态效果复选框控件”。

（2）创建一个以CButton类为基类的派生类CCustomCheck。

（3）在工作区窗口中选择RecourceView选项卡，向对话框中导入一个位图资源。

（4）在CCustomCheck类的头文件中声明变量，代码如下。

（5）在CCustomCheck类的构造函数中创建图像列表，代码如下。

（6）重载CCustomCheck类的DrawItem虚方法，在该方法中绘制复选框，代码如下。

（7）处理CCustomCheck类的WM_LBUTTONDOWN事件，当鼠标按下时修改复选框的选中状态，代码如下。

实例的运行结果如图11.9所示。

图11.9　三态效果复选框控件

说明

本实例中，复选框的3种状态分别是选中状态、未选中状态和必选状态。

11.5　小结

通过本章的学习，可以使读者更进一步地了解Visual C++ 6.0集成开发环境中的控件，从而在这些控件的基础上定义自己需要的控件。本章重点介绍了编辑框控件和按钮控件的绘制，这两个控件是在开发过程中最常用的控件，读者可以通过实例掌握控件的自定义方法，为日后开发应用程序打下良好的基础。

11.6　实践与练习

1. 设计一个静态文本控件，显示电子时钟形式的数字，效果如图11.10所示。（答案位置：资源包\ TM\sl\11\9）

2. 设计一个静态文本控件，使控件具有分隔条，效果如图11.11所示。（答案位置：资源包\TM\sl\ 11\10）

图11.10　数字格式的静态文本控件

图11.11　具有分隔条的静态文本控件

第12章

文本、图形、图像处理

（视频讲解：1小时3分钟）

众所周知，Windows操作系统属于图形界面操作系统，也被称为视窗操作系统。它以其美观的图形界面简化了用户的各种操作，使其在PC机（Personal Computer，个人计算机）上广泛应用。为了在Windows操作系统上开发出漂亮的图形界面应用程序，操作系统提供了一组GDI（Graphics Device Interface）函数，使用它们可以在窗口中输出文本、图形、图像信息。为了降低开发难度，Visual C++提供的MFC类库对GDI函数进行了封装。本章将介绍有关Visual C++中对文本、图形、图像进行处理的相关技术。

通过阅读本章，您可以：

了解什么是设备上下文

了解什么是GDI对象

掌握如何在窗口中输出文本

掌握如何在窗口中输出图形图像

掌握如何绘制JPEG图像

学会应用GDI+进行绘图操作

12.1　GDI概述

GDI全称为Graphics Device Interface，即图形设备接口，是Windows操作系统提供的一组函数。为了能够在窗口中绘制各种图形信息，操作系统为每个窗口关联一个设备上下文（Device Context，DC），所有的图形图像操作均在设备上下文中进行。为了能够输出文本、图形、图像等信息，操作系统提出了字体、位图、画刷等GDI对象的概念。假设窗口的客户区域是一张纸，那么GDI就是工具箱，GDI中封装的各种对象就是工具箱中包含的画笔、颜料等工具，用户可以通过这些工具来写字、画图形和画图像。本节将介绍有关设备上下文及GDI对象的相关知识。

12.1.1　设备上下文

设备上下文是操作系统提供的一个画布，它关联于某一个窗口。当用户需要在某个窗口中绘图时，其绘图操作在设备上下文中完成。在Visual C++中，窗口类CWnd提供了GetDC方法用于获取窗口的设备上下文，该方法返回的是一个CDC类型的指针（CDC是MFC类库提供的用于封装Windows设备上下文的类，该类提供了各种绘图的方法）。下面举一个简单的实例来演示使用CDC类进行窗口绘图，有关CDC类的常用方法在后文中将逐一进行介绍。

【例12.1】输出简单文本。（实例位置：资源包\TM\sl\12\1）

具体操作步骤如下。

（1）创建一个单文档／视图应用程序，工程名为OutputText。

（2）在视图类的OnDraw方法中输出文本，代码如下。

（3）运行程序，结果如图12.1所示。

图12.1　输出文本

说明

“10,10”是绘制文本的左上角坐标，“同一个世界，同一个梦想”是要绘制的文本。

12.1.2　GDI对象

为了能够绘制各种类型的文本、图形、图像信息，Windows操作系统提供了6个GDI对象，分别为字体、位图、画刷、调色板、画笔和区域。在MFC类库中，分别对应于CFont、CBitmap、CBrush、CPalette、CPen和CRgn类。下面以CFont类为例介绍如何使用GDI对象，CFont类用于描述文本的字体信息。下面的实例将指定字体格式的文本输出到窗口中。

【例12.2】输出指定字体格式的文本。（实例位置：资源包\TM\sl\12\2）

具体操作步骤如下。

（1）创建一个单文档／视图结构应用程序，工程名为SpecificFont。

（2）在视图类的OnDraw方法中设置设备上下文的字体，并输出文本，代码如下。

（3）运行程序，结果如图12.2所示。

在使用GDI对象时，首先需要调用设备上下文的SelectObject方法选中一个GDI对象，该方法在选中当前的GDI对象时，会将之前选中的同类的GDI对象返回（应用程序通常会定义一个临时GDI对象指针获取返回的GDI对象），然后调用设备上下文进行绘图操作，最后再次调用设备上下文的SelectObject方法选中之前的GDI对象。

图12.2　输出特殊字体格式的文本

注意

由于在设备上下文中重载了所有的GDI对象，因此在使用时不必考虑GDI对象的类型。

12.2　文本输出

在开发应用程序时，用户经常需要在窗口中输出文本信息，一些应用软件还能够在图像背景上输出透明的文本。这是如何做到的呢？本节将介绍各种文本格式的输出。

12.2.1　在具体位置和区域中输出文本

在输出文本时，需要标识文本的输出位置。文本位置的标识可以有多种方法，其中最常用的方法是根据坐标或区域来输出文本。在设备上下文CDC类中提供了TextOut方法，用于在指定的坐标处输出文本。

语法格式如下：

参数说明：

x：文本位置的x轴坐标。

y：文本位置的y轴坐标。

str：输出的文本信息。

在第12.1.1节的实例中，笔者就使用TextOut函数在坐标（10,10）处输出了“同一个世界，同一个梦想”信息。

CDC类还提供了一个DrawText方法，用于在某一个区域内输出文本。

语法格式如下：

参数说明：

str：被输出的字符串。

lpRect：一个区域对象，字符串将绘制在该区域中。

nFormat：文本被格式化的方式，即文本在区域中的显示方式。其参数值可以是如表12.1所示值的任意组合。

表12.1　nFormat参数值描述

下面举例介绍DrawText方法的使用，实例将在矩形区域的居中位置输出信息。

【例12.3】在矩形区域的居中位置输出信息。（实例位置：资源包\TM\sl\12\3）

具体操作步骤如下。

（1）创建一个单文档／视图应用程序，工程名称为DrawText。

（2）在视图类的OnDraw方法中添加如下代码。

注意

通过DT_CENTER和DT_VCENTER这两个标记即可将绘制的文本指定在矩形区域的正中间，但是在使用DT_VCENTER时，一定要使用DT_SINGLELINE标记。

（3）运行程序，结果如图12.3所示。

图12.3　DrawText方法的应用

12.2.2　利用制表位控制文本输出

除了根据坐标或矩形区域输出文本外，CDC类还提供了TabbedTextOut方法基于制表位控制文本输出。

语法格式如下：

参数说明：

x：输出文本起点的x轴坐标。

y：输出文本起点的y轴坐标。

str：输出的文本字符串。

nTabPositions：标识lpnTabStopPositions的元素数量。

lpnTabStopPositions：制表位数组。

nTabOrigin：x轴从制表位开始扩展的位置，可以与x参数相同，也可以不同。

如果nTabPositions参数为0，并且lpnTabStopPositions参数为NULL，则制表符被扩展为平均字符宽度的8倍。

下面举例介绍TabbedTextOut方法的使用，实例将字符串中的制表符按照指定的宽度转换为空格输出。

【例12.4】将字符串中的制表符按照指定的宽度转换为空格输出。（实例位置：资源包\TM\sl\12\4）

具体操作步骤如下。

（1）创建一个单文档／视图结构应用程序，工程名称为Tabout。

（2）在视图类的OnDraw方法中编写如下程序代码。

说明

利用制表位输出文本可以轻松地设置文本对齐。

（3）运行程序，结果如图12.4所示。

图12.4　制表位控制文本输出

12.2.3　设置字体及文本颜色

在向窗口输出文本时，通常需要设置文本的字体及文本的显示颜色。设置文本颜色，只要使用CDC类的SetTextColor方法即可。该方法需要使用一个RGB值表示文本的颜色，用户可以使用Visual C++提供的RGB宏来根据红、绿、蓝颜色确定一个RGB颜色值，如“RGB（255, 0, 0）”将获得一个红色的RGB颜色值。设置文本字体属性的操作略微复杂一些，首先需要定义一个字体对象，即CFont类对象，然后通过该对象创建一个字体，接着使用CDC类的SelectObject方法选中新创建的字体，最后输出文本。

对于初学者来说，设置字体的难点在于字体的创建。创建字体之前，先来学习一下有关Windows字体的相关知识。字体分为3类—矢量字体（vector font）、光栅字体（raster font）和TrueType字体。这些字体的不同之处在于每个字符或符号的图像字符（gIyph）存储在单独的字体资源文件中。在光栅字体中，图像字符是一个位图，Windows用来绘制每一个字符或符号。在矢量字体中，图像字符是一个端点的集合，描述一个线段，Windows用来绘制每一个字符或符号。在TrueType字体中，图像字符是一个直线或曲线命令的集合，Windows使用直线或曲线命令来为每一个字符或符号定义位图轮廓。TrueType是一种与设备无关的字体，可以被任意放大或旋转，在任何尺寸上都可以达到较为满意的显示效果，尤其是在显示器和打印机上使用相同的字体。

在程序中，通常使用CFont类的CreateFont方法或CreatePointFont方法来创建字体。

CreateFont方法的语法格式如下：

CreateFont方法的参数较多，下面逐一介绍：

nHeight：以逻辑单位标识字体字符的高度（这里的字符高度是指字符单元格的高度减去内部标头的值）。如果为正数，字体映射机制会根据指定的高度从系统字体列表中匹配一种最为接近的字体，此时的匹配方式是以字体的单元高度为参考。如果为负数，匹配方式是以字体的字符高度为参考依据。这里笔者解释一下字符的单元高度和字符高度。在向屏幕中输出字符时，每个字符占据一个矩形区域，这个矩形区域被称为字符的单元格，字符在该单元格中被显示，如图12.5所示。

图12.5　字符显示

nWidth：以逻辑单位表示字符的平均宽度。

nEscapement：文本显示时的倾斜角度，以x轴为参考。参数值为实际旋转角度的10倍。

nOrientation：字符显示时的倾斜角度，以x轴为参考。参数值为实际旋转角度的10倍。

nWeight：字体的重量，即粗细程度，范围为0～1000。

bItalic：字体是否为斜体。

bUnderline：字体是否显示下画线。

cStrikeOut：字体是否显示删除线。

nCharSet：字体使用哪种字符集。

nOutPrecision：字体映射机制如何根据提供的参数来选择合适的字体。

nClipPrecision：字体的裁剪精度。

nQuality：字体输出质量，即字体参数与实际输出字符效果的接近程度。

nPitchAndFamily：可以设置两方面的内容，包括字符间距和字体属性。

lpszFacename：字体名称，包括结尾的终止符不能超过22个字符。

对于初学者来说，CFont类的CreateFont方法调用较为烦琐，为了简化字体的创建，CFont类还提供了CreatePointFont方法来创建字体。

语法格式如下：

参数说明：

nPointSize：字体大小。

lpszFaceName：字体名称。

pDC：一个设备上下文的指针，以该设备上下文来转换nPointSize为字体的高度。如果为NULL，将以屏幕的设备上下文作为转换的依据。

下面举例介绍如何输出指定的字体，并设置字体的输出颜色。

【例12.5】输出指定的字体，并设置字体的输出颜色。（实例位置：资源包\TM\sl\12\5）

具体操作步骤如下。

（1）创建一个单文档／视图结构应用程序，工程名称为SelFont。

（2）在视图类的OnDraw方法中编写如下程序代码。

说明

通过CreateFontIndirect方法和LOGFONT结构也可以创建字体。

（3）运行程序，结果如图12.6所示。

图12.6　设置输出字体

12.2.4　在路径中输出文本

路径提供了一种机制，使用户可以绘制出更为复杂的图形。MFC类库没有对路径进行封装，但是用户可以使用CDC类提供的路径函数来应用路径。其中，BeginPath函数用于在当前的设备上下文中打开一个路径。EndPath函数用于关闭一个打开的路径。程序中在调用BeginPath函数后，调用EndPath函数之前的所有点、线、文本、图形图像输出均称为当前路径的一部分。StrokePath函数用当前的画笔描述路径。由于CDC类提供的路径相关函数语法比较简单，多数函数没有参数，因此笔者不对这些函数进行语法介绍。下面举例介绍路径在文字输出中的作用，实例将利用路径在窗口中输出空心的文字。

【例12.6】利用路径在窗口中输出空心的文字。（实例位置：资源包\TM\sl\12\6）

具体操作步骤如下。

（1）创建一个文档／视图结构的应用程序，工程名称为PathFont。

（2）在视图类的OnDraw方法中编写如下程序代码。

（3）运行程序，结果如图12.7所示。

图12.7　根据路径输出文本

注意

在使用路径时必须是一个闭合图形，否则可能无法绘制出任何图形。

12.2.5　在图像背景上输出透明文本

在设计应用程序界面时，通常需要在原有图像基础上输出透明的文本。为了能够输出背景透明的文本，可以调用CDC类的SetBkMode方法将背景模式设置为透明。

语法格式如下：

其中，nBkMode表示设置的背景模式。如果为TRANSPARENT，表示背景模式为透明；如果为OPAQUE，表示以当前的背景颜色填充背景。

为了演示输出透明文本，下面的实例首先在窗口中绘制一幅位图，然后在位图上输出透明的文本。

【例12.7】在位图上输出透明的文本。（实例位置：资源包\TM\sl\12\7）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为TransText。

（2）在视图类的OnDraw方法中编写如下程序代码。

注意

在设置背景模式时，必须在每次绘制文字前调用SetBkMode方法。

（3）运行程序，结果如图12.8所示。

图12.8　透明文本

12.3　绘制图形

在设计电子地图、多媒体应用软件、股票分析软件等应用程序时，经常需要利用图形来描述实际数据。本节将介绍有关图形绘制的相关知识。

12.3.1　利用线条绘制多边形

图形是由点和线构成的，如果能够绘制点和线，绘制多么复杂的图形都不成问题。设备上下文CDC类提供了多种方法用于绘制线条，包括直线和曲线，下面分别进行介绍。

1. MoveTo

该方法用于移动当前的坐标点到指定的位置上。

语法格式如下：

参数说明：

x：当前位置的x轴坐标。

y：当前位置的y轴坐标。

2. LineTo

该方法用于从当前坐标点绘制一条直线到参数指定的位置。

语法格式如下：

参数说明：

x：直线终点的x轴坐标。

y：直线终点的y轴坐标。

3. Arc

该方法以一个外接矩形为依据绘制一个椭圆的弧线。

语法格式如下：

参数说明：

x1：矩形边框的左上角x轴坐标。

y1：矩形边框的左上角y轴坐标。

x2：矩形边框右下角x轴坐标。

y2：矩形边框右下角y轴坐标。

x3：弧线起点的x轴坐标。

y3：弧线起点的y轴坐标。

x4：弧线终点的x轴坐标。

y4：弧线终点的y轴坐标。

下面通过一个综合实例演示利用线条绘制多边形及弧线。

【例12.8】利用线条绘制多边形及弧线。（实例位置：资源包\TM\sl\12\8）

具体操作步骤如下。

（1）创建一个单文档／视图结构应用程序，工程名称为DrawLine。

（2）在视图类的OnDraw方法中编写如下程序代码。

注意

在使用LineTo方法时，第一次需要用MoveTo方法设置起点。第二次使用时，起点默认是第一次使用LineTo方法时设置的点，即不用再设置起点，直接设置终点即可，程序会将上一次设置的点作为起点。

（3）执行上述代码，结果如图12.9所示。

图12.9　绘制多边形及弧线

12.3.2　直接绘制多边形

第12.3.1节中介绍了利用绘制线条的方法绘制多边形，其缺点是需要用户自己计算每一条直线的长度。其实，设备上下文CDC类还提供了一些方法用于直接绘制多边形，下面分别进行介绍。

1. Rectangle

该方法使用当前的画笔绘制一个矩形。

语法格式如下：

参数说明：

x1、y1：矩形区域的左上角坐标。

x2、y2：矩形区域的右下角坐标。

lpRect：一个CRect对象指针，标识一个矩形区域。

2. RoundRect

该方法用于绘制一个圆角矩形。

语法格式如下：

参数说明：

x1、y1：矩形区域的左上角坐标。

x2、y2：矩形区域的右下角坐标。

x3：圆角的宽度。

y3：圆角的高度。

lpRect：一个CRect对象指针，标识一个矩形区域。

point：圆角的宽度和高度。

3. Polygon

该方法使用当前的画笔并根据列举的坐标点绘制一个多边形。

语法格式如下：

参数说明：

lpPoints：多边形的各个端点，这些端点的连线将构成一个多边形。

nCount：lpPoints元素的数量。

4. Polyline

该方法使用当前的画笔绘制坐标点中的直线。

语法格式如下：

参数说明：

lpPoints：各个端点、相邻的端点将用直线连接。

nCount：lpPoints元素的数量。

下面举例介绍如何直接绘制多边形。

【例12.9】绘制多边形。（实例位置：资源包\TM\sl\12\9）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为Poly。

（2）在视图类的OnDraw方法中编写如下程序代码。

注意

在使用Polygon方法绘制多边形时，系统会自动连接开始点和结束点而组成一个封闭图形。

（3）运行程序，结果如图12.10所示。

图12.10　直接绘制多边形

12.3.3　绘制控件外观

设备上下文CDC类不仅提供了绘制多边形的方法，还提供了绘制Windows常用控件的方法。当用户需要在界面中绘制一个Windows常用控件时，如绘制一个按钮控件，可以直接使用CDC类提供的DrawFrameControl方法。

语法格式如下：

参数说明：

lpRect：控件所在的矩形区域。

nType：控件的类型。当为DFC_BUTTON时，表示绘制按钮；为DFC_CAPTION时，表示绘制标题栏；为DFC_MENU时，表示绘制菜单；为DFC_SCROLL时，表示绘制滚动条。

nState：控件的风格或状态，取决于控件的类型。

下面举例介绍如何在窗口中绘制控件。

【例12.10】在窗口中绘制控件。（实例位置：资源包\TM\sl\12\10）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为DrawCtrl。

（2）在视图类的OnDraw方法中编写如下程序代码。

（3）运行程序，结果如图12.11所示。

图12.11　绘制控件

技巧

以要绘制的控件类为基类派生一个新的类，然后在新创建的类中绘制控件外观，这样获得的控件不仅具有源控件的功能，还具有好看的外观。

12.3.4　填充图形区域

在绘制图形时，有时需要在图形中填充一些颜色或某个图像，那么该如何向图形区域中填充颜色或图像呢？设备上下文CDC类提供了4个填充区域的函数，下面分别进行介绍。

1. FillRect

该函数用所标识的画刷填充一个矩形区域。

语法格式如下：

参数说明：

lpRect：需要填充的矩形区域。

pBrush：一个画刷对象指针，函数将使用该画刷填充矩形区域。

2. FillSolidRect

该函数用指定的颜色填充一个矩形区域。

语法格式如下：

参数说明：

lpRect：需要填充的矩形区域。

clr：标识一个RGB颜色值，函数将使用该颜色填充矩形区域。

3. FillRgn

该函数用所标识的画刷填充一个选区。

语法格式如下：

参数说明：

pRgn：一个选区对象。可通过系统提供的CreateRectRgn、CreateEllipticRgn和CreatePolygonRgn等API函数创建。

pBrush：一个画刷对象指针，函数将使用该画刷填充选区。

4. FillPath

该函数用当前的画刷填充路径。

语法格式如下：

下面通过一个实例来演示各个区域的填充方法。

【例12.11】填充区域的方法。（实例位置：资源包\TM\sl\12\11）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为FillRC。

（2）在视图类的OnDraw方法中编写如下程序代码。

注意

使用CombineRgn函数可以将图形组合在一起而形成一个新的图形。

（3）运行程序，结果如图12.12所示。

图12.12　填充区域

12.4　图像显示

在设计应用程序界面时，经常需要绘制窗口的背景图片，以使界面更加美观。本节将介绍有关图像输出显示的相关知识。

12.4.1　在设备上下文中绘制图像

在设备上下文中绘制图像有多种方法。例如，在第12.3.4节中创建一个位图画刷，利用其填充一个区域来实现图像的绘制。此外，还可以使用CDC类的位图函数输出位图到设备上下文中。下面分别进行介绍。

1. BitBlt

该函数用于从源设备中复制位图到目标设备中。

语法格式如下：

参数说明：

x：目标矩形区域的左上角x轴坐标点。

y：目标矩形区域的左上角y轴坐标点。

nWidth：在目标设备中绘制位图的宽度。

nHeight：在目标设备中绘制位图的高度。

pSrcDC：源设备上下文对象指针。

xSrc：源设备上下文的起点x轴坐标，函数从该起点复制位图到目标设备。

ySrc：源设备上下文的起点y轴坐标，函数从该起点复制位图到目标设备。

dwRop：光栅操作代码。可选值如表12.2所示。

表12.2　光栅操作代码

2. StretchBlt

该函数复制源设备上下文的内容到目标设备上下文中。与BitBlt方法不同的是，StretchBlt方法能够延伸或收缩位图以适应目标区域的大小。

语法格式如下：

参数说明：

x：目标矩形区域的左上角x轴坐标点。

y：目标矩形区域的左上角y轴坐标点。

nWidth：在目标设备中绘制位图的宽度。

nHeight：在目标设备中绘制位图的高度。

pSrcDC：源设备上下文对象指针。

xSrc：源设备上下文的起点x轴坐标，函数从该起点复制位图到目标设备。

ySrc：源设备上下文的起点y轴坐标，函数从该起点复制位图到目标设备。

nSrcWidth：需要复制的位图宽度。

nSrcHeight：需要复制的位图高度。

dwRop：光栅操作代码。可选值如表12.2所示。

下面通过一个实例介绍在设备上下文中绘制图像的各种方法。

【例12.12】在设备上下文中绘制图像。（实例位置：资源包\TM\sl\12\12）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为OutputBmp。

（2）在视图类的OnDraw方法中编写如下程序代码。

（3）运行程序，结果如图12.13所示。

图12.13　绘制图像

说明

由于大多数的图像和需要显示此图像的控件大小都不相同，因此通常需要使用StretchBlt方法对图像进行缩放，以达到图像的完整显示。

12.4.2　从磁盘中加载图像到窗口中

在开发程序时，通常需要从磁盘中动态加载一幅图像到窗口中。用户可以使用LoadImage函数来从磁盘加载图像文件。

语法格式如下：

参数说明：

hinst：表示包含图像的实例句柄，可以为NULL。

lpszName：表示图像的资源名称，如果从磁盘中加载，该参数表示图像的名称，包含完整路径。

uType：表示加载的图像类型。当为IMAGE_BITMAP时，表示加载位图；为IMAGE_CURSOR时，表示加载鼠标指针；为IMAGE_ICON时，表示加载图标。

cxDesired：表示图标或鼠标指针的宽度，如果加载的是位图，则该参数必须为0。

cyDesired：表示图标或鼠标指针的高度，如果加载的是位图，则该参数必须为0。

fuLoad：表示加载类型，如果为LR_LOADFROMFILE，表示从磁盘文件中加载位图。

返回值：函数返回加载的图像资源句柄。

下面通过一个实例介绍如何从磁盘中加载图像到窗口中。

【例12.13】从磁盘中加载图像到窗口中。（实例位置：资源包\TM\sl\12\13）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为LoadBmp。

（2）在视图类中添加一个成员变量m_hBmp。

（3）在视图类的构造函数中调用LoadImage方法从磁盘中加载文件，代码如下。

（4）在视图类的OnDraw方法中绘制位图，代码如下。

说明

Attach方法用于将位图对象关联到位图句柄上，Detach方法是与Attach方法配对使用的，该方法用于将位图句柄从位图对象上分离。

（5）运行程序，结果如图12.14所示。

图12.14　加载磁盘中的图像文件

12.4.3　位图文件分析

位图是Windows系统中一种最简单也是最基本的图像格式，了解位图的文件结构有助于对位图进行各种操作。下面分析一下位图文件的构成。

位图文件主要由4部分构成，分别为位图文件头、位图信息头、调色板和实际位图数据，如图12.15所示。

图12.15　位图结构

1. 位图文件头

位图文件头对应的结构为BITMAPFILEHEADER，共占用14个字节，其定义如下。

参数说明：

bfType：表示文件的类型，值为0x4d42，即字符串BM。

bfSize：文件的大小，也就是图12.15中描述的4部分的大小。

bfReserved1、bfReserved2：保留字。

bfOffBits：从文件头到实际的位图数据的偏移字节数，也就是图12.15中前3部分长度之和。

2. 位图信息头

位图信息头对应的结构为BITMAPINFOHEADER，共占用40个字节，其定义如下。

参数说明：

biSize：这个结构的大小，固定值为40。

biWidth：图像的宽度，单位为像素。

biHeight：图像的高度，单位为像素。

biPlanes：调色板的数量，必须为1。

biBitCount：指定颜色使用的位（Bit）数。若为1，表示黑白二色图；为4，表示16色图；为8，表示256色图；为24，表示真彩色图。

biCompression：位图是否压缩，通常为BI_RGB，即不压缩。

biSizeImage：实际位图数据的大小。

biXPelsPerMeter：目标设备的水平分辨率，单位是像素数／米。

biYPelsPerMeter：目标设备的垂直分辨率，单位是像素数／米。

biClrUsed：指定图像实际用到的颜色数，如果为0，表示用到了2的biBitCount次方。

biClrImportant：图像中重要的颜色数，如果为0，表示所有颜色都是重要的。

3. 调色板

调色板是一种GDI对象，可以认为它是一个颜色数组，列举了图像用到的所有颜色。对于真彩色位图来说，是没有调色板的，此时在位图信息后直接是位图的实际数据。

调色板的结构为LOGPALETTE，其定义如下。

参数说明：

palVersion：系统的版本号。

palNumEntries：调色板中包含的项目数，每一个项目表示一个颜色。

palPalEntry[1]：PALETTEENTRY数组中的第一个颜色。PALETTEENTRY结构描述了颜色信息，定义如下。

peRed：红色分量。

peGreen：绿色分量。

peBlue：蓝色分量。

peFlags：表示调色板中的项目如何被使用，可以为NULL。

4. 实际位图数据

对于使用了调色板的位图，位图实际数据描述的是像素值在调色板中的索引值；如果没有使用调色板，位图实际数据就是实际的RGB颜色值。

12.4.4　根据位图数据直接绘图

在处理网络应用程序传输图像时，接收方收到的将是数据流，如何能够将图像数据流显示在窗口中呢？Windows系统提供了StretchDIBits函数，通过它能够根据位图的数据流将图像显示在窗口中。

语法格式如下：

参数说明：

dc：标识一个设备上下文对象。

Dest：在设备上下文的x轴起点处绘图。

Dest：在设备上下文的y轴起点处绘图。

DestWidth：需要绘制的图像宽度。

nDestHeight：需要绘制的图像高度。

XSrc：从图像的x轴起点处复制图像。

YSrc：从图像的y轴起点处复制图像。

nSrcWidth：需要复制的图像宽度。

nSrcHeight：需要复制的图像高度。

lpBits：位图图像数据。

lpBitsInfo：位图信息。

iUsage：表示lpBitsInfo结构的bmiColors成员是否被提供。为DIB_PAL_COLORS时，表示位图数据中包含的是调色板中的索引；为DIB_RGB_COLORS时，表示位图数据是实际的RGB颜色值。

dwRop：表示图像如何被显示到目标设备中，具体值如表12.2所示。

下面以一个实例来介绍如何使用StretchDIBits根据数据流绘制图像。

【例12.14】使用StretchDIBits根据数据流绘制图像。（实例位置：资源包\TM\sl\12\14）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为OutputStream。

（2）在视图类中定义一个缓冲区，用于存储数据流。

（3）在视图类的构造函数中读取文件到数据流中，代码如下。

说明

有关CFile类的使用，将在第15章中进行介绍。

（4）在视图类的析构函数中释放缓冲区，代码如下。

（5）向视图类中添加一个成员函数，根据数据流输出图像，代码如下。

（6）在视图类的OnDraw方法中调用OutputStream方法绘制图像，代码如下。

（7）运行程序，结果如图12.16所示。

图12.16　根据数据流绘制图像

说明

由于BMP文件中数据的存储就是以位图文件格式存储的，因此通过BMP文件中的数据可以直接绘制图形。

12.4.5　显示JPEG图像

前面所介绍的图像输出都是以位图为例的。位图文件最大的缺点就是压缩比较小，占用空间大，因此网页均不采用位图的格式显示，而是以JPEG或GIF格式显示。在Visual C++中，显示JPEG图像并不像显示BMP图像那么简单，它需要通过流操作来实现。具体思路是将JPEG文件加载到堆中，然后在堆中创建一个数据流，接着调用OleLoadPicture函数加载流中的数据到IPicture接口中，最后调用IPicture接口的Render方法输出图像信息。下面举例介绍在窗口中显示JPEG图像。

【例12.15】在窗口中显示JPEG图像。（实例位置：资源包\TM\sl\12\15）

具体操作步骤如下。

（1）创建一个单文档／视图结构的应用程序，工程名称为ShowJPEG。

（2）向视图类中添加成员变量，代码如下。

（3）在视图类的构造函数中从磁盘中加载JPEG图像到流中，代码如下。

（4）在视图类的OnDraw方法中绘制JPEG图像，代码如下。

（5）运行程序，结果如图12.17所示。

图12.17　显示JPEG图像

说明

本实例是通过IPicture接口来绘制JPEG图像的，同样只要修改一下文件扩展名即可直接绘制GIF的图像。

12.5　GDI+图像编程

GDI+是微软.NET类库提供的用于图像编程的一组类，被组织在6个命名空间中。GDI+是基于GDI的，对GDI进行了重新封装，极大地降低了开发人员进行图像编程的难度。本节将介绍有关GDI+图像编程的相关知识。

12.5.1　在Visual C++ 6.0中使用GDI+

GDI+是微软.NET类库的一个组成部分，虽然它没有集成在Visual C++ 6.0开发环境中，但是用户仍可以在Visual C++ 6.0环境下使用它。下面来介绍如何在Visual C++ 6.0中使用GDI+。

（1）下载GDI+包文件。

（2）引用Gdiplus.h头文件。

（3）引用Gdiplus命名空间，格式如下。

（4）定义两个全局变量，格式如下。

（5）在应用程序或对话框初始化时加载GDI+，格式如下。

（6）在应用程序结束时卸载GDI+，格式如下。

（7）在程序中链接gdiplus.lib库文件，格式如下。

说明

因为Visual C++ 6.0中没有GDI+开发包，所以要想使用GDI+，就需要手动将其下载到程序中。

12.5.2　利用GDI+实现图像类型转换

在Visual C++ 6.0中，实现各种类型的图像转换是比较复杂的，有时还需要用户了解图像的各种格式，以及图像的编码、解码算法。使用GDI+，用户则可以非常方便地实现图像类型的转换。下面编写一个实例实现BMP、JPEG、GIF图像格式的相互转换。

【例12.16】实现BMP、JPEG、GIF图像格式的相互转换。（实例位置：资源包\TM\sl\12\16）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为ConvertImage，对话框资源设计窗口如图12.18所示。

图12.18　对话框资源设计窗口

（2）引用Gdiplus的头文件和命名空间，代码如下。

（3）向对话框类中添加成员变量，代码如下。

（4）在对话框初始化时初始化GDI+，并设置按钮的状态，代码如下。

（5）向对话框中添加OnCancel方法，在对话框关闭时卸载GDI+，代码如下。

（6）向对话框中添加GetCodecClsid方法，根据图像格式获取其CLSID，代码如下。

（7）处理“…”按钮的单击事件，选择待转换的图像，代码如下。

说明

加粗部分表示要打开的文件类型。

（8）向对话框类中添加TestButtonState方法，根据按钮的命令ID判断按钮是否被选中，代码如下。

（9）处理“转换”按钮的单击事件，将源图像转换为指定的图像格式，代码如下。

（10）运行程序，结果如图12.19所示。

图12.19　图像格式转换

12.5.3　使用GDI+显示GIF图像

GIF（Graphics Interchange Format，图形交换格式）是由CompuServe公司开发的图形文件格式，用户在浏览网页时经常可以看到GIF格式的动画。在设计应用程序时，该如何显示GIF动画呢？

用户可以通过第12.4.5节中介绍的显示JPEG图像的方法显示GIF图像，但是它只能显示一幅静态的图像，如果GIF文件中包含有多帧，则不能显示动画效果。本节将介绍使用GDI+实现GIF动画的显示。GDI+提供了一个Image类，使用该类的GetFrameDimensionsCount方法可以获取GIF文件中帧的维数，通过GetFrameDimensionsList方法获取图像帧的GUID，通过GetFrameCount方法根据GUID获取图像的帧数，通过SelectActiveFrame方法设置图像显示的当前帧，最后使用Graphics类的DrawImage方法显示当前帧的图像即可显示GIF动画。下面通过一个实例介绍GIF图像的显示。

【例12.17】显示GIF图像。（实例位置：资源包\TM\sl\12\17）

具体操作步骤如下。

（1）创建一个文档／视图结构的应用程序，工程名称为ShowGIF。

（2）引用GID+的头文件、命名空间和库文件，代码如下。

（3）向视图类中添加成员变量，代码如下。

（4）在视图类的构造函数中初始化GDI+，并读取GIF文件的相关信息，代码如下。

（5）定义一个内存画布类，防止图像闪烁，代码如下。

（6）处理视图类的WM_TIMER消息，按照一定的时间间隔绘制不同的图像帧，代码如下。

注意

由于GIF图像是由多幅图像组成的，因此想显示可运动的GIF图像就必须绘制出每幅图像。

（7）处理视图类的WM_CREATE消息，在视图窗口创建后开启一个计时器，代码如下。

（8）在视图类的析构函数中卸载GDI+，代码如下。

（9）运行程序，结果如图12.20所示。

图12.20　显示GIF图像

12.6　小结

本章通过大量实例介绍了在Visual C++中绘制文本、图形、图像的各种方法。灵活地运用这些技术，读者可以设计各种图像窗口，进行各种常规图像操作，并且可以设计第三方可视控件。

12.7　实践与练习

1. 在标题栏中输出文本。（答案位置：资源包\TM\sl\12\18）

2. 设计一个可以适应窗口大小变化的背景图像。（答案位置：资源包\TM\sl\12\19）

3. 设计一个可以换肤的按钮控件。（答案位置：资源包\TM\sl\12\20）

第13章

文档与视图

（视频讲解：30分钟）

文档／视图结构是利用MFC开发应用程序的一种规范，使用文档／视图结构可以使开发过程模块化。文档／视图结构的好处是把应用程序的数据从用户操作数据的方法中分离出来，使文档对象只负责数据的存储、装载与保存，而数据的显示则由视图类来完成。

通过阅读本章，您可以：

了解文档／视图结构

掌握文档／视图结构的创建

熟悉文档模板

熟悉文档对象

熟悉视图对象

熟悉框架窗口

掌握文档／视图的典型应用

13.1　构建文档／视图应用程序

文档／视图结构是MFC类库的主要特征之一，它采用面向对象的设计手法，将数据的显示和存储相分离。本章将围绕文档／视图结构，依据源代码剖析其原理。

13.1.1　文档／视图结构概述

文档／视图结构是MFC应用程序的重要组成部分。想学好Visual C++，则必须掌握文档／视图结构。文档是数据的载体，负责数据的存储、管理和维护；视图是一个子窗口，负责显示文档中的数据。

文档与视图之间采用“观察者”设计模式。在文档对象（CDocument）中保存了一个视图列表（m_viewList），当文档中的数据改变时，它将通知所有关联的视图更新数据。同理，在视图对象（CView）中也保存了一个文档对象（m_pDocument），当用户通过视图修改了数据，它可以通知文档对象保存数据。

为了显示视图，MFC定义了一个框架类CFrameWnd。CFrameWnd可以作为视图的容器，所有的视图都显示在其中。此外，在框架类中，还提供了菜单、工具栏、状态栏等界面元素。

在MFC中，为了管理和维护文档、视图、框架之间的关系，定义了一个文档模板类CDocTemplate,并从该类派生了两个子类CSingleDocTemplate和CMultiDocTemplate。实际上，文档、视图、框架的创建，都是通过CDocTemplate或其派生类实现的。当应用程序的文档模板为CSingleDocTemplate时，表示应用程序为单文档应用程序；如果应用程序的文档模板为CMultiDocTemplate，表示应用程序是多文档应用程序。单文档应用程序与多文档应用程序的区别是：单文档应用程序一次只能打开一个框架窗口，同一时刻，只能存在一个文档实例；多文档应用程序一次可以打开多个框架窗口，每个框架窗口都可以包含一个文档实例。

13.1.2　创建文档／视图结构应用程序

MFC提供了类向导帮助用户创建文档／视图应用程序，具体步骤如下。

（1）选择File/New命令，打开New对话框，如图13.1所示。

（2）选择Projects选项卡，在列表中选择MFC AppWizard[exe]选项，在Projects name编辑框中输入工程名称，单击OK按钮进入MFC应用程序向导，如图13.2所示。

（3）选中Single document单选按钮，创建一个单文档视图应用程序，单击Finish按钮完成应用程序的创建。

图13.1　New对话框

图13.2　MFC应用程序向导

（4）运行程序，结果如图13.3所示。

图13.3　程序运行结果

13.2　文档／视图结构的创建

文档／视图结构是MFC应用程序的重要组成部分。文档是数据的载体，负责数据的存储、管理和维护；视图是一个子窗口，负责显示文档中的数据。

13.2.1　文档模板的创建

文档、视图、框架的创建是由文档模板创建的。当用户创建一个文档／视图结构的应用程序时，在应用程序的InitInstance方法中会发现如下代码。

程序首先定义了一个文档模板指针pDocTemplate，然后调用构造函数在堆中创建实例。CSingleDocTemplate的构造函数主要是调用了基类CDocTemplate的构造函数初始化内部的数据成员m_pDocClass、pFrameClass、pViewClass和nIDResource，主要代码如下。

在应用程序的InitInstance方法中，当文档模板创建后，调用AddDocTemplate将文档模板添加到文档管理器（CDocManager）中。CWinApp提供了一个文档管理器指针m_pDocManager，应用程序就是通过该指针将文档模板添加到文档管理器中的，代码如下。

文档管理器的AddDocTemplate方法，是在CDocManager中定义了一个文档模板列表m_templateList和一个静态的文档模板指针pStaticList。当AddDocTemplate方法传递的文档模板为空时，会将静态文档模板指针中的模板添加到m_templateList中，如果文档模板不为空，将该模板添加到m_templateList的末尾，代码如下。

13.2.2　文档的创建

文档、视图、框架是由文档模板创建的，但之前分析文档模板的创建时，并没有对文档进行处理。下面介绍文档是如何被创建的。

在应用程序的InitInstance方法中，有下面几行代码。

其中，应用程序CWinApp的ProcessShellCommand方法用于处理命令行，代码如下。

由于命令行类CCommandLineInfo的默认构造函数将m_nShellCommand初始化为FileNew，因此将执行黑体部分代码，默认情况下，应用程序对象在消息映射部分处理了ID_FILE_NEW消息，代码如下。

因此，会调用应用程序的OnFileNew方法（从ProcessShellCommand方法中可以知道，即使应用程序没有处理ID_FILE_NEW消息，也会调用OnFileNew方法，因此，用户如果把黑体部分语句注释掉，系统依然会创建一个文档）。继续追踪应用程序的OnFileNew方法。

由于文档管理器在应用程序调用AddDocTemplate方法时就已经创建了，因此程序将执行黑体部分代码，调用文档管理器的OnFileNew方法。该方法首先判断文档管理器中的文档模板数量是否大于1，如果是，将弹出一个对话框，让用户选择一个文档模板，代码如下。

在OnFileNew方法的最后调用了文档模板的OpenDocumentFile方法。

由于在文档管理器的OnFileNew方法中调用OpenDocumentFile方法时，传递了空值，因此将执行黑体部分代码，调用文档模板的CreateNewDocument方法。

综上所述，文档对象由模板对象的m_pDocClass（运行时类对象）成员创建。

13.2.3　框架与视图的创建

在创建框架的同时，也创建了视图对象。在第13.2.2节中介绍文档的创建时，介绍过文档模板的OpenDocumentFile方法。实际上，框架和视图也是在该方法中创建的。下面再次分析OpenDocumentFile方法。

在OpenDocumentFile方法中将调用CreateNewFrame方法来创建框架。

在CreateNewFrame方法中，首先通过模板对象的类成员m_pFrameClass创建框架对象，然后调用框架的LoadFrame方法创建框架窗口，在LoadFrame方法中将调用Create方法创建框架窗口，Create方法又调用CreateEx方法创建窗口，而CreateEx方法又调用CreateWindowEx方法创建窗口，利用CreateWindowEx方法在创建窗口时发送WM_CREATE消息，导致调用CFrameWnd的OnCreate方法，在OnCreate方法中会引起连串地调用OnCreateHelper方法、OnCreateClient方法和CreateView方法。视图最终在CreateView方法中被创建。

试一试：修改应用程序向导自动产生的程序，使文档视图类程序运行先显示一个消息框“欢迎光临”，然后进入主程序。

13.3　文档模板概述

应用程序CWinApp对文档模板的管理是通过文档管理器（CDocManager）实现的。在应用程序CWinApp类中定义了一个文档管理器对象指针m_pDocManager，应用程序通过它来管理文档模板。

为了让读者了解文档管理器的作用，下面介绍文档管理器的主要数据成员和方法。

13.3.1　文档管理器

在文档管理器CDocManager中定义了一个文档模板列表m_templateList，用于存储多个模板。同时，文档管理器还维护了一个全局的文档管理器和全局文档模板列表，代码如下。

静态成员变量pStaticInit确定是否将文档模板放入全局的文档模板列表（pStaticList）中。默认情况下，MFC向导在创建文档／视图结构时，将不通过全局文档模板列表存储文档模板。下面一段代码利用全局文档模板列表存储文档模板。需要说明的是，全局文档模板列表只是临时地存储文档模板，文档管理器最终会将全局文档模板列表中的文档模板放入m_templateList中。

首先在全局作用域中将CDocManager::bStaticInit设置为TRUE，然后定义一个单文档模板pDocTemplate。运行程序，发现一切正常。对于上面的代码，许多读者会产生疑问：文档模板如何被存储在全局文档模板列表中？没有发现应用程序调用AddDocTemplate方法，应用程序的文档管理器m_pDocManager是如何添加文档模板的呢？

对于第一个问题，可以在文档模板CDocTemplate的构造函数中找到答案。

当在程序中定义一个全局的文档模板对象时，会执行黑体部分代码，创建全局文档模板列表和全局文档管理器，然后将文档模板添加到全局文档模板列表中。

对于第二个问题，可以在应用程序的InitApplication方法中得到答案。

在应用程序启动时，会执行InitApplication方法，将应用程序的文档管理器m_pDocManager指向全局的文档管理器，然后调用文档管理器的AddDocTemplate方法开始创建文档、框架、视图，再次分析该方法。

由于在InitApplication方法中调用AddDocTemplate时，传递的是NULL参数，因此程序将执行黑体部分代码，利用递归的方式将全局文档模板列表中的数据添加到文档模板列表m_templateList中。

为了管理文档模板，文档管理器还提供了多个方法。

（1）GetFirstDocTemplatePosition方法

该方法获取文档管理器中第一个文档模板的位置，语法格式如下。

（2）GetNextDocTemplate方法

该方法返回指定位置的文档模板，然后将位置指向下一个文档模板，语法格式如下。

其中，pos标识文档的位置。

（3）GetOpenDocumentCount方法

该方法用于获取文档模板列表中的文档对象数量，语法格式如下。

（4）CloseAllDocuments方法

该方法用于关闭文档模板列表中的所有文档，语法格式如下。

其中，bEndSession标识会话是否被结束，如果为TRUE，会话将要被结束。

（5）SaveAllModified方法

该方法保存所有修改的文档，语法格式如下。

（6）OpenDocumentFile方法

该方法用于创建或打开一个文档，语法格式如下。

其中，lpszFileName如果为NULL，将创建一个新的文档对象，否则根据文件名打开文档。

13.3.2　文档模板

MFC提供了两种文档模板，单文档模板CSingleDocTemplate和多文档模板CmultiDoc Template。这两个模板均继承于CDocTemplate。下面介绍CDocTemplate的主要成员和方法。

（1）CDocTemplate方法

该方法用于构造一个文档模板，语法格式如下。

其中，nIDResource标识一个字符串资源句柄，用于初始化m_strDocStrings成员。pDocClass、pFrameClass、pViewClass分别为文档、框架、视图类的指针。

（2）AddDocument方法

该方法用于将文档添加到文档模板的列表中，语法格式如下。

其中，pDoc标识一个文档指针。在CDocTemplate类中，该方法只是设置文档对象的模板设置。派生类CSingleDocTemplat和CMultiDocTemplate均改写了该方法，真正地将文档添加到文档模板列表中。

（3）GetFirstDocPosition方法

该方法用于获取文档模板列表中第一个文档的位置，语法格式如下。

其中，该方法是一个纯虚函数，派生类CSingleDocTemplat和CMultiDocTemplate均改写了该方法。

（4）GetNextDoc方法

该方法用于获取指定位置的文档对象，并将当前位置设置为下一个文档所在位置，语法格式如下。

其中，rPos标识文档的位置。该方法是一个纯虚函数。派生类CSingleDocTemplat和CMultiDocTemplate均改写了该方法。在CSingleDocTemplat中，由于只有一个文档对象，因此当rPos为BEFORE_START_POSITION时，会返回一个文档对象，否则函数值为NULL。在CMultiDocTemplate中，该方法才真正具有意义。

（5）CreateNewDocument方法

该方法用于创建一个新的文档对象，然后调用AddDocument方法将文档对象添加到文档列表中，语法格式如下。

（6）CloseAllDocuments方法

该方法用于关闭所有文档。实际上，文档管理器的CloseAllDocuments方法调用了文档模板的CloseAllDocuments方法实现关闭文档的功能，语法格式如下。

其中，bEndSession标识会话是否被结束，如果为TRUE，会话将要被结束。

（7）SaveAllModified方法

该方法用于保存所有修改的文档，语法格式如下。

（8）SetDefaultTitle方法

该方法用于设置默认的文档标题，语法格式如下。

其中，pDocument标识一个文档对象指针。该方法是一个纯虚函数，CSingleDocTemplat和CMultiDocTemplate均改写了该方法。对于单文档模板，利用docName对应的字符串作为文档标题；对于多文档应用程序，需要对文档进行记数。

13.4　文档对象

13.4.1　文档对象的主要方法

文档对象作为数据的载体，提供了多个方法用于管理数据，其主要方法如下。

（1）GetTitle方法

该方法用于获取文档的标题，语法格式如下。

（2）SetTitle方法

该方法用于设置文档标题，语法格式如下。

其中，lpszTitle是一个字符串指针，标识文档标题。

（3）GetDocTemplate方法

该方法用于获取文档对象所在的文档模板，语法格式如下。

（4）IsModified方法

该方法用于判断文档是否已经被修改，语法格式如下。

（5）SetModifiedFlag方法

该方法用于设置或清除修改标记，语法格式如下。

其中，当bModified的值为TRUE时，将设置修改标记；为FALSE时，将清除修改标记。

（6）AddView方法

该方法用于向视图列表（CDocument的数据成员m_viewList）中添加视图对象，语法格式如下。

其中，pView标识一个视图指针。

（7）GetFirstViewPosition方法

该方法用于返回视图列表中第一个视图的位置，语法格式如下。

（8）GetNextView方法

该方法用于获取视图列表中指定位置的视图对象，并将当前位置设置为下一个视图对象的位置，语法格式如下。

其中，rPosition标识视图位置。

（9）OnNewDocument方法

在CDocument中，该方法主要用于清除修改标记。用户可以改写该方法，当文档被创建时，应用程序会调用该方法，这样就可以执行用户的代码，语法格式如下。

（10）OnOpenDocument方法

该方法用于打开指定的文档，语法格式如下。

其中，lpszPahtName标识文档名称，包含完整路径。

（11）OnSaveDocument方法

该方法用于保存指定的文档，语法格式如下。

其中，lpszPathName标识指定的文档名称，包含完整路径。

（12）OnCloseDocument方法

该方法用于关闭文档，文档关联的框架和视图将要被释放，语法格式如下。

（13）GetFile方法

该方法用于打开指定的文件，并返回文件指针，语法格式如下。

其中，lpszFileName标识文件名称；nOpenFlags标识文件打开标记；pError标识一个文件异常指针，如果文件打开错误，pError中将记录错误信息。返回值是打开的文件指针。

（14）DoSave方法

该方法用于保存文档。它在内部调用OnSaveDocumen方法实现保存操作，语法格式如下。

其中，lpszPathName标识文件名称，如果为空，将弹出保存对话框；bReplace标识执行保存操作还是另存为操作。

（15）DoFileSave方法

该方法用于保存文档。DoFileSave首先读取文件属性，然后根据文件属性的不同调用DoSave方法，语法格式如下。

13.4.2　文档的初始化

文档对象是通过调用文档模板的OpenDocumentFile方法创建的。在文档对象被新建时，将会进行一系列的初始化。

调用SetDefaultTitle方法设置文档标题。

调用OnNewDocument方法，执行用户编写的OnNewDocument事件代码。

调用DeleteContents方法进行清理工作。默认情况下，该方法不执行任何动作，用户可以改写该方法执行必要的清理工作。

调用SetModifiedFlag方法将修改标记设置为FALSE。

对于单文档模板，当用户再次新建或打开一个文档时，程序会复用以前的文档对象。下面列出打开一个文档时发生的一系列动作。

调用SaveModified方法保存当前文档。

调用SetModifiedFlag方法将修改标记设置为FALSE。

调用GetFile方法获取打开的文件指针。

调用DeleteContents方法进行清理工作。默认情况下，该方法不执行任何动作，用户可以改写该方法执行必要的清理工作。

调用SetModifiedFlag方法将修改标记设置为TRUE。

调用Serialize方法进行序列化。

调用ReleaseFile方法删除文件指针。

调用SetModifiedFlag方法清除修改标记。

13.4.3　保存文档

在第13.4.1节介绍文档对象的方法时，曾讲解了几个关于保存文档的方法。那么，用户在菜单中单击“保存”按钮时，这些方法是如何相互调用的呢？

在保存文档时，首先调用OnFileSave方法，然后调用DoFileSave方法，接着调用DoSave方法，最后调用OnSaveDocument方法，有关这些方法的介绍请参考第13.4.1节。

13.4.4　文档的命令处理

文档CDocument派生于CCmdTarget，所以能够处理命令消息。

从上述代码可以知道，对于文档对象没有处理的命令消息，将交给文档模板进行处理。当用户选择“保存”“另存为”菜单项时，实际上就是通过CDocument类的消息映射调用OnFileSave、OnFileSaveAs方法进行保存。当用户选择“保存”菜单项时，程序将调用框架类CFrameWnd的OnCmdMsg方法。在CFrameWnd的OnCmdMsg方法中，首先调用当前视图的OnCmdMsg方法，如果在视图类的消息映射表中没有处理该消息，继续判断是否在框架类的消息映射表中处理了该消息，如果还没有处理，最后将调用应用程序的OnCmdMsg方法，即在应用程序类的消息映射表中搜索消息对象的处理函数。CFrameWnd的OnCmdMsg方法代码如下。

在上面的代码中，并没有直接调用文档对象的OnCmdMsg方法。下面继续分析视图类的OnCmdMsg方法。

由以上代码可以看出，视图类的OnCmdMsg方法中调用了文档类的OnCmdMsg方法。如果没有在视图类的消息映射表中发现相应的消息处理函数，将调用文档类的OnCmdMsg方法。那“保存”菜单项的命令ID（ID_FILE_SAVE）是如何与OnFileSave方法关联的呢？在DOCCORE.CPP的源文件中，有下面的代码。

分析上面的代码，可以这样猜想：如果在视图类中处理了ID_FILE_SAVE命令消息，就不会调用文档类的OnCmdMsg方法，因此也就不会执行保存操作？为了验证这种想法，可以先做一个测试。

（1）创建一个单文档／视图应用程序。

（2）按Ctrl+W快捷键，打开类向导对话框，如图13.4所示。

图13.4　MFC类向导

（3）在Class name组合框中选择视图类，在Object IDs列表框中选择ID_FILE_SAVE选项，在Messages列表框中双击COMMAND选项，编写事件处理方法OnFileSave。

（4）在OnFileSave方法中可以不写任何代码，运行程序，选择“文件”／“保存”菜单项，发现程序没有任何反应。所以，上面的猜想是正确的。

13.4.5　文档的销毁

文档的销毁是指删除文档对象。在应用程序中，通常在两种情况下会销毁文档：一是应用程序结束时，用户单击“退出”按钮，会销毁文档；二是调用文档对象的OnFileClose方法。这两种方式最终都是通过调用文档对象的OnCloseDocument方法销毁文档的。

首先分析第一种情况。当应用程序结束时会调用OnAppExit方法向主窗口（框架窗口）发送WM_CLOSE消息。

由于当前主窗口是框架窗口，因此会调用框架窗口的OnClose方法，在该方法中调用了CloseAll-Documents方法关闭所有文档。

跟踪应用程序的CloseAllDocuments方法，该方法调用了文档管理器的CloseAllDocuments方法，代码如下。

文档管理器的CloseAllDocuments方法遍历所有的文档模板，调用文档模板的CloseAllDocuments方法，代码如下。

继续跟踪文档模板的CloseAllDocuments方法，发现在该方法中最终调用了文档对象的OnCloseDocument列表销毁文档。

稍后再讨论文档对象的OnCloseDocument方法。下面分析调用文档对象的OnFileClose方法是如何销毁文档的。

在OnFileClose方法中调用了OnCloseDocument方法。如前所述，两种方式均调用OnCloseDocument方法销毁文档，该方法是如何销毁文档的呢？

在文档对象的OnCloseDocument方法中有delete this语句，该语句将执行文档对象的析构函数销毁文档。

在文档对象的析构函数中，首先调用DisconnectViews方法清空视图列表，断开视图列表与文档的关联；然后调用文档模板的RemoveDocument，清空文档对象关联的文档模板，也就是将文档对象的m_pDocTemplate成员设置为空。

文档对象的DisconnectViews方法的代码如下。

文档模板的RemoveDocument方法的代码如下。

13.5　视图对象

13.5.1　视图对象的主要方法

视图的作用是显示文档中的数据，用户也可以通过视图修改文档中的数据。在视图CView中还封装了打印和打印预览功能，使打印工作变得简单。下面介绍视图对象的主要方法。

（1）DoPreparePrinting方法

该方法用于显示打印对话框，创建打印机画布，语法格式如下。

其中，PInfo标识打印信息。如果进行打印或打印预览操作，返回值为非零，否则为零。

（2）GetDocument方法

该方法用于获取视图关联的文档对象，语法格式如下。

（3）OnDraw方法

该方法用于在视图上绘制数据，语法格式如下。

（4）OnBeginPrinting方法

该方法在打印或打印预览之前调用，用户可以改写该方法，以实现特殊的功能，语法格式如下。

其中，pDC标识一个画布指针；pInfo包含了打印信息，用户可以在该方法中修改打印信息。

（5）OnEndPrinting方法

该方法在打印或打印预览结束后，由框架调用。用户可以改写该方法，以实现特殊的功能。通常情况下，在该方法中释放OnBeginPrinting方法中分配的GDI资源，语法格式如下。

其中，pDC标识一个画布指针；pInfo包含了打印信息，用户可以在该方法中修改打印信息。

（6）OnEndPrintPreview方法

该方法在用户退出打印预览时由框架调用。用户可以改写该方法，在打印预览结束时实现其他功能，语法格式如下。

其中，pDC标识打印机设备指针，pInfo记录打印信息，point标识最后一页上的点，PView标识预览视图指针。

（7）OnPrepareDC方法

该方法在OnDraw方法或OnPrint方法被调用前，由框架调用。默认情况下，该方法没有实现任何功能，用户可以在派生类中改写该方法，设置画布的属性，语法格式如下。

其中，pDC标识一个画布指针，pInfo标识打印信息。如果是由绘画导致调用该方法，则pInfo为NULL；如果是由打印或打印预览导致调用该方法，则pInfo包含了打印信息。

（8）OnPrint方法

在打印或打印预览文档中的每一页时，由框架调用该方法，语法格式如下。

其中，pDC标识打印机画布，PInfo记录打印信息。

13.5.2　视图的初始化

前面已经介绍，视图是通过调用框架的CreateView方法创建的。当视图调用Create方法时，会通过::CreateWindowEx方法引发WM_Create消息，执行OnCreate事件处理函数，将自身添加到文档对象的视图列表中。那么视图在被创建时，是如何初始化的呢？

这需要查看文档模板的OpenDocumentFile方法。在OpenDocumentFile方法的结尾处调用了InitialUpdateFrame方法初始化更新，而该方法直接调用了框架对象的Initial UpdateFrame方法，在框架对象的InitialUpdateFrame方法中向所有视图发送了WM_INITIAL UPDATE消息。

这样，视图对象将处理WM_INITIALUPDATE消息，执行OnInitialUpdate方法，而该方法直接调用OnUpdate方法更新数据。

13.5.3　视图的绘制

视图的主要作用是显示数据，但视图是如何显示数据的呢？

第13.5.2节已经介绍过，视图的初始化通过调用OnUpdate方法来更新数据。分析OnUpdate方法，该方法调用了Invalidate方法，进而引发了WM_PAINT消息，最终调用了视图对象的OnPaint消息处理函数。

在视图类的OnPaint方法中，首先调用OnPrepareDC方法设置画布属性，然后调用OnDraw方法绘制数据。

当用户创建一个文档／视图应用程序时，系统会自动实现视图的OnDraw方法。例如：

用户可以在该方法中利用画布指针pDC绘制视图数据。默认情况下，OnDraw方法只是获得文档对象指针，不绘制任何数据。下面的代码演示了如何在视图中绘制数据。

13.5.4　视图的销毁

视图作为框架的子窗口，当框架被关闭时，视图将被自动销毁。视图CView派生于CWnd，因此视图的销毁也分为两个过程，首先销毁窗口资源，然后释放窗口对象。

在应用程序中，视图的销毁通常是由框架关闭引起的。因此，这里就从框架关闭开始介绍视图是如何被销毁的。

当框架CFrameWnd关闭时，会接收到WM_CLOSE消息，通过消息映射会执行OnClose方法。

在OnClose方法中，由于框架是应用程序的主窗口，因此会调用应用程序的CloseAllDocuments方法。

应用程序的CloseAllDocuments方法调用了文档管理器的CloseAllDocuments方法。

文档管理器的CloseAllDocuments方法调用了文档模板的CloseAllDocuments方法。

文档模板的CloseAllDocuments方法调用了文档对象的OnCloseDocument方法。

文档对象的OnCloseDocument方法调用了框架的DestroyWindow方法，框架的DestroyWindow方法直接调用了CWnd的DestroyWindow方法释放窗口。

在窗口类CWnd的DestroyWindow方法中，调用了API函数DestroyWindow释放窗口。该函数会向窗口发送WM_DESTROY消息，执行框架类的OnDestroy方法，然后向子窗口（包括视图对象）发送WM_DESTROY消息，执行视图类的OnDestroy方法，接着释放掉视图窗口，在视图窗口被释放后，向视图窗口发送最后一个窗口消息WM_NCDESTROY，执行视图的PostNcDestroy方法。

语句delete this最终会调用视图的析构函数释放掉视图对象。图13.5显示了框架关闭时，视图被销毁的流程。

图13.5　消息流程图

13.6　框架窗口

13.6.1　框架对象的主要方法

框架作为视图的容器，提供了多个方法以维护和管理视图。

（1）LoadFrame方法

该方法从资源模板中加载框架资源，语法格式如下。

其中，nIDResource标识管理框架窗口的资源ID，dwDefaultStyle标识窗口风格，pParentWnd用于指定框架的父窗口，pContext用于指定框架关联的文档、视图信息。

（2）ActivateFrame方法

该方法用于激活或显示一个框架窗口，语法格式如下。

其中，nCmdShow标识用于传递到ShowWindow的参数，默认值为-1，表示框架被显示，并且还原到之前的大小。

（3）GetActiveFrame方法

该方法用于获得当前活动的框架窗口，多用于MDI应用程序，语法格式如下。

（4）SetActiveView方法

该方法用于将某个视图设置为当前视图，语法格式如下。

其中，pViewNew标识框架中的一个视图对象；bNotify标识在视图被激活时，是否调用OnActivateView方法，如果为TR UE，则调用OnActivateView方法，否则不调用。

（5）GetActiveView方法

该方法用于返回框架中当前活动的视图对象，语法格式如下。

（6）CreateView方法

该方法用于创建一个框架内的视图，语法格式如下。

其中，pContext记录文档、视图的类型信息，nID标识视图的ID。

（7）GetActiveDocument方法

该方法用于返回关联当前视图的文档对象指针，语法格式如下。

（8）OnCreateClient方法

该方法用于为框架创建客户窗口，语法格式如下。

其中，lpcs记录了窗口结构信息；pContext记录了文档、视图类型信息；OnCreateClient方法在框架执行OnCreate方法时自动调用，不需要在程序中再次调用该方法。

13.6.2　框架的初始化

在13.2.3节中，介绍了框架对象最终是通过调用CreateWindowEx的API函数创建框架窗口的，该函数在创建窗口时发送WM_CREATE消息，导致调用CFrameWnd的OnCreate方法。下面从CFrameWnd的OnCreate方法开始讨论框架窗口是如何被初始化的。

在OnCreate方法中获取了文档、视图的类型信息，记录在pContext中，然后调用OnCreateHelper方法，代码如下。

在OnCreateHelper方法中，首先调用基类的OnCreate方法，然后调用OnCreateClient方法创建视图窗口，最后调用RecalcLayout方法排列子窗口大小。

13.6.3　命令消息处理

在MFC中，命令消息的处理就不像系统消息那么单纯了。以文档／视图应用程序为例，当用户选择某个菜单项时，会执行框架窗口的OnCmdMsg方法。

在OnCmdMsg方法中首先调用了视图对象的OnCmdMsg方法。也就是说，视图会最先处理框架的命令消息，如果没有处理，消息会继续向下传递。分析视图的OnCmdMsg方法如下。

在视图的命令消息中，首先会搜索视图本身的消息映射表，如果没有发现处理函数，则调用文档对象的OnCmdMsg方法。

如果文档没有处理命令消息，会执行CWnd::OnCmdMsg语句，搜索框架本身的消息映射表。如果没有搜索到命令消息的处理方法，会执行pApp->OnCmdMsg语句，调用应用程序的OnCmdMsg方法。图13.6显示了命令消息的处理流程。

图13.6　消息流程图

13.7　文档／视图的应用

通过前面的讲解可知，在文档／视图结构中，数据保存在文档中，视图是用于显示数据的。本书通过一个具体实例来介绍文档／视图结构的应用。

本实例首先要把数据保存在文档中，以便使用文档进行文件读写。这样视图使用数据时，就要先从文档中去取，即获取文档指针GetDocument()，有了文档指针，方可操作文档中的数据。如果文档要访问视图中的数据，就要用到视图指针，GetFirstViewPosition和GetNextView可以获取视图指针。如果文档只是想更新视图的显示，而不需要访问视图中的具体数据，这时只要调用UpdateAllViews即可。

13.7.1　实例说明

实例功能很简单，只是画一些直线，即按住鼠标按键，确定直线起始点；释放鼠标按键，确定直线终止点，并画一条直线。这部分功能在视图类中实现，需要用到视图类的LBUTTONDOWN、LBUTTONUP事件和OnDraw方法。画线用到的参数除了两点坐标，还有颜色、粗细和线的样式（虚线或实线），这些参数通过工具栏输入。所有参数定义为一个结构体类型，保存在结构体中，多条直线则用一个链表来表示。MFC中的CObList类实现了对链表的封装，这样可以把所有数据用一个链表表示，它作为文档类的成员存在。视图每产生一条直线时，都将直线保存在文档类的链表中。

在文档类中，用序列化方法（Serialize）保存数据。它直接使用默认工具栏上的打开、保存按钮。Carchive对象的IsStoring()表示正在执行保存操作，否则就是打开操作。这部分内容可以参考第15章文件部分。

程序运行效果如图13.7所示。

图13.7　画线程序的运行效果

13.7.2　实例实现

【例13.1】文档视图的应用。（实例位置：资源包\TM\sl\13\1）

具体操作步骤如下：

（1）创建一个单文档视图应用程序，工程文件名为xa。

（2）定义一个直线结构体类型。为使用方便，该结构体定义在文档类的头文件中（XaDoc.h）。

（3）在文档类中定义一个链表类型成员，用于保存以上结构体类型数据。

（4）在视图类中定义一个直线对象l，保存正在绘制的那条直线的信息。还有一个逻辑值isFinished，表示一条直线是否已经画完，该值释放鼠标按键时为true，按下鼠标按键时为false。

（5）在视图类的构造函数中完成第一条直线颜色、粗细和样式的初始化。

（6）通过类向导为视图类的LBUTTONDOWN消息增加消息响应函数。

（7）通过类向导为视图类的LBUTTONUP消息增加消息响应函数。

（8）在视图类的OnDraw方法中完成绘图。

（9）在类视图中打开工具栏，增加新的工具按钮，如图13.8所示。

图13.8　新增工具栏

其中，4个颜色按钮、4个粗细按钮和2个样式（虚实）按钮是新增加的。

（10）通过类向导为10个按钮增加COMMAND消息响应函数。

红色按钮的消息响应函数如下，其他3种颜色类似。

工具栏中1个像素按钮的消息响应函数如下，其他3种类似。

实线的消息响应函数如下。虚线的样式值为PS_DASHDOT。

（11）工具栏按钮的当前选中状态，有时希望在按钮上显示出来。例如，当前线条颜色是红色，希望红色按钮处于按下的状态，其他两组也是这样。

可以通过按钮的SetCheck方法来实现，选中时用SetCheck(true)，未选中时用SetCheck(false)。程序可以写在对应按钮的UPDATE_COMMAND_UI事件中，并为每个按钮增加一个事件处理函数。以红色为例，代码如下。

（12）文档类中的文件打开、保存操作在文档类的序列化函数（Serialize）中实现。

13.8　术语

（1）文档／视图结构：文档／视图结构是MFC应用程序的重要组成部分。文档是数据的载体，负责数据的存储、管理和维护；视图是一个子窗口，负责显示文档中的数据。

（2）文档类：负责内存数据与磁盘之间的交互，最重要的是包括3项：OnOpenDocument（读入）、OnSaveDocument（写盘）和Serialize（读写）。

（3）视图类：负责内存数据与用户的交互，包括数据的显示、用户操作的响应（如菜单的选取、鼠标的响应）。最重要的是OnDraw（重画窗口），通常用CWnd::Invalidate()来启动。另外，它通过消息映射表处理菜单、工具条、快捷键和其他用户消息。

（4）框架类：框架是视图的容器，其中可以包含工具栏、菜单栏和状态栏等。

13.9　小结

通过本章的学习，读者可以初步了解文档／视图应用程序的构建及使用，理解文档和视图之间的交互方法，并能设计出简单的文档／视图结构应用程序。

13.10　实践与练习

设计一个文档／视图结构程序，在工具栏中增加一个字体按钮，单击此按钮，调用字体对话框设置字体，改变视图区显示的字体。（答案位置：资源包\TM\sl\13\2）

第3篇

高级应用

第14章　打印技术

第15章　文件与注册表操作

第16章　ADO编程

第17章　动态链接库

第18章　多线程程序设计

第19章　网络套接字编程

本篇主要介绍打印技术、文件与注册表操作、ADO编程、动态链接库、多线程程序设计和网络套接字编程等内容。学习完本篇，读者将能够开发数据库应用程序、多线程程序和网络程序等。

第14章

打印技术

（视频讲解：26分钟）

打印机是计算机常用的外设之一。由于人们在工作和学习中经常需要打印文件，因此在编写应用程序时就需要加入打印功能。使用Visual C++开发的程序可以通过两种方法实现打印：一种是使用文档／视图结构中封装的打印功能；另一种是在对话框程序中通过获得打印机设备上下文来绘制要打印的文件。本章将通过大量实例，详细介绍在程序中如何编辑打印功能。

通过阅读本章，您可以：

了解如何获取打印机设备上下文

掌握文档／视图应用程序的打印

掌握对话框应用程序的打印

掌握“打印设置”对话框的使用

掌握“页面设置”对话框的使用

掌握如何修改文档／视图结构的打印预览

14.1　打印基础

在进行打印编程之前，首先要了解一些打印的基础知识。其中，设置映射模式是必不可少的，掌握映射模式之后还要获取打印机设备上下文才能进行打印操作。

14.1.1　映射模式

要在屏幕上绘制图像或文字，使用GDI对象可以很容易实现。但将这些图形和文字打印出来，能否与屏幕的效果相同呢？这取决于映射模式。映射模式反映了逻辑设备单位与实际物理坐标单位之间的对应转换关系。映射模式可以通过设备环境类的一个成员函数SetMapMode来设置。

语法格式如下：

其中，nMapMode表示Windows提供的映射模式，可选值如表14.1所示。

表14.1　Windows映射模式

注意

y轴正方向的映射模式在不改变原点时，因为逻辑坐标y值向上增长，所以视口中的逻辑坐标值都是负的，绘制图形时很容易将图形绘制到屏幕上方。

14.1.2　获取打印机设备上下文

打印机的设备上下文可以通过“打印”对话框来获取。首先创建一个“打印”对话框，然后调用GetPrinterDC方法获取打印机设备上下文。

【例14.1】获取打印机设备上下文。

14.2　文档／视图应用程序打印

在MFC的文档／视图结构中内置了功能强大的打印和打印预览功能，极大地简化了用户对打印参数的编辑。用户在编写打印程序时，需要重载OnDraw函数，通过该函数可以将图像输出到屏幕上。在打印时，用户选择“文件”／“打印”命令，窗口将把打印机的设备上下文传递给OnDraw函数，从而实现在打印机上绘制图像。

在OnDraw函数中调用IsPrinting函数，该函数用于确定正在使用的设备上下文是否用于打印。

语法格式如下：

返回值：使用打印机设备上下文时返回非零值。

【例14.2】文档／视图应用程序打印。（实例位置：资源包\TM\sl\14\1）

具体操作步骤如下。

（1）创建一个基于文档／视图结构的应用程序。

（2）在视图类的头文件中声明变量，代码如下。

（3）添加PrintText函数，在该函数中设置打印文本。

（4）重载OnBeginPrinting函数，在该函数中获取打印机每英寸像素数，并计算打印机与屏幕的像素比率，代码如下。

注意

打印机的打印分辨率与计算机的屏幕分辨率是不同的。为了将窗口中的信息原样输出到打印机中，需要处理计算打印分辨率与屏幕分辨率的比率。

（5）重载OnDraw函数，在应用程序窗口中绘制打印文本，代码如下。

运行程序，结果如图14.1所示。

图14.1　文档／视图应用程序打印

14.3　对话框应用程序打印

在基于对话框的应用程序中并没有封装打印功能，要进行打印只能调用“打印”对话框或者使用具有打印功能的函数。本节将介绍在对话框程序中如何进行打印。

14.3.1　打印对话框中的表格

使用Visual C++编写的很多程序都是基于对话框的应用程序，如果要编写打印功能，可以通过调用Windows的“打印”对话框来实现。“打印”对话框的创建和常用方法参见第6.7.5节。

【例14.3】打印对话框中的表格。（实例位置：资源包\TM\sl\14\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“打印对话框中的表格”。

（2）向对话框中添加一个列表视图控件和一个按钮控件，为列表视图控件关联变量m_List。

（3）在对话框的OnInitDialog函数中设置列表视图的扩展风格，并向列表视图中插入数据，代码如下。

注意

本实例使用了数据库，请参照第16章的内容进行学习。

（4）处理“打印”按钮的单击事件，调用“打印”对话框打印列表视图中的数据，代码如下。

运行程序，结果如图14.2所示。

图14.2　打印对话框中的表格

14.3.2　打印磁盘中的文件

打印磁盘中的文件可以使用API函数ShellExecute来实现。

语法格式如下：

参数说明：

hwnd：窗口句柄。

lpOperation：执行的操作，包括open、print和explore。

lpFile：文件路径。

lpParameters：执行操作的参数。

lpDirectory：指定默认目录。

nShowCmd：是否显示。

【例14.4】打印磁盘中的文件。（实例位置：资源包\TM\sl\14\3）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“打印磁盘中的文件”。

（2）向对话框中添加一个群组控件、一个静态文本控件和两个按钮控件。

（3）处理“选择文件”按钮的单击事件，获取选择文件的完整路径，并通过静态文本控件显示出来，代码如下。

（4）处理“打印”按钮的单击事件，在该事件中调用ShellExecute函数实现打印文件的功能，代码如下。

运行程序，结果如图14.3所示。

图14.3　打印磁盘中的文件

说明

外壳函数ShellExecute不仅可以实现打印磁盘文件的功能，还可以直接运行磁盘中的可执行文件。

14.4　打印设置

编写打印程序之所以复杂，在于其处理的信息比较复杂，所以要想编写一个好的打印程序，掌握打印相关的设置是必不可少的。本节通过“打印设置”和“页面设置”对话框来介绍打印设置。

14.4.1　设置打印方向

“打印设置”对话框也是使用CPrintDialog类创建的，将bPrintSetupOnly参数设置为TRUE，即可创建“打印设置”对话框。然后调用GetDevMode方法即可获取包含打印机的初始化信息和环境信息的DEVMODE结构指针。DEVMODE结构中的成员dmOrientation用于保存打印方向，可选值有两个，即为DMORIENT_LANDSCAPE值时是横向打印，为DMORIENT_PORTRAIT值时是纵向打印。

【例14.5】使用“打印设置”对话框设置打印方向。（实例位置：资源包\TM\sl\14\4）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用‘打印设置’对话框设置打印方向”。

（2）在工作区窗口中选择RecourceView选项卡，导入3个图标资源。

（3）在对话框头文件中声明变量，代码如下。

（4）在对话框的OnInitDialog函数中创建工具栏并设置打印文本，代码如下。

（5）处理WM_CTLCOLOR消息，在该消息中将对话框的背景颜色改为白色。

说明

在WM_CTLCOLOR消息的处理函数中创建一个白色的画刷，然后使用画刷填充窗体的客户区域。

（6）定义一个自定义函数DrawText，用于绘制打印和预览的文本，代码如下。

（7）处理工具栏中“打印设置”按钮的单击事件，判断获取打印方向，代码如下。

（8）处理工具栏中“打印”按钮的单击事件，调用“打印”对话框进行打印，代码如下。

运行程序结果如图14.4所示。

图14.4　使用“打印设置”对话框设置打印方向

说明

在设置横向打印时，也可以不使用“打印设置”对话框，而直接使用代码进行打印方向的控制。

14.4.2　设置打印页面

通过“页面设置”对话框可以进行设置纸张、页边距、打印方向等操作。可以通过CPageSetupDialog类的构造函数来创建“页码设置”对话框。

语法格式如下：

参数说明：

dwFlags：定制对话框的标志。

pParentWnd：指向对话框父窗口的指针。

设置“页面设置”对话框后，可以调用GetMargins来获取打印机当前的页边距设置。

语法格式如下：

参数说明：

lpRectMargins：当前打印机的打印边距。

lpRectMinMargins：当前打印机的最小打印边距。

【例14.6】使用“页面设置”对话框设置打印页面。（实例位置：资源包\TM\sl\14\5）

具体操作步骤如下。

（1）创建一个基于文档／视图结构的应用程序。

（2）打开菜单资源编辑器，在菜单资源中添加一个“页面设置”菜单项，如图14.5所示。

图14.5　菜单资源

（3）在视图类的头文件中声明变量，代码如下。

（4）通过类向导为“页面设置”菜单项处理单击事件，在该事件的处理函数中调用“页面设置”对话框，代码如下。

（5）重载OnBeginPrinting函数，在该函数中获取打印机每英寸像素数，并计算打印机与屏幕的像素比率，代码如下。

注意

在默认情况下，OnBeginPrinting函数的参数pDC是被注释的，用户需要手动删除两边的注释符“／*”和“*／”。

（6）重载OnDraw函数，在应用程序窗口中绘制打印文本，打印时在指定页面位置绘制文本，代码如下。

运行程序，结果如图14.6所示。

图14.6　默认设置时预览

（7）选择“页面设置”命令，在打开的“页面设置”对话框中设置页边距，结果如图14.7所示。

图14.7　设置页边距

14.5　修改打印预览

文档／视图结构默认的打印预览是可以修改的，下面通过CPreviewView类的派生类CPreView来修改文档／视图结构默认的打印预览的按钮，使打印预览的界面看起来更美观。调用DoPrintPreview函数可以修改工具栏。

语法格式如下：

DoPrintPreview方法中的参数说明如表14.2所示。

表14.2　DoPrintPreview方法中的参数说明

【例14.7】修改文档／视图结构默认的打印预览。（实例位置：资源包\TM\sl\14\6）

具体操作步骤如下。

（1）创建一个基于文档／视图结构的应用程序。

（2）在工作区窗口中选择RecourceView选项卡，导入4个图标资源。

（3）新建一个对话框资源，将ID改为IDD_TOOLBAR_DIALOG，设置Style属性为Child，Border属性为None。向对话框中添加4个按钮控件，分别设置ID属性为AFX_ID_PREVIEW_PRINT、AFX_ID_PREVIEW_PREV、AFX_ID_PREVIEW_NEXT和AFX_ID_PREVIEW_CLOSE，为4个按钮选择Owner draw属性，使按钮允许自绘。对话框资源如图14.8所示。

图14.8　对话框资源

（4）以CButton类为基类派生一个CIconBtn类，该类的创建过程参见第11.2.1节。

（5）以CPreviewView类为基类派生一个CPreView类。

注意

在该类的头文件中引用afxpriv.h头文件和IconBtn.h头文件，目的是可以访问CPreviewView类和CIconBtn类。

（6）在CPreView类头文件中声明变量，代码如下。

（7）添加WM_CREATE消息的处理函数，将4个CIconBtn类对象关联到对话框中的按钮资源，代码如下。

（8）在CPrintPreviewView类中为ID_FILE_PRINT_PREVIEW消息添加消息处理函数，代码如下。

（9）在视图类的头文件中声明变量，代码如下。

（10）添加PrintText函数，在该函数中设置打印文本。

说明

在PrintText函数中，要给str数组中的元素赋值。

（11）重载OnBeginPrinting函数，在该函数中获取打印机每英寸像素数，并计算打印机与屏幕的像素比率，代码如下。

（12）重载OnDraw函数，在应用程序窗口中绘制打印文本，代码如下。

运行程序，结果如图14.9所示。

图14.9　修改文档／视图结构默认的打印预览

14.6　小结

本章介绍了Windows提供的映射模式，并通过实例重点讲解了文档／视图应用程序的打印和对话框应用程序的打印，以及通过“打印设置”和“页面设置”对话框进行打印设置等，读者从中可以由浅入深地逐步了解有关打印编程的知识，为以后设计打印“打印即所得”程序打下坚实的基础。

14.7　实践与练习

1. 设计一个实例，实现个人简历的打印。（答案位置：资源包\TM\sl\14\7）

2. 设计一个实例，打印一份简单的报表，结果如图14.10和图14.11所示。（答案位置：资源包\TM\sl\14\8）

图14.10　简单报表

图14.11　打印预览

第15章

文件与注册表操作

（视频讲解：40分钟）

在开发程序时，经常需要对文件和注册表进行操作。例如，从文件或注册表中加载程序的初始化信息，或将程序中的数据导出到磁盘文件或注册表中，以持久地保存这些数据。本章将介绍有关文件与注册表的相关知识。

通过阅读本章，您可以：

掌握如何使用C函数对文件进行操作

掌握如何使用CFile类对文件进行操作

掌握如何查找文件

掌握如何设计串行化类

掌握如何读写INI文件

掌握如何读写注册表

15.1　普通文件操作

文件操作主要包括文件的读写、创建、删除、查找等操作。本节将从C语言和MFC类库的角度介绍如何对文件进行操作。

15.1.1　应用C函数进行文件操作

文件操作是进行程序开发的基本操作，因此程序设计语言都提供有读写文件的函数。下面介绍一些C语言中有关文件操作的函数。

1. fopen

fopen函数用于打开一个文件。

语法格式如下：

参数说明：

filename：打开的文件名称。

mode：文件打开模式，可选值如表15.1所示。

表15.1　文件打开模式

返回值：函数返回一个关联文件的指针。

注意

在文件的打开模式中，有些模式可以组合在一起使用。

2. fclose

fclose函数用于关闭打开的文件。

语法格式如下：

其中，stream表示之前打开的文件指针。

3. fflush

fflush函数用于清理文件缓冲区。

语法格式如下：

其中，stream表示之前打开的文件指针。如果文件关联的流是输出流（从内存中读取数据表示输出），则函数写缓冲区中的数据到文件；如果文件关联的流是输入流，函数将清空缓冲区的内容。

4. fread

fread函数从文件中读取数据。

语法格式如下：

参数说明：

buffer：存储数据的缓冲区。

size：读取数据单位的大小。

count：读取的单位数量，即读取多少个单位。

stream：之前打开的文件指针。

返回值：函数返回实际读取的单位数量。

5. fwrite

fwrite函数用于向文件中写入数据。

语法格式如下：

参数说明：

buffer：被写入数据的缓冲区。

size：写入数据单位的大小。

count：写入数据的最大单位数量。

stream：之前打开的文件指针。

返回值：函数返回实际写入的单位数量。

6. fgetc

fgetc函数从文件中读取一个字符。

语法格式如下：

其中，stream表示之前打开的文件指针。

返回值：函数返回一个以整数值表示的字符，如果有错误发生或者文件指针到达文件尾，EOF被返回。

7. fputc

fputc函数用于向文件中写入一个字符。

语法格式如下：

参数说明：

c：以整数表示被写入的字符。

stream：之前打开的文件指针。

说明

在读取和写入文件时，fread、fwrite比较常用，而fgetc、fputc使用得较少。

8. feof

feof函数用于判断文件是否到达文件尾。

语法格式如下：

其中，stream表示之前打开的文件指针。

返回值：如果文件指针到达了文件尾，函数返回非零值，否则返回0。

9. fseek

fseek函数用于将文件指针设置在指定的位置。

语法格式如下：

参数说明：

stream：之前打开的文件指针。

offset：基于origin位置偏移的数量。

origin：初始位置，函数将从该点开始移动文件指针。当为SEEK_CUR，表示当前位置；为SEEK_END，表示文件尾；为SEEK_SET，表示文件开始位置。

下面通过一个实例来演示如何使用C函数进行文件操作。实例将以文本的形式写入和读取数据。

【例15.1】以文本的形式写入和读取数据。（实例位置：资源包\TM\sl\15\1）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为WriteReadFile。

（2）处理“写入”按钮的单击事件，向文件中写入数据，代码如下。

（3）处理“读取”按钮的单击事件，从文件中读取数据，代码如下。

（4）运行程序，向文件中写入数据，并读取数据，如图15.1所示。

图15.1　读取／写入文件

15.1.2　使用CFile类进行文件操作

为了方便对文件进行操作，MFC类库提供了一个CFile类，该类封装了对文件的各种操作。使用CFile类，用户可以非常容易地对文件进行操作。CFile类的主要方法介绍如下。

1. Open

Open方法用于打开一个文件。

语法格式如下：

参数说明：

lpszFileName：要打开的文件名。可以包含完整路径，也可以是相对的文件名。

nOpenFlags：文件打开标记，可选值如表15.2所示。

表15.2　CFile打开标记

pError：一个异常的指针。一般情况下可以使用NULL指针，这个指针在打开文件过程中如果产生错误，Open将抛出一个CFileException异常，而不是返回FALSE。

2. Close

Close方法用于关闭打开的文件。

语法格式如下：

3. Read

Read方法用于从文件中读取数据到缓冲区中。

语法格式如下：

参数说明：

lpBuf：接收数据的缓冲区。

nCount：从文件中读取数据的最大数量。

返回值：函数返回实际读取的字节数。

4. ReadHuge

ReadHuge方法从文件中读取数据到缓冲区，主要用于大文件的读取。

语法格式如下：

参数说明：

lpBuffer：接收数据的缓冲区。

dwCount：从文件中读取数据的最大数量。

返回值：函数返回实际读取的字节数。

5. Write

Write方法用于从缓冲区写入数据到文件中。

语法格式如下：

参数说明：

lpBuf：表示待写入数据的缓冲区。

nCount：表示向文件中写入数据的数量。

6. WriteHuge

WriteHuge方法用于从缓冲区中写入数据到文件中，主要用于较大数据量数据的写入。

语法格式如下：

参数说明：

lpBuf：待写入数据的缓冲区。

dwCount：向文件中写入数据的数量。

注意

平常在读写文件时，使用Read函数和Write函数即可。

7. Flush

Flush方法将文件缓冲区中的数据强制写入文件中。

语法格式如下：

8. Seek

Seek方法用于重新设置文件指针的位置。

语法格式如下：

参数说明：

lOff：文件指针移动的字节数。

nFrom：指针移动的起点。当为CFile::begin，表示从文件头开始，把指针向后移动lOff字节；为CFile::current，表示从当前位置开始，把指针向后移动lOff字节；为CFile::end，表示从文件尾开始，把指针向前移动lOff字节。

9. LockRange

LockRange方法用于锁定文件中指定区域的数据，以阻止其他进程对该区域数据的访问。

语法格式如下：

参数说明：

dwPos：从开始字节到被封锁的字节的偏移量。

dwCount：封锁的字节数。

10. UnlockRange

UnlockRange方法用于解锁文件中指定区域的数据，以允许其他进程对该区域数据的访问。

语法格式如下：

参数说明：

dwPos：从开始字节到被解锁的字节的偏移量。

dwCount：解锁的字节数。

11. Rename

Rename方法用于对文件进行重命名。

语法格式如下：

参数说明：

lpszOldName：原文件名。

lpszNewName：新文件名。

12. GetStatus

GetStatus方法用于获取文件的状态。

语法格式如下：

其中，rStatus是文件状态对象，其类型是CFileStatus。CFileStatus是一个存储状态信息的数据结构，其结构成员如表15.3所示。

表15.3　CFileStatus结构成员

注意

GetStatus是静态方法，调用时可以不创建CFile类对象而直接进行使用。例如，语句“CFile::GetStatus（name,status）；”，其中name是文件名字符串，status是CFileStatus的结构成员。

13. SetStatus

SetStatus方法用于设置文件的状态。

语法格式如下：

参数说明：

lpszFileName：文件的名称。可以是绝对的，也可以是相对的，但不能是网络路径。

status：CFileStatus结构指针，表示设置的文件状态。

14. GetLength

GetLength方法用于获取文件的长度。

语法格式如下：

下面通过一个实例来介绍如何使用CFile类进行文件操作。

【例15.2】使用CFile类进行文件操作。（实例位置：资源包\TM\sl\15\2）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为OperateFile。

（2）处理“写入”按钮的单击事件，向文件中写入数据，代码如下。

（3）处理“读取”按钮的单击事件，读取文件的内容和属性信息，代码如下。

（4）运行程序，结果如图15.2所示。

图15.2　使用CFile类操作文件

15.1.3　使用CFileFind类进行文件查找

MFC提供的CFile类封装了对文件的基本操作，但是没有提供文件查找的功能，而是单独提供了CFileFind类用于文件查找。下面介绍CFileFind类的主要方法。

1. MatchesMask

MatchesMask方法用于设置要查找文件的属性，即只查找符合指定文件属性的文件。

语法格式如下：

其中，dwMask用来指定文件属性。文件属性定义在WIN32_FIND_DATA结构中，其可选值如表15.4所示。

表15.4　文件属性

说明

在此介绍一下存档属性。存档属性又称为A属性（Archive），通常是提供给备份软件或备份命令使用的。当用户创建或修改一个文件时，存档属性自动被标识，以提示备份软件文档尚未备份，当备份后，存档属性自动取消。如果用户再次修改了文件，存档属性会再次标识。

2. GetFileName

GetFileName方法用于获取查找的文件名称，包含扩展名，不包含路径。

语法格式如下：

返回值：返回查找的文件名称。

3. GetFileTitle

GetFileTitle方法用于获取文件的名称，去除扩展名，不包含路径。

语法格式如下：

4. GetFilePath

GetFilePath方法用于获取文件的完整名称，包括扩展名和完整路径。

语法格式如下：

5. GetFileURL

GetFileURL方法用于获取已查找到文件的网络全路径，对查找本地文件没有实际意义，主要是为继承自CFileFind类的子类提供接口。

语法格式如下：

6. GetRoot

GetRoot方法用于获取文件所在的路径，不包含文件名。

语法格式如下：

7. GetCreationTime

GetCreationTime方法用于获取文件的创建时间。

语法格式如下：

其中，refTime表示一个CTime对象，记录文件的创建时间。

8. IsDots

IsDots方法用于判断文件夹是否为“. ”或“..”文件夹。“.”和“..”表示两类系统文件夹，“.”代表文件夹本身，“..”代表父文件夹。

语法格式如下：

返回值：如果查找到的是“. ”或“..”文件夹，则返回值为1，否则返回值为0。

9. FindFile

FindFile方法用于查找文件，即设置待查找文件的文件名称。

语法格式如下：

参数说明：

pstrName：设置待查找文件的文件名。要查找所有文件，可以将此名称设为“*. *”。

dwUnused：保留，以后用作多态的基类。

返回值：如果成功，返回非零值。

10. FindNextFile

FindNextFile方法用来查看当前已查找到的文件的下一个文件。此方法当待查找的文件不止一个时使用，即在调用FindFile和FindNextFile后返回值不是0的情况下使用。

语法格式如下：

返回值：如果成功，返回非零值。

下面通过一个实例介绍如何使用CFileFind类进行文件查找。

【例15.3】使用CFileFind类进行文件查找。（实例位置：资源包\TM\sl\15\3）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为FindFile，对话框资源设计窗口如图15.3所示。

（2）向对话框中添加EnumDisk方法，列举系统磁盘，代码如下。

注意

GetLogicalDriveStrings函数用于获取磁盘目录字符串。

（3）处理“查找”按钮的单击事件，查找文件，代码如下。

（4）运行程序，结果如图15.4所示。

图15.3　对话框资源设计窗口

图15.4　使用CFileFind类查找文件

15.2　串行化

对象数据的持久化过程被称为串行化，也被称为序列化。通常在程序中定义一个类对象，其数据在对象释放时会消失，串行化使得对象在释放时其数据可以永久地保存到磁盘文件中。用户也可以通过磁盘文件将对象恢复到原来的状态。本节将介绍有关串行化的相关知识。

15.2.1　串行化基础

在介绍串行化之前，先来了解一下与串行化有关的CArchive类。CArchive类简化了对文件的读写操作，它能够将对象的数据永久地保留到磁盘文件中，并能够从磁盘文件中恢复对象。在CArchive类中重载了C++的“<<”和“>>”运算符，其中“<<”用于将CObject类型或基本类型的数据存储到文件中，“>>”用于从文件中加载CObject或基本类型的数据。下面介绍CArchive类的主要方法。

1. CArchive

CArchive方法是CArchive类的构造函数。

语法格式如下：

参数说明：

pFile：文件对象指针，对象的数据将存储在该文件中。

nMode：表示对象是加载数据还是保存数据。当为CArchive::load，表示从存档对象中加载数据；为CArchive::store，表示存储对象到存档对象中；为CArchive::bNoFlushOnDelete，表示禁止存档对象在释放时自动调用Flush方法。

nBufSize：内部文件缓冲区大小，默认为4096个字节。

lpBuf：一个可选的指针，指向用户提供的nBufSize大小的缓冲区。如果未指定该参数，存档对象从应用程序的局部堆中分配缓冲区，并且在对象释放时释放缓冲区。如果用户指定了该缓冲区，在对象释放后需要手动释放该缓冲区。

2. Read

Read方法用于从存档对象中读取数据。

语法格式如下：

参数说明：

lpBuf：接收数据的缓冲区。

nMax：以字节为单位，表示读取数据的大小。

3. Write

Write方法用于向存档对象中写入数据。

语法格式如下：

参数说明：

lpBuf：写入数据的缓冲区。

nMax：以字节为单位，表示写入数据的大小。

4. IsLoading

IsLoading方法用于确定存档对象是否被加载数据。

语法格式如下：

5. IsStoring

IsStoring方法用于确定存档对象是否存储数据。

语法格式如下：

下面通过一个实例来演示如何使用CArchive类读写文件。

【例15.4】使用CArchive类读写文件。（实例位置：资源包\TM\sl\15\4）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为RWFile。

（2）处理“写入”按钮的单击事件，向文件中写入数据，代码如下。

注意

只有在调试状态下才能通过Debug Windows命令激活相应的调试窗口。

（3）处理“读取”按钮的单击事件，从文件中读取数据，代码如下。

（4）运行程序，结果如图15.5所示。

图15.5　使用CArchive类读写文件

15.2.2　编写串行化类

第15.2.1节介绍了使用CArchive类进行文件操作，本节将介绍使用CArchive类实现类的串行化。要设计一个串行化类，通常需要5个步骤。首先，用户定义的类必须派生于CObject或CObject的子类；然后重载Serialize方法，用以保存对象的数据成员到CArchive对象以及从CArchive对象载入对象的数据成员的状态；接着在类体中加入DECLARE_SERIAL宏，这是串行化类所必需的；然后为类提供一个无参数的构造函数，也就是默认构造函数；最后在类的源文件中加入IMPLEMENT_SERIAL宏。下面通过一个实例来介绍如何编写串行化类。

【例15.5】编写串行化类。（实例位置：资源包\TM\sl\15\5）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为Serialize。

（2）创建一个CSerialClass类，该类派生于CObject，头文件代码如下。

（3）编写CSerialClass类源文件代码，如下所示。

说明

“<<”符号用于写入，“>>”符号用于读取。

（4）处理对话框中“保存对象”按钮的单击事件，保存对象的数据成员，代码如下。

（5）处理“加载对象”按钮的单击事件，加载文件中的数据到对象的数据成员中，代码如下。

（6）运行程序，结果如图15.6所示。

图15.6　串行化

15.3　INI文件操作

INI文件多用于存储程序的初始化信息，如记录程序连接数据库的名称、上一次用户登录的名称、用户的注册信息等。本节将介绍有关INI文件操作的相关知识。

15.3.1　INI文件基本结构

在介绍操作INI文件之前，先来了解INI文件的基本结构。一个典型的INI文件主要由节名、键名和键值3部分构成，如图15.7所示。

图15.7　INI文件结构图

在INI文件中，节名由“[]”标识（其中的内容为节名），其后是键名，键名之后有一个等号，然后是键值。对于一个INI文件，可以有多个节，如图15.7所示的INI文件中包含了两个节。

注意

在INI文件中，键名与“=”之间和键值与“=”之间的空格数量可以是任意的，它们不会影响键名和键值。

15.3.2　读写INI文件

为了方便读写INI文件，系统提供了函数。下面分别进行介绍。

1. WritePrivateProfileString

WritePrivateProfileString函数用于向INI文件中写入一个字符串数据。

语法格式如下：

参数说明：

lpAppName：节名。如果INI文件中节名不存在，将创建一个节名。

lpKeyName：键名。如果该键名在所在的节中不存在，将创建一个键名。如果该参数为NULL，节及节下的所有项目都将被删除。

lpString：写入到键值中的数据。

lpFileName：INI文件的名称。

2. GetPrivateProfileString

GetPrivateProfileString函数用于获取INI文件中的字符串数据。

语法格式如下：

参数说明：

lpAppName：节名。如果该参数为NULL，函数将复制所有的节名到所标识的缓冲区中。

lpKeyName：键名。如果该参数为NULL，函数将lpAppName节下所有的键名复制到lpReturnedString缓冲区中。

lpDefault：默认值。

lpReturnedString：用于接收数据的缓冲区。

nSize：以字符为单位，表示lpReturnedString缓冲区的大小。

lpFileName：文件名称。

3. GetPrivateProfileInt

GetPrivateProfileInt函数用于从INI文件中获取整型数据。

语法格式如下：

参数说明：

lpAppName：节名。

lpKeyName：键名。

nDefault：默认值。

lpFileName：文件名称。

返回值：函数返回实际读取的整数值。

注意

如果节名为空，函数将复制所有节名到所标识的缓冲区中；如果键名为空，函数将把lpAppName节下所有的键名复制到缓冲区中。

4. GetPrivateProfileSectionNames

GetPrivateProfileSectionNames函数用于返回INI文件中的所有节名。

语法格式如下：

参数说明：

lpszReturnBuffer：接收节名的数据缓冲区。

nSize：缓冲区的大小。

lpFileName：INI文件名称。

5. GetPrivateProfileSection

GetPrivateProfileSection函数返回指定节下所有的键名和键值。

语法格式如下：

参数说明：

lpAppName：节名。

lpReturnedString：接收数据的缓冲区。

nSize：缓冲区的大小。

lpFileName：INI文件名称。下面举例介绍如何读写INI文件。

【例15.6】读写INI文件。（实例位置：资源包\TM\sl\15\6）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为OperateIniFile，对话框资源设计窗口如图15.8所示。

（2）处理“确定”按钮的单击事件，向INI文件中写入数据，代码如下。

图15.8　对话框资源设计窗口

技巧

如果INI文件在当前程序的根目录下，可以直接设置为". /setting.ini"。

（3）处理“读取”按钮的单击事件，从INI文件中读取数据，代码如下。

（4）运行程序，结果如图15.9所示。

图15.9　操作INI文件

15.4　注册表操作

注册表（Registry）是Windows系统存储系统配置信息的数据库。在早期的Windows 3.x操作系统中，硬件设备信息和应用程序的配置信息都保存在INI文件中。随着应用程序的不断增加，INI文件变得越来越大，在INI文件中查找信息变得越来越慢。从Windows 95操作系统开始，引入了注册表的概念，将原来保存在INI文件中的数据转移到注册表中保存。本节将介绍有关注册表操作的相关知识。

15.4.1　使用API函数操作注册表

在操作注册表之前，先来介绍一下注册表的基本结构。注册表实际上是一个树结构，由根键、子键和项构成，其中项是子键下的数据，包括项的名称、数据类型和数据。为了查看注册表的信息，用户可以在运行窗口中输入regedit命令，打开“注册表编辑器”窗口，如图15.10所示。

图15.10　“注册表编辑器”窗口

为了方便用户读写注册表，系统提供了一组注册表函数。下面对主要函数进行介绍。

说明

有3种操作注册表的函数，两种是API函数，一种是MFC类库。API函数包括以SH开头的函数和以Reg开头的函数，其中最基础的是以Reg开头的API函数，也就是接下来要介绍的函数。

1. RegCreateKey

RegCreateKey函数用于打开指定的键，如果键不存在，则新建一个键或子键。

语法格式如下：

参考说明：

hKey：打开键的句柄。可以是系统预定义的值，如HKEY_CLASSES_ROOT、HKEY_CURRENT_USER和HKEY_LOCAL_MACHINE等。

lpSubKey：函数打开或创建的键名。

phkResult：函数返回的打开或创建键的句柄指针。

2. RegCloseKey

RegCloseKey函数用于关闭注册表中的键。

语法格式如下：

其中，hKey表示之前打开的键句柄。

3. RegCreateKeyEx

RegCreateKeyEx函数用于创建或打开注册表中的键。

语法格式如下：

参考说明：

hKey：父键句柄。可以是系统预定义的值，如HKEY_CLASSES_ROOT、HKEY_CURRENT_USER和HKEY_LOCAL_MACHINE等，也可以是由RegOpenKeyEx函数返回的句柄。

lpSubKey：函数打开或创建的键名。

Reserved：保留，必须为0。

lpClass：键的类型。

dwOptions：键的打开方式。为REG_OPTION_BACKUP_RESTORE，表示以备份或还原的方式打开键；为REG_OPTION_NON_VOLATILE，表示键信息在系统重启后保存到文件中，这是默认的设置；为REG_OPTION_VOLATILE，表示键信息保存在内存中，当系统关闭后这些信息将不被保存。

samDesired：访问权限。如果为KEY_ALL_ACCESS，表示具有全部的访问权限。

lpSecurityAttributes：安全属性信息，即子进程能否继承父进程中的该句柄。

phkResult：函数打开或创建的键句柄。

lpdwDisposition：用于返回函数执行的动作。如果为REG_CREATED_NEW_KEY，表示键不存在，函数将创建键信息；为REG_OPENED_EXISTING_KEY，表示键已存在，函数只是打开键。

4. RegDeleteKey

RegDeleteKey函数用于从注册表中删除某个子键。

语法格式如下：

参考说明：

hKey：当前打开的父键句柄。

lpSubKey：将要删除的子键名称。

5. RegOpenKey

RegOpenKey函数用于打开注册表中所标识的键。

语法格式如下：

参考说明：

hKey：当前打开的父键句柄。

lpSubKey：将要打开的子键名称。

phkResult：用于返回打开的子键句柄。

6. RegOpenKeyEx

RegOpenKeyEx函数用于打开注册表中所标识的键。

语法格式如下：

参考说明：

hKey：当前打开的父键句柄。

lpSubKey：将要打开的子键名称。

ulOptions：保留，必须为0。

samDesired：未使用，必须为0。

phkResult：用于返回打开的子键句柄。

7. RegDeleteValue

RegDeleteValue函数用于从注册表中移除指定键下的项。

语法格式如下：

参考说明：

hKey：当前打开的键句柄。

lpValueName：将要删除的项名称。

8. RegQueryValue

RegQueryValue函数用于获取注册表中指定键下的默认值。

语法格式如下：

参考说明：

hKey：当前打开的键句柄。

lpSubKey：子键名称。

lpValue：返回的数据。

lpcbValue：标识lpValue缓冲区的大小。

9. RegQueryValueEx

RegQueryValueEx函数用于获取注册表中指定键下某个项的值。

语法格式如下：

参考说明：

hKey：当前打开的键句柄。

lpValueName：项名称。

lpReserved：保留，必须为NULL。

lpType：一个指针，用于接收项的数据类型。

lpData：一个数据缓冲区，用于存储函数返回的数据。

lpcbData：lpData数据缓冲区的大小。

10. RegSetValue

RegSetValue函数用于设置关联键的默认值。

语法格式如下：

参考说明：

hKey：打开的父键句柄。

lpSubKey：子键名称。

dwType：数据的存储类型。

lpData：待设置的数据。

cbData：lpData缓冲区的大小。

11. RegSetValueEx

RegSetValueEx函数用于设置指定键下的项信息。

语法格式如下：

参考说明：

hKey：打开的键句柄。

lpValueName：项名称。

Reserved：保留，必须为0。

dwType：项的数据类型。

lpData：待设置的数据。

cbData：lpData缓冲区的大小。

下面通过一个实例来介绍如何应用注册表函数操作注册表。

【例15.7】应用注册表函数操作注册表。（实例位置：资源包\TM\sl\15\7）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为OperateReg，对话框资源设计窗口如图15.11所示。

图15.11　对话框资源设计窗口

（2）处理“确定”按钮的单击事件，向注册表中写入数据，代码如下。

说明

当RegSetValueEx函数的返回值为ERROR_SUCCESS时，表示设置成功。

（3）处理“读取”按钮的单击事件，从注册表中读取数据，代码如下。

（4）运行程序，结果如图15.12所示。

图15.12　注册表操作

15.4.2　基于CRegKey类操作注册表

在Visual C++中，为了简化注册表操作，MFC提供了一个CRegKey类。该类封装了对注册表的相关操作，用户可以非常方便地操作注册表。CRegKey类的主要方法介绍如下。

注意

在使用CRegKey类前，需要引用头文件atlbase.h。

1. Create

Create方法用于创建所标识的键。

语法格式如下：

参考说明：

hKeyParent：打开的父键句柄。

lpszKeyName：键名称。

lpszClass：键的类型。

dwOptions：键的打开方式。如果为REG_OPTION_BACKUP_RESTORE，表示以备份或还原的方式打开键；为REG_OPTION_NON_VOLATILE，表示键信息在系统重启后保存到文件中，这是默认的设置；为REG_OPTION_VOLATILE，表示键信息保存在内存中，系统关闭后这些信息将不被保存。

samDesired：访问权限。

lpSecAttr：键句柄的安全属性。

lpdwDisposition：用于返回函数执行的动作。如果为REG_CREATED_NEW_KEY，表示键不存在，函数将创建键信息；为REG_OPENED_EXISTING_KEY，表示键已存在，函数只能打开键。

2. Open

Open方法用于打开注册表键值。

语法格式如下：

参考说明：

hKeyParent：打开的父键句柄。

lpszKeyName：打开的键名称。

samDesired：键的访问权限。

3. SetValue

SetValue方法用于设置指定键下的项信息。

语法格式如下：

参考说明：

dwValue：设置的整数值。

lpszValueName：设置的项名称。

lpszValue：设置的字符串数据。

hKeyParent：父键句柄。

lpszKeyName：键名称。

lpszValue：设置的项数据。

lpszValueName：项名称。

4. QueryValue

QueryValue方法用于获取指定键下的数据。

语法格式如下：

参考说明：

pszValueName：查询的项名称。

pdwType：整型指针，用于返回项的数据类型。

pData：数据缓冲区，用于存储函数返回的数据。

pnBytes：数据缓冲区pData的大小。在函数返回后，该参数表示实际返回的数据大小。

dwValue：用于存储函数返回的整型数据。

lpszValueName：要查询的注册表项名称。

szValue：数据缓冲区，用于存储函数返回的字符串数据。

pdwCount：表示字符串数据的大小。

下面举例介绍如何应用CRegKey类对注册表进行操作。

【例15.8】应用CRegKey类对注册表进行操作。（实例位置：资源包\TM\sl\15\8）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为RWReg，对话框资源设计窗口如图15.13所示。

图15.13　对话框资源设计窗口

（2）在对话框的源文件中引用atlbase.h头文件，目的是使用CRegKey类。

（3）处理“写入”按钮的单击事件，将编辑框中的文本写入注册表中，代码如下。

注意

使用Create方法创建注册表键时，设置好父键句柄和键名以后，其他参数可以省略。

（4）处理“读取”按钮的单击事件，从注册表中读取数据，代码如下。

（5）运行程序，结果如图15.14所示。

图15.14　使用CRegKey类操作注册表

15.5　小结

本章介绍了普通文件的读写操作、类的串行化、INI文件的读写操作、注册表的读写操作等知识。对普通文件、INI文件及注册表的操作在程序开发中经常使用到，读者应该熟练掌握。类的串行化在程序开发中很少使用，读者了解即可。

15.6　实践与练习

1. 设计一个应用程序，获取文件的属性信息。（答案位置：资源包\TM\sl\15\9）

2. 设计一个应用程序，实现局域网中文件的复制功能。（答案位置：资源包\TM\sl\15\10）

3. 设计一个应用程序，实现文件和文件夹的重命名。（答案位置：资源包\TM\sl\15\11）

4. 设计一个应用程序，实现文件的查找。（答案位置：资源包\TM\sl\15\12）

第16章

ADO编程

（视频讲解：31分钟）

操作数据库时，ADO技术有着广泛的应用，已经逐步取代了ODBC和DAO等数据库操作技术。本章将介绍ADO的相关知识，其中详细讲解了ADO对象中常用的4种对象，使读者初步了解ADO对象，为日后开发数据库系统打下良好的基础。为了便于读者理解，在讲解过程中列举了ADO对象的应用。

通过阅读本章，您可以：

了解ADO

了解如何在Visual C++中应用ADO技术

掌握ADO对象

掌握ADO对象的封装

掌握使用ADO对象操作数据

掌握使用ADO对象检索数据

16.1　ADO编程基础

ADO（ActiveX Data Objects）是当前流行的数据库访问技术。要使用ADO进行编程，首先要对ADO有所了解，本节将对ADO进行简单的介绍。

16.1.1　ADO概述

ADO是Microsoft数据库应用程序开发的新接口，是建立在OLE DB底层技术之上的高层数据库访问技术。OLE DB是数据库底层接口，为各种数据源提供了高性能的访问；ADO封装了OLE DB所提供的接口，用户通过编写应用程序，可以访问和操作数据库服务器中的数据。ADO的优点在于使用简便、速度快、内存支出少和占用磁盘空间少，同时还具有远程数据服务功能，可以在一次往返过程中将数据从服务器移动到客户端程序，然后在客户端对数据进行处理，并将更新结果返回到服务器。此外，ADO还提供了多语言支持，除了面向Visual C++外，还提供了面向其他各种开发工具的应用。

16.1.2　在Visual C++中应用ADO技术

在Visual C++中使用ADO操作数据库有两种方法，一种是使用ActiveX控件，另一种是使用ADO对象。

1. ActiveX控件

使用ActiveX控件操作数据库相对简单，使用ActiveX控件绑定数据源即可对数据库进行操作。

2. ADO对象

使用ADO对象操作数据库虽然比使用ActiveX控件复杂一些，但是使用ADO对象具有更大的灵活性，将ADO对象封装到类中可以很好地简化对数据库的操作。

16.2　ADO对象

ADO中包含了7个对象，分别是ADO连接对象（Connection）、ADO记录集对象（Recordset）、ADO命令对象（Command）、ADO参数对象（Parameter）、ADO域对象（Field）、ADO错误对象（Error）和ADO属性对象（Property）。本节将对其中的4个对象进行介绍。

16.2.1　ADO连接对象

ADO连接对象用于连接数据源，并处理一些命令和事务。在使用ADO访问数据库之前，必须先创建一个ADO连接对象，然后才能通过该对象打开到数据库的连接。

1. ADO连接对象操作

使用ADO连接对象的集合、方法和属性可执行下列操作。

在打开到数据库的连接之前，需要使用ConnectionString、ConnectionTimeout和Mode属性配置连接。

使用CursorLocation属性指定支持批更新的“客户端游标提供者”的位置。

使用DefaultDatabase属性设置连接的默认数据库。

使用IsolationLevel属性设置在连接上打开的事务的隔离级别。

使用Provider属性指定OLE DB提供者。

使用Open方法建立到数据源的物理连接，使用Close方法中断连接。

使用Execute方法执行对连接的命令，并且可以使用CommandTimeout属性对执行的命令进行配置。

使用BeginTrans、CommitTrans和RollbackTrans方法以及Attributes属性管理打开的连接上的事务（如果提供者支持则包括嵌套的事务）。

使用Errors集合检查数据源返回的错误。

通过Version属性读取所使用的ADO执行版本。

使用OpenSchema方法获取数据库概要信息。

2. ADO连接对象属性

ADO连接对象的属性介绍如下。

Attributes：读／写，其值可以为以下值中的任意一个或多个。

AdXactCommitRetaining：执行保留的事务提交。

AdXactAbortRetaining：执行保留的事务终止。

CommandTimeout：该属性允许由于网络拥塞或服务器负载过重产生的延迟而取消Execute方法调用。指示在终止尝试和产生错误之前执行命令期间需等待的时间。

ConnectionString：该属性包含用来建立到数据源的连接的信息。通过传递包含一系列由分号分隔的argument = value语句的详细连接字符串可指定数据源。ADO支持以下7个argument语句。

Provider：指定连接所需的提供者名称。

Data Source：指定连接所需的数据源名称。

User ID：指定打开连接所需的用户名。

Password：指定打开连接所需的密码。

File Name：指定提供者描述文件名。

Remote Provider：指定打开客户端连接所需的提供者名称。

Remote Server：指定打开客户端连接所需的服务器名称。

ConnectionTimeout：由于网络堵塞或服务器负载过重导致的延迟使得必须放弃连接尝试时，使用该属性，指示在终止尝试和产生错误前建立连接期间所等待的时间。

CursorLocation：该属性允许在可用于提供者的各种游标库中进行选择，或返回游标引擎的位置。可选值如下。

adUseClient：使用客户端游标引擎。

adUseServer：使用数据提供者游标引擎。

DefaultDatabase：设置或返回指定Connection对象上默认数据库的名称。

IsolationLevel：该属性允许读／写，表示Connection对象的隔离级别。直到下次调用BeginTrans方法时，该设置才会生效。可选值如下。

adXactUnspecified：不使用指定的级别，级别不确定。

adXactChaos：不能覆盖更高级别事务中未提交的更改。

adXactBrowse：可以浏览其他事务中未提交的更改。

adXactCursorStability：可以浏览其他事务中已提交的更改。

adXactRepeatableRead：重新查询可以获得新的记录集。

adXactIsolated：事务管理与其他事务分隔。

Mode：可设置或返回当前连接上提供者正在使用的访问权限。Mode属性在关闭Connection对象时方可设置。可选值如下。

adModeUnknown：未知的权限。

adModeRead：只读权限。

adModeWrite：只写权限。

adModeReadWrite：读写权限。

adModeShareDenyRead：只读共享权限。

adModeShareDenyWrite：只写共享权限。

adModeShareExclusive：独占权限。

adModeShareDenyNone：无权限。

Provider：设置或返回连接提供者的名称。

State：指定对象的当前状态。该属性是只读的，可选值如下。

adStateClosed：默认，指示对象是关闭的。

adStateOpen：指示对象是打开的。

Version：ADO版本号。

3. ADO连接对象方法

ADO连接对象的方法介绍如下。

BeginTrans：开始一个新事务。

CommitTrans：保存所有更改并结束当前事务，也可以启动新事务。

RollbackTrans：取消当前事务中所做的任何更改并结束事务，也可以启动新事务。

Cancel：取消执行挂起的异步Execute或Open方法的调用。

Close：关闭打开的对象及任何相关对象。

Execute：执行指定的查询、SQL语句、存储过程或特定提供者的文本等内容。

Open：打开到数据源的连接。

OpenSchema：从提供者获取数据库纲要信息。

下面对ADO连接对象的常用方法进行详细介绍。

（1）Open方法

语法格式如下：

Open方法中的参数说明如表16.1所示。

表16.1　Open方法中的参数说明

（2）Execute方法

语法格式如下：

参数说明：

CommandText：指定要执行的命令文本。

RecordsAffected：返回受影响的记录数。

Options：指定命令类型。

说明

如果Execute方法执行的SQL语句要求有记录集返回，如执行Select查询语句，Execute方法则返回一个ADO记录集对象。

（3）Close方法

语法格式如下：

16.2.2　ADO记录集对象

ADO记录集对象可操作来自数据源的数据，通过Recordset对象可对几乎所有的数据进行操作。所有的Recordset对象均使用记录（行）和字段（列）进行构造。

1. ADO记录集对象操作

使用ADO记录集对象的集合、方法和属性可执行下列操作。

使用CursorType属性或Open方法指定游标类型。

使用Open方法打开记录集，使用Close方法关闭记录集。

使用AddNew、Update和Delete方法编辑记录。

2. ADO记录集对象属性

ADO记录集对象的属性介绍如下。

AbsolutePage：识别当前记录所在的页码，可以为1至所含页码的长整型值，或以下值。

adPosUnknown：记录集为空。

adPosBof：当前记录集指针在BOF。

adPosEof：当前记录集指针在EOF。

AbsolutePosition：根据其在Recordset中的序号位置移动记录，或确定当前记录的序号位置。

ActiveConnection：确定在其上将执行指定命令的对象或打开指定记录集的连接对象。

BOF：表示当前记录位置位于记录集对象的第一个记录之前。

EOF：表示当前记录位置位于记录集对象的最后一个记录之后。

Bookmark：保存当前记录的位置并随时返回到该记录。

CacheSize：控制提供者在缓存中所保存的记录数目，并可控制一次恢复到本地内存的记录数。

CursorLocation：允许在各种游标库中进行选择。可选值如下。

adUseClient：使用客户端游标。

adUseServer：使用提供者游标。

CursorType：指定打开记录集对象时应该使用的游标类型。可选值如下。

adOpenUnspecified：指出一个光标类型的不确定值，利用这个光标类型可以查询OLE DB允许什么样的光标存取。

adOpenForwardOnly：仅向前游标，除仅允许在记录中向前滚动外，其行为类似于动态游标。这样，当需要在记录集中单程移动时即可提高性能。

adOpenKeyset：键集游标。其行为类似于动态游标，不同的只是禁止查看其他用户添加的记录，并禁止访问其他用户删除的记录，其他用户所做的数据更改将依然可见。它始终支持书签，因此允许记录集中各种类型的移动。

adOpenDynamic：动态游标，用于查看其他用户所做的添加、更改和删除，并用于不依赖书签的记录集中各种类型的移动。如果提供者支持，可使用书签。

adOpenStatic：静态游标，提供记录集合的静态副本以查找数据或生成报告。它始终支持书签，因此允许记录集中各种类型的移动。其他用户所做的添加、更改或删除将不可见。这是打开客户端（ADOR）记录集对象时唯一允许使用的游标类型。

EditMode：确定当前记录的编辑状态。可选值如下。

adEditNone：无编辑操作。

adEditInProgress：已经被编辑，还未保存。

adEditAdd：已经调用AddNew方法，新记录还未保存到数据库中。

Filter：选择性地屏蔽记录集对象中的记录，已筛选的记录集将成为当前游标。可选值如下。

adFilterNone：移除当前过滤器，恢复记录视图。

adFilterPendingRecords：允许查看已更改但还未发送到服务器的记录。

adFilterAffectedRecords：允许查看受CancelBatch、Delete、Resync和UpdateBatch方法调用影响的记录。

adFilterFetchedRecords：允许查看当前缓冲区中的记录。

LockType：指定打开时提供者应该使用的锁定类型。可选值如下。

adLockUnspecified：未指定。

adLockReadOnly：只读记录集。

adLockPessimistic：数据在更新时锁定其他所有动作，这是最安全的锁定机制。

adLockOptimistic：只在实际的更新命令执行时才锁定记录，锁定状态维持时间最短，因此使用得最频繁。

adLockBatchOptimistic：在数据库执行批量操作时使用，是一种非常不安全的记录锁定方式，但是在应用程序需要处理许多记录时经常使用。

MarshalOptions：要被调度返回服务器的记录。可选值如下。

adMarshalAll：所有行的记录都可以返回给服务器。

adMarshalModifiedOnly：只有编辑过的行可以返回给服务器。

MaxRecords：对提供者从数据源返回的记录数加以限制。

PageCount：确定记录集对象中数据的页数。

PageSize：确定组成逻辑数据页的记录数。

RecordCount：确定记录集对象中记录的数目。

Sort：指定一个或多个以Recordset关键字（ASCENDING或DSCENDING）排序的字段名，并指定按升序还是降序对字段进行排序。

Source：记录集对象中数据的来源（命令对象、SQL语句、表的名称或存储过程）。

State：确定指定对象的当前状态。该属性是只读的。

Status：有关批更新或其他大量操作的当前记录的状态。

3. ADO记录集对象方法

ADO记录集对象的方法介绍如下。

AddNew：可创建和初始化新记录。

Cancel：取消执行挂起的异步Execute或Open方法的调用。

CancelBatch：可取消批更新模式下记录集中所有挂起的更新。

CancelUpdate：可取消对当前记录所作的任何更改或放弃新添加的记录。

Clone：从现有的记录集对象创建记录集对象的副本。可选择指定该副本为只读。

Delete：可标记记录集对象中的当前记录或一组记录以便删除。

Move：移动记录集对象中当前记录的位置。

MoveFirst：移动到记录集的第一条记录。

MoveLast：移动到记录集的最后一条记录。

MoveNext：将当前记录位置向前移动一条记录（向记录集的底部）。

MovePrevious：将当前记录位置向后移动一条记录（向记录集的顶部）。

NextRecordset：清除当前记录集对象并通过提前命令序列返回下一条记录集。

Open：可打开代表基本表、查询结果或者以前保存的记录集中记录的游标。

Requery：通过重新执行对象所基于的查询来更新记录集对象中的数据。

Resync：从当前行数据库刷新当前记录集对象中的数据。

Save：将记录集保存（持久）在文件中。

Supports：确定记录集对象所支持的功能类型。

Update：保存对记录集对象的当前记录所做的所有更改。

UpdateBatch：将所有挂起的批更新写入磁盘。

下面对ADO记录集对象的常用方法进行详细介绍。

（1）Open方法语法格式如下：

Open方法中的参数说明如表16.2所示。

表16.2　Open方法中的参数说明

（2）AddNew方法

语法格式如下：

参数说明：

FieldList：指定字段列表数组。

values：指定字段值数组。

（3）Update方法

语法格式如下：

参数说明：

Fields：指定字段数组。

values：指定字段值数组。

注意

Update方法不仅在修改记录集记录时使用，也可在向记录集中添加记录时使用。

（4）Delete方法

语法格式如下：

其中，AffectRecords是AffectEnum枚举类型值，指定受影响的记录数。

16.2.3　ADO命令对象

ADO命令对象用于执行传递给数据源的命令。

1. ADO命令对象操作

使用ADO命令对象的集合、方法和属性可执行下列操作。

使用CommandText属性定义命令的可执行文本。

通过参数对象和Parameters集合定义参数化查询或存储过程参数。

可使用Execute方法执行命令，并在适当的时候返回Recordset对象。

执行前使用CommandType属性指定命令类型，以优化性能。

使用Prepared属性决定提供者是否在执行前保存编译好的命令版本。

使用CommandTimeout属性设置提供者等待命令执行的秒数。

通过设置ActiveConnection属性，使打开的连接与命令对象关联。

设置Name属性，将命令标识为与连接对象关联的方法。

将命令对象传送给记录集的Source属性，以便获取数据。

2. ADO命令对象属性

ADO命令对象的属性介绍如下。

ActiveConnection：可确定在其上将执行指定命令对象或打开指定记录的连接对象。

CommandText：包含要发送给提供者的命令的文本。

CommandTimeout：在终止尝试和产生错误之前执行命令期间需等待的时间。

CommandType：命令对象的类型。可选值如下。

adCmdText：文本命令。

adCmdTable：数据表名。

adCmdStoredProc：存储过程。

adCmdUnknown：未知的。

Prepared：可使提供者在首次执行命令对象前保存CommandText属性中指定的已编译的查询版本。

State：可以确定指定对象的当前状态。该属性是只读的，返回值如下。

adStateClosed：默认，指示对象是关闭的。

adStateOpen：指示对象是打开的。

3. ADO命令对象方法

ADO命令对象的方法介绍如下。

Cancel：取消执行挂起的异步Execute或Open方法的调用。

CreateParameter：可用指定的名称、类型、方向、大小和值创建新的参数对象。

Execute：执行在CommandText属性中指定的查询、SQL语句或存储过程。

下面对ADO命令对象的常用方法进行详细介绍。

（1）CreateParameter方法

语法格式如下：

CreateParameter方法中的参数说明如表16.3所示。

表16.3　CreateParameter方法中的参数说明

（2）Execute方法

语法格式如下：

参数说明：

RecordsAffected：返回受影响的记录数。

Parameters：指定使用的具体参数。

Options：指定命令类型。

16.2.4　ADO参数对象

ADO参数对象代表参数或基于参数化的命令对象的参数信息。需要进行的操作在这些命令中只定义一次，但可以使用参数改变命令的细节。

1. ADO参数对象操作

使用ADO参数对象的集合、方法和属性可执行下列操作。

使用Name属性可设置或返回参数名称。

使用Value属性可设置或返回参数值。

使用Attributes、Direction、Precision、NumericScale、Size以及Type属性可设置或返回参数特性。

使用AppendChunk方法可将长整型二进制或字符数据传递给参数。

2. ADO参数对象属性

ADO参数对象的属性介绍如下。

Attributes：读／写。可选值如下。

AdParamSigned：默认值。指示该参数接受带符号的值。

AdParamNullable：指示该参数接受NULL值。

AdParamLong：指示该参数接受长二进制数据。

Direction：参数的传递方向。可选值如下。

adParamUnknown：指示参数方向为未知。

adParamInput：默认值。指示为输入参数。

adParamOutput：指示为输出参数。

adParamInputOutput：指示为输入和输出参数。

adParamReturnValue：指示为返回值。

Name：对象的名称。

NumericScale：可确定用于表明数字型参数或字段对象的值的小数位数。

Precision：可确定表示数字参数或字段对象值的最大位数。

Size：参数对象的最大值（按字节或字符）。

Type：指示参数对象、字段对象或属性对象的操作类型或数据类型，对参数对象是读／写，对其他所有对象Type属性是只读。

Value：可以设置或返回来自Field对象的数据、Parameter对象的参数值或者Property对象的属性设置。

3. ADO参数对象方法

AppendChunk方法：可将长二进制或字符数据填写到对象中。

语法格式如下：

其中，Data表示长二进制或字符数据。

说明

在使用操作数据库时，主要使用的是连接对象和记录集对象。

16.3　ADO对象应用

ADO对象应用是本章的重点，通过本节的介绍，读者可以了解ADO对象在程序中是如何应用的。

16.3.1　封装ADO对象

为了简化程序的操作，在使用ADO对象时可以将其封装到类中。这样做的好处是在程序的不同模块中操作数据库时，只要引用封装类的头文件，即可使用封装过的ADO对象。

在此笔者封装了一个ADO类，该类的主要功能是完成数据库和记录集的打开与关闭操作。ADO类的头文件如下：

接下来介绍各个成员函数是如何实现的。

（1）ADO类中的成员函数OnInitADOConn用于连接数据库，在该函数中调用ADO连接对象的Open方法，连接数据库，代码如下。

注意

在使用时，需要将加粗部分的“连接语句”换成实际连接数据库的字符串。如果用户不知道连接字符串如何设置，可以在桌面上创建一个文本文件，然后将其扩展名改为“.udl”，双击该文件，也可以弹出数据库连接属性对话框。在该对话框中设置连接的数据库，然后用记事本打开该文件，即可获得连接字符串。

（2）OpenRecordset成员函数用来打开记录集，在该函数中调用ADO记录集对象的Open方法打开记录集，代码如下。

（3）CloseRecordset成员函数用来关闭记录集，代码如下。

（4）CloseConn成员函数用来关闭数据库连接，代码如下。

（5）GetRecordCount成员函数用于获得记录集中记录的个数，代码如下。

注意

不要使用记录集对象的RecordCount属性来获取记录数，而要通过循环记录集中记录的方法进行统计。

16.3.2　使用ADO对象添加、修改、删除数据

本节将使用ADO对象对数据库中的数据进行添加、修改和删除等操作。首先创建一个ADO类，通过ADO类连接数据库，并打开记录集。

【例16.1】使用ADO对象添加、修改、删除数据。（实例位置：资源包\TM\sl\16\1）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用ADO对象添加、修改、删除数据”。

（2）向对话框中添加1个列表视图控件、3个静态文本控件、3个编辑框控件和4个按钮控件，并为控件关联变量。

（3）创建一个ADO类，参照第16.3.1节封装ADO对象。

（4）在StdAfx.h中导入ADO动态链接库，代码如下。

（5）在对话框的OnInitDialog函数中设置列表视图控件的扩展风格和列标题，代码如下。

（6）添加AddToGrid函数，向列表视图控件中插入数据，代码如下。

注意

在使用ADO类时，需要引用该类的头文件ADO.h。

（7）处理“添加”按钮的单击事件，将编辑框中的文本添加到数据库中，代码如下。

（8）处理列表视图控件的单击事件，在列表项被选中时，将列表项中的数据显示到编辑框中，代码如下。

（9）处理“修改”按钮的单击事件，根据编辑框中的数据修改数据库中的数据，代码如下。

注意

使用Move方法可以移动记录集指针，但是一定要计算好移动的位置，否则会发生错误。

（10）处理“删除”按钮的单击事件，删除列表框中被选中的列表项，代码如下。

注意

在使用Delete方法删除记录后，要调用Update方法更新记录集。

实例的运行结果如图16.1所示。

图16.1　使用ADO对象添加、修改、删除数据

16.3.3　使用ADO对象检索数据

通过ADO记录集对象可以检索数据。在ADO记录集对象中有一个Filter成员变量，该变量中存放着SQL语句中WHERE子句的条件字符串，查询时对记录集进行过滤，返回过滤后的结果。

【例16.2】使用ADO对象检索数据。（实例位置：资源包\TM\sl\16\2）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，将对话框的Caption属性修改为“使用ADO对象检索数据”。

（2）向对话框中添加一个列表视图控件、两个静态文本控件、一个组合框控件、一个编辑框控件和一个按钮控件，并为控件关联变量。

（3）创建一个ADO类，参照第16.3.1节封装ADO对象。

（4）在StdAfx.h中导入ADO动态链接库，代码如下。

（5）在对话框的OnInitDialog函数中设置列表视图控件的扩展风格以及列标题，并向列表视图控件中插入数据，代码如下。

（6）处理“检索”按钮的单击事件，在该事件中根据组合框和编辑框中的文本进行查询，将查询结果显示在列表中，代码如下。

注意

查询时除了设置查询条件外，还可以通过记录集的Sort属性对记录进行排序。

实例的运行结果如图16.2所示。

图16.2　使用ADO对象检索数据

16.4　小结

本章学习了ADO编程技术，并重点介绍了ADO连接对象、ADO记录集对象、ADO命令对象和ADO参数对象，同时为了使读者达到熟练掌握ADO对象的目的，加入了介绍ADO对象应用这一节，读者可以根据实例更好地熟悉ADO对象的使用，为日后开发数据库应用程序打下坚实的基础。

16.5　实践与练习

1. 设计一个实例，用ADO动态连接SQL Server数据库。（答案位置：资源包\TM\sl\16\3）

2. 设计一个实例，对Access数据库录入和提取图片。（答案位置：资源包\TM\sl\16\4）

第17章

动态链接库

（视频讲解：21分钟）

动态链接库（Dynamic Linkable Library，DLL）是Windows操作系统的基础和核心，在最初的Windows操作系统中就包含了DLL。与普通的应用程序类似，DLL中可以有数据和代码，也可以包含各种资源（如图标、位图、对话框等），但是DLL并不能单独运行，而是为其他DLL或应用程序提供函数、资源的服务。本章将介绍有关DLL的相关知识。

通过阅读本章，您可以：

了解动态链接库的优点

掌握如何创建动态链接库

掌握如何调用动态链接库中的函数

掌握如何调用动态链接库中的C++类和资源

掌握如何设计多国语言动态链接库资源

17.1　动态链接库基础

动态链接库通常用于封装一些功能函数，在不公开源码的情况下可以供其他应用程序使用，因此Windows的系统函数均是以动态链接库的形式提供的。采用动态链接库有何好处呢？本节就来解答这一问题。

17.1.1　动态链接库的特点

许多应用软件都有自己的动态链接库，其中通常是一些功能函数的实现。可是，为何不将函数的实现直接放入应用软件中，而单独放入动态链接库中呢？如果动态链接库丢失，将会导致应用程序无法运行，为什么还要使用动态链接库呢？使用动态链接库，自然有其好处。

增强应用程序的扩展性

如果厂家想要更改或扩展应用程序的功能，只需要让用户更换相应的动态链接库即可，应用程序本身可以丝毫不作改动。

能够用多种语言编写

动态链接库可以用Delphi、Visual Basic、Visual C++等多种语言来编写，这样就使不同的团队可以协同开发项目。例如，Visual Basic团队编写动态链接库，Visual C++团队设计应用程序，并调用Visual Basic团队编写的动态链接库。

节省内存

当有多个应用程序使用同一个动态链接库时，其页面只需在内存中加载一次，所有的应用程序即可共享其内存页面。

有助于资源共享

动态链接库中不仅可以包含函数，还可以包含如对话框、位图、图标和字符串等资源，其他应用程序也可以共享这些资源。

17.1.2　动态链接库的访问

当应用程序访问动态链接库时，需要加载动态链接库，并将动态链接库的文件镜像映射到进程的地址空间中。可以通过两种方式加载动态链接库，一种是静态加载，另一种是动态加载。动态链接库被成功加载后，其代码和数据会被映射到进程的地址空间中。此时应用程序如何访问动态链接库中的函数呢？通常在设计动态链接库时会提供一个头文件，其中描述了动态链接库输出的函数原型。在发布动态链接库时，该头文件会一起发布，用户程序根据该头文件可以知道动态链接库中函数的定义形式（函数原型），也可以调用动态链接库中的函数。

17.1.3　查看动态链接库

为了能够查看动态链接库信息，Visual C++提供了一个dumpbin工具。例如，查看动态链接库中的函数，用户可以通过观察动态链接库中的输出节来查看信息。下面以查看system32目录下的acledit.dll动态链接库为例，在命令窗口中输入“dumpbin -exports C:\WINDOWS\system32\acledit.dll”来获取acledit.dll的详细信息，如图17.1所示。

图17.1　查看DLL输出节

此外，用户也可以使用Visual C++提供的Depends工具来查看动态链接库的信息，如图17.2所示。

图17.2　查看DLL信息

说明

在右下角的列表中列出了动态链接库中函数的序号、函数功能号、函数名以及函数的映射地址。

17.2　创建和使用Win32动态链接库

前面介绍了动态链接库的特点、作用以及查看方式，本节将介绍如何在Visual C++中创建和使用Win32动态链接库。

17.2.1　创建动态链接库

Visual C++的工程窗口中提供了创建动态链接库的选项，用户可以选择该选项来创建一个动态链接库工程。

【例17.1】创建动态链接库。（实例位置：资源包\TM\sl\17\1）

具体操作步骤如下。

（1）选择File/New命令，打开New对话框，选择Projects选项卡，如图17.3所示。

（2）在列表框中选择Win32 Dynamic-Link Library选项，表示创建动态链接库工程，在Project name编辑框中输入工程名称，单击OK按钮进入向导对话框，如图17.4所示。

图17.3　New（新建工程）对话框

图17.4　DLL向导对话框

（3）在向导对话框中选中A simple DLL project单选按钮，单击Finish按钮完成工程的创建。

说明

选中A simple DLL project单选按钮，向导会生成以DllMain函数为主的程序代码及程序文件；选中An empty DLL project单选按钮，不会生成任何代码及文件；选中A DLL that exports some symbols单选按钮，生成的代码中会加入对C++的支持。

（4）向工程中添加一个头文件，名称为ShowDlg.h。

（5）在头文件ShowDlg.h中定义动态链接库的导出函数，代码如下。

为了将动态链接库中的函数导出，在导出函数前需要使用_declspec(dllexport)关键字。extern"C"用于防止C++编译器对函数名进行命名改编。由于C++语言支持函数重载，因此C++编译器在编译动态链接库时，会对导出函数进行命名改编。使用extern"C"有许多限制，例如，当函数的调用约定改变时，依然会发生命名改编的情况。稍后会介绍其解决方法。

（6）在动态库的源文件中定义ShowDialog函数，代码如下。

（7）运行程序，将生成一个动态链接库文件，扩展名为.dll。使用Depends工具打开生成的动态链接库，如图17.5所示。

图17.5　查看动态链接库

如果修改动态链接库中的ShowDialog方法，改变其默认的调用约定，如使用_stdcall调用约定，动态链接库中的函数将发生命名改编。修改ShowDlg.h头文件中的函数声明，代码如下。

修改动态链接库源文件中的函数定义，代码如下。

重新编译／运行程序，查看动态链接库中的函数信息，这时会发现函数名不再是ShowDialog，如图17.6所示。

图17.6　观察动态链接库

_stdcall是一种比较流行的函数调用约定，为了防止发生函数命名改编的情况，可以定义一个.def文件，其中加入EXPORTS节，设置导出函数名，如图17.7所示。

图17.7　设置.def文件

将设置的.def文件添加到动态链接库工程中，重新运行应用程序，发现函数没有发生命名改编，如图17.5所示。

17.2.2　调用动态链接库

在第17.2.1节中创建了一个动态链接库，本节将介绍如何访问动态链接库中的函数。前文已经介绍过，动态链接库可以有两种加载方式，分别为静态加载和动态加载。下面以动态加载为例介绍动态链接库函数的调用。

【例17.2】调用动态链接库。（实例位置：资源包\TM\sl\17\2）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为InvokeDll。

（2）定义一个函数指针类型，其定义与动态链接库中的函数原型相同，代码如下。

（3）处理“确定”按钮的单击事件，加载动态链接库，代码如下。

（4）运行程序，结果如图17.8所示。

动态加载动态链接库是通过使用LoadLibray函数实现的。下面介绍静态加载动态链接库的方法。静态（隐式）加载动态链接库通常需要提供3个文件，分别为动态链接库文件、动态链接库的头文件和LIB文件（编译动态链接库时生成的文件）。首先将动态链接库的头文件加载到当前的工程中，然后使用#pragma comment (lib,"ShowDlg.lib")语句链接LIB文件，最后直接访问动态链接库头文件中的函数。

图17.8　调用动态链接库的结果

17.2.3　向动态链接库中添加C++类和资源

动态链接库中不仅可以包含函数，还可以包含C++类和各种资源文件。本节将介绍如何向动态链接库中添加C++类和资源。

【例17.3】向动态链接库中添加C++类和资源。（实例位置：资源包\TM\sl\17\3）

具体操作步骤如下。

（1）选择File/New命令，打开New对话框，选择Projects选项卡，如图17.9所示。

（2）在列表框中选择MFC AppWizard [dll]选项，表示创建MFC动态链接库，在Project name编辑框中输入动态链接库的名称，单击OK按钮进入MFC应用程序向导对话框，如图17.10所示。

图17.9　New（新建工程）对话框

图17.10　MFC应用程序向导对话框

（3）保留默认的设置，单击Finish按钮完成工程的创建。在动态链接库的源文件中定义一个C++类，代码如下。

注意

为了能够让客户端访问动态链接库中CTextDlg类的方法，CTextDlg类方法必须是虚方法。

（4）定义两个全局函数，分别用于创建和释放一个CTextDlg类对象，代码如下。

（5）在工作区的资源视图窗口中导入一个位图，如图17.11所示。

图17.11　资源视图窗口

（6）在工作区的文件视图窗口中打开Resource.h文件，观察资源文件IDB_BACKGROUND对应的资源ID值，本例为2000，在第17.2.4节介绍访问动态链接库中的资源时，将会使用该资源ID值来加载位图资源。

（7）编译并运行程序，将生成DLL文件。

17.2.4　访问动态链接库中的C++类和资源

第17.2.3节介绍了如何向动态链接库中添加C++类和资源，本节将继续上面的内容，介绍如何访问动态链接库中的C++类和资源。

【例17.4】访问动态链接库中的C++类和资源。（实例位置：资源包\TM\sl\17\4）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为AccessDll，对话框资源设计窗口如图17.12所示。

（2）定义一个名称为CTextDlg的抽象类。因为需要访问动态链接库中的CTextDlg类，因此客户端需要定义一个框架，即抽象类CTextDlg，代码如下。

图17.12　对话框资源设计窗口

（3）定义两个函数指针，用于关联动态链接库中的全局函数，代码如下。

（4）处理“确定”按钮的单击事件，访问动态链接库中CTextDlg类的方法，并加载动态链接库中的位图资源，代码如下。

（5）运行程序，单击“确定”按钮，结果如图17.13所示。

图17.13　访问动态链接库中的C++类和资源

17.2.5　使用动态链接库设计多国语言的应用程序

在设计应用程序时，为了使应用程序适应多国语言，通常将程序中的资源放置在单独的动态链接库中，一个资源通常对应多个语言版本。例如，为了使应用程序能够在汉语和英语间切换，需要为每个资源提供汉语版的资源和英语版的资源。下面通过一个实例介绍如何向动态链接库中添加不同语言的资源。

【例17.5】向动态链接库中添加不同语言的资源。（实例位置：资源包\TM\sl\17\5）

具体操作步骤如下。

（1）创建一个MFC动态链接库，工程名称为LanguageRes。

（2）在工作区的资源视图窗口中插入一个对话框资源，如图17.14所示。

图17.14　对话框资源设计窗口

（3）在工作区的资源视图窗口中选中对话框的资源ID，按Alt+Enter快捷键打开属性窗口，如图17.15所示。

图17.15　对话框资源属性窗口

（4）在ID编辑框中设置对话框资源的ID，这里为IDD_CH_DIALOG；在Language组合框中选择资源的语言为Chinese [P.R.C.]。

（5）在工作区的资源视图窗口中选中对话框的资源ID，复制该对话框资源。按Alt+Enter快捷键打开属性窗口，设置新的对话框资源ID也为IDD_CH_DIALOG，在Language组合框中选择资源的语言为English[U.S.]。

（6）编译并运行应用程序，将生成DLL文件。

注意

因为设置了不同的语言环境，所以两个对话框资源ID可以相同。

在设计完动态链接库后，下面设计一个客户端应用程序，程序将利用动态链接库中的对话框资源设计一个包含汉语和英语两个版本的简单加法计算器。

【例17.6】设计汉语和英语两个版本的简单加法计算器。（实例位置：资源包\TM\sl\17\6）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为MultiLanguage，对话框资源设计窗口如图17.16所示。

图17.16　对话框资源设计窗口

（2）将动态链接库工程中Resource.h头文件中的资源ID复制到当前的工程Resource.h头文件中，代码如下。

（3）创建一个MFC类，名称为CCalculate，基类为CDialog，不要提供对话框的资源ID。

（4）向CCalculate类中提供3个成员变量，分别对应于动态链接库对话框资源中的3个编辑框，代码如下。

（5）在CCalculate类的DoDataExchange方法中映射编辑框的名称到编辑框的资源上，代码如下。

（6）向CCalculate类中添加OnCalculte方法，实现加法运算，代码如下。

（7）在CCalculate类的消息映射部分添加ON_BN_CLICKED映射宏，使OnCalculate方法与按钮的命令ID关联，代码如下。

（8）自定义一个窗口函数，用于作为对话框的临时窗口函数，代码如下。

（9）向对话框中添加CreateDlgFromDll方法，搜索动态链接库中的对话框资源，并根据对话框资源创建对话框窗口，代码如下。

注意

使用FindResourceEx函数查找对话框资源时，要根据对话框资源的语言环境进行区分。

（10）添加CCalculate类的析构函数，如果该类关联一个窗口，则在释放对象时销毁窗口，代码如下。

（11）在主对话框中定义两个CCalculate类对象，代码如下。

（12）处理汉语版按钮的单击事件，从动态链接库中加载汉语版的对话框资源，并根据该资源创建窗口，代码如下。

（13）处理英语版按钮的单击事件，从动态链接库中加载英语版的对话框资源，并根据该资源创建窗口，代码如下。

（14）运行程序，结果如图17.17和图17.18所示。

图17.17　汉语版对话框

图17.18　英语版对话框

17.3　小结

本章首先讲述了动态链接库的特点及其应用范围，然后介绍了各种动态链接库的设计和访问方法。通过本章的学习，读者应该掌握基本的动态链接库创建和访问。例如，在动态链接库中定义函数，在应用程序中访问动态链接库中的函数。

17.4　实践与练习

1. 利用动态链接库实现简单的界面换肤功能。（答案位置：资源包\TM\sl\17\7）

2. 使用动态链接库设计具有插件功能的应用程序。（答案位置：资源包\TM\sl\17\8）

3. 编写一个应用程序，获取应用程序使用的动态链接库。（答案位置：资源包\TM\sl\17\9）

第18章

多线程程序设计

（视频讲解：31分钟）

线程属于系统内核对象之一，描述的是代码的执行路径。一个应用程序开始运行时，系统将创建一个进程，同时创建一个主线程开始执行程序的代码。如果用户在程序中需要同时实现多个操作，如在执行一个费时操作时还能够随时响应界面操作，这就需要使用多线程来完成。本章将介绍多线程程序设计的相关知识。

通过阅读本章，您可以：

理解系统内核对象

理解进程和线程的概念

掌握如何创建多线程应用程序

掌握如何实现线程同步

18.1　线程概述

对于Windows编程的初学者来说，线程是一个抽象的概念，理解线程是比较困难的。本节将介绍与线程有关的基础知识，包括系统内核对象、进程和线程的概念及关系。

18.1.1　理解系统内核对象

线程是系统内核对象之一。在介绍线程之前，先来了解一下系统的内核对象。内核对象是系统内核分配的一个内存块，该内存块描述的是一个数据结构，其成员负责维护对象的各种信息。内核对象的数据只能由系统内核来访问，应用程序无法在内存中找到这些数据结构并直接改变它们的内容。

常用的系统内核对象有事件对象、文件对象、作业对象、互斥对象、管道对象、进程对象和线程对象等。不同类型的内核对象，其数据结构各不相同，但是内核对象有一些共有的属性。

引用计算属性：内核对象具有引用计数属性。内核对象在进程中被创建，但不被进程所拥有。创建一个内核对象时，内核对象的使用计数为1；其他进程访问该内核对象时，使用计数将递增1；当内核对象的使用计数为0时，系统内核将自动撤销该对象。这就意味着，如果在一个进程中创建了一个内核对象，在其他进程中访问了该内核对象，当创建内核对象的进程结束时，内核对象未必会释放，只有当内核对象的使用计数为0时才会被释放。

安全属性：内核对象还具有安全属性，由安全描述符来表示。安全描述符描述了谁创建了该内核对象，谁有权访问或使用内核对象，内核对象是否能够被继承等信息。

18.1.2　理解进程和线程

进程是一个正在运行的程序的实例，它也属于系统内核对象。进程提供了地址空间，其中包含有可执行程序和动态链接库的代码和数据，此外还提供了线程堆栈和进程堆空间等动态分配的空间。

从上述描述中可以发现，进程主要由两部分构成，即系统内核用于管理进程的进程内核对象和进程地址空间。那么进程是如何实现应用程序行为的呢？可以将进程简单理解为一个容器，它只是提供空间，执行程序的代码是由线程来实现的。线程存在于进程中，它负责执行进程地址空间中的代码。当一个进程创建时，系统会自动为其创建一个线程，该线程被称为主线程。在主线程中用户可以通过代码创建其他线程，当进程中的主线程结束时，进程也就结束了。

18.2　线程的创建

在熟悉了线程的基本概念后，接下来介绍线程的创建方法。在Visual C++中，用户可以通过多种方式来创建线程，本节将逐一介绍。

18.2.1　使用CreateThread函数创建线程

为了能够创建线程，系统提供了CreateThread API函数。

语法格式如下：

参数说明：

lpsa：线程的安全属性，可以为NULL。

cbStack：线程栈的最大大小，该参数可以被忽略。

lpStartAddr：线程函数，当线程运行时，将执行该函数。

lpvThreadParam：向线程函数传递的参数。

fdwCreate：线程创建的标记。为CREATE_SUSPENDED，表示线程创建后被立即挂起，只有在其后调用ResumeThread函数时才开始执行线程；为STACK_SIZE_PARAM_IS_A_RESERVATION，表示cbStack参数将不被忽略，使cbStack参数可表示线程栈的最大大小。

lpIDThread：一个整型指针，用于接收线程的ID。线程ID在系统范围内唯一标识线程，有些系统函数需要使用线程ID作为参数。如果该参数为NULL，表示线程ID不被返回。

返回值：如果函数执行成功，返回线程句柄，否则返回NULL。

注意

lpStartAddr参数中设置的线程函数原型为：“DWORD ThreadProc（LPVOID lpParameter）；”。

下面通过一个实例介绍如何使用CreateThread函数设计多线程应用程序。

【例18.1】使用CreateThread函数设计多线程应用程序。（实例位置：资源包\TM\sl\18\1）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为MultiThread，对话框资源设计窗口如图18.1所示。

图18.1　对话框资源设计窗口

（2）编写一个线程函数，实现简单的计数，代码如下。

（3）处理“确定”按钮的单击事件，创建一个线程，并立即执行线程，代码如下。

（4）运行程序，单击“确定”按钮，计数器开始计数。此时拖动窗口标题栏移动窗口，发现在计数过程中依然可以拖动窗口，结果如图18.2所示。

图18.2　多线程计数器

18.2.2　使用_beginthreadex函数创建线程

除了可以使用CreateThread API函数创建线程外，用户还可以使用C++语言提供的_beginthreadex函数来创建线程。

语法格式如下：

参数说明：

security：线程安全属性信息，如果为NULL，表示线程句柄不被子进程继承。

stack_size：线程的栈大小，可以为0。

start_address：线程函数，线程运行时将执行该函数。

arglist：传递到线程函数中的参数。

initflag：线程的初始化标记。为0，表示线程立即执行线程函数；为CREATE_SUSPENDED，表示线程暂时被挂起。

thrdaddr：一个整型指针，用于返回线程ID。

返回值：如果函数执行成功，返回值为线程句柄。

注意

要使用_beginthreadex函数，需要在工程中引用process.h头文件。

下面通过一个实例介绍如何使用_beginthreadex函数创建线程。

【例18.2】使用_beginthreadex函数创建线程。（实例位置：资源包\TM\sl\18\2）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为MultyThread，对话框资源设计窗口如图18.3所示。

（2）引用process.h头文件，目的是使用_beginthreadex函数，代码如下。

（3）设计线程函数。在线程函数中设计一个较大的循环，代码如下。

（4）处理“确定”按钮的单击事件。调用_beginthreadex函数创建一个线程，代码如下。

（5）运行程序，单击“确定”按钮创建一个线程，执行循环，然后拖动窗口标题栏，发现在循环的过程中仍可以拖动窗口，结果如图18.4所示。

图18.3　对话框资源设计窗口

图18.4　多线程演示窗口

18.2.3　使用AfxBeginThread函数创建线程

在MFC应用程序中，用户还可以使用AfxBeginThread函数创建一个线程。

语法格式如下：

参数说明：

pfnThreadProc：线程函数指针，函数原型如下。

pParam：线程函数的参数。

nPriority：线程的优先级。

nStackSize：线程堆栈大小。

dwCreateFlags：线程的创建标记。

lpSecurityAttrs：线程的安全属性。

pThreadClass：派生于CWinThread类的运行时类对象。

返回值：新创建的线程对象指针。

下面通过一个实例介绍如何使用AfxBeginThread函数创建线程。

【例18.3】使用AfxBeginThread函数创建线程。（实例位置：资源包\TM\sl\18\3）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为OperateThread，对话框资源设计窗口如图18.5所示。

（2）向对话框类中添加一个成员变量，代码如下。

（3）定义一个线程函数，用于执行循环，代码如下。

（4）处理“确定”按钮的单击事件，调用AfxBeginThread函数创建一个线程，代码如下。

（5）处理“取消”按钮的单击事件，在对话框关闭时释放线程对象，代码如下。

说明

GetExitCodeThread函数用于根据线程对象获取线程的退出代码，如果退出代码为STILL_ACTIVE，则表示线程仍在运行。

（6）运行程序，单击“确定”按钮创建并执行线程，在执行循环的同时可以拖动窗口，结果如图18.6所示。

图18.6　多线程演示程序

图18.5　对话框资源设计窗口

18.2.4　应用MFC类库创建线程

MFC类库提供的CWinThread类封装了对线程的支持。该类提供了两种类型的线程，即工作者线程和用户界面线程。所谓工作者线程是指线程中没有消息循环，主要在后台进行计算或完成某些功能；用户界面线程是指线程具有消息循环，能够处理从系统接收的消息。下面对CWinThread类的主要成员和方法进行介绍。

1. m_bAutoDelete

该成员确定在线程终止时，线程对象是否自动被释放。

2. m_hThread

该成员表示线程对象关联的线程句柄。

3. m_nThreadID

该成员用于记录线程ID。

4. m_pMainWnd

该成员表示应用程序的主窗口。

5. m_pActiveWnd

如果线程是OLE服务器的一部分，该成员表示容器应用程序主窗口。

6. CreateThread

该方法用于创建一个线程。

语法格式如下：

参数说明：

dwCreateFlags：线程的创建标识。如果为CREATE_SUSPENDED，线程在创建后会立即挂起，直到调用ResumeThread方法才开始执行线程函数；如果为0，线程创建后会立即执行。

nStackSize：线程堆栈的大小。如果为0，堆栈的大小将与主线程堆栈的大小相同。

lpSecurityAttrs：线程的安全属性。

7. GetMainWnd

该方法用于获取应用程序的主窗口指针。如果是OLE服务器应用程序，该方法返回的是m_pActiveWnd成员，否则返回m_pMainWnd成员。

语法格式如下：

8. ResumeThread

该方法用于重新唤醒线程。它将线程的挂起计数减1，如果线程的挂起计数为0，将开始执行线程。

语法格式如下：

返回值：如果函数执行成功，返回值是线程之前挂起的计数，否则返回0xFFFFFFFF。返回值为0，表示线程之前没有被挂起；返回值为1，表示线程之前被挂起，但是线程计数会减1，因此线程会马上执行；返回值大于1，线程将继续挂起。

9. SuspendThread

该方法用于挂起线程。

语法格式如下：

返回值：如果函数执行成功，返回值为线程之前的挂起计数，否则为0xFFFFFFFF。

下面举例介绍如何使用CWinThread类创建多线程应用程序。

【例18.4】使用CWinThread类创建多线程应用程序。（实例位置：资源包\TM\sl\18\4）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为UserThread，对话框资源设计窗口如图18.7所示。

（2）在工作区的类视图窗口中再创建一个对话框类，名称为CEmployee，对话框资源设计窗口如图18.8所示。

图18.7　对话框资源设计窗口1

图18.8　对话框资源设计窗口2

（3）在工作区的类视图窗口中创建一个CUserThread类，基类为CWinThread。在该类的InitInstance方法中定义并显示一个对话框，代码如下。

（4）在主对话框中编写“用户界面线程”按钮的单击事件，利用AfxBeginThread函数创建一个用户界面线程，代码如下。

说明

用户界面线程需要使用一个线程类的运行时对象作为参数，可以使用RUNTIME_CLASS来获得某一个类的运行时类信息。

（5）在对话框关闭时释放创建的线程对象，代码如下。

（6）运行程序，单击“用户界面线程”按钮将创建一个用户界面线程，并执行CUserThread类的InitInstance方法，创建一个对话框，结果如图18.9所示。

图18.9　用户界面线程

18.3　线程的挂起、唤醒与终止

在线程创建并运行后，用户可以对线程执行挂起和终止操作。所谓挂起，是指暂停线程的执行，随后可以通过唤醒操作来恢复线程的执行。线程终止，是指结束线程的执行。系统提供了SuspendThread、ResumeThread、ExitThread和TerminateThread等函数实现线程的挂起、唤醒和终止操作，下面逐一介绍。

1. SuspendThread

SuspendThread函数用于挂起线程。

语法格式如下：

其中，hThread表示线程句柄。

返回值：如果函数执行成功，返回值为之前挂起的线程次数；如果函数执行失败，返回值为0xFFFFFFFF。

2. ResumeThread

ResumeThread函数用于减少线程挂起的次数。如果线程挂起的次数为0，将唤醒线程。

语法格式如下：

其中，hThread表示线程句柄。

返回值：如果函数执行成功，返回值为之前挂起的线程次数；如果函数执行失败，返回值为0xFFFFFFFF。

3. ExitThread

ExitThread函数用于结束当前的线程。

语法格式如下：

其中，dwExitCode表示线程退出代码。

4. TerminateThread

TerminateThread函数用于强制终止线程的执行。

语法格式如下：

参数说明：

hThread：待终止的线程句柄。

dwExitCode：线程退出代码。

下面以一个实例介绍如何创建、挂起、唤醒和终止线程。

【例18.5】创建、挂起、唤醒和终止线程。（实例位置：资源包\TM\sl\18\5）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为ThreadManage，对话框资源设计窗口如图18.10所示。

（2）编写一个线程函数，在线程函数中设计一个较大的循环，显示进度条的进度，代码如下。

（3）处理“创建线程”按钮的单击事件，创建一个新的线程，并且开始执行线程函数，代码如下。

（4）处理“挂起线程”按钮的单击事件，挂起当前执行的线程，代码如下。

（5）处理“唤醒线程”按钮的单击事件，唤醒挂起的线程，代码如下。

注意

如果线程被挂起多次，则需要进行相同次数的唤醒操作，才能够唤醒线程。

（6）处理“终止线程”按钮的单击事件，终止线程的执行，代码如下。

（7）运行程序，结果如图18.11所示。

图18.10　对话框资源设计窗口

图18.11　线程管理

18.4　线程同步

在多线程应用程序中，由于每个线程都可以访问进程中的资源，因此有可能导致一些危险。例如，线程A正在使用一个对象，而线程B对这个对象进行了修改，导致线程A的执行结果不可预料。本节将介绍如何通过线程同步来解决资源访问冲突问题。

18.4.1　多线程潜在的危险

在多线程应用程序中，最容易出现的问题就是资源访问冲突问题。本节通过一个简单实例来演示多线程资源访问冲突的情况。实例中创建了两个线程，按照计数递增的顺序输出计数。

【例18.6】多线程资源访问冲突。（实例位置：资源包\TM\sl\18\6）

具体操作步骤如下。

（1）创建一个控制台应用程序，工程名称为ResConf l ict。

（2）引用windows.h头文件，使用系统函数。

（3）定义两个线程函数，输出一个全局计数，代码如下。

（4）在主函数中创建线程，并执行线程函数，代码如下。

注意

本实例是为了查看线程的运行结果，所以设置了while (true)循环。在开发程序时，切记不可这样写，否则将陷入死循环。

（5）运行程序，结果如图18.12所示。

从图18.12中可以发现，有时线程1或线程2重复输出计数，并且有时输出的计数并不连续。这就是多线程程序潜在的危险。后面几节将介绍如何通过线程同步来解决上述问题。

图18.12　资源访问冲突

18.4.2　使用事件对象实现线程同步

事件对象属于系统内核对象之一，经常使用事件对象来实现线程同步。事件对象可以分为两类，即人工重置事件对象和自动重置事件对象。对于人工重置事件对象，可以同时有多个线程等待到事件对象，成为可调度线程。对于自动重置事件对象，等待该事件对象的多个线程只能有一个线程成为可调度线程。此外，如果事件对象为自动重置事件对象，当某个线程等待到事件对象后，系统会自动将事件对象设置为未通知状态。

为了使用事件对象，系统提供了一组与事件对象有关的函数，下面逐一介绍。

1. CreateEvent

CreateEvent函数用于创建一个事件对象。

语法格式如下：

参数说明：

lpEventAttributes：事件对象的安全属性。

bManualReset：事件对象的类型。为TRUE，表示创建人工重置事件对象；为FALSE，表示创建自动重置事件对象。

bInitialState：事件对象初始的通知状态。为TRUE，表示通知状态；为FALSE，表示未通知状态。

lpName：事件对象的名称。

2. SetEvent

SetEvent函数用于将事件设置为通知状态。

语法格式如下：

其中，hEvent表示事件对象句柄。

3. ResetEvent

ResetEvent函数用于将事件设置为未通知状态。

语法格式如下：

其中，hEvent表示事件对象句柄。

在设计线程同步时，通常需要使用WaitForSingleObject函数来等待内核对象的状态。

语法格式如下：

参数说明：

hHandle：等待对象的句柄。

dwMilliseconds：等待的时间，单位为毫秒。

说明

如果dwMilliseconds参数设置的等待时间超过了该参数表示的时间，函数将会返回。如果将该参数设置为INFINITE，则函数会一直等待，直到hHandle参数表示的对象处于通知状态。

下面改写第18.4.1节的例18.6，使用事件对象实现线程同步，来解决资源访问冲突问题。

【例18.7】使用事件对象实现线程同步来解决资源访问冲突问题。（实例位置：资源包\TM\sl\18\7）

具体操作步骤如下。

（1）创建一个控制台应用程序，工程名称为ThreadSynch。

（2）引用windows.h头文件，使用系统函数。

（3）定义两个线程函数，输出一个全局计数，代码如下。

（4）在主函数中创建线程，并执行线程函数，代码如下。

（5）运行程序，结果如图18.13所示。

图18.13　事件对象实现线程同步

18.4.3　使用信号量对象实现线程同步

信号量对象也属于系统内核对象之一，包含有使用计数。当使用计数为0时，信号量对象处于无信号状态；当使用计数大于0时，信号量对象处于有信号状态。系统同样提供了一组操作信号量的函数，下面分别进行介绍。

1. CreateSemaphore

CreateSemaphore函数用于创建一个信号量对象。

语法格式如下：

参数说明：

lpSemaphoreAttributes：信号量的安全属性，可以为NULL。

lInitialCount：信号量的初始计数。

lMaximumCount：信号量的最大计数。

lpName：信号量的名称。

2. ReleaseSemaphore

ReleaseSemaphore函数用于递增信号量的使用计数。

语法格式如下：

参数说明：

hSemaphore：信号量对象句柄。

lReleaseCount：信号量的递增数量。

lpPreviousCount：用于返回之前的信号量的使用计数。

下面使用信号量对象实现线程同步来修改第18.4.1节的例18.6。

【例18.8】使用信号量对象实现线程同步。（实例位置：资源包\TM\sl\18\8）

具体操作步骤如下。

（1）创建一个控制台应用程序，工程名称为ThreadSynch。

（2）引用windows.h头文件，使用系统函数。

（3）定义两个线程函数，输出一个全局计数，代码如下。

说明

在上述代码中，线程函数部分使用了Sleep函数进行延时操作，这主要是为了演示数字的输出效果。

（4）在主函数中创建线程，并执行线程函数，代码如下。

（5）运行程序，结果如图18.13所示。

18.4.4　使用临界区对象实现线程同步

临界区又称为关键代码段，指的是一小段代码，在代码执行前，临界区需要独占某些资源。在程序中通常将多个线程同时访问某个资源的代码作为临界区。为了使用临界区，系统提供了一组操作临界区对象的函数，下面分别进行介绍。

1. InitializeCriticalSection

InitializeCriticalSection函数用于初始化临界区对象。

语法格式如下：

其中，lpCriticalSection表示一个临界区对象指针。在使用临界区对象时，首先需要定义一个临界区对象，然后使用该函数进行初始化。

2. EnterCriticalSection

EnterCriticalSection函数用于等待临界区对象的所有权。

语法格式如下：

其中，lpCriticalSection表示一个临界区对象指针。

3. LeaveCriticalSection

LeaveCriticalSection函数用于释放临界区对象的所有权。

语法格式如下：

其中，lpCriticalSection表示一个临界区对象指针。

4. DeleteCriticalSection

DeleteCriticalSection函数用于释放为临界区对象分配的相关资源，使临界区对象不再可用。

语法格式如下：

其中，lpCriticalSection表示一个临界区对象指针。

说明

要使用这些函数，需要引用windows.h头文件。

下面使用临界区对象实现线程同步来修改第18.4.1节的例18.6。

【例18.9】使用临界区对象实现线程同步。（实例位置：资源包\TM\sl\18\9）

具体操作步骤如下。

（1）创建一个控制台应用程序，工程名称为ThreadSynch。

（2）引用windows.h头文件，使用系统函数。

（3）定义两个线程函数，输出一个全局计数，代码如下。

（4）在主函数中创建线程，并执行线程函数，代码如下。

（5）运行程序，结果如图18.13所示。

18.4.5　使用互斥对象实现线程同步

互斥对象属于系统内核对象，能够使线程拥有对某个资源的绝对访问权。互斥对象主要包含使用数量、线程ID和递归计数器等信息。其中，线程ID表示当前拥有互斥对象的线程，递归计数器表示线程拥有互斥对象的次数。互斥对象的使用方式为：当互斥对象的线程ID为0时，表示互斥对象不被任何线程所拥有，此时系统会发出该互斥对象的通知信号，等待互斥对象的某个线程将会拥有该互斥对象，同时互斥对象的线程ID为拥有该互斥对象的线程ID。当互斥对象的线程ID不为0时，表示当前有线程拥有该互斥对象，系统不会发出互斥对象的通知信号，其他等待互斥对象的线程继续等待，直到拥有互斥对象的线程释放互斥对象的所有权。下面介绍与互斥对象有关的系统函数。

1. CreateMutex

CreateMutex函数用于创建一个互斥对象。

语法格式如下：

参数说明：

lpMutexAttributes：互斥对象的安全属性，可以为NULL。

bInitialOwner：互斥对象的初始状态。如果为TRUE，互斥对象的线程ID为当前调用线程的ID，互斥对象的递归计数器为1，当前创建互斥对象的线程拥有互斥对象的所有权；为FALSE，互斥对象的线程ID为0，互斥对象的递归计数器为0，系统会发出该互斥对象的通知信号。

lpName：互斥对象的名称。如果为NULL，将创建一个匿名的互斥对象。

返回值：如果函数执行成功，返回值是互斥对象的句柄，否则返回值为NULL。

2. ReleaseMutex

ReleaseMutex函数用于释放互斥对象的所有权。

语法格式如下：

其中，hMutex表示互斥对象句柄。

下面使用互斥对象实现线程同步来修改第18.4.1节的例18.6。

【例18.10】使用互斥对象实现线程同步。（实例位置：资源包\TM\sl\18\10）

具体操作步骤如下。

（1）创建一个控制台应用程序，工程名称为ThreadSynch。

（2）引用windows.h头文件，使用系统函数。

（3）定义两个线程函数，输出一个全局计数，代码如下。

（4）在主函数中创建线程，并执行线程函数，代码如下。

（5）运行程序，结果如图18.13所示。

说明

使用第18.4.2节～18.4.5节介绍的方法都可以实现线程同步，读者可以根据需要进行选择。

18.5　小结

本章介绍了有关线程的相关知识，包括线程的各种创建方法、线程的挂起、唤醒及终止等，最后介绍了线程同步技术。通过本章的学习，读者应该掌握简单的线程应用，学会一种或多种线程同步技术。

18.6　实践与练习

1. 编写一个应用程序，描述线程的优先级。（答案位置：资源包\TM\sl\18\11）

2. 编写一个应用程序，实现多线程文件下载。（答案位置：资源包\TM\sl\18\12）

3. 编写一个视频显示程序，在线程函数中创建视频窗口，并处理窗口消息。（答案位置：资源包\TM\ sl\18\13）

第19章

网络套接字编程

（视频讲解：54分钟）

随着社会的进步和网络技术的不断发展，如今上网的人越来越多，人们越来越依赖于互联网来获取信息，这极大地刺激了网站、网络应用程序的开发。本章将介绍有关Visual C++开发网络应用程序的相关知识。

通过阅读本章，您可以：

了解网络的基本结构

了解TCP/IP协议

理解套接字的概念

掌握如何使用套接字函数进行网络程序开发

掌握如何使用MFC类库进行网络程序开发

19.1　计算机网络基础

计算机网络是计算机和通信技术相结合的产物，代表了计算机发展的重要方向，了解计算机的网络结构有助于用户开发网络应用程序。本节将介绍有关计算机网络的基础知识和一些基本概念。

19.1.1　OSI参考模型

开发式系统互联（Open System Interconnection，OSI）是国际标准化组织（ISO）为了实现计算机网络的标准化而颁布的参考模型。OSI参考模型采用分层的划分原则，将网络中的数据传输划分为7层，每一层使用下层的服务，并向上层提供服务。表19.1描述了OSI参考模型的结构。

表19.1　OSI参考模型

说明

OSI参考模型的建立，不仅创建了通信设备之间的物理通道，还规划了各层之间的功能，为标准化组合和生产厂家定制协议提供了基本原则，有助于用户了解复杂的协议（如TCP/IP、X.25协议等）。用户可以将这些协议与OSI参考模型对比，从而了解这些协议的工作原理。

19.1.2　IP地址

为了使网络上的计算机能够彼此识别对方，每台计算机都需要一个IP地址来标识自己。IP地址由IP协议规定，由32位的二进制数表示。最新的IPv6协议将IP地址升为128位，这使得IP地址更加广泛，能够很好地解决目前IP地址紧缺的问题。但是IPv6协议距离实际应用还有一段距离，目前多数操作系统和应用软件都是以32位的IP地址为基准的。

32位的IP地址主要分为两部分，即前缀和后缀。前缀表示计算机所属的物理网络，后缀确定该网络上唯一的一台计算机。在互联网上，每一个物理网络都有一个唯一的网络号。根据网络号的不同，可以将IP地址分为5类，即A类、B类、C类、D类和E类。其中，A类、B类和C类属于基本类，D类用于多播发送，E类属于保留类。表19.2描述了各类IP地址的范围。

表19.2　各类IP地址的范围

在上述IP地址中，有几个IP地址是特殊的，有其单独的用途。

网络地址

在IP地址中主机地址为0的表示网络地址，如128.111.0.0。

广播地址

在网络号后跟所有位全是1的IP地址，表示广播地址。

回送地址

127.0.0.1表示回送地址，用于测试。

19.1.3　地址解析

地址解析是指将计算机的协议地址解析为物理地址，即MAC地址，又称为媒体设备地址。通常，在网络上由地址解析协议ARP来实现地址解析。下面以本地网络上的两台计算机通信为例，介绍ARP协议解析地址的过程。

假设主机A和主机B处于同一个物理网络上，主机A的IP地址为192.168.1.21，主机B的IP地址为192.168.1.23，当主机A与主机B进行通信时，主机B的IP地址192.168.1.23将按如下步骤被解析为物理地址。

（1）主机A从本地ARP缓存中查找IP地址为192.168.1.23对应的物理地址。用户可以在命令窗口中输入arp -a命令查看ARP缓存，如图19.1所示。

图19.1　本地ARP缓存

（2）如果主机A在ARP缓存中没有发现192.168.1.23映射的物理地址，将发送ARP请求帧到本地网络上的所有主机，在ARP请求帧中包含了主机A的物理地址和IP地址。

（3）本地网络上的其他主机接收到ARP请求帧后，检查是否与自己的IP地址匹配，如果不匹配，则丢弃ARP请求帧。如果主机B发现与自己的IP地址匹配，则将主机A的物理地址和IP地址添加到自己的ARP缓存中，然后主机B将自己的物理地址和IP地址发送到主机A，当主机A接收到主机B发来的信息，将以这些信息更新ARP缓存。

（4）当主机B的物理地址确定后，主机A即可与主机B通信。

19.1.4　域名系统

虽然使用IP地址可以标识网络中的计算机，但由于IP地址容易混淆，并且不容易记忆，人们更倾向于使用主机名来标识IP地址。在Internet上存在许多计算机，为了防止主机名相同，Internet管理机构采取了在主机名后加上后缀名的方法标识一台主机，其后缀名被称为域名。例如，www.mingrisoft.com，主机名为www，域名为mingrisoft.com。com为一级域名，表示商业组织；mingrisoft为二级域名，表示本地名。为了能够利用域名进行不同主机间的通信，需要将域名解析为IP地址，该过程称之为域名解析。域名解析是通过域名服务器来完成的。假如主机A的本地域名服务器是dns.local.com，根域名服务器是dns.mr.com；所要访问的主机B的域名为www.mingribook.com，域名服务器为dns.mrbook.com。当主机A通过域名www.mingribook.com访问主机B时，将发送解析域名www.mingribook.com的报文；本地域名服务器收到请求后，查询本地缓存，假设没有该记录，则本地域名服务器dns.local.com将向根域名服务器dns.mr.com发出请求解析域名www.mingribook.com；根域名服务器dns.mr.com收到请求后查询本地记录，如果发现mingribook.com NS dns.mrbook.com信息，将给出dns.mrbook.com的IP地址，并将结果返回给主机A的本地域名服务器dns.local.com，当本地域名服务器dns.local.com收到信息后，会向主机B的域名服务器dns.mrbook.com发送解析域名www.mingribook.com的报文；当域名服务器dns.mrbook.com收到请求后，开始查询本地的记录，发现www.mingribook.com A 211.119.X.X（其中，211.119.X.X表示域名www.mingribook.com的IP地址）。类似的信息，则将结果返回给主机A的本地域名服务器dns.local.com。

19.1.5　TCP/IP协议

TCP/IP（Transmission Control Protocal/Internet Protocal，传输控制协议／网际协议）协议是互联网上最流行的协议，它能够实现互联网上不同类型操作系统的计算机相互通信。对于网络开发人员来说，必须了解TCP/IP协议的结构。TCP/IP协议将网络分为4层，分别对应于OSI参考模型的7层结构。表19.3列出了TCP/IP协议与OSI参考模型的对应关系。

表19.3　TCP/IP协议与OSI参考模型的对应关系

从表19.3中可以发现，TCP/IP协议不是单个协议，而是一个协议簇，包含多种协议，其中主要的协议有网际协议（IP）和传输控制协议（TCP）等。下面介绍TCP/IP主要协议的结构。

1. TCP协议

传输控制协议TCP是一种提供可靠数据传输的通信协议，它是TCP/IP体系结构中传输层上的协议。在发送数据时，应用层的数据传输到传输层，加上TCP的首部，数据就构成了报文。报文是网络层IP的数据，如果再加上IP首部，就构成了IP数据报。TCP协议C语言数据描述如下。

2. IP协议

IP协议又称为网际协议，工作在网络层，主要提供无链接数据报传输。IP协议不保证数据报的发送，只最大限度地发送数据。IP协议C语言数据描述如下。

说明

在IP数据包结构中，第1个成员和第2个成员各占4位，也就是半个字节。我们知道为对象分配空间最小单位是1个字节，为了描述IP数据包中的成员，在定义数据包结构时使用了位域，具体指定每一个成员占用的位数。

3. ICMP协议

ICMP协议又称为网际控制报文协议，负责网络上设备状态的发送和报文检查，可以将某个设备的故障信息发送到其他设备上。ICMP协议C语言数据描述如下。

4. UDP协议

用户数据报协议UDP是一个面向无链接的协议，采用该协议，两个应用程序不需要先建立链接，即它可为应用程序提供一次性的数据传输服务。UDP协议不提供差错恢复，不能够数据重传，因此该协议传输数据安全性略差。UDP协议C语言数据描述如下。

19.1.6　端口

在网络上，计算机是通过IP地址来标识自身的。但是当两台计算机具体通信时，还会出现一个问题—如果主机A中的应用程序A1想与主机B中的应用程序B1通信，如何知道主机A中是A1应用程序与主机B中的应用程序通信，而不是主机A中的其他应用程序与主机B中的应用程序通信呢？反之，当主机B接收到数据时，又如何知道数据是发往应用程序B1的呢？因为在主机B中可以同时运行多个应用程序。

为了解决上述问题，TCP/IP协议提出了端口的概念，用于标识通信的应用程序。当应用程序（严格说应该是进程）与某个端口绑定后，系统会将收到的该端口的数据送往该应用程序。端口用一个16位的无符号整数值来表示，范围为0～65535。低于256的端口被作为系统的保留端口，用于系统进程的通信；不在这一范围的端口号被称为自由端口，可以由进程自由使用。

19.2　套接字概述

套接字是网络通信的基石，是网络通信的基本构件，最初是由加利福尼亚大学Berkeley学院为UNIX开发的网络通信编程接口。为了在Windows操作系统上使用套接字，20世纪90年代初微软和第三方厂商共同制定了一套标准，即Windows Socket规范，简称WinSock。本节将介绍有关Windows套接字的相关知识。

19.2.1　套接字概述

所谓套接字，实际上是一个指向传输提供者的句柄。在WinSock中，就是通过操作该句柄来实现网络通信和管理的。根据性质和作用的不同，套接字可以分为3种，分别为原始套接字、流式套接字和数据包套接字。原始套接字是在WinSock 2规范中提出的，能够使程序开发人员对底层的网络传输机制进行控制，在原始套接字下接收的数据中包含有IP头。流式套接字提供了双向、有序、可靠的数据传输服务，该类型套接字在通信前，需要双方建立连接。大家熟悉的TCP协议采用的就是流式套接字。与流式套接字对应的是数据包套接字，数据包套接字提供双向的数据流，但是它不能保证数据传输的可靠性、有序性和无重复性。UDP协议采用的就是数据包套接字。

19.2.2　网络字节顺序

不同的计算机结构有时使用不同的字节顺序存储数据。例如，基于Intel的计算机存储数据的顺序与Macintosh（Motorola）计算机相反。通常，用户不必为在网络上发送和接收的数据的字节顺序转换担心，但在有些情况下必须转换字节顺序。例如，程序中将指定的整数设置为套接字的端口号，在绑定端口号之前，必须将端口号从主机顺序转换为网络顺序，有关转换的函数将在第19.3节中进行介绍。

19.2.3　套接字I/O模式

套接字的I/O（Input/Output）模式有两种，分别为阻塞模式和非阻塞模式。在阻塞模式下，I/O操作完成之前，套接字函数会一直等待下去，函数调用后不会立即返回。默认情况下，套接字为阻塞模式。而在非阻塞模式下，套接字函数在调用后会立刻返回。程序中可以使用ioctlsocket函数来设置套接字的I/O模式，有关该函数的介绍参见第19.3节。

19.3　套接字函数

为了使用套接字进行网络程序开发，Windows操作系统提供了一组套接字函数。使用这些函数，用户可以开发出功能强大的网络应用程序。本节将介绍有关套接字函数的相关知识。

19.3.1　套接字函数介绍

Windows系统提供的套接字函数通常封装在ws2_32.dll动态链接库中，其头文件winsock2.h提供了套接字函数的原型，库文件ws2_32.lib提供了ws2_32.dll动态链接库的输出节。在使用套接字函数前，用户需要引用winsock2.h头文件，并链接ws2_32.lib库文件。例如：

此外，在使用套接字函数前还需要初始化套接字，可以使用WSAStartup函数来实现。例如：

下面介绍网络程序开发中经常使用的套接字函数。

1. WSAStartup

WSAStartup函数用于初始化ws2_32.dll动态链接库。在使用套接字函数之前，一定要初始化ws2_32.dll动态链接库。

语法格式如下：

参数说明：

wVersionRequested：调用者使用的Windows Socket的版本，高字节记录修订版本，低字节记录主版本。例如，如果Windows Socket的版本为2.1，则高字节记录1，低字节记录2。

lpWSAData：一个WSADATA结构指针，该结构详细记录了Windows套接字的相关信息。其定义如下。

wVersion：调用者使用的ws2_32.dll动态链接库的版本号。

wHighVersion：ws2_32.dll支持的最高版本，通常与wVersion相同。

szDescription：套接字的描述信息，通常没有实际意义。

szSystemStatus：系统的配置或状态信息，通常没有实际意义。

iMaxSockets：最多可以打开多少个套接字。

iMaxUdpDg：数据报的最大长度。在套接字版本2或以后的版本中，该成员将被忽略。

lpVendorInfo：套接字的厂商信息。在套接字版本2或以后的版本中，该成员将被忽略。

注意

在套接字版本2及以后的版本中，iMaxSockets成员将被忽略。

2. socket

socket函数用于创建一个套接字。

语法格式如下：

参数说明：

af：一个地址家族，通常为AF_INET。

type：套接字类型，如果为SOCK_STREAM，表示创建面向链接的流式套接字；如果为SOCK_DGRAM，表示创建面向无链接的数据报套接字；如果为SOCK_RAW，表示创建原始套节字。对于这些值，用户可以在winsock2.h头文件中找到。

potocol：表示套接口所用的协议，如果用户不指定，可以设置为0。

返回值：是创建的套接字句柄。

3. bind

bind函数用于将套接字绑定到指定的端口和地址上。

语法格式如下：

参数说明：

s：套接字标识。

name：一个sockaddr结构指针，该结构中包含了要结合的地址和端口号。

namelen：确定name缓冲区的长度。

返回值：如果函数执行成功，返回值为0，否则为SOCKET_ERROR。

4. listen

listen函数用于将套接字设置为监听模式。

语法格式如下：

参数说明：

s：套接字标识。

backlog：等待连接的最大队列长度。例如，如果backlog被设置为2，此时有3个客户端同时发出连接请求，那么前两个客户端连接会放置在等待队列中，第3个客户端会得到错误信息。

注意

对于流式套接字，必须处于监听模式才能接收客户端套接字的连接。

5. accept

accept函数用于接受客户端的连接。对于流式套接字，必须处于监听状态，才能接受客户端的连接。

语法格式如下：

参数说明：

s：一个套接字，应处于监听状态。

addr：一个sockaddr_in结构指针，包含一组客户端的端口号、IP地址等信息。

addrlen：用于接收参数addr的长度。

返回值：一个新的套接字，它对应于已经接受的客户端连接，对于该客户端的所有后续操作，都应使用这个新的套接字。

6. closesocket

closesocket函数用于关闭套接字。

语法格式如下：

其中，s用于标识一个套接字。如果参数s设置有SO_DONTLINGER选项，则调用该函数后会立即返回，但此时如果有数据尚未传送完毕，会继续传递数据，然后才关闭套接字。

7. connect

connect函数用于发送一个连接请求。

语法格式如下：

参数说明：

s：一个套接字。

name：套接字s想要连接的主机地址和端口号。

namelen：name缓冲区的长度。

否则返回值为：如果函数执行成功，返回值为0；否则为SOCKET_ERROR，用户可以通过WSAGETLASTERROR得到其错误描述。

8. htons

htons函数将一个16位的无符号短整型数据，由主机排列方式转换为网络排列方式。

语法格式如下：

其中，hostshort是一个主机排列方式的无符号短整型数据。

返回值：16位的网络排列方式数据。

9. htonl

htonl函数将一个无符号长整型数据，由主机排列方式转换为网络排列方式。

语法格式如下：

其中，hostlong表示一个主机排列方式的无符号长整型数据。

返回值：32位的网络排列方式数据。

10. inet_addr

inet_addr函数将一个由字符串表示的地址转换为32位的无符号长整型数据。

语法格式如下：

其中，cp表示一个IP地址的字符串。

返回值：32位无符号长整数。

11. recv

recv函数用于从面向连接的套接字中接收数据。

语法格式如下：

参数说明：

s：一个套接字。

buf：接收数据的缓冲区。

len：buf的长度。

flags：函数的调用方式。如果为MSG_PEEK，表示查看传来的数据，数据被复制到接收缓冲区，但是不会从输入队列中移走；为MSG_OOB，表示用来处理Out-Of-Band数据，也就是带外数据。

12. send

send函数用于在面向连接方式的套接字间发送数据。

语法格式如下：

参数说明：

s：一个套接字。

buf：存放要发送数据的缓冲区。

len：缓冲区长度。

flags：函数的调用方式。

13. select

select函数用来检查一个或多个套接字是否处于可读、可写或错误状态。

语法格式如下：

参数说明：

nfds：无实际意义，只是为了和UNIX下的套接字兼容。

readfds：一组被检查可读的套接字。

writefds：一组被检查可写的套接字。

exceptfds：被检查有错误的套接字。

timeout：函数的等待时间。

14. WSACleanup

WSACleanup函数用于释放为ws2_32.dll动态链接库初始化时分配的资源。

语法格式如下：

15. WSAAsyncSelect

WSAAsyncSelect函数用于将网络中发生的事件关联到窗口的某个消息中。

语法格式如下：

参数说明：

s：套接字。

hWnd：接收消息的窗口句柄。

wMsg：窗口接收来自套接字中的消息。

lEvent：网络中发生的事件。

16. ioctlsocket

ioctlsocket函数用于设置套接字的I/O模式。

语法格式如下：

参数说明：

s：待更改I/O模式的套接字。

cmd：对套接字的操作命令。

argp：命令参数。

注意

cmd参数如果为FIONBIO，当argp为0时表示禁止非阻塞模式，当argp为非零时表示设置非阻塞模式；如果为FIONREAD，表示从套接字中可以读取的数据量；如果为SIOCATMARK，表示是否所有的带外数据都已被读入，这个命令仅适用于流式套接字，并且该套接字已被设置为可以在线接收带外数据（SO_OOBINLINE）。

19.3.2　基于套接字函数的网络聊天系统

在介绍了套接字函数后，本节利用套接字函数设计一个网络聊天系统。网络聊天系统分为两部分，即客户端和服务器端。客户端用于发送和显示数据，服务器端则用于转发客户端的数据。下面分别介绍客户端和服务器端的设计过程。

1. 客户端程序设计

【例19.1】使用套接字函数设计网络聊天室系统客户端。（实例位置：资源包\TM\sl\19\1）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为Client，对话框资源设计窗口如图19.2所示。

图19.2　对话框资源设计窗口

（2）在对话框类的头文件中引用winsock2.h头文件，并导入ws2_32.lib库文件，代码如下。

（3）在应用程序的InitInstance方法中初始化套接字，代码如下。

（4）改写应用程序的ExitInstance虚方法，在应用程序结束时释放套接字资源，代码如下。

（5）在对话框类中添加如下成员变量。

（6）在对话框初始化时创建套接字，代码如下。

说明

在本实例中，创建的是面向链接的流式套接字。

（7）向对话框中添加ReceiveInfo方法，用于接收服务器端发来的数据，代码如下。

（8）在对话框的消息映射部分添加ON_MESSAGE消息映射宏，将自定义的消息与ReceiveInfo方法关联，代码如下。

（9）处理“登录”按钮的单击事件，开始登录服务器，代码如下。

说明

如果在程序中两次调用WSAAsyncSelect函数，则第一次调用WSAAsyncSelect函数设置的网络事件将被取消。

（10）处理“发送”按钮的单击事件，向服务器发送数据，再由服务器转发数据，代码如下。

（11）运行程序，结果如图19.3和图19.4所示。

图19.3 　客户端窗口1

图19.4　客户端窗口2

2. 服务器端程序设计

【例19.2】使用套接字函数设计网络聊天室系统服务器。（实例位置：资源包\TM\sl\19\2）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为Server，对话框资源设计窗口如图19.5所示。

图19.5　对话框资源设计窗口

（2）在对话框类的头文件中引用winsock2.h头文件，并导入ws2_32.lib库文件，代码如下。

（3）在应用程序的InitInstance方法中初始化套接字，代码如下。

（4）改写应用程序的ExitInstance虚方法，在应用程序结束时释放套接字资源，代码如下。

（5）向对话框类中添加如下成员变量。

（6）在对话框初始化时创建套接字，代码如下。

注意

WM_USER+1设置的是来自套接字的消息。

（7）向对话框中添加TranslateData方法，接受客户端的连接，并转发客户端发来的数据，代码如下。

（8）在对话框的消息映射部分添加ON_MESSAGE消息映射宏，将消息与TranslateData方法关联，代码如下。

（9）处理“设置”按钮的单击事件，绑定套接字到指定的地址上，使套接字处于监听模式，代码如下。

（10）运行程序，结果如图19.6所示。

图19.6　“服务器设置”窗口

19.4　MFC套接字编程

为了降低网络程序开发的难度，MFC对套接字函数进行了封装，提供了CAsyncSocket类和CSocket类用于网络程序开发。本节将介绍CAsyncSocket类和CSocket类的相关知识。

19.4.1　CAsyncSocket类

CAsyncSocket类对套接字函数进行了简单封装，提供了基于事件的I/O异步模型，使用户可以方便地处理接收和发送等事件。但是，用户需要自己处理网络的字节顺序、不同字符集间的转换等问题。下面介绍CAsyncSocket类的主要方法和事件。

1. Create

Create方法用于创建一个Windows套接字，并将其附加到CAsyncSocket类对象上。

语法格式如下：

参数说明：

nSocketPort：套接字端口，如果为0，系统将自动选择一个端口。

nSocketType：套接字的类型。如果为SOCK_STREAM，表示流式套接字；如果为SOCK_DGRAM，表示数据包套接字。

lEvent：套接字能够处理的网络事件，其值可以是表19.4描述的任意值的组合。

表19.4　套接字网络事件

lpszSocketAddress：套接字的IP地址。

2. GetLastError

GetLastError方法用于获取最后一次操作失败的状态信息。

语法格式如下：

说明

在进行网络操作时，如果某一项操作失败，想要了解其错误原因，可以调用GetLastError方法来获取错误代码。

3. GetPeerName

GetPeerName方法用于获取套接字连接的IP地址信息。

语法格式如下：

参数说明：

rPeerAddress：用于接收函数返回的IP地址。

rPeerPort：用于记录端口号。

lpSockAddr：一个sockaddr结构指针，用于记录套接字名称。

lpSockAddrLen：用于确定lpSockAddr的大小。

4. Accept

Accept方法用于接受客户端的连接。

语法格式如下：

参数说明：

rConnectedSocket：对应当前连接的套接字引用。

lpSockAddr：一个sockaddr结构指针，用于记录套接字地址。

lpSockAddrLen：用于确定lpSockAddr的大小。

5. Bind

Bind方法用于将IP地址和端口号绑定到套接字上。

语法格式如下：

参数说明：

nSocketPort：套接字端口。

lpszSocketAddress：IP地址。

lpSockAddr：一个sockaddr结构指针，该结构记录了套接字的地址信息。

nSockAddrLen：用于确定lpSockAddr的大小。

6. Connect

Connect方法用于发送一个连接请求。

语法格式如下：

参数说明：

lpszHostAddress：主机的IP地址或网址。

nHostPort：主机的端口。

lpSockAddr：一个sockaddr结构指针，该结构标识套接字地址信息。

nSockAddrLen：用于确定lpSockAddr的大小。

7. Close

Close方法用于关闭套接字。

语法格式如下：

8. Listen

Listen方法用于将套接字置于监听模式。

语法格式如下：

其中，nConnectionBacklog表示等待连接的最大队列长度。

9. Receive

Receive方法用于在流式套接字中接收数据。

语法格式如下：

参数说明：

lpBuf：接收数据的缓冲区。

nBufLen：确定缓冲区的长度。

nFlags：确定函数的调用模式。

说明

nFlags参数如果为MSG_PEEK，表示查看传来的数据，此时数据被复制到接收缓冲区，但不会从输入队列中移走；为MSG_OOB，表示处理带外数据。

10. ReceiveFrom

ReceiveFrom方法用于从数据包套接字中接收数据。

语法格式如下：

参数说明：

lpBuf：接收数据的缓冲区。

nBufLen：缓冲区的大小。

rSocketAddress：用于接收数据报的目的地（IP地址）。

rSocketPort：用于记录端口号。

lpSockAddr：一个sockaddr结构指针，用于记录套接字地址信息。

lpSockAddrLen：用于确定lpSockAddr的大小。

nFlags：确定函数的调用模式。如果为MSG_PEEK，表示用来查看传来的数据，数据会被复制到返回缓冲区，但是不会从输入队列中移走；为MSG_OOB，表示处理带外数据。

11. Send

Send方法用于向流式套接字中发送数据。

语法格式如下：

参数说明：

lpBuf：要发送数据的缓冲区。

nBufLen：用于确定缓冲区的大小。

nFlags：函数调用方法。

12. SendTo

SendTo方法用于在流式套接字或数据包套接字上发送数据。

语法格式如下：

参数说明：

lpBuf：要发送数据的缓冲区。

nBufLen：缓冲区大小。

nHostPort：主机端口号。

lpszHostAddress：主机地址。

lpSockAddr：一个sockaddr结构指针，用于确定主机套接字地址信息。

nSockAddrLen：用于确定lpSockAddr的大小。

nFlags：函数调用方式。

13. ShutDown

ShutDown方法用于在套接字上断开数据的发送或接收。

语法格式如下：

其中，nHow用于确定ShutDown函数的行为，0表示不允许接收，1表示不允许发送，2表示不允许接收和发送。

14. OnAccept

当套接字接受连接请求时触发OnAccept事件。

语法格式如下：

其中，nErrorCode表示错误代码。

15. OnClose

当套接字关闭时触发OnClose事件。

语法格式如下：

其中，nErrorCode表示错误代码。

16. OnConnect

当套接字连接后触发OnConnect事件。

语法格式如下：

其中，nErrorCode表示错误代码。

17. OnReceive

当套接字上有数据被接收时触发OnReceive事件。

语法格式如下：

其中，nErrorCode表示错误代码。

18. OnSend

当套接字发送数据时触发OnSend事件。

语法格式如下：

其中，nErrorCode表示错误代码。

19.4.2　CSocket类

CSocket类派生于CAsyncSocket类，该类对套接字函数进行了更高层次的封装，并提供了同步技术，用户可以独立使用CSocket类进行套接字网络程序开发。下面介绍CSocket类的主要方法。

1. Create

Create方法用于创建一个套接字，将其附加到CSocket类对象上。

语法格式如下：

参数说明：

nSocketPort：套接字端口号，如果为0，MFC将自动选择一个端口。

nSocketType：套接字的类型。如果为SOCK_STREAM，表示流式套接字；如果为SOCK_DGRAM，表示数据包套接字。

lpszSocketAddress：套接字IP地址。

2. Attach

Attach方法用于将一个套接字句柄附加到CSocket类对象上。

语法格式如下：

其中，hSocket表示套接字句柄。

3. FromHandle

FromHandle方法根据套接字句柄获得CSocket对象指针。

语法格式如下：

其中，hSocket表示套接字句柄。

返回值：CSocket对象指针。

4. IsBlocking

IsBlocking方法用于判断套接字是否处于阻塞模式。

语法格式如下：

返回值：如果返回值为0，表示处于非阻塞状态；非零，表示处于阻塞状态。

5. CancelBlockingCall

CancelBlockingCall方法用于取消套接字的阻塞模式。

语法格式如下：

19.4.3　基于TCP协议的网络聊天室系统

在介绍完MFC提供的CAsyncSocket类和CSocket类后，下面利用CSocket类设计一个网络聊天室系统。系统分为客户端和服务器端两个模块，下面分别介绍其实现过程。

1. 客户端模块实现过程

【例19.3】使用CSocket类设计网络聊天室系统客户端。（实例位置：资源包\TM\sl\19\3）

具体操作步骤如下。

（1）创建一个基于对话框的工程，工程名称为Client，窗户端对话框资源设计窗口如图19.7所示。

图19.7　客户端对话框资源设计窗口

（2）在应用程序的InitInstance方法中初始化套接字，代码如下。

（3）从CSocket类派生一个子类CClientSocket，在该类中添加m_pDialog成员，代码如下。

说明

在CClientSocket类的头文件中要引用CClientDlg类的头文件ClientDlg.h。

（4）在CClientSocket中添加SetDialog方法，用于设置成员变量，代码如下。

（5）改写CClientSocket类的OnReceive方法，在套接字有数据接收时调用该方法，代码如下。

说明

m_pDialog是主窗口类的对象指针，ReceiveText是用于接收数据的方法。

（6）在对话框类中添加如下成员变量。

（7）向对话框类中添加ReceiveText方法接收数据，代码如下。

（8）在对话框初始化时创建套接字，代码如下。

（9）处理“登录”按钮的单击事件，开始登录服务器，代码如下。

说明

如果连接服务器成功，则向服务器端发送信息，通知客户端用户进入聊天室。

（10）处理“发送”按钮的单击事件，向服务器发送数据，再由服务器转发这些数据，代码如下。

（11）运行程序，结果如图19.8和图19.9所示。

图19.8　客户端窗口1

图19.9　客户端窗口2

2. 服务器端模块实现过程

【例19.4】使用CSocket类设计网络聊天室系统服务器端。（实例位置：资源包\TM\sl\19\4）

具体操作步骤如下。

（1）创建一个基于对话框的应用程序，工程名称为Server，服务器对话框资源设计窗口如图19.10所示。

图19.10　服务器端对话框资源设计窗口

（2）在应用程序的InitInstance方法中初始化套接字，代码如下。

（3）从CSocket类派生一个子类CServerSocket，在该类中定义成员变量m_pDlg，代码如下。

（4）向CServerSocket类中添加SetDialog函数，为m_pDlg成员变量赋值，代码如下。

（5）改写CServerSocket类的OnAccept虚方法，在套接字中有连接请求时接受其连接，代码如下。

（6）从CSocket类再次派生一个新类CClientSocket，在该类中定义成员变量m_pDlg，代码如下。

（7）向CClientSocket类中添加SetDialog函数，为m_pDlg成员变量赋值，代码如下。

（8）改写CClientSocket类的OnReceive方法，在套接字有数据接收时接收数据，代码如下。

（9）在对话框类中添加如下成员变量。

说明

m_socketlist变量用于装载连接服务器端的客户端套接字。

（10）向对话框类中添加AcceptConnect方法，接受客户端的连接，代码如下。

（11）向对话框类中添加ReceiveData方法，用于接收套接字数据，代码如下。

（12）处理“设置”按钮的单击事件，创建并开始监听套接字，代码如下。

（13）运行程序，结果如图19.11所示。

图19.11　服务器窗口

19.5　小结

本章首先介绍了网络的基础知识，如IP地址、域名系统和TCP/IP协议等，然后介绍了网络程序开发使用的套接字函数，最后结合实例演示了如何使用MFC类库提供的CAsyncSocket类和CSocket类开发网络应用程序。通过本章的学习，读者应该掌握使用套接字开发网络应用程序的思路和方法，为开发复杂的网络应用程序打下基础。

19.6　实践与练习

1. 编写一个应用程序，获取本机的名称和IP地址。（答案位置：资源包\TM\sl\19\5）

2. 利用UDP协议进行信息广播。（答案位置：资源包\TM\sl\19\6）

3. 编写一个应用程序，实现IP端口扫描。（答案位置：资源包\TM\sl\19\7）

第4篇

项目实战

第20章　图像处理系统

本篇运用软件工程的设计思想，通过开发一个大型、完整的图像处理系统，学习进行软件项目开发的全流程。书中按照编写“开发背景→需求分析系统设计→公共模块设计→主窗体设计→图像旋转模块设计→图像缩放模块设计→图像水印效果模块设计→ PSD文件浏览模块设计→照片版式处理模块设计→开发技巧与难点分析”的流程进行介绍，带领读者一步一步亲身体验项目开发的全过程。

第20章

图像处理系统

（Visual C++ 6.0+GDI+技术实现）（视频讲解： 3小时14分钟）

图像处理技术在计算机领域有着很重要的地位，现实生活中也随处可以见到图像处理技术的应用场景。例如，设计广告宣传页、装修效果图或模具等。如今，优秀的图像处理软件有很多，如大家熟知的Photoshop、CoreIDraw等。本章将使用Visual c++和GDI技术设计一个图像处理软件。

通过阅读本章，可以学习到：

图像的任意角度旋转

图像浮雕效果

图像水印效果

照片版式设置

20.1　开发背景

随着图形图像技术的蓬勃发展，在现实生活中，人们对图像的要求越来越高，图像处理软件的应用也越来越广泛。例如，设计宣传图、装修效果图或模具等。所以，一款简单实用的图像处理软件是人们迫切需要的。本章介绍的图像处理软件虽然功能没有那么强大，但却简单实用，符合广大用户的图像处理需求。

20.2　需求分析

在使用图像处理软件时，对图像文件进行旋转、反色、灰度化等处理是必不可少的。另外，作为一款图像处理软件，需要具有格式转换功能，使用户在处理好图像后可以方便地存储为指定的格式。最后，添加水印、PSD文件管理和照片处理功能也是用户迫切需要的功能。所以，经过分析，本系统应具有以下功能：

可显示BMP、JPEG、GIF等各种格式图像。

可实现BMP与JPEG、GIF图像的批量转换。

可实现对位图的旋转、反色和灰度化等各种操作。

可实现对位图添加水印的功能。

可实现PSD文件管理。

可实现照片处理功能。

20.3　系统设计

20.3.1　系统目标

本系统属于小型的图像处理软件，主要用于实现图像的显示与批量转换操作。具体设计要求如下。

能实现各种图像格式的显示。

采用良好的人机对话模式，界面设计美观、友好。

支持图像的单一转换与批量转换。

能实现位图的各种常规操作，如图像旋转、灰度化处理等。

能实现对PSD文件的管理操作以及照片处理功能。

能最大限度地实现易维护性和易操作性。

系统运行稳定、安全可靠。

20.3.2　系统功能结构

图像处理系统主要包含图像处理、批量转换与处理、PSD文件管理和照片处理4个部分。其中，图像处理部分主要是对图像进行各个效果的显示，如图像的锐化效果、浮雕效果等；批量转换与处理部分主要实现各种图像格式之间的转换；PSD文件管理部分主要是对PSD文件进行浏览和转换操作；照片处理部分主要用于照片的排版。图像处理系统的功能结构如图20.1所示。

图20.1　图像处理系统功能结构图

20.3.3　系统预览

图像处理系统由多个功能模块组成，下面仅列出几个典型的功能模块，其他功能模块的设计与开发可参见资源包中的源程序。

图像锐化处理窗口如图20.2所示，该窗口用于对位图文件进行锐化操作；图像旋转处理窗口如图20.3所示，该窗口用于对位图文件进行旋转操作。

图像缩放处理窗口如图20.4所示，该窗口用于放大或缩小位图；图像水印处理窗口如图20.5所示，该窗口用于为图像添加水印。

图20.2　图像锐化处理窗口

图20.3　图像旋转处理窗口

图20.4　图像缩放处理窗口

图20.5　图像水印处理窗口

照片版式处理窗口如图20.6所示，该窗口用于设置照片版式。

图20.6　照片版式处理窗口

20.3.4　业务流程

图像处理系统的业务流程图如图20.7所示。

图20.7　图像处理系统的业务流程图

20.4　公共模块设计

设计图像显示时，有些图像可能比较大，在窗口中不能完全显示出来。为了让用户浏览图像，可以单独设计一个滚动窗口，用户通过滚动条来浏览图像的各个部分。滚动窗口设计步骤如下。

（1）创建一个对话框，类名为CImageContainer，对话框的属性设置如图20.8所示。

图20.8　对话框属性设置

（2）处理对话框的WM_HSCROLL消息，在用户触发水平滚动消息时，根据触发消息的不同方式（如单击左右的滚动箭头、拖动滚动条、单击滚动区域等），设置相应的滚动位置，代码如下。

（3）处理对话框的WM_VSCROLL消息，在用户触发垂直滚动消息时，根据触发消息的不同方式（如单击上下的滚动箭头、拖动滚动条、单击滚动区域等），设置相应的滚动位置，代码如下。

（4）处理鼠标滚动时的事件，适当地滚动窗口，代码如下。

20.5　主窗体设计

图像处理系统的主窗口设计如图20.9所示。

图20.9　图像处理系统主窗口

主窗体设计步骤如下。

（1）启动Visual C++ 6.0，选择File/New命令，打开New对话框。在该对话框左侧的列表视图中选择MFC AppWizard[exe]选项，在Project name文本框中输入工程名称，在Location文本框中设置工程保存的路径，如图20.10所示。

图20.10　New对话框

（2）单击OK按钮进入MFC AppWizard-Step1对话框，如图20.11所示。

图20.11　MFC AppWizard-Step1对话框

（3）单击Finish按钮，完成工程的创建。

（4）在工作区窗口的ResourceView视图中右击，在弹出的快捷菜单中选择Insert Bitmap命令，插入BMP图像资源。

（5）在工作区的资源视图窗口中修改菜单资源ID为IDR_MAINMENU，菜单资源文件代码如下。

（6）在主窗口（框架类）的头文件中引用自定义菜单和工具栏类的头文件，代码如下。

（7）向框架类的头文件中添加成员变量，代码如下。

（8）在框架窗口创建时创建菜单和工具栏，代码如下。

代码贴士

CreateEx：该方法用于创建工具栏。

SetSizes：该方法用于设置工具栏按钮的大小。

（9）在视图类的OnDraw方法中绘制背景图像，代码如下。

说明

绘制背景以后，还要为菜单项和工具栏按钮设置消息响应。消息响应函数中的代码用于打开各个功能模块窗口，其中涉及的类是各个模块窗口对应的类。读者可参见资源包中的源代码进行设计。

20.6　图像旋转模块设计

20.6.1　图像旋转模块概述

图像旋转模块主要实现对图像的任意角度旋转，窗口界面效果如图20.12所示。

图20.12　图像旋转模块

20.6.2　图像旋转模块技术分析

在进行图像旋转时，用户可以使用GDI+提供的类来实现，也可以利用数学公式来实现。使用GDI+只能实现一些固定角度的旋转，如45°、90°和135°等，利用数学公式则可以实现任意角度的旋转。在本模块中就是利用数学公式实现图像旋转。

在数学中，利用矩阵变换可以轻而易举地实现旋转，但是在计算机图形学中，需要进行坐标原点的转换。以一幅图像为例，计算机中的原点为图像的左上角，如图20.13所示。而在数学中，坐标原点为图像的中心点，如图20.14所示。

图20.13　计算机图像坐标

图20.14　数学坐标

在进行某一点的旋转前，需要将计算机的图像坐标转换为数学坐标，公式为：

式中， W 表示源图像的宽度； H 表示源图像的高度。

在旋转之后，还需要将数学坐标转换为图像坐标，公式为：

式中， W new 和 H new 分别表示新图像的宽度和高度。通过三角函数或线性代数可知，一个点的旋转坐标可以表示为：

式中， A 表示旋转角度。需要注意的是，公式中的坐标都是数学坐标，在编写代码时首先需要将图像坐标转换为数学坐标，然后再将数学坐标转换为图像坐标。这里可以通过矩阵变换，抽象出两个与 x 和 y 无关的变量，如下所示：

通过这两个变量，可以在旋转之后计算出正确的坐标。

这就是图像旋转的公式，有了这个公式，就可以轻而易举地实现图像旋转。

20.6.3　图像旋转模块实现过程

图像旋转模块的实现过程如下。

（1）创建一个对话框类，类名为CRotateImage。

（2）向对话框中添加按钮、编辑框、单选按钮、滑块和图像等控件。

（3）设置主要控件属性，如表20.1所示。

表20.1　控件属性设置

（4）处理“…”按钮的单击事件，首先加载位图图像，读取位图信息，然后将位图数据转换为4个字节表示的位图数据，以对图像进行处理，最后根据位图的大小设置窗口的滚动范围，代码如下。

代码贴士

GetPathName：该方法用于返回用户选择文件的完整路径，包括文件的路径、文件名和文件扩展名。

ReadHuge：该方法用于读取文件到缓冲区，主要用来读取大文件。

（5）向对话框中添加RotateBmp方法，按指定的角度旋转图像。其中，参数pBmpData表示源位图的实际数据，pDesData表示缩放后的位图数据，nWidth和nHeight表示源位图的宽度和高度，nDesWidth和nDesHeight表示缩放后的位图宽度和高度，dAngle表示要旋转的角度，代码如下。

（6）向对话框中添加RotationImage方法，获取源图像和目的图像的信息，并调用RotateBmp方法旋转图像数据，代码如下。

（7）处理“旋转”按钮的单击事件，旋转已加载的位图。首先确定用户设置的旋转方式和旋转角度，然后调用RotationImage方法旋转图像数据，接着将旋转后的图像数据转换为3个字节表示像素的位图数据格式，最后显示旋转后的位图图像，代码如下。

（8）处理“保存”按钮的单击事件，保存旋转后的图像。方法是将旋转后的图像数据转换为3个字节表示像素的实际位图数据，然后根据位图信息定义位图文件头和位图信息头，有了位图文件头、位图信息头和位图数据，就可以生成位图文件了，代码如下。

20.7　图像缩放模块设计

20.7.1　图像缩放模块概述

图像缩放模块主要实现对图像的比例缩放和非比例缩放，缩放方式可以按百分比和实际的图像像素缩放。图像缩放模块窗口界面如图20.15所示。

图20.15　图像缩放模块

20.7.2　图像缩放模块技术分析

图像缩放主要是改变图像大小，使得图像在缩放后宽度和高度发生变化。缩放主要有两种方式：一种是按比例缩放，即缩放后的图像宽度和高度比保持不变；另一种是随意缩放。图像缩放可以采用多种方法，如最临近插值法、双线性插值法等，这里采用最临近插值法。该算法的思想非常简单，可理解为四舍五入，使一个实数的坐标像素等于与该点最近的图像像素值，如图20.16所示。

图20.16　最临近插值法示意图

从图中可以发现，当坐标为（5.8,5.8）时，它的取值将为（6,6）。有些读者可能会问，坐标怎么能为5.8呢？这是由缩放比例造成的。下面的语句就使用最临近插值法根据图像中的坐标获取了对应源图像坐标。

这里的dYRate和dXRate表示缩放的比例。

20.7.3　图像缩放模块实现过程

图像缩放模块的实现过程如下。

（1）新建一个对话框类，类名为CStretchImage。

（2）向对话框中添加按钮、静态文本、群组框、滑块、复选框和图像等控件。

（3）设置主要控件属性，如表20.2所示。

表20.2　控件属性设置

（4）向对话框中添加ZoomImage方法，对位图数据进行缩放操作。参数pBmpData表示源位图数据，pTmpData表示目标位图数据，nWidth和nHeight表示源位图的宽度和高度，nzmWidth和nzmHeight表示缩放后位图的宽度和高度，dXRate和dYRate表示水平和垂直方向的缩放比例，代码如下。

（5）处理“…”按钮的单击事件。利用文件打开对话框从磁盘中加载图像文件，读取位图文件头、位图信息头和位图数据。利用位图信息头获取位图的宽度和高度，并修改位图数据为4字节形式，便于进行图像处理，代码如下。

（6）处理“缩放”按钮的单击事件。根据用户选择的缩放方式和数据计算缩放后的图像宽度和高度，然后调用ZoomImage方法缩放图像数据，最后将4字节形式的图像（每个像素需要4个字节表示）数据转换为3字节形式的位图数据（每个像素需要3个字节来表示），并显示位图，代码如下。

代码贴士

GetCheck：该方法用于返回按钮的选中状态。

SetScrollRange：该方法用于设置滚动条的滚动范围。

20.7.4　单元测试

在开发完图像缩放模块后，为了保证程序正常运行，需要对该模块进行单元测试。单元测试在程序开发中非常重要，只有通过单元测试才能发现模块中的不足之处，使该模块更加完善。下面将对图像缩放模块中容易出现的错误进行分析。

在测试图像缩放模块的过程中，添加图片后会发生错误，从而退出程序。这是为什么呢？打开源代码，通过调试可以发现，在加载位图资源以后，用于记录位图数据大小的nBmpData变量的值竟然是空的，所以会产生错误。找到错误以后，解决起来就方便了。当发现记录的位图大小为0时，重新计算位图的大小，然后再赋值给nBmpData变量就可以了。添加的代码如下：

20.8　图像水印效果模块设计

20.8.1　图像水印效果模块概述

图像水印是指在原有图像的基础上添加文字。通常，网站为了防止图片被他人非法盗用，会为图片添加水印效果。图像水印效果模块的窗口界面如图20.17所示。

图20.17　图像水印效果模块

20.8.2　图像水印效果模块技术分析

图像水印处理技术应用得非常广泛。在图像处理模块中，设计了一个给图像添加水印的功能，主要是使用GDI+实现的。

首先使用Graphics类的DrawImage方法绘制位图，接着调用DrawString方法在位图中输出字符串，这样就实现了图像的水印效果。主要代码如下。

20.8.3　图像水印效果模块实现过程

图像水印效果模块的实现过程如下。

（1）创建一个对话框类，类名为CImageWater。

（2）向对话框中添加静态文本、按钮、群组框、编辑框和图像等控件。

（3）设置主要控件属性，如表20.3所示。

表20.3　控件属性设置

（4）处理“…”按钮单击事件，加载图像文件到对话框中。由于每次设置水印效果时需要清除原来的水印文字，这里采用的方式是在首次加载图像时，以流的形式保存图像信息。每次进行水印处理时，都从流中获取位图对象并进行处理，代码如下。

代码贴士

CreateStreamOnHGlobal：该函数用于在堆中创建一个流对象。

GetHBITMAP：该方法用于获取位图的句柄。

（5）处理“水印效果”按钮的单击事件，将用户设置的文本输出在预定的位置。这里主要使用GDI+提供的Graphics类实现，通过调用该类的DrawString方法可以在图像上输出文本，详细信息请参考20.8.2节，代码如下。

20.9　PSD文件浏览模块设计

20.9.1　PSD文件浏览模块概述

PSD文件是Photoshop图像软件使用的图像文件格式。由于Photoshop软件被广泛地应用于图像设计，因此能处理PSD文件也成为很多用户的需求。例如，用户可能需要在未安装Photoshop的情况下浏览PSD文件，需要将大量的PSD文件转换为其他格式的文件等。本模块实现了对PSD文件的浏览，窗口界面效果如图20.18所示。

图20.18　PSD文件浏览模块窗口

20.9.2　PSD文件浏览模块技术分析

PSD是Photoshop软件使用的图像格式。鉴于Photoshop软件的广泛应用，PSD也成了一种通用的图像格式。由于PSD文件格式不公开，因此不使用Photoshop软件很难读取PSD文件中的图像。当用户需要将大量的PSD文件转换为位图或JPEG图像时，只能通过Photoshop进行转换。在图像处理模块中，实现了对PSD文件的浏览和转换。实际上，只要能够读取PSD文件中的图像数据，就可以实现PSD文件的浏览和转换。为了读取PSD文件，这里使用了开发包libpsd，该开发包是基于开源的，用户可以在其基础上修改并重新发布。

实际上使用libpsd，用户可以非常方便地进行PSD文件操作。在获取解压后的PSD图像数据后，其每个像素占用4个字节，而不是像真彩色位图那样占用3个字节。每个像素占用4个字节避免了位图的字节对齐，因为每一行数据一定是4的整数倍，另外，像素采用4个字节可以方便地进行图像处理。在本模块的许多地方，位图数据也是采用4个字节方式存储，然后在保存或显示位图时，再将其转换为3个字节的形式。了解了这些内容，显示PSD文件就非常简单了。首先调用psd_image_load方法加载位图，然后根据第一个参数的merged_image_data成员获得解压后的图像数据，最后将4个字节形式的图像数据转换为3个字节形式的真彩色位图数据即可。主要代码如下：

20.9.3　PSD文件浏览模块实现过程

PSD文件浏览模块实现过程如下。

（1）新建一个对话框类，类名为CPsdBrown。

（2）向对话框中添加按钮、编辑框、群组框、单选按钮、静态文本和图像等控件。

（3）设置主要控件属性，如表20.4所示。

表20.4　控件属性设置

（4）引用libpsd 库的头文件，代码如下。

（5）处理“…”按钮的单击事件，加载PSD文件，读取PSD文件信息，例如，图层数和当前图层名称，读取PSD解压后的图像数据，将其转换为位图数据，代码如下。

（6）处理“保存”按钮的单击事件，将PSD文件保存为位图或JPEG图像。由于在加载PSD文件时已将PSD图像数据转换为位图数据，因此保存图像的操作也就相对简单了。

20.10　照片版式处理模块设计

20.10.1　照片版式处理模块概述

照片版式处理模块的主要功能是将图像按照照片大小或自定义大小进行剪裁，并且可以将剪裁的图像保存到磁盘文件中，以便于冲洗为照片。照片版式处理模块的运行效果如图20.19所示。

图20.19　照片版式处理模块

20.10.2　照片版式处理模块技术分析

在设计本模块的照片版式处理功能时，需要利用窗口来选择一个图像区域，效果如图20.20所示。

图20.20　局部选择窗口

在设计如图20.20所示的局部选择窗口时，可以采用CStatic类派生的子类来实现，但是由于用户可以调整选择窗口的大小，使用CStatic相对麻烦。这里可以改为对话框，由于对话框本身就具有动态调整大小的功能，将对话框作为子窗口，就可以实现想要的选择窗口的功能了。下面介绍其具体实现过程。

（1）创建一个对话框类，类名为CClipDlg，设置对话框属性，如图20.21所示。

图20.21　对话框属性窗口

（2）处理对话框的WM_CTLCOLOR消息，将设备上下文的背景设置为透明，代码如下。

（3）在初始化对话框时设置对话框的风格，代码如下。

（4）处理对话框的WM_LBUTTONDOWN消息，允许用户通过鼠标拖动对话框，代码如下。

（5）处理对话框的WM_NCPAINT消息，绘制对话框的红色边框，代码如下。

（6）处理对话框的WM_WINDOWPOSCHANGING消息，在对话框位置将要改变时限制对话框的位置，目的是防止窗口超出其父窗口，代码如下。

（7）处理对话框的WM_WINDOWPOSCHANGED消息，在对话框位置改变时更新父窗口，在父窗口中根据当前窗口的区域截取图像，将其显示在另一个窗口中，代码如下。

（8）处理对话框的WM_SIZING消息，在调整对话框大小时判断是否按宽度和高度的比例缩放。如果是，则以宽度或高度为基准，适当调整对话框的宽度或高度，代码如下。

（9）处理对话框的WM_SIZE消息，在对话框大小改变后更新父窗口，代码如下。

20.10.3　照片版式处理模块实现过程

照片版式处理模块的实现过程如下。

（1）新建一个对话框类，类名为CPsdBrown。

（2）向对话框中添加按钮、编辑框、群组框、单选按钮、静态文本和图像等控件。

（3）设置主要控件属性，如表20.5所示。

表20.5　控件属性设置

（4）处理“…”按钮的单击事件，加载图像文件，读取图像数据到流中，设置窗口的滚动范围，初始化单选按钮的状态，代码如下。

（5）处理照片选择窗口位置改变时的事件，获取其当前选择窗口区域的位图，将其显示在单独的区域中，代码如下。

（6）处理单选按钮的单击事件，根据用户选择的照片版式截取图像。代码首先遍历单选按钮，判断用户选中了哪个单选按钮，然后根据用户选择的版式确定截取图像的大小，最后截取并显示图像，代码如下。

（7）处理“实际大小”复选框选中时的事件。如果选中该复选框，将根据用户设置的图像选择窗口的位置来截取图像，代码如下。

20.11　开发技巧与难点分析

20.11.1　位图数据的存储形式

进行图像处理时，首先要解决的问题是实现对位图数据的直接操作。当然，可以通过设置图像的像素来实现图像处理，但是这样做有很多局限性。例如，当图像较大，需要使用滚动条来浏览图像时，通常只能处理当前可见部分，并且在滚动图像后会出现问题。因此，在进行图像处理时，通常是针对位图数据进行的。

在实现对位图的数据操作前，需要了解位图数据的存储形式。位图文件主要由4个部分构成，分别为位图文件头、位图信息头、调色板和实际的位图数据，如图20.22所示。

图20.22　位图结构

其中，位图文件头对应的结构为BITMAPFILEHEADER，共占用14个字节，其定义如下。

参数说明：

bfType：表示文件的类型，值为0x4d42，即字符串BM。

bfSize：表示文件的大小，也就是图20.22中描述的4个部分的大小。

bfReserved1、bfReserved2：是保留字。

bfOffBits：表示从文件头到实际的位图数据的偏移字节数，即图20.22中前3部分长度之和。

位图信息头对应的结构为BITMAPINFOHEADER，共占用40个字节，其定义如下：

参数说明：

biSize：表示该结构的大小，固定值为40。

biWidth：表示图像的宽度，单位为像素。

biHeight：表示图像的高度，单位为像素。

biPlanes：表示调色板的数量，必须为1。

biBitCount：指定表示颜色使用的位（Bit）数。如果为1，表示黑白二色图；如果为4，表示16色图；为8，表示256色图；为24，表示真彩色图。

biCompression：表示位图是否压缩。通常为BI_RGB，即不压缩。

biSizeImage：表示实际位图数据的大小。

biXPelsPerMeter：表示目标设备的水平分辨率，单位是像素数／米。

biYPelsPerMeter：表示目标设备的垂直分辨率，单位是像素数／米。

biClrUsed：指定图像实际用到的颜色数。如果为0，表示用到了2的biBitCount次方。

biClrImportant：表示图像中重要的颜色数。如果为0，表示所有颜色都是重要的。

调色板是一种GDI对象，可以认为它是一个颜色数组，列举了图像用到的所有颜色。对于真彩色位图来说，是没有调色板的，此时，在位图信息之后直接是位图的实际数据。调色板的结构为LOGPALETTE，其定义如下。

参数说明：

palVersion：表示系统的版本号。

palNumEntries：表示调色板中包含的项目数，每个项目表示一个颜色。

palPalEntry[1]：表示PALETTEENTRY数组中的第一个颜色。

PALETTEENTRY结构描述了颜色信息，其定义如下。

参数说明：

peRed：表示红色分量。

peGreen：表示绿色分量。

peBlue：表示蓝色分量。

peFlags：表示调色板中的项目如何被使用，可以为NULL。

对于使用了调色板的位图，位图实际数据描述的是像素值在调色板中的索引值，如果没有使用调色板，位图实际数据就是实际的RGB颜色值。

为了便于对图像进行操作，本章对位图的操作都是针对真彩色位图进行的。对于真彩色位图来说，没有调色板，一个像素需要3个字节来表示。在对位图数据进行操作时应注意以下问题。

位图数据每行的字节数必须是4的整数倍。

以真彩色位图为例，如果位图的宽度为150，由于一个像素需要3个字节，因此一行需要450个字节。由于450并不是4的整数倍，需要填充2个字节，变为452，为4的整数倍，这样一行需要填充2个字节。正是这个原因，在旋转位图时会导致源位图与目标位图的大小不一致。以150×100大小的位图为例，旋转后的图像大小为100×150，旋转后的宽度为100，每行需要300个字节，正好是4的整数倍，因此不再需要填充字节，这样就导致转换后的位图比原来的位图小。

位图数据是从下向上存储的。

对于位图数据来说，最低端的数据表示的是位图第一行的颜色数据，最上方的数据表示的是位图最后一行的颜色数据。每一行数据表示的颜色仍是按照从左向右存储的。

对于真彩色位图来说，每一行数据是按照BGR的颜色顺序表示的。

真彩色位图每一行数据表示的颜色都是按照绿、红、蓝顺序，而非红、绿、蓝顺序。例如，每行第1个字节表示蓝色分量，第2个字节表示绿色分量，第3个字节表示红色分量，第4个字节表示蓝色分量，以此类推。

20.11.2　在Visual C++中使用GDI+

在Visual C++ 6.0中，实现各种类型的图像转换是比较复杂的，有时还需要用户了解图像的各种格式，以及图像的编码、解码算法。使用GDI+，则可以非常方便地实现图像类型的转换。在设计图像类型转换时，采用的就是GDI+技术。

GDI+是微软.net类库的一个组成部分，它并没有集成在Visual C++ 6.0开发环境中，但却可以在Visual C++ 6.0环境下使用GDI+。下面介绍如何在Visual C++ 6.0中使用GDI+。

（1）下载GDI+包文件。

（2）引用Gdiplus.h头文件。

（3）引用Gdiplus命名空间，代码如下。

（4）定义两个全局变量，代码如下。

（5）在应用程序或对话框初始化时加载GDI+，代码如下。

（6）在应用程序结束时卸载GDI+，代码如下。

（7）在程序中链接gdiplus.lib库文件，代码如下。

20.12　小结

使用Visual C++进行图像处理具有先天性的优势，但是对于初学者来说也可以称之为不足。因为Visual C++中没有现成的控件进行图像处理。要进行图像处理必须了解图像的文件格式和各种图像处理算法，这无疑增加了初学者的学习难度。本章通过20多个小模块实现图形图像处理系统，虽然由于篇幅的限制，仅对几个重要和典型模块进行了讲解，但是能够充分地向读者说明图像处理系统是如何开发的。学完本章以后，读者可以对图像进行简单的处理，以及对图像进行格式转换等操作。