You are on page 1of 414

Vi

s lC++ 编程与项目开发
ua

李 英 编著
图书在版编目(
CIP数据
Vis lC++编程与项目开发/李英编著.—上海:华东理
ua
工大学出版社, 2008.1
ISBN978 7 5628 2204 2

Ⅰ.Vis
ual.
.. Ⅱ.李... Ⅲ.C 语 言 程序设计
Ⅳ.
TP312

中国版本图书馆 CIP 数据核字(


2007)第177232号


isu
alC++编程与项目开发
·······················································································

编 著/李 英
责任编辑 / 李国平 纪冬梅
责任校对 / 金慧娟 张 波
封面设计 / 戚亮轩
出版发行 / 华东理工大学出版社
社 址:上海市梅陇路130号, 200237
电 话:( 64250306(营销部)
021)
传 真:
(021)
64252707
网 址:
www. hd
lgpres
s.c
om.
cn
印 刷 / 江苏通州市印刷总厂有限公司
开 本 /787mm×1092mm 1/16
印 张 /26
字 数 /646千字
版 次 /2008年1月第1版
印 次 /2008年1月第1次
印 数 /1—3050册

书 号 /ISBN978 7 5628 2204 2/TP爛152


定 价 /39.80元(附赠光盘)

(本书如有印装质量问题,请到出版社营销部调换。)
内 容 简 介

本书主要讲述 C+ + 语言基础、VC+ + 编程技术、软件项目开发过程和开发实例。 全书共包


括十七章:第一章软件开发环境与软件项目开发过程,第二章 C+ + 语言基础,第三章 Windows
应用程序编程与 MFC,第四章文档/视图结构及其编程,第五章程序界面设计,第六章对话框
与控件,第七章绘图,第八章文件操作,第九章打印,第十章异常处理,第十一章数据库编程,
第十二章动态链接库,第十三章 ActiveX 控件,第十四章多媒体技术,第十五章多进程与多
线程编程,第十六章网络通信编程,第十七章项目开发实例—学生管理信息系统。
书中列举了大量精心编制的实例,实例在配套的光盘中,所有实例都已通过调试。
本书可供大专院校师生、从事 IT 业的工程技术人员及所有编程爱好者使用 。 既适用于
初学 Visual C+ + 的读者,也适用于已有过一些编程经验和项目开发经验的读者 。
前 言

随着计算机可视化技术的发展,可视化编程技术已广泛应用于教育、科研、工程和金融
等领域,越来越多的人员开始研究并应用可视化编程技术。 Visual C+ + 是 Microsoft 公司
目前极为流行的可视化软件开发工具之一 。
目前有许多介绍 Visual C+ + 的书,有的以介绍基础知识为主,有的以项目的开发为主。
以介绍基础知识为主的书,容易偏向于知识点的简单罗列。 以项目开发为主的书以一个项
目的开发过程来讲述,如果读者某一个环节没有掌握好,那么对这本书后面的内容就难以掌
握,并且在程序编制过程中一个项目贯穿始终,由于重要概念和内容没有单独提出,读者难
以读懂代码,很难掌握。
本书根据作者多年的本科教学和研究生教学经验以及实际开发经验写成,围绕项目开
发所需的基本知识点将复杂的概念用通俗易懂的语言和配以实例的方式来说明,最后综合
运用编程基础知识和软件项目开发知识给出项目开发的实例 。 本书力求深入浅出,重点和
全面相统一。书中列举的大量实例是经过精心编制而成的,在实例说明中采用了较为独特
的说明方式:先将实例程序要实现的功能和界面表示出来,然后给出每个实例详细的开发制
作步骤和源代码;读者根据实例说明的步骤都可方便地将实例制作出来(在教学中已验证)。
本书主要讲述 C+ + 语言基础、VC+ + 编程技术、软件项目开发过程和开发实例。 全书共包
括十七章:第一章软件开发环境与软件项目开发过程,第二章 C+ + 语言基础,第三章 Windows
应用程序编程与 MFC,第四章文档/视图结构及其编程,第五章程序界面设计,第六章对话框
与控件,第七章绘图,第八章文件操作,第九章打印,第十章异常处理,第十一章数据库编程,
第十二章动态链接库,第十三章 ActiveX 控件,第十四章多媒体技术,第十五章多进程与多
线程编程,第十六章网络通信编程,第十七章项目开发实例—学生管理信息系统。 张春亮同
志参与本书第十七章的编写和第十七章例题的制作 。在本书的编写过程中得到顾春华等同
志的帮助,在此深表感谢。
由于笔者水平有限,本书不足之处在所难免,敬请读者批评指正。

编 者
2007 年 11 月
目 录

第一章 软件开发环境与软件项目开发过程 ……………………………………… 1



1 VC60用户界面 …………………………………………………………………… 1

11 工程工作区窗口 ……………………………………………………………… 2

12 主工作区窗口 ………………………………………………………………… 3

13 输出窗口 ……………………………………………………………………… 4

2 VC6 0菜单介绍 …………………………………………………………………… 4

21 Fi
le菜单 ……………………………………………………………………… 4

22 Edit菜单 ……………………………………………………………………… 5

23 View 菜单 ……………………………………………………………………… 6

24 In
sert菜单 …………………………………………………………………… 6

25 Pro
jet菜单 …………………………………………………………………… 6


26 Bui
ld菜单 ……………………………………………………………………… 8

27 Debug菜单 …………………………………………………………………… 8

28 Toos菜单 …………………………………………………………………… 9


29 Windows菜单 ………………………………………………………………… 9

210 Help菜单 …………………………………………………………………… 10

211 右键菜单(快捷菜单)………………………………………………………… 10
3 使用 C
1 l
assWiz
ard ………………………………………………………………… 11

31 Mes
sageMaps标签 …………………………………………………………… 11

32 MemberVar
iab
les标签 ……………………………………………………… 12

33 Automa
tion标签 ……………………………………………………………… 13

34 Act
iveXEven
ts标签 ………………………………………………………… 14

35 Cl
ass标签 ……………………………………………………………………… 15
4 软件项目开发过程 ………………………………………………………………… 16


41 软件生存期 …………………………………………………………………… 16

42 制定计划 ……………………………………………………………………… 16

43 需求分析 ……………………………………………………………………… 17

44 软件设计 ……………………………………………………………………… 18

45 编码 …………………………………………………………………………… 19

46 测试 …………………………………………………………………………… 19

47 软件维护 ……………………………………………………………………… 21
5 面向对象方法的软件项目开发过程 ……………………………………………… 21


51 软件生存期与类生存期 ……………………………………………………… 21

52 面向对象的软件项目开发过程 ……………………………………………… 22 1

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


6 Vi s
ualC++编程规范 …………………………………………………………… 24

61 基本要求 ……………………………………………………………………… 24

62 命名 …………………………………………………………………………… 24

63 注释与可读性 ………………………………………………………………… 25

64 结构化要求 …………………………………………………………………… 25
第二章 C++语言基础 ……………………………………………………………… 26
1 简单的 C++程序和 C++语言的特点 ………………………………………… 26


11 简单的 C++程序 …………………………………………………………… 26

12 C++语言的基本特点 ……………………………………………………… 27
2 数据类型、变量和运算符 …………………………………………………………… 28


21 基本数据类型 ………………………………………………………………… 28

22 加修饰符的基本数据类型 …………………………………………………… 29

23 变量 …………………………………………………………………………… 29

24 数组 …………………………………………………………………………… 30

25 结构 …………………………………………………………………………… 32

26 枚举 …………………………………………………………………………… 33

27 联合 …………………………………………………………………………… 33

28 指针 …………………………………………………………………………… 34

29 类型定义 ……………………………………………………………………… 35

210 运算符 ………………………………………………………………………… 35
3 流程控制语句 ……………………………………………………………………… 38


31 表达式语句和块语句 ………………………………………………………… 38

32 选择语句 ……………………………………………………………………… 38

33 swi
tch分支语句 ……………………………………………………………… 39

34 循环语句 ……………………………………………………………………… 40

35 转移语句 ……………………………………………………………………… 40
4 函数 ………………………………………………………………………………… 41


41 函数定义 ……………………………………………………………………… 41

42 函数的参数传递 ……………………………………………………………… 41

43 局部变量和静态变量 ………………………………………………………… 42

44 内联函数 ……………………………………………………………………… 43

45 函数重载 ……………………………………………………………………… 43

46 函数模板 ……………………………………………………………………… 44

47 多态性和虚函数 ……………………………………………………………… 44
5 类和对象 …………………………………………………………………………… 46


51 类的定义和声明 ……………………………………………………………… 46

52 对象 …………………………………………………………………………… 47

53 构造函数和析构函数 ………………………………………………………… 48

54 继承和派生 …………………………………………………………………… 48
2 2
55 t
his指针 ……………………………………………………………………… 50
目 录
……………………………………………………………………………………………………………………………………………

6 常类型(
2 con
st ……………………………………………………………………… 50

61 常引用 ………………………………………………………………………… 50

62 常对象 ………………………………………………………………………… 51

63 常成员函数 …………………………………………………………………… 52

64 常数据成员 …………………………………………………………………… 52
7 运算符重载 ………………………………………………………………………… 53


8 I /O输入/输出)流结构 …………………………………………………………… 55
9 异常处理 …………………………………………………………………………… 56


91 异常处理的语法 ……………………………………………………………… 56

92 异常处理的执行过程 ………………………………………………………… 57
第三章 Wi
ndows应用程序编程与 MFC ………………………………………… 59
1 MFC 类库 …………………………………………………………………………… 59


11 MFC 基础类库 ………………………………………………………………… 60

12 COb
jet类 …………………………………………………………………… 60

2 利用 MFC 创建 Wi
3 ndows应用程序框架 ………………………………………… 64
3 程序中的文件和主要类 …………………………………………………………… 70


31 程序中的文件和主要类 ……………………………………………………… 70

32 应用程序类( CMyPain
terApp类)…………………………………………… 70

33 程序的其他类 ………………………………………………………………… 74
4 消息和消息处理 …………………………………………………………………… 75


41 消息的分类 …………………………………………………………………… 76

42 消息映射 ……………………………………………………………………… 77

43 消息处理函数 ………………………………………………………………… 79

44 消息传递 ……………………………………………………………………… 83
第四章 文档/视图结构及其编程 …………………………………………………… 85
1 文档 ………………………………………………………………………………… 85


11 使用文档管理数据的一般步骤 ……………………………………………… 85

12 文档类(
CDo
c t类)中的主要数据成员和成员函数 …………………… 85
umen

13 多文档类型 …………………………………………………………………… 87
2 视图 ………………………………………………………………………………… 87


21 视图操作的一般步骤 ………………………………………………………… 87

22 视图类( ew 类)中的主要成员函数 ……………………………………… 87
CVi

23 多视图 ………………………………………………………………………… 88

24 派生的视图类 ………………………………………………………………… 88
3 框架(边框窗口)类 ………………………………………………………………… 89

4 文档模板类 ………………………………………………………………………… 90

5 文档/视图结构各对象之间的关系 ………………………………………………… 90

6 文档/视图结构编程实例 …………………………………………………………… 91


61 单文档应用程序实例 ………………………………………………………… 91

62 多文档应用程序实例 ………………………………………………………… 92 3

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


63 多视图应用程序实例 ………………………………………………………… 94

64 文档与视图结构之间相互作用关系分析实例 ……………………………… 99
第五章 程序界面设计 ………………………………………………………………… 100
1 界面设计原则 ……………………………………………………………………… 100


11 界面布局原则 ………………………………………………………………… 100

12 用户帮助模型 ………………………………………………………………… 102
2 菜单 ………………………………………………………………………………… 102


21 菜单资源编辑器 ……………………………………………………………… 102

22 CMenu类 ……………………………………………………………………… 103

23 CCmdUI类 …………………………………………………………………… 104

24 标准菜单编程实例 …………………………………………………………… 105

25 带有图标的菜单编程实例 …………………………………………………… 107

26 快捷菜单(上下文菜单,右键菜单)编程实例 ………………………………… 108

27 动态菜单编程实例 …………………………………………………………… 109
3 工具栏 ……………………………………………………………………………… 111


31 工具栏资源编辑器 …………………………………………………………… 111

32 CToo
l r类 …………………………………………………………………… 112
Ba

33 常规工具栏编程实例 ………………………………………………………… 113

34 下拉式工具栏按钮编程实例 ………………………………………………… 115

4 CReBar和 CD
ial r …………………………………………………………… 117
ogBa

41 CReBar类 ……………………………………………………………………… 117

42 CDi
al r类 ………………………………………………………………… 118
ogBa

43 编程实例 ……………………………………………………………………… 118
5 状态栏 ……………………………………………………………………………… 121


51 CS
tat
u r类 ………………………………………………………………… 121
sBa

52 状态栏的创建 ………………………………………………………………… 121

53 编程实例 ……………………………………………………………………… 122
第六章 对话框与控件 ………………………………………………………………… 125
1 对话框基本知识 …………………………………………………………………… 125


11 对话框的组成 ………………………………………………………………… 125

12 对话框的类型 ………………………………………………………………… 125

13 编写对话框程序的流程 ……………………………………………………… 126
2 消息对话框 ………………………………………………………………………… 126


21 消息对话框函数 ……………………………………………………………… 126

22 消息对话框编程实例 ………………………………………………………… 127
3 对话框资源编辑器 ………………………………………………………………… 128

4 控件 ………………………………………………………………………………… 128

5 对话框类与对话框调用 …………………………………………………………… 131


51 对话框类 ……………………………………………………………………… 131
4 6
52 对话框调用及其编程实例 …………………………………………………… 131
目 录
……………………………………………………………………………………………………………………………………………

6 对话框数据交换与验证机制 ……………………………………………………… 135




61 对话框数据交换 ……………………………………………………………… 135

62 对话框数据验证 ……………………………………………………………… 135

63 四则运算编程实例 …………………………………………………………… 135
7 常用控件应用 ……………………………………………………………………… 141


71 常用控件编程实例一 ………………………………………………………… 141

72 常用控件编程实例二 ………………………………………………………… 147
8 图像列表控件、列表控件与树形控件的应用 ……………………………………… 152


81 图像列表控件 ………………………………………………………………… 152

82 列表控件 CLi
stCtl…………………………………………………………… 153


83 树形控件 CTreeCt
rl ………………………………………………………… 155

84 编程实例 ……………………………………………………………………… 157
9 属性单、属性页和向导 ……………………………………………………………… 164


91 CPr
ope
rt e类 …………………………………………………………… 165
yPag

92 CPr
ope
rtyShe
et类 …………………………………………………………… 165

93 编程实例 ……………………………………………………………………… 166
10 通用对话框类 ……………………………………………………………………… 170


10
1 CFontD
ialog类 ……………………………………………………………… 170

10
2 CFi
leDi
alog类 ……………………………………………………………… 171

10
3 CCol
orDia
log类 ……………………………………………………………… 171

10
4 CPr
intD
ialog类 ……………………………………………………………… 171

10
5 CFi
ndReplac
eDi
alog类 ……………………………………………………… 171
第七章 绘图 ……………………………………………………………………………… 173
1 设备环境类 ………………………………………………………………………… 173


11 CDC 类 ………………………………………………………………………… 173

12 映射模式 ……………………………………………………………………… 174
2 GDI对象与 CGd
7 iObje
ct类 ………………………………………………………… 174

21 画笔(
CPen …………………………………………………………………… 175

22 画刷(
CBruh ………………………………………………………………… 176


23 字体(
CFon ………………………………………………………………… 177


24 位图(
CBi
tma p ……………………………………………………………… 179

3 CPoint、
CSie和 CRe
z ct …………………………………………………………… 181

31 CPoi
nt 类 ……………………………………………………………………… 181

32 CSi
ze 类 ………………………………………………………………………… 181

33 CRe
ct类 ……………………………………………………………………… 182
4 常见的绘图任务 …………………………………………………………………… 182


41 绘制图形的一般步骤 ………………………………………………………… 182

42 绘制文本 ……………………………………………………………………… 183

43 绘点 …………………………………………………………………………… 183

44 绘直线 ………………………………………………………………………… 183 5

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


45 绘矩形 ………………………………………………………………………… 184

46 绘椭圆 ………………………………………………………………………… 184

47 绘弧线 ………………………………………………………………………… 185

48 绘位图 ………………………………………………………………………… 185
5 绘图编程实例 ……………………………………………………………………… 186


51 GDI对象和基本绘图函数的应用编程实例 ………………………………… 186

52 鼠标绘图编程实例 …………………………………………………………… 192

53 动态绘图编程实例 …………………………………………………………… 202
第八章 文件操作 ……………………………………………………………………… 207

1 CF i
le类 ……………………………………………………………………………… 207

11 打开文件 ……………………………………………………………………… 208

12 关闭文件 ……………………………………………………………………… 209

13 文件读写 ……………………………………………………………………… 209

14 文件定位 ……………………………………………………………………… 209
2 文件流f
8 st
ream …………………………………………………………………… 210

21 打开文件 ……………………………………………………………………… 210

22 关闭文件 ……………………………………………………………………… 211

23 编程实例 ……………………………………………………………………… 211

3 CArch
ive类与序列化 ……………………………………………………………… 214

31 创建可序列化的类 …………………………………………………………… 214

32 序列化对象 …………………………………………………………………… 215

33 CArchi
ve类的数据成员和成员函数 ………………………………………… 215

34 Se
ria
lie函数串行化处理数据 ……………………………………………… 216

第九章 打印 ……………………………………………………………………………… 219
1 MFC 的基本打印和打印预览 ……………………………………………………… 219


11 缺省打印实例 ………………………………………………………………… 219

12 视类中的打印函数 …………………………………………………………… 220

13 打印控制过程 ………………………………………………………………… 221
2 打印缩放、映射模式选择及其编程实例 …………………………………………… 222

3 多页打印及其编程实例 …………………………………………………………… 223

第十章 异常处理 ……………………………………………………………………… 226
10
1 CEx
cet
pion类 …………………………………………………………………… 226
10
11 CEx
cet
pion类的函数 ……………………………………………………… 226
10
12 CEx
cet
pion类的派生类 …………………………………………………… 227
2 文件异常 CF
10 i
leEx
cet
pion类 ……………………………………………………… 227
10
21 CF
ileEx
cet
pion类数据成员 ………………………………………………… 227
10
22 CF
ileEx
cet
pion类成员函数 ………………………………………………… 228
10
23 编程实例 ……………………………………………………………………… 228
6 3 数据库异常类 ……………………………………………………………………… 229
10
目 录
……………………………………………………………………………………………………………………………………………

第十一章 数据库编程 ………………………………………………………………… 231


1 数据库概述 ………………………………………………………………………… 231
11
11
11 数据库对象 …………………………………………………………………… 231
11
12 ODBC ………………………………………………………………………… 232
11
13 DAO ………………………………………………………………………… 232
11
14 OLEDB ……………………………………………………………………… 232
11
15 ADO ………………………………………………………………………… 232
2 ODBC 类与 ODBC 数据库应用程序 …………………………………………… 232
11
11
21 ODBC 类 ……………………………………………………………………… 232
11
22 注册数据库 …………………………………………………………………… 235
11
23 ODBC 数据库编程实例 ……………………………………………………… 236
3 ADO 数据库编程 ………………………………………………………………… 245
11
11
31 ADO 结构 …………………………………………………………………… 245
11
32 连接对象 ……………………………………………………………………… 246
11
33 命令对象 ……………………………………………………………………… 246
11
34 记录集对象 …………………………………………………………………… 247
11
35 域对象 ………………………………………………………………………… 248
11
36 参数对象 ……………………………………………………………………… 248
11
37 错误对象 ……………………………………………………………………… 249
11
38 集合 …………………………………………………………………………… 249
11
39 ADO 数据库编程实例 ……………………………………………………… 251
第十二章 动态链接库 ………………………………………………………………… 260
1 动态链接库的分类与创建 ………………………………………………………… 260
12
12
11 动态链接库分类 ……………………………………………………………… 260
12
12 使用 AppWi
zad创建 MFCDLL ………………………………………… 262

2 从 DLL 中导出函数 ……………………………………………………………… 262
12
12
21 使用.DEF 文件导出函数 …………………………………………………… 263
12
22 使用关键字_ de
clspe
cd
llexpo
rt…………………………………………… 263
12
23 使用 _ _
AFX EXT CLASS 导出和导入 ……………………………………… 264
12
24 调用约定 ……………………………………………………………………… 264
3 链接 DLL 到可执行程序 ………………………………………………………… 264
12
12
31 隐式链接 ……………………………………………………………………… 264
12
32 显式链接 ……………………………………………………………………… 265
4 动态链接库编程实例 ……………………………………………………………… 265
12
12
41 编程实例一———采用关键字导出、隐式链接 ……………………………… 265
12
42 编程实例二———采用.DEF 导出、显式链接 ……………………………… 267
12
43 编程实例三———扩展 MFC 的 DLL ……………………………………… 269
12
44 编程实例四———动态链接库嵌套 …………………………………………… 271
12
45 编程实例五———带数据库的动态链接库 …………………………………… 273


isu
alC++编程与项目开发 ……………………………………………………………………………………………………

第十三章 Ac
tieX 控件 ………………………………………………………………… 278

13
1 Ac t
iveX 的基本概念 ……………………………………………………………… 278
13
11 组件对象模型 COM ………………………………………………………… 278
13
12 对象链接与嵌入 ……………………………………………………………… 279
13
13 自动化服务器与自动化控制器 ……………………………………………… 280
13
14 Acti
veX 控件 ………………………………………………………………… 280
13
2 Ac t
iveX 控件编程实例 …………………………………………………………… 280
13
21 Ac
tieX 控件框架的创建 …………………………………………………… 281

13
22 控件的类 ……………………………………………………………………… 282
13
23 Ac
tieX 控件的测试 ………………………………………………………… 283

13
24 控件的外观设计 ……………………………………………………………… 283
13
25 设置属性 ……………………………………………………………………… 287
13
26 设置事件 ……………………………………………………………………… 289
13
27 设置方法 ……………………………………………………………………… 289
13
3 Ac t
iveX 控件的注册 ……………………………………………………………… 290
13
4 Ac t
iveX 控件应用编程实例 ……………………………………………………… 290
第十四章 多媒体技术 ………………………………………………………………… 293
1 多媒体文件格式 …………………………………………………………………… 293
14
2 播放多媒体文件 …………………………………………………………………… 293
14
3 MCI控制方法 …………………………………………………………………… 294
14
14
31 MCI设备类型 ……………………………………………………………… 294
14
32 MCI函数接口 ……………………………………………………………… 294
14
33 常用的 MCI命令消息 ……………………………………………………… 295
4 多媒体文件调用编程实例 ………………………………………………………… 296
14
第十五章 多进程与多线程编程 ……………………………………………………… 298
1 多进程编程 ………………………………………………………………………… 298
15
15
11 进程 …………………………………………………………………………… 298
15
12 创建进程 ……………………………………………………………………… 298
15
13 结束进程 ……………………………………………………………………… 300
15
14 多进程编程实例 ……………………………………………………………… 301
2 多线程编程 ………………………………………………………………………… 304
15
15
21 线程的创建与结束 …………………………………………………………… 304
15
22 线程的调度和优先级 ………………………………………………………… 306
15
23 线程间通信 …………………………………………………………………… 307
15
24 多线程编程实例 ……………………………………………………………… 308
3 线程同步 …………………………………………………………………………… 313
15
15
31 线程同步的概念 ……………………………………………………………… 313
15
32 临界区 ………………………………………………………………………… 313
15
33 事件内核对象 ………………………………………………………………… 313
8 15
34 互斥内核对象 ………………………………………………………………… 314
目 录
……………………………………………………………………………………………………………………………………………

15
35 信号量内核对象 ……………………………………………………………… 314
15
36 线程同步编程实例 …………………………………………………………… 315
第十六章 网络通信编程 ……………………………………………………………… 320
16
1 TCP/ IP 协议 ……………………………………………………………………… 320
16
11 TCP/IP 协议的体系结构 …………………………………………………… 320
16
12 IP 地址和通信端口 ………………………………………………………… 320
16
13 客户机/服务器模式 ………………………………………………………… 321
16
2 So c
ket概念与 Wi
ndowsSo
cke I …………………………………………… 322
tAP
16
21 Soc
ket的类型 ………………………………………………………………… 322
16
22 阻塞和非阻塞 ………………………………………………………………… 322
16
23 WindowsSocke I主要函数 …………………………………………… 322
tAP
16
24 s
tructso
ckaddr结构 ………………………………………………………… 325
16
25 WindowsSocke I辅助函数 …………………………………………… 325
tAP
16
3 Wi ndowsSo
c t编程流程与编程实例 ………………………………………… 326
ke
16
31 流套接字编程流程 …………………………………………………………… 326
16
32 数据报套接字编程流程 ……………………………………………………… 328
16
33 WindowsSocke I编程实例 …………………………………………… 328
tAP
4 MFC 中的 Wi
16 nso
ck ……………………………………………………………… 335
16
41 CAsyncSo
cket类及其主要成员函数 ……………………………………… 336
16
42 CAsyncSo
c t类编程实例 ………………………………………………… 337
ke
16
43 CSo
c t类 …………………………………………………………………… 343
ke
5 串行端口通信编程 ………………………………………………………………… 343
16
16
51 Wi I串行端口通信函数及编程实例 …………………………… 344
ndowsAP
16
52 利用端口函数实现串行端口通信编程 ……………………………………… 351
16
53 MSComm 控件及其编程实例 ……………………………………………… 351
第十七章 项目开发实例———学生管理信息系统 ………………………………… 354
1 管理信息系统设计原则 …………………………………………………………… 354
17
2 需求分析 …………………………………………………………………………… 354
17
17
21 系统主要功能 ………………………………………………………………… 354
17
22 数据流 ………………………………………………………………………… 355
3 系统设计 …………………………………………………………………………… 356
17
17
31 系统的功能模块 ……………………………………………………………… 356
17
32 业务流程设计 ………………………………………………………………… 357
17
33 数据库设计 …………………………………………………………………… 357
4 详细设计 …………………………………………………………………………… 359
17
17
41 主体框架模块 ………………………………………………………………… 359
17
42 登录权限验证模块 …………………………………………………………… 363
17
43 重新封装 ADO ……………………………………………………………… 367
17
44 院系数据管理模块 …………………………………………………………… 374
17
45 学生数据管理模块 …………………………………………………………… 377 9

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

17
46 课程成绩管理模块 …………………………………………………………… 386
17
47 课程成绩统计模块 …………………………………………………………… 389
17
48 帮助的制作 …………………………………………………………………… 393
5 项目包装和项目打包 ……………………………………………………………… 394
17
17
51 项目包装 ……………………………………………………………………… 394
17
52 项目打包 ……………………………………………………………………… 395
参考文献 …………………………………………………………………………………… 400

10
第一章 软件开发环境与软件
项目开发过程
软件开发环境是支持软件产品开发的软件系统。 20 世纪 90 年代,面向对象技术和构
架/构件技术受到学术界和工业界的重视,出现了支持这些技术的软件开发环境。 Visual
C++ 就是其中的一种。软件工程是要用工程化、规范化的方法实现软件的开发和维护,软件
工程确定和规范了软件项目的开发过程和开发方法 。
本章将介绍 Visual C++ 60 集成开发环境(IDE),从软件工程的角度介绍软件项目开发
的各个过程及各过程的主要任务 。
在本书以下的叙述中,为了简单起见,将 Visual C++ 60 简称为 VC60。
本章要点:
* VC60 的用户界面
* VC60 的菜单使用
* ClassWizard 的使用方法
* 软件项目开发过程
* Visual C++ 编程规范


1 VC6
0 用户界面
VC60 是 Microsoft Developer Studio 的组件之一,而后者通常被称为集成开发环境
(IDE),亦即 VC60 的用户界面。图 1 1 是 VC60 的用户界面,它的界面是智能化的,而且非


图1 1 VC6.
0用户界面

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

常宽容,鼓励用户去实践和尝试。
VC60 的用户界面由上端的菜单栏和工具栏、左边的工程工作区 (ProjectWorkspace)、
右边的进行文件与资源编辑的主工作区以及下端的输出窗口 (Output)和状态栏组成。 在调
试时,平台还提供各种窗口,包括观察窗口、变量窗口、寄存器窗口、存储器窗口、调试堆栈窗
口和反汇编窗口等。


11 工程工作区窗口
在工程工作区窗口下端有 3 个标签:ClassView、ResourceView 和 FileView。
(1)ClassView
ClassView 视图中以树形结构显示了某一工程创建的所有类,并在每个类中列出了成员
变量和成员函数,图 1 2 即显示了工程工作区的 ClassView 视图。 每一个类首先列出带有
紫色图标的成员函数,然后才是带有绿蓝色图标的成员变量,每个成员的图标左边都有一个
标志,以表示成员类型和存取类别的信息,保护型成员图标旁边的标志为一钥匙,私有成员
的标志是一挂锁,而公有成员图标旁边没有标志。
在 ClassView 视图中双击类名,会在主工作区中打开这个类的头文件,显示出类的声
明;而双击某个类的成员,则主工作区中会显示该成员的定义代码。 右击某个类名,可以弹
出一个快捷菜单对类进行操作。
(2)ResourceView
单击 ResourceView 标签,会在工程工作区窗口中将应用程序中所有的可视资源显示为
树状结构,如图 1 3 所示。

图1 2 C
las
sVew 视图
i 图1 3 Re
sou
rceV
iew 视图

在 Windows 编程中,经常会用到不同种类的资源,例如位图、光标、图标、菜单、工具栏和
对话框模板等。该视图用于管理各种资源。
在编程过程中,程序员可能只是需要查看工程中资源文件的内容,而不是打开一个资
2 源。例如,程序员可能试图在所有的对话框资源中搜索一个字符串,而不必单独地打开每一
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

个对话框资源。这时可以使用以文本形式打开资源文件的方法看到资源,并且执行文本编
辑器支持的全局操作。
资源编辑器具有可视化的技术和界面,可以快速并且简单地创建和修改应用程序的资
源。可以使用资源编辑器创建一个新的资源,修改一个已经存在的资源,拷贝一个已经存在
的资源或删除一个旧的资源。使用资源编辑器的主要目的就是为了简单而方便地进行上述
的操作。当创建或者打开一个资源时,相应的资源编辑器就自动打开了。 同时,也可以在一
个文本格式的文件里打开一个资源文件 。
(3)FileView
在 FileView 视图中,工程文件以树状结构列表,列表文件分为源文件、头文件、资源文
件和帮助文件 4 种类型,如图 1 4 所示。
在此视图中用鼠标右键单击文件夹,在弹出的如图 1 5 所示的右键菜单 (快捷菜单 )中
选择 New Folder,就会弹出如图 1 6 所示的新建文件夹对话框。在对话框中,用户可以定义
文件夹的名称和其所包含文件的扩展名,从而将自定义的文件种类加入到工程 。

图1 5 鼠标放在文件夹上的右键菜单

图1 4 F
ileV
iew 视图 图1 6 NewFo
ldr对话框

在 FileView 视图中能够对工程文件进行添加、删除和拷贝,如果在视图中存在多个工
程,则可以通过拖曳在工程之间交换文件 。双击视图中列出的文件,就可以将此文件打开进
行编辑。而删除文件时,选择要删除的文件,按下 Del 键即可。在 FileView 视图中单击鼠标
右键,可以弹出快捷菜单,对项目工程和文件进行操作。 其中,某些文件前向下的箭头表示
该文件正在被当前设置使用。


12 主工作区窗口
用户在开发应用程序的过程中,源代码和资源的编辑工作都是在主工作区窗口中进行
的。当双击工程工作区中的某一项时,在主工作区中就会出现相应的屏幕显示 。 例如,在工
程工作区的 ClassView 视图中双击某成员函数,主工作区中就会出现该成员函数的源代码。 3

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

用户可以像编辑文本那样,对源代码进行修改。如果在工程工作区的 ResourceView 视图中


双击某一个资源,例如一个位图,在主工作区中就会出现位图图像。 用户可以使用这时同时
出现的绘图工具来编辑位图。
当在主工作区中进行代码编辑时,用户会注意到出现的代码使用了色彩调配,这就是所
谓的语法着色。默认情况下,常规代码为黑色,注释为绿色而C++ 关键字为蓝色。 用户也可
自行定义语法着色。选择 Tool- > Options 菜单命令,这时会出现如图 1 7 所示的 Options
对话框,在该对话框的 Format 标签下,可以定义语法着色的格式。

图1 7 定义语法着色(
Opt
ios对话框)

语法着色可以帮助用户发现一些由于粗心而造成的错误,这些错误往往不容易被发现。
例如,用户在定义整型变量时,将 int 输入为 itn,由于 itn 不会变成蓝色,这就提醒了用户
关键字拼写错误从而在进行编译之前防止了许多编译错误的发生 。


13 输出窗口
图 1 1 所示的下部空白区是输出窗口,用于显示输出内容和错误信息,该窗口有 5 个标
签:Build、Debug、Find in Files 1、Find in Files 2 和 Results。
Build 标签:用于显示编译和链接的结果。
Debug 标签:用于通知来自调试器的提示。
Find in Files 1 标签:用于显示 Find in Files 查找的结果。
Find in Files 2 标签:用于显示两个 Find in Files 结果区域,其一显示前一结果。
Results 标签:用于显示使用 profiler 等工具后的结果。


2 VC6
0 菜单介绍
菜单栏包括 10 个菜单项,界面显示 9 个,其中 Debug 菜单与 Build 菜单切换显示。菜单项
提供的命令可以完成几乎所有 VC60 的功能,因此了解和掌握这些菜单命令是非常必要的。


21 F
ile菜单
4 File(文件)菜单中主要包括编程过程中对文件进行操作的命令选项,见表 1 1。
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

表1 1 F
ile菜单
菜 单 项 作 用
New 创建一个新的文件、工程、工程工作区或其他文件
Open 打开一个现存文件或工程
Close 关闭打开的文件
Open Workspace 打开一个工程的工作区
Save Workspace 保存工作区中的内容
Close Workspace 关闭工程工作区
Save 保存当前活动窗口中的内容
Save As 用新的文件名来保存打开的文件
快速保存所有窗口中的内容,对于新的窗口,将激活 Save As
Save All
对话框窗口来选择文件
Page Setup 页面设置
Print 打印全部或部分当前被编辑的窗口
Recent Files 最近打开的文件
Recent Workspaces 最近打开的工程
Exit 关闭 VC60


22 Ed
it菜单
Edit(编辑)菜单主要用于文本文件的编辑、查找和替换,见表 1 2。
表1 2 Ed
it菜单
菜 单 项 作 用
Undo 取消最近的编辑操作
Redo 恢复 Undo 操作
Cut 剪切
Copy 拷贝
Paste 粘贴
Delete 删除被选择的内容。若清除的内容可恢复,则 Undo 按钮有效
Select All 选定当前文件的所有内容。在对话框编辑窗口中,则选择所有控件
Find 在当前窗口文件中寻找指定内容
Find in Files 在多个文件中寻找指定内容
Replace 在文件中对指定内容进行替换
Go To 弹出 Go To 对话框,将光标移动到指定行
显示一个允许读者在 Code 窗口中使用、创建或删除位置标志符,移
Bookmarks
到下一个或上一个书签(bookmark),或清除所有书签
Advanced 给出针对 Code 窗口中代码的一系列编辑方法
打开 BreakPoints 对话框,该对话框可设置、删除、禁止、激活或查
Breakpoints
看断点,读者设置的断点将作为工程的一部分保存起来
List Members 给出一个所选类或结构中合法成员变量或函数的列表
Type Info 获得类型信息
Parameter Info 获得参数信息
当用户输入了足够的字符后,系统将弹出一个用户已输入单词的列表,
Complete Word 允许用户从中选择自己要输入的整个单词。当系统能够根据用户已输
入字符唯一的匹配以前输入的单词时,系统将自动拼出整个单词 5

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


23 V
iew 菜单
View(视图)菜单主要用于对集成开发环境中一些窗口的控制,见表 1 3。

表1 3 V
iew 菜单

菜 单 项 作 用

ClassWizard 打开类向导对话框,见 13 节

显示 Resourse Symbols 对话框窗口,窗口中列出工程中所


Resourse Symbols 有资源的 ID 值,可以在这 个 窗 口 中 增 加 或 修 改 资 源 的
ID 号

显示 Resourse Includes 对话框窗 口 ,窗 口 中 列 出 工 程


Resourse Includes
中所使用的符号头文件及其对应的编译时间伪指令

Full Screen VC++ 应用程序的编辑区全屏显示


Workspace 显示工程工作区窗口
Output 显示数据输出窗口
Debug Windows 选择不同的调试窗口项
Refresh 刷新屏幕显示

显示所选对象的属性对话框,对于任何一个对象,都可以
Properties
选择此项来查看该对象的属性


24 I
nse
rt菜单
Insert(插入)菜单主要用于创建新的类、表单或资源,也可以通过对菜单项的选择将已
有文件插入到当前文件中或将新的 ATL 对象插入到项目中,见表 1 4。

表1 4 I
nse
rt菜单

菜 单 项 作 用

New Class 打开 New Class 对话框,用于创建新的类库

New Form 打开 New Form 对话框,用于创建新的表单


打开 Insert Resource 对话框,创建新的资源或插入资源
Resource
到资源文件中
Resource Copy 复制选定的资源
打开 Insert File 对话框,用于将已有文件的内容插入到
File As Text
当前文件中
New ATL Object 向工程中添加新的 ATL 对象


25 P
roec
j t菜单
Project(项目)菜单主要用于项目和工作区的管理,用于向当前项目内插入类、资源和
6 文件,见表 1 5。
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

表1 5 Pr
oje
ct菜单

菜 单 项 作 用
Set Active Project 设置某工程为工作区中的活动工程
Add to Project 向项目中添加文件、文件夹、数据链接以及组件等
Dependencies 能够编辑工程之间的依赖关系
弹出图 1 8 所示 Project Settings 对话框,该对话
Settings
框有以下 10 个标签,见表 1 6
Export Makefile 能够以 Make 文件格式导出可建立的工程
Insert Project into
能够向工作区中插入工程
Workspace

图1 8 Pr
oje
ctS
ett
ins对话框

表1 6 Pr
oje
ctS
ett
ins对话框标签

标 签 作 用
用于在 Appwizard 建立工程时选择静态库或共享 DLL,以及确定中间文件(源文件
general
和对象)和输出文件(EXE、DLL、DCX)存放的目录
Debug 设置调试器的有关选项
编译器设置。默认情况下 Category 组合框选中 General。用户可通过从组合框
C/C++ 选中一个种类来改变 Category 的设置。用户可改变优化标准(可选择的标准是:
Maximize Speed、Minimize Size、Customize 和 Disable)和警告等级
Link 控制着链接选项,这些选择不需改变,设置分为 5 个种类
Resource 能够修改应用程序所使用的语言,也能选择编译到应用程序中的资源
OLE Type 用于从对象描述文件(ODL)建立类型库
用于处理 Go To Definition,Go To Declaration 及其他类似选项时使用的 Browse
Browse Info
Info(.bsc)文件
Custom Build 允许用户按自己的过程创建应用程序
Pre_Link Step 在链接步骤之前,允许插入预链接命令
Post_Build Step 可以在其他步骤已成功完成之后增加自己的步骤


isu
alC++编程与项目开发 ……………………………………………………………………………………………………


26 Bu
id菜单

Build(生成)菜单主要用于编译和建立应用程序,见表 1 7。
表1 7 Bu
ild菜单

菜 单 项 作 用
Compile 编译当前的源代码文件
Build 编译当前的可执行文件
Rebuild All 重新编译所有的文件
Batch Build 设置要通过批处理编译的工程
Clean 删除工程编译过程中的中间文件和输出文件
Start Debug 开始程序的调试,程序运行到断点或一直到结束
Debugger Remote 打开 Remote Connection 对话框,用于链接远程过
Connection 程,对远程过程进行调试
Execute 执行一个 exe 可执行文件
Set Active Configuration 将某个工程的编译或链接配置激活
Configurations 配置当前项目的输出模式
Profile 设置工程的配置文件


27 Deb
ug菜单
启动 VC60 的调试器后(可按 F5 键),集成开发环境中的 Build 菜单被 Debug 菜单取代,
Debug 菜单中包括了调试过程中常用到的命令,见表 1 8。
表1 8 De
bug菜单

菜 单 项 作 用
Go 开始程序的调式,程序运行到断点或一直到结束
Restart 重新开始程序的调试
Stop Debugging 停止程序的调试
Break 暂停程序的调试
Apply Code Changes 调试过程中把对代码的更改应用到源文件中
调试过程中单步执行下一条语句,并且能够对程序中任
Step Into 何可访问的函数调用进行跟踪,进入函数内部,查看其运
行情况
与 Step Into 功能类似,区别在于它并不对程序中的函数
Step Over
调用进行跟踪,而是直接执行该调用语句
可以用此命令使程序直接向下运行,直到从函数内部返
Step Out
回,在函数调用语句后的语句处停止
Run to Cursor 程序运行到光标所在的位置时停止
Step Into Specific
单步执行选定的函数
Function
弹出对话框,显示各种异常并设置程序对异常是否停止
Exceptions
运行
显示程序调试过程中所有可用的线程,可以对其进行设
Threads
置,挂起和恢复线程并设置焦点
Modules 显示当前程序中装入的模块
Show Next Statement 显示即将执行的代码行
8 QuickWatch 查看、修改变量和表达式,也可以将其添加到观察窗口
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………


28 Too
ls菜单
Tools(工具)菜单主要用于定制菜单、工具箱和工具栏,设置宏等操作,见表 1 9。

表1 9 To
ols菜单

菜 单 项 作 用
Source Browser 弹出浏览器窗口
Close Source Browser File 关闭浏览文件
出现可视化组件管理器,它可以极大地方便各种
Visual Component Manager
组件的应用
Register Control 向 Windows 操作系统注册 OLE 控件
Error Lookup 检查 Win32 API 函数返回的标准错误代码信息
ActiveX Control
为测试 Active 控件提供了一个简单的环境
Test Container
提供安装在系统上的所有 OLE 和 ActiveX 对象的
OLE/COM Object Viewer
信息
Spy++ 提供观察 Windows 操作细节的方法
在调试应用程序时,可以用来激活各种级别的调
MFC Tracer 试信 息, 再 由 MFC 发 送 到 Developer Studio 的
Output 窗口
弹出 Customize 对话框,可以定制命令、工具栏、菜
Customize
单和快捷键
弹出如图 1 7 所示的 Options 对话框,用户可以
Options 在这个对话框中修改 VC60 的环境设置。Options
对话框中的标签见表 1 10

表1 10 Op
tio
ns对话框标签

标 签 作 用
Editor 选择滚动条、拖放和下拉,并设置自动保存和加载
Tabs 设置与 Tabs 键和缩进相关的选项
Debug 设置调试过程中所显示的信息
Compatibility 选择模拟其他编辑器全部或部分界面
Build 生成一个外部编译文件或者一个建立日志
Directories 设置包含文件、可执行文件、库以及源文件的路径
Workspace 设置浮动窗口、状态栏和工程重新加载
Data View 管理 Data View 的显示
Macros 设置重新加载一个修改宏的规则
Help System 设置帮助系统的语言与集合
Format 设置色彩体系,包括对源文件和其他窗口的语法着色方法


29 Wi
ndows菜单
Windows(窗口)菜单主要用于开发环境中窗口及其属性的控制,见表 1 11。 9

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表1 11 Wi
ndows菜单

菜 单 项 作 用
New Window 打开包含当前原文件的新窗口
Split 将窗口拆分为多个窗口
Docking View 控制窗口的停靠特征
Close 关闭当前窗口
Close All 关闭主工作区内的所有窗口
Next 使下一个窗口成为当前窗口
Previous 使上一个窗口成为当前窗口
Cascade 使当前所有打开的窗口在屏幕上向下重叠排放
Tile Horizontally 使主工作区内当前所有打开的窗口在屏幕上横向平铺
Tile Vertically 使主工作区内当前所有打开的窗口在屏幕上纵向平铺
Open Windows 列出最近打开的窗口文件名,最多可以列出 10 个文件名
Windows 弹出 Windows 对话框,从中可关闭、保存或激活选中窗口


210 He
lp菜单
Help(帮助)菜单提供了各种浏览、检索 VC60 技术文档的工具以及帮助用户查看 VC60
的帮助信息、升级信息以及版本信息等,见表 1 12。
表1 12 He
lp菜单
菜 单 项 作 用
Contents 打开 MSDN Library 帮助系统,并进入其中的目录标签
Search 进入帮助系统的搜索标签
Index 进入帮助系统的索引标签
Use Extension Help 启用扩充帮助系统,而禁止 MSDN Library 帮助系统
弹出 Help Keyboard 对话框,选择上端下拉框中的选项,可
Keyboard Map
以查看所定义的快捷键
Tip of the Day 显示 Tip of the Day 对话框
Technical Support 得到 Microsoft 所提供的技术支持网址
Microsoft on the Web 列出 Developer Studio 和其他产品的 Web 地址列表
About Visual C++ 弹出VC++ 的 About 对话框


211 右键菜单(快捷菜单)
在 VC60 中有许多右键菜单,鼠标在不同位置点击右键弹出的右键菜单是不相同的,下
面主要介绍两种右键菜单。
表1 13 右键菜单(鼠标放在类上)

菜 单 项 作 用

Go to Definition 打开类的头文件,光标到类的定义处

Add Member Function 弹出向类中添加成员函数对话框

Add Member Variable 弹出向类中添加成员变量对话框


10
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

续 表
菜 单 项 作 用
Add Virtual Function 弹出向类中添加虚函数对话框
Add Windows Message Handler 弹出向类中添加消息函数对话框
References 类的信息
Derived Classes 查看本类有哪些派生类
Base Classes 查看本类的基类
New Folder 弹出新建文件夹对话框
对 class View 视图中所选类的成员变量
Group by Access
和函数按公有、 保护和私有类型进行排列
Docking View 工程工作区窗口的显示方式
Hide 显示或隐藏工程工作区窗口
Properties 类的特性

表1 14 右键菜单(鼠标放在菜单、工具条空白处)

菜 单 项 作 用
Output 打开或关闭输出窗口
Workspace 打开或关闭工程工作区
Standard 打开或关闭 Standard 工具栏
Build 打开或关闭 Build 工具栏
Build MiniBar 打开或关闭 Build MiniBar 工具栏
ATL 打开或关闭 ATL
Resource 打开或关闭 Resource 工具栏
Edit 打开或关闭 Edit 工具栏
Debug 打开或关闭 Debug 工具栏
Browse 打开或关闭 Browse 工具栏
Database 打开或关闭 Database 工具栏
Source Control 打开或关闭 Source Control 工具栏
WizardBar 打开或关闭 WizardBar 工具栏
Customize 打开或关闭 Customize 对话框

3 使用 C
1 las
sWi
zar

在 VC60 中打开一个工程后,选择菜单 View- > ClassWizard 或使用快捷键 Ctrl+ W,打
开如图 1 9 所示的 MFC ClassWizard 对话框,该对话框包含 5 个标签,现分述如下。


31 Me
ss s标签
ageMap
此标签内容用于处理消息映射,允许程序员添加或删除 Windows 消息句柄,这是设计
Windows 事件驱动程序的基本环节。默认情况下,MFC ClassWizard 对话框显示此标签内容,
如图 1 9 所示,它包括的内容见表 1 15。 11

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图1 9 C
las
sWi
zad的 Me
r ssa
geMa
ps标签

表1 15 Me
ssa
geMa
ps标签的内容

控 件 作 用
Project 下拉列表框 选择要处理的工程
Class name 下拉列表框 选择要处理的类
Object IDs 列表框 列出了应用程序中所有能够接受消息的对象标识符
Messages 列表框 列出了当前所选对象所能处理的消息
Member functions 列表框 列出了当前类中的所有成员函数
Add Class 按钮 打开 New Class 对话框,用于创建新的类库
Add Function 按钮 能够为工程添加新的消息处理成员函数
Delete Function 按钮 能够删除在 Member functions 列表框所选中的消息处理函数
Edit Code 按钮 能够编辑在 Member functions 列表框所选中的消息处理函数


32 Membe
rVa
riab
les标签
在该标签中能够为应用程序中的类创建成员变量(也称数据成员),这些类往往是和一
些控件联系在一起的。此标签如图 1 10 所示,它包括的内容见表 1 16。
表1 16 Memb
erVa
ria
bls标签内容

控 件 作 用
Project 下拉列表框 选择要处理的工程
Class name 下拉列表框 选择要处理的类
控件成员列表框 列出了控件标识符、与控件对应的变量及变量类型
Add Class 按钮 打开 New Class 对话框,用于创建新的类库
Add Variable 按钮 能够为选定的控件增加成员变量
Delete Variable 按钮 能够删除选定控件的成员变量
Update Columns 按钮 能够选择数据源,该按钮只对记录集有效
12 Bind All 按钮 能够进行数据源绑定
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

图1 10 C
las
sWi
zad的 Memb
r erVa
ria
bls标签


33 Au
toma
tin标签

此标 签 为 程 序 员 提 供 了 对 OLE 自 动 化 类 的 方 法 和 属 性 的 管 理 和 使 用, 主 要 用 于
Automation 功能管理,此标签如图 1 11 所示,它包括的内容见表 1 17。
表1 17 Au
toma
tin标签内容

控 件 作 用
Project 下拉列表框 选择要处理的工程
Class name 下拉列表框 选择要处理的类
列出了已添加到当前类中的方法及属性的外部名,供 Automation
External names 列表框
客户程序使用
Implementation 编辑框 显示 External names 列表框中所选项的属性
Add Class 按钮 打开 New Class 对话框,用于创建新的类库
允许 用 户 为 当 前 类 添 加 新 的 Automation 方 法,该 项 只 对
Add Method 按钮
Automation 类有效

允许 用 户 为 当 前 类 添 加 新 的 Automation 属 性,该 项 只 对
Add Property 按钮
Automation 类有效
Delete 按钮 允许用户删除当前所选定的方法或属性名
Edit Code 按钮 能够编辑在 Member functions 列表框所选中的消息处理函数
Data Binding 按钮 用于指定 Automation 控件支持的数据绑定层次

13

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图1 11 C
las
sWi
zad的 Au
r toma
tin标签


34 Ac
tiveXEven
ts标签
此标签管理 ActiveX 类所支持的 ActiveX 事件。此标签如图 1 12 所示,它包括的内容
见表 1 18。

图1 12 C
las
sWi
zad的 Ac
r tiv
eXEv
ent标签

表1 18 Ac
tiv
eXEv
ent标签内容

控 件 作 用
Project 下拉列表框 选择要处理的工程
Class name 下拉列表框 选择要处理的类
14
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

续 表
控 件 作 用
列出了已添加到当前类中的成员函数与成员变量名,供 Automation 客
External names 列表框
户程序使用
Implementation 编辑框 显示 External names 列表框中所选项的属性
Add Class 按钮 打开 New Class 对话框,用于创建新的类库
能够为 当 前 类 添 加 新 的 Automation 事 件, 该 项 只 对 Automation 类
Add Event 按钮
有效
Delete 按钮 能够删除当前所选定的 Automation 事件


35 C
las标签

此标签用于显示应用程序中所包含类的一般信息,包括定义的头文件和源文件类名,以
及与之相联系的基类。此标签如图 1 13 所示,它包括的内容见表 1 19。
表1 19 Ca
lssI
nfo 标签内容

控 件 作 用
Project 下拉列表框 选择要处理的工程
Class name 下拉列表框 选择要处理的类
File details 组框 列出当前类源文件、头文件、基类与资源信息
Message filter 下拉列表框 列出当前类的所有有效信息,并按所属窗口分类
Foreign class 下拉列表框 列出与当前类有关的外部类名
Foreign variable 下拉列表框 列出与当前类有关的外部变量
Add Class 按钮 打开 New Class 对话框,用于创建新的类库

图1 13 C
las
sWi
zad的 C
r las标签

15

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

4 软件项目开发过程


41 软件生存期
软件工程是要用工程化、规范化的方法实现软件的开发和维护。 软件生存期模型是对
软件的整个开发过程从工程学的意义上给出的总体结构框架 。 有多种软件生存期模型。 如
瀑布模型、逐增模型、变化模型等。根据瀑布模型软件项目开发过程的总体流程如图 1 14
所示。

图1 14 软件开发过程


42 制定计划
制定计划是项目开发的第一步,通常包括以下几个方面。
(1)项目实施计划
这是软件开发的综合性计划,通常应包括任务、进度、人力、环境、资源和组织等多个方
面。其目的是提供一个框架,使得软件项目的主管人员可以对资源 、成本以及进度合理的估
算。这些估算应在软件项目开始后的一个有限时间内完成 。
(2)质量保证计划
把软件开发的质量要求具体的规定为每一个开发阶段可以检查的质量指标和保 证
活动。
(3)软件测试计划
规定测试活动的任务、测试方法、测试进度、测试资源和测试人员的职责等。
(4)文档编制计划
规定所开发软件项目应编写的文档种类 、内容、进度和编写人员的职责等。
(5)用户培训计划
规定对用户进行技术培训的目标 、要求、进度和参与技术培训的人员的职责等 。
(6)综合支持计划
规定项目开发过程中所需的支持条件,以及如何获取和利用这些支持条件 。
(7)软件分发与维护计划
16 规定软件项目完成后,用户在运行阶段应如何进行软件维护以及软件维护的任务、方
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

法、资源和维护人员的职责等。


43 需求分析
需求分析的基本任务是把“用户需要系统做什么?”转换成一个完全的精细的软件逻辑
模型并写出软件的需求规格说明书,准确地表达用户的要求。 目的是使系统开发方与用户
方对软件的目的和要达到的功能达成一致的见解 。
(1)需求分析阶段的基本任务
① 确定目标系统的综合要求
这也就是解决所开发的软件要做什么,做到什么程度。主要从以下四个方面考虑:
● 功能需求 即要划分出软件系统必须完成的全部功能 。
● 性能需求 给出所开发软件的技术性能指标,包括存储容量限制、运行时间限制、安
全保密性要求等。
● 环境要求
主要表现为对系统运行环境的要求 。运行环境一般包括软件支持环境和
硬件运行环境以及系统运行的人文环境 。
● 扩充要求
虽然系统的扩充要求不属于当前系统的开发范围,但经过分析知道用户
将来还要做什么工作。因此,了解系统的扩充要求,留给用户一个系统再扩充的自由环境,
对软件开发方和用户来说,都将是很有益的。
② 分析系统的数据要求
对系统中所涉及的全部数据进行分析是需求分析阶段的一个很重要的任务 。 软件开发
人员需要准确而全面地定义数据,给出数据的结构及其处理的直观而规范的描述 。
③ 导出系统的逻辑模型
此阶段的逻辑模型是指比较详细的数据流图 、数据词典等。 逻辑模型的导出过程,也就
是数据流图的分解和细化过程。
④ 修正开发计划
通过需求分析阶段的工作,会进一步加深对系统的了解,也就可能发现可行性分析阶段
制定的软件计划中存在的问题,修正这些问题,对系统的开发意义很大。
⑤ 开发原型系统
开发原型系统是指在需求分析阶段建造软件 “样机”。 其目的主要是显示系统的功能,
使用户通过实践来获得未来系统将如何工作的直观概念,从而使用户可以更准确地提出和
修改他们的要求,使所开发的系统、软件能够得到更加令人满意的结果 。
⑥ 写出需求规格说明书
需求规格说明书,又称软件规格说明书,它是软件需求分析阶段的结果,是软件开发和
软件验收以及软件管理的重要依据 。
(2)需求分析的结构化方法(SA)
结构化分析方法主要采取自顶向下逐层分解的分析思想和原则 。 利用结构化分析
方法对目标系统进行分析,分 析 的 结 果 将 以 规 格 化 的 软 件 需 求 说 明 书 的 形 式 给 出 。 软
件需求说明书由一套分层的数 据 流 图 、一 本 数 据 词 典 、一 组 小 说 明 、相 关 的 一 些 补 充 材
料组成 。
① 数据流图
数据流图描述系统由哪些部分组成以及各部分之间有什么联系 。 数据流图从数据传递 17

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

和加工的角度,以图形的方式来刻画系统中数据流从输入到输出的移动变化过程,具体实例
可参见第 17 章的图 17 1。
② 数据词典
数据词典描述系统中的每一个数据 。数据流图给出了系统的组成及其内部各元素相互
之间的关系,但却未说明数据元素的含意。 仅靠数据流图人们无法理解它所描述的对象。
数据词典的任务是对数据流图中出现的所有被命名的图形元素,包括数据流、加工、数据文
件、数据元素,以及数据的源、汇点等,在数据词典中作为一个词条加以定义,使得每一个图
形元素都有一个准确的解释。


44 软件设计
软件设计阶段的主要任务就是根据需求分析得到的结果进行软件的设计工作,即在明
确了“做什么”的基础上,解决“如何做”的问题。
软件设计阶段通常分为两个步骤:一是系统设计,二是详细设计。 系统设计阶段主要解
决的问题是如何构成实现系统需求的程序模块及其结构 。这包括如何把欲开发的软件系统
划分成若干个模块,并确定模块间的相互关系,以及模块之间信息是怎样传递的等问题 。 详
细设计就是对每个模块内部进行算法设计 。
(1)系统设计
系统设计就是设计系统的 功 能 模 块,画 出 系 统 的 模 块 结 构 图 。 结 构 化 设 计 方 法 用
模块结构图表达系统模块之间的关系 。 由于数据流图和模块结构图之间存在着一定的
联系,因此结构化设计方法可 以 和 需 求 分 析 中 采 用 的 结 构 化 分 析 方 法 很 好 地 衔 接 。 使
用结构化设计方法的关键是恰当地划分模块,恰当地处理模块之间的联系,以便达到较
好的设计效果 。
① 结构化设计方法的步骤
结构化设计方法是在模块化 、自顶向下 、逐层细化的结构程序设计等技术基础上发
展起来的一种方法,其基本设计思想是将系统设计成由相对独立 、功能单一的模块组成
的结构 。
用结构化设计方法进行系统设计时一般分为两步 。
建立初始结构图:一个满足系统要求的初始结构图,可以从需求分析阶段使用结构化

分析方法获得的数据流程图中导出 。
改进初始结构图:建立初始结构图之后,应该审查分析这个结构,通过模块的分解或

合并来降低模块间的相互联系,也即降低模块间的耦合度,提高模块的聚合度。
② 结构化设计的描述工具———模块结构图
模块结构图又被称作程序结构图,是采用结构化设计方法进行软件总体设计时的重要
描述工具。模块结构图被用来描述系统的层次和分块结构关系,它能够很清晰地表达出模
块之间的相互联系。
模块结构图中的模块以矩形表示,在矩形框内可以标以模块的名字或功能的缩写字。
模块间的连线表示两个模块间的调用关系。 通常处在不同位置的两个模块,上层为调用模
块,下层为被调用模块。 在结构化设计中模块有 3 种形式,即顺序调用、选择调用和循环
调用。
18 ③ 从数据流程图导出模块结构图
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

(2)详细设计
系统设计完成软件系统的总体模块结构设计,并规定了各个模块的功能及各模块之间
的相互联系。详细设计就是对每个模块内部进行算法设计 。
① 结构化详细设计的思想和方法
结构化详细设计使用一组基本逻辑构造,利用这组逻辑构造可以构成任何程序。 这些
构造就是顺序构造、条件构造和重复构造。顺序构造是实现任何一个算法的基本处理步骤;
条件构造根据某种逻辑条件的出现而选择相应的处理步骤;而重复构造则是为循环而设置
的。这 3 种构造是结构程序设计的基础。
任何一个程序,不管它的应用领域或者技术复杂性如何,都可以仅用这 3 种结构化构造
来设计和实现。
② 结构化详细设计的描述工具———流程图
结构化详细设计的描述工具有流程图 、方块图、HIPO 图和 PAD 图等。流程图又被称为程
序框图。流程图往往独立于各种程序设计语言,而且具有直观、清晰的特点。 用于描述结构
化程序的流程图由上述 3 种基本结构组成。
详细设计完成后应交付的文件主要有详细设计说明书和初步的模块开发卷宗 。


45 编码
所谓软件编码,就是将软件详细设计转换成计算机可以接受的程序代码,也就是选择一
种特定程序设计语言来编写源程序 。


46 测试
为了保证软件质量需要进行软件测试,软件测试的主要任务是发现并排除在分析、设
计、编码各个阶段中可能产生的各种类型的错误并加以修正,以便得到一个可运行的、可靠
的软件系统。它是保证软件可靠性的主要方法之一 。
(1)软件测试的一些基本原则
① 在开始测试时,不应默认程序中不会找到错误。
② 测试不应由编写程序的个人或小组承担 。
③ 测试文件必须说明预期的输出结果 。
④ 要对合理的和不合理的输入数据都进行测试 。
⑤ 除检查程序功能是否完备外,还应检查程序功能是否多余,换句话说,还应当检查该
程序是否产生了不希望的副作用 。
⑥ 应该完整地保留所有的测试文件 (包括测试数据集、预期的结果、程序执行的记录
等),直至该软件产品废弃不用为止。
(2)软件测试的基本手段
软件测试的基本手段有 3 种,即动态检测、静态检测和软件的正确性证明。 动态检测方
法中使用的最为普遍的是动态分析技术。 可以把程序看成是一个函数,此函数描述了输入
和输出之间的关系。输入的全体叫程序的定义域,输出的全体叫程序的值域。 该方法是使
程序有控制的运行,从多个角度观察程序运行时的行为,以发现其中存在的错误。 一个动态
检测过程可分为以下 5 步:
① 选取在定义域中的有效值,或定义域外的无效值。 19

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

② 对已选的值决定预期的结果。
③ 用选取的值执行程序。
④ 观察程序的行为,并获取结果。
⑤ 将结果与预期的结果对比,如果不吻合,则证明程序存在错误。
这种方法的有效性取决于测试中选取的用来运行程序的值,即所谓的测试用例。 人们
希望用最小的测试用例组合得到最大的测试彻底度 。 因此,如何设计好的测试用例与如何
衡量彻底度,便成为测试工作两个关键性的技术问题 。
按照产生测试用例的不同方式,动态测试可分为功能测试和结构测试两种。 功能测试
又叫“黑盒测试”。它从需求分析的系统说明书出发,按程序的输入、输出特性和类型选择测
试数据。结构测试又叫“白盒测试”,测试用例的产生涉及程序的具体结构,所以它反映程序
的结构性质。产生的测试用例应使程序的所有语句至少执行一次;或使程序的所有通路至
少通过一次,也即使程序的每个分支至少通过一次。 另外,动态检测技术还有接口测试,它
包括测试数据接口和控制接口。测试数据接口主要是测试例行程序或模块间的数据传递的
正确性;测试控制接口主要是测试例行程序或模块间的调用关系的正确性 。
静态测试的对象可以是需求文件、设计文件或程序,应力争找出其中的全部错误或可疑
之处。静态测试时不执行被分析的程序 。
流程图分析法也是常用的一种静态测试技术,它是通过分析程序的流程图来实现错误
检测的,它只分析代码的结构而不执行代码 。因此,流程图分析方法比较适合于编码实现阶
段。流程图分析所能得到的信息主要有:
① 语法错误信息。
② 每个语句中标识符的引用分析,如变量、参数等。
③ 每个例行程序调用的子例行程序和函数 。
④ 未给初值的变量。
⑤ 已定义的但未使用的变量。
⑥ 未经说明或无用的符号。
⑦ 对任何一组输入数据均不可能执行到的代码段 。
使用流程图分析比较直观,而且能为动态测试产生测试数据,并显示各种测试数据执行
的路径,便于分析测试结果。
(3)软件测试的基本步骤
软件测试可分为 3 个基本步骤:单元测试、组装测试和确认测试。
① 单元测试
单元测试是对程序的每个模块进行独立测试 。 根据详细设计的说明,应测试重要的控
制路径,力求在模块范围内发现错误。
单元测试一般以白盒法为主,而且可以使多个模块平行进行。 在单元测试中,一般同时
还对模块接口、局部数据接口进行测试。
② 组装测试
组装测试即把每个已通过测试的模块并入软件总体结构中进行组装和测试 。 每并入一
个模块,都要找出由此产生的错误。组装测试通常采用黑盒法来设计测试用例 。
③ 确认测试
20 确认测试是根据软件需求说明书中定义的全部功能和性能要求及确认测试计划,来测
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

试整个软件系统是否达到要求,并提交最终的用户手册和操作手册 。
确认测试应交付的文件有确认测试分析报告、最终的用户手册和操作手册及项目开发
总结报告。


47 软件维护
软件维护是指在软件系统交付用户使用以后,为了改正错误或满足新的需要而修改软
件的过程。它是软件系统提交用户运行阶段对软件系统实施的质量保证手段 。 软件系统交
付用户使用以后,在软件运行过程中仍然存在一些原因需要对软件进行变动 。

5 面向对象方法的软件项目开发过程


51 软件生存期与类生存期
为适应面向对象开发方法的特点,给出如图 1 15 的软件生存期模型。
在图 1 15 中,各个阶段的顺序是线性的,但开发过程实际上不是线性的。 使用瀑布模
型时存在的问题是它没有考虑超出一个单独的项目的情形,也没有考虑任何比整个应用更
小的“产品”。软件开发经济学强调软件部件的复用,因此,必须重视把开发可复用的软件作
为应用开发过程的一部分。面向对象方法把类当做单元,而且可分别考虑类的生存期与软
件的生存期。

图1 15 一个基于复用的软件生存期

在面向对象软件开发过程中,需要特别重视复用。 因此,软件部件应当独立于当初开发
它们的应用而存在的类生存期。 部件的开发瞄准某些局部的设计和实现,因此能够帮助解
决当前的问题,但为了在以后的项目中使用,它们还应当足够通用。 在以后的应用开发中,
可以调整这些独立部件以产生适合于特定问题解决的功能 。类就是一个希望能够复用的单
元。图 1 16 为类生存期模型。
类生存期与软件生存期是交叉的 。就是说,类的标识是软件生存期的一个阶段,但类生
存期的步骤独立于任一特殊应用的开发 。这样可以完整地描述一个基本实体 。 而不仅仅考
虑当前正在开发的系统。
通常面向对象的设计(OOD)分为两个阶段:高层设计 (或系统设计 )和低层设计 (或详细
设计)。高层设计主要是建立应用的体系结构,包括像开发用户界面那样的问题的解决部 21

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图1 16 类生存期

分。低层设计集中于类的详细设计阶段 。


52 面向对象的软件项目开发过程
下面把软件生存期与类生存期放在一起,阐述面向对象的软件项目开发过程,图 1 15
为开发的步骤。
(1)分析阶段
分析阶段包括两个步骤:论域分析和应用分析。 它们都要标识问题论域中的抽象。 在
分析中,需要找到特定对象,基于对象的公共特性把它们组合成集合,直到最后能够标识出
对这个问题的一个抽象为止。
① 论域分析
论域分析开发问题论域的模型。 论域分析应当在应用分析之前进行,因为在了解问题
之前应当对问题敞开思想考虑,不应加以限制。客户可能会改变需求,而且问题环境也可能
改变,所以,论域分析应考察问题论域内的一个较宽的范围,分析覆盖的范围应比直接要解
决的问题更广。
② 应用分析
应用(或系统)分析细化在论域分析阶段所开发出来的信息,并且把注意力集中于当前
要解决的问题。因为通过论域分析,分析人员具有了较宽的论域知识,因而能开发出更好的
抽象。
面向对象分析(OOA)是软件开发过程中的问题定义阶段,图 1 17 是 Coad 与 Yourdon 提
出的面向对象分析的五个层次的概念模型 。
(2)高层设计(系统设计)
在一个纯面向对象环境中,系统设计与类设计常常是同一的过程,但还是应当把系统与
类的设计分开。
系统设计包括将系统划分为子系统的决策;子系统的软、硬件分配;设计框架的主要概
念和策略性决策。
构建待开发软件的总体模型。在这个阶段,标识在计算机环境中进行问题解决工作所
需要的概念,并增加了一批需要的类。 这些类包括那些可使用应用软件与系统的外部世界
22 交互的类。此阶段的输出是适合应用软件要求的类 、类间的关系、应用的子系统视图规格说
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

图1 17 面向对象分析的概念模型

明。通常,利用面向对象设计得到的系统框架如图 1 18 所示。

图1 18 OOD设计导出的结构

① 面向对象设计的五个层次和四个部分 。
面向对象设计建立的是求解域的对象模型 。 在设计阶段,OOD 模型是 OOA 模型的扩展。
OOD 模型同样包括 OOA 模型的五个层次,但同时又引进了四个部分。
问题域部分(PDC):面向对象分析的结果直接放入该部分 。
人机交互部分(HIC):包括对用户分类、描述人机交互的脚本、设计命令层次结构、设计
详细的交互、生成用户界面的原型、定义 HIC 类等。
任务管理部分(TMC):识别任务 (进程)、任务所提供的服务、任务的优先级、进程的驱动 23

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

模式,以及任务与其他进程和外界如何通信等 。
数据管理部分(DMC):确定数据存储模式,如使用文件系统、关系数据库管理系统还是面
向对象数据库管理系统等。
② 子系统间的交互方式。
在应用系统中,子系统之间的关系可分为客户/服务器关系和同等伙伴关系两种。 这两
种关系对应两种交互方式,即客户/服务器交互方式和同等伙伴交互方式 。
客户/服务器交互方式 客户/服务器交互方式也称单向交互方式。 在该交互方式中,
作为客户的子系统调用作为服务器的子系统,执行某些服务后并返回结果。 在该交互中,任
何交互行为都是客户驱动的,因此,作为客户的子系统必须了解作为服务器的子系统的接
口,而服务器不必了解客户的接口。
同等伙伴交互方式 同等伙伴交互方式也称双向交互方式。 在该交互方式中,每个子
系统都可能调用其他子系统,因此,每个子系统都必须了解其他子系统的接口,子系统间必
须相互了解接口。
(3)详细设计(类的开发)
详细设计阶段基本上是类的开发,因为一个应用往往是用一个单个的类表示,它亦可分
成几个类。高层设计将标识对各个类的要求,且给出类的定义。 这个阶段贯穿于各个类的
生存期。结果是支持这个应用的一组类。
(4)实例的建立
这个阶段的结果就是对问题的解决 。在此阶段要建立各个对象的实例 。
(5)组装测试
这一阶段把系统当做一个完整的应用来进行测试 。各个类的封装和类测试的完备性可
减少组装测试所需要的时间。
(6)维护
维护的要求将影响应用和各个类 。多数维护活动发生在类级。


6 V
is lC++ 编程规范
ua
在软件项目的开发过程中,制定一个统一的编程规范是非常必要的,这样可以使得软件
的可维护性大大提高,以下主要说明在 Visual C++( 简称 VC++) 平台下进行项目开发时的编
程规范。


61 基本要求
(1)程序结构清晰,简单易懂,单个函数的程序行数不得超过 100 行。
(2)尽量使用标准库函数和公共函数 。
(3)不要随意定义全局变量,尽量使用局部变量。
(4)使用括号以避免二义性。


62 命名
包括变量命名、函数命名、资源 ID 号、类名等,命名必须具有一定的实际意义。
24 (1)变量命名:形式为 xAbcFgh,x 由变量的类型确定,Abc、Fgh 表示连续意义字符串,如
第一章 软件开发环境与软件项目开发过程
……………………………………………………………………………………………………………………………………………

果连续意义字符串仅有两个,可都大写。
(2)函数命名:第一个字母大写,要求用大小写字母组合规范函数名 。
(3)资源 ID 号:资源 ID 号全部用大写字母,根据意义确定 ID 号。
(4)类名:第一字母为大写的 C,后面用 AbcFgh 的形式,Abc、Fgh 表示连续意义字符串。


63 注释与可读性
(1)作为中文版的软件,原则上注释要求使用中文。
(2)每个源文件都有文件头说明。
文件开头的注释内容包括:文件名、公司名称、版权、作者名称、时间、文件描述、版本等。
复杂的算法还需要加上流程说明 。
(3)每个函数都有函数头说明。
在函数的原型说明包括引用外部函数和内部函数 。
引用外部函数必须在右侧注明函数来源:模块名及文件名。
如:void UpdateDBTable();//来源于:模块 1/stu.c
引用内部函数必须在右侧注明函数来源:文件名。
如:void ShowTable();//来源于:tec.c
在函数的源文件中,函数注释包括:输入、输出、函数描述、流程处理、全局变量、调用模
块等,复杂函数需要加上变量用途说明 。
(4)程序中的注释:修改时间、作者、方便理解的说明等。
(5)当定义或引用主要变量时,注释能反映其含义,常量定义有相应说明。
(6)注释可以与语句在同一行,也可在上一行。
(7)注释行数(不包括程序头和函数头说明部分)应占总行数的 1/5 到 1/3。
(8)利用缩进来显示程序逻辑结构,缩进量一致并以 Tab 键为单位。
(9)循环、分支层次不要超过 5 层。
(10)一目了然的语句不加注释。


64 结构化要求
(1)禁止出现两条等价的支路。
(2)禁止 GOTO 语句。
(3)每个函数都有函数头说明。

25
第二章 C++语言基础

20 世纪 80 年代,AT&T 贝尔实验室的 Bjarne Stroustrup 博士为了解决面向对象的程


序工程, 而 开 发 了 C++ 语 言。 C++ 语 言 是 在 C 语 言 的 基 础 上 发 展 起 来 的 一 种 面 向 对 象
(ObjectOreinted Programming, OOP)的程序设计语言。
本章对C++ 语言的基本结构作简要介绍,以便读者了解C++ 语言的特点和编程方法。
本章要点:
* C++ 的语言的特点
* C++ 语言的编程方法

1 简单的C++程序和C++语言的特点


11 简单的C++程序
【例2 1】 一个简单的C++ 程序 Ex02_1 及在VC++ 平台下,如何编写、编译C++ 程序。
(1)在资源管理器中,在 D 盘下新建一个文件夹 Ex02_1。
(2)在VC++ 平台下,新建一个C++ Source 文件,文件名为 Ex02_1.cpp。
① 启动 VC60。
② 点击菜单:File- > New,弹出图 2 1 所示的界面,选择 Files 标签下的 C++ Source
File,在 Files 中输入文件名 Ex02_1,在 Location 中输入文件保存的路径名 D:\Ex02_1。
③ 单击图 2 1 中的“OK”按钮,出现图 2 2 所示的文件编辑界面。

图2 1 新建文件 图2 2 文件编辑界面
26
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

(3)在图 2 2 中输入主函数(见清单 2 1)。


清单2 1 简单的C++程序

1//This is a simple C++ program


2 # include <iostream.h>
3voi dma in(void)
4 {
5 int i;
6 cout<<“欢迎使用《VC++ 编程与项目开发》! \n”;
7 cout<<“请输入要返回整数值:\n”;
8 cin>>i;
9

为方便起见,程序每一行都加上了行号,实际编程时不能加。下面对各语句加以解释:
① 第 1 条语句是C++ 语言的注释语句。其中,“
//”是C++ 语言的注释符号。
② 第 2 条语句用于将定义输入/输出函数( 的头文件 iostream.h 包含到程序中来。
cout,cin)
③ 第 3 条语句说明主程序的开始,表示程序返回的是 void 类型的数据,其传递值也是
void 类型。
④ 第 5 条语句声明了一个整型变量 i。
⑤ 第 6 条语句和第 7 条语句是C++ 语言的输出语句,可以将指定的字符串输出到屏幕上。
⑥ 第 8 条语句是C++ 语言的输入语句,表示从屏幕上向变量中输入一个值 。
从上面这个程序可以看出,C++ 语言继承了 C 语言的许多特点,同时增加了许多的功能。
(4)编译
单击菜单:Build- > Compile Ex02_1.cpp,弹出图 2 3 所示的界面,点击按钮 “是”,自动
生成项目框架,见图 2 4。

图2 3 是否生成项目框架 图2 4 生成的项目框架

(5)运行
单击菜单:Build- > Execute 或按工具栏中的 按钮 (或按
Ctrl+ F5),运行界面如图 2 5。


12 C++语言的基本特点 图2 5 运行的界面

C++ 语言是一种面向对象的程序设计语言,该语言提出了把数据和对数据的操作封装在 27

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

一起的类、对象和方法的机制,并通过派生、继承和多态性等特性,实现软件重用和程序自动
生成,使得软件(特别是大型复杂软件)的构造和维护变得更加有效和容易,并使软件开发能
更自然地反映事物的本质,从而大大提高了软件的开发效率和质量 。其基本特点如下。
(1)抽象(Abstract)
面向对象方法中的抽象是对具体问题 (对象)进行概括,抽出一类对象的公共性质并加
以描述的过程。
(2)封装(Encapsulation)
就是将数据和对数据的操作方法即函数组合在一起构成类 (Class),从而实现数据抽象
和数据隐藏。封装意味着对象应具有明确的功能,并有能和其他对象相互作用的接口。 也
意味着对象内部代码受到保护,只有处于对象中的代码才可以访问该对象的内部数据,这就
是数据的隐藏。
(3)继承(Inheritance)
继承的概念是从人工智能工程衍生而来的,利用继承可以避免相同的内容重复出现,能
够节省大量的时间以及存储空间 。在C++ 语言中,可以从一个类派生另一个类。 派生类 (也
称之为子类)继承了其父类和祖先类的数据成员和成员函数,并通过声明新的数据成员和成
员函数来增加新的功能。
(4)多态性(Polymorphism)
对象在接收到同样的函数调用时,会产生不同的执行动作,这样的功能叫多态性。 多态
是通过重载函数和虚函数等技术来实现的 。

2 数据类型 、变量和运算符

程序是由数据和处 理 数 据 的 操 作 组 成 的 。 因 此 学 习 C++ 编 程 之 前,必 须 首 先 了 解
C++ 语言的基本数据类型和运算符,以及两者结合后对数据的简单处理方式 ———运算和
表达式 。


21 基本数据类型
C++ 提供了大量可以使用的预先定义的数据类型 。 也可以把不同的数据类型结合起来
生成更复杂的数据类型。类型名可以是一个标识符、一个关键字或者是一个表达式。
基本数据类型是C++ 类型系统的基本组成部分。 这些基本数据类型包括字符类型、整
数类型、浮点类型、双精度类型和无值类型,相应的关键字是 c
har、
int、
fl
oat、do
ube和 v
l oid。
这些数据类型的位数和相应的取值范围如表 2 1 所示。
表2 1 基本数据类型的位数与取值

类型名 表示的含义 位 数 范 围
char 字符 8 - 128~127
int 整数 16 - 32 768~32 767
float 浮点数 32 34E - 38~34E+38
double 双精度浮点数 64 17E - 308~17E+308
void 无值 0 无值
28
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………


22 加修饰符的基本数据类型
在基本数据类型中除 void 类型外,其他类型前面可以加类型修饰符用于改变基本类型
的含义,以满足各种情况下的需要。 C++ 的类型修饰符包括:signed (有符号 )、unsigned (无
符号)、short(短型)和 long(长型)。修饰符 signed 和 unsigned 可以用于字符类型和整型,
而 short 和 long 可以用于整型,long 还可以用于双精度类型。 表 2 2 给出了基本数据类
型和类型修饰符的组合情况。
除了这些基本数据类型以外,C++ 的数据类型还包括常量(整常量、浮点常量和字符串常
量)、类、结构、数组和指针等,其他数据类型将在以后进行讨论 。
表2 2 组合类型的位数与取值

类 型 名 位数 范 围 类 型 名 位数 范 围
char 8 - 128~127 unsigned int 16 0~65 535
- 2 147 483 648~
signed char 8 - 128~127 long int 32
2 147 483 647
- 2 147 483 648~
unsigned char 8 0~225 signed long int 32
2 147 483 647

short int 16 - 32 768~32 767 unsigned long int 32 0~4 294 967 295

signed short int 16 - 32 768~32 767 float 32 34E-38~34E+38

unsigned short int 16 0~65 535 double 64 17E-308~17E+308

int 16 - 32 768~32 767 long double 80 34E~11E+4932

signed int 16 - 32 768~32 767


23 变量
程序要对数据进行读、写等操作。 当存储特定值或存储计算结果时,往往需要用到变
量。变量是用于表示一条信息的名称 。
无论什么时候要访问存储在变量内的信息,只需要使用变量的名称即可。因为C++ 对数
据的类型要求很严格,所以必须在使用变量之前,先定义这个数据的类型。

 命名变量

在给变量起名时,受到以下一些限制:
(1)变量名不能以数字开头。
(2)变量名不能有空格。
(3)变量名中除了能使用 26 个英文字母和 0~9 的数字外,只能使用“_”。
(4)变量名不能与C++ 语言中的特定名称相同,这些名称是关键词。
(5)变量名不能与C++ 中的库函数名相同。
下面一些是合法的变量名:
29

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

wave_play
RightOn
Number12

下面是一些非法的变量名:

case 这是个关键词
3Left 以数字开头
A Num 中间带有空格
+ - Ri 带有非法字符

 定义变量

在使用一个变量之前,必须先定义这个变量。 要定义变量,只需先注明变量的类型,再
写出变量的名称,例如:

int Number1;
float Length;
double width;

如果需要定义的变量的类型都相同,可以把相同类型的变量定义写在同一行里,中间以
逗号相隔。例如:

int Num1, Num2, NumberOfSongs;

 变量初始化

在定义一个变量的同时,也可以给变量一个初始值。 这个初始值是这个变量第一次使
用时的值,初始值写在变量名和“= ”的后面,例如:

int b = 200;
long a = 32700;
char string1, string2 = \n , string3 = a ;


24 数组
数组是一组相同变量的集合,其中的每一个变量称为数组的元素。 数组可分为一维数
组和多维数组。说明数组的一般形式为:
数据类型 数组名[元素个数]
其中数据类型说明数组元素的类型,但不能为 void 类型和函数类型。 元素个数是整型
数,用来说明数组的大小,方括号是个数组类型说明符。 在使用数组前,必须对数组进行
声明。
下面是一些对数组的声明:

int nums3; //声明了一个整型一维数组


float nums8; //声明了一个浮点型一维数组
30 float trees2014;//声明了一个浮点型二维数组
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

一维数组包含的元素个数与其下标数一致,而多维数组包含的变量个数是其下标的连
乘积。如:

int nums3; //声明了一组包含 3 个元素的整型数组


float groups56; //声明了一组包含 5×6 个元素的浮点型数组

定义了数组后就在内存中为其分配了一定的存储空间,数组名表示数组存储区在内存
中的首地址,是一个地址常量。在引用数组时应该遵循下列规则:
·数组下标的最小值为 0;
·数组元素的个数为方括号中的值,有效下标为 0 到元素个数的值减 1;
·在调用数组元素时,应给数组元素赋值,否则其值无法预料,通常非常之大;
·要使用有效下标。
C++ 可以对数组进行初始化,初始化的数值依次放在一个大括号中,数据之间用逗号分
隔,数据可写在多行:
·若初始化数据的个数少于数组元素的个数,则多于数据个数的数组元素赋值为 0;
·若初始化数据的个数多于数组元素的个数,则编译时会报错;
·方括号中元素个数的值可以缺省,也可用初始化数据的个数指定该值 。
数组的初始化有许多方式,下面是对静态数组常用的初始化方式:

int num3=1,2,3; //对一维整型数组的简单赋值


float results23=19,24,35,44; //对二维浮点型数组的分组赋值
int a=2,4,5; //对整型不定元素个数的数组赋值
int b10=12,3; //对整型数组的不完全赋值
从上面代码可以看出,对数组的赋值可以采取固定赋值、分组赋值、不定个数赋值与不
完全赋值的方式。其中,在不完全赋值时,剩下的数组元素将被缺省赋值为 0。 在使用前,将
数组元素赋初值是一个良好的编程习惯 。
C++ 对多维数组初始化时,数值列表中每一项的顺序与被初始化多维数组元素在内存中
的存储顺序一致,例如:

int x23=l, 2, 3, 4, 5, 6;

初始化的结果是:

x00= 1,x01= 2,x02= 3,x10= 4,x11= 5,x12= 6

多维数组初始化时,只有最左边的方括号的数字可以缺省,例如:

int x3=2, 4, 6, 8, 10, 12;

多维数组在存放数据时,是按一定的下标排列规律,在存储空间连续地存放数据的。 这
个排列规律是从最右边的下标开始,依次往左变化下标元素,例如,三维数组 Array23
2的存储顺序是这样的:
Array000起始内存地址、Array001、Array010、Array011、
Array020、Array021、Array100、Array101、Array110、Array
111、Array120、Array121终止内存地址。 31

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

C++ 允许把函数的参数用数组表示。 有两种形式的数组参数,一种是定界数组参数,另


一种是变界数组参数。定界数组参数用来接收有固定大小的数组,在调用时,这种参数的值
一定要符合该参数的类型和大小。 而变界数组参数用来接收大小不定的数组,它只在第一
维使用空的方括号,说明参数值的第一维的大小是可变的,其余维的大小必须符合数组参数
的相应维的大小。
定界数组参数定义语法:数据类型 参数名 [第一维大小][第二维大小]
示范代码如下:

int placeArr (int arr20100, int n);


void sort (double mat2145, int rows, int cols);

变界数组参数定义语法:数据类型 参数名 [][第二维大小]


示范代码如下:

int placeArr (int arr100, int n);


void sort (double mat45, int rows, int cols);


25 结构
C++ 语言中提供的结构数据类型是一种复合数据类型,用于将某些相关的不同类型的数据
组织到一个新的数据结构中。结构数据类型的声明以关键字s t
rut开始,其一般形式为:

struct 结构名{ 数据成员说明列表 };

“结构名”是一个有效的C++ 标识符,“
数据成员说明列表”
是由变量说明语句构成的一个语
句序列,但不能在该列表中对变量进行初始化。下面的例子描述了某点平面坐标的结构:

struct point double x;


double y;
;
结构说明只给出了构成相同的一类变量的描述,仅对编译器有意义。 结构说明为编译
器建立一个样板,使编译器可以知道如何为该结构的变量分配内存。 还可以用没有名称的
结构来声明变量,这称为无标记结构,例如:

struct double x;
double y;
s1, s2, s3;
C++ 允许在声明结构变量的同时进行初始化,例如:point s1 =15,50;
使用时,必须先把“结构名”作为类型名在程序中说明变量,下面的例子声明了两个结构
变量:point start,end;
对结构的数据成员进行处理时,必须用成员运算符 “.”来引用一个结构变量中的某个数
据成员,其一般形式为:

变量名.数据成员名

32 对结构变量中的数据成员进行初始化赋值和处理都可以采用这种方式,下面是计算两
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

点之间距离的示范代码:

float distance;
start.x = 100;
start.y = 20;
end.x = 0;
end.y = 0;
distance = sqrt(pow(start.xend.x,2)+ pow(start.yend.y,2));

由以上例子可以看出,结构的用法和其他变量没有太大区别 。 同时,结构还可以声明为
数组,或通过声明结构指针的方式方便地进行访问 。


26 枚举
枚举数据类型就是将变量的值都列出来的一种数据类型 。 它所定义的变量的值只能是
列出值之一。C++ 用关键字 enum 来定义枚举类型,其语法如下:

enum 枚举类型名{
<枚举标识符表> };

例如:

enum weekSunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday;

通过上面这个定义以后,C++ 编译器按定义时的顺序给枚举类型的每个标识符分配一个
整数值。如:Sunday = 0,Monday = l,Tuesday = 2 等等。 枚举类型中每个标识符必须是唯一
的,而它们的值可以不唯一。例如:

enum CPUi286 = 2,i386DX = 3,i386SX = 3,i486DX = 4,i486SX = 4,i586 = 5;

还可以对每个标识符显式地赋值和间断地赋值,例如:

enum colorred = l,yellow,blue = 5,green = 7,black,whites,

其中,因为 red = l,那么计算机就自动给 yellow 赋值为 2,black 为 8,whites 为 9。


27 联合
联合是一种特殊的结构,它的各个成员共享一个存储区。 联合的长度等于它的最大成
员的长度。C++ 用 union 来定义联合,其语法如下:

union 联合名
{ 成员列表 };

例如:

union mark
char name;
int page;
;
联合提供了快速数据转换的方法,使用联合可使几个变量共用一个存储区,这样可以节 33

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

省系统的开销。
枚举和联合对其数据成员的访问也都是使用操作符“.”来进行的。


28 指针
指针是C++ 语言中一种强大而有力的工具。 所谓指针是指包含另一变量内存地址的变
量。如果一个变量包含的是另一个变量的地址,可以称第一个变量指向第二变量。 如果一
个变量要存放另一个变量的地址,这个变量必须声明为指针类型的变量。 声明指针变量的
一般形式为:

数据类型 * 指针名; 或 数据类型 * 指针名= & 变量;

符号“* ”在说明语句中是个指针类型的说明符,& 加在变量的前面表示取这个变量的


地址,例如:

int * pointer;
char * str;
float * px = &x;

其中 pointer 是指向整型变量的指针;str 是指向字符变量的指针;px 是指向浮点型变


量 x 的指针。
指针变量可以和非指针变量一起声明,例如:

int * pointer, not_pointer;

在这里,pointer 是指针变量,not_pointer 是非指针变量,而不要误以为所定义的都是


指针变量。在使用指针时,必须对它进行初始化,就像初始化普通变量一样,否则,可能产生
不可预测的错误。一旦声明了指针变量后,就可以使用 “* ”加指针名来访问变量的值。 例
如,px 是指向变量 x 的指针,就可用* px 来访问变量 x 的值。
关于指针的运算符有三种:取地址运算符* 、引用运算符 & 和下标运算符 []。 下标运
算符已经在数组中用过,下面只介绍地址运算符和引用运算符 。地址运算符* 表示取变量中
的内容,引用运算符 & 表示取变量所在的地址。
【例2 2】 Ex02_2 指针运算,见清单 2 2。
清单2 2 指针运算符

#i
nclude<iostream.h>
man()

{ int num = 10; //声明一个整型变量 num
int * pointer = # //声明一个指针变量并把 num 所在的地址赋给 pointer
cout<< "num = "num<< "\n "; //输出 num 的值
cout<< "* pointer = "pointer<< "\n "; //输出 pointer 的值
cout<< "* pointer = "* pointer; //输出 * pointer 的值

程序运行结果如下:

num = 10
34 pointer = 0x0024DDEE
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

* pointer = 10

由此可见,pointer 代表了 num 的地址,而* pointer 则代表了 num 的值。


指针变量与其他变量一样,可进行运算,并可用 new 与 delete 在程序中进行动态分配。
它可以指向变量,也可以指向数组或数组元素。 C++ 规定数组名为数组的首地址,根据指针
的概念,可以把数组名赋给一个指针变量,这样就得到一个指向数组的指针 。
C++ 允许使用指针访问数组中的不同元素,在访问数组 X 的元素 Xi时,编译后的代码
执行两个步骤,先获得数组 X 的基地址,即第一个元素的所在的位置;再计算下标 i 相对于
数组基地址的偏移量,从而得到元素 Xi的地址。例如:

int X4;
int * pX;
pX = &X0; //等价于 pX = X;
pX 为指向 X数组的指针,表示第一个数组元素的地址。根据规则,Xi= * (pX+i)或
者(X+i)= pXi。
VC++ 支持指向结构的指针的声明和使用 。指针变量存储结构变量的地址。一旦指针拥
有结构变量的地址,就可以用操作符“- > ”来访问结构的成员,例如:

struct point double x;


double y;;
point start;
point * end = &start;
end- > x = 40;
end- > y = end- > x+14;


29 类型定义
C++ 提供关键字 typedef 来给已存在的数据类型命名一个新的名称,作为它的别名,其
语法如下:
typedef 已知类型名 新类型名;
例如:typedef int integer;
关键字 typedef 为一个已经存在的数据类型取一个别名,以便更好地说明该数据类型的用
途。读者可以取一个名字更短或者更熟悉和便于理解的名字,在程序编写、阅读时能提高效
率。经过类型定义后,新的类型名就可以在程序中使用了。例如,经过上面的类型定义,就可
以使用下面的语句来定义整型变量了,C++ 编译器将数据类型 integer 识别为 int 数据类型。
integer x,y;


210 运算符
C++ 语言中定义的运算符包括算术运算符 、赋值操作符、增量和减量运算符、
si
zef运算

符、关系和逻辑运算符以及位处理运算符等,下面将分别进行介绍。
(1)算术运算符
编译器是根据操作数的类型来执行相应类型的操作,如果两个操作数都是整型表达式, 35

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

编译器就产生整数操作的代码;如果操作数有一个或两个是浮点表达式,编译器产生浮点操
作的代码。表 2 3 列出了C++ 的算术运算符。
表2 3 算术运算符

运算符 功 能 数据类型 实 例
+ 单目加 数值 A = +B +2
- 单目减 数值 A = -B
+ 加 数值 C = A +B
- 减 数值 C = A -B
* 乘 数值 C = A*B
/ 除(取商的整数) 数值 C = A/B
% 模(取余数) 数值 C = A%B

算术表达式是一个语句中的一部分,代表的是一个数值,一般可以分为三类:
① 直接常量是最简单的算术表达式,这种只包含单一变量或常量的表达式是第一类算
术表达式。例如:

i 36

② 第二类算术表达式包含操作数和一个运算符,操作数可以是变量也可以是常量,例如:
4* x 123/543 PI+314

③ 更复杂的算术表达式包括多个运算符甚至函数,例如:
log(var)* 36
(3+y)* (8/x)- 5* y

(2)赋值操作符
赋值语句的一般形式为:变量名= 表达式,例中:

y = a*x+b;

C++ 允许在一条语句中使用多重赋值操作给多个变量赋一个值,例如:a = b = c = 50
C++ 还支持其他类型的操作符,如表 2 4 所示。
表2 4 算术赋值操作符

赋值操作符 等价的形式 举 例
x+= y x = x+y x+= 4
x-= y x = x-y x-= 10-a
x*= y x = x*y x*= 12
x/= y x = x/y x/= 5*a
x%= y x = x%y x%= 02

(3)增量和减量运算符
增量(++ )和减量 (- - )操作符是将变量中的值分别加 1 和减 1,其语法和应用举例见
36 表 2 5。
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

表2 5 增量和减量运算

举例 2(执行完下面每个独立的等式后
运算符 说 明 举例 1
的结果)(int a = 8; b = 4, d;)
变量++ 使用该变量后其值加 1 a ++ d = a ++;//d = 8,a = 9
++ 变量 使用该变量前其值加 1 ++a d = ++a;//d = 9,a = 9
变量- - 使用该变量后其值减 1 b -- d = b - -;//d = 4,b = 3
- - 变量 使用该变量前其值加 1 -- b d = - -b;//d = 3,b = 3

(4)sizeof 操作符
sizeof 操作符用于获得一种数据类型或一个变量的字节数,其语法如下:
sizeof({变量名/数据类型}) 或 sizeof变量名/数据类型}。
【例2 3】 Ex02_3 sizeof 操作符,见清单 2 3。
清单2 3 s
ize
of操作符
示范代码 运行结果显示(各类型数据字节数)
#inc
lude <iostream.h> Data type Memory Used
main() - - - - - - - - - - - - - - - - - - - -
{ short int aShort; short int 2
int anInt; integer 4
long along; long integer 4
char aChar; char 1
float aFloat; float 4
cout<< "Data type Memory Used \n ";
cout << "- - - - - - - - - - - - - - ";
cout<< "\n short int " <<sizeof(aShort);
cout<< "\n integer " <<sizeof(anInt);
cout<< "\n long integer " <<sizeof(aLong);
cout << "\n char " <<sizeof (aChar);
cout << "\n float " <<sizeof(aFloat);
cout << "\n \n \n ";
return 0;

(5)关系和逻辑运算符
关系和逻辑运算符指的是一个值与另一个值之间的关系,逻辑运算符指的是连接关系
的方式。C++ 中定义的关系和逻辑运算符如表 2 6 所示。
关系和逻辑运算符的优先级比算术运算符低 。 例如,6> 4+ 5 等价于 6> (4+ 5),其结果为
假。C++ 不支持预先定义的布尔标识符,而是把 0 当作假,把非 0 当作真。 在 VC 中可以用系
统预定义的布尔标识符 TRUE 和 FALSE 分别代表 1 和 0。
表2 6 关系和逻辑运算符

运算符 功能 举 例 运算符 功能 举 例
&& 逻辑与 if (i>1&&i<6) <= 小于等于 if(i<= 5)
|| 逻辑或 if (I = = 1||I = = 6) >= 大于等于 if(i>= 1)
! 逻辑非 if (!(i>1&&i<6) == 等于 if(i = = \o )
< 小于 if (i<6) != 不等于 if(i!= \n \)
> 大于 if (i>0) ?: 条件赋值 if(i<1)? 1:i
注意:不要误将赋值操作符“= ”当作关系操作符“= = ”使用。 37

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(6)位处理运算符
位处理运算符用来对一个字节或字的位进行开关、设置、查询和移位,具体如表 2 7
所示。
表2 7 位处理运算符

运算符 功 能 举 例 运算符 功 能 举 例
& 逐位与 i&12 ~ 逐位非 ~i
| 逐位或 i|58 << 逐位左移 i<<1
逐位异或 i 12 >> 逐位右移 i>>2

3 流程控制语句

语句是C++ 语言中最小的可执行 单 位,程 序 流 程 的 控 制 是 通 过 控 制 语 句 来 完 成 的,
主要的流程有分支和循环 。 C++ 的流程控制语句决 定 程 序 执 行 时 的 结 构 。 这 些 流 程 控
制语句主要包括:表达式和块语句 、选择语句 、switch 语句 、循环语句和辅助流程控制的
转移语句 。


31 表达式语句和块语句
大多数的C++ 语句是表达式语句。 一个简单的表达式语句由表达式后面加上分号组
成。例如:a = b+c;
块语句是以 {开始,以 }结 尾 的 语 句 。 在 {和 }之 间 封 装 着 一 系 列 的 语 句 。 这 些 语 句
可以是C++ 中的任何一 种 句 子 。 块 语 句 在 语 法 上 等 价 于 一 个 单 语 句,可 以 用 于 任 何 单
语句可以使用的地方 。 它与单语句的主要区别在于其后不加分号 。 下面是一个块语句
的例子:
{int A = 34,B = 35,sum;
sum = A+B;


32 选择语句
C++ 中提供了 if 选择语句,用于在条件满足时执行某些操作。 if 语句包括以下两种
形式:
(1)if 语句(单选条件判断语句)
其语法为:

if(条件表达式)
块语句;

如果 if 后面的条件表达式的逻辑值为真,则执行后面的块语句,否则不执行,例如:

if(i> 0)
i = j; //如果 i> 0 的话,将 j 的值赋给 i。
38 注意:如果需要处理的块语句只有一条语句的话,则可以不加大括号。
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

(2)if_else 语句(双选择的条件判断语句),其语法为:

if(条件表达式)
块语句 1;
else
块语句 2;

C++ 还允许条件判别语句的嵌套来产生一个多选择的判别语句,其语法为:

if(条件表达式 1)
块语句 1;
else if(条件表达式 2)
块语句 2;
else if(条件表达式 3)
块语句 3;

else if(条件表达式 n)
块语句 n;
else
块语句 n+1;

测试每一个条件表达式,哪一个的逻辑值为真,就执行哪一条块语句,若所有的逻辑值
都为假,则执行最后一个 else 下的块语句。


33 sw
ith分支语句

switch 语句可以根据当前的条件进行分支处理 。它将一个表达式的值与某些常量进行
连续比较,然后执行多分支程序的一支。switch 语句的一般形式是:

switch(表达式)

case 常量 1:
语句列表 1
break;
case 常量 2:
语句列表 2
break;

case 常量 n:
语句列表 n
break;
default:
语句列表 n+1 39

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

break;

在 switch 语句中,表达式的值类型和常量的类型必须一致。 当表达式的值与 case 中
指定的常量相等时,执行该 case 后的语句,直到遇到 break 语句或到达 switch 语句末尾。
如果不出现相等匹配的情况,则执行 default 后的语句。 无 default 时,如果所有匹配都失
败,程序直接执行 switch 语句之后的语句。


34 循环语句
循环语句是在给定条件成立的情况下,重复执行某个程序段,重复执行的程序段称为循
环体,循环体可以是单语句或块语句。 C++ 中的循环语句包括 wh
ile、
dowh
ile和f
or语句。
具体用法和区别见表 2 8。
表2 8 循环语句

while 语句 dowhile 语句 for 语句


语法 while(条件表达式) do 语句列表; } for(e1; e2; e3)
定义 {语句列表;} while(条件表达式) 语句
e1 用于设置进入
循 环 时 的 初 值; e2
首先计算表达式的值,在 其 值 为
首先执行循环体中 的 语 句,然 后 用于 控 制 循 环 执
真时,执行循环体中的语句,而在其
说明 对表达式求值,在表达 式 为 真 的 情 行。e3 用于改变某
值为假时,终止循环的执行,程序接
况下重复执行循环体中的语句 些 变 量 的 值, 以 使
着执行循环体后的语句
e2 的 值 最 终 为 假,
使 for 循环终止

计算并显示 1 到 9 的平方 计算并显示 1 到 9 的平方


# include<iostream.h> # include<iostream.h>
main() main()
输出 0 到 99 的整数
{ int i = 1; { int i = 1;
for(int i = 0; i <
举例 while(i<10) do
100;i++)
{ cout<<i<<" 2 = "<<i*i<<"\n "; cout<<i<<" 2 = "<<i*i<<"\n ";
cout<<i;
++i; 
 while(++i<10)
 }


35 转移语句
转移语句包括 break 语句、continue 语句和 goto 语句。具体用法和区别见表 2 9。
表2 9 转移语句

break 语句 continue 语句 goto 语句

通 常 与 循 环 语 句 混 合 使 用。
可以用于 switch 语句中,还可
程序 遇 到 continue 语 句, 跳 过 不受限制地转向程序中
作用 用于循环语句的循环体中,以终
continue 语句后的其余语句,开 的任何地方
止、跳出循环体
始进入下一次循环

40
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

续 表
break 语句 continue 语句 goto 语句

for(int i = 0;i<100;i ++ ) for (int i = 0;i<100;i ++ )


goto lable;
{ {int j = i+ 1;

cout<<i; continue; lable:
break; cout<<i; 其中标号 lable 可以在
举例   goto 语 句 之 前,也 可 以 在
循 环 体 只 执 行 了 1 次,break 程序并没有输出变量 i 的值。 goto 语句之后
语句就终止了循环体,跳出执行 程序流程在 continue 语句的作
下面的语句 用下直接跳到循环的头部,进入
下一次循环执行 j 的赋值运算

注意:goto 语句转向的随意性,程序的流程容易产生混乱。所以一般不提倡在程序中使用 goto 语句。

4 函数

函数是由功能相关的语句序列所组成的独立模块 。描述一个函数所执行的算法的过程
称为函数定义,而使用一个函数的过程称为函数的调用 。 函数调用完成后,一般情况下要向
它的使用者返回一个值,这个值称为函数返回值。下面将介绍在C++ 程序中进行函数定义和
调用的方法。


41 函数定义
在程序中使用函数必须先进行函数定义 。函数定义为:

返回类型 函数名(数据类型 参数 1,数据类型 参数 2,…)


{函数体(语句序列)}

返回类型是表示返回值的类型,如果没有设置返回值的类型,系统自动设置返回值类型
为整型。函数的返回值是通过 return 语句获得的,如果在设计函数时不希望返回任何值
时,那么应将该函数的类型设置为 void 型。
参数 1,参数 2 这些量称为函数的形参,用于接受调用参数时传送给这个函数的数据。
参数表可以为空,但其后的圆括号不能省略。
函数体是一个语句系列,描述这个函数所要进行的操作。
【例2 4】 Ex02_4 函数定义,见清单 2 4。
清单2 4 函数定义


oub
led istane(
c d oubl
ex1d oubley1d oublex2d o
ubl
ey //计算平面上两点的距离
2)
{ double dis = 0;
dis = dis+sqrt(pow(x1-x2,2)+pow(y1-y2,2));
return dis;


42 函数的参数传递
在函数未被调用时,函数的形参并不占有实际的内存空间,也没有实际的值。 只有在函
数被调用时才分配形参的存储单元,并将实参与形参结合。 函数的参数传递指的就是形参 41

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

与实参结合(简称形实结合)的过程,形实结合的方式有值调用和引用调用 。
(1)值调用
值调用是指当发生了函数调用时,给形参分配内存空间,并用实参来初始化形参 (直接
将实参的值传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参
脱离关系,此后无论形参发生了怎样的改变,都不会影响到实参。
(2)引用调用
值调用时参数是单向传递,那么如何使在子函数中对形参做的更改对主函数中的实参
有效呢? 这就需要使用引用调用。
引用是一种特殊类型的变量,可以被认为是另一个变量的别名,通过引用名与通过被引
用的变量名访问变量的效果是一样的 。
【例2 5】 Ex02_5a 值调用和 Ex02_5b 引用调用,见清单 2 5。
清单2 5 值调用和引用调用

值调用 Ex02_5a 引用调用 Ex02_5b


目的:从键盘输入两个整数,交换次序后输出。 目的:从键盘输入两个整数,交换次序后输出。
# include<iostream.h> # include<iostream.h>
//函数声明 //函数声明
void Swap(int a,int b); void Swap(int &a,int &b);
in
tma in() in
tma in()
{int x = 2; {int x = 2;
int y = 10; int y = 10;
cout<< "x = "<<x<<" y = "<<y<<endl; cout<< "x = "<<x<<" y = "<<y<<endl;
Swap(x,y); Swap(x,y);
cout<< "x = "<<x<<" y = "<<y<<endl; cout<< "x = "<<x<<" y = "<<y<<endl;
return 0; return 0;
 

oidSwa p(
inta
intb) v
oidSwa p(
int&a
int&b)
{int t; {int t;
t = a; t = a;
a = b; a = b;
b = t; b = t;
 
运行结果: 运行结果:
x = 2 y = 10 x = 2 y = 10
x = 2 y = 10 x = 10 y = 2
分析:从结果看出,没有达到交换的目的。这是 分析:从结 果 看 出,采 用 引 用 调 用 成 功 地 实 现 了 交
因为采用的是值调用,函数调用是传递的是实参 换。引用调用与值调用的区别只是函数的形参写法
的值,是单向传递过程。形参值的改变对实参不 不同。主调函数中的调用语句是完全一样的。
起作用。


43 局部变量和静态变量
函数的局部变量仅当函数被调用时才存在,一旦函数被终止,系统就把局部变量删除。
函数每次被调用时,系统会对局部变量进行初始化。例 2 4 中 dis 为局部变量。
函数的静态变量是在局部变量的数据类型前加上 static 关键字,它的初始化仅能执行
42 一次。当函数终止时将静态变量保存在独立的内存单元中,所以下一次调用该函数时,静态
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

变量能保持它原来的值。可以在不同的函数中使用相同的静态变量名,这不会造成编译器
的混淆,因为编译器一直跟踪每个函数的静态变量 。
【例2 6】 Ex02_6 静态变量,见清单 2 6。
清单2 6 静态变量


oubled istane(
c doublex 1doubl
ey 1doubl
ex 2doub
l 2)
ey //计算平面上两点的距离
{static double dis = 0;
dis = dis+sqrt(pow(x1-x2,2)+pow(y1-y2,2));
return dis;

这个例子和例 2 4 不同点只是把 dis 定义成静态变量。 如果以 distance(10,0,0,10)


的形式进行调用,第一次调用,两个例子的结果都为 14142 1。 但是如果以相同的形式进行
第二次调用,则上一例子的返回值为 14142 1,而本例将返回 28284 3。 这是因为在本例中,
dis 将保存上一次调用时的值。


44 内联函数
当程序调用内联函数时,是将该内联函数展开并直接插入到调用处,而一般函数的调用
是转到被调用函数体中,执行完函数后,退回到调用函数语句的下一句。 所以,内联函数可
以加快代码的运行速度,但是以增加程序的代码为代价。 因此,对于一些功能简单、规模较
小又使用频繁的函数,可以设计为内联函数。内联函数定义的语法为:

inline 返回类型 函数名(参数表)


{函数体}
注意:使用内联函数时,内联函数体内不能使用静态变量,循环语句,switch 语句。


45 函数重载
函数重载是C++ 语言特有的。两个以上的函数取相同的函数名,但是形参的个数或者类
型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动调整调用哪一个函数,这就
是函数的重载。
【例2 7】 Ex02_7 函数重载,见清单 2 7。
清单2 7 函数重载

# include<iostream.h> //函数定义
//函数声明 in
tadd(intx i
n ty)
int add(int x,int y); {
double add(double x,double y); return x+y;
vo
i dma in() 
{int a = 1; do
ublea dd(doubl
ex
doub
ley)
int b = 2; {
int result1; return 2*x+y;
double c = 31; 
double d = 25;
double result2

43

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

//调用函数 执行该程序后的结果是:
result1 = add(a,b); After using the Function
result2 = add(c,d); result1 = 3
//显示执行函数后的值 result2 = 87
cout<< "After using the Function \n ";
cout<< "result1 = "<< result1<< "\n ";
cout<< "result2 = "<< result2<< "\n ";


46 函数模板
很多情况下,我们设计的算法可以处理多种数据类型,但是用函数实现算法时,即使设
计为重载函数也只是使用相同的函数名,函数体仍然要分别定义。 请看下面两个求绝对值
的函数:
int abs(int x)
{ return x<0?-x:x;

double abs(double x)
{ return x<0?-x:x;

这两个函数只是参数类型不同,功能完全一样,使用函数模板便可以避免函数体的重复
定义。函数模板可以用来创建一个通用功能的函数,以支持多种不同类型的形参,简化重载
函数的函数体设计。函数模板的定义形式是:
template<typename 标识符>
类型名 函数名(参数表)
{函数体定义}
【例2 8】 Ex02_8 定义一个求绝对值的函数模板,见清单 2 8。
清单2 8 函数模板

# include <iostream.h> 运行结果:


template<typename T> 2
Ta bs(Tx) //函数模板 65
{ return x<0? —x:x;


oidma in() //主函数
{int n = - 2;
double a = - 65;
cout<<abs(n) <<endl;
cout<<abs(a) <<endl;


47 多态性和虚函数
44 对象在接收到同样的函数调用时,会产生不同的执行动作,这样的功能叫多态性。 C++
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

语言中,多态性具有两种形态:编译多态性和运行多态性。 使用重载函数可以获得编译多态
性,使用继承和虚函数可以获得运行多态性 。多态性的实现和联编有关。C++ 中有两种联编
方式:静态联编和动态联编。静态联编时,编译器可以根据调用时使用的实参,在编译时就
确定调用的函数;动态联编时,虚函数对多态性进行支持。 要获得多态性的对象,必须建立
一个类等级,然后在派生类中重新定义基函数,就可获得多态性对象。
虚函数定义的语法为:

virtual 返回类型 函数名(参数表)


{函数体}

虚函数只能是类中的一个成员函数,但不能是静态成员,关键字 virtual 用于类中该函


数的说明。使用虚函数保证了在通过一个基类类型的指针调用一个虚函数时,C++ 系统对该
调用进行动态联编,但是通过一个对象访问虚函数时使用静态联编。 虚函数的调用比较自
由,可以在成员函数、构造函数和析构函数中进行调用 。
虚函数的使用保证了C++ 可以在很多情况下获得多态性的支持 。
【例2 9】 Ex02_9 虚函数实现多态性,见清单 2 9。
清单2 9 虚函数实现多态性

//情况一:不是虚函数 Ex02_9a //情况二:虚函数 Ex02_9b


# include <iostream.h>
# include <iostream.h>

lassBa seC

lassBa seC
public:
public:
void display()
virtual void display()
{cout<< "Basec display "<< "\n ";
{cout<< "Basec display "<< "\n ";


;
;

lassDe rivedC  publicBaseC

lassDe rivedC  publ
icBa s
eC
 public:
 public:
void display()
void display()
{cout<< "Derived display "<< "\n ";
{cout<< "Derived display "<<"\n ";


;
;


oidma i n()//主函数 程序运行结果:
{ BaseC * pp; 情况一结果:两次都显示“Basec display”
BaseC x 情况二结果:第一次显示 “Basec display”第二
Derived y; 次显示“Derived display”
pp = &x; 原因:在情况一中,基类的 display()函数没有
pp- >display(); 定义为虚函数,所以基类的指针始终调用基类的
pp = &y; 函数。
pp- >display(); 在情况二中,基类的 display()函数定义为虚函
} 数,所以基类的指针可以调用派生类的函数

45

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

5 类和对象


51 类的定义和声明
类(class)是一种用户自定义的数据类型。它概括了一组对象的共同性,包括相同的数
据和函数组成。C++ 语言中,类的说明关键词是 class 开始,类说明的内容在花括号 {和 }之
间。C++ 说明类的一般方法见表 2 10。
表2 10 类的定义

C++ 类定义的一般方式 类定义举例


las类名
s c
las
smy base
{ private:  private:
私有数据和函数 float a;
protected: protected:
保护型数据和函数 int b;
public: public:
公有数据和函数 double c:
} float Geta();
int Getb();

在类中可以包含以下三种类型数据和函数,见表 2 11。
表2 11 访问权限

成 员 类 型 访问权限说明
公有类型(public) 声明为公有的成员可以被程序中的任何代码访问
类本身的成员函数可以访问
保护类型(protected) 友元类的成员函数可以访问
派生类的成员函数可以访问
类本身的成员函数可以访问
私有类型(private)
友元类的成员函数可以访问

在类中,数据成员可以像在C++ 语言那样进行说明,但不能在类中对数据成员进行初始
化,如赋值等操作。类说明的数据成员描述了对象的内部结构,其中的成员函数用于对这些
对象进行操作。但类中对这些成员函数进行了函数说明,还必须在程序中定义这些成员函
数的实现。定义成员函数的一般形式见表 2 12。
表2 12 成员函数的定义

定义成员函数的一般形式 成员函数说明举例

返回值类型 类名::函数成员名(参数表) mybase 类的成员函数:


{函数体} f
loatmy base
Gea()

{return a;

注意:“::”是作用域运算符,“类名::”用于表明其后的成员函数名是在“类名”中说明的。“函数体”中的成员函数描
46 述成员函数对数据成员的操作。
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

在类中定义的函数,都必须在定义类后给出定义,同时这些函数的操作数据只能是这个
类中的数据成员。在很多情况下,类必须具有构造函数和析构函数。 对类中数据成员的初
始化,是在类 中 的 构 造 函 数 中 完 成 的。 如 果 一 个 类 在 它 的 生 存 期 中 创 建 了 一 块 不 能 被
Windows 操作系统自动释放的内存,必须在类的析构函数中将这块内存释放掉,把资源还给
Windows。


52 对象
(1)对象的定义
与结构一样,类也是一种数据类型。 要想使用类,还必须声明对象。 对象是类的实例。
下面的语句声明了一个类变量(或类对象),其中 mybase 为类名(见表 2 10),而 A1 为类变量
(或对象):

mybase A1;//声明类对象 A1

对象声明后即可使用,使用方法为:

对象名.成员函数名 或 对象名.数据成员名
(2)类对象的访问权限
类的成员函数可以访问类的所有成员,没有任何限制,而类的对象对类的成员的访问是
受成员访问控制符制约的。如定义了一个表 2 10 的类 mybase。

mybase A1; //定义 mybase 类的对象 base


A1.a; //非法,a 为 mybase 的私有类型
A1.b //非法,b 为 mybase 的保护类型
A1.c //合法,c 为 mybase 的公有类型
不能通过类 mybase 的实例对象来访问类的私有成员 a 和保护成员 b。
使用类和对象进行程序设计可以带来的好处有:
① 外部对类的修改不影响类内部私有的数据和函数,也就不会影响使用这个类程序。
② 在类定义之外不能为类中的数据定义做进一步的操作,使类的对象在任何程序中都
具有清晰明确的功能。
③ 定义一次类,就能够在程序中创建许多个对象,并且这些对象间相互的干扰、
影响小。
④ 运用类进行程序设计可以使程序模块化,不同的文件模块可以自由使用这个类的定
义。各个模块间只能通过公有数据成员发生联系,使模块的依赖性减少,增强了程序的可移
植性。
【例2 10】 Ex02_10 类、对象,见清单 2 10。
清单 2 10 中对 location 类中私有成员变量 x、y 的访问是通过公有成员函数 getX()和
getY()来实现的。
清单2 10 类、对象

#incl
ude<i
ost
ream.h>
//建立一个描述位置的类
cl
assl
oca
tion //lo
cat
ion类
{ private: 47

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

int x,y;
public:
void init(int i,int j){x = i; y = j;//内联函数
int getX();
int getY(){return y;
;

ntlocat
ione
gtX()
{return x;

//类应用举例
vo
idmai n()
{ int x,y;
location A,B;
A.init(5,8);
B.init(7,3);
x = A.getX();
y = B.getY();
cout<<x<< "\n" <<y<<endl;

例题输出结果为 5、3。


53 构造函数和析构函数
(1)构造函数
构造函数是类的一个特殊的公有成员函数,它与类同名,并且没有返回值。 构造函数的
作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态,使此对
象具有区别于彼对象的特征。C++ 在创建一个对象时,会自动调用类的“构造函数”完成初始
化成员变量的操作。
(2)析构函数
析构函数是类的一个公有成员函数,它的名称是由类名前加 “~ ”构成,也没有返回值。
析构函数与构造函数的作用几乎正好相反,它用来完成对象被删除前的一些清理工作,也就
是专门作扫尾工作的。一般情况下,析构函数是在对象的生存期即将结束的时刻由系统自
动调用的。它的调用完成之后,对象也就消失了,相应的内存空间也被释放。


54 继承和派生
自然界中,许多事物具有共同的特征,但普遍存在一些细微的差别。 使用层次分类的方
法来描述事物间的相同点和不同点是很方便的 。 C++ 语言中使用了这种层次分类的方法。
在面向对象的语言中,继承是一个重要的机制。它使用层次分类的方法,在一个一般类的基
础上建立新类。这种新类既具有继承下来的一般类的特征,也具有新的属于自己的特征。
在C++ 中,一个类从另一个类继承特征成为派生类,一般被继承的类称为基类。 这个过程可
以无限地进行下去,从而得出一个类的等级结构。
C++ 中有两种继承方式:单一继承和多重继承。在单一继承中,派生类只有一个基类,而
在多重继承中,派生类可以有多个基类。派生类继承了基类中所有的数据成员和成员函数,
48 同时又增加了新的数据成员和成员函数。 在这过程中,单一继承和多重继承有着不同的
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

特性。
(1)单一继承(单一继承中,派生类的一般形式见表 2 13)

表2 13 在单一继承中,派生类的一般形式

在单一继承中,派生类的一般形式 单一继承的例子


lass类名1:访问控制 类名2 c
lassextendpubl
icmy base
{private: private:
私有数据和函数 int extdata;
protected: public:
保护型数据和函数 int GetExtData();
public: }
公有数据和函数

在上面的例子中,extend 类继承了 mybase 类的公有函数,通过 mybase 类的公有函数实


现了对 mybase 类函数的数据访问,同时又添加了本身的成员函数。
在继承和派生中,访问控制权 限 应 受 到 特 别 注 意 。 关 键 字 public 使 派 生 出 的 类 保
持了基类数据的控制 权 限,而 关 键 词 private 则 使 派 生 类 中 属 于 基 类 的 数 据 成 员 和 成
员函数全部都变成为私有,改 变 了 原 来 基 类 中 公 有 成 员 的 控 制 权 限 。 这 里 需 要 提 到 类
中的一种特殊关键字 protected。 关键字 protected 其后的成员称为保护成员 。 保护成
员具有私有成员和公有成员的双重角色 。 对派生类的成员函数,保护成员是公有成员,
但对所在类之外定义的其他 函 数 则 是 私 有 成 员 。 在 公 有 派 生 下,基 类 中 的 保 护 成 员 仍
是保护成员,但在私有派生下,它 在 私 有 派 生 类 中 成 为 私 有 成 员,从 该 私 有 派 生 类 进 一
步派生的类不能直接访问 。 表2 14说明了公有派生和私有派生情况下,基类成员在派
生类中的访问权限 。
表2 14 继承和派生中的访问权限

派生性质 基类中的访问权限 派生类中的访问权限

public public
public protected protected
private private

public private
private protected private
private private

(2)多重继承
C++ 语言中允许一个类从多个类中派生出来,使这一个类具有多个基类的功能 。多重继
承的访问权限和单一继承的访问权限规则一样 。多重继承可以视为单一继承的扩展。 下面
是一个类从多个类派生的一般形式:

class 类名 1:访问控制 类名 2,访问控制 类名 3...访问控制 类名 n


 private:
成员说明列表
public: 49

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

成员说明列表

类名 1 继承了类名 2 到类名 n 的所有数据成员和成员函数。 访问控制限制了其后类中


成员在类名 1 中的访问权限。在多重继承的情况下,基类构造函数执行的顺序按继承和派
生时被说明的顺序从左到右依次调用 。
注意:构造函数是不能被继承的,所以,一个派生类只能调用它的直接基类的构造函数。
多重继承容易产生一个类有多个拷贝的情况 。 这时,如果在多条继承路径上有一个公
共的基类,在这些路径中的某几条路径的回合处,公共基类会产生多个拷贝。 当继承类的路
径过多时,就会造成内存的浪费。为了使这个公共的基类只产生一个拷贝,可以将次基类说
明为虚基类。
当从一个基类派生新类时,使用关键字 virtual 将基类说明为虚类,就能使多条继承路
径上的公共基类只产生一个拷贝 。


55 t
his指针
一个类可以定义多个对象实例,每个对象都有自己的数据成员,但不论程序中定义多少
个对象实例,其成员函数只有一个。但编译程序是如何确定调用成员函数的对象,并处理其
相应的数据成员的呢? 这里就涉及 this 指针。
this 指针是一个特殊的隐含指针,它隐含于每一个类的成员函数中,也就是说,每个成
员函数都有一个 this 指针参数,指向调用该函数的对象。
this 指针只能在类的成员函数中使用,它指向该成员函数被调用的对象。 静态成员不
能访问 this 指针,因为静态成员函数不从属于任何对象 。this 指针一般用于返回当前对象
自身。
使用 this 指针时应该注意的问题是:this 指针是一个 const 指针,成员函数不能对其
进行赋值。

6 常类型 (
2 con
st)
常类型是指使用 const 声明的类型 。 变量或对象被声明为常类型后,其值就不能被更
新,因此声明常类型时必须初始化 。 虽然数据隐藏保证了数据的安全性,但各种形式的数
据共享却又不同程度地破坏了数据的安全性 。 因此,对于既要共享又需要防止改变的数
据应该声明为 常 量,以 利 于 保 护 。 常 类 型 主 要 有 常 对 象 、常 引 用 、常 成 员 函 数 和 常 数 据
成员 。


61 常引用
使用 const 关键字声明的引用被称为常引用 。 常引用所引用的对象不能被更新,常
引用声明时,必须同时进行初始化 。 常引用的声明如下:const < 类 型 标 识 符 > & < 引 用
名>
【例2 11】 Ex02_11 常引用例题,见清单 2 11。

50
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

清单2 11 常引用

Ex02_11a 不赋初值 Ex02_11b 赋初值

# include<iostream.h> # include<iostream.h>

oi dma i n() v
oi dma in()
{ int n = 6; { int n = 6;
const int &m; const int &m = n;
m = 10; cout<< "m = "<<m<<endl;
cout<< "m = "<<m<<endl; 

注:左边的程序编译时有两个错误:errorC2530: m : references must be initialized


error C2166: lvalue specifies const object
右边的程序编译无错误,程序运行结果为 m = 6;

由此可见,常引用的值不能被更新。因此常引用声明时,必须同时进行初始化。
如果用常引用作形参,便不会发生对实参意外的更改。
【例2 12】 Ex02_12 常引用作形参例题,见清单 2 12。

清单2 12 常引用作形参

#inc
lude<i o
stre
am. h> 程序运行结果为:x = 75

oiddis l
pya (co
nst doube&x);
l //常引用作形参

oidma in()
{ double d = 75;
display(d);


oiddisplay(co
nstd oube&x)

{ cout<< "x = "<<x<<endl;

常引用作形参时,函数不能更新 x 所引用的对象,因此,对应的实参就不会被破坏。


62 常对象
使用 const 关键字声明的对象被称为常对象 。常对象的声明如下:

const <类名> <对象名> 或 <类名> const <对象名>

声明常对象的同时,也要进行初始化,声明常对象例题见清单 2 13。

清单2 13 常对象


lasA//类定义

{ private:
int x,y;
public:
A(int i = 0,int j = 0){x = i;y = i
 
//声明常对象
const A acobj(1,2);
A const bcobj(3,4)
51

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


63 常成员函数
使用 const 关键字声明的成员函数被称为常成员函数 。常成员函数的声明如下:

<类型标识符> <函数名> (参数表)const;

说明:
(1)const 是加在函数声明后面的类型修饰符,它是函数类型的一个组成部分,因此在
实现部分也要带 const 关键字。
(2)const 关键字可以被用于对重载函数的区分 。
(3)常成员函数不能更新对象的数据成员,也不能调用该类中没有用 const 修饰的成员
函数。
(4)如果将一个对象说明为常对象,则通过该对象只能调用它的常成员函数,而不能调
用其他成员函数。
【例2 13】 Ex02_13 常成员函数,见清单 2 14。

清单2 14 常成员函数

//常成员函数 程序运行结果:
#inc
lude<iostream. h> 成员函数: x = 1,y = 2
cla
ssA// 类定义 常成员函数:x = 3,y = 4
{ private:
int x,y;
public:
A(int i = 0,int j = 0){x = i;y = j;
vo
idf un() //成员函数
{ cout<< " 成员函数:x =" <<x<< " ,y =" <<y<<endl;

vo
idf un()c ons
t//常成员函数
{ cout<< " 常成员函数:x =" <<x<< " ,y = " <<y<<endl;

;
//主函数
voidma in()
{ A aobj(1,2);
aobj.fun();
const A bobj(3,4);
bobj.fun();

在 A 类中声明了两个公有成员函数 fun(),其中一个是常成员函数。在主函数中说明了
两个对象 aobj 和 bobj,其中对象 bobj 是常对象。 通过对象 aobj 调用的是一般的成员函
数,而通过对象 bobj 调用的是 const 修饰的常成员函数。


64 常数据成员
使用关键字 const 不仅可以说明成员函数,还可以说明数据成员 。 如果在一个类中
52 说明了常数据成员 (包括常引用 、常对象等 ),由于常数据成员不能被更新,因此,在类中
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

说明数据成员时,只 能 用 成 员 初 始 化 列 表 的 方 式 通 过 构 造 函 数 对 该 数 据 成 员 进 行 初
始化 。
【例2 14】 Ex02_14 常数据成员例题,见清单 2 15。
清单2 15 常数据成员

#incl
ude<iostream. h> 程序运行结果:

lasA//类定义
s x = 1,y = 5,z = 1
{ private: x = 3,y = 5,z = 3
const int x;
static const int y;//静态常数据成员
public:
const int &z;//常引用
A(int i):x(i),z(x){} //常数据成员通过初始化列表获得初值
vo
idf un()//成员函数
{ cout<< " x =" <<x<< " ,y =" <<y<< " ,z =" <<z<<endl;

;
co
n s
tin
tA y=5 //静态常数据成员的初始化
vo
idmain()//主函数
{ A aobj(1),bobj(3);
aobj.fun();
bobj.fun();

7 运算符重载

C++ 语言中已有的运算符允许重新定义(即重载)。 运算符重载是通过编写函数定义实
现的。函数定义虽然也包括函数头和函数体,但是函数名是由关键字 operator 和其后要重
载的运算符符号组成的。例如,函数名 Operator+ ()重载了运算符+ 。
【例2 15】 Ex02_15 重载复数的四则运算符。
复数由实部和虚部构造,可以定义一个复数类,然后再在类中重载复数四则运算的运算
符。见清单 2 16。
清单2 16 重载四则运算的运算符重载

#incl
ude<io
str
eam. h>

las
sc ompl
ex
 public:

omplex ()
{ real = imag = 0;


ompl
ex( doubler do ublei)
{ real = r; imag = i;

complex operator+ (const complex &c);
complex operator- (const complex &c);
complex operator * (const complex &c);
53

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

complex operator/(const complex &c);


friend void print(const complex &c);
private:
double real, imag;
;

nlin
ec omp l
exc omplex o
p e
rator+( c
on s
tcomp l
ex&c) //复数相加
{ return complex(real+ c.real,imag+ c.imag);


nlin
ec omp l
exc omplex o
p e
rator-( c
on s
tcomp l
ex&c) //复数相减
{ return complex(real- c.real,imag- c.imag);


nlin
ec omp l
exc omplex o
p e
rator( constcomplex&c) //复数相乘
{ return complex(real * c.real- imag * c.imag, real * c.imag+ imag * c.real);


nlin
ec omp l
exc omplex o
p e
rator/(c
onstcomp l
ex&c) //复数相除
{ return complex ((real * c.real+ imag * c.imag)/(c.real * c.real+ c.imag * c.imag),
(imag * c.real- real * c.imag)
/(c.real * c.real+ c.imag * c.imag));


oidma in()//主函数 该程序的运行结果为:
{complex c1(20,30),c2(40,- 20),c3; c1+ c2 = 6+ 1i
c3 = c1+ c2; c1- c2 = - 2+ 5i
cout<<"\nc1+ c2 =" ; c1* c2 = 14+ 8i
print(c3); c1/c2 = 045+ 08i
c3 = c1- c2; (c1+ c2)* (c1- c2)* c2/c1 = 961538+ 252308i
cout<<"\nc1- c2 =" ;
print(c3);
c3 = c1 * c2;
cout<<"\nc1* c2 =" ;
print(c3);
c3 = c1/c2;
cout<<"\nc1/c2 =" ;
print(c3);
c3 = (c1+ c2)* (c1- c2)* c2/c1;
cout<<"\n(c1+ c2)* (c1- c2)* c2/c1 =" ;
print(c3);
cout<<endl;

在程序中,类 complex 定义了 4 个成员函数作为运算符重载函数。 将运算符重载函数


说明为类的成员函数格式如下:

<类名> operator<运算符> (
<参数表> )

其中,operator 是定义运算符重载函数的关键字 。
程序中出现的表达式:

c1+c2

编译程序将给解释为:

54 c1.operator+ (c2)
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

其中,c1 和 c2 是 complex 类的对象。operator+ ()是运算+ 的重载函数。


重载运算符与重载一般函数的区别在于参数的个数上,C++ 运算符所能操作的操作
数的个数是规定好 的 。 因 此,重 载 运 算 符 时 参 数 的 个 数 必 须 指 定 。 C++ 编 译 器 会 根 据
参数个数和类型来决定调用 哪 一 个 重 载 函 数 。 因 此,可 以 为 同 一 个 运 算 符 定 义 几 个 运
算符函数进行不同的操作 。 一个 重 载 的 运 算 符 没 有 缺 省 参 数,并 保 持 原 来 的 优 先 级 和
结合性 。
C++ 中,程序员不能定义新的运算符,只能从C++ 已有的运算符中进行重载。 但C++ 不允
许重载以下 4 个运算符:(1)·(2)·* (3)::(4)?:

2 /O(输入/输出 )流结构
8 I
C++ 的 I/O 是以字节流的形式实现的,流实际上就是一个字节序列 。 在输入操作中,
字节从输入设备 (如键盘 、磁盘 、网络连接等 )流向内存;在输出操作中,字节从内存流向输
出设备 (如显示器 、打印机 、磁盘 、网络连接等 )。 传统 C 语言的输入/输出函数中最常见的
是 printf 函数和 scanf 函数 。 其中,printf 函数是输出函数,scanf 函数是输入函数 。 在
C++ 语言中,仍然可以使用这种方法进行输入/输出 。 但是,C++ 语言提供了更为方便的输
入/输出处理功能,即输入/输出流 (I/O stream),Visual C++ 中,I/O 流的类结构如图 2 6
所示 。

图2 6 I
/O 流结构

其中,ios 是基类,所有的 I/O 流类都是 ios 的派生类。 第一层派生类包含 istream 类


和 ostream 类。 istream 类主要用于处理输入,包含成员函数 get,getline,read 和>> 等;
ostream 类主要用于处理输出,包含成员函数 put,write 和<< 等。
第二层派生类包含 istrstream 类、istream_withassign 类、ifstream 类、iostream 类、
ofstream 类、ostream_withassign 类和 ostrstream 类。 这些类是由 istream 类或 ostream
类派生的,因此继承了相应的输入或输出功能 。
第三层派生类包含 fstream 类、strstream 类和 stdiostream 类。 fstream 类支持磁盘
文件的输入与输出,strstream 类支持内存中字符串的输入与输出,stdiostream 类是标准
I/O 文件的输入/输出类。
运算符重载为完成输入/输出提供了一种方便的途径。 重载的左移运算符 (
<< )表示流 55

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

的输出,称为流插入运算符;重载的右移运算符 ( >> )表示流的输入,称为流提取运算符。 这
两个运算符可以和标准对象 cin cout cerr 和 clog,以及用户自定义的流的对象一起使用 。
、 、
cout 是类 ostream_withassign 类的一个对象,它与标准输出设备 (通常指显示设备 )连
在一起,必须结合操作符<< 使用。 下面的语句用流插入运算符把整型变量 x 的值从内存输
出到标准输出设备上。
cout << x;
cout 最大的特点是输出的数据可以是各种数据类型,包括整型、实型、字符型等,在使用
该语句时不必考虑待输出数据的类型,也不必有相应的格式控制符 (如 printf 函数中的% d
和%c 等)。这样,无论输出变量的类型是否做了修改,cout 语句都完全一致。
cin 是派生类 istream_withassign 类的一个对象,它与标准输入设备 (通常指键盘 )连
在一起,必须结合操作符>> 使用。下面的语句用流提取运算符把整型变量 x 的值从 cin 输
入到内存中。
cin >> x;
在输入数据时,不论哪种数据类型,使用的格式都相同。

9 异常处理

异常(Exception)是程序可能检测到的、运行时刻不正常的情况。运行不正常的情况包
括被 0 除、数组越界和内存耗尽等。这样的异常独立于正常函数之外,并且需要程序立即停
止响应处理异常。
C++ 程序使用 try,throw 和 catch 关键字来处理异常。try 块用来标志运行时可能遇到
问题的代码。catch 块紧跟在 try 块后面,用来保存遇到异常时的处理代码。 throw 语句用
于将程序代码运行时遇到的异常通知异常处理代码 。
异常处理代码所使用的机制实际上相当简单 。先把要监督的有可能发生错误的代码放
在 try 程序块内部,接着构造 catch 程序块作为错误处理者。如果在 throw 块中的代码抛出
了一个异常,try 块立即停止执行,接着执行 catch 块中的部分。


91 异常处理的语法
(1)throw 语法

throw <表达式> ;

当某段程序发现了自己不能处理的异常,就可以使用 throw 语句将这个异常抛掷给调


用者。throw 语句的使用与 return 语句相似,如果程序中有多处要抛出的异常,应该用不同
的表达式类型来互相区别,表达式的值不能用来区别不同的异常 。
(2)try 块语法

try
 复合语句

因为被调用的函数可以抛出异常,所以主调函数必须能够响应这一事件 。 这种响应
56 通过捕获语句实现 。 为了捕获异常需要将代码放到 try 块中 。 try 块内的所有语句通常
第二章 C++语言基础
……………………………………………………………………………………………………………………………………………

都将按顺序处理,除非其中一个被调用函数抛出异常 。 如果发生这种情况,从 try 中传递


到相应的 catch 块的第一行 。 catch 语句便依次被检查 。 若某个 catch 语句的异常类型声
明与被抛掷的异常类型一致,则执行该段异常处理程序 。 如果异常类型声明是一个省略
号 (...),catch 语句处理任何类型的异常,但这段处理程序必须是 try 块的最后一段处理
程序 。
(3)catch 语法

catch(异常类型 1 参数 1)
{ 针对异常类型 1 的处理语句

catch(异常类型 2 参数 2)
{ 针对异常类型 2 的处理语句


catch(异常类型 n 参数 n)
{ 针对异常类型 n 的处理语句

catch 语句的参数如果是省略号 “...”那么它表示的是捕获的所有异常,包括已知的和


未知的。如果要和别的 catch 语句组合使用,那么该 catch 语句应当放在最后的位置。 该
语句放置在最后的原因是 catch 子句被检测的顺序和它们在 try 程序块之后出现的顺序是
一致的,一旦有一个匹配的异常,后面的 catch 子句将不再被检测。


92 异常处理的执行过程
异常处理的执行过程如下:
(1)通过正常的顺序执行到达程序的 try 语句,然后执行 try 块内的保护段。
(2)如果在执行 try 块内的保护段期间没有引起异常,那么跟在 try 块后的 catch 语句
就不执行,程序从异常抛掷的 try 块后跟随的最后一个 catch 语句后面的语句继续执行
下去。
(3)如果在保护段执行期间或保护段调用的任何函数中 (直接或间接的调用 )有异常被
抛掷,则从 throw 创建的对象中创建一个异常对象 。catch 处理程序按其在 try 块后出现的
顺序被检查。如果没有找到合适的处理程序,则继续检查下一个动态封闭的 try 块。 此处
理继续下去,直到最外层的封闭 try 块被检查完。
(4)如果找到了一个匹配的 catch 处理程序,catch 处理程序被执行,接下来程序跳转
到跟随在处理程序之后的语句。
(5)如果找不到一个匹配的 catch 处理程序,则程序失败,终止程序。
【例2 16】 Ex02_16 程序中的内存分配异常,见清单 2 17。
程序清单 2 17 给出了一个异常处理的程序,它分配了一些内存,接着又立即删除这些
内存。因为内存分配可能会失败,所以把内存分配的代码包含在 try 程序块中。 如果内存
分配返回的指针是 NULL,则 try 块抛出一个异常。在本例中异常对象是一个字符串 。
清单2 17 内存分配异常的处理 57

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

# include <iostream.h>

ntma in()
{ int * buffer;
try
 buffer = new int256;
if(buffer = = NULL)
throw "内存分配失败! "
else delete buffer;

catch(char* exception)
{ cout<<exception<<endl;

return 0;

程序运行说明:
当程序运行时抛出了异常时,程序的执行将跳到 catch 块的第一行 (在 try 块中的剩余
代码不被执行),catch 块的第一行仅打印输出一条消息,然后执行函数的 return 行,结束
程序。
【例2 17】 Ex02_17 除数为 0 的异常处理,见清单 2 18。
清单2 18 除数为零的异常处理

# include<iostream.h>
int Divide(int x,int y) 程序运行后的结果为:

oi dma in() 8/2 = 4;
{ try 除数为零!
 cout<< "8/2 = "<<Divide(8,2) <<endl; 这是最后一句!
cout<< "8/0 = "<<Divide(8,0)
<<endl;
cout<< "8/1 = "<<Divide(8,1)
<<endl;

catch(int)
{ cout<< "除数为零! "<<endl;

cout<< "这是最后一句! "<<endl;


ntD i
vide(i
ntx inty)
{ if(y = = 0)
throw y;
return x/y;

58
第三章 ndows应用程序
Wi
编程与 MFC
在 Windows 操作系统下,应用程序主要是以窗口的形式存在。 在不同的窗口间传递信
息,就成了 Windows 操作系统和应用程序间、不同的应用程序间互相交流的主要形式。 在
DOS 等控制台模式程序中,对诸如鼠标、键盘等的控制是通过轮询(分别查询这些设备的输入
请求)完成的。而在 Windows 环境中,这些控制是通过消息完成的,因此 Windows 也被称为
“基于事件的,消息驱动的”操作系统。
Visual C+ + (以 下 简 称 VC+ + )开 发 面 向 对 象 应 用 程 序 ,主 要 有 两 种 手 段 :一 种 是 使
用 Windows 提 供 的 Windows API 函 数 , 另 一 种 方 法 是 直 接 使 用 Microsoft 提 供 的 MFC
类库。
API 是应用程序编程接口 (Application Programming Interface)的缩写,Windows API
为 Windows 系统和 Windows 应用程序间的标准程序接口,它为应用程序提供系统的各种
特殊函数及数据 结 构 定 义,Windows 应 用 程 序 可 以 利 用 上 千 个 标 准 API 函 数 调 用 系 统
功能 。
MFC(Microsoft Foundation Class 微软基础类库)类库封装了大部分 Windows API 函数,
所包含的功能涉及整个 Windows 操作系统。它为 Windows 消息响应机制提供了比较完整的
处理功能,构成 MFC 类库的很多基类都具有常用消息的函数 。MFC 不仅提供了 Windows 环境
下应用程序的框架,而且提供了创建应用程序的组件。 使用 MFC 类库和VC++ 提供的高度可
视的应用程序开发工具,可以简化应用程序的设计,缩短开发周期,并且使代码的可靠性和
可重用性得到了很大提高。
本章要点:
* MFC 类库
* MFC 创建 Windows 程序的过程
* Windows 程序编程机制
* 消息映射与消息传递

1 MFC 类库

在VC++ 20 以上版本中,Microsoft 公司已经推出了 MFC 类库,并在 MFC 类库中提供了
一种用于基本 Windows 应用程序的结构。在面向对象的程序设计中,MFC 提供了功能完善的
应用程序框架,而且提供了以类为基础的代码块 。
MFC 完整地封装了 Windows API 函数;MFC 支持多线程;MFC 库具有同以 C 语言为基础的
使用 Windows API 开发的 Windows 应用程序的共存能力,在同一程序中,可以同时使用 MFC 59

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

的类和 Windows API 调用;MFC 提供自动消息处理功能;MFC 类库使用与 Windows API 相同的


命名约定,根据类名就可以知道类的功能。


11 MFC 基础类库
MFC 类基础类库如下所示。
(1)根类
MFC 类库中大部分类都是由 CObject 根类派生的。
(2)应用程序结构类
CCmdTarget 类是 CObject 的子类,它是 MFC 库中所有具有消息映射属性的基类。 消息
映射规定了当一对象接收到消息命令时,应调用哪一个函数对该消息进行处理。 程序员很
少需要从 CCmdTarget 类中直接派生出新类,往往都是从它的子类中派生出新类 。图 3 1 为
应用程序结构类的主要分层继承关系 。如:线程类 (CWinThread)、应用程序类 (CWinApp)、窗
口类(CWnd )、视 类 (CView )、框 架 窗 口 类 (CFrameWnd )、文 档 类 (CDocument )、文 档 模 板 类
(CDocTemplate)等。

图3 1 应用程序结构类

(3)图形处理类
(4)简单数据类型类
(5)数组、列表和映射类
(6)文件和数据库类
(7)Internet 和网络类
(8)OLE 类
(9)调试和异常处理类
MFC 各类的具体展开见图 3 2。


12 COb
j t类
ec
CObject 类是 MFC 类库的首要基础类,大部分的 MFC 类都是由 CObject 派生的。 由于
CObject 类描述了几乎所有 MFC 中其他类的一些共有特性,因此,有必要了解一下 CObject
类。CObject 类的声明在头文件 afx.h 中,该类的部分定义见清单 3 1。
60
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

61

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单3 1 COb
jet类的定义



lassCo bject
 public:
virtual CRuntimeClass* GetRuntimeClass()const;

public:
BOOL IsSerializable()const;
BOOL IsKindOf(const CRuntimeClass* pClass)const;
//Overridables
virtual void Serialize(CArchive& ar);
# if defined(_DEBUG)|| defined(_AFXDLL)
//Diagnostic Support
virtual void AssertValid()const;
virtual void Dump(CDumpContext& dc)const;
# endif
//Implementation
public:
static const AFX_DATA CRuntimeClass classCObject;
# ifdef _AFXDLL
static CRuntimeClass* PASCAL _GetBaseClass();
# endif
;

从以上代码可以看出,CObject 包含了支持一系列基本操作的成员函数,为派生类提供
了一些基础服务,主要包括以下几方面。
(1)串行化(串行类)
串行化是指对象将其状态保存在一个字节流中并用该字节流重建对象的能力 。 如果一
个对象支持串行化,那么它就可以保存到一个文件中 。具体见第 8 章。
(2)运行期的类型信息(Dynamic 和 DynCreate 类)
当一个类支持 MFC 的运行期类型信息 (RTTI)时,它能响应对它的名字和它的基类的请
求。CObject 的成员函数 IsKindOf 用来支持运行时类信息。 IsKindOf 用于判断对象是否
属于或派生于指定 的 类,参 数 为 CRuntimeClass 对 象,可 以 用 RUNTIME _ CLASS 宏 来 得 到。
RUNTIME_CLASS 宏的参数是类名。如果对象是指定类的成员或是指定类的派生类的成员,函
数返回 TRUE。

例: CMyPainterDoc * CMyPa
i nterView GetDocumen t()
{ ASSERT(m_pDocument- > IsKindOf(RUNTIME_CLASS(CMyPainterDoc)));
return (CMyPainterDoc* )m_pDocument;

若要支持一个 Dynamic 形式的运行期类型信息(RTTI)时,首先要从 CObject 类间接或直
接派生自己的类。然后在类的声明中使用 DECLARE_DYNAMIC 宏,而在类的实现中 (* .cpp 文
件)使用 IMPLEMENT_DYNAMIC 宏。
Dynamic 类的一个扩展类是 DynCreate。 若要支持动态类的创建,首先要从 CObject 类
62 间接或直接派生自己的类。 然后在类的声明中使用 DECLARE_DYNCREATE 宏,并定义一个无
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

参数的构造函数(默认构造函数),而在类的实现(* .cpp 文件)中使用 IMPLEMENT_DYNCREATE


宏。最后在类的源文件使用 CRuntimeClass 的成员函数 CreateCObject 来创建对象。
(3)支持诊断
① AssertVaild 成员函数:如果要检查类的某个对象是否有效,那么可以重载 CObject
的 AssertVaild 成员函数,该函数只有在调试状态才有效。

例://类的声明
class CMyPainterDoc: public CDocument

public:
# ifdef _DEBUG
virtual void AssertValid()const;
virtual void Dump(CDumpContext& dc)const;
# endif

//类的实现
# ifdef _DEBUG
void CMyPainterDoc::AssertValid()const
 CDocument::AssertValid();
ASSERT(m_no!= 0)

ASSERT 宏 ASSERT 宏的参数是一个 BOOL 表达式。如果表达式为真,那么程序正常运


行,否则弹出一个对话框。对话框有三种选择,让用户确定是立即退出程序,还是关闭对话
框后继续执行,或者在 ASSERT 失败之处进入调试器。 ASSERT 宏在调试 (Debug)状态下才计
算表达式。因此,如果要在发行(Release)状态下产生表达式的值,那么可以用 VERIFY 宏来
代替 ASSERT 宏。
ASSERT_KINDOF宏 ASSERT_KINDOF 宏是 ASSERT 宏的导出宏,它用于检查指针对象
是否为指定类或派生类的对象。用法:ASSERT_KINDOF(类名,指针对象)
② Dump 成员函数:如果发现问题或想输出对象的当前状态信息,那么可以重载 CObject
的成员函数 Dump。Dump 只有在调试状态才有效。

例:

oidCMyPain
terDocDump( CDumpCon
tex c)c
t&d ons

 CDocument::Dump dc ;
( )
dc << "姓名: "<< m_name << "\n "<< "学号: "<< m_no << "\n ";

Dump 的输出会被送到输出窗口。
除了使用成员函数 Dump 外,还可以使用 TRACE 宏将输出写入调试环境。 TRACE 宏可以
跟踪程序运行时变量的值。TRACE 宏接收包括位置占位符在内的字符串,使用与 C 程序中的
printf 函数相同的变量输出方式。 63

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

例:int i = 1;
char sz= "one "
TRACE("Integer = %d,string = %s\n ",i,sz);

2 利用 MFC 创建 Wi
3 ndows应用程序框架
使用 MFC AppWizard 创建基于 MFC 的 Windows 应用程序框架时,AppWizard 显示一系列
提示对话框,给出各种选项;程序员选择满足要求的选项。确定所有选项后,AppWizard 生成
应用程序文件,包括源文件、头文件、资源文件和工程文件等。
下面以建立一个多文档应用(MDI)程序为例说明操作步骤。
(1)启动 VC 60
(2)从 File 菜单中选择 New,打开 New 对话框
(3)确定创建的类型
选择 Projects 标签,从左边的工程类型列表框中选择要创建的工程类型 。可选择的应
用程序类型如表 3 1 所示。

表3 1 应用程序类型

项 目 名 称 含 义
ALT COM AppWizard 创建活动模板(ALT)项目工程,一般用于编写小的 ActiveX 控件
Cluster Resource Type Wizard 创建能够在微软群服务器上模拟和管理的项目工程
Custom AppWizard 以用户定制的模板向导创建工程
Database Project 创建数据库工程
DevStudio AddIn Wizard 创建一个向开发环境加入命令的项目工程框架
Extended Stored Proc Wizard 创建扩展存储过程的程序向导
ISAPI Extension 创建用于 Internet 的 DLL
Makefile 创建与内部 makefile 协同工作的项目工程
MFC ActiveX Control Wizard 创建基于 MFC 的 ActiveX 控件程序
MFC AppWizard(dll) 用 AppWizard 创建 MFC 动态链接库
MFC AppWizard(exe) 用 AppWizard 创建 MFC 可执行程序
New Database Wizard 创建网络数据库工程
Utility Project 创建不产生任何预定义文件输出工程
不使用 MFC 和 AppWizard,只是简单地为用户创建一个工程文件,用户
Win32 Application
必须自己生成所有的代码
Win32 Console Application 创建 Windows 控制台程序
Win32 DynamicLink Library 创建不需要模板且不利用 MFC 的 DLL,所有代码都必须由用户完成
Win32 Static Library 创建静态链接库的默认项目工程

本例中选择 MFC AppWizard (exe),如图 3 3 所示。表示利用 MFC AppWizard(应用程序


向导)建立可执行程序。
(4)在 Project name 文本框中键入要创建的工程名
本例工程名为 MyPainter。AppWizard 将根据该名派生相应的开始文件和类的缺省名,
64 并在根目录下创建以其命名的子目录 。
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

图3 3 Pr
oje
cts标签

(5)单击 Location 文本框右边的“...”按钮修改缺省目录


(6 ) 如 果 要 创 建 新 的 工 程 并 将 其 添 加 到 当 前 工 程 中, 则 选 择 单 选 按 钮 Add to
current workspace
(7)单击 OK 按钮,进入 MFC AppWizard Step 1,打开如图 3 4 所示的对话框
AppWizard 可以创建以下三种类型的应用程序结构 。
① Single document: 单 文 档 界 面 (SDI )一 次 只 允 许 打 开 一 个 文 档。 例 如: 记 事 本
notepad 就是典型的 SDI 应用程序。
② Multiple documents:多文档界面 (MDI)允许同时打开多个文档。 例如,Microsoft
Word 和 Excel 就是典型的 MDI 应用程序。

图3 4 MFCAppWi
zar
dS
tep1o
f6 图3 5 MFCAppWi
zar
dS
tep2o
f6

③ Dialog based:基于对话框的应用程序不支持文档/视图结构,而是仅仅显示一个简
单的对话框。这对于小的应用程序是有用的,但如果要用到菜单、工具栏和打印等功能,那
65

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

么就应该创建基于文档的应用程序 。
本例选中 Multiple documents(多文档 )单选按钮:资源语言为中文 (中国 )(APPWZCHS.
DLL)。
(8)单击 Next 按钮进入 MFC AppWizard Step 2 of 6
打开如图 3 5 所 示 的 对 话 框 ,从 中 选 择 数 据 库 支 持 。 各 种 选 择 的 具 体 作 用 见 表
3 2。
表3 2 MFCAppWi
zar
dS
te f6各选项的含义与作用
p2o
控件 选 项 含义与作用
不支持任何 ODBC 库。如果应用程序不使用数据库,那么选择该项将
None
建立比较小的应用程序
提供最低限度的数据库支持,包含数据库头文件(AFXDB.H)和链接库,
单 Header files only
但不创建任何与数据库相关的类,用户必须自己创建

按 包含所有的数据库头文件和链接,并创建与数据库相关的类。这个
Database view without
钮 选项创建的应用程序允许用户查看和更新记录集中的记录,并具有
file support
文档但不支持串行化
Database view with
与上一选项唯一不同之处在于不仅有文档支持还有串行化支持
file support

如果 应 用 程 序 包 含 数 据 库 视 图,那 么 还 应 定 义 数 据 源,方 法 是:单 击 “Date source”按 钮,从 弹 出 的


“Database Options”对话框来确定应用程序的数据源和记录集类型

本例选择缺省选择 None,表示不带数据库支持。
(9)单击 Next 按钮,进入 MFC AppWizard Step 3 of 6
打开如图 3 6 所示的对话框。从中可以选择复合文档 (Compound document)支持选项。
各选项的含义与作用见表 3 3。
表3 3 MFCAppWi
zar
dS
te f6各选项的含义与作用
p3o
选 项 作 用
None 默认设置,不带复合文档支持
Container 应用程序作为容器,可以合并嵌入对象和链接对象到自己的文档中
应用程序可以创建和管理复合文档对象,但不能单独运行,仅支持嵌入
Mini_server
对象
应用程序既可创建和管理复合文档,又可单独运行,同时支持链接与嵌
Full_server
入对象
Both container and server 应用程序既作为容器,又作为服务器

使用复合文件格 式 来 串 行 化 容 器 的 文 档。复 合 文 件 格 式 能 在 一 个
Yes please 文件中保存 一 个 或 多 个 自 动 化 对 象,并 允 许 被 单 独 的 自 动 化 对 象
访问

不使用复合文档格式来串行化容器文档。该选项必须一次将包含自动
化对象的整个文件装入到内存中,不允许增量式保存单独的自动化对
No thank you
象。如果一个自动化对象被更改并加以保存,那么文件中所有的自动
化对象都必须保存
66
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

续 表
选 项 作 用
使应用程序支持自动化,这样应用程序就可以操作其他应用程序创建
Automation
的对象,或者暴露自动化对象给自动化客户访问
使应用程序使用 ActiveX 控件。如果不选择该项,以后要插入 ActiveX
ActiveX Controls 控件到项目中,就必须在应用程序的 InitInstance 成员函数中添加对
AfxEnableControlContainer()函数的调用

本例选择缺省 None,表示不带复合文档支持。

图3 6 MFCAppWi
zar
dS
tep3o
f6 图3 7 MFCAppWi
zar
dS
tep4o
f6

(10)单击 Next 按钮,进入 MFC AppWizard Step 4 of 6


打开如图 3 7 所示的对话框。各选项的含义与作用见表 3 4。
表3 4 MFCAppWi
zar
dS
te f6各选择的含义与作用
p4o
选 项 含义与作用
Docking toolbar 缺省设置,设置一个工具栏
Initial status bar 缺省,创建一个用于显示菜单提示和其他信息的状态栏
缺省,文件菜单中将含有打印和打印预览选项。并且 AppWizard 将生成
Printing and print preview
有关打印功能的大多数代码
帮助菜单中将含有帮助主题(H)选项,AppWizard 生成有关帮助功能的
Context sensitive help
大多数代码
3D controls 缺省,使界面具有三维外观
MAPI(Messaging API) 可以使用 Messaging API 传递传真、e_mail 或其他信息
Windows Sockets 可以通过 FTP 和 HTTP(WWW 协议)连接 Internet
Normal 缺省,工具栏为传统风格
Internet Explorer ReBars 工具栏为 IE 风格,其中可以包含任何标准 Windows 控件

单击 Advanced 按钮将显示如图 3
8 所示的对话框,其中包括两个标签。 在该对话框
中,AppWizard 为应用程序建立了一些名称和提示,可用于简化应用程序名。
图 3 9 为 Advanced Options 对话框的 Window styles 标签。各选项含义见表 3 5。选
择所需选项后,单击 Close 按钮返回到 AppWizard 第 4 步。 67

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图3 8 Ad
van
cedOp
tio
ns对话框 图3 9 Ad
van
cedOp
tio
ns对话框之二

表3 5 Ad
van
cedOp
tio
ns对话框的 Wi
ndows
tyl
es标签各选项的含义与作用

选 项 含义与作用
Use split window AppWizard 将生成使用切分窗口的代码
Thick frame 框架将有一个可见的较粗的边缘,并且可以按通常的 Windows 方式调整尺寸
Minimize box 在框架的右上角有一个最小化框标志
Maximize box 在框架的左上角有一个最大化框标志
System menu 在框架的左上角有一个系统菜单
Minimized 当应用程序启动时,框架被最小化
Maximized 当启动应用程序时,最大化框架

(11)单击 Next 按钮,进入 MFC AppWizard Step 5 of 6


打开如图 3 10 所示的对话框。(对话框中的各选项的含义与作用见表 3 6)接受所有
缺省设置。

图3 10 MFCAppWi
zar
dS
tep5o
f6 图3 11 MFCAppWi
zar
dS
tep6o
f6

(12)单击 Next 按钮,进入 MFC AppWizardStep 6 of 6


68 打开如图 3 11 所示的对话框,显示 AppWizard 即将创建的类、头文件 (.h)基类和实现
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

文件(.cpp)的名称,可以修改。
表3 6 MFCAppWi
zar
dS
te f6各选项的含义与作用
p5o
选 项 含义与作用
设置工程风格
MFC Standard 缺省,标准风格
Windows Explorer Wndows 资源管理器风格
确定是否生成源文件注释
缺省,Appwzard 在源文件中插入相应注释以
Yes,please
指示在何处需要添加程序员自己的代码
No,thank you 不插入源文件注释
确定如何使用 MFC 库
As a shared DLL 缺省,应用程序在运行时调用 MFC 库
As a statically linked library 在创建应用程序时与静态 MFC 库链接

(13)单击 Finish 按钮,显示上述创建选择信息的 New Project Information 对话框


(14)单击 New Project Information 对话框的 OK 按钮,AppWizard 自动生成应用程序的
各种文件
通过以上步骤完成程序框架的建立 。
(15)编译和运行可执行的应用程序
可执行程序有调试版 (Debug)和发布版 (Release)两种。 在调式过程中选择 Debug 版,
这样得到的可执行程序不能脱离 VC 平台运行;程序调试好后即将投入使用选择 Release
版,这样得到的可执行程序可脱离 VC 平台运行。编译和运行步骤如下:
① 从 Build 菜单中选择“Set Active Project Configuration”弹出如图 3 12 所示的
对话框。
在该对话框中选择 MyPainterWin32 Debug,然后点击“OK”按钮。

图3 12 S
etAc
tiv
ePr
oje
ctCo
nfi
gur
ati
on对话框 图3 13 应用程序运行窗口

② 从 Build 菜单中选择“Build MyPainter.exe”命令编译、链接,以生成可执行文件。


③ 从 Build 菜单中选择“Execute MyPainter.exe”运行应用程序,结果如图 3 13 所示。
从图 3 13 可以看出,MyPainter 程序含有标题栏、菜单栏、工具栏和状态栏,这些都在
主边框窗口中,作为 MDI 应用程序,还有一个打开的文档框架窗口,即子边框窗口。
69

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

3 程序中的文件和主要类


31 程序中的文件和主要类
AppWizard 实际为“MyPainter”应用程序创建了 19 个文件,这些文件存放在工程目录
MyPainter\下,如表 3 7 所示。

表3 7 AppWi
zad为 MyPa
r int
er应用程序创建的文件

文 件 名 描 述 文 件 名 描 述
ChildFrm.h 子窗口类头文件 MyPainter.rc 通用资源文件
ChildFrm.cpp 子窗口类源程序文件 Resource.h 应用程序资源头文件
MainFrm.h 主窗口类头文件 StdAfx.h 标准应用程序结构头文件
MainFrm.cpp 主窗口类源程序文件 StdAfx.cpp 标准应用程序结构源程序文件
MyPainter.h 应用类头文件 MyPainter.rc2 应用程序资源文件
MyPainter.cpp 应用类源程序文件 MyPainter.ico 应用程序图标文件
MyPainter Doc.h 文档类头文件 MyPainterDoc.ico 应用程序文档图标文件
MyPainter Doc.cpp 文档类源程序文件 Toolbar.bmp 工具按钮图形文件
MyPainter View.h 视图类头文件 ReadMe.txt 帮助文本文件
MyPainter View.cpp 视图类源程序文件

AppWizard 为程序自动生成了以下 6 个类,这些类的基类和所对应的文件如表 3 8


所示。

表3 8 AppWi
zad为程序自动生成的6个类

派 生 类 MFC 基类 头文件名 源文件名


CMyPainterApp CWinApp MyPainter.h MyPainter.cpp
CMyPainterDoc CDocument MyPainterDoc.h MyPainterDoc.cpp
CMyPainterView CView MyPainterView.h MyPainterView.cpp
CChildFrame CMDIChildWnd ChildFrm.h ChildFrm.cpp
CMainFrame CMDIFrameWnd MainFrm.h MainFrm.cpp
CAboutDlg CDialog MyPainter.h MyPainter.cpp

其中 CAboutDlg 对应着应用程序 Help 菜单下的 About 对话框,CChildFrame 只有在选择创


建多文档程序时才会出现。CWinApp, CDocument, CView, CFrameWnd 类再加上 CDocTemplate 这
五个类构成了大部分 MFC 应用程序的核心。程序中惟一的一个必不可少的类就是 CWinApp。


32 应用程序类(
CMyPa
int
erApp类)
CMyPainterApp 类是由 CWinApp 派生的,应用程序的初始化、运行和结束工作都是由应
用程序类来完成的。MyPainterApp.h 头文件代码见清单 3 2 所示。
在该头文件中声明的所有函数都是由 AppWizard 自动添加的。 以//Overrides 开始的
代码是用于覆盖由基类继承的函数,而以//Implementation 开始的代码是关于消息映射的
70 函数。需要指出的是,这些是由程序员自行处理,主要是通过 ClassWizard 来完成。
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

清单3 2 MyPa
int
e h头文件
r.


lassCMyPa interApppub licCWi nApp
 public:
virtual BOOL PreTranslateMessage(MSG* pMsg);
CMyPainterApp();
//Overrides
//ClassWizard generated virtual function overrides
//AFX_VIRTUAL(CMyPainterApp)
public:
virtual BOOL InitInstance();
//}}AFX_VIRTUAL
//Implementation
//AFX_MSG(CMyPainterApp)
afx_msg void OnAppAbout();
//NOTEthe ClassWizard will add and remove member functions here.
//DO NOT EDIT what you see in these blocks of generated code !
//AFX_MSG
DECLARE_MESSAGE_MAP()
};

应用程序类控制了执行程序的流程,程序运行流程见图 3 14。

图3 14 w
i ows应用程序运行机制
nd

在运行应用程序时,程序的基本框架首先取得控制权,做一些初始化的工作,构造应用程
序类的对象。每个 MFC 程序都必须有且只有一个由 CWinApp 派生的类的对象,在 MyPainter 程
序的 MyPainter.cpp 文件中,定义了 CMyPainterApp theApp,并把它声明为全局类型,构造对象时
要调用程序类的构造函数(声明为全局变量可以保证先调用构造函数)。
所有的 Windows 程序中都有一个主函数 WinMain, MFC 把 WinMain 函数隐藏在应用程序
的基本框架内部。创建完所有的静态和全局对象后,调用入口函数 WinMain。WinMain 函数首
先获得指向应用程序类对象的指针,然后进行初始化工作,即调用 InitInstance 函数进行初始
化。然后调用应用程序类的 Run 函数,运行程序的消息循环,该循环持续反复检测是否有用户 71

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

事件发生。每次检测到一个用户事件,程序就对之作出响应。这些事件包括移动鼠标指针、按
键、
单击或双击按钮等。当 Windows 接收到这些事件后,会产生一些相应的消息。 应用程序
接到这些消息后,按每一个消息的专门用途,产生一系列的执行动作。 这些执行动作称之为
响应。响应包括重画窗口、改变窗口大小、关闭窗口、打开一个位图、调用一个动态库等。 最
后调用应用程序类的 ExitInstance 函数结束程序,删除注册的窗口,释放内存等。
下面介绍应用程序类中的几个常用函数,它们分别完成程序初始化、 运行和结束等工作。
(1)InitInstance 函数
这个函数用来初始化一个应用程序实例,Windows 允许同时运行同一程序的多个副本或
实例。InitInstance 函数的源代码见清单 3 3。
清单3 3 I
nit
Ins
tan
ce()函数(在 MyPa
int
er.
cpp源文件中)
BOOLCMyPa interApp Ini
tInst
ance()
{ AfxEnableControlContainer();
# ifdef _AFXDLL
Enable3dControls();//Call this when using MFC in a shared DLL
# else
Enable3dControlsStatic(); //Call this when linking to MFC statically
# endif
SetRegistryKey(_T("Local AppWizardGenerated Applications" ));
LoadStdProfileSettings(); //Load standard INI file options (including MRU)
//Register the applications document templates. Document templates
//serve as the connection between documents, frame windows and views.
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_PAINTETYPE,
RUNTIME_CLASS(CMyPainterDoc),
RUNTIME_CLASS(CChildFrame),//custom MDI child frame
RUNTIME_CLASS(CMyPainterView));
AddDocTemplate(pDocTemplate);
//create main MDI Frame window
CMainFrame* pMainFrame = new CMainFrame;
if (! pMainFrame- >LoadFrame(IDR_MAINFRAME))
return FALSE;
m_pMainWnd = pMainFrame;
//Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
//Dispatch commands specified on the command line
if (! ProcessShellCommand(cmdInfo))
return FALSE;
//The main window has been initialized, so show and update it.
pMainFrame- >ShowWindow(m_nCmdShow);
pMainFrame- >UpdateWindow();
return TRUE;

InitInstance 函数代码解释如下。
① InitInstance 函数首先调用 AfXEnableControlContainer 函数使应用程序能够包含
72 ActiveX 控件,然后调用 Enable3dControls/Enable3dControlsStatic 函数来允许 3D 控件,
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

其 中 Enable3dControls 函 数 应 用 程 序 用 于 使 用 动 态 链 接 库 的 情 况, 而
Enable3dControlsStatic 函数则用于应用程序使用静态链接库的情况 。
② 调用 SetRegistryKey 函 数 通 知 MFC 使 用 注 册 表 而 不 是 INI 文 件 存 储 文 件 信 息。
LoadStdProfileSetting 函数加载标准 INI 文件设置和 MRU 文件列表 (最近使用过的文件名
称)。
③ 通过以下代码创建并注册文档模板 。
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_PAINTETYPE,
RUNTIME_CLASS(CMyPainterDoc),
RUNTIME_CLASS(CChildFrame),//custom MDI child frame
RUNTIME_CLASS(CMyPainterView));
AddDocTemplate(pDocTemplate);

其中,传给 CMultiDocTemplate 构造函数的第一个参数文档类型中使用的资源 ID,可能


包括菜单、图标、加速键等。其他三个参数中的 RUNTIME_CLASS 是在 afx.h 中定义的宏,每次
调用都返回一个指向 CRuntimeClass 的指针,其定义如下:

# define RUNTIME_CLASS(class_name)(class_name::GetThisClass())

文档模板对象创建后,使用 AddDocTemplate 来注册模板对象,将文档模板存放在应用


程序对象中。
④ 注册文档模板后,InitInstance 建立了一个空的 CommandLineInfo 对象来保存所有
参数,然后调用 ProcessShellCommand 完成参数所请求的任务。 表 3 9 为此函数所支持的
命令行参数。

表3 9 Pr
oce
ssSh
ell nd函数的命令行参数
Comma

参 数 作 用
无 启动应用程序并打开新文件
文件名 启动应用程序并打开指定文件
/p 文件名 启动应用程序并在缺省打印机上打印指定文件
/pt 文件名打印机驱动程序端口 启动应用程序并在指定打印机上打印指定文件
/DDe 启动应用程序并等待 DDE 命令
/Automation 启动 OLE 自动化服务器
/Embedding 启动应用程序并编辑嵌入的 OLE 对象

⑤ InitInstance 使用如下语句显示并更新窗口。
m_pMainWnd- > ShowWindow(SW_SHOW);
m_pMainWnd- > UpdateWindow();

在函数末尾,返回 TRUE 以表明应当运行应用程序的其他部分 。对于 myPainter 来说,其


每个实例都拥有一个主框架窗口、一个子框架窗口,一个文档和一个视图与 CMyPainterApp
类对象相连接。 73

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(2)Run 函数
初始化完成后,WinMain 函数调用 CWinApp 的成员函数 Run 处理消息循环。 Run 函数不
断地查询应用程序的消息队列,如果有消息,就把消息发给 Windows,由 Windows 调用处理消
息的窗口函数 (窗口函数同样被隐藏在应用程序的基本框架中 ),如果没有消息,就调用
CWinApp 的成员函数 OnIdle 作一些程序在空闲时做的工作,如果空闲时要做的工作也没有,
那么 应 用 程 序 就 一 直 等 待。 当 用 户 关 闭 应 用 程 序 的 实 例 时, Run 函 数 就 会 调 用
ExitInstance 函数,作一些退出前的处理工作。通常不要重载 Run 函数,因为它执行了缺省
的消息循环。
(3)ExitInstance 函数
当用户需要结束应用程序的运行实例时,就由 Run 调用 ExitInstance 函数,做一些专门
的扫尾清理工作。如果用户需要在程序退出前做专门的清理工作,如释放执行期间占用的
内存,可以重载 ExitInstance 函数。
(4)OnIdle 函数
OnIdle 函数在应用程序消息队列中没有消息时由 Run 函数调用。 默认时 OnIdle 用于
执行更新用户界面对象的状态及清理运行过程中的临时对象 。 用户可以重载它完成一些希
望在后台执行的操作。


33 程序的其他类
(1)文档类(CMyPainterDoc 类)
MyPainter 程 序 的 文 档 类 称 为 CMyPainterDoc, 该 类 是 从 CDocument 类 派 生 的。
CMyPainterDoc 类的头文件为 MyPainterDoc.h,源文件为 MyPainterDoc.cpp。文档类用于响
应数据文件的读写以及存储视图类所要查看和处理的信息 。
(2)视图类(CMyPainterView 类)
MyPainter 程 序 的 视 图 类 称 为 CMyPainterView, 该 类 是 从 CView 类 派 生 的。
CMyPainterView 类的头文件为 MyPainterView.h,源文件为 MyPainterView.cpp。 视图类指
定用户以什么方式见到文档数据,以及如何与其进行交互。
(3)主边框窗口类 (CMainFrame 类)
MyPainter 程 序 的 主 边 框 窗 口 类 为 CMainFrame, 该 类 是 从 CMDIFrameWnd 类 派 生 的。
CMainFrame 类的头文件为 MainFrm.h,源文件为 MainFrm.cpp。主边框窗口类用于管理应用
程序窗口,显示标题栏、菜单栏、工具栏、状态栏、控制菜单和控制按钮。 主边框窗口是所有
MDI 子窗口的包容器。
(4)子边框窗口类(CChildFrame 类)
MyPainter 程序的子边框窗口类为 CChildFrame,该类 是 从 CMDIChildWnd 类 派 生 的。
CChildFrame 类的头文件为 ChildFrm.h,源文件为 ChildFrm.cpp。 子边框窗口类用于管理
在 MDI 主边窗口中打开的各个文档。每个文档及其视图都有一个 MDI 子窗口。 MDI 子窗口
包含在主边框窗口中,而且没有自己的菜单栏、工具栏和状态栏,必须与包含它的主边窗口
共享。
(5)对话框类(CAboutDlg 类)
该对象是专门定义 About 对话框的内容和显示的,派生于 CDialog 类。 该类的定义和
74 实现函数都在 MyPainter.cpp 文件中,见清单 3 4。
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

清单3 4 CAb
outD
lg类的定义和实现代码


lassCAb outDlgpub licCD ia
log
 public:
CAboutDlg();
//Dialog Data
//AFX_DATA(CAboutDlg)
enum IDD = IDD_ABOUTBOX;
//AFX_DATA
//ClassWizard generated virtual function overrides
//AFX_VIRTUAL(CAboutDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX);//DDX/DDV support
//AFX_VIRTUAL
//Implementation
protected:
//AFX_MSG(CAboutDlg)
//No message handlers
//AFX_MSG
DECLARE_MESSAGE_MAP()
};
CAbo u
tD lgCAb o utDg():CD
l ial
og(
CAb outDlgIDD)
{ //{{AFX_DATA_INIT(CAboutDlg)
//}}AFX_DATA_INIT


oidCAboutDlgDoDat aEx
change(CDat
aEx
cha
nge pDX)
{ CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CAboutDlg)
//}}AFX_DATA_MAP

BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
//{{AFX_MSG_MAP(CAboutDlg)
//No message handlers
//AFX_MSG_MAP
END_MESSAGE_MAP()
//App command to run the dialog

oidCMyPa int
erApp OnAppAb out()
{ CAboutDlg aboutDlg;
AboutDlg.DoModal();

4 消息和消息处理

在 Windows 操作系统下,应用程序所做的事大部分是对消息驱动进行响应,而这些响应
几乎都是基于处理 Windows 消息的。 MFC 类库为窗口下的消息处理提供了框架 。 这些从
CCmdTarge 的类派生出来的类能够拥有自己的消息映射,这种功能使 MFC 类库的消息处理更
为简单,并能够加强类的功能性封装,避免了使用类时的重复性操作。

75

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


41 消息的分类
Windows 系统预定义了许多消息,每个消息都拥有一个宏定义,即用形象的字符串来标识
消息。可以在 Microsoft Visual Studio 所在的目录的 WinUser.h 文件中找到这些定义。
Windows 可以处理 3 种不同类型的消息。
(1)标准 Windows 消息
除了 WM_COMMAND 消息,所有以 WM_为前缀的消息都是标准的 Windows 消息。 例如 WM_
PAINT、WM_CHAR、WM_MOUSEMOVE 等。
标准的 Windows 消息由窗口和视图处理,CWnd 和它的派生类都可以接收标准的 Windows
消息。

# define WM_PAINT 0x000F//绘图


# define WM_CHAR 0x0102//字符消息
# define WM_MOUSEMOVE 0x0200//鼠标移动消息

(2)控件通知消息
控件通知消息包括按下按钮、编辑框等控件产生的消息。它以 WM_COMMAND 为消息名。同
标准 Windows 消息一样,控件通知消息由窗口和视图处理。例如,当用户对编辑控件中的文本
作出了修改后,编辑控件向其父窗口发送的 WM_COMMAND 消息中包含了 EN_CHANGE 控件通告码。
窗口的消息处理函数随即对该通告消息作出合适的处理,比如接收输入到控件中的文本。
(3)命令消息
与控件通知消息相同,命令消息也以 WM_COMMAND 为消息名。命令消息是由用户界面对
象产生的,如工具栏按钮、菜单或加速键等都是可以产生命令消息的用户界面对象 。
命令消息与其他消息的处理不同,它可被更广泛的对象 (如文档 、文档模块 、应用程
序模块 、窗口和视图等 )处理 。 如 果 某 条 命 令 直 接 影 响 到 某 个 对 象,则 由 该 对 象 来 处 理
这条命令 。
MFC 类库为了进一步扩展重复使用性,为大多数 Windows 应用程序的命令提供了默认操
作,这些默认操作中大部分同时也被包含在由 AppWizard 产生的默认菜单中,表 3 10 列出
了 MFC 类库中有默认操作的菜单命令。这些由 AppWizard 创建的菜单命令消息在 AFXRES.H
中定义,它们的命名规则一般是 ID_+ 菜单名+ 命令名。
如果想执行这些菜单命令,可以从应用程序中的任何一处发送一条预定义的命令消息,
MFC 类库就会进行自己的默认处理。程序员也可以对这些命令进行重载、编辑,以实现自己
定义的功能。
表3 10 MFC 类库中默认操作菜单命令

菜单项 默认菜单命令

文件菜单 New, Open, Close, Save, Save as, Page Setup, Print Setup, Print, Print Preview, Exit

编辑菜单 Clear, Clear All, Copy, Cut, Find, Paste, Replace, Select All, Undo, Redo
窗口菜单 New, Arrange, Cascade, Tile Horizontal, Tile Vertical, Splite
视图菜单 Toolbar, Status Bar
76 帮助菜单 Index, Using Help, About
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

如果有必要的话,用户也可以自定义消息用于特定操作使用 。
不同的消息有不同的消息前缀,见表 3 11 所示。

表3 11 Wi
ndows消息前缀和对象类型

消息前缀 对象类型 消息前缀 对象类型 备 注


ABM、ABN 系统菜单消息 NM 列表视图消息 有的为成对出
ACM、CAN 动画控件消息 PBM 父窗口通知消息 现的消息前
BMBN 按钮控件消息 PBT 进度条控件消息 缀, 例 如 ABM
和 ABN。 如 果
CB、CBN 组合框控件消息 PSM、PSN 属性表消息
以 M 结尾表示
CDM、CDN 普通对话框消息 SB 状态栏消息
消息是发送给
CPL 控制面板应用程序消息 SBM 滚动条消息 控 件 的, 而 如
DBT 设备更改消息 STM、STN 静态控件消息 果以 N 结尾则
DL 列表框拖动消息 TB、TBN 工具栏消息 表示消息是传
DM 对话框消息 TBM 轨迹条消息 递给窗口的
EM、EN 编辑框消息 TCM、TCN 标签控件消息
FMEVENT 文件管理器消息 TTM、TTN 工具提示消息
HKM 标题控件消息 TVM、TVN 树形控件消息
IMC、IMN 热键控件消息 UDM 上下控件消息
LB、LBN IME 窗口消息 WM 一般窗口消息
LVM、LVN 列表框控件消息

(4)消息标志符取值范围
消息标志符取值范围见表 3 12。

表3 12 消息标志的取值范围

消 息 类 型 取 值 范 围 消 息 类 型 取 值 范 围
系统定义消息 0x0000 到 0x03FF 系统定义消息 0x0800 到 0xBFF
用户定义内部消息 0x0400 到 0x07FF 用户定义外部消息 0xC000 到 0xFFFF

所有在 Windows 环境下的编程,都必须谨慎而恰当地处理消息。 在 Windows 中,消息是


独立的语言,是由大量的物理数据组成的,很容易被排列和优先化。 消息独立于特定的语言
或处理器类型,这使基于消息的程序能很容易地移植到不同机器的 Windows 环境下。 对
Windows 消息的相同处理机制,使包括VC++ 在内的一系列可视化编程得到长足的发展 。


42 消息映射
MFC 应用程序通过一些宏将特定的消息映射到派生类中相应的成员函数上,这种方法被
称作消息映射机制。消息映射机制实质上就是消息及其处理函数的一一对应表以及分析、
处理这张表的应用程序内部的一些代码 。
在 MFC 中,凡是从 CCmdTArget 派生的类都可以有消息映射,如果要建立消息映射,需要
进行以下操作。
(1)在类的头文件(.h)中声明消息映射表
在通常情况下被写在一个类定义的最后 。
例:DECLARE_MESSAGE_MAP()
//清单 3 2 的最后一句 77

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(2)在类的头文件(.h)中声明消息函数
例:afx_msg void OnAppAbout();//清单 3 2
(3)在类的源文件(.cpp)中加入消息映射表
//消息映射表开始
BEGIN_MESSAGE_MAP(CMyPainterApp, CWinApp)
//{{AFX_MSG_MAP(CMyPainterApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout) //具体映射关系
//}}AFX_MSG_MAP
//Standard file based document commands
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
//Standard print setup command
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP() //消息映射表结束
在 BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP 之间的宏为消息映射表。在映射表中,每个
消息映射都是由一组预定义的宏组成,这些在消息映射内部的宏称为消息映射宏。 映射宏
有许多种,消息映射中使用的消息映射宏由所处理消息的类型决定,依照消息类型分类,相
应表中映射宏大致可分为:标准 Windows 消息映射宏、命令消息映射宏、控件通知消息映射
宏和其他映射宏。见表 3 13。
表3 13 预定义的映射宏

消 息 类 型 映 射 宏 宏 定 义
标准 Windows 消息 ON_WM_XXXX ON_WM_PAINT()
命令消息 ON_COMMAND ON_COMMAND(id,memberFxn)
命令标志范围 ON_COMMAND_RANGE ON_COMMAND_RANGE(id1.id2.,memberFxn)
扩展命令消息 ON_COMMAND_EX ON_COMMAND_EX(id,memberFxn)
扩展命令消息的范围 ON_COMMAND_EX_RANGE ON_COMMAND_EX_RANGE(id1.id2.,memberFxn)
更新命令消息 ON_UPDATE_COMMAND_UI ON_UPDATE_COMMAND_UI(id,memberFxn)
ON_ UPDATE _ COMMAND _ UI _ ON _ UPDATE _ COMMAND _ UI _ RANGE (id1. id2.,
要更新命令的消息范围
RANGE memberFxn)
控件通知消息 ON_XXXX ON_XXXX(wNotifyCode,id,memberFxn)
自定义控件通知消息 ON_CONTRO ON_CONTROL (wNotifyCode, id, memberFxn)
ON_CONTROL_RANGE
控件标志符的范围 ON_CONTROL_RANGE
(wNotifyCode, id1, id2,memberFxn)
用户自定义消息 ON_MESSAGE ON_MESSAGE(message,memberFxn)
ON_REGISTERED_MESSAGE
已注册 Windows 消息 ON_REGISTERED_MESSAGE
(nMessageVariable,memberFxn)
公共控件通知消息 ON_NOTIFY ON_NOTIFY(wNotifyCode,id,memberFxn)
ON_NOTIFY_RANGE
公共控件通知消息范围 ON_NOTIFY_RANGE
(wNotifyCode, id1, id2,memberFxn)
扩展公共控件通知消息 ON_NOTIFY_EX ON_NOTIFY_EX(wNotifyCode,id,memberFxn)
扩展 公 共 控 件 通 知 消 息 ON_NOTIFY_EX_RANGE
ON_NOTIFY_EX_RANGE
78 范围 (wNotifyCode,id1,id2,memberFxn)
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

(4)在类的源文件(.cpp)中编写消息处理函数具体的代码
//具体代码}
例:void CMyPainterApp::OnAppAbout(){ //见清单 3 2


43 消息处理函数
由上可知消息映射主要由消息的 ID 与消息处理函数两部分组成的。
所有消息处理函数(也称消息响应函数,简称消息函数 )原型前面都有关键字 afx_msg
前缀,用于把 消 息 函 数 与 其 他 函 数 区 分 开 来。 依 照 消 息 分 类,可 将 消 息 函 数 分 为:标 准
Windows 消息函数、命令消息函数、控件通知消息函数。
消息函数可以用 ClassWizard 添加也可手工添加。

 使用 犆犾
1 犪狊狊犠犻狕犪狉犱 添加消息处理函数
使用 ClassWizard 添加消息处理函数的步骤如下。
(1)选中 ClassWizard 对话框中的 Message Maps 标签。
(2)在 Project 中选择要操作的项目工程。
(3)在 Class name 下拉列表框中选择类。
(4)在 Object IDs 选择框中选择工程中的对象 ID,并在右边的 Messages 选择框中选择
消息,这时 Add Function 按钮会变黑,处于被激活状态。
(5)单击 Add Function,弹 出 一 个 函 数 生 成 的 确 认 对 话 框 Add Member Function,见
图 3 15。
(6)单击 Add Member Function 对话框中的 OK 按钮后,用 ClassWizard 添加函数的过程
就完成了。

图3 15 添加消息处理函数

注意:在 Add Member Function 对话框中有自动加入的函数名。 VC60 中对于菜单中的


消息响应函数的命名规则是:On+菜单名;对于控件的消息响应函数的命名规则是:On+控件
ID 号。通常情况下不要改动 ClassWizard 自动生成的函数名。 如果改动,可能会导致依靠
ClassWizard 的自动编程不能进行。 79

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

用 ClassWizard 添加消息处理函数后,在头文件和源文件中共有 3 处变化。


① 在类的头文件(.h)中声明消息函数;
② 将消息映射关系添加到类的源文件(.cpp)的消息映射表中;
③ 在类的源文件(.cpp)中给出消息函数的框架。
关于手工添加将在以后章节的例题中加以说明 。

 标准 犠犻
2 狀犱狅狑狊消息函数
对于标准 Windows 消息,CWnd 类中已经预定义了默认的处理函数。 这些函数的名称以
On 开头,以它相应的 Windows 消息中不包括 WM_的部分作为函数名的后半部分。 例如,消息
函数 OnPaint 对应处理的是 WM_PAINT 消息。在 CWnd 的直接或间接派生类中如果定义了某
个标准 Windows 消息函数,则该函数重载基类中相应的消息函数。 主要的 Windows 消息处
理函数见表 3 14。
表3 14 主要的标准 Wi
ndows消息函数

序号 标准 Windows 消息 消 息 函 数 说 明
afx_msg void OnKeyDown
1 WM_KEYDOWN 键按下
(UINT nChar, UINT nRepCnt, UINT nFlags)
afx_msg void OnKeyUp
2 WM_KEYUP 键松开
(UINT nChar, UINT nRepCnt, UINT nFlags)
afx_msg void OnChar
3 WM_CHAR 按键
(UINT nchar,UINT nRepCnt,UINT nFlags)
afx _ msg void OnMouseMove (UINT nFlags, CPoint
4 WM_MOUSEMOVE 鼠标移动
point)
afx_msg void OnLButtonDown
5 WM_LBUTTONDOWN 鼠标左键按下
(UINT nFlags, CPoint point)
afx _ msg void OnLButtonUp (UINT nFlags, CPoint
6 WM_LBUTTONUP 鼠标左键松开
point)
afx_ msg void OnRButtonDown (UINT nFlags, CPoint
7 WM_RBUTTONDOWN 鼠标右键按下
point)
afx _ msg void OnRButtonUp (UINT nFlags, CPoint
8 WM_RBUTTONUP 鼠标右键松开
point)
afx_msg void OnLButtonDblClk
9 WM_LBUTTONDBCLICK 双击鼠标左键
(UINT nFlags, CPoint point)
afx_msg void OnRButtonDblClk
10 WM_RBUTTONDBCLICK 双击鼠标右键
(UINT nFlags, CPoint point)
11 WM_PAINT afx_msg void OnPaint() 窗口重绘
afx_msg void OnHScroll(UINT nSBCode, UINT nPos,
12 WM_HSCROLL 水平滚动栏
CScrollBar* pScrollBar)
afx_msg void OnVScroll(UINT nSBCode, UINT nPos,
13 WM_VSCROLL 垂直滚动栏
CScrollBar* pScrollBar)
14 WM_TIMER afx_msg void OnTimer(UINT nIDEvent) 时钟期结束
15 WM_CREATE afx_msg int OnCreate(
LPCREATESTRUCT lpCreateStruct) 创建窗口
80
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

续 表
序号 标准 Windows 消息 消 息 函 数 说 明
16 WM_CLOSE afx_msg void OnClose() 关闭窗口
17 WM_QUERYENDSSION afx_msg BOOL OnQueryEndSession() 退出 Windows
关 闭 窗 口 后, 子
18 WM_DESTROY afx_msg void OnDestroy()
窗口被销毁前
关 闭 窗 口 后, 子
19 WM_NCDESTROY afx_msg void OnNcDestroy()
窗口被销毁后

表 3 14 中有关函数的具体解释如下。
(1)键盘消息函数
表中 1~3 为 键 盘 消 息 函 数。 其 中 OnChar 消 息 函 数 最 重 要,它 通 常 在 OnKeyDown 和
OnKeyUp 两个函数之间被调用。函数参数 NChar:表示所按键的字符代码值;nRepCnt:表示重
复击键次数;nFlags:表示扫描码、键的先前状态等,其具体含义见表 3 15。
表3 15 OnCh
ar函数中 nF
lag标志位的具体描述
位 说 明
0~15 表示用户重复单击键的次数,即 nPepCnt
16~23 表示扫描码,具体依赖于 OEM 厂商
24 如果是扩展码,则为 1;否则为 0
25~28 由 Windows 内部使用
29 如果 ALT 键被按下为 1;否则为 0
30 表示先前键的状态,如果消息发出前键是按下的,则为 1;否则为 0
31 表示键的转换状态,如果键被松开,则为 1,否则为 0

(2)鼠标消息函数
表中 4~10 为键盘消息函数。函数中的第一个参数 nFlags 用来表示鼠标事件发生时,
键盘上某些按键的状态,nFlag 对应的具体值见表 3 16。
表3 16 鼠标消息函数中 nF
lag取值

nFlag 取值 说 明 nFlag 取值 说 明

MK_CONTROL Ctrl 键按下 MK_RBUTTON 鼠标右键按下

MK_LBUTTON 鼠标左键按下 MK_SHIFT Shift 键按下

MK_MBUTTON 鼠标中键按下

第二个参数 point 用来表示鼠标事件发生时,鼠标光标的位置坐标。一般情况下,这个


坐标是相对于窗口左上角而言的 。
(3)OnPaint()函数
当窗口必须被重新绘制(如窗口最小化后再还原和被其他窗口遮盖后又移开后 )或用户
确实需要重绘窗口时,系统将发送 WM_PAINT 消息。
调用 UpdateWindows 或者 ReDrawWindows 函数时都会向应用程序发送 WM_PAINT 消息。
(4)OnHScroll()和 OnVScroll()函数
函数共有三个参数,其中第一个参数 nSBCode 表示用户滚动请求的滚动条代码,它的具 81

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

体取值见表 3 17。
表3 17 nSBCo
de取值

OnHScroll OnVScroll
nSBCode 取值 说 明 nSBCode 取值 说 明
SB_LEFT 滚动到最左边 SB_BOTTOM 滚动到最底部
SB_ENDSCROLL 终止滚动 SB_ENDSCROLL 终止滚动
SB_LINELEFT 向左滚动 SB_LINEDOWN 向下滚动
SB_LINERIGHT 向右滚动 SB_LINEUP 向上滚动
SB_PAGELEFT 向左滚动一页 SB_PAGEDOWN 向下滚动一页
SB_PAGERIGHT 向右滚动一页 SB_PAGEUP 向上滚动一页
SB_RIGHT 滚动到最右边 SB_TOP 滚动到顶部
SB_THUMBPOSITION 滚动到 nPos 指定的位置 SB_THUMBPOSITION 滚动到 nPos 指定的位置
拖动滚动框到 nPos 拖动滚动框到 nPos
SB_THUMBTRACK SB_THUMBTRACK
指定的位置 指定的位置

第二个参数 nPos 用于指定滚动框的位置,但只有当第一个参数为 SB_THUMBPOSITION 和


SB_THUMBTRACK 时它才有效。
第三个参数 pScrollBar 为指向控件的指针。当用户单击窗口的滚动栏时,这个参数为
NULL。需要注意的是,这个指针只是临时指针,不能被存储下来以备以后使用。
(5)OnTimer()函数
在 Windows 操作系统中,可以使用成员函数 SetTimer 定制一个时钟周期,当每个时钟周
期结束时,SetTimer 函数定期向应用程序发送 WM_TIMER 消息。 OnTimer 函数只有一个参数
nIDEvent,它用于指示计时器的标志符。
可以使用这 个 函 数 来 激 活 某 个 程 序,尤 其 是 当 应 用 程 序 作 为 一 个 任 务 在 后 台 运
行时 。
(6)OnCreate()函数
当应用程序框架调用 Create 或 CreateEx 函数时,WM_CREATE 消息就会被发送,而这时
窗口的创建还未完成,窗口还不可见,因此在 OnCreate 函数内部不能调用那些依赖于窗口
的已经创建的 Windows 函数。
如果该函数返回值为 0,则继续创建 CWnd 对象;如果返回值为-1,窗口就会被销毁 。
(7)OnClose()函数
当用户关闭窗口时,Windows 就会发送 WM_CLOSE 消息,应用程序框架调用 OnClose 函数
来作为 CWnd 或者应用程序终止的信号。
可以通过重载 OnClose 函数来控制关闭程序的过程。 例如,如果需要提醒用户保存修
改后的文件,就可以在 OnClose 函数中完成。
(8)OnQueryEndSession()函数
当 用 户 选 择 退 出 Windows 或 者 应 用 程 序 调 用 ExitWindows 函 数 时 就 会 发
WM_QUERYEDNSESSION消息给所有正在运行的应用程序。 应用程序执行 OnQueryEndSession
函数。如果应用程序不能被顺利关闭,函数就会返回一个零值,这样 Windows 就会不停地调
用 OnQueryEndSession 函数直到应用程序返回一个非零值 。
82 (9)OnDestroy()函数
第三章 Wi ows应用程序编程与 MFC
……………………………………………………………………………………………………………………………………………
nd

Windows 在发送 WM_CLOSE 消息后,紧接着就会发送 WM_DESTROY 消息,而 OnDestroy 函数


就是与其对应的消息函数。
当 CWnd 对象从屏幕中移除后就会调用 OnDestroy 函数,在此函数运行期间可以假设已
经销毁了 CWnd 对象,但所有的子窗口仍然存在。利用这个函数就可以对依赖于当前窗口对
象的所有东西进行清理,例如释放与窗口有关的已经被分配内存的对象 。
(10)OnNcDestroy()函数
WM_DESTROY 消息在子窗口被销毁前就被发送出来了,而 WM_NCDESTROY 消息是在子窗口
被销毁后才被发送出来,通知窗口它的客户区正在被销毁 。
OnNcDestroy 函数是窗口被销毁前最后一个调用的成员函数 。 默认的 OnNcDestroy 函
数用于执行最后的一些清理工作,然后调用虚拟的成员函数 PostNcDestroy 来取消动态申
请的窗口对象。
也可以重载 OnNcDestroy 函数来做一些自己的清理工作,但需要注意,必须在基类中调
用 OnNcDestroy 函数以保证内部分配给窗口的所有内存都能够被释放 。

 控件通知消息函数

控件通知消息一般没有默认的消息函数,其函数名理论上可以随意,但最好遵循一定的
规则,例如以 On 开头。
当使用向导添加消息函数时,系统会提供一个建议的函数名,这个函数名主要是根据控
件通知消息的通知代码来命名的,用户最好采用此函数名。
例如:afx_msg void OnBNClickedOk()表明单击 OK 按钮要处理的消息函数。
afx_msg void OnBnDoubleClickedButtonApply()表明双击 ID 为 ID_BUTTON_APPLY 按钮
要处理的消息函数。

 命令消息函数

命令消息也没有默认的消息函数 。命名原则与上相同。
例如:afx_msg void OnEditCut()表明处理命令标志符为 ID_EDIT_CUT 的命令消息函数。
由于 Windows 下的应用程序大多都有统一的界面风格,有一些命令在大多数应用程序
中都存在,如文件菜单下的新建、保存、退出,编辑菜单下的复制、粘贴、剪切等。 VC++ 已经对
这些常用的命令标志符(ID)进行了预定义,保存在 afxres.h 文件中。


44 消息传递
应用程序的构造过程被执行之后,CWinApp 类的 Run 函数就来检索消息,并把消息发送
到适当的窗口,在 MFC 程序中,非命令消息和命令消息采用两种不同的传递方式 。
(1)非命令消息的传递
标准 Windows 消息和控件通知消息为非命令消息,必须由窗口类的对象,也就是直接或
间接由 CWnd 类或其派生类的对象进行处理。 这个窗口可能是主框架窗口、标准控件、对话
框、视图或其他类型的子窗口。 在程序运行时,每个 Windows 窗口都与窗口对象联系在一
起。每个窗口对象都有自己的消息映射和处理函数 。 MFC 利用消息映射把消息与其对应的
处理函数相匹配。
(2)命令消息的传递 83

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

命令消息的处理与其他消息不同,命令消息可以被更广泛的对象处理,而不仅仅局限于
窗口类,所有派生自 CCmdTArget 的类,如文档类、文档模板类和应用程序类等都可以处理命
令消息。
既然这么多对象都可以处理命令消息,那么当命令消息被发送后,究竟选择谁来处理它
比较好呢? 理论上,使用何种对象处理它都是可以的,但一般情况下,为了增强程序的可维
护性,可以遵循就近原则。例如,如果某个命令是对文档内容进行操作,如复制、粘贴等命令
就可以把它交给文档对象处理;如果某个命令是进行绘图操作,就可以把它交给视图对象处
理;如果某个命令操作关系到程序的流程,如退出程序等,就可以把它交给应用程序类的对
象进行处理。但这种规则也不绝对,需要具体问题具体分析。
另外,还要解决一个问题,如果某个命令在多个对象中都有消息处理函数,那是不是调
用所有的处理函数呢? 显然不是,每次只有一个函数被调用。具体到哪个函数被调用,这就
涉及了命令消息的传递路径,表 3 18 列出了常用命令消息的详细传送路径 。
表3 18 命令消息传递路径

对象类型 消息发送路径 对象类型 消息发送路径

1 活动的 CMDIChildWnd
1 本视
MDI 主框架 2 本框架窗口 视 图
2 与本视图相连的文档
3 应用程序(CWinApp)

1 活动的视
1 文本档
MDI 子框架 2 本框架窗口 文 档
2 文档
3 应用程序

1 活动的视 1 本对话框
SDI 主框架 2 本框架窗口 对话框 2 拥有本对话框的窗口
3 应用程序 3 应用程序

当某个命令消息产生后,它被应用程序框架沿着命令消息的传递路径发送。 传递路径
上的每一个对象都按顺序检查自己的消息映射,看看是否能够处理这个命令消息。 如果消
息与处理函数相匹配,则调用处理函数处理该消息并且不再搜索后面的对象,反之就继续传
递下去。
在多文档的应用程序中,选择“编辑”菜单下的“查找”菜单项,将产生一条命令消息。 假
定其处理函数是应用程序文档模板类的成员函数 。那么消息传递路径如下:
① MDI 主框架窗口对象首先收到这个命令消息 。
② 主框架窗口在搜索自己的消息映射前把命令消息传给当前活动的子窗口 。
③ 当前活动的子窗口在检查自己的消息映射前把命令消息传给当前活动的视 。
④ 视图先搜索自己的消息映射,查找相应的处理函数,没找到,接着按照传递顺序把命
令消息发送给与视图相关联的文档 。
⑤ 关联的文档先搜索自己的消息映射,查找相应的处理函数,没找到,接着按照传递顺
序把命令消息发送给与文档相关联的文档模板 。
⑥ 文档模板找到与命令消息对应的消息处理函数,调用该函数,完成相应的操作并终止
命令消息的传递。

84
第四章 文档/视图结构及其编程

文档用以保存应用程序的数据,并提供与磁盘文件的交互以及与视的交互等。 视图以
可见的图形方式显示文档数据、提供用户界面、接受用户操作信息,并将与视图相关的操作
传递到文档,以及接受文档的更新视操作等。
MFC 的文档/视图结构将数据与用户实际显示和处理数据的方式分开来,图 4 1 显示文
档、视图和用户的关系。

图4 1 文档、视图与用户的关系

默认情况下,MFC 应用程序向导使用文档类与视图类来创建应用程序的主干 。向导创建


的文档类由 CDocument 类派生,视图类由 CView 类派生。 它们具体的名称由应用程序的项
目决定。CDocument 类、CView 类、CFrameWnd 类和 CDocTemplate 类一起构成了文档/视图结
构的核心。本章将结合这几个核心类对文档/视图结构进行较详细的分析。
本章要点:
* 文档、视图和框架窗口
* 文档/视图结构
* 文档/视图编程

1 文档


11 使用文档管理数据的一般步骤
文档用于管理应用程序的数据,在使用文档类时的一般步骤为:
(1)CDocument 是所有文档类的基类,以 CDocument 类为基类派生出自己的文档类;
(2)添加用于保存数据的成员变量;
(3)编写实现读和修改文档数据的成员函数;
(4)重载文档类的成员函数 Serialize,实现将文档数据写入磁盘和从磁盘读出文档数
据的操作。


12 文档类(
CDoc t类)中的主要数据成员和成员函数
umen
(1)文档类的数据成员
文档数据必须用文档类的数据成员来实现。 在定义文档数据成员时,为了设置和获取 85

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

数据项,通常把成员函数添加到文档类中,并在成员函数上执行其他有用操作 。
视图通过指向文档的指针来访问文档对象,可以通过调用 CView::GetDocument 来获取
指向文档的指针。获得指向文档类的指针后,才能访问文档的成员。
(2)成员函数 OnNewDocument()
函数原型为:virtual BOOL OnNewDocument();
当用户选择“文件”菜单的 “新建”命令时,将调用这个函数对文档进行初始化。 如果是
多文档应用程序,将创建一个新文档并调用它完成初始化。 如果是单文档应用程序则调用
这个函数初始化已经存在的文档 。
当用 AppWizard 生成框架时,已定义了 OnNewDocument 的基本结构,见清单 4 2。
(3)成员函数 DeleteContens()
函数原型为:virtual void DeleteContens();
DeleteContens 函数用于在不删除文档对象的情况下清除文档中的数据。 这对于使用
同一文档对象的单文档应用程序特别重要 。
可以重载该函数以便实现 “编辑 ”菜单的 “全部清除 ”命令或类似命令来删除文档中的
数据。
(4)成员函数 OnOpenDocument()
函数原型为:virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
当用户选择“文件”菜单的 “打开”命令时,将调用 OnOpenDocument 来打开指定的文件,
并调用 DeleteContents 来清除文档中的数据,调用 Serialize 来从磁盘文件中加载新文档。
如果需要对文档进行初始化,那么可以重载 OnOpenDocument。
(5)成员函数 OnSaveDocument()
函数原型为:virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
当用户选择“文件”菜单的“保存”或者“另存为”命令时,将调用该函数打开指定文件,并
调用 Serialize 函数向文件中写入文档数据。
(6)成员函数 OnCloseDocument()
函数原型为:virtual void OnCloseDocument();
当一个文档被关闭时,框架 就 会 调 用 这 个 函 数 。 函 数 的 默 认 操 作 是 关 闭 所 有 用 于
浏览文档的框架窗口和视图,清除文档的内容,并调用 DeleteContents 函数删除文档中
的数据 。
(7)成员函数 Serialize()
将对象写入永久性存储媒体(如磁盘文件)或从其中读取对象的过程叫做序列化 。 在文
档类中通常使用 Serialize 函数来完成序列化的操作。 MFC 应用程序向导重写 CDocument
的成员函数 Serialize 的主干部分并将其放入用户创建的文档类中 。 在实现应用程序的成
员变量后,一般需要自己编写 Serialize 函数,在应用程序向导生成的框架中,Serialize 函
数的基本结构见清单 4 2。
(8)成员函数 UpdateAllViews()
函数原型为:void UpdateAllViews (CView* pSender, LPARAM lHint = 0L, CObject*
pHint = NULL);
如果所有的视图都要更新,那么参数 pSender 为 NULL,否则为指向视图的指针;1Hint 包
86 含与修改有关的信息,可以传递任何数据;pHint 指向用于保存修改信息的对象,可以传递
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

CObject 派生的任何对象的指针。
该函数将通知与文档对应的所有视图文档已经被更改了 。
如果在派生类文档类的成员函数中调用 UpdateAllViews 函数,调用形式为:

UpdateAllViews(NULL);

如果在派生视图类的成员函数中调用 UpdateAllViews 函数,调用形式为:

GetDocument()- > UpdateAllViews(this);

了解文档对象和视图对象在程序中是如何工作的是一件很重要的事,因为文档类在
VC++ 程序中控制着数据存储和显示的基本格式 。 文档对象是用来存储数据的,而视图对象
作为主窗口的一个子窗口,通常占据整个客户区,负责相应的数据显示。 在程序中这些主要
对象贯穿着所有函数调用的全过程 。


13 多文档类型
AppWizard 创建的应用程序框架开始只支持单个文档类。 如果应用程序要支持多文档
类型,那么必须创建新的文档类。 每种文档类型都有自己的文档类和视图类。 当用户选择
“文件”菜单的“新建”命令时,应用程序将显示对话框,列出所支持的文档类型,然后再按用
户选择的类型创建文档。每种文档类型都由自己的文档模板对象管理 。
要创 建 新 的 文 档 类, 请 打 开 ClassWizard, 用 ClassWizard 来 创 建 。 创 建 时, 选 择
CDocument 类作为派生新 类 的 基 类 并 提 供 必 要 的 文 档 信 息,然 后 再 实 现 这 个 新 类 的 数
据,最后在应用程 序 对 象 的 成 员 函 数 InitInstance 中 增 加 一 次 对 AddDocTemplate 函 数
的调用 。

2 视图


21 视图操作的一般步骤
视图用于显示文档内容并管理与用户的交互,视图的操作步骤一般为:
(1)以 CView 类为基类派生出自己的视类;
(2)通过派生类的 OnDraw 成员函数向视图提供文档数据;
(3)在派生视类中增加界面设计中涉及的各对象的 Windows 消息处理函数;
(4)编写相应的消息处理函数以响应用户操作;
(5)根据需要修改该派生视类的其他成员函数 。


22 视图类(
CVew 类)中的主要成员函数

(1)成员函数 OnDraw
函数原型为:virtual void OnDraw(CDC* pDC)= 0;
应用程序几乎所有的绘制工作 都 是 在 OnDraw 成 员 函 数 中 实 现 的,编 程 时 必 须 在 视
图类中重载此函数 。 在 AppWizard 生成的框架中,已经定义了 OnDraw 的基本结构,见清
单4 3。
OnDraw 通过调用文档成员函数获取文档数据,并传给 OnDraw 的设备文本对象,通过其 87

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

成员函数来显示数据。
当文档数据被更改时,视图必须重新绘制以反映数据的更改 。 例如,当用户通过视图更
改文档时,视图必须调用文档的成员函数 UpdateAllViews 来更新同一文档的所有视图。
(2)成员函数 GetDocument
函数原型为:CDocument* GetDocument()const;
该函数返回指向与视图相联的文档对象的 指 针,如 果 没 有 文 档 与 视 图 相 联,则 返 回
NULL。调用该函数可以获得一个指向当前视图所对应的文档类指针,利用它能够对文档类
的成员函数和公共数据成员进行访问 。
(3)成员函数 OnUpdate
函数 原 型 为: virtual void OnUpdate (CView * pSender, LPARAM lHint, CObject *
pHint);
调 用 该 成 员 函 数 可 以 使 视 图 反 映 文 档 数 据 的 变 化, 该 函 数 被 文 档 类 中 的
UpdateAllViews 函数所调用。不过用户也可以直接在派生视图类中调用该函数,通常派生
视图类的 OnUpdate 函数要访问文档、读取文档的数据,然后再更新视图的数据成员或控件,
以便反映出文档的变化。缺省情况下,OnUpdate 使整个视图窗口无效,同时触发 OnDraw 函
数,使用更新后的文档数据重新绘制窗口 。
(4)成员函数 OnInitialUpdate
函数原型为:virtual void OnInitialUpdate();
框架窗口在视图第一次与文档相联时调用该函数 。 在基类的 OnInitialUpdate 函数
中,除 了 调 用 OnUpdate 函 数 外, 不 作 其 他 任 何 事 情。 如 果 需 要 在 派 生 视 图 类 中 重 载
OnInitialUpdate 函数则一定要在其中调用基类的 OnInitialUpdate 函数,或者调用派生视
图类的 OnUpdate 函数。通过重载 OnInitialUpdate 函数可以完成视图类的初始化。应用程
序框架在创建视图时,首先调用 OnCreate 函数,然后调用 OnInitialUpdate 函数。 OnCreate
函数只能够被调用一次,而 OnInitialUpdate 函数可以被调用多次。


23 多视图
多数文档只要求单视图,但支持单个文档的多个视图是可能的。 每个文档对象保存有
该文档的视图列表,并提供用于添加和删除视图的成员函数,以及在文档数据发生变化时提
供 UpdateAllView 成员函数来更新所有视图。MFC 支持以下三种多视图模式:
(1)同一文档的多个视图对象,每个对象置于独立的 MDI 子窗口中。 例如,选择 “窗口”
菜单的“新建窗口”命令打开同一文档的多个 MDI 子窗口,然后可以在多个 MDI 子窗口中同
时查看文档的不同部分。
(2)同一文档边框窗口中有同一文档的多个视图对象 。例如,分割窗口可以将单个文档
边框窗口的视图空间分割成多个独立的视图,应用程序从同一视图类中创建多个视图对象 。
(3)单个边框窗口中有不同类的视图对象,多个视图共享单个边框窗口,每个视图从不
同类构造。例如,可以在一个视图显示文本文档,而在另一视图显示图形文档。


24 派生的视图类
为了增强应用 程 序 的 视 图 功 能,MFC 还 提 供 了 几 个 专 用 的 视 图 类,这 些 类 都 派 生 于
88 CView 类,这些派生的视图类见表 4 1。
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

表4 1 CV
iew 的几个重要派生类

派生视类 说 明
CScrollView 为视图提供滚动和缩放显示功能
CFormView 提供可滚动的视图来显示由对话框控件组成的表单
CRecordView 在控件中显示数据库表中字段的表单视图
CDaoRecordView 在控件中显示数据库表中字段的表单视图
CCtrlView 控件视图类

CEditView CCtrlView 的派生类,封装了编辑控件的功能,提供了一个简单的文本编辑器的视图类

CCtrlView 的派生类,封装了 RichEdit 控件,支持多种字体、颜色、OLE 的高级编辑器的


CRichEditView
视图类
CListView CCtrlView 的派生类,封装了列表控件的功能,管理项目列表的视图类
CTreeView CCtrlView 的派生类,封装了树形控件功能的视图类

3 框架 (边框窗口 )类

每个应用程序都有一个主边框窗口,主边框窗口标题栏中显示有应用程序的名称。 每
个文档都有一个文档边框窗口,一个文档边框窗口至少含有一个视图用于显示文档的内容 。
对于每个 SDI 应用程序,都有一个从 CFrameWnd 类派生的边框窗口,该窗口既是主边框
窗口,又是文档边框窗口。
对于每个 MDI 应用程序,包含有主边框窗口和子边框窗口 。MDI 主边框窗口中含有一个
称为 MDICLIENT 客户区窗口,该窗口管理主边框窗口的客户区并有自己的子窗 (文档边框窗
口)。图 4 2 为 MDI 主边框窗口、文档边框窗口 (子边框窗口 )和视图的关系。 利用 MFC

图4 2 MDI主边框窗口,文档边框窗口(子边框窗口)与视图的关系

89

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

AppWizard 生成的应用程序建立了主边框窗 口 CMainFrame 类和子边框窗口 CChildFrame


类。CMainFrame 类是从 CMDIFrameWnd 类派生的, CChildFrame 类是从 CMDIChildWnd 类派生
的。

4 文档模板类

为了管理创建文档及其关联的视图和框架窗口的复杂过程,框架使用了两个文档模板
类: 用 于 单 文 档 应 用 程 序 的 CSingleDocTemplate 和 用 于 多 文 档 应 用 程 序 的
CMultiDocTemplate,它们都是 CDocTemplate 的直接派生类。
在文档/视图结构中,实质上由应用程序对象创建文档模板,由文档模板创建文档和框
架窗口,而由框架窗口创建文档对应的视图 。应用程序类中 InitInstance 函数的一个主要
任务就是构造一个或多个适合的文档模板 。
下面的代码(见清单 3 3)阐述了在 InitInstance 函数中创建 CMultiDocTemplate 的过
程。

CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_PAINTETYPE,
RUNTIME_CLASS(CMyPainterDoc),
RUNTIME_CLASS(CChildFrame),//custom MDI child frame
RUNTIME_CLASS(CMyPainterView));
AddDocTemplate(pDocTemplate);

首 先 声 明 一 个 指 向 CMultiDocTemplate 类 的 指 针, 并 使 用 new 操 作 符 和
CMultiDocTemplate 类的构造函数对其初始化。 构造函数的参数包括应用程序主框架的标
志符以及三个 CRuntimeClass 参数,这三个参数用于指定窗口框架类、文档类和视图类,它
们由文档模板在响应 “文件”菜单上 “新建”或多文档窗口菜单的 “新建窗口 ”等命令时动态
创建。
另外,为 了 使 RUNTIME _ CLASS 机 制 正 常 工 作, 在 派 生 的 框 架 窗 口 类 中 必 须 使 用
DECLARE_DYNCREATE 宏声明,这是因为框架需要使用 CObject 类的动态构造机制创建文档框
架窗口。

5 文档/视图结构各对象之间的关系

文档/视图结构各对象之间的关系见表 4 2。 文档保留该文档的视图列表以及指向创
建该文档的文档模板的指针;视图保留指向其文档的指针,视图窗口是文档框架窗口的子窗
口;文档框架窗口中保留指向活动视图的指针;文档模板保留其已打开文档的列表;Windows
跟踪所有打开的窗口,以便可以向这些窗口发送消息。 所有这些关系都是在文档/视图结构
的创建期间建立的,通过调用全局函数 AfxGetApp,任何对象均可以获得指向应用程序对象
的指针。

90
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

表4 2 文档/视图结构各对象之间的关系

对 象 访问其他对象的方法
使 用 GetFirstViewPosition 和 GetNextView 访 问 文 档 的 视 图 列 表; 调 用
文档
GetDocTemplate 获取文档模板。文档更改后利用 UpDateAllViews 通知所有视图
视图 调用 GetDocument 获得指向文档的指针,调用 GetParentFrame 获取框架窗口
文档框架窗口 调用 GetActiveView 获取当前视图;调用 GetActiveDocument 获取当前视图文档
MDI 框架窗口 调用 MDIGetActive 获取当前活动的 CMDIChildWnd

6 文档/视图结构编程实例


61 单文档应用程序实例
【例4 1】 Ex04_1 创建一单文档应用程序,并显示字符串,具体说明见表 4 3。

表4 3 例4 1具体说明

项 目 图示和说明

程序界面

程序功能 单文档应用程序,在界面中显示:Hello VC++ !This is my first SDI program.

(1)启动 VC 60
(2)从 File 菜单中选择 New,打开 New 对话框
(3)选择 MFC AppWizard (exe)
(4)在 Project name 文本框中键入要创建的工程名(Ex04_1)
(5)单击 Location 文本框右边的“...”按钮,输入项目的保存路径
(6)单击 OK 按钮,进入 AppWizard Step 1 of 6
选中 Single documents(单文档)单选按钮,资源语言为中文(中国)(APPWZCHS.DLL)。
(7)AppWizard 的第 2 步~第 6 步,都为缺省选择
(8)单击 AppWizard Step 6 of 6 的 Finish 按钮
(9)单击 New Project Information 对话框中的 OK 按钮
(10)编译和运行程序
得到表 4 3 的应用程序界面,但界面上不显示字符串 Hello VC++ !This is my first
SDI program.
(11)显示字符串
在 Ex04_1View.cpp 文件的 OnDraw()函数中增添显示字符串的代码,见清单 4 1。 91

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单4 1 OnDr 04_


aw 函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx 04_1View OnDraw( CDC pDC)
{ CEx04_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
//TODO: add draw code for native data here
pDC->Te xtOut(50,50,"He l
loVC++Th isismyf
irs
tSDIp
ror
gam. //在(50,50)的位置显示字
" );
//符串

(12)再次编译和运行程序


62 多文档应用程序实例
【例4 2】 Ex04_2 创建一多文档应用程序,显示字符串,具体说明见表 4 4。
表4 4 例4 2具体说明

项 目 图示和说明

程序界面

(1) 点击菜单:文件- >新建,即可新建一文档窗口,并且新建的文档窗口中,显示


字符串“Hello,Visual C++ !”
程序功能
(2) 点击菜单:文件- >保存,弹出保存为对话框,输入要保存的文件名,即可保存
文档中的字符串到磁盘

(1)新建一多文档应用程序 Ex04_2(按照第 3 章程序 MyPainter 的步骤)


(2)在文档类中声明一个私有成员变量和一个公有成员函数
添加成员变量的步骤如下:
① 选择 ClassView 标签,将鼠标放在 CEx04_2Doc
类上,单击鼠标右键,弹出如图 4 3 所示的右键菜单。
② 选择 Add Member Variable,弹出如图 4 4 所示
的对话框,在 Variable Type(变量类型 )编辑框中输入
CString,在 Variable Name (变量名称 )编辑框中输入
string,选中 Private 单选按钮。
添加成员函数的步骤如下:
在图 4 3 中选择 Add Member Function 选项,弹出
92 如图 4 5 所示的对话框,在 Function Type(函数值类 图4 3 右键菜单(鼠标放在类上)
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

型)编辑框中输入 CString,在 Function Declaration(函数声名 )编辑框中输入 GetString,


选中 Public 单选按钮。

图4 4 AddMemb
erVa
ria
ble对话框 图4 5 AddMemb
erFun
cti
on对话框

按上述方法添加完成员变量和成员函数后,在 CEx04_2 Doc.h 头文件中将自动添加成员


变量和成员函数的声明:
在 CEx04_2 Doc.cpp 源文件中将自动添加成员函数的框架 。

CS
tri
n 04_
gCEx 2Do
c
Get
Str
in //自动添加成员函数框架
g()
{ }

(3)在 Ex04_2Doc.cpp 文件中,完成以下 3 个工作


① 在 OnNewDocument()函数中初始化 String
② 写 GetString()函数
③ 在 Serialize(CArchive& ar)函数中完成串行化
以上 3 个函数的代码见清单 4 2。

04_
清单4 2 3个函数的源代码(在 Ex 2Do
c.pp文件中)

BOOLCEx 04_ 2Do cOnNewDo cume nt()


{ if (!CDocument::OnNewDocument())
return FALSE;
//TODO: add reinitialization code here
//(SDI documents will reuse this document)
string =" Hello,Visual C++ !" ;//赋初值
return TRUE;

CSt
rin
gCEx 04_2Do c
Get
Str
in //自动添加成员函数框架
g()
{ return string;


oidCEx04_ 2DocSer
ial
ize(
CAr
chi
v r)
e&a
{ if (ar.IsStoring())
{ ar<<string;//向磁盘写数据}
else
 ar>>string;//从磁盘读数据}

(4)在视图类实现字符串的显示,重载 OnDraw()函数(见清单 4 3)

93

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单4 3 OnDr 04_


aw 函数(在 Ex 2Vi
ew.
cpp文件中)

vo
idCEx 04_2V i
ew OnDr aw(CDC pDC)
{ CEx04_2Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(100,50,pDo c->Ge
tStr
in //在 x=100
g()); y=50的位置输出字符串

(5)编译和运行程序


63 多视图应用程序实例
【例4 3】 Ex04_3 创建一个具有多个视图的单文档应用程序,具体说明见表 4 5。
表4 5 例4 3具体说明

项 目 图示和说明

程序界面

(1)打开应用程序即显示上述界面,界面有 4 个视图
(2)在编辑控件中改变 x0、y0、x1 和 y1 的取值,单击确定按钮,椭圆位置和大
程序功能
小将随之改变
(3)选择文件- >保存即可实现实现图形数据的保存

(1)新建一单文档应用程序 Ex04_3
在 AppWizard 的 step 4 of 6 对话框中,单击 Advanced 按钮,选 Advanced Options 对话
框中的 Window styles 标签,选中 Use split window 复选框。见图 4 6。其余步骤按缺省选
择即可。
编译、链接和运行程序得到图 4 7 的界面。
(2)为工程添加 CEx04_FormView 类
选择菜单 Insert- > New Form,弹出图 4 8 所示的对话框。在 Name 编辑框中输入 CEx04
_FormView,在 Base Classes 下拉组合框中选择 CFormView。 这样将一个表单视图插入到项
目中。如果编译运行程序,会出现如图 4 9 所示的对话框,以确定程序采用哪个视图。

94
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

图4 6 选中 Us
esl
pitw
ind
ow 图4 7 分割成4个视图的程序界面

图4 8 插入表单视图(
FormV
iew) 图4 9 选择视图

(3)取消 CEx04_FormView 对象的创建


打开 Ex04_3.cpp 文件,看到 InitInstance 函数,注释第一个文档模板对象 (FormView)。
见清单 4 4。
清单4 4 I
nit
Ins
tan
c 04_
e函数(在 Ex 3.pp文件中)

BOOLCEx 04_3App Ini
tInstance()
{...
//注释第一个文档模板对象
/* CSingleDocTemplate* pNewDocTemplate = new CSingleDocTemplate(
IDR_EX04_FORMVIEW_TMPL,
RUNTIME_CLASS(CEx04_3Doc), //document class
RUNTIME_CLASS(CMainFrame), //frame class
RUNTIME_CLASS(CEx04_FormView)); //view class
AddDocTemplate(pNewDocTemplate);* /
//下面的文档模板对象保留
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME, 95

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

RUNTIME_CLASS(CEx04_3Doc),
RUNTIME_CLASS(CMainFrame), //main SDI frame window
RUNTIME_CLASS(CEx04_3View));
AddDocTemplate(pDocTemplate);

(4)将视图与切分窗口对应起来
打开 MainFrm.cpp 文件,重载 OnCreateClient 函数,见清单 4 5。
清单4 5 重载 OnCr
eat
eCl
iet函数(在 Ma
n inF
rm.
cpp文件中)

BOOLCMa inFr
ame OnCr e
ateCli
ent(LPCREATESTRUCT/l pcs/CCr eateContext pCo n
text)
{ //注释原有代码
/* return m_wndSplitter.Create(this,
2, 2, //TODO: adjust the number of rows, columns
CSize(10, 10), //TODO: adjust the minimum pane size
pContext);* /
//添加以下代码
VERIFY(m_wndSplitter.CreateStatic (this,2, 2));
VERIFY(m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CEx04_3View),CSize(400,200),pContext));
VERIFY(m_wndSplitter.CreateView(1,0,RUNTIME_CLASS(CEx04_3View),CSize(400,200),pContext));
VERIFY(m_wndSplitter.CreateView(0,1,RUNTIME_CLASS(CEx04_3View),CSize(400,200),pContext));
VERIFY(m_wndSplitter.CreateView(1,1,RUNTIME_CLASS(CEx04_FormView),CSize(400,200),pContext));
return true;

并在 MainFrm.cpp 文件加入三个头文件;

# include" Ex04_3Doc.h"
# include" Ex04_3View.h"
# include" Ex04_FormView.h"

(5)编译和运行程序
得到 4 个视图的界面。3 个视图为 CEx04_3View,1 个视图为 CEx04_FormView。 但界面
上没有任何显示的内容。
(6)在文档类中声明成员变量和初始化成员变量(见清单 4 6 和清单 4 7)

04_
清单4 6 声明成员变量(在 Ex 3Do
c.h文件中)


las
sCEx 04_ 3Docpub
licCDo
cume
nt

public:
int x0;
int y0;
int x1;
int y1;

96
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

04_
清单4 7 初始化成员变量(在 Ex 3Do
c.pp文件中)

04_
CEx 3Do c 04_
CEx 3Do //构造函数
c()
{ x0 = 10;
y0 = 10;
x1 = 100;
y1 = 50;

(7)读写磁盘,保存数据(见清单 4 8)
清单4 8 S
eri
ali
z 04_
e函数(在 Ex 3Do
c.pp文件中)


oidCEx04_ 3DocSer
ial
ize(
CAr
chi
v r)
e&a
{ if (ar.IsStoring())
{ ar<<x0<<y0<<x1<<y1;

else
 ar>>x0>>y0>>x1>>y1;

(8)绘图和显示图形,重载 OnDraw()函数(见清单 4 9)
清单4 9 OnDr 04_
aw 函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx04_3V i
ew OnDr aw(CDC pDC)
{ CEx04_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC ->Ell
ipse(pDoc->x 0pDo c->y0 c->x
pDo 1 c->y
pDo //绘椭圆
1);

(9)编译和运行程序
得到在 3 个 CEx04_View 类视图中显示绘制的椭圆,但 CEx04_FormView 类视图上没有任
何显示的内容。
(10)编辑 CEx04_FormView 视图类对应的窗体资源
本例添加 4 个静态文本,4 个编辑框和 1 个下压按钮。
(11)添加与控件关联的成员变量和消息响应函数(见表 4 6)
表4 6 控件ID 号、成员变量和消息函数

控 件 标题 ID 号 变量名 类型 消息相应函数
静态文本 X0:
静态文本 Y0:
静态文本 X1:
静态文本 Y1:
编辑控件 IDC_X0 m_x0 int
编辑控件 IDC_Y0 m_y0 int
编辑控件 IDC_X1 m_x1 int
编辑控件 IDC_Y1 m_y1 int
下压按钮 确定 IDC_BUTTON_OK OnButtonOk 97

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(12)在文档和视图间建立数据通讯
① 在 Ex04_FormView.h 文件开头添加
# include "Ex04_3Doc.h"
② 在 Ex04_FormView.h 文件的类中声明 GetDocument 函数
Public:
CEx04_3Doc* GetDocument();
③ 在 Ex04_FormView.cpp 文件中,编写 GetDocument 函数,获取指向文档的指针,见清
单 4 10。
清单4 10 Ge
tDo
cume
n 04_
t函数(在 Ex FormV
iew.
cpp文件中)

04_
CEx 3Do c CEx 04_FormV iewGetDo c
ument()
{ ASSERT(m_pDocument- > IsKindOf(RUNTIME_CLASS(CEx04_3Doc)));
return (CEx04_3Doc* )m_pDocument;

④ 在 Ex04_FormView.cpp 文件中,添加和重载 OnInitialUpdate 函数,见清单 4 11。


清单4 11 On
Ini
ti
alUpd
at 04_
e函数(在 Ex FormV
iew.
cpp文件中)


oidCEx 04_ FormV i
ewOnIni
tialUpdae()

{ CFormView::OnInitialUpdate();
CEx04_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
m_x0 = pDoc- >x0;
m_y0 = pDoc- >y0;
m_x1 = pDoc- >x1;
m_y1 = pDoc- >y1;
UpdateData(false);

⑤ 在 Ex04_FormView.cpp 文件中,编写单击按钮的消息函数 OnButtonOk,见清单4 12。


清单4 12 OnBu
tto 04_Mu
nOk函数(在 Ex lti
View.
cpp文件中)


oidCEx 04_ FormV i
ewOnButtonOk()
{ CEx04_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
UpdateData(true);
pDoc- >x0 = m_x0;
pDoc- >y0 = m_y0;
pDoc- >x1 = m_x1;
pDoc- >y1 = m_y1;
pDoc- >UpdateAllViews(this);

(13)编译和运行


64 文档与视图结构之间相互作用关系分析实例
98 例 4 3 中文档与视图结构之间相互作用的关系分析如下 。
第四章 文档/视图结构及其编程
……………………………………………………………………………………………………………………………………………

(1)应用程序启动
·CEx04_3Doc 对象被创建
·CEx04_3View 对象被创建
·CEx04_FormView 对象被创建
·CEx04_3View::OnCreate 函数被调用
·CEx04_3View::OnInitialUpdate 函数被调用
·调用 CEx04_3View::OnUpdate 函数
·初始化视图
(2)用户执行视图命令
·CEx04_FormView 的 OnButtonOk()函数更新 CEx04_3Doc 数据成员
·调用 CDocument::UpdateAllViews

爛其他视图的 OnUpdate()函数被调用
(3)用户执行文档命令
·CEx04_3Doc 数据成员进行更新
·调用 CDocument::UpdateAllViews()
·调用 CEx04_3View::OnUpdate()
·其他视图的 OnUpdate()函数被调用
(4)用户退出应用程序
·视图对象被删除
·CEx04_3Doc 对象被删除

99
第五章 程序界面设计

应用程序的界面对用户的影响很大 ———无论程序代码如何高效、功能如何强大,如果用
户发现它太难于使用,那么这个程序就不会得到广泛的应用 。 菜单、工具栏为应用程序提供
了传递用户命令的界面,而状态栏提供了提示信息的输出区域 。 菜单、工具栏和状态栏共同
组成了 Windows 应用程序的友好界面。
本章要点:
* 菜单资源编辑器的使用
* 在应用程序中使用菜单的方法
* 工具栏资源编辑器的使用
* 在应用程序中使用工具栏的方法
* 在应用程序中使用状态栏的方法

1 界面设计原则

用户界面设计的一个总的原则就是用户至上 。 Windows 操作系统的一个重要优势在于
它为所有应用程序提供了相同的界面。 这样,一个有经验的用户就能很快掌握原先并没有
使用过的 Windows 应用程序。 菜单就是个很好的例子:大多数 Windows 应用程序遵循 “文
件”菜单在最左边,“编辑”、“工具”菜单紧接其后,“帮助”菜单在最右边的规则。 此外菜单命
令的位置也很重要。例如,用户希望在“编辑”菜单下找到“拷贝”、“剪切”和“粘贴”命令。 因
此,除非有充分的理由否则不应该改变公认的 Windows 界面规则。


11 界面布局原则
应用程序的界面布局不仅影响其外观,而且对其本身的易用性也有着举足轻重的作用 。
界面布局包括控件的位置、元素之间的协调性、空间的使用以及设计的简单性等等 。
(1)控件位置
在大多数界面设计中,并非所有的组成元素都具有同等的重要性,因此必须保证常用的
重要元素处于最明显的位置。
绝大多数语言都具有从左到右和从上到下的书写顺序 。 因此当用户观看屏幕时,会从
左上角开始,最重要的元素应该放置在那里。例如,如果要求在屏幕上显示包括消费者信息
的表单,则应该首先显示姓名字段。而诸如“确定”或“下一步”之类的按钮,则应该在屏幕的
左下方显示。这是因为,通常用户只是在完成了整个表单后,才会单击这些按钮。
将元素和控件分组也非常重要。 将信息功能相近或关联的控件分组排列,要比分散排
100 列效果好。分组排列大多是通过组框等控件完成的 。
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

(2)协调性
用户界面必须保证其协调性。 协调的外观将使应用程序看起来很舒服。 相反,缺乏协
调性,会使应用程序看起来很混乱,从而使用户低估其功能和稳定性 。
为了可视协调性,需要在开发前有一些大概的规划。 例如:控件的种类、尺寸以及字体
等等。这时先做一个模型将会有助于后续的开发 。
由于VC++ 提供了多种类型的控件,这很容易导致将各种类型的控件都加以使用的想法 。
在开发中必须注意避免出现此类想法,而应该仔细选择最适合应用程序的一些控件。 例如
列表框、组合框、列表视图控件和树视图控件都能用于显示一系列信息,但应该尽可能地选
择单一风格的控件。
此外,还需要注意所选用的控件是否合适。 例如将编辑框设置为只读用来显示文本不
如使用静态文本控件更合适。
(3)一致性
抽象地说一致性就是某个对象的可视化线索,如在 Windows 应用程序中,“打开”工具栏
按钮图标就是用于打开文件夹的;通俗地讲,一致性就是看到其外观就能猜到其功能 。
用户界面也需要使用一致性。例如,应用程序中的具有三维效果的按钮表示它们可以
被按下;那么同时又存在具有平坦风格的按钮,程序就失去了一致性。 在应用程序中保持一
致性是压倒一切的。
(4)空间的使用
应用程序中空间的使用也很重要,它有助于改善程序的外观和对某些元素的强调。 当
然空间并非一定要使用。然而过多的控件拥挤在一起,会增加寻找的时间而降低运行效率。
在设计时,需要综合考虑以确定最佳的空间分布 。
使控件间距一致,以及控件垂直和水平对齐也会提高应用程序的易用性 。
(5)简单性
界面设计中最重要的准则可能就是简单性了 。简单即美,这也是艺术中的一个准则。
测试应用程序简单性的最好方法就是实际使用该程序 。 如果在没有帮助的情况下,普
通用户不能很快完成某个操作,那么就需要考虑重新设计了。
(6)颜色
颜色的效果是千差万别的,用户的喜好也会因人而异。在设计应用程序时,需要注意不同文
化中颜色的差异。一般来说,在颜色的使用上应该持保守的态度,尽量选择软色调和中性颜色。
当然,对颜色的选择也需要考虑用户的情况 。例如,在设计儿童软件时,亮红色、绿色和
黄色是很好的选择而对于金融或银行应用程序,这些颜色会产生负面影响。 使用少量的亮
色能够有效地突出重要区域。作为原则,在应用程序中应该限制颜色的数量,而且应该保持
前后的一致性;应该尽可能使用标准 16 色调色板,这样会扩大应用程序的兼容性;此外,还
要考虑色盲用户。
(7)图片和图标
在应用程序中使用图片和图标,能提高应用程序的吸引力。 图片所传达的信息有时是
文字所远远不能及的,同样,其致命的弱点在于不同人的理解可能也会不同 。
工具栏按钮上的不同图标,形象地表达了不同的功能。 但是当用户不能确定某个图标
的功能时,就会产生反作用。 在设计工具栏图标时,应该参考其他应用程序以得到设计的标
准。例如绝大多数 Windows 应用程序,会将打开文件夹的图像作为 “打开”命令工具栏按钮图 101

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

标。如果你的应用程序违反了标准,就会给用户带来混乱。此外,还需要考虑文化的差异。
(8)字体
字体是用户界面的重要组成部分,在选择字体时,必须保证它能在各种不同分辨率的显
示器上都有良好的可读性。
除非计划将应用程序与字体一同发行,否则最好使用标准 Windows 字体,例如 Arial、
New Times Roman 或 System 字体。如果用户的系统不包括指定的字体,则系统将使用其他字
体,这就有可能造成实际的显示与预期不一致 。如果应用程序将在世界范围内发布,则需要
为每种语言选择合适的字体。此外还需要考虑字体所占的空间,例如中文比英文所占的空
间大 50%。为了保持一致性,类似功能的文本要使用相同的字体 。


12 用户帮助模型
帮助是应用程序的重要组成部分,它通常是用户寻找问题答案的首选。 用户帮助模型
的设计应该在开发前进行。模型的内容则与应用程序的复杂程度以及预期的用户有关 。
在创建帮助的主题和索引时,一定要从用户的角度出发。 例如, “应该如何格式化页
面?”要比“编辑、页面格式菜单”更容易定位问题。 此外,一定要保证上下文敏感性,也就是
说当用户选择了“格式”然后按下 F1 键,那么应该出现的是针对这个词的帮助,而不是出现
帮助主题。印刷或电子的帮助手册是另一种非常有用的工具,它们能提供在简捷帮助主题
中难于传递的信息。此外,工具提示、状态栏等也能够为用户提供很大的方便 。

2 菜单

菜单是 Windows 中的重要资源,它是一系列选项的列表,可以选中选项并产生一条消息
传递给窗口。根据菜单的外观和使用不同可分为:标准菜单、带有图标的菜单、右键菜单和
动态菜单等。


21 菜单资源编辑器
菜单的编辑是在菜单编辑器中完成的。 使用菜单资源编辑器,可以完成创建和编辑菜
单项和菜单命令。单击工程工作区的 Resource View 标签,进入资源视图,然后按照下面的
步骤进行菜单编辑:
(1)打开菜单资源编辑器:在 ResourceView 标签中单击 Menu 项左边的 “+ ”,然后双击
IDR_MYPAINTETYPE 打开菜单资源编辑器,见图 5 1。

102 图5 1 菜单资源编辑器 图5 2 Me
nuI
temPr
ope
rti
es对话框
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

(2)编辑菜单:选择菜单条上的新条目框上的一个空的矩形,单击右键弹出快捷菜单选
择 Properties,弹出如图 5 2 所示的对话框。在属性对话框的 Caption 文本框中输入菜单
标题如“菜单测试”,同时可以看到输入的文字菜单条的菜单项中 。 输入菜单中的名字时,在
一个符号前面加上一个“&”符号定义菜单加速键。
(3)填写菜单项的属性页:菜单项属性页中的各项属性说明见表 5 1。
表5 1 Me
nuI
temPr
ope
rti
es对话框中各属性说明

属性名称 说 明
Separator 复选框 分隔线,它的作用是把菜单中的选项分开以便查看。如果选择了此特性,则表示
加上的菜单项为一条分隔线。令此属性选择为空
弹出式菜单,如果选择了此选项,表示创建的这个菜单项为一个弹出式菜单的根
Popup 复选框
条目
非激活状态,如果下面的 Grayed 属性被选中,则此选项也同时被选中。如果单选
Inactive 复选框 此选项,而没有选择 Grayed 属性选项,则表示此菜单项初始化为非激活状态,即
它对鼠标的单击不响应。令此选项为空
选中标志,如果选中此属性,则表示创建的菜单项在初始化时为选中状态。令此
Checked 复选框
属性选择为空
加灰选项,如果选择了此选项,则表示创建的菜单项初始化为灰色非激活状态。
Grayed 复选框
令此属性选择为空
菜单项居右,如果选择了此选项,则表示创建的菜单项在运行时位于菜单的最右
help 复选框
边。令此属性选择为空
菜单项资源号,如果创建的菜单项不是一个弹出式菜单,就必须填写此菜单项资
ID 编辑 源 ID 号,由于前面选择了 Popup 选项,因此创建的菜单项是一个弹出式菜单项,
此文本输入会变灰,禁止输入
菜单项的标题。创建菜单项时必须为此菜单项输入一个名字,即为菜单项的
Caption 编辑框
标题
None 此选择为缺省值,表示不选中此属性
对于静态的菜单条目,这个值把创建的条目放置在一个新
的行上。对于弹出式菜中,这个值把创建的条目放置在一
此 属 性 Cloumn
个新的栏上。设置这个属性只是在应用程序运行时影响菜
Break 下拉列表框 有 三 个 单外观,这种外观的影响并不在菜单编辑器中显示出来
可选值
对于弹出式菜单,这个值使用一条垂直线,把新的片和旧的
Bar 栏分开。设置这个属性只是在应用程序运行时影响菜单的
外观,这种外观的影响并不在菜单编辑器中显示出来

提示性文本。这个属性包含了可以出现在状态条上的文本。当菜单项成为焦点
时,在此属性框里输入的文本会出现在状态栏上。如果输入的文本中有“ \n”则
Prompt 编辑框 “
\ n”后的文本将出现在工具提示中,而之前的文本将出现在状态栏中。这个属
性只在使用了 MFC 类库支持时起作用。由于前面选择了 Popup 选项,因此创建
的菜单项是一个弹出式菜单项,此文本输入框会变灰,禁止输入


2 u类
2 CMen
CMenu(菜单类)是 CObject 类的直接派生类,用于管理应用程序窗口中的菜单。 该类的
主要成员函数见表 5 2。 103

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表5 2 CMe
nu类的主要成员函数

成 员 函 数 说 明
BOOL AppendMenu() 将一新菜单项增加到一个菜单尾部
BOOL Attach() 将一已存在的 Windows 菜单连接到一个 CMenu 对象
BOOL CreatMenu() 创建一菜单,并将该菜单连接到 CMenu 对象
BOOL CreatPopupMenu() 创建一弹出式菜单
HMENU Detach() 将一菜单与 CMenu 对象断开连接
BOOL DeleteMenu() 从菜单中删除一个菜单项
BOOL DestroyMenu() 销毁菜单及所有 Windows 资源
UINT GetMenuItemCount() 获得菜单所包含的项数
int GetMenuString() 拷贝指定菜单项标签到缓冲区
CMenu* GetSubMenu() 获得指定弹出式菜单的指针
BOOL InsertMenu() 在指定位置插入一个新菜单项
BOOL LoadMenu() 加载一个菜单资源
BOOL SetMenuItemBitmaps() 将指定位图与一菜单项相关联
BOOL TrackPopupMenu() 在指定位置处显示一个浮动的弹出式菜单


2 I类
3 CCmdU
该类没有基类,仅用于 CCmdTarget 派生类的 ON_UPDATE_COMMAND_UI 处理函数中。在应用
程序运行中,经常需要根据应用程序当前的状态对菜单项进行改变。例如对于 Edit|Copy 菜单
项,如果没有内容被选的话,该菜单项就呈灰色被禁止使用。这就是说,需要使用状态与菜单
项保持同步,在 MFC 类库中采用了如下方法:每当弹出式菜单第一次被显示时,都会发送特殊的
更新命令 UI 消息,该消息通常被传递给与菜单项相联系的对象。更新命令 UI 消息的控制函数
以一个 CCmdUI 对象作为其参数,而该 CCmdUI 对象中包含了一个指向相应菜单项的指针,于是
控制函数就可以利用该指针对菜单项进行修改。更新命令 UI 消息只适用于弹出式菜单的菜
单项和工具栏按钮,而对于始终显示的顶层菜单项则不适用。例如,不能利用更新命令 UI 消息
来禁止 Edit 菜单项。CCmdUI 类的数据成员及常用成员函数见表 5 3。
表5 3 CCmdUI类的数据成员及常用成员函数

数据成员及成员函数 说 明
m_nID 该数据成员中存储了用户接口对象的 ID
m_nIndex 该数据成员中存储了用户接口对象的索引
该数据成员为指向 CCmdUI 所代表的菜单的指针。如果该参数为 NULL,则表示
m_pMenu
该对象不是菜单
该数据成员为指向 CCmdUI 所代表的菜单中包含的第一个菜单项的指针。如
m_pSubMenu
果该参数为 NULL,则表示该对象不是菜单
m_pOther 该数据成员为发送通告消息的 Windows 对象
void Enable(
) 允许或禁止用户接口项,如菜单、
工具栏按钮等
void SetCheck(
) 设置用户接口项的检查状态
void SetRaido(
) 设置单选组的选择状态
void SetText(
) 设置用户接口项的文本
104 void ContinueRouting(
) 通知消息循环机制继续运行
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………


24 标准菜单编程实例
以例 5 1 说明标准菜单的编辑、 菜单更新与菜单消息函数等有关内容。
例5 1】 Ex05_1 标准菜单的编辑、
【 消息响应与菜单更新,具体说明见表 5 4。
表5 4 例5 1具体说明

项 目 图示和说明

程序界面

图(
a)菜单 图(
b)消息对话框 图(
c)消息对话框 图(
d)更新菜单

( 消息对话框 1”
1)点击图(a)菜单“ 和“消息对话框 2”
分别弹出如图( 和(
b) 所示的消息对话框。
c)
程序功能 (2)点击图(a)菜单“连接”菜单出现如图(d)所示的菜单,“连接”菜单无效,且“连接”菜单前
有打勾标记。点击“断开”菜单则出现“断开”菜单无效,且“断开”菜单前有打勾标记

(1)新建一单文档应用程序 Ex05_1
(2)编辑菜单
① 创建“菜单测试”下拉菜单(见图 5 2)。
② 在“菜单测试”下拉式菜单下创建 “调用对话框 ”、“连接 ”和 “断开 ”三个菜单命令项。
注意创建“调用对话框”菜单要选中属性页中的 Popup 选项,创建“连接”和 “断开”菜单时不
要选中属性页中的 Popup 选项,并给出菜单 ID 号。
③ 在“调用对话框”的下拉子菜单下创建“消息对话框 1...”和“消息对话框 2...”,菜单
项有省略号表示单击该项将弹出对话框 。各菜单 ID 号见表 5 5。
表5 5 需要添加的菜单命令消息函数

菜单名称 菜单 ID 号 消 息 菜单消息函数
消息对话框 1... ID_TEST_MESSAGEDLG1 COMMAND OnTestMessagedlg1
消息对话框 2... ID_TEST_MESSAGEDLG2 COMMAND OnTestMessagedlg2
ID_TEST_CONNECT COMMAND OnTestBreak
连 接
UPDATE_COMMAND_UI OnUpdateTestBreak
ID_TEST_BREAK COMMAND OnTestConnect
断 开
UPDATE_COMMAND_UI OnUpdateTestConnect

(3)添加消息函数
通过 ClassWizard 为相应菜单命令增加命令消息处理(WM_COMMAND)函数,见图 5 3。
在 Class name 中选择 CEx05_1View;在 Object IDs 列表框中选择需要处理的菜单项 ID;
在 Message 列表框中选择 COMMAND 消息,再单击 Add Function 按钮即可。 添加的消息
响应函数见表 5 5。
(4)添加菜单更新消息函数
添加方法与上相同,只是在 Message 列表框中选择 UPDATE_COMMAND_UI 消息,再单击
Add Function 按钮即可。添加菜单更新消息函数见表 5 5。
(5)定义成员变量 105

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图5 3 利用“
MFCC
las
sWi
zar
d"添加菜单命令消息函数

在 Ex05_1View.h 文件中定义两个 BOOL 变量。

05_
清单5 1 定义两个 BOOL 变量(在 Ex 1Vi h文件中)
ew.


lassCEx 05_ 1V
iewpub l
icCV
iew
...
public:
BOOL bCheck;
BOOL bEnable;
...

(6)初始化成员变量(在构造函数中初始化,见清单 5 2)
(7)添加和编写菜单消息函数与菜单更新消息函数(见清单 5 2)

05_
清单5 2 初始化成员变量和消息函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx05_1V i 05_
ewCEx 1Vi //构造函数,始化成员变量
ew()
{ bCheck = 1;
bEnable = 1;

//菜单消息函数
vo
idCEx05_1V iew
OnTe
stMess
age
d l
g //点击消息对话框 1 菜单的消息函数
1()
{ AfxMessageBox("这是消息对话框 1!");}
vo
idCEx05_1V iew
OnTe
stMess
age
d l
g //点击消息对话框 2 菜单的消息函数
2()
{ AfxMessageBox("这是消息对话框 2!");}
vo
idCEx05_1V iew
OnTe
stBr
eak()//点击断开菜单的消息函数
{ bCheck = 1;
bEnable = 1;


oidCEx05_1V iew
OnTe
stCo
nne
c //点击连接菜单的消息函数
t()
{ bCheck = 0;
bEnable = 0;
106 
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

//菜单更新消息函数
vo
idCEx05_1V iewOnUpd ateTestConnet(
c //更新连接菜单
CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(bCheck);//打勾或不打勾
pCmdUI- >Enable(!bEnable);//禁用或启用

vo
idCEx05_1V iewOnUpd ateTestBr
e k(
a CCmdUI pCmdUI)//更新断开菜单
{ pCmdUI- >SetCheck(!bCheck);//打勾或不打勾
pCmdUI- >Enable(bEnable);//禁用或启用

(8)编译和运行


25 带有图标的菜单编程实例
在许多 Windows 应用软件中,其菜单命令旁边带有一个图标,可使用 CMenu 类的成员函
数 SetMenuItemBitmaps,该函数原型如下:

BOOL SetMenuItemBitmaps (UINT nPosition, UINT nFlags, const CBitmap *


pBmpUnchecked,const CBitmap* pBmpChecked);

参数: nPosition 指 定 要 改 变 的 菜 单 项; nFlags 指 定 如 何 解 释 nPosition 参 数;


pBmpUnchecked 指定 用 于 检 取 菜 单 项 的 位 图; pBmpChecked 指 定 用 于 被 检 取 菜 单 项 的
位图 。
【例5 2】 Ex05_2 实现带有图标的菜单,具体说明见表 5 6。

表5 6 例5 2具体说明

项 目 图示和说明

程序界面

程序功能 点击图标菜单,上面两个图标交替出现

(1)新建一单文档应用程序 Ex05_2
(2)编辑菜单
在查看 菜 单 后 增 加 菜 单: 图 标 菜 单 测 试, 在 其 下 增 加 下 拉 菜 单 项: 图 标 菜 单 ID _
TEST_BMPMENU
(3)添加两个位图资源
选择 ResourceView 视图,将鼠标放在文件夹 Ex05_2 resources 上,单击鼠标右键,在弹
出的右键菜单中,点击 insert  弹出 Insert Resource 对话框,选中其中的 Bitmap,这样就
插入了一个 ID 号为 IDB_BITMAP1 的位图,编辑该位图。同样插入第 2 个位图。
位图 1:IDB_BITMAP1 ;位图 2:IDB_BITMAP2
(4)定义成员变量(见清单 5 3)
(5)初始化成员变量(在构造函数中初始化,见清单 5 4)
(6)编写 OnDraw()函数(见清单 5 4) 107

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(7)添加和编写菜单消息函数与菜单更新消息函数(见清单 5 4)

05_
清单5 3 定义成员变量(在 Ex 2Vi h文件中)
ew.


lassCEx 05_2Viewpub licCV i
ew
...public:
CBitmap bitmap1,bitmap2;
BOOL bCheck;

05_
清单5 4 函数的编写(在 Ex 2Vi
ew.
cpp文件中)

05_
CEx 2View 05_
CEx 2View()//构造函数,初始化成员变量
{ bitmap1.LoadBitmap(IDB_BITMAP1);
bitmap2.LoadBitmap(IDB_BITMAP2);
bCheck = 1;


oidCEx 05_2V iewOnDr aw( CDC pDC) //在 OnDraw()函数实现位图与菜单的关联
{ CMenu * m_pMenu = AfxGetMainWnd()- >GetMenu();
m_pMenu- > SetMenuItemBitmaps(ID_TEST_BMPMENU, MF_BYCOMMAND,
&bitmap1,&bitmap2);


oidCEx 05_2V iewOnTe s
tBmpme nu()//菜单消息函数
{ bCheck = !bCheck;


oidCEx05_ 2ViewOnUpd ateTes
t nu(
Bmpme //菜单更新消息函数
CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(!bCheck);

(8)编译和运行


26 快捷菜单(上下文菜单,右键菜单)编程实例
用户单击鼠标右键时弹出的菜单被称为快捷菜单 。下面举例说明其创建过程。
【例5 3】 Ex05_3 快捷菜单,具体说明见表 5 7。
表5 7 例5 3具体说明

项目 图示和说明

程序菜单

图(
a) 快捷菜单 图(
b) 消息对话框

点击鼠标右键弹出图(a)所示的快捷菜单,再点击快捷菜单消息对话框,弹出图(b)的消
程序功能
108 息对话框
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

(1)新建一单文档应用程序 Ex05_3
(2)编辑菜单资源
① 创建一个新的菜单条。
选择 Insert Resource 菜单命令,在弹出的对话框中选择 Menu,然后单击 New 按钮,即可
向应用程序中添加一个新的菜单资源 。 添加操作也可通过右键单击 ResourceView 视图中
Menu 项,并 在 弹 出 的 快 捷 菜 单 中 选 择 Insert | Menu 命 令, 修 改 菜 单 资 源 的 ID 号 为
IDR_RIGHT_MENU。
② 添加 菜 单 项 和 菜 单 命 令, 步 骤 与 前 面 所 介 绍 的 完 全 类 似, 编 辑 完 成 后 的
IDR_RIGHT_MENU菜 单 资 源 如 图 5 4 所 示 。 其 中 “消 息 对 话 框 ... ”菜 单 ID 号 为
ID_SUB0_MSGDLG。
(3)添加消息函数
① OnContextMenu
VC60 专 门 为 使 用 快 捷 菜 单 提 供 了 WM _
CONTEXT_MENU 消息。利用 ClassWizard 添加,可以
添加到 CMainFrame 类或视类,本例添加到视类。
② OnSub0Messagedlg
利用 ClassWizard 添加点击 “消息对话框...”
图5 4 快捷菜单
菜单的消息响应函数 OnSub0Messagedlg。
(4)编写消息函数(见清单 5 5)

05_
清单5 5 消息函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx05_3V iewOnCo ntextMenu(CWnd pWndCPo in
tp oin //OnContextMenu 函数
t)
{ CRect rect;//定义 CRect 类对象
GetClientRect(&rect);//将客户区赋给 rect
CMenu menu;
menu.LoadMenu(IDR_CONTEXT_MENU);//载入快捷菜单资源
CMenu* pContextMenu = menu.GetSubMenu(0);//获得上下文菜单的第 0 号子菜单,
GetCursorPos(&point);//获得当前光标位置
pContextMenu- >TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON,
point.x, point.y, this,&rect);
menu.DestroyMenu();//释放菜单资源,以免浪费空间


oidCEx05_3V iewOnS ub0Me s
saged
lg()//菜单消息函数
{ AfxMessageBox(“您点击了快捷菜单中的消息对话框菜单!”);

(5)编译和运行


27 动态菜单编程实例
在应用程序中,可能需要动态的创建和改变菜单 。 这时用户可以定制另外的菜单资源,
并需要在应用程序中调用一些必要的函数,将菜单载入并激活。
【例5 4】 Ex05_4 动态菜单,具体说明见表 5 8。

109

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表5 8 例5 4具体说明

项 目 图示和说明

程序界面

图(a) 选中删除颜色菜单 图(b) 选中增加颜色菜单 图(c) 颜色菜单


(1) 点击“增加颜色菜单”,将 color 菜单增加到主菜单条上,见图(b)
程序功能
(2) 点击“删除颜色菜单”,将 color 菜单从主菜单条上删除, 见图(a)

(1)新建一单文档应用程序 Ex05_4
(2)编辑菜单资源
① 增加一新菜单 IDR_MENU1,其中包含 color 菜单,
见图 5 5。
② 在 IDR_MENUFRAME 菜单条的查看菜单下增加 2
个菜单项,分别为:增加颜色菜单 ID_VIEW_ADD 和删除颜 图5 5 增加IDR_MENU1菜单

色菜单 ID_VIEW_DELETE。
(3)定义成员变量(见清单 5 6)
清单5 6 定义成员变量(在 Ma
inF h文件中)
rm.


lassCMa inFramepub
licCF
rameWnd
...
public:
BOOL bCheck;//是否选中
...}

(4)在构造函数中初始化 bCheck(见清单 5 7)
(5)添加和编写菜单消息函数与菜单更新消息函数(见清单 5 7)
清单5 7 编写构造函数与消息函数 (在 Ma
inF
rm.
cpp文件中)

CMainF
rame CMa i
nF rame()//在构造函数中初始化 bCheck
 bCheck = 0;

//菜单消息函数
vo
idCMa i
nF r ameOnV iewAdd()//点击增加颜色菜单的消息函数
{ CMenu addmenu,* mainmenu;
if(!addmenu.LoadMenu(IDR_MENU1))
{MessageBox("菜单装入失败!");
return;//如装入失败,显示消息框,且返回

CString str =" 颜色(&C)";//要增加的菜单项的标签。
mainmenu = AfxGetMainWnd()- >GetMenu();//取得指向窗口菜单的 CMenu 对象的指针。
//将弹出式菜单插入到第 2 项菜单之前(菜单项从 0 开始计算)
mainmenu- >InsertMenu (1, MF_POPUP|MF_BYPOSITION|MF_STRING,
(UINT)addmenu.GetSubMenu(0)- > m_hMenu,str);
addmenu.GetSubMenu(0)- > m_hMenu;//是被装入菜单的第一个菜单项的弹出式菜单的菜单句柄。
110 mainmenu- >Detach();//将窗口菜单与 CMenu 对象分离。
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

addmenu.Detach();//将资源菜单(IDR_MENU1)与 CMenu 对象分离


DrawMenuBar();//重画菜单
bCheck = 1;


oidCMainFrame OnV iewDelete()//点击删除颜色菜单的消息函数
{ CMenu * mainmenu;
CString str;
mainmenu = GetMenu();//取得指向窗口菜单的 CMenu 对象的指针
//取得菜单的项数。
for(int i = mainmenu- >GetMenuItemCount()-1;i>= 0;i - - )
{//将指定菜单项的标签拷贝到指定的缓冲区 MF_BYPOSITION 的解释见上
mainmenu- >GetMenuString(i,str,MF_BYPOSITION);
if(str = =" 颜色(&C)") //如果是刚才我们增加的菜单项,则删除。
{ mainmenu- >DeleteMenu(i,MF_BYPOSITION);
break;


mainmenu- >Detach();//将窗口菜单与 CMenu 对象分离。
DrawMenuBar();//重画菜单。
bCheck = 0;

//菜单更新消息函数
vo
idCMa i
nF rameOnUpd ateViewAdd( //增加颜色菜单更新
CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(bCheck);

vo
idCMa i
nF rameOnUpd ateViewDel
ete(
CCmdUI pCmdUI)//删除颜色菜单更新
{ pCmdUI- >SetCheck(!bCheck);

(6)编译和运行

3 工具栏

工具栏中包含了一组用于执行命令的位图式按钮,其按钮被按下时与菜单选项和键盘
加速键作用相同,也会发送相应的命令消息。 工具栏一般
位于主框架窗口客户区域的上方,用户可以拖动工具栏将
其停靠在客户区的任何位置,也可以使用鼠标改动它的
大小。


31 工具栏资源编辑器
图5 6 为工具栏编辑器界面,使用工具栏编辑器可
以完成的功能包括:创建新的工具栏和按钮把位图转换为
工具栏资源,创建、移动和编辑工具栏按钮。
(1)创建工具栏
创建新的工具栏和按钮可以按下面的步骤完成:
① 选择 Insert |Resources 菜单命令; 图5 6 工具栏编辑器
② 在弹出的资源选择对话框中选择 Toolbar 选项; 111

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

③ 单击 New 按钮。
(2)创建、移动和编辑工具栏按钮
① 创建一个新的工具栏按钮
● 用鼠标选择工具栏最右端的空白按钮 。
● 在按钮上绘图。
● 双击该按钮,然后在弹出的按钮属性对话框图 5 7 中输入按钮 ID 号和提示。

图5 7 按钮属性对话框

按钮属性对话框各项说明见表 5 9。

表5 9 按钮属性对话框的各项说明

项 目 说 明
ID 按钮的 ID 号
Width 按钮中位图的宽度值,以像素为单位
Height 按钮中位图的高度值,以像素为单位
Prompt 按钮条目被选中时,\n 前面的文本将要显示在状态条上。\n 后面的文本为工具提示

② 移动一个工具栏按钮
首先用鼠标选择一个工具栏按钮,然后按住鼠标左键并移动此按钮到工具栏的相应位置。
③ 删除一个工具栏按钮
首先用鼠标选择一个工具栏按钮,使用鼠标把它拖到工具栏外。
④ 在工具栏上的两个按钮中插入一个空白区
● 在一个后面不是空白区的按钮前插入空白区时,只需要把此按钮用鼠标向后拖动,
直到此按钮的一半部分与后面的按钮重叠在一起为止 。
● 在一个后面是空白区的按钮前插入空白区时,并且仍然保持后面的空白区时,只需
要把此按钮用鼠标向后拖动,直到此按钮的一半部分与后面的按钮重叠在一起,或者此按钮
的右边边缘部分与下一个按钮刚好接触 。
● 在一个后面是空白区的按钮前插入空白区,并且删除后面的空白区,使前后两个按钮
相邻时,只需要把此按钮用鼠标向后拖动,直到此按钮的一半部分与下一个按钮重叠为止。
⑤ 删除工具栏按钮的空白区
● 选择工具栏中的一个空白区两边的其中一个按钮 。
● 使用鼠标把此按钮向空白区方向拖动,直到此按钮的一半部分与另一边的按钮的
一半重叠为止。


32 CToo
l r类
Ba
112 CToolBar 是工具栏类,用于管理应用程序窗口中的工具栏。 CToolBar 类的继承关系如
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

图 5 8 所示。

图5 8 CTo
ol r类的继承关系
Ba

CToolBar 类的主要成员函数见表 5 10。


表5 10 CTo
ol r类的主要成员函数
Ba

成 员 函 数 说 明
int CommandToIndex() 获取工具栏中第一个有给定 ID 的按钮的索引
BOOL Create() 创建工具栏
void GetButtonInfo() 获取按钮的有关信息
UINT GetButtonStyle() 获取按钮的风格
CString GetButtonText() 获取按钮的文本
UINT GetItemID() 获取给定索引按钮的 ID
void GetItemRect() 获取给定索引项目的显示矩形
CTool BarCtrl&GetToolBarCtrl() 获取对 CToolBar 对象描述的 CToolBarCtrl 对象的引用
BOOL LoadBitmap() 加载工具栏的按钮图像
BOOL LoadToolBar() 加载工具栏资源
BOOL SetBitmap() 设置新的工具栏按钮位图
void SetButtonInfo() 设置按钮的 ID 风格和图像序号
BOOL SetButtons() 设置工具栏按钮的 ID
void SetButtonStyle() 设置按钮的风格
BOOL SetButtonText() 设置按钮的文本
void SetHeight() 设置工具栏的高度
void SetSizes() 设置按钮的大小


33 常规工具栏编程实例
常规工具栏的操作与应用主要包括工具栏创建、 载入、
显示与隐藏等,具体实例见例 5 5。
【例5 5】 Ex05_5 工具栏的创建、载入、显示与隐藏,具体说明见表 5 11。
表5 11 例5 5具体说明

项 目 图示和说明

程序界面

(1)程序启动时载入标准工具栏和绘图工具栏
程序功能
(2)点击查看下面的绘图工具栏菜单可以使绘图工具栏显示或隐藏

(1)新建一单文档应用程序 Ex05_5
(2)编辑工具栏资源
① 创建绘图工具栏:ID_DRAW_TOOLBAR;
② 在绘图工具栏中添加绘图按钮;
直线: ID_DRAW_LINE; 矩形: ID_DRAW_RECTANGLE
圆: ID_DRAW_CIRCLE; 文字: ID_DRAW_TEXT 113

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(3)将工具栏载入应用程序中
在应用程序中一般通过重载 CMainFrame 类中的 OnCreate 函数载入工具栏。 如果应用
程序中包括不同的子框架窗口类型,需要为每个子框架窗口创建不同的工具栏,这时也可在
CChildFrame 类的 OnCreate 函数中载入工具栏。OnCreate 函数是 WM_CREATE 消息映射的处
理函数,该消息处理函数将在框架窗口创建时被调用 。
① 定义 CToolBar 对象(见清单 5 8);
清单5 8 定义对象 (在 Ma
inF h文件中)
rm.


las
sCMa inFramepub li
cCF rameWnd
 public:
CToolBar m_DrawToolBar;//定义 CToolBar 类的对象

② 工具栏载入(见清单 5 9)。
(4)工具栏的显示与隐藏
① 编辑查看菜单下的绘图工具栏菜单;
菜单名:绘图工具栏 ID 号:ID_VIEW_DRAWTOOLBAR
② 添加和编写菜单消息函数与菜单更新消息函数(见清单 5 9)。
清单5 9 函数(在 Ma
inF
rm.
cpp文件中)

//将工具栏载入应用程序中
in
tCMa inF r
ame OnCreae(
t LPCREATESTRUCTl pCre at
eStruct)
{...
//载入绘图工具栏
if (!m_DrawToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
|CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)||
!m_DrawToolBar.LoadToolBar(IDR_DRAW_TOOLBAR))
{ TRACE0("绘图工具栏创建失败! \n" );
return - 1; //fail to create

m_DrawToolBar.SetWindowText("绘图工具栏");//给标准工具栏加上标题
//绘图工具栏停靠
m_DrawToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_DrawToolBar);
return 0;

//菜单消息函数
vo
idCMainFr ameOnV iewDrawb r()

{ BOOL bVisible = ((m_DrawToolBar.GetStyle()& WS_VISIBLE)!= 0);
ShowControlBar(&m_DrawToolBar,!bVisible, FALSE);
RecalcLayout();

//菜单更新消息函数
vo
idCMainFr ameOnUpd ateViewDr awbr(
a CCmdUI pCmdUI)
{ BOOL bVisible = ((m_DrawToolBar.GetStyle()& WS_VISIBLE)!= 0);
pCmdUI- >SetCheck(bVisible);

114
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

(5)编译和运行


34 下拉式工具栏按钮编程实例
使用 SetButtonlnfo 函数将工具栏按钮的风格设置为 TBSTYLE _DROPDOWN,即可创建下
拉式工具按钮。
在应用程序中,要求当用户按下缩放观察比例命令按钮时,就会打开一个菜单,该菜单
中包括视 图 能 够 被 缩 放 的 比 例。 由 于 此 时 按 钮 向 主 框 架 窗 口 传 递 的 并 非 一 般 按 钮 的
WM_COMMAND消息,而是通告下拉操作的 TBN_DROPDOWN 消息。 框架窗口所能处理的工具栏按
钮消息只是 WM_COMMAND 和 UPDATE_COMMAND_UI 消息,因此要想使框架窗口对按钮的下拉操
作作出处理的话,必须使用别的途径。
MFC 中提供了 ON_NOTIFY 宏用于处理复合对象消息,实际上,对对象的每个操作都会向
框架窗口发送 WM_NOTIFY 消息。WM_NOTIFY 消息包含了发送消息的控件 ID(该 ID 存放在其
消息处理函数的 wParam 参数中 )和一个存放在 lParam 中的结构指针。 该结构指针可以执
行一个 NMHDR 结构。
【例5 6】 Ex05_6 下拉工具栏的创建与应用,具体说明见表 5 12。
表5 12 例5 6具体说明

项 目 图示和说明

程序界面

图(a) 下拉工具栏 图(b) 消息对话框

程序功能 点击 按钮,弹出图(a)所示的缩放菜单,点击缩放菜单 25% 弹出图(b)。

(1)新建一单文档应用程序 Ex05_6
(2)编辑工具栏资源
在标准工具栏中添加放大按钮 ,该按钮的 ID 号为 ID_ENLARGE
(3)创建下拉式按钮(见清单 5 10)
清单5 10 在 OnCr
eae函数中创建下拉按钮(在 Ma
t inF
rm.
cpp文件中)

ntCMainFrame OnCreate(LPCREATESTRUCTl pCreateStruct)
{ //载入标准工具栏
m_wndToolBar.SetWindowText("标准工具栏");//给标准工具栏加上标题
//标准工具栏停靠
//在放大按钮旁显示三角箭头
m_wndToolBar.GetToolBarCtrl().SetExtendedStyle(TBSTYLE_EX_DRAWDDARROWS);
//在放大按钮 9(从 0 开始编号,所以第 9 个按钮编号为 8,其中空格也算一个按钮)旁创建下拉按钮
m_wndToolBar.SetButtonStyle(8,TBSTYLE_BUTTON | TBSTYLE_DROPDOWN);
} 115

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(4)编辑菜单资源
添加一个缩放菜单,ID 号为:IDR_ZOOM,下面带各种缩放比例的子菜单。
25% ID_ZOOM_25
50% ID_ZOOM_50
100% ID_ZOOM_100
200% ID_ZOOM_200
400% ID_ZOOM_400
(5)使用 ON_NOTIFY 宏显示下拉式按钮菜单
① 在视类的实现文件中添加 ON_NOTIFY 消息映射(见清单 5 12);
② 在视类的头文件中声明 OnDropDown 函数(见清单 5 11)。
(6)添加和编写 OnDropDown 消息函数(见清单 5 12)
(7)定义变量和赋初值
在 Ex05_6View.h 文件中定义 BOOL 型变量 bCheck25(见清单 5 11)。
在构造函数中赋初值 bCheck25 = 0(见清单 5 12)。

05_
清单5 11 声明变量与消息函数(在 Ex 6Vi h文件中)
ew.


lassCEx 05_6Viewpub li
cCV i
ew
...
public:
BOOL bCheck25;//定义 BOOL 型变量 bCheck25
protected:
//AFX_MSG(CEx05_6View)
...
//}}AFX_MSG
afx_ms gv oidOnDr opDown(NMHDR pNo t
ifySt
ruc
tLRESULT pRe
sul //手工添加
t);
DECLARE_MESSAGE_MAP()

(8)添加和编写不同放大比例的菜单消息响应函数和菜单更新消息响应函数 (见清单
5 12)

05_
清单5 12 函数(在 Ex 6Vi
ew.
cpp文件中)
...
BEGIN_MESSAGE_MAP(CEx05_6View, CView)
//{{AFX_MSG_MAP(CEx05_6View)
...
//}}AFX_MSG_MAP
//Standard printing commands
ON_ NOTIFY(TBN_ DROPDOWNAFX_ IDW_
TOOLBAROnDr
o //添加消息映射
pDown)
END_MESSAGE_MAP()
CEx 05_6ViewCEx05_ 6View() //构造函数
{ bCheck25 = 0;

//缩放菜单下拉工具按钮响应消息函数 OnDropDown
vo
idCEx 05_ 6View
OnDropDown( NMHDR pNo ti
fyStruc
tLRESULT pRe
sut)

{ NMTOOLBAR* pNMToolBar = (NMTOOLBAR* )pNotifyStruct;
116 CRect rect;
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

CMainFrame * pWnd = (CMainFrame * )AfxGetMainWnd();


//将工具栏按钮的矩形转换为屏幕坐标,以便确定从何处打开下拉菜单
pWnd- >m_wndToolBar.GetToolBarCtrl().GetRect(pNMToolBar- >iItem, &rect);
rect.top = rect.bottom;
::ClientToScreen(pNMToolBar- >hdr.hwndFrom, &rect.TopLeft());
if(pNMToolBar- >iItem = = ID_ENLARGE)
{ CMenu menuR;
CMenu* pPopupR;
//从资源中载入菜单
menuR.LoadMenu(IDR_ZOOM);
pPopupR = menuR.GetSubMenu(0);
pPopupR- >TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, rect.left, rect.top+1,
AfxGetMainWnd());

* pResult = TBDDRET_DEFAULT;

//菜单消息函数
vo
i 05_
dCEx 6View OnZoom25() //点击菜单25%的消息函数
{ AfxMessageBox("缩放 25%!");
bCheck25 = 1;//bCheck25 为 Bool 型在 Ex05_6View.h 文件中定义,在构造函数中赋初值 0;

//菜单更新消息函数
vo
i 05_
dCEx 6View OnUpd at
eZoom25( CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(bCheck25);

(9)修改 MainFrm.h 头文件中成员变量 m_wndToolBar 的定义


将 protected:
CToolBar m_wndToolBar;
修改为 public:
CToolBar m_wndToolBar;
(10)在 Ex05_6View.cpp 文件中添加# include “MainFrm.h”
(11)编译和运行

5 r 和 CDi
4 CReBa alogBa


4 r类
1 CReBa
ReBar 是一种工具栏,它的主要用途是作为子窗口、通用对话框控件、菜单和工具栏等的
容器。一个 ReBar 可以包含一个或多个带区 (band)。每一个带区可以包含拖动条 (gripper
bar)、位图、文本标签和子窗口的任意组合。
CReBar 类的继承关系如图 5 9 所示。

图5 9 CReBa
r类的继承关系
117

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

CRebar 类的主要成员函数见表 5 13。


表5 13 CReBa
r类的主要成员函数

成 员 函 数 说 明
BOOL AddBar() 在 ReBar 中增加一个区段
BOOL Create() 创建一个 ReBar
CReBarCtrl& GetReBarCtrl() 允许直接访问在下面的公共控件


42 CD
ial r类
ogBa
DialogBar 是一个基于对话 框 模 板 的 控 件 栏。 它 与 对 话 框 有 些 类 似,可 以 包 含 标 准
Windows 通用控件和一些用户自定义的控件,并可以在这些控件之间进行转换 。它具有无模
式对话框的功能,使用对话框编辑器可以设计并创建对话框栏 。
CDialogBar 类的继承关系如图 5 10 所示。

图5 10 CD
ial
o r类的继承关系
gBa

CDialogBar 类的主要成员函数为 Create,其作用是载入对话框资源并创建对话框栏


窗口。


43 编程实例
【例5 7】 Ex05_7 ReBar 创建与应用,具体说明见表 5 14。

表5 14 例5 7具体说明

项 目 图示和说明

程序界面

ReBar 中包含两个带,其一为地址组合框,它由 CDialogBar 类的 Create 函数创建;其二


为复选框,它由 CButton 类的 Create 函数创建
(1)初始化时,将 ReBar 中包含的两个带都显示在界面;复选框未被选中,在视图中显示“复
程序功能 选框未被选中!”
(2)复选框被选中,在视图中显示“复选框被选中!”

(1)新建一单文档应用程序 Ex05_7
(2)创建 ReBar
① 在 MinFrm.h 中定义 CReBar 类的对象(见清单 5 13);
② 在 CMainFrm.cpp 文件中用 CReBar 类的 Create 函数创建 ReBar(见清单 5 14)。
(3)创建一 DialogBar
118 ① 建立对话框资源:IDR_DLGBAR,见图 5 11;
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

图5 11 创建对话框IDR_
DLGBAR

② 在对话框中添加控件:组合框 IDC_ADDRESS;
③ 在 MinFrm.h 中定义 CDialogBar 类的对象(见清单 5 13);
清单5 13 定义对象(在 Ma
inF h文件中)
rm.


lassCMa inFramepubl
icCF rameWnd
...
public:
CReBar m_RebarToolBar;//定义一 CReBar 类对象
CDialogBar m_DlgToolBar;//定义一 CDialogBar 类对象
CButton m_check;//定义一 CButton 类对象
...}

④ 在 CMainFrm.cpp 文件中用 CDialogBar 类的 Create 函数创建(见清单 5 14)。


(4)创建一复选框按钮
① 选择菜单 View|Resource Symbols 并单击 New 按钮。输入 IDC_CHECK 作为控件 ID 号;
② 在 MinFrm.h 中定义 CButton 类对象(见清单 5 13);
③ 在 MainFrm.cpp 文件中用 CButton 类的 Create 函数创建(见清单 5 14);
(5)将对话框条和复选框控件添加到 ReBar 的两个带区上(见清单 5 14)
清单5 14 OnCr
eae函数 (在 Ma
t inF
rm.
cpp文件中)


ntCMa inFrame OnCreate(LPCREATESTRUCTl
pCr
eat
eSt
rut)

{...
//创建 ReBar
if(!m_ReBarToolBar.Create(this))
{ TRACE0("创建 ReBar 失败!\n" );
return-1;

//创建 CDialogBar
if (!m_DlgToolBar.Create(this, IDR_DLGBAR,CBRS_ALIGN_TOP, AFX_IDW_DIALOGBAR))
{ TRACE0("Failed to create dialogbar\n" );
return-1;//fail to create

//创建复选框
if(!m_check.Create("请选择",WS_CHILD|WS_VISIBLE|BS_AUTOCHECKBOX,
CRect(0,0,20,20),this,IDC_CHECK))
{ TRACE0("创建复选框失败!\n" );
return-1;

//将对话框条添加到 ReBar 上
m_RebarToolBar.AddBar(&m_DlgToolBar," 地址:",NULL,RBBS_GRIPPERALWAYS);
//将复选对话框添加到 ReBar 上
m_RebarToolBar.AddBar(&m_check," 复选框:",NULL,RBBS_BREAK|RBBS_GRIPPERALWAYS);

119

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(6)添加复选按钮的消息响应函数
① 添加单击复选按钮的 OnClick 消息函数声明(见程序清单 5 15);
清单5 15 OnC
lic 05_
k消息函数声明(在 Ex 7Vi h文件中)
ew.


lassCEx 05_ 7Viewpub l
icCV iew
...
protected:
//AFX_MSG(CEx05_7View)
//NOTEthe ClassWizard will add and remove member functions here.
//DO NOT EDIT what you see in these blocks of generated code !
//AFX_MSG
afx_ms gv o
idOnC li
ck();
DECLARE_MESSAGE_MAP()
};

② 添加单击复选按钮的消息映射说明(见清单 5 16);
③ 编写 OnClick 消息函数 (见清单 5 16);
④ 编写 OnDraw 函数 (见清单 5 16);
⑤ 在 CEx05_7View.cpp 文件开头添加# include" MainFrm.h" (见清单 5 16)。

05_
清单5 16 消息映射说明和函数编写(在 Ex 7Vi
ew.
cpp文件中)

#i nc
lude"Ma inF rm.h"
...
IMPLEMENT_DYNCREATE(CEx05_7View, CView)
BEGIN_MESSAGE_MAP(CEx05_7View, CView)
//{{AFX_MSG_MAP(CEx05_7View)
//NOTEthe ClassWizard will add and remove mapping macros here.
// DO NOT EDIT what you see in these blocks of generated code!
//AFX_MSG_MAP
//Standard printing commands
...
ON_ BN_ CLICKED( IDC_ CHECK OnC li
ck)//复选按钮的消息映射说明
END_MESSAGE_MAP()
voi
dCEx 05_ 7ViewOnC l
ick()//单击复选框的消息响应函数
{ Invalidate();

voi
dCEx 05_ 7ViewOnDr aw(CDC pDC) //OnDr aw 函数
{ CEx05_7Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
//TODO: add draw code for native data here
CString clickmsg;
if(((CMainFrame*)(AfxGetApp()- >m_pMainWnd))- >m_check.GetCheck())
clickmsg =" 复选框被选中!";
else
clickmsg =" 复选框未被选中!";
pDC- >TextOut(10,10,clickmsg);

120
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

5 状态栏

在应用程序中的状态栏可以在用户不断工作的情况下为用户显示有用信息 。 它通常位
于窗口底部,既不接受用户输入也不产生命令消息。 状态栏的作用就是在程序的控制下在
窗格(pane)中显示一些文本。


51 CS
tat
u r类
sBa
CStatusBar 是状态栏类,用于管理应用程序窗口中的状态栏。 CStatusBar 类的继承关
系如图 5 12 所示。

图5 12 CS
tat
us r类的继承关系
Ba

CStatusBar 类的主要成员函数见表 5 15。

表5 15 CS
tat
us r类的主要成员函数
Ba

成 员 函 数 说 明
int CommandToIndex() 获取给定 ID 的指示器索引值,第一个指示器的索引值为 0
BOOL Create() 创建状态栏
UINT GetItemID() 获取给定索引的指示器的 ID
void GetPaneInfo() 获取给定索引的指示器窗格样式和宽度
void GetPaneStyle() 获给定索引的指示器的样式
CString GetPaneText() 获给定索引的指示器的文本
CStatusBarCtrl& GetStatusBarCtrl() 获取 CStatusBarCtrl 类的成员函数的权限
void SetPaneInfo() 设置给定索引的指示器标窗格样式和宽度
void SetPaneStyle() 设置定索引的指示器的样式
BOOL SetPaneText() 设置定索引的指示器的文本


52 状态栏的创建
(1)创建一个状态栏的步骤
① 声明状态栏对象(见清单 5 17);
② 定义状态栏指示器字符串 ID 数组(见清单 5 18);
③ 创建状态栏(见清单 5 18)。
清单5 17 声明状态栏对象(在 Ma
inF h文件中)
rm.


lassCMa inFramepub li
cCF rameWnd
...
protected:
CStatusBar m_wndstatusBar;//声明状态栏对象
...}
121

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单5 18 状态栏指示器字符串ID 数组和创建状态栏(在 Ma


inF
rm.
cpp文件中)

...
//定义状态栏指示器字符串 ID 数组
stat
i cUINTi nd
icator
s=//指示器组

ID_SEPARATOR, //状态栏第一窗格,提示信息输出栏
ID_INDICATORCAPS, //状态栏第二窗格,显示 CapS Lock 键状态
ID_INDICATORNUM, //状态栏第三窗格,显示 Num Lock 键状态
IDINDICATORSCRL, //状态栏第四窗格,显示 Scroll Lock 键状态
};
intCMa inF rame OnCreate(LPCREATESTRUCTl pCr e
atest
ruct)
{...
//创建状态栏
if(! m _wndStatusBar.Create(this)||! m wndStatusBar.SetIndicators(indicators,
sizeof(Indicators)/sizeof(U INT)))
{ TRACEO("Failed to create status bar\n " );
return -l;//fail create

(2)把一个窗格添加到状态栏中的步骤
① 为窗格创建一个 ID 命令;
② 创建窗格的缺省字符串;
③ 添加窗格的命令 ID 到状态栏的指示器数组;
④ 为窗格创建一个处理程序。


53 编程实例
【例5 8】 Ex05_8 在状态栏显示鼠标的位置和时间,具体说明见表 5 16。

表5 16 例5 8具体说明

项 目 图示和说明

程序界面
(状态栏)

程序功能 在状态栏中动态显示鼠标的位置和系统时间

(1)新建一单文档应用程序 Ex05_8
(2)创建新的命令 ID 和缺省字符串
鼠标的命令 ID 为:ID_INDICATOR_MOUSE,缺省字符串为:鼠标位置。
时间的命令 ID 为:ID_INDICATOR_CLOCK,缺省字符串为:00:00:00。
创建和设置方法有两种:
一种是 View- > Resource Symbol,添加 ID;然后到 String table 中添加字符串。
另一种是 ID 号和字符串都在 String Table 中添加。 当用后一种方式操作时,若完成
后,时钟栏并不显示时间,则需要将此 New String 在 String Table 中对应的 Value 值加 1
122 (可在 Resource.h 中修改)。
第五章 程序界面设计
……………………………………………………………………………………………………………………………………………

(3)添加 ID 到指示器数组(见清单 5 19)


(4)设置状态栏的外观(见清单 5 19)
(5)设置定时器 SetTimer(见清单 5 19)
(6)添加和编写 OnTimer 函数(见清单 5 19)
(7)添加和编写 OnClose 函数,销毁定时器 KillTimer(见清单 5 19)
清单5 19 Ma
inF
rm.
cpp文件

...
//添加 ID 到指示器数组
stat
i cUINTi ndic
ators=

ID_SEPARATOR, //status line indicator
ID_INDICATOR_MOUSE, //鼠标位置状态栏
ID_INDICATOR_CLOCK, //时钟位置状态栏
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
;
...
intCMa inFrame OnCre a
te(LPCREATESTRUCTl pCr e a
teS
tru
ct)
{...
//载入状态栏到程序界面
if (!m_wndStatusBar.Create(this)||!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)
/sizeof(UINT)))
{TRACE0("Failed to create status bar\n" );
return-1; //fail to create

//设置显示鼠标和时间状态栏窗格的外观
int index = m_wndStatusBar.CommandToIndex(ID_INDICATOR_MOUSE);//获取鼠标指示器 ID 的索引
//设定鼠标指示器窗格的外观,SBPS_POPOUT 为外凸型
m_wndStatusBar.SetPaneInfo(index,ID_INDICATOR_MOUSE,SBPS_POPOUT,80);
int index1 = m_wndStatusBar.CommandToIndex(ID_INDICATOR_CLOCK);//获取时间指示器 ID 的索引
//设定时间指示器窗格的外观,SBPS_STRETCH 为下凹型
m_wndStatusBar.SetPaneInfo(index1,ID_INDICATOR_CLOCK,SBPS_STRETCH,80);
SetTimer(1,1000,NULL);//安装定时器,并将其时间间隔设为 1000 毫秒
...


oidCMa inF r
ame OnT imer(UINTn IDEvent)//OnTimer 函数,用 ClassWizard 添加
{ CTime time;
time = CTime::GetCurrentTime();//得到当前时间
Cstring s = time.Format("%H:%M:%S" );//转换时间格式
int index1 = m_wndStatusBar.CommandToIndex(ID_INDICATOR_CLOCK);
m_wndStatusBar.SetPaneText(index1,s);
CFrameWnd::OnTimer(nIDEvent);


oidCMa inF r
ame OnC lo
se()//OnClose 函数,用 ClassWizard 添加
{ KillTimer(1);
CFrameWnd::OnClose();

123

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

SetTimer()函数是每隔一定时间触发一次 OnTimer()函数,在本例中程序每隔 1000 ms


(即 1 s),执行一次 OnTimer 函数。
(8)添加鼠标移动的消息响应函数,更新状态栏中鼠标位置窗格的文字(见清单 5 20)
清单5 20 OnMo
useMo
v 05_
e函数 (在 Ex 8Vi
ew.
cpp文件中)


oidCEx05_8V i
ew OnMo useMo ve(UINTnFlagsCPo intp oint)
{ CStatusBar* pStatus = (CStatusBar* )
AfxGetApp()- >m_pMainWnd- >GetDescendantWindow(ID_VIEW_STATUS_BAR);
if(pStatus)
{ char mouseposition40;
sprintf(mouseposition," (
%d,%d) ",point.x,point.y);//将鼠标位置赋值给 mouseposition
pStatus- >SetPaneText(1,mouseposition,TRUE);//在状态条的第二个窗格中输出当前鼠标
//的位置
CView::OnMouseMove(nFlags, point);

124
第六章 对话框与控件

对话框是 Windows 应用程序设计中最常用的交互手段,应用程序通过对话框得到用


户的输入信息并作出相应的响应,而用户也能够从对话框中获取应用程序执行的情况 。
在应用程序的对话框中,通常都包含了一系列的控件。 每个控件通常都是一个小的窗
口,能够完成一些基本的交互任务。除了在对话框中使用控件外,用户可以在任何需要的时
候在其他窗口中创建和使用控件 。本章将介绍有关对话框与控件的内容 。
本章要点:
* 对话框及其常用控件基本知识
* 对话框编辑器的使用
* 对话框的调用

1 对话框基本知识


11 对话框的组成
对话框主要由两部分组成:
(1)一个指定了对话框中各控件及其相对位置的对话框模板资源;
(2)一个对话框类,通常都是从 CDialog 类派生。


12 对话框的类型
(1)按调用分类
Windows 应用程序中的对话框分为模式对话框和无模式对话框两类 。
模式对话框(Modal Dialog)就是在未关闭该对话框前,不能选择应用程序的其他功能。
例如在“Word”字处理软件中选择[格式][字体]菜单命令,在退出字体对话框前,无法继续进
行文本编辑。
无模式对话框(Modeless Dialog)则在关闭该对话框前,用户可以选择应用程序的其他
功能。例如“Word”字处理软件中的“Find”对话框。
(2)按创建分类
① 消息对话框 用 MessageBox()或 AfxMessageBox()显示消息对话框
② 通用对话框 字体、颜色、文件、查找等通用对话框
③ 一般对话框

125

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


13 编写对话框程序的流程
编写对话框程序的一般流程如下:
(1)创建对话框资源,并添加各种所需的控件;
(2)创建与对话框资源相关联的 CDialog 的派生类;
(3)对话框的显示;
(4)创建与控件相关的数据成员变量;
(5)创建对话框中相关控件的消息处理函数 。

2 消息对话框


21 消息对话框函数
最简单 的 对 话 框 是 显 示 消 息 的 消 息 框, 只 需 用 MFC 类 库 中 的 函 数 MessageBox 或
AfxMessageBox 来创建、显示和操作对话框。AfxMessageBox 的原型为:

int AfxMessageBox(LPCTSTR lpszText,UINT nType = MB_OK,UINT nIDHelp = 0);

该函数至少带一个参数 lpszText,即在消息对话框中显示的文本。 第二个参数 nType


可选,为图标风格和按钮的排列方式。第三个参数也可选,为所显示信息的帮助内容的 ID。
消息框可选的图标类型有四种,分别代表特定含义,见表 6 1;消息框中按钮的可能排
列见表 6 2;消息框的返回值说明了选择的按钮,可能的返回值见表 6 3。

表6 1 消息框中所用的图标

图 标 含 义 nType 可取的值
IDC_STATIC 警 告 MB_ICONEXCLAMATION
IDC_STATIC 信 息 MB_ICONINFORMATION
IDC_STATIC 问 题 MB_ICONQUESTION
IDC_STATIC 错 误 MB_ICONSTOP

表6 2 消息框中按钮的排列

包含在消息框中的按钮 nType 可取的值 包含在消息框中的按钮 nType 可取的值


终止、重试、忽略 MB_ABORTRETRYIGNORE 重试、取消 MB_RETRYCANCEL
确定 MB_OK 是、否 MB_YESNO
确定、取消 MB_OKCANCEL 是、否、取消 MB_YESNOCANCEL

表6 3 消息框的返回值

返 回 值 选择的按钮 返 回 值 选择的按钮
IDABORT 终止 IDOK 确定
IDCANCEL 取消 IDRETRY 重试
IDIGNORE 忽略 IDYES 是
IDNO 否
126
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………


22 消息对话框编程实例
【例6 1】 Ex06_1 消息对话框,具体说明见表 6 4。
(1)新建一单文档应用程序 Ex06_1
(2)编辑菜单资源
(3)添加菜单消息函数(见表 6 5)
表6 4 例6 1具体说明

项 目 图示和说明

图(
a) 菜单 b) 警告消息对话框
( c) 信息消息对话框

程序界面

图(
d) 问题消息对话框 e) 错误消息对话框
( 图(
f) 缺省消息对话框
程序功能 点击消息对话框菜单下的各菜单项分别弹出以上消息对话框

表6 5 菜单消息函数

菜 单 ID 号 消 息 消 息 函 数
警告... ID_MSG_EXCLADLG COMMAND OnMsgExcladlg()
信息... ID_MSG_INFODLG COMMAND OnMsgInfodlg()
问题... ID_MSG_QUEDLG COMMAND OnMsgQuedlg()
错误... ID_MSG_STOPDLG COMMAND OnMsgStopdlg()
缺省... ID_MSG_DEFDLG COMMAND OnMsgDefDlg()

(4)编写菜单消息函数(见清单 6 1)

06_
清单6 1 菜单消息函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx06_1V i
ew OnMs
gExcladg()

{ AfxMessageBox("警告", MB_YESNO|MB_ICONEXCLAMATION);


oidCEx06_1V i
ew OnMs
gInfodg()

{ AfxMessageBox("信息", MB_OKCANCEL|MB_ICONINFORMATION);


oidCEx06_1V i
ew OnMs
gQue d
lg()
{ AfxMessageBox("问题", MB_ABORTRETRYIGNORE|MB_ICONQUESTION);


oidCEx06_1V i
ew OnMs
gStopdg()

{ AfxMessageBox("错误!", MB_OK|MB_ICONSTOP);

127

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidCEx06_1V i
ew OnMs
gDefD
lg()
{ AfxMessageBox("缺省");

(5)编译和运行

3 对话框资源编辑器

对话框资源编辑器用于创建或编辑对话框资源或对话框模板。使用对话框编辑
器 能 够 向 对 话 框 中 添 加 控 件 的 布 局 和 测 试 对 话 框 运 行 ,图 6 1 为对话框资源编辑器
界面。
打开对话框资源编辑器时,同时还应出现图 6 2 所示的 Controls 工具栏和图 6 3 所
示的 Dialog 工具栏。如果 Controls 或 Dialog 工具栏没有出现,可将鼠标移到菜单栏位置,
单击 鼠 标 右 键, 在 出 现 的 快 捷 菜 单 中 选 择 Controls 命 令 或 Dialog 命 令 即 可。 利 用
“Controls”工具栏可以为对话框资源添加控件 。“Dialog”工具栏提供了一些对控件进行对
齐、均布和尺寸分配等功能。

图6 1 对话框资源编辑器窗口

图6 3 D
ial
og工具栏 图6 2 Co
ntr
ols工具栏

4 控件

在“Controls”工具栏中可以看到许多种类的控件,这些控件在 Windows 应用程序中得
到广泛的应用。各种控件类的继承关系如图 6 4 所示:

图6 4 控件类继承关系

控件资源的添加有两种方法,一种是使用对话框编辑器向对话框资源中添加控件;另一
128
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

种是用控件所属类的 Create 函数编程创建。


静态文本(Static Text):用来在指定的位置显示特定的字符串,一般用来标识附近另一
控件的内容。显示在静态文本控件中的字符串一般不再改变,但是在需要的时候,也可以通
过调用相应的函数进行设置。MFC 提供了 CStatic 类支持静态控件。
编辑框(Edit Box):用来接收用户输入的字符串。编辑框可以接收字符串、数字、密码等
等;编辑框还可以设置成接收多行字符串的模式;可以自动进行大小写转换。 编辑框可能向
其父窗口发送多种控件通知,如果用户需要,可以对这些控件通知进行处理。 MFC 提供了
CEdit 类支持编辑框控件。
成组框控件(Group):成组框只能起到说明和修饰作用,虽然成组框控件的作用并不大,
但从程序的界面和可读性方面考虑,应尽可能地使密切相关的控件使用成组框控件,并可以
通过设置成组框的标题来说明控件的用途 。
下压按钮(Button):按钮是一种特殊的矩形窗口,按钮可以响应单击或双击动作。 可以
把按钮分为三类型:下压按钮、复选按钮和单选按钮控件。 下压按钮是最常见的,如对话框
的确认、取消按钮都是下压按钮。MFC 提供了 CButton 类支持按钮控件。
复选框(Check Box):用来显示某种可能的选择,该项选择是独立的,用户可以选中或取
消该选项。在选项被选中的时候复选标记出现,选择被取消时复选标记消失。 在 MFC 中由
CButton 类对复选框进行支持,用户可以通过 SetCheck 函数和 GetCheck 函数设置获取复选
框当前的状态。
单选按钮(Radio Button):用来选择某种可能的选项,与复选按钮不同,该选项不是独立
的。一般的情况是,几个单选按钮组成一组,同组中的单选按钮可以有也只能有一个按钮被
选中。MFC 同样使用 CButton 类对单选按钮控件进行支持,SetCheck 函数和 GetCheck 函数
对单选按钮也是适用的。
列表框(List Box):用来选择一系列的可能选项,用户通过滚动条可以浏览这些选项。
在列表框中,可以进行单项选择,也可以进行多项选择,这取决于用户在控件属性对话框中
的设置。MFC 提供了 CListBox 类对列表框控件进行支持。
组 合 框 (Combo Box):列 表 框 和 编 辑 框 的 组 合 ,除 了 可 以 在 列 表 中 对 已 经 存 在 的 选
项 进 行 选 择 外 , 还 可 以 输 入 新 的 选 项 。 MFC 提 供 了 CComboBox 类 对 组 合 框 控 件 进 行
支持。
滚动条(Scroll Bar):这包括水平滚动条和垂直滚动条,除了在视觉效果上方向不同外,
水平滚动条在被滚动时发送 WM_HSCROLL 信息,而垂直滚动条在被滚动时发送WM_VSCROLL信
息。MFC 提供了 CScrollBar 类进行支持。
微调按钮(Spin Button):包括一对紧靠在一起的上下箭头,使用微调按钮可以增大或者
减小某个特定的数值。微调按钮往往都需要一个 “伙伴”控件,这通常都是一个编辑框。 当
微调按钮 的 向 上 箭 头 被 单 击 时, 编 辑 框 中 的 数 字 就 增 大; 反 之 则 减 小。 MFC 提 供 了
CSpinButtonCtrl 类进行支持。
进度条(Progress):在进行一项需要占用较长时间的操作时用来反应当前的操作进度 。
当操作的进度不断前进时,进度条就用特定颜色填充进度条框。 用户可以设置进度条的范
围和当前所在位置。MFC 提供了 CProgressCtrl 类进行支持。
滑块控件(Slider):通常用来在程序中接受一系列离散的数值。 用户可以设置滑块控
件的取 值 范 围, 并 可 以 为 控 件 加 上 刻 度 标 记 以 显 示 特 定 位 置 的 含 义。 MFC 提 供 了 129

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

CSliderCtrl 类进行支持。
热键控件 (Hot Key):热键控件看起来就像一个编辑框,但是用热键控件能够立刻反映
用户刚刚按下的键组合,这在设置程序的热键时特别有用 。 热键控件只是在 “视觉 ”上显
示了按键组合,设 置 热 键 的 工 作 还 需 要 用 户 添 加 代 码 完 成 。 MFC 提 供 了 CHotKey 类 进 行
支持 。
列 表 控 件 (List Control):按 一 定 的 排 列 顺 序 显 示 一 系 列 带 图 标 的 字 符 串 。 列 表
控 件 提 供 了 四 种 显 示 模 式 :大 图 标 、小 图 标 、列 表 和 详 细 信 息 。 用 户 可 以 向 列 表 控 件
中 添 加 新 的 项 , 也 可 以 选 择 列 表 控 件 的 显 示 模 式 。 MFC 提 供 了 CListCtrl 类 进 行
支持。
树形控件(Tree Control):用来显示一系列项目的层次关系,最典型的例子就是显示磁
盘上的文件和文件夹。如果有子项的话,单击树形控件中的项目可以展开或者收缩其子项 。
MFC 提供了 CTreeCtrl 类进行支持。
属性列表控件(Tab Control):属性列表控件用来包含大量的控件,可以满足用户显示或
获取大量数据的要求。每个属性表又可分为好几个属性页,这些属性页根据各自的标签进
行区分,这些属性页中都可以包容其他控件 。在显示属性列表的时候,一次只能够显示一个
属性页的全部内容,同时显示属性页的标签,用户通过单击标签打开相应的属性页。 MFC 提
供了 CTabCtrl 类进行支持。
动画控件(Animation):用来播放一段 AVI 格式的视频剪辑。用户可以控制视频剪辑的
播放、停止和定位,但仅限于这些功能。 动画控件甚至不能播放音频剪辑,如果用户需要更
高层次的视频或音频支持,请选用 MCIWnd 控件。MFC 提供了 CAnimateCtrl 类对动画控件进
行支持。
高级编辑框(Rich Edit):很显然,高级编辑框是编辑框控件功能的扩展 。在高级编辑框
中,除了简单地输入和编辑字符串外,用户还可以为字符或段落指定特定的格式,用户甚至
还可以向高级编辑框中插入 OLE 项。高级编辑框基本上实现了一个带格式的文本编辑器的
功能,而只需用户添加少量的接口。MFC 提供了 CRichEditCtrl 类进行支持。
日历控件 (Month Calender):日历控件看起来与一个真正的日历很相似,操作起来也是
类 似 的。 这 样 就 很 直 观 地 为 用 户 提 供 了 观 察 和 显 示 日 期 的 途 径 。 MFC 提 供 了
CMonthCalCtrl 类进行支持。
日期/时间选择器(Date Time Picker):日期/时间选择器向用户提供了一种直观的选择
日期和时间的方法。日期/时间选择器在外观上类似于一个组合框,但是当用户单击下拉箭
头时就会展开一个日历控件供用户进行选择,而一旦用户做出了选择,日期/时间选择器就
会自动显示新的日期/时间。MFC 提供了 CDateTimeCtrl 类进行支持。
IP 地址控件 (IP Address):IP 地址控 件 用 来 输 入 和 编 辑 IP 地 址 。 该 控 件 外 观 类 似
于一个编辑框,但是可以自动对输入的 字 符 按 三 个 一 组 进 行 分 区 和 加 间 隔 圆 点 。 IP 地
址控件为开发支持 Internet 技术的程 序 提 供 了 方 便 。 MFC 提 供 了 CIPAddressCtrl 类 进
行支持 。
扩展组合框(Extended Combo Box):扩展组合框在普通组合框的基础上还支持图像列
表。也就是说,可以在组合框中显示特定的图标表示相应的选择,而不仅仅是显示文本。
MFC 提供了 CComboBoxEx 类进行支持。
130 有关控件的应用将在程序实例中加以介绍 。
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

5 对话框类与对话框调用


51 对话框类
CDialog 类是所有对话框类的基类。 在 VC60 中,可利用对话框编辑器创建对话框资
源,并将其保存到资源中,然后利用 ClassWizard 创建 CDialog 派生类。
CDialog 类的继承关系见图 6 5。

图6 5 CD
ial
og类继承关系

CDialog 类的主要成员函数见表 6 6。

表6 6 CD
ial
og类的主要成员函数

成员函数 返回值 说 明

CDialog 构造 CDialog 对象
初始化 CDialog 对 象;创 建 非 模 式 对 话 框 并 将 其 连 接 到 CDialog
Create BOOL
对象
CreateIndirect BOOL 从内存中(非基于资源)的对话框模板中创建非模式对话框
DoModal int 调用(显示)模式对话框,完成后返回一整型数值
EndDialog void 关闭模式对话框
GotoDlgCtrl void 将焦点移至对话框中指定的对话框控件
GetDefID DWORD 获取对话框缺省按钮控件的 ID
从内存中(非基于资源)的对话框模板中创建模式对话框。保存参
InitModalIndirect BOOL
数直到调用 DoModal 函数
判断给定消息是否是非模式对话框所期望的消息,如果是,则处理
IsDialogMessage BOOL
此消息
MapzDialogRect void 将矩形的对话框单位转换为屏幕单位
NextDlgCtr void 将焦点移至对话框中下一对话框控件
重载该函数以实现“Cancel”按钮或 Esc 键的特定动作。缺省实现为
OnCancel void
关闭对话框,并且 DoModal 返回 IDCANCEL
OnInitDialog BOOL 重载该函数以进一步初始化对话框
重载该函数以实现“OK”按钮的特定动作。缺省实现为关闭对话框,
OnOk void
并且 DoModal 返回 IDOK
OnSetFont void 重载该函数以实现模式对话框控件绘制文本时使用的字体
PrevDlgCtrl void 将焦点移至对话框中上一对话框控件
SetDefID void 将对话框中缺省的按钮控件设置为指定按钮
SetHelpID void 设置对话框的与环境相关的帮助 ID


52 对话框调用及其编程实例
对模式对话框,调用 DoModal 函数加载对话框模板,显示对话框并管理与对话框对象的 131

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

交互,直到用户单击“确定”或“取消”按钮。
对非模式对话框,调用 Create 函数加载对话框模板。若对话框模板具有 WS_VISIBLE 风
格。则立即显示对话框,否则须调用 ShowWindow 成员函数显示对话框。
【例6 2】 Ex06_2 对话框模式与无模式调用,具体说明见表 6 7。
表6 7 例6 2具体说明

项 目 图示和说明

程序界面

图(
a) 菜单 图(
b) 模式对话框 图(
c) 模式对话框

点击菜单:对话框调用- >模式对话框...,弹出图(b),对话框关闭前不能进行其他操作
程序功能
点击菜单:对话框调用- >非模式对话框...,弹出图(c),对话框关闭前可以进行其他操作

(1)新建一单文档应用程序 Ex06_2
(2)添加模式对话框和非模式对话框资源
① 插入对话框
在 Workspace 窗口的 ResourceView 的资源列表中,鼠标放在 Dialog 文件夹,右击鼠标,在
快捷菜单中选择“ Insert Dialog”选项,此时在资源列表中新建名为“IDD_DIALOGl”的对话框资
源,对话框资源编辑器随之启动,将新对话框“ 调入其中,如图 6 6 所示。
IDD_DIALOGl”

图6 6 新建对话框资源

② 设置对话框属性
在新对话框的空白处 (注意不要在控件上 )单击鼠标右键,在弹出的快捷菜单中选择
“Properties ”命 令, 打 开 “Dialog Properties ”对 话 框, 如 图 6 7 所 示。 “Dialog
Properties”对话框中共有 5 个标签。本例将 General 标签中的 ID 号设置为 IDD_MODELDLG。
Caption 设置为“模式对话框”,对话框所使用的字体和字号,需要通过单击 “Font”按钮显示
的“Font”对话框来设置。
同样插入另一对话框,对话框 ID 号为:IDD_ MODELESSDLG;Caption 设置为 “非模式对
话框”。
132
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

图6 7 D
ial
ogPr
ope
rti
es对话框

(3)添加控件
在两个对话框中各添加一静态文本控件 。添加的步骤如下 (以 IDD_MODELDLG 对话框为
例说明):
① 鼠标移动到“Controls”工具栏的静态文本控件图标上,按下鼠标左键,将静态文本框
拖动到 IDD_MODELDLG 对话框模板的适当位置;
② 选中该静态文本框按下鼠标右键从弹出的快捷菜单中选择 Properties 选项可打开
图 6 8 所示的“Text Properties”对话框。

图6 8 Te
xtPr
ope
rti
es对话框

③ 在 Caption 中输入“模式对话框!”,ID 号不变。


(4)创建对话框类
在对话框编辑器中,双击“ 对话框,因为该对话框资源是新建的资源,所以显
IDD_MODELDLG”
示如图 6 9 所示的 “Adding a Class”对话框。 单击 “OK”按钮,显示如图 6 10 所示的 “New
对话框,在类名 Name 编辑框中输入 CModelDlg,单击“
Class” 按钮,即创建了“
OK” 类。
CModelDlg”

图6 9 Add
ingaC
las对话框
s 图6 10 NewC
las对话框

同理在对话框编辑器中,双击 “IDD_MODELESSDLG”对话框,在 “New Class”对话框的类名 133



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

Name 编辑框中输入 CModelessDlg,单击“OK”按钮,即创建了“CModelessDlg”类。


(5)编辑菜单资源和添加菜单消息函数
利用菜单资源编辑器,编辑例题中要求的菜单,菜单 ID 号及菜单消息函数见表 6 8。
表6 8 菜单消息函数

菜 单 ID 号 消 息 消息函数
模式对话框... ID_SHOWDLG_MODELDLG COMMAND OnShowdlgModeldlg()
非模式对话框... ID_SHOWDLG_MODELESSDLG COMMAND OnShowdlgModelessdlg()

(6)实现模式调用
① 编写模式调用消息处理函数 OnShowdlgModeldlg(见清单 6 2)。
② 在 Ex06_2View.cpp 文件开头中,添加# include" ModelDlg.h" (见清单 6 2)。
(7)实现非模式调用
① 编写非模式调用消息处理函数 OnShowdlgModelessdlg(见清单 6 2)。
② 在 Ex06_2View.h 文件开头,添加# include" ModelessDlg.h" (见清单 6 3)。
③ 在 Ex06_2View.h 文件中,定义 CModelessDlg 类的指针 pdlg(见清单 6 3)。
④ 在 Ex06_2View.cpp 构造函数中初始化 pdlg(见清单 6 2)。
⑤ 在 Ex06_2View.cpp 析构函数中删除 pdlg(见清单 6 2)。
06_
清单6 2 构造函数(在 Ex 2Vi
ew.
cpp文件中)


# include" ModelDlg.h"

CEx 06_2View CEx06_ 2V
i //构造函数
ew()
{ pdlg = NULL;

CEx06_2View~CEx 06_2V ew()
i //析构函数
{ delete pdlg;//删除 pdlg,释放内存

vo
idCEx 06_2View OnShowd l
gMo dedg()
l //点击“模式对话框...”菜单消息函数
{ CModelDlg modeldlg;//定义 CModelDlg 对话框类对象
modeldlg.DoModal();//显示模式对话框

vo
idCEx 06_2ViewOnSh owdlgMo d
eles
s d
l //点击“非模式对话框...
g() ”菜单消息函数
{ pdlg = new CModelessDlg(this);
pdlg- >Create(IDD_MODELESSDLG);
pdlg- >ShowWindow(SW_RESTORE);

06_
清单6 3 包含头文件和定义变量(在 Ex 2Vi h文件中)
ew.

# include" ModelessDlg.h"

lassCEx 06_ 2Viewpub l
icCV i
ew

private:
CModelessDlg * pdlg;
134 
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

(8)编译和运行

6 对话框数据交换与验证机制

程序对话框数据交换(DDX, Dialog Data Exchange)用于初始化对话框中的控件并获取
用户的数据输入,而对话框数据验证(DDV, Dialog Data Validation)则用于验证对话框中数
据输入的有效性。MFC 在每个对话框类中提供了一个用于重载的虚函数 DoDataExchange 来
实现对话框数据交换和验证工作 。


61 对话框数据交换
如果使用 DDX 机制,则通常在 OnInitDialog 程序或对话框构造函数中设置对话框对象成员变
量的初始值。在对话框即将显示前,应用程序框架的 DDX 机制将成员变量的值传输给对话框中的
控件,当对话框响应 DoModal 或 Create 而被显示时,对话框控件将“ 显示”这些值。CDialog 类中的
OnInitDialog 函数默认时将调用 CWnd 类的 UpdateData 成员函数初始化对话框中的控件。
UpdateData 函数的原型如下:
BOOL UpdateData(BOOL bSaveAndValidate = TRUE);
参数 bSaveAndValidate 是一个标志位,如果 bSaveAndValidate 为 TRUE,表示将对话框
控件中界面数据传给程序代码的成员变量;如果 bSaveAndValidate 为 FALSE,表示将类中成
员变量数据状态传递给对话框及其控件界面 。


62 对话框数据验证
除了调用 DDX 参数指定数据交换外,用户还可以使用 DDV 函数进行对话框数据验证。在调
用控件的 DDX 函数后,必须立即调用该控件的 DDV 函数。大部分 DDV 函数的原型如下所示:
DDV_MinMaxCustom(pDX,Data,MinData,MaxData);
其中,参数 pDX 是一个指向 CDataExchange 对象的指针,参数 Data 中存放着即将被验证
的数据,后两个参数用于定制数据的范围 。


63 四则运算编程实例
【例6 3】 Ex06 3 四则运算的实现,具体说明见表 6 9。
表6 9 例6 3具体说明

项 目 图示和说明

图(
a) 菜单
程序包含的控件有:
程序界面
3 个编辑控件、6 个下压按钮、
3 个静态文本控件以及 2 个成组控件。

图(
b) 四则运算对话框
135

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
项 目 图示和说明

(1)点击菜单:对话框- >四则运算...,弹出四则运算对话框
程序功能
(2)在四则运算对话框的 num1、num2 编辑框中输入数据,然后点击+、-、*、
/按钮即进行相应的
计算,并将计算结果输出到 result 编辑框

(1)新建一单文档应用程序 Ex06_3
(2)添加对话框资源
插入对话框,打开 “
Dialog Properties”
对话框,将 General 标签中的 ID 号设置为
IDD_CALDLG。 Caption 设置为 “四 则 运 算
对话框”。
(3)添加控件 图6 11 Ed
itPr
ope
rti
es对话框
① 添加编辑控件;
添加 3 个编辑控件,分别用于 num1、num2 及 result 的文本说明。 打开图 6 11 所示的
“Edit Properties”对话框,对话框中的有关属性见表 6 10。
表6 10 编辑控件的属性

属性项 说 明
ID 指定控件的标识符
Visible 指定控件最初可见
Disable 指定控件最初被禁用
Group 指定基于 Tab 键顺序的一组控件中的第一个控件
TabStop 指定用户可以使用 TAB 键移动到该控件
ID Help 为控件分配基于资源 ID 的帮助 ID
Align Text 对齐文本,可以为左、居中、右
Multiline 如果控件文本对于控件的宽度而言太长,则将文本折为多行
Number 编辑框中只能输入数字
Horizontal Scroll 指定控件将具有水平滚动条
Auto HScroll 用户在行尾键入字符时,自动使文本滚动到左侧
Vertical Scroll 指定控件将具有垂直滚动条
Auto VScroll 当用户在最后一行按 Enter 键时,自动使文本向上滚动
Password 对输入到编辑控件中的每个字符显示星号
No Hide Selection 设定编辑控件将总是显示某选定内容,即使它没有焦点
OEM Convert 将输入到编辑框中的文本转换为 OEM 字符集
Want Enter 指定编辑控件要接收 Enter 键
Border 指定控件将具有窄边框
UpperCase 在编辑控件中输入字符时,将所有字符转换为大写
LowerCase 在编辑控件中输入字符时,将所有字符转换为小写
Read Only 防止用户在编辑框控件中输入或编辑文本

② 添加下压按钮控件;
136 添加 4 个下压按钮,分别用于+、-、*、
/的计算。 打开 “Push Button Properties”对话框,
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

如图 6 12 所示,共有 3 个属性设置标签,与前面控件不同的有关属性见表 6 11。

图6 12 Pu
shBu
tto
nPr
ope
rti
es对话框

表6 11 下压按钮控件的属性

属性项 说 明
Default button 指定控件是对话框的默认命令按钮
Owner Draw 指定控件为所有者描述的按钮
Horizontal alignment 指定控件的水平对齐方式:默认、右、左、居中
Vertical alignment 指定控件的垂直对齐方式:默认、底、顶、中间

③ 添加静态文本控件;
添加 3 个静态文本控件,静态文本控件与前面控件不同的属性见表 6 12。
表6 12 静态文本控件的属性

属性项 说 明
No Prefix 防止“and”符()被解释为键盘助记键
No Wrap 指定文本不换行
Simple 指定控件显示单行左对齐文本
Sunken 指定控件具有半凹陷边框

④ 添加成组框控件。
添加 2 个成组框控件, 打开 “Group Box Properties”对话框,如图 6 13 所示。 无需改
变 General 标签中的 ID 号。

图6 13 Gr
oupBo
xPr
ope
rti
es对话框

(4)创建对话框类
在对话框编辑器中,双击“四则运算对话框 ”对话框,添加新类;在 “New Class”对话框的
类名 Name 编辑框中输入 CCalDlg。单击“OK"按钮。即创建了“CCalDlg”类。
(5)添加成员变量
添加成员变量步骤如下:
137

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

① 显示“MFC ClassWizard”对话框,选择其中的“Member Variables”标签(如图 6 14)。


② 在“Class name”组合框中选择 “CCalDlg”类,表示为该类定义成员变量。“Control
IDs”列表框列出 IDD_CALDLG 对话框中所添加控件的 ID 号。
③ 如选择 ID_EDIT_NUM1,然后点击“Add Variable”按钮,将显示如图 6 15 所示的 “Add
Member Variable”对话框(双击该 ID 号也可调出此对话框)。
④ 在 “Member variable name ”中 定 义 数 据 成 员: 输 入 成 员 变 量 名 为: m _ num1; 在
“Category”中选择 Value;在“Variable type”中选择 double,表示双精度值。点击“OK”按钮
即为 ID_EDIT_NUM1 控件定义了一个成员变量 m_num1。
⑤ 成员变量定义完成后见图 6 14。单击“OK”按钮完成定义。

图6 14 利用 MFCC
las
sWi
zad添加成员变量
r 图6 15 AddMemb
erVa
ria
ble对话框

⑥ 添加数据验证,在图 6 14 的 Minimum Value 和 Maximum Value 处输入 m_num1 的最小


和最大值。数据验证可不在此输入,用户可自己编程加以限制。
按上面的方法添加其他各控件的数据成员,各数据成员如表 6 14 所示。
表6 14 添加的控件和成员变量

控 件 标题(Caption) ID 号 成员变量 变量类型


静态文本 num1 IDC_STATIC
静态文本 num2 IDC_STATIC
成组框 数据 IDC_STATIC
成组框 计算 IDC_STATIC
编辑框(num1) IDC_EDIT_NUM1 m_num1 double
编辑框(num2) IDC EDIT_NUM2 m_num2 double
编辑框(result) IDC_EDIT_RESULT m_result double
单选按钮 + IDC_BUTTON_ADD int
单选按钮 - IDC_BUTTON_MINUS
单选按钮 * IDC_BUTTON_MULTPLY
单选按钮 / IDC_BUTTON_DIVIDE
按钮 OK IDOK
按钮 Cancel IDCANCEL

138 添加完成员变量后,Class Wizard 自动产生一些初始化对话框中控件的代码以及对话


第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

框数据交换和验证的代码,见清单 6 4。
清单6 4 对话框数据交换和验证(在 Ca
lDl
g.pp文件中)

CCa
lD l
g CCa lDlg(CWnd pPar
ent/=NULL/):CD
ial
og(
CCa
lDl
g
IDDpPa
ret)
n //构造函数

//{{AFX_DATA_INIT(CCalDlg)
m_num1 = 00;
m_num2 = 00;
m_result = 00;
//AFX_DATA_INIT


oidCCalDlgDoDataEx c
ha n
ge(CDa taExchang //对话框数据交换
e pDX)

CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CCalDlg)
DDX_Text(pDX, IDC_EDIT_NUM1, m_num1);
DDV_MinMaxDouble(pDX, m_num1,-01, 100.);
DDX_Text(pDX, IDC_EDIT_NUM2, m_num2);
DDX_Text(pDX, IDC_EDIT_RESULT, m_result);
//}}AFX_DATA_MAP

(6)添加单击下压按钮的消息函数
本例需要添加 4 个单击按钮(+、-、*、
/)的消息函数。
使用如图 6 16 所 示 的 ClassWizard 的 Message Maps 标 签 添 加 消 息 函 数, 首 先 在
Object IDs 列表框中选择要添加消息处理的控件 (如 ID_BUTTON_ADD),然后在 Messages 列
表框中选择要添加的消息(如 BN_CLICKED 即单击按钮),再单击 Add Function 按钮弹出图中
Add Member Function 的对话框,消息函数名为 OnButtonAdd,点击“OK”按钮,即完成消息函数
OnButtonAdd ()的添加工作。按此方法添另 3 个消息函数,各消息函数见表 6 15。

图6 16 添加消息函数
139

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表6 15 “四则运算对话框”消息函数

标题(Caption) ID 号 消 息 消息函数
+ IDC_BUTTON_ADD BN_CLICKED OnButtonAdd
- IDC_BUTTON_MINUS BN_CLICKED OnButtonMinus
* IDC_BUTTON_MULTIPLY BN_CLICKED OnButtonMultiply
/ IDC_BUTTON_DIVIDE BN_CLICKED OnButtonDivide

(7)编写消息函数
按上述方法添加完消息函数后,还要编写具体的函数代码。清单 6 5 列出了两个消息
函数的代码清单。
清单6 5 消息函数(在 Ca
lDl
g.pp文件中)


oidCCalDlgOnBu t
tonAdd()
{ UpdateData(true);//将界面数据传给成员变量
m_result = m_num1+ m_num2;
UpdateData(false);//将成员变量数据传给界面


oidCCalDlgOnBu t
tonMinus()
{ UpdateData(true);
m_result = m_num1- m_num2;
UpdateData(false);


oidCCalDlgOnBu t
tonMu l
til
py()
{ UpdateData(true);
m_result = m_num1* m_num2;
UpdateData(false);


oidCCalDlgOnBu t
tonDivi
de()
{ UpdateData(true);
if(m_num2 = = 0)
{ AfxMessageBox("除数不能为零,请重新输入 m_num2!" );
return;

m_result = m_num1/m_num2;
UpdateData(false);

(8)对话框调用(模式对话框)
对话框调用步骤如下:
① 定义菜单选项
在菜单编辑器中,创建“对话框”菜单,在“对话框”菜单中添加“四则运算...”菜单选项。
② 添加和编写菜单命令消息函数。
使用 ClassWizard 在“CEx06_3View”中添加菜单对象 “ID_DIALOG_CAL”的单击命令消息
控制函数 OnDialogCaldg(),见清单 6 6。

③ 添加 CalDlg.h 的说明。
在 Ex06_3View.cpp 文件中涉及 CCalDlg 类,因此需要将其头文件 CalDlg.h 的说明添加
140 到文件中。
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

06_
清单6 6 菜单调用对话框(在 Ex 3Vi
ew.
cpp文件中)

#inc
lude" Ex06_3View.h"
#inc
lude" CalDlg.h"//包含对话框头文件


oidCEx 06_3V i
ew OnDi a
logCa
ldg()

{ CCalDlg dlg;
dlg.DoModal();//模式调用四则运算对话框

(9)编译和运行

7 常用控件应用


71 常用控件编程实例一
【例6 4】 Ex06_4 常用控件应用一,具体说明见表 6 16。
(1)新建一单文档应用程序 Ex06_4
(2)添加对话框资源
插入对话框,打开“Dialog Properties”对话框,将 General 标签中的 ID 号设置为 IDD_
STUMSGDLG。Caption 设置为“学生信息”。
(3)添加控件
① 添加 5 个静态文本控件。
② 添加 3 个成组框控件。
③ 添加 3 个编辑控件,分别用于输入姓名、学号和电话。
④ 添加 1 个微调 CSpin 控件用于调整学号。
表6 16 例6 4具体说明

项目 图示和说明

对话框包括的控件有:
3 个编辑控件、
5 个静态文本控件、
1 个组合框控件、
程序 1 个列表框、
界面 2 个单选按钮、
2 个复选按钮、
2 个下压按钮
以及 3 个成组控件。

(1)点击菜单:登记- >学生信息...弹出学生信息对话框
程序
(2)在学生信息对话框中按图中信息进行输入和选择,然后点击“OK”按钮,程序将关闭对话框,并
功能
将对话框信息传递到视图中显示
141

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

微调控件的属性选择见图 6 17,属性含义说明见表 6 17。

图6 17 微调控件的属性选择

表6 17 微调控件的属性

属性项 说 明
Alignment 设置控件与其伙伴窗口的位置关系
Arrow 通过按箭头键增加或减少微调控件的位置
No Thousands 指定不在每三位十进制之间插入千位分隔符
Wrap 如果增加或减小位置时超出该范围,则使位置回绕
Auto Buddy 自动按照顺序选择上一个控件作为微调控件的伙伴窗口
Hot Track 当光标移过控件时,突出显示控件的箭头按钮
Set Buddy Integer 指定微调控件在它的位置改变时设置伙伴窗口的文本

⑤ 添加 1 个列表控件用于输入班级。
列表控件的属性选择见图 6 18,属性含义说明见表 6 18。

图6 18 列表控件的属性选择

表6 18 列表框的属性

属性项 说 明
Disable No Scroll 控件中一直显示滚动条
No Integral Height 指定组合框的大小正好是由应用程序指定的大小
No Redraw 指定在进行更改后不更新列表框的外观
Vertical Scrollbar 指定列表框将具有垂直滚动条
Has Strings 指定用户描述的组合框中包含由字符串组成的项
MultiColumn 指定水平滚动的多列列表框
No Data 指定所描述的列表框不包含任何数据
Selection 可以为单项、多项、扩展、无之中的一种
Sort 对添加到列表框的字符串进行自动排序
指定如果 在 列 表 框 具 有 焦 点 时 按 下 某 个 键, 列 表 框 的 所 有 者 将 接 到
Want Key Input
WM_VKEYTOITEM消息

⑥ 添加 1 个组合框控件用于输入学院。组合框控件的属性选择见图 6 19。
142
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

在“Data”标签中添加各学院名称,注意输入一个学院后,使用 “Ctrl+ Enter”键换行。 组


合框中组合选项也可采用组合框类的成员函数 AddString()添加。
组合框控件是由列表控件和静态控件 (或编辑控件)组成的,其属性含义可参考相应控
件的属性。
⑦ 添加 2 个单选按钮,以用作性别说明。单选按钮的属性选择见图 6 20。
在一组单选按钮中,第一个单选按钮的属性对话框中选中复选按钮 Group,这一组其他
单选按钮都不要选中这一项。

图6 20 单选按钮的属性选择

图6 19 组合框控件的属性选择 图6 21 复选按钮的属性选择

⑧ 添加 2 个 复 选 按 钮, 以 用 作 已 学 先 修 课 程 说 明 。 复 选 按 钮 的 属 性 选 择 见 图
6 21。
按钮可分为三类:下压按钮、单选按钮和复选按钮,这几个按钮的特性基本一致,见下压
按钮部分。
(4)创建对话框类
类的创建过程同前,在新类 “New Class”对话框中:输入类名为:CStumsgDlg;选择基类
为:CDialog;Dialog ID 为:IDD_STUMSGDLG。
(5)添加成员变量
用“MFC ClassWizard”添加成员变量, CStumsgDlg 类的成员变量见表 6 19。
表6 19 各控件的ID 号和成员变量

控 件 标题(Caption) ID 号 成员变量 变量类型


静态文本 姓名 IDC_STATIC
静态文本 学号 IDC_STATIC
静态文本 电话 IDC_STATIC
静态文本 学院 IDC_STATIC
静态文本 班级 IDC_STATIC
143

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
控 件 标题(Caption) ID 号 成员变量 变量类型
成组框 性别 IDC_STATIC
成组框 已学先修课 IDC_STATIC
(姓名)编辑框 IDC_NAME m_name CString
学号编辑框 IDC_NUMBER m_number CString
电话编辑框 IDC_TEL m_tel CString
微调控件 IDC_SPIN_NUMBER m_ctrlspin CSpinButtonCtrl
m_college CString
组合框 IDC_COLLEGE
m_ctrlcollege CComboBox(控制)
m_banji CString
列表框 IDC_BANJI
m_ctrlbanji CListCtrl(控制)
m_men int
单选按钮 男 IDC_MEN
m_ctrlmen CButton
单选按钮 女 IDC_WOMEN
复选按钮 C 语言程序设计 ID_CHECK_CPROGRAM m_bcprogram BOOL
复选按钮 软件基础 ID_CHECK_SOFTWARE m_bsoftware BOOL
下压按钮 OK IDOK
下压按钮 Cancel IDCANCEL

(6)添加消息函数
添加的改变组合框选项的消息函数 OnSelchangeCollege()(按图 6 22 所示选择添加)
和初始化对话框的函数 OnInitDialog()(按图 6 23 所示选择添加)。

图6 22 添加 OnS
elc
han
geCo
lle
ge()函数

(7)编写对话框类成员函数
① 重载初始化对话框函数(见清单 6 7)
144
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

图6 23 添加 On
InD
ial
og()函数

② 改变组合框选项的消息处理函数(见清单 6 7)
清单6 7 On
Ini
tDi
alg和 OnS
o elc
han
geCo
lle
ge函数(在 S
tums
gDl
g.pp文件中)

BOOLCS t
ums gDlgOn In
itDialg()
o //初始化对话框

CDialog::OnInitDialog();
m_ctrlcollege.SetCurSel(0);//将组合框的第一项,即机械工程学院显示在界面上
m_ctrlspin.SetRange(00001,10000);//设置微调控件的调节范围
//添加机械工程学院的班级到列表框
m_ctrlbanji.AddString("机设 021");
m_ctrlbanji.AddString("机设 022");
m_ctrlbanji.AddString("机设 023");
m_ctrlbanji.AddString("机设 024");
m_ctrlmen.SetCheck(1);//初始化性别为男
return TRUE;//return TRUE unless you set the focus to a control
//EXCEPTION: OCX Property Pages should return FALSE


oidCStumsgD l
gOnSelchangeCol
lege()//改变组合框选项

switch(m_ctrlcollege.GetCurSel())

case 0://机械工程学院
{ m_ctrlbanji.ResetContent();
m_ctrlbanji.AddString("机设 021");
m_ctrlbanji.AddString("机设 022");
m_ctrlbanji.AddString("机设 023");
m_ctrlbanji.AddString("机设 024");
break;

145

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

case 1://信息学院
{ m_ctrlbanji.ResetContent();
m_ctrlbanji.AddString("信息 011");
m_ctrlbanji.AddString("信息 012");
m_ctrlbanji.AddString("信息 013");
m_ctrlbanji.AddString("信息 014");
break;

case 2://化工学院
{ m_ctrlbanji.ResetContent();
m_ctrlbanji.AddString("化工 011");
m_ctrlbanji.AddString("化工 012");
m_ctrlbanji.AddString("化工 013");
m_ctrlbanji.AddString("化工 014");
break;


(8)调用学生信息对话框,并将学生信息对话框的数据在界面上显示
① 编辑菜单资源。
学生信息菜单 ID 号:ID_RECODE_STUMSG
② 在 Ex06_4Doc.h 头文件中定义成员变量(见清单 6 8)。
06_
清单6 8 定义成员变量(在 Ex 4Do
c.h头文件中)


las
sCEx 06_4Docpub licCDo
cume
nt
 public:
BOOL m_bsoftware;
BOOL m_bCProgram;
int m_men;
CString m_banji;
CString m_college;
CString m_tel;
CString m_number;
CString m_name;

③ 添加和编写菜单消息处理函数,见清单 6 9。
④ 在 Ex06_4Doc.cpp 文件的开头添加 # include" StumsgDlg.h" (见清单6 9)。
清单6 9 OnRe
cod
eSt 06_
u 函数(在 Ex 4Do
c.pp文件中)

#inc
lude“StumsgD l
g.h”


oidCEx 06_4Do cOnRe codeStu()
{ CStumsgDlg dlg;
//将学生信息对话框的数据传给文档
if(dlg.DoModal()= = IDOK)
146
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

{ m_name = dlg.m_name;
m_number = dlg.m_number;
m_tel = dlg.m_tel;
m_college = dlg.m_college;
m_banji = dlg.m_banji;
m_bCProgram = dlg.m_bCProgram;
m_bsoftware = dlg.m_bsoftware;
m_men = dlg.m_men;

UpdateAllViews(NULL);

⑤ 重载 OnDraw 函数,将文档中学生信息的数据在界面上显示(见清单 6 10)。


清单6 10 OnDr 06_
aw 函数(在 Ex 4Vi
ew.
cpp文件中)


oidCEx 06_ 4ViewOnDr aw( CDCpDC)
{ CEx06_4Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
//TODO: add draw code for native data here
pDC- >TextOut(20,20,pDoc- >m_name);
pDC- >TextOut(80,20,pDoc- >m_number);
pDC- >TextOut(200,20,pDoc- >m_tel);
pDC- >TextOut(280,20,pDoc- >m_college);
pDC- >TextOut(400,20,pDoc- >m_banji);
switch(pDoc- >m_men)

case 0:
 pDC- >TextOut(150,20+i," 男");
break;

case 1:
 pDC- >TextOut(150,20+i," 女");
break;


if(pDoc- >m_bCProgram)
{ pDC- >TextOut(500,20+i," C 语言程序设计");

if(pDoc- >m_bsoftware)
{ pDC- >TextOut(650,20+i," 软件基础");

(9)编译和运行


72 常用控件编程实例二
【例6 5】 Ex06_5 控件的应用二,具体说明见表 6 20。

147

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表6 20 例6 5的具体说明

项 目 图示和说明

对话框包括的控件有:
3 个静态文本控件、
程序界面 1 个组合框控件、
5 个单选按钮、
1 个进度条控件、
1 个滑块控件和
1 个滚动条控件。

(1)点击开始进度条按钮,进度条从 0 开始向前移动;点击停止进度条按钮,进度条停止向
前移动;点击继续进度条按钮,进度条从停止的位置开始继续向前移动;用文字显示
程序功能 进度
(2)拖动滑块条,用文字显示滑块位置
(3)拖动滚动条,用文字显示滚动位置

(1)建立一基于对话框的应用程序:Ex06_5
程序自动创建一 ID 号为 IDD_EX06_5_DIALOG 的对话框模板
(2)设置对话框属性
打开 IDD_Ex06_5_DIALOG 的对话框模板的“Dialog Properties”对话框,将 General 标签
的 Caption 设置为“Ex06_5(控件)”。
(3)添加控件
① 添加 1 个进度条控件,进度条控件的属性选择见图 6 24。
② 添加 1 个滑块条控件,滑块条控件的属性选择见图 6 25,属性含义见表 6 21。

图6 24 进度条控件属性 图6 25 滑块条控件属性

表6 21 滑块条的属性

属性项 说 明
Auto Ticks 指定滑块控件在其值范围内对于每个增量都有一个刻度
Enable Selection 指定滑块控件只显示选择范围
Orientation 指定滑块控件的方向:水平或垂直
Point 指定滑块箭头的指向
Tick Marks 指定滑块显示刻度线
Tooltips 滑块支持工具提示

③ 添加 1 个滚动条控件。
148 滚动条控件的属性选择见图 6 26。
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

④ 添加 3 个静态文本控件。
⑤ 添加 1 个成组框。
⑥ 添加 3 个下压按钮控制进度条移动、
停止和继续移动。
(4)定义成员变量(用 ClassWizard)
各控件 ID 号与成员变量见表 6 22。 图6 26 滚动条控件属性

表6 22 控件ID 号与成员变量

控 件 标 题 ID 号 成员变量 变量类型
静态文本 进度条 IDC_STATIC
静态文本 滑块条 IDC_STATIC
静态文本 滚动条 IDC_STATIC
成组框 控件 IDC_STATIC
进度条 IDC_PROGRESS1 m_ctrlProgress CProgressCtrl
m_ns int
滑块条 IDC_SLIDER1
m_ctrlSlider CSliderCtrl
滚动条 IDC_SCROLLBAR1 m_ctrlScrollbar CScrollBar
下压按钮 开始进度条 IDC_BUTTON_BEGIN
下压按钮 继续进度条 IDC_BUTTON_GOON
下压按钮 停止进度条 IDC_BUTTON_STOP

(5)重载初始化对话框的函数(见清单 6 11)
清单6 11 On
Ini
tDi
alo 06_
g()函数(在 Ex 5Dl
g.pp文件中)

BOOLCEx 06_5D l gOnInitDialog()


{ //系统自动生成的
CDialog::OnInitDialog();

SetIcon(m_hIcon, TRUE); //Set big icon
SetIcon(m_hIcon, FALSE); //Set small icon
//以下是添加的
//进度条控件的初始化
m_ctrlProgress.SetRange(0,100);
m_ctrlProgress.SetStep(1);
//滑块控件的初始化
m_ctrlSlider.SetRange(0,100); //设置滑块条在滑块控件中的范围
m_ctrlSlider.SetPos(0); //设置滑块条在滑块控件中的当前位置
for(int i = 5;i<100;i = i+ 5)
m_ctrlSlider.SetTic(i); //每隔 5 设置 1 个记号标志
m_ctrlSlider.SetTicFreq(2); //显示记号标志的频率为 2
//滚动条控件的初始化
m_ctrlScrollbar.SetScrollRange(0,100);
m_ctrlScrollbar.SetScrollPos(0);
return TRUE; //return TRUE unless you set the focus to a control

(6)控制进度条
149

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

① 添加 3 个下压按钮的消息函数见表 6 23,编写的消息函数见清单 6 12。


表6 23 下压按钮ID 号与消息函数

控 件 标题(Caption) ID 号 消息函数
下压按钮 开始进度条 IDC_BUTTON_BEGIN OnButtonBegin
下压按钮 继续进度条 IDC_BUTTON_GOON OnButtonGoon
下压按钮 停止进度条 IDC_BUTTON_STOP OnButtonStop

② 添加 OnTimer 函数。
OnTimer 函数的添加见图 6 27,OnTimer 函数由 SetTimer 函数确定时间间隔来触发,编
写的 OnTimer 函数见清单 6 12。

图6 27 OnT r函数的添加
ime

清单6 12 OnT
ime 06_
r函数(在 Ex 5Dl
g.pp文件中)


oidCEx 06_ 5DlgOnTimer( UINTn IDEvet)

{ int position = m_ctrlProgress.OffsetPos(1);
char s10;
wsprintf(s,"%d%% " , position);
CClientDC clientDC(this);
clientDC.SetTextColor(RGB(0,0,255));
clientDC.TextOut(200, 50, s);
CDialog::OnTimer(nIDEvent);


oidCEx 06_ 5DlgOnButtonBegin()
{ SetTimer(1, 100, NULL); //100 毫秒触发一次 OnTimer 函数
m_ctrlProgress.SetPos(0);


oidCEx 06_ 5DlgOnButtonGo on()
{ SetTimer(1, 100, NULL);


oidCEx 06_ 5DlgOnButtonStop()
{ KillTimer(1); //停止触发

150
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

(7)添加 OnHScroll 消息函数控制滑块条的滑动和滚动条的滚动


用 ClassWizard 添加 WM_HSCROLL 消息的消息函数 OnHScroll,编写的函数见清单6 13。

清单6 13 OnHS
cro
l 06_
l函数(在 Ex 5Dl
g.pp文件中)


oidCEx06_ 5D l
g OnHS c
rol(
l UINTnSBCo deUINTnPo sCS
cro
ll rpS
Ba cro
ll r)
Ba
{ //滑块条
CSliderCtrl*slider = (CSliderCtrl*)pScrollBar;
if(slider = = &m_ctrlSlider)
{ int position = slider- >GetPos();
char s10;
wsprintf(s," %d%% " , position);
CClientDC clientDC(this);
clientDC.SetTextColor(RGB(0,255,0));
clientDC.TextOut(200, 90, s);

//滚动条
int step;
step = 1;
CScrollBar* scrollbar = (CScrollBar* )pScrollBar;
m_ctrlScrollbar.GetScrollPos();
if(scrollbar = = &m_ctrlScrollbar)
{switch(nSBCode)
{ case SB_THUMBPOSITION:
 scrollbar- >SetScrollPos(nPos);
break;

case SB_LINEUP:
 if((m_ns-step)> 0)
{ m_ns = m_ns- step;

else
 m_ns = 0;

scrollbar- >SetScrollPos(m_ns);
break;

case SB_LINEDOWN:
 if((m_ns+step)< 100)
{ m_ns = m_ns+step;

else
 m_ns = 100;

scrollbar- >SetScrollPos(m_ns);
break;


int position = scrollbar- >GetScrollPos();
char s10;
151

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

wsprintf(s,"%d%% " , position);
CClientDC clientDC(this);
clientDC.SetTextColor(RGB(0,0,255));
clientDC.TextOut(200, 150, s);

CDialog::OnHScroll(nSBCode, nPos, pScrollBar);

(8)编译和运行

8 图像列表控件 、列表控件与树形控件的应用


81 图像列表控件
有些控件依赖于图像列表,如列表控件和树形控件。 下面主要讲述图像列表控件的创
建和初始化。
(1)创建图像列表
用 CImageList::Create()函数创建图像列表,该函数原型为:

BOOL Create(int cx, int cy, UINT nFlags, int nInitial, int nGrow);

参数:cx,cy 表示图像宽度与高度,以像素为单位。
nFlag:图像列表的类型,可以取 ILC_COLOR、ILC_COLOR4、ILC_COLOR8、ILC_COLOR16、ILC_
COLOR24、ILC_COLOR32、ILC_COLORDDB、ILC_MASK,除 ILC_COLOR 只能单独使用外,其余可以组
合使用。
nInitial:图像列表初始化时包含的图像数 。
nGrow:系统需要改变大小时,图像列表允许扩大到的图像数,为 0 表明列表在运行时不
能增加。
创建举例:

CImageList* pImageList = new CImageList;


pImageList- > Create(12,12,ILC_COLOR,1,0);

(2)初始化图像列表
在图像列表创建完成后,需要向列表中增加图像。 增加图像最简单的方法是把图像包
含在应用程序的资源文件中,作为它的一部分,并从资源文件中加载。 向列表中增加图像可
用 CImageList::Add 函数,该函数的原型为:

int Add(CBitmap* pbmImage, CBitmap* pbmMask);


int Add(CBitmap* pbmImage, COLORREF crMask);
int Add(HICON hIcon);

① 资源文件为位图的加载;
首先编辑位图(假设为 2 个),资源 ID 号为:IDB_BITMAP1,IDB_BITMAP2,然后添加代码将
152 位图加载到每个图像列表中。
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

CBitmap bitmap;//定义位图类对象
bitmap.LoadBitmap(IDB_BITMAP1);//加载位图 1
pImageList- > Add(&bitmap,(COLORREF)0x000001);//添加位图 1 到图像列表
bitmap.DeleteObject();//释放位图
bitmap.LoadBitmap(IDB_BITMAP2);//加载位图 2
pImageList- > Add(&bitmap,(COLORREF)0x000001);//添加位图 2 到图像列表
bitmap.DeleteObject();//释放位图

② 资源文件为图标的加载。
首先编辑 2 个图标,资源 ID 号为:IDI_ICON1,IDI_ICON2,然后添加代码将图标加载到每
个图像列表中。

HICON hIcon = ::LoadIcon(AfxGetResourceHandle(


),MAKEINTRESOURCE(IDI_ICON1));
pImageList- > Add(hIcon);
hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICON2));
pImageList- > Add(hIcon);


82 列表控件 CL
ist
Ctr

CListCtrl 类封装了列表视控件的功能。例如 Windows 资源管理器中的显示文件名称、
文件长度和最近的修改日期等,应用的就是 CListCtrl 控件。
(1)创建列表控件
用 CListCtrl::Create()函数创建列表控件,该函数原型为:
BOOL Create(DWORD dwStyle, const RECT & rect, CWnd*pParentWnd, UINT nID);
参数 dwStyle:列表控件风格,可取值见表 6 24。
表6 24 dwS
tye取值(列表控件风格)

风 格 说 明
LVS_ALIGNLEFT 在大图标和小图标视图中左对齐项目
LVS_ALIGNTOP 在大图标和小图标视图中上对齐项目
LVS_AUTOARRANGE 在大图标和小图标视图中自动排列项目
LVS_EDITLABELS 允许用户编辑项目标签
LVS_ICON 设置控件为大图标视图
LVS_LIST 设置控件为列表视图
LVS_NOCOLUMNHEADER 在报告视图中不显示列标题
LVS_NOITEMDATA 仅保存每个项目的状态
LVS_NOLABELWRAP 不允许项目标签的多行显示
LVS_NOSCROLL 关闭滚动
LVS_NOSORTHEADER 不显示列标题的按钮
LVS_OWNERDRAWFIXED 在报告视图中允许实现所有者自画项目
LVS_REPORT 设置控件为报告视图

153

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
风 格 说 明
LVS_SHAREIMAGELISTS 在控件不需要图像列表时,不允许控件删除图像列表
LVS_SINGLESEL 不允许选择多个项目
LVS_SMALLICON 设置控件为小图标视图
LVS_SORTASCENDING 按升序排列项目
LVS_SORTDESCENDING 按降序排列项目

rect:说明列表控件的大小和位置。
pParentWnd:指向控件父窗口的指针,由于控件必定有父窗口,因此这个参数一定不能
为 NULL。
NID:定制列表控件的 ID。
创建举例:

CListCtrl m_ctrlList;
m_ctrlList.Create(WS_VISIBLE|WS_CHILD|WS_BORDER|LVS_REPORT|LVS_NOSORTHEADER
|LVS_EDITLABELS,CRect(300,10,700,420),this,IDC_LISTCTRL);
(2)列表控件的列和项目
创建列表视图 对 象 后,需 要 创 建 列 表 视 的 列 和 项 目。要 创 建 列,首 先 必 须 申 明
LV_COLUMN 结 构 ,利 用 该 结 构 可 以 和 系 统 交 换 信 息 。 MFC 定 义 的 LV_ COLUMN 结 构 见 清
单6 14 。
清单6 14 LV_
COLUMN 结构定义


yped
e fstruct_LV_ COLUMN

UINT mask; //指示有效域的标志
int fmt; //列对齐方式
int cx; //列宽度
LPSTR pszText; //字符串缓冲区地址
int cchTextMax; //字符串缓冲区的大小
int iSubItem; //该列的子项目索引
}LV_COLUMN

该结构的 mask 成员告诉系统系统结构的哪个成员有用,哪个被忽略。 可以使用的标记


如下:
● LVCF_FMT fmt 有效
● LVCF_SUBITEM iSubItem 有效
● LVFC_TEXT pszText 有效
● LVFC_WIDTH cx 有效
fmt 成 员 指 示 列 的 对 齐 方 式 , 可 以 是 LVCFMT _ CENTER、LVCFMT _ LEFT 或 LVCFMT _
RIGHT。
注意:第一列,即包括主项目的列,总是左对齐的。 在报表视图中的其他列可按用户的
要求对齐。
154 项目的创建与列的创建相似, LV_ITEM 结构用于项目创建时的初始化,并将其传递到创
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

建项目的函数中。MFC 定义的 LV_ITEM 结构见清单 6 15。


清单6 15 LV_
ITEM 结构定义


yped
e fstruct_LV_ ITEM
 UINT mask; //指示有效域的标志
int iItem; //项目索引
int iSubItem; //子项目索引
UINT state; //项目的当前状态
UINT stateMask; //有效项目状态
LPSTR pszText; //字符串缓冲区地址
int cchTextMax; //字符串缓冲区的大小
int iImage; //该项目的图像索引
LPARAM lParam; //附加信息(32 位)
}LV_ITEM


83 树形控件 CT
reeC
trl
(1)创建树形控件
用 CTreeCtrl::Create 函数创建树形控件,该函数原型为:

BOOL Create(DWORD dwStyle, const RECT & rect, CWnd*pParentWnd, UINT nID);

参数:dwStyle:树形控件风格,取值见表 6 25。
表6 25 dwS
tye取值(树形控件风格)

风 格 说 明
TVS_DISABLEDRAGDROP 禁止拖动操作
TVS_EDITLABELS 允许用户编辑标签
TVS_HASBUTTONS 为每个父项目分配一个按钮
TVS_HASLINES 树中两个项目之间加上一根线
TVS_LINESATROOT 在根和子项目之间加上一根线
TVS_SHOWSELALWAYS 在失去焦点时仍强制一个选项保持选中状态

rect:说明树形控件的大小和位置
pParentWnd:指向控件父窗口的指针,由于控件必定有父窗口,因此这个参数一定不能
为 NULL
NID:要创建的树形控件的 ID
创建举例:

CTreeCtrl m_ctrlTree;
m_ctrlTree.Create (WS_VISIBLE|WS_CHILD|WS_BORDER|TVS_HASLINES|TVS_LINESATROOT|
TVS_HASBUTTONS|TVS_EDITLABELS,CRect(20,10,280,420),this,IDC_TREECTRL);
(2)树形控件的项目
MFC 中定义的 TV_ITEM 结构见清单 6 16。
155

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单6 16 TV_
ITEM 结构定义

Ty
pe d
efs t
ruct_ TV_ ITEM
 UINT mask; //指示有效域的标志
HTREEITEM hItem; //项目句柄
UINT state; //项目的当前状态
UINT stateMask; //有效项目状态
LPSTR pszText; //字符串缓冲区地址
Int cchTextMax; //字符串缓冲区的大小
int iImage; //该项目的图像索引,未选中状态
int iSelectedImage; //该项目的图像索引,选中状态
int cChilren; //项目是否有与其相关的子项目
LPARAM lParam; //附加信息(32 位)
}TV_ ITEM

mask 取值见表 6 26。


表6 26 ma
sk取值
值 说 明
TVIF_CHILDREN cChildren 有效
TVIF_IMAGE iMage 有效
TVIF_HANDLE hItem 有效
TVIF_PARAM iParam 有效
TVIF_SELECTEDIMAGE iSelectedImage 有效
TVIF_STATE state 和 stateMask 有效
TVIF_TEXT PszText 和 cchTextMax 有效

state 和 stateMask 的状态可取一种或多种,见表 6 27。


表6 27 s
tae和s
t tat
eMa
sk取值

状 态 状 态 状 态
TVIS_BOLD TVIS_CUTE TVIS_DROPHILLED
TVIS_EXPANDED TVIS_EXPANDONC TVIS_FOCUSED
TVIS_OVERLAYMASK TVIS_SELECTED TVIS_STATEIMAGEMASK
TVIS_USERMASK

(3)插入结构的定义
TV_INSERTSTRUCT 保存如何将结构 TV_ITEM 插入到树形控件中的信息,其结构定义见清
单 6 17。
清单6 17 TV_
INSERTSTRUCT结构定义

Ty
pe d
efstructTV_ INSERTSTRUCT
 HTREEITEM hParent; //父树形控件项目的句柄
HTREEITEM hInsertAfter; //指定在其后插入新项目的项目句柄
TVITEM item; //包含插入树中的项目的信息的 TVITEM 结构
}TV_ INSERTSTRUCT

156 参数 hPparent:可以取 NULL 或 TVI_ROOT 值指定项目应放置在树根处。


第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

hInsertAfter:可以是 TVI_FIRST(列表开始)、TVI_LAST(列表结尾)或 TVI_SORT(字母顺


序)。


84 编程实例
【例6 6】 Ex06_6 图像列表控件、列表控件与树形控件的应用编程,具体说明见表
6 28。

表6 28 例6 6具体说明

项目 图示和说明

程序
界面

图(
b) 菜单

图(
a) 主界面

包括的控件有:
1 个树形控件、1 个列表控件
(1)图(a):在华东理工大学下面为学院,学院下面为年级,点击年级将在列表控件中显示年级中
程序
的班级名称、人数和班导师
功能
(2)图(b):点击查看菜单下大图标、小图标、列表和报告,列表控件将按所选方式显示

(1)建立一单文档应用程序:Ex06_6
(2)编辑 5 个图标(见表 6 29)

表6 29 5个图标

ICON 的 ID 号 图 标 说 明

IDI_ICONTREE1 用于树形控件中的学校

IDI_ICONTREE2 用于树形控件中的学院

IDI_ICONTREE3 用于树形控件中的年级

IDI_ICONB 用于列表控件中大图标

IDI_ICONS 用于列表控件中小图标

(3)初始化视图
初始化视图一般重载 OnInitialUpdate 函数,重载的该函数见清单 6 18。 157

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单6 18 On
Ini
ti
alUpd
at 06_
e函数(在 Ex 6Vi
ew.
cpp文件中)


oidCEx 06_6V i
ew On I
nit
ial
Upda
te()
{ CView::OnInitialUpdate();
CreateListCtrl();//创建列表控件
CreateTreeCtrl();//创建树形控件

(4)创建树形控件
① 定义一树形控件对象、图像列表对象和创建树形控件函数(见清单 6 19)。
② 编写创建树形控件函数(见清单 6 20)。
③ 定义 IDC_TREECTRL。
操作菜单 View- >Resource Symbols 弹出 Resource Symbols 对话框(
见图 6 28)
,点击 New 弹
出 New Symbol 对话框,在 Name 编辑框中输入 IDC_TREECTRL, Value 编辑框的值取系统自动给定的
值 101。点击 New Symbol 对话框中的 OK 按钮和 Resource Symbols 对话框中的 Close 按钮。

图6 28 定义IDC_
TREECTRL

(5)创建列表控件
① 定义列表控件对象、图像列表对象和创建列表控件的函数(见清单 6 19)。
06_
清单6 19 定义成员变量和函数(在 Ex 6Vi h头文件中)
ew.


las
sCEx 06_ 6Viewpub l
icCV iew

public:
CImageList m_ctrlSImageList; //定义图像列表对象,用于列表控件小图标
CImageList m_ctrlBImageList; //定义图像列表对象,用于列表控件大图标
CImageList m_ctrlTreeImageList; //定义图像列表对象,用于树形控件
CTreeCtrl m_ctrlTree; //定义树形控件对象
void CreateTreeCtrl(); //创建和初始化树形控件函数
CListCtrl m_ctrlList; //定义树形控件对象
void CreateListCtrl(); //创建和初始化列表控件函数

② 编写创建列表控件的函数(见清单 6 20)。
③ 定义 IDC_LISTCTRL。
158 操作过程同树形控件。
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

06_
清单6 20 创建树形控件和列表控件的函数(在 Ex 5Vi
ew.
cpp文件中)


oidCEx 06_ 5V i
ew CreateTre
eC t
rl()//创建树形控件函数
{ //创建图像列表
m_ctrlTreeImageList.Create(13,13,false,3,0);
HICON hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICONTREE1));
m_ctrlTreeImageList.Add(hIcon);
hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICONTREE2));
m_ctrlTreeImageList.Add(hIcon);
hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICONTREE3));
m_ctrlTreeImageList.Add(hIcon);
//创建树形控件
m_ctrlTree.Create(WS_VISIBLE|WS_CHILD|WS_BORDER|TVS_HASLINES|TVS_LINESATROOT|
TVS_HASBUTTONS|TVS_EDITLABELS,CRect(20,10,220,300),this,IDC_TREECTRL);
//将图像列表与树形控件相联系
m_ctrlTree.SetImageList(&m_ctrlTreeImageList,LVSIL_NORMAL);
//为根目录初始化 TVITEM 结构
TVITEM tvItem;
tvItem.mask = TVIF_TEXT|TVIF_IMAGE|TVIF_SELECTEDIMAGE;
tvItem.pszText =" 华东理工大学";
tvItem.cchTextMax = 16;
tvItem.iImage = 0;
tvItem.iSelectedImage = 0;
TVINSERTSTRUCT tvInsert;
tvInsert.hParent = TVI_ROOT;
tvInsert.hInsertAfter = TVI_FIRST;
tvInsert.item = tvItem;
HTREEITEM hRoot = m_ctrlTree.InsertItem(&tvInsert);
//将各学院插入到学校的根目录下:
tvItem.pszText =" 机械学院";
tvItem.cchTextMax = 12;
tvItem.iImage = 1;
tvItem.iSelectedImage = 1;
tvInsert.hParent = hRoot;
tvInsert.hInsertAfter = TVI_FIRST;
tvInsert.item = tvItem;
HTREEITEM hCollege1 = m_ctrlTree.InsertItem(&tvInsert);
tvItem.pszText =" 信息学院";
tvItem.cchTextMax = 12;
tvItem.iImage = 1;
tvItem.iSelectedImage = 1;
tvInsert.hParent = hRoot;
tvInsert.hInsertAfter = TVI_FIRST;
tvInsert.item = tvItem;
HTREEITEM hCollege2 = m_ctrlTree.InsertItem(&tvInsert);
//将年级插入到学院的根目录下:
tvItem.pszText =" 机械 01 级";
tvItem.cchTextMax = 12;
tvItem.iImage = 2;
tvItem.iSelectedImage = 2;
tvInsert.hParent = hCollege1;
tvInsert.hInsertAfter = TVI_FIRST;
159

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

tvInsert.item = tvItem;
HTREEITEM hClass = m_ctrlTree.InsertItem(&tvInsert);

tvItem.pszText =" 机械 02 级";
tvItem.cchTextMax = 12;
tvItem.iImage = 2;
tvItem.iSelectedImage = 2;
tvInsert.hParent = hCollege1;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item = tvItem;
hClass = m_ctrlTree.InsertItem(&tvInsert);

tvItem.pszText =" 机械 03 级";
tvItem.cchTextMax = 12;
tvItem.iImage = 2;
tvItem.iSelectedImage = 2;
tvInsert.hParent = hCollege1;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item = tvItem;
hClass = m_ctrlTree.InsertItem(&tvInsert);

tvItem.pszText =" 机械 04 级";
tvItem.cchTextMax = 12;
tvItem.iImage = 2;
tvItem.iSelectedImage = 2;
tvInsert.hParent = hCollege1;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item = tvItem;
hClass = m_ctrlTree.InsertItem(&tvInsert);


oidCEx 06_ 6V i
ew CreateLi
stCtr //创建列表控件函数
l()
{ //创建图像
m_ctrlSImageList.Create(16,16,false,1,0);
m_ctrlBImageList.Create(32,32,false,1,0);
//添加图像到图像列表中
HICON hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICONS));
m_ctrlSImageList.Add(hIcon);
hIcon = ::LoadIcon(AfxGetResourceHandle(),MAKEINTRESOURCE(IDI_ICONB));
m_ctrlBImageList.Add(hIcon);
//创建列表控件
m_ctrlList.Create(WS_VISIBLE|WS_CHILD|WS_BORDER|LVS_REPORT|LVS_NOSORTHEADER|
LVS_EDITLABELS,CRect(250,10,500,300),this,IDC_LISTCTRL);
//将图像列表与列表控件关联
m_ctrlList.SetImageList(&m_ctrlSImageList,LVSIL_SMALL);
m_ctrlList.SetImageList(&m_ctrlBImageList,LVSIL_NORMAL);
//创建列表控件列
LV_COLUMN lvColumn;
lvColumn.mask = LVCF_FMT|LVCF_WIDTH|LVCF_TEXT|LVCF_SUBITEM;
lvColumn.fmt = LVCFMT_CENTER;
lvColumn.cx = 75;
lvColumn.iSubItem = 0;
160
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

lvColumn.pszText =" 班级";
m_ctrlList.InsertColumn(0,&lvColumn);
lvColumn.iSubItem = 1;
lvColumn.pszText =" 人数";
m_ctrlList.InsertColumn(1,&lvColumn);
lvColumn.iSubItem = 2;
lvColumn.pszText =" 班导师";
m_ctrlList.InsertColumn(2,&lvColumn);

(6)实现列表控件的不同显示
① 编辑菜单资源。
② 添加菜单消息函数和菜单更新消息函数(见表 6 30)。
表6 30 菜单ID 号和所添加消息函数

菜 单 ID 号 消息处理函数
OnViewBicon()
大图标 ID_VIEW_BICON
OnUpdateViewBicon(CCmdUI*pCmdUI)
OnViewSicon()
小图标 ID_VIEW_SICON
OnUpdateViewSicon(CCmdUI*pCmdUI)
OnViewList()
列表 ID_VIEW_LIST
OnUpdateViewList(CCmdUI*pCmdUI)
OnViewReport()
报告 ID_VIEW_REPORT
OnUpdateViewReport(CCmdUI*pCmdUI)

③ 定义 4 个 BOOL 变量,见清单 6 21。


06_
清单6 21 定义4个 BOOL 变量(在 Ex 6Vi h文件中)
ew.


las
sCEx 06_ 6Viewpub li
cCV
iew

protected:
BOOL m_bBIcon;
BOOL m_bSIcon;
BOOL m_bList;
BOOL m_bReport;

④ 在构造函数中初始化 4 个 BOOL 变量(见清单 6 22)。


⑤ 编写菜单消息函数和菜单更新消息函数(见清单 6 22)。
06_
清单6 22 构造函数与菜单消息函数(在 Ex 6Vi
ew.
cpp文件中)


oidCEx 06_6V i 06_
ewEx 6Vi //构造函数
ew()
{ m_bBIcon = 0;
m_bSIcon = 0;
m_bList = 0;
m_bReport = 1;
161

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


//列表控件的不同显示
vo
idCEx 06_6V i
ewOnV i
ewB i
con()//显示为大图标
{ SetWindowLong(m_ctrlList.m_hWnd,GWL_STYLE,WS_VISIBLE|WS_CHILD| WS_BORDER
|LVS_ICON|LVS_EDITLABELS);
m_bBIcon = 1;
m_bSIcon = 0;
m_bList = 0;
m_bReport = 0;


oidCEx 06_6V i
ewOnV i
ewSicon()//显示为小图标
{ SetWindowLong(m_ctrlList.m_hWnd,GWL_STYLE,WS_VISIBLE|WS_CHILD| WS_BORDER|
LVS_SMALLICON|LVS_EDITLABELS);
m_bBIcon = 0;
m_bSIcon = 1;
m_bList = 0;
m_bReport = 0;


oidCEx 06_6V i
ewOnV i
ewL i
st()//显示为列表形式
{ SetWindowLong(m_ctrlList.m_hWnd,GWL_STYLE,WS_VISIBLE|WS_CHILD| WS_BORDER|
LVS_LIST|LVS_EDITLABELS);
m_bBIcon = 0;
m_bSIcon = 0;
m_bList = 1;
m_bReport = 0;


oidCEx 06_6V i
ewOnV i
ewRe por //显示为报告形式
t()
{ SetWindowLong(m_ctrlList.m_hWnd,GWL_STYLE,WS_VISIBLE|WS_CHILD| WS_BORDER|
LVS_REPORT|LVS_EDITLABELS);
m_bBIcon = 0;
m_bSIcon = 0;
m_bList = 0;
m_bReport = 1;


oidCEx06_ 6ViewOnUpd ateVi
ewB icon(CCmdUIpCmdUI)
{ pCmdUI- >SetCheck(m_bBIcon);


oidCEx06_ 6ViewOnUpd ateVi
ewS ic
on( CCmdUIpCmdUI)
{ pCmdUI- >SetCheck(m_bSIcon);


oidCEx06_ 6ViewOnUpd ateVi
ewL it(
s CCmdUIpCmdUI)
{ pCmdUI- >SetCheck(m_bList);


oidCEx06_ 6ViewOnUpd ateVi
ewRe port(CCmdUIpCmdUI)
{ pCmdUI- >SetCheck(m_bReport);

(7)实现改变树形控件的操作
① 在 Ex06_6View.h 头文件中声明 OnSelchangedTree 函数(见清单 6 23)。

162
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

清单6 23 声明 OnS
elc
han
gedTr
e 06_
e函数(在 Ex 6Vi h文件中)
ew.


las
sCEx 06_ 6Vi
ewpub licCView
 protected:
//AFX_MSG(CEx06_6View)

//}}AFX_MSG
afx_ms gvoidOnS el
changedTr
ee(
NMHDRpNMHDRLRESULTpRe
sul //手工添加
t);
DECLARE_MESSAGE_MAP()

② NOTIFY 说明 OnSelchangedTree 函数与树形控件的映射(见清单 6 24)。


06_
清单6 24 消息映射声明(在 Ex 6Vi
ew.
cpp文件中)

BEGIN_MES SAGE_MAP( CEx 06_6ViewCV


iew)
//{{AFX_MSG_MAP(CEx06_6View)

//}}AFX_MSG_MAP
//Standard printing commands
ON_ NOT IFY(TVN_ SELCHANGEDIDC_ TREECTRLOnS
elc
han
gedTr
e //手工添加
e)
END_MESSAGE_MAP()

③ 编写 OnSelchangedTree 函数(见清单 6 25)。


清单6 25 OnS
elc
han
gedTr
e 06_
e函数(在 Ex 6Vi
ew.
cpp文件中)


oidCEx 06_ 6V i
ew OnS el
changedTre(
e NMHDRpNMHDRLRESULTpRe
sut)

{ NM_TREEVIEW *pNMTreeView = (NM_TREEVIEW *)pNMHDR;
//TODO: Add your control notification handler code here
*pResult = 0;
CString strText;
HTREEITEM hTreeItem;
hTreeItem = m_ctrlTree.GetSelectedItem();
strText = m_ctrlTree.GetItemText(hTreeItem);
if(strText = =" 机械 01 级")
{m_ctrlList.DeleteAllItems();
Add01toList();

else if(strText = =" 机械 02 级")
{ m_ctrlList.DeleteAllItems();
Add02toList();

else if(strText = =" 机械 03 级")
{ m_ctrlList.DeleteAllItems();
Add03toList();

else (strText = =" 机械 04 级")
{ m_ctrlList.DeleteAllItems();
Add04toList();


163

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

④ 声明 Add01toList()—Add04toList()函数(见清单 6 26)。
清单6 26 声明 Add
01t
oLi
st()—Add
04t
oLi
s 06_
t()函数(在 Ex 6Vi h文件中)
ew.


las
sCEx 06_6Viewpub li
cCVi
ew
 public:
void Add01toList();
void Add02toList();
void Add03toList();
void Add04toList();

⑤ 编写 Add01toList()—Add04toList()函数(见清单 6 27)。
清单6 27 Add
01t
oLi
st()—Add
04t
oLi
s 06_
t()函数(在 Ex 6Vi
ew.
cpp文件中)

vo
idCEx 06_6V iew Add01 t
oLit()

{ //创建列表控件项目
LV_ITEM lvItem;
lvItem.mask = LVIF_TEXT|LVIF_IMAGE|LVIF_STATE;
lvItem.state = 0;
lvItem.stateMask = 0;
lvItem.iImage = 0;
lvItem.iItem = 0;
lvItem.iSubItem = 0;
lvItem.pszText =" 机械 011";
m_ctrlList.InsertItem(&lvItem);
m_ctrlList.SetItemText(0,1,"30");
m_ctrlList.SetItemText(0,2,"李老师");
lvItem.iItem = 1;
lvItem.iSubItem = 0;
lvItem.pszText =" 机械 012";
m_ctrlList.InsertItem(&lvItem);
m_ctrlList.SetItemText(1,1,"31");
m_ctrlList.SetItemText(1,2,"张老师");
lvItem.iItem = 2;
lvItem.iSubItem = 0;
lvItem.pszText =" 机械 013";
m_ctrlList.InsertItem(&lvItem);
m_ctrlList.SetItemText(2,1,"32");
m_ctrlList.SetItemText(2,2,"刘老师");

//Add02toList()- Add04toList()略

(8)编译和运行

9 属性单 、属性页和向导

属性单(Property Sheet)是带标签的对话框。每个属性单至少带有两个属性页,每个属
164 性页 (Property Page )都 基 于 对 话 框 模 板, 出 现 在 属 性 单 的 某 一 标 签 中。 MFC 类 的
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

CpropertySheet 和 CpropertyPage 分别用于管理属性单和属性页。


向导(Wizard)是一种特殊的属性单,它使用按钮从一个页面移动到另一个页面 (而不是
标签),用于指导用户一步一步完成一个复杂的操作过程 。


91 CP
rope
rtyPage类
CPropertyPage 类是 CDialog 类的一个派生类,继承关系见图 6 29。

图6 29 CPr
ope
rtyPa
ge类的继承关系

CPropertyPage 类的主要成员函数见表 6 31。

表6 31 CPr
ope
rtyPa
ge类的主要成员函数

成员函数 返回值 说 明
CPropertyPage 构造 CpropertyPage 对象
构造 CpropertyPage 对象。若需要在运行时指定参数,或者使
Construct void
用数组指定参数,则使用构造函数 Construct
在对模式属性单中的某一页进行了不可恢复的改变后,调用该
CancelToClose void
函数将 OK 按钮改为 Close 按钮,将 Cancle 按钮灰化处理
SetModified void 激活或禁止 Apply 按钮
向属性单的每一个属性页依次发送消息,如果某属性页返回非
QuerySiblings LRESULT
零值,就停止发送
OnCancel void 当单击 Cancel 按钮时由框架调用此函数
OnKillActive BOOL 当前页不再是活动页时由框架调用此函数
OnOK void 当单击 OK、Apply 或 Close 按钮时由框架调用此函数
OnSetActivate BOOL 当属性页被激活时,由框架调用此函数
OnApply BOOL 当单击 Apply 按钮时,由框架调用此函数
OnReset void 当单击 Cancel 按钮时,由框架调用此函数
当单击 Cancel 按钮,但相关操作还没有执行时由框架调用此
OnQueryCancel BOOL
函数
在向导类型的属性表中,在单击上一页按钮时由框架调用此
OnWizardBack LRESULT
函数
在向导类型的属性表中,在单击下一页按钮时由框架调用此
OnWizardNext LRESULT
函数
在向导类型的属性 表 中,在 单 击 完 成 按 钮 时 由 框 架 调 用 此
OnWizardFinish void
函数


92 CP
rope
rt t类
yShee
CPropertySheet 类是 CWnd 类的一个直接派生类,继承关系见图 6 30。

图6 30 CPr
ope
rtSh
y e
et类的继承关系
165

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

CPropertySheet 类的主要成员函数见表 6 32。


表6 32 CPr
ope
rtSh
y e
et类的主要成员函数

成员函数 返回值 说 明
CPropertySheet 构造 CpropertySheet 对象
构造 Cpropertysheet 对象。当定义了 CpropertySheet 类对象
Construct void 而没有调用构造函数时,如定义 了 CpropertySheet 对 象 数 组
时,必须显示地调用 Construct
GetActiveIndex int 获取属性单中当前活动页的索引
GetPageIndex int 获取属性单中指定属性页的索引
GetPageCount int 获取属性单中属性页的数量
GetPage CPropertyPage* 获取指向指定属性页的指针
GetActivePage CPropertyPage* 返回当前活动页对象的指针
SetActivePage BOOL 设置活动页对象
SetTitle void 设置属性单的标题
GetTabControl CTabCtrl* 获取指向 tab 控件的指针
SetFinishText void 设置 Finish 按钮的文本
SetWizardButtons void 设置向导中 Set 按钮的显示模式
SetWizardMode void 设置为向导模式
DoModal int 显示模式属性单
Create BOOL 显示非模式属性单
AddPage void 在属性单中增加一个属性页
RemovePage void 从属性单中删除一个属性页
PressButton BOOL 模拟属性单中指定按钮的单击
EndDialog void 关闭属性单


93 编程实例
【例6 7】 Ex06_7 属性单、属性页的应用,具体说明见表 6 33 所示。
表6 33 例6 7的具体说明

项目 图示和说明

程序
界面

图(
a) 属性单 图(
b) 向导
166
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

续 表
项目 图示和说明

(1)点击菜单:查看- >属性单...,将弹出图(a)
程序
(2)点击菜单:查看- >向导...,将弹出向导的第一页(见图(b)),按下一步到第二页,再按一次到
功能
第三页

(1)建立一单文档应用程序:Ex06_7
(2)插入 3 个对话框
对话框 ID 号为 IDD_DIALOG1、IDD_DIALOG2、IDD_DIALOG3。
在对话框属性选择中将 style 选为 Child,Border 选为 Thin,选中 Disabled 复选按钮,
删除 OK 和 Cancel 按钮。
(3)为 3 个对话框分别建类
类名 分 别 为: CPage1Dlg、CPage2Dlg 和 CPage3Dlg; 基 础 类 选 为: CPropertyPage; 见 图
6 31。

图6 31 CPa
ge1D
lg类的建立 图6 32 CPa
geD
lgSh
eet类的建立

(4)属性单的实现
① 创建属性单类。
点击 Insert 菜单下的 NewClass,在 Name 中输入 CPageDlgSheet,在 Base class 中选
择 CPropertySheet,见图 6 32。单击 OK 按钮即建立了 CPageDlgSheet 类。
② 在 PageDlgSheet.h 头文件中,声明属性页对象和成员函数(见清单 6 28)。
③ 添加 3 个包含属性页类头文件的语句(见清单 6 28)。
清单6 28 Pa
geD
lgSh
ee h头文件
t.

//CPageDlgSheet
//添加 3 个包含属性页的语句
# include" Page1Dlg.h"
# include" Page2Dlg.h"
# include" Page3Dlg.h"
classCPa geDlgSheetpub l
icCPr ope
rtSh
y e
et
 DECLARE_DYNAMIC(CPageDlgSheet)
167

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


public:
CPageDlgSheet(UINT nIDCaption, CWnd*pParentWnd = NULL, UINT iSelectPage = 0);
CPageDlgSheet(LPCTSTR pszCaption, CWnd*pParentWnd = NULL, UINT iSelectPage = 0);

public:
//声明 3 个属性页的对象
CPage1Dlg m_page1dlg;
CPage2Dlg m_page2dlg;
CPage3Dlg m_page3dlg;
//声明成员函数
void AddPageToSheet();
};

④ 编写构造函数和 AddPageToSheet()函数(见清单 6 29)。


清单6 29 构造函数和 AddPa
geTo
She
et函数(在 Pa
geD
lgSh
eet.
cpp文件中)

CPageDlgShee
tCPageDlgShet(
e UINTn IDCa
p t
ionCWndpPare
ntWndUINTiSe
lec
tPage)
:CProper
t Sh
y eet(
nIDCa t
pionpPa r
entWndiSel
ectPae)

{ AddPageToSheet();

CPageDlgShee
tCPageDlgShet(
e LPCTSTRp szCapt
ionCWndpPar
entWndUINTiS
ele
ctPa
ge)
:CProper
t Sh
y eet(
pszCapti
o npParentWndi
S e
lec
tPage)
{ AddPageToSheet();

CPageDlgShee
t~CPa geDlgShet()



vo
idCPa geDlgShe
etAddPa eTo
g Sheet()
{ AddPage(&m_page1dlg);
AddPage(&m_page2dlg);
AddPage(&m_page3dlg);

(5)向导的实现
① 创建向导类。
点击 Insert 菜单下的 NewClass,在 NewClass 对话框的类名中输入 CPageDlgWizard,
在 Base class 中选择 CPropertySheet。单击 OK 按钮即建立了 CPageDlgWizard 类。
② 在 PageDlgWizard.h 文件中,声明属性页对象和成员函数(见清单 6 30)。
③ 添加 3 个包含属性页类头文件的语句(见清单 6 30)。
清单6 30 Pa
geD
lgWi
zar
d.h头文件

//CPageDlgWizard
//添加 3 个包含属性页的语句
# include" Page1Dlg.h"
# include" Page2Dlg.h"
168
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………

# include" Page3Dlg.h"

lassCPa geD lgWizardpublicCPr o
per
tySh
eet

public:
//声明 3 个属性页的对象
CPage1Dlg m_page1dlg;
CPage2Dlg m_page2dlg;
CPage3Dlg m_page3dlg;
//声明成员函数
void AddPageToWizard();
};

④ 编写构造函数和 AddPageToWizard()函数(见清单 6 31)。


清单6 31 构造函数和 AddPa
geToWi
zad()函数(在 Pa
r geD
lgWi
zar
d.pp文件中)

CPaeD
g lgWizardCPa geDl
gWizad(
r UINTnIDCapt
ionCWndpPare
ntWndUINTiSe
lec
tPage)
:CPr
o pert
yShee (
tn IDCa pt
ionpPare
n tWndi
Sel
ectPa
ge)
{ AddPageToWizard();

CPaeD
g lgWizardCPa geDl
gWizad(
r LPCTSTRpszCapt
ionCWndpPar
entWndUINTiS
ele
ctPa
ge)
:CPr
o pert
ySheet(pszCapti
onpParentWndiSe
lec
tPage)
{ AddPageToWizard();

vo
idCPa geDlgWizar dAddPaeToWi
g za
rd()
{ AddPage(&m_page1dlg);
AddPage(&m_page2dlg);
AddPage(&m_page3dlg);
SetWizardMode();

(6)调用属性单和向导
① 编辑菜单。
② 添加菜单消息函数(见表 6 34)和编写菜单消息函数(见清单 6 32)。
表6 34 添加菜单消息函数

菜 单 ID 号 消 息 函 数
属性单... ID_VIEW_SHEET OnViewSheet()
向导... ID_VIEW_WIZARD OnViewWizard()

③ 添加 2 个包含 PageDlgSheet.h 和 PageDlgWizard.h 头文件的语句(见清单 6 32)。


06_
清单6 32 调用属性单和向导(在 Ex 7Vi
ew.
cpp文件中)

# include" Ex06_7View.h"
# include" PageDlgSheet.h"//包含属性页头文件
# include" PageDlgWizard.h"//包含向导类头文件

oi dCEx 06_7V i
ew OnV iewShe
e //点击菜单“属性单...”消息处理函数
t()
{ CPageDlgSheet dlgsheet("属性单",this,0);//显示属性单
169

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

dlgsheet.DoModal();


oidCEx06_7View OnViewWizar //点击菜单“向导...”消息处理函数
d()
{ CPageDlgWizard dlgWizard("向导",this,0);
dlgWizard.DoModal();//显示向导

(7)编译和运行

10 通用对话框类

通用对话框类用于各种 Windows 程序中,执行各种标准操作。 它们由 CDialog 类派生
而来,其资源全部包含在\Windows\SYSTEM 目录下的 COMMDLG.DLL 动态链接库中。 表 6 35
列出了通用对话框类的名称及其作用 。
表6 35 通用对话框类的名称及其作用

类 名 作 用 数据成员及其结构名称
CFontDialog 从提供的字体列表中选择一种字体 m_cf:CHOOSEFONT
CFileDialog 打开或保存一个存在或新建的文件 m_ofn:OPENFILENAME
CColorDialog 选择或创建颜色 m_cc:CHOOSECOLOR
设置打印 机 及 与 打 印 有 关 的 参 数,打 印
CPrintDialog m_pd:RRWTDLG
文档
CFindReplaceDialog 在文本文件中查找替换指定字符串 m_fr:FINDREPLACE
CPageSetupDialog 设置打印机页面 m_psd:PAGESETUPDLE
COleDialog 派生了多种 OLE 控制对话框类 m_io 等


10
1 CFo
ntD
iaog类

该类为字体对话框类,主要成员函数见表 6 36,具体应用见第 7 章。
表6 36 CFo
ntD
ial
og类
成 员 函 数 作 用 使 用 方 法
GetColor 获得当前颜色
GetCurrentFont 获得当前字体
GetFaceName 获得字体的字体名称
(1)构造 CFontDialg 类的对象
GetStyleName 获得字体的风格名称 (2)通过对象的数据成员 m_cf 初始化字
GetSize 获得字体的点大小 体对话框中各控件的值或状态
GetWeight 获得字体的磅数 (3)调用成员函数 DoModal 显示对话框
IsStrikeOut 确定字体是否带删除线 (4)应用程序通过 CFontDialog 类的成员
函数获得各种信息
IsUnderline 确定字体是否带下划线
IsBold 确定字体是否是粗体
IsItalic 确定字体是否斜体

170
第六章 对话框与控件
……………………………………………………………………………………………………………………………………………


10
2 CF
ileD
iaog类

该类为文件对话框类,主要成员函数见表 6 37,具体应用见第 8 章。
表6 37 CF
ileD
ial
og类
成员函数 作 用 使 用 方 法
GetFileName 返回所选文件名 (1)构造 CFileDialog 类的对象
GetPathName 返回所选文件路径 (2)通过对象的数据成员 m_ofn 初始
GetFileExt 返回所选文件扩展名 化文 件 对 话 框 中 各 控 件 的 值 或
状态
GetFileTitle 返回所选文件标题
(3)调 用 成 员 函 数 DoModal 显 示 对
GetNextPathName 返回下一个所选文件标题 话框
GetReadOnlyPref 返回文件的只读属性 (4)应用程序通过 CFileDialog 类的
GetStartPosition 返回文件列表中的第一个元素的位置 成员函数获得各种信息


10
3 CCo
lorD
iaog类

该类为颜色对话框类,主要成员函数见表 6 38,具体应用见第 7 章。
表6 38 CCo
lorD
ial
og类
成 员 函 数 作 用 使 用 方 法
返回 一 个 COLORREF 结 构,其 中 包 (1)构造 CColorDialog 类的对象
GetColor
含所选的颜色 (2)通过对象的数据成员 m_cc 初始化颜
色对话框中各控件的值或状态
GetSavedCustomColors 获得用户创建的定制颜色
(3)调用成员函数 DoModal 显示对话框
(4)应用程序通过 CColorDialog 类的成
GetCurrentColor 将当前颜色设定为指定颜色 员函数获得各种信息


10
4 CP
rin
tDi
alog类
该类为打印对话框类,主要成员函数见表 6 39。
表6 39 CPr
intD
ial
og类
成 员 函 数 作 用 使 用 方 法
GetCopies 获得打印份数
GetDefaults 获得未在对话框中显示的设备缺省值
(1)构造 CPrintDialog 类的对象
GetDeviceName 获得当前所选的打印设备名称 (2)通过对象的数据成员 m_pd 初始化
GetDevMale 获得一个 DEVMODE 结构 打印对话框中各控件的值或状态
GetDriverName 获得当前所选的打印驱动名称 (3)调 用 成 员 函 数 DoModal 显 示 对
GetFromPage 获得打印队列中的开始页 话框
(4)应用程序通过 CPrintDialog 类的
GetTopPage 获得打印队列中的结束页
成员函数获得各种信息
GetPortName 获得当前所选的打印端口名称
GetPrinterDC 获得一个打印设备关联的控制


10
5 CF
indRep
laceD
iaog类

该类为查找替换对话框类,主要成员函数见表 6 40。 171

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表6 40 CF
indRe
pla
ceD
ial
og类
成 员 函 数 作 用 使 用 方 法
决定用户是否查找字符串的下一 (1)构造 CPrintDialog 类的对象
FindNext
目标 (2)通过对象的数据成员 m_fr 初始化查
GetFindString 返回当前查找字符串 找替换对话框中各控件的值或状态
GetReplaceString 返回当前替换字符串 (3)调用成员函数 DoModal 显示对话框
(4)应用程序通过 CFindReplaceDialog 类
用户是否需要替换所有匹配字
Replace All 的成员函数获得各种信息
符串

172
第七章 绘 图

图形设备接口 GDI(Graphics Device Interface)是 Windows 用来管理图形操作的一个


与设备无关的模块,是 Windows 结构的重要组成部分,它是一个可执行程序,接受 Windows 应
用程序的绘图请求(表现为 GDI 函数的调用),并将这些函数传递给相应的设备驱动程序,完
成在物理设备中的输出。Windows 程序设计的一大特点是设备无关性,在程序设计中,需要
画一个圆时,不必直接操作显示器或打印机,而通过 Windows 提供的设备环境 DC (Device
Context)完成。应用程序通过 DC 操作物理设备的层次关系如下:
应用程序—MFC 设备 DC 类—图形设备接口 GDI—设备驱动程序—物理设备。
本章要点:
* 设备环境类
* GDI 对象的创建、应用
* 绘图函数

1 设备环境类

为了支 持 GDI 绘 图,MFC 提 供 了 两 种 重 要 的 类:设 备 环 境 类 (CDC 类 )和 绘 图 对 象 类
(CGdiObject 类)。设备环境类用于设置绘图属性和绘制图形;绘图对象类封装了各种 GDI
绘图对象和方法。


11 CDC 类
CDC 类(设备环境类)的基类是 CObject,CDC 类封装了所有绘图函数。用法如下:
CDC * pDC;//定义一个 CDC 类的指针对象
pDC- > TextOut(100,100, "Hello V C++");//绘文字(文字也是一种图形)

CDC 类有四个派生类 CClientDC、CPaintDC、CWindowDC、CMetaFileDC。


(1)CClientDC 类
该类 用 于 客 户 区 绘 图, 它 在 构 造 函 数 中 封 装 了 GetDC 函 数, 在 析 构 函 数 中 封 装 了
ReleaseDC 函数。

CClientDC dc(this);
dc.TextOut(100,100, "Hello V C++");
(2)CPaintDC 类
该类是为 OnPaint 函数进行重画时所提供的显示描述表,该类的构造函数自动调用
BeginPaint 函数,而析构函数自动调用 EndPaint 函数。 在 PaintDC 上绘图直到下次重画时 173

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

才会显示。在应用程序运行时,改变窗口大小或移动覆盖在窗口 上 的 窗 口 或 对 话 框 时,
Windows 会发送 WM_PAINT 消息以通知客户区的变动,同时发送 WM_PAINT 消息通知非客户区
的变化。一般来说,对 WM_PAINT 消息的处理在 OnPaint 消息处理函数中进行;而在非客户区
的重画由系统完成。

CPaintDC dc(this);
OnPrepareDC(&dc);
dc.TextOut(100,100, "Hello V C++");
(3)CWindowDC 类
该类用于应用程序窗口包括客户区与非客户区(包括窗口边框、标题栏、控制按钮等 )的
绘制。
(4)CMetaFileDC 类
该类用于图源文件的绘制。图源文件记录一组 GDI 命令,可以通过这一组 GDI 命令重
建图形输出。使用 CMetaFileDC 时,所有的图形输出命令会自动记录到一个与 CMetaFileDC
相关的图源文件中。


12 映射模式
在绘图中所用的坐标都是按逻辑单位(像素)给出的。 1 个逻辑单位代表多少实际的长
度,这个关系模式称为映射模式(map mode)。共有三类八种映射模式,见表 7 1。
表7 1 Wi
ndows中的映射模式

映 射 模 式 说 明
逻辑点 像素点 MM_TEXT 1 个逻辑点= 1 个设备像素。正 X 在右,正 Y 在上
MM_TWIPS 1 个逻辑点 twip = 1/1 440 英寸,正 X 在右,正 Y 在上
MM_HIENGLISH 1 个逻辑点= 0001 英寸,正 X 在右,正 Y 在上
逻辑点 固定长度
MM_HIMETRIC 1 个逻辑点= 0001 厘米,正 X 在右,正 Y 在上
(约束映射)
MM_LOENGLISH 1 个逻辑点= 001 英寸,正 X 在右,正 Y 在上
MM_LOMETRIC 1 个逻辑点= 001 厘米,正 X 在右,正 Y 在上
MM_ANISOTROPIC 将逻辑单位换算为应用程序定义的值
逻辑点 非固定长
与 MM_ANISOTROPIC 类似,但将 X 轴上的某个单位转换为 Y 轴上
度(非约束映射) MM_ISOTROPIC
相同距离的单位

可 以 使 用 CDC:: GetMapMode 函 数 获 取 设 备 环 境 使 用 的 当 前 映 射 模 式, 使 用 CDC::


SetMapMode 函数设置映射模式。如:

CDC * pDC;
pDC- > SetMapMode(MM_LOMETRIC);

2 GDI对象与 CGd
7 iOb
j t类
ec
GDI 对象是 Windows 图形设备接口的抽象绘图工具,包括画笔、画刷、位图、调色板、字体
和区域等。这些工具在绘图中起着不同的作用,例如画笔用来指定所绘制图形的线条类型,
174 画刷用来指定填充图形内部的模式,而字体用来指定输出文本的字体等 。
第七章 绘 图
……………………………………………………………………………………………………………………………………………

CGdiObject 类的 基 类 是 CObject 类, CGdiObject 类 有 6 个 派 生 类, 下 面 介 绍 CPen、


CBrush、CFont 和 CBitmap 类。


21 画笔(
CPen)
CPen 类是从 CGdiObject 类中派生出来的类,是 Windows GDI 提供的绘制线条和图形的
工具,它可以以多种颜色和线形绘制各种直线 、曲线或图形边框。
(1)画笔的分类
画笔可分为堆画笔和自定义画笔,自定义画笔可分为修饰画笔和几何画笔。 堆画笔是
系统已定义好的画笔,包括 BLACK_PEN(黑色画笔)、WHITE_PEN(白色画笔)和 NULL_PEN(空画
笔即不画线或不画边框)三种画笔。自定义画笔对画笔的属性要进行编程设定,修饰画笔用
在对绘图有较高要求的场合;几何画笔用于绘制一般的直线、曲线等。
(2)几何画笔的三种属性
几何画笔具有下列三种属性:类型、宽度和颜色。 画笔的宽度以像素为单位,默认的宽
度为 1 个像素单位。在 Windows 中常用的画笔类型如表 7 2 所示。
表7 2 画笔类型

画 笔 类 型 描 述 画 笔 类 型 描 述
PS _SOLID 实 线 PS_NULL 空 线
PS_DASH 画 线 PS_INSIDEFRAME 内框线
PS_DOT 点 线 PS_GEOMETRIC 几何笔
PS_DASHDOT 点画线 PS _COSMETIC 修饰笔

(3)画笔的构造函数
CPen 类有三种构造函数,其原型分别为:

CPen();
CPen(int nPenStyle,int nWidth,COLORREF crColor);
CPen(int nPenStyle,int nWidth, const LOGBRUSH * pLogBrush,int nStyleCount = 0,
const DWORD * lpStyle = NULL);

前两个用于构造几何画笔,后一个用于构造修饰画笔。
参数:nPenStyle 为画笔类型见表 7 2;nwidth 为画笔宽度;crColor 为画笔颜色。
如果使用未带参数的构造函数,一般使用 CreatePen 成员函数来构造 CPen 对象。 如果
使用带参数的构造函数,则无需初始化。
构造画笔举例:创建了一个黑色、笔宽为 2 个像素点、绘制虚线的画笔。
① 采用一步构造法
一步构造法是指在定义画笔对象的同时就设定画笔的特性 。如:

CPen newPen (PS_DASH,2,RGB(255,255,255));

② 采用二步构造法
二步构造法是指先定义画笔对象,然后用 CreatePen 函数设定画笔的特性。如:

CPen newPen;
NewPen.CreatePen (PS_DASH,2,RGB(255,255,255)); 175

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


22 画刷(
CBr
ush)
画刷是应用程序用颜色填充控件 、窗口或其他区域的 GDI 对象。
(1)画刷的分类
画刷可分为:堆画刷、实心画刷、阴影画刷和图案画刷。堆画刷包括 6 种,见表 7 3。
表7 3 堆画刷的类型

堆画刷类型 说 明
BLACK_BRUSH 黑色画刷
WHITE_BRUSH 白色画刷
DKGRAY_BRUSH 暗灰色画刷
GRAY_BRUSH 灰色画刷
LTGRAY_BRUSH 淡灰色画刷
NULL_BRUSH 空画刷,内部不填充

(2)画刷的三种属性
画刷有下列三种属性:颜色、图案和阴影类型。阴影类型见表 7 4。
表7 4 画刷的阴影类型

阴影类型 说 明 索引号
HS_HORIZONTAL 阴影线为水平线 0
HS_VERTICAL 阴影线为垂直线 1
HS_BDIAGONAL 阴影线为+45°斜线(从左到右) 2
HS_FDIAGONAL 阴影线为-45°斜线(从右到左) 3
HS_CROSS 阴影线为交叉的水平线和垂直线 4
HS_DIAGCROSS 阴影线为斜向交叉的直线 5

(3)画刷的构造函数
CBrush 类有四种构造函数,其原型分别为:

CBrush();
CBrush(COLORREF crColor);
CBrush(int nIndex,COLORREF crColor);
CBrush(CBitmap*pBitmap);

参数:nIndex 代表阴影的类型,见表 7 4。
(4)创建画刷
① 一步法创建画刷
CBrush newBrush(RGB(0,0,255));//创建绿色实心画刷
//创建具有 45 度向上斜线阴影的绿色画刷
CBrush newBrush(HS_FDIAGONAL, RGB(0,0,255));
//创建位图图案的画刷
CBitmap pBitmap;
176 CBrush newBrush(&pBitmap);
第七章 绘 图
……………………………………………………………………………………………………………………………………………

② 两步法创建画刷
● 用 CreateSolidBrush(DWORD crColor)函数创建一个实心画刷。
例:创建一个黑色实心画刷。

CBrush newBrush;
newBrush. CreateSolidBrush(RGB(255,255,255));
● 用 CreateHatchBrush (int nIndex,DWORD crColor )函 数 创 建 一 个 带 阴 影 的 画 刷,
nIndex 取值见表 7 4。
例:创建一个红色水平线填充画刷。

CBrush newBrush;
newBrush. CreateHatchBrush(HS_HORIZONTAL,RGB(255,0,0));
● 用 CreatePatternBrush(CBitmap * pBitmap)函数创建一个图案画刷。


23 字体(
CFo
nt)
文本以某种字体方式显示输出,字体是一种 GDI 对象。 文本输出之前,首先创建字体,
然后用所创建的字体输出文本。
(1)字体的分类
Windows 使用两种字体族:固定宽度字体和可变宽度字体。
Windows 定义了三种不同的字体类型,分别是以下三种。
光栅字体:也称位图字体,是一种与设备有关的字体,其显示与屏幕分辨率有关。
向量字体:由图形线段组成的字体,与设备无关,但显示速度较慢。
True Type 字体:由图形线段定义字符外形的字体,因其显示效果好,故应用最广。
(2)字体的属性
字体一般有 14 个属性,用 CreatFont 函数创建字体的 14 个属性,见清单 7 1。
清单7 1 字体的属性

BOOLCr eat
eFont(
Int nHeight, //字符高度
Int nWidth, //字符宽度
Int nEscapement, //行倾斜度
Int nOrientation, //字符旋转度
Int nWeight, //字符线宽
BYTE bItalic, //斜体
BYTE bUnderline, //下画线
BYTE cStrikeOut, //删除线
BYTE nCharSet, //字符集
BYTE nOutPrecision, //输出匹配
BYTE nClipPrecision, //区域裁剪
BYTE nQuality, //属性匹配
BYTE nPitchAndFamily, //字体间距
LPCTSTR lpszFacename //字体名称
);

其中字符高度的值可为正数、负数或 0。 大于 0 时,输出字符的纯高度为 nHeight 个逻 177



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

辑单位(每个逻辑单位高度取决于坐标系纵轴单位长度 );小于 0 时,为字符纯高度和字符上


标高度之和,即字符总高度为 nHeight 个逻辑单位;为 0 时,字符高度为缺省设置。
字符宽度值为 0 时,为缺省宽度,如果前一字符高度参数不为 0,则以其高度参数值为字
符高度,按字体原本设计的高宽比为比例设定字符高度;字符宽度值不为 0 时,字体的高宽
比将按新比例显示。字体宽度为 nWidth 个逻辑单位。
行倾斜度以 01 度为单位,逆时针全行旋转 nEscapement 个单位的角度。
字符旋转度是文本行中每个字符自转 nOrientation 个单位的角度,单位值也是 01 度。
当纵轴向下递减时,字符逆时针旋转;纵轴向下递增时,字符顺时针旋转。但在整行转动时,
nOrientation 的值将自动与 nEscapement 取值相同,忽略该参数。
字符线宽 nWeight 的值域为 0 到 1 000,最常用的线宽常量 FW_NORMAL 的值为 400,可据
此确定字体所需线宽。
斜体、下画线、删除线是文字处理常用的手段,灵活应用是很有益处的。
最后一个参数字体名称,如果为 Null,则 Windows95/98 将视字体大小自动选择最佳
字体。
(3)LOGFONT 数据结构
LOGFONT 数据结构包含了逻辑字体的 14 个特征。LOGFONT 结构见清单 7 2。
清单7 2 LOGFONT 结构定义


ype
de fstructtagLOGFONT
LONG lfHeight; //以逻辑单位指定字体的高度
LONG lfWidth; //以逻辑单位指定字体中字符的平均宽度
LONG lfEscapement; //以十分之一度为单位指定以该字体写出的一行文字的角度
LONG lfOrientation; //以十分之一度为单位指定每个字符的基线的角度
LONG lfWeight; //字体线宽(0~1 000)
BYTE lfItalic; //若设置为 TRUE,则指定为斜体字体
BYTE lfUderline; //若设置为 TRUE,则指定为下画线字体
BYTE lfStrikeOut; //若设置为 TRUE,则指定为删除线字体
BYTE lfCharSet; //指定字符的字体集
BYTE lfOutPrecision; //指定输出精度
BYTE lfClipPrecision; //指定裁剪精度
BYTE lfQuality; //指定输出质量
BYTE lfPitchAndFamily; //指定字体的字距和字体族
CHAR lfFaceNameLF_FACESIZE; //指向指定字体的字样名以 NULL 结束的字符串,长度不得超过
32 个字符。
LOGFONT;

在使用逻辑字体前,先将其选入设备环境,选入后逻辑字体成为物理字体。
(4)TEXTMETRIC 数据结构
TEXTMETRIC 结构可以设置或获取文本字符的有关信息 。其定义见清单 7 3。
清单7 3 TEXTMETRIC 结构定义


ype
de fstructtagTEXTMETRIC
int tmHeight; //字符高度
int tmAscent; //字符基线以上高度
int tmDescent; //字符基线以下高度
178
第七章 绘 图
……………………………………………………………………………………………………………………………………………

int tmInternalLeading; //字体点尺寸和物理尺寸间的差别


int tmExternalLeading; //两行字符间距
int tmAveCharWidth; //字体中所有字符的平均宽度
int tmMaxChaWidth; //字体中最宽字符的宽度
int tmWeight; //字体线宽
BYTE tmItalic; //不为 0 时字体为斜体
BYTE tmUnderlined; //不为 0 时字体带下划线
BYTE tmStruckOut; //不为 0 时字体带删除线
BYTE tmFirstChar; //字体中第一个字符
BYTE tmLastChar; //字体中最后一个字符
BYTE tmDefaultChar; //字体中缺省字符
BYTE tmBreakChar; //定义字中断的字符
BYTE tmPitchAndFamily; //字体间距和族
BYTE tmCharSet; //字符集
int tmOverhang; //特殊字体的额外宽度
int tmDigitizedAspectX; //设备的水平特性
int tmDigitizedAspectY; //设备的垂直特性
}TEXTMETRIC

(5)字体的创建
CFont 类有四种字体创建函数,其原型分别为:
BOOL CreateFont(14 个参数),见清单 7 1。
BOOL CreateFontIndirect(const LOGFONT * lpLogFont );参 数 * lpLogFont 为 逻 辑 字 体
指针。
BOOL CreatePointFont(int nPointSize, LPCTSTR lpszFaceName, CDC*pDC = NULL);
BOOL CreatePointFontIndirect(const LOGFONT*lpLogFont, CDC*pDC = NULL);
(6)使用字体对话框
使用字体对话框的程序段见清单 7 4。
清单7 4 使用字体对话框

COLORREF m_TextColor; //定义字体颜色变量


LOGFONT m_logfont; //定义逻辑字体 LOGFONT 结构变量
CFontDialog dlg; //声明字体
//定义字体通用对话框的初值
dlg.m_cf.lpLogfont = & m_logfont;
dlg.m_cf.rgbColors = m_color;
//打开通用对话框
if(dlg.DoModal()= = IDOK)
{ dlg.GetCurrentFont(&m_logfont);//获取字体对话框的字体设置
m_Textcolor = dlg.GetColor(); //获取字体对话框的字体颜色设置
};


24 位图(
CBtmap)

CBitmap 位图类封装了 Windows 的图形设备接口位图,提供管理位图的成员函数。 要使
用该对象,首先构造该对象,然后通过初始化成员函数连接位图句柄到该对象,最后调用该
对象的成员函数。 179

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(1)位图结构
位图结构主要定义位图的有关信息,其定义见清单 7 5。

清单7 5 位图结构


ypedefstructtagBITMAP

int bmType;//位图类型
int bmWidth;//位图宽度
int bmHeight;//位图高度
int bmWidthBytes;//
BYTE bmPlanes;//位图的位面数
BYTE bmBitsPixel;//像素位数
LPVOID bmBits;//
 BITMAP;

(2)创建位图
① 用 CreateBitmap 函数创建位图
函数原型:CreateBitmap (int nWidth,int nHeight,int nPlanes,int nBits,const *
volid lpBits);
参数:nWidth 和 nHeight:以像素为单位的位图的宽度与高度;
nPlanes:指定位图的位面数;
nBits:各个像素的位数;
lpBits:在创建位图时,位图图案通常不被指定,因此在这种情况下一般为 NULL。
例:创建一个宽 50、高 80、1 个位面、每个像素 24 位并且没有初始化位图。

CBitmap bitmap;
bitmap. CreateBitmap(50,80,1,24,NULL);

② 用 CreateBitmapIndirect 函数创建位图
例:创建一个宽 150、高 180、1 个位面、每个像素 16 位的位图。

BITMAP bm;
bm. bmType = 0;
bm. bmWidth = 150;
bm. bmHeight = 180;
bm. bmWidthBytes = 200;
bm. bmPlanes = 1;
bm. bmBitsPixel = 16;
bm. bmBits = NULL;
bitmap.CreateBitmapIndirect(&bm);

(3)加载位图
LoadBitmap 函数可以加载位图。函数原型如下:
BOOL LoadBitmap(LPCTSTR lpszResourceName);
180 BOOL LoadBitmap(UINT nIDResource);
第七章 绘 图
……………………………………………………………………………………………………………………………………………

参数:lpszResourceName:资源文件名;
nIDResource:资源 ID 号。
使用 LoadBitmap 函 数 加 载 位 图 无 需 使 用 CreateBitmap 或 其 他 位 图 创 建 函 数,
LoadBitmap 函数创建与当前视频系统兼容的位图,同时不考虑资源位图的格式。 以下是一
个具有 IDB_BITMAP1 资源 ID 的位图加载到 CBitmap 对象中的例子:

CBitmap Bitmap;
Bitmap.LoadBitmap(IDB_BITMAP1);


3 CPo
int、
CSze 和 CRec
i t
CPoint,CSize 和 CRect 分别表示点类、矩形大小类及矩形类。 这些为简单的数据类,没
有基类,这些类分别封装了标准结构 POINT,SIZE 和 RECT。


31 CPo
int类
CPoint 类封装了标准结构 POINT,结构 POINT 表示一个屏幕上的二维点,它的定义如下:

typedef struct tagPOINT


 LONG x;
LONG y;
POINT;
其中 x、y 分别是点的横坐标和纵坐标。
构造 CPoint 类对象举例如下:

CPoint point1,point2;
point1.x = 100;
point1.y = 100;
point2.x = 200;
point2.y = 200;


32 CS
ize类
CSize 类封装了标准结构 SIZE,结构 SIZE 表示一个矩形的长度和宽度,只确定了矩形的
大小,没有确定具体的位置,它的定义如下:

typedef struct tagSIZE


 int cx;
int cy;
SIZE;
其中 cx、cy 分别是矩形的长度和宽度。
构造 CSize 类对象举例如下:

CSize size1; 181



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

size1.cx = 100;
size1.cy = 100;
或 CSize size(100,100)


3 t类
3 CRec
CRect 类封装了标准结构 RECT,结构 RECT 表示一个矩形,与 SIZE 不同的是它既确定了
大小又确定了位置,它的定义如下:

typedef struct tagRECT


 LONG left;
LONG top;
LONG right;
LONG bottom;
RECT;
其中 left、top 表示矩形左上角顶点的横坐标、纵坐标;right、bottom 表示矩形右下角
顶点的横、纵坐标。
构造 CRect 类对象举例如下:

CRect rect1(point1,point2);
CRect rect2(10,10,50,100);

4 常见的绘图任务


41 绘制图形的一般步骤
(1)定义 CDC 类或其派生类的对象
(2)定义 GDI 对象
(3)创建 GDI 对象
(4)用 CDC::SelectObject 函数选择新的 GDI 对象,保存老的 GDI 对象
(5)做一些绘图工作
(6)还原老的 GDI 对象
绘图步骤举例见清单 7 6。
清单7 6 绘图步骤举例

CClientDC dc(this); //定义 CClientDC 类的对象


CPen newPen,* pOldPen; //定义画笔对象 newPen 和用于保存老画笔的指针对象
* pOldPen
NewPen.CreatePen(PS_SOLID,1,RGB(255,0,0)); //创建一宽度为 1 的红色实线
pOldPen = dc.SelectObject(&newPen) //选择新画笔,保存老画笔
dc.Rectangle(100,100,200,300); //绘制矩形
SelectObject(pOldPen); //还原老画笔

182
第七章 绘 图
……………………………………………………………………………………………………………………………………………


42 绘制文本
使用设备环境选中的字体、文本颜色和背景颜色可以绘制文本 。绘制文本的步骤如下。
(1)字体大小计算
通过调用 GetTextMetric()函数返回当前使用字体的尺寸描述 。
(2)文本属性的设置
设置文本颜色:CDC::SetTextColor(int nColor)
设置背景颜色:CDC::SetBkColor(int nColor)
设置背景模式:CDC::SetBkMode(intnBkMode)
参数:nBkMode 为背景模式,可取 OPAQUE (不透明,用设置的背景色填充文本范围 )和
TRANSPARENT(透明,背景色不起作用)
(3)输出文本函数
GDI 提 供 了 5 个 文 本 输 出 函 数: DrawText ()、ExtTextOut ()、GrayString ()、
TabbedTextOut()、TextOut()。
其中最常用的是 TextOut(),其函数原型支持两种参数方式:

virtual BOOL TextOut(int x,int y,LPCTSTR lpszString, int nCount)和


BOOL TextOut(int x, int y, const CString & str)。

参数 x,y:输出文本行的左上角坐标值。 LpszString:指向输出文本的指针,nCount:输
出字符数,str:输出字符串本身。
BOOL 型返回值返回文本是否正常输出 。
绘制文本举例:

CClientDC dc(this);
dc.SetTextColor(RGB(255,0,0));//设置文字颜色为红色
dc.SetBkColor(RGB(0,0,255));//设置文字背景色为蓝色
dc.TextOut(50,50, "输出文本 ");


43 绘点
SetPixel 函数的功能是在指定坐标处按指定颜色绘一点 。
(1)绘点函数

COLORREF SetPixel(int x,int y, COLORREF crColor);


COLORREF SetPixel(POINT point, COLORREF crColor);

(2)绘点举例

CClientdc dc(this);
dc.SetPixel(200,100,RGB(128,0,128));


44 绘直线
MoveTo 函数的功能是将直线起点移动到指定坐标处,LineTo 函数的功能是从起点开始 183

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

画直线到终点。
(1)绘直线函数
MoveTo 函数原型如下:

CPoint MoveTo(int x,int y);


CPoint MoveTo(POINT point);

LineTo 函数原型如下:

BOOL LineTo(int x, int y);


BOOL LineTo(POINT point);
(2)绘直线举例

CClientdc dc(this);
dc.MoveTo(100,100);
dc.LineTo(200,300);


45 绘矩形
(1)绘矩形函数
Rectangle 函数的功能是使用当前选定的画笔绘制一个矩形,并使用当前选定的画刷填
充矩形。函数原型如下:

BOOL Rectangle(int x1, int y1, int x2, int y2);


BOOL Rectangle (LPCRECT lpRect);

参数:x1、y1 和 x2、y2:分别指定了矩形的左上和右下点 。
LpRect:指向矩形的指针,定义了矩形的四个角。
(2)绘矩形举例

CClientdc dc(this);
dc. Rectangle (100,100,200,300);


46 绘椭圆
(1)绘椭圆函数
Ellipse 函数的功能是使用当前选定的画笔绘制一个椭圆,并使用当前选定的画刷填充
椭圆。函数原型如下:

BOOL Ellipse (int x1, int y1, int x2, int y2);
BOOL Ellipse(LPCRECT lpRect);

参数:x1、y1 和 x2、y2:分别指定了包围椭圆的矩形的左上和右下点;
lpRect:定义包围椭圆的矩形区域。
(2)绘椭圆举例

CClientdc dc(this);
184 dc. Ellipse (100,100,200,300);
第七章 绘 图
……………………………………………………………………………………………………………………………………………


47 绘弧线
(1)绘弧线函数
Arc 函数的功能是绘弧线,绘弧线其实是先绘一个椭圆,然后取其中的一部分,即为圆
弧。函数原型如下:

BOOL Arc(nit xl,int y1,int x2,int y2,int x3,int y3,int x4,int y4);
BOOL Arc(LPCRECT lpRect,POINT ptStart,POINT ptEnd);

参数:x1、y1 和 x2、y2:分别指定了外接矩形的左上和右下点;
x3、y3 和 x4、y4:指定了弧的起点和弧的终点;
lpRect:指定了弧的外接矩形;
ptstart、ptEnd:指定了弧的起点和终点。
(2)绘弧线举例

CClientdc dc(this);
dc.Arc(100,100,400,200,80,120,300,150);


48 绘位图
BitBlt 函数从源设备环境拷贝一个位图到当前设备环境 。 StretchBilt 函数是将位图
从源矩形拷贝到目标矩形中,并按需要拉伸或压缩位图使其适应目标矩形的大小 。
(1)绘位图函数

BOOL BitBlt (int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int
ySrc, DWORDdwRop);

参数:x、y:目的矩形左上角的逻辑 x 和 y 坐标;
nWidth、nHeight:目的矩形和源位图的宽度和高度(按逻辑单位);
pSrcDC:指向一个 CDC 对象的指针,用于标识从中拷贝位图的源设备环境;
xSrc、ySrc:源位图左上角的逻辑 x 和 y 坐标
dwRop:要执行的光栅操作码,其操作码定义了图形设备接口 (GDI)在输出操作中如何组
合颜色,可取的值见表 7 5。
表7 5 dwRo
p取值
光栅操作码 说 明
BLACKNESS 使所有输出变成黑色
DSTINVERT 将目标位图反相
MERGECOPY 用 AND 操作符将模式与源位图结合
MERGEPAINT 用 OR 操作符将反相的源位图与目标位图结合
NOTSRCCOPY 将反相的源位图拷贝到目标位图
NOTSRCERASE 将目标和源位图的 OR 操作结果反相
PATCOPY 将模式拷贝到目标位图
PATINVERT 用 XOR 操作符将目标位图与模式结合
PATPAINT 用 OR 操作符将反相的源位图与模式结合后使用 OR 操作符将结果与目标位图结合 185

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
光栅操作码 说 明
SRCAND 用 AND 操作符将目标位图像素与源位图像素结合
SRCCOPY 将源位图拷贝到目标位图上
SRCERASE 使目标位图反相,并使用 AND 操作符将此结果与源位图结合
SRCINVERT 用 XOR 操作符将目标位图与源位图结合
SRCPAINT 用 OR 操作符将目标位图像素与源位图像素结合
WHITENESS 使所有输出变成白色

BOOL StretchBlt (int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc,
int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop);
参数:nSrcWidth、nSrcHeight 表示源矩形的逻辑宽度和逻辑高度;
其余参数同 BilBlt 函数中的参数。
(2)显示位图举例

pDC- > BitBlt(10,10,100,200, & MemDC, 0,0, SRCCOPY);

显示位图完整的过程见清单 7 11。

5 绘图编程实例


5 I对象和基本绘图函数的应用编程实例
1 GD
【例7 1】 Ex07_1GDI 对象和基本绘图函数的应用,具体说明见表 7 6。
表7 6 例7 1具体说明

项 目 图示和说明

图(
a) 画笔 图(
b) 画刷 图(
c) 字体
程序界面

186 图(
d) 位图 图(
e) 字体对话框 图(
f) 再单击字体菜单
第七章 绘 图
……………………………………………………………………………………………………………………………………………

续 表
项 目 图示和说明

(1)没有点击过菜单字体对话框的操作:
点击菜单“画笔”将出现图(a);点击菜单画刷将出现图(b);点击菜单字体将出现图(c)点
击菜单“位图”将出现图(d);重复上述操作重复出现图(a)~图(d)
(2)点击过菜单“字体”对话框的操作:
程序功能
点击菜单“画笔”将出现图(a);点击菜单“画刷”将出现图(b);点击菜单“字体”将出现图
(f);点击菜单“位图”将出现图(d);重复上述操作重复出现图(a)(b)(f)(d)
(3)改变字体对话框字体的选择,图(f)中“CFontDialog 对 话 框”将 根 据 选 择 的 字 体 重 新
改变

(1)新建一单文档应用程序 Ex07_1
(2)资源准备
① 编辑菜单资源与添加菜单消息函数(见表 7 7)
表7 7 菜单消息函数

菜 单 名 ID 号 消 息 消 息 函 数
字体对话框... ID_FONTDLG COMMAND OnFontdlg();
COMMAND OnGdiPen()
画笔 ID_GDI_PEN
UPDATE_COMMAND_UI OnUpdateGdiPen(CCmdUI*pCmdUI)
COMMAND OnGdiBrush()
画刷 ID_GDI_BRUSH
UPDATE_COMMAND_UI OnUpdateGdiBrush(CCmdUI*pCmdUI)
COMMAND OnGdiFont()
字体 ID_GDI_FONT
UPDATE_COMMAND_UI OnUpdateGdiFont(CCmdUI*pCmdUI)
位图 ID_GDI_BITMAP COMMAND OnGdiBitmap()

② 位图资源的准备
选择 Resource View 选项卡,鼠标放在 Ex07_1 resources 文件夹上,然后单击鼠标右键,
弹出图 7 1 所示的右键菜单;点击“Insert...”,弹出如图 7 2 所示的对话框;选择 Bitmap,
将 ID 号为 IDB_BITMAP1 的资源插入项目中;编辑位图,见图 7 3。

图7 1 右键菜单 图7 2 “
Ins
ertRe
sou
rce”对话框 图7 3 编辑好的位图

(3)字体的设置
① 在 Ex07_1View.h 中 定 义 变 量 m_ logfont,m_ logfont1 和 m_ TextColor (见 清 单 7
6)

187

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

07_
清单7 6 声明变量和函数(在 Ex 1Vi h文件中)
ew.


las
sCEx 07_ 1Viewpub licCVi ew

protected:
LOGFONT m_logfont,m_logfont1; //逻辑字
COLORREF m_TextColor; //文字颜色
enumPens,Brushes,Fonts,Bitmapsm_GdiShow; //声明枚举变量,在(4)① 步添加
//声明 4 个函数,在(3)① 步要求定义
void ShowFonts();
void ShowBrushes();
void ShowPens();
void ShowBitmaps(CDC * pDC);
//声明 4 个 BOOL 变量,在(4)① 步添加
BOOL bPens;
BOOL bBrushes;
BOOL bFonts;
BOOL bBitmaps;

② 编写菜单消息函数 OnFontdlg()设置字体(见清单 7 7)

07_
清单7 7 字体设置 (在 Ex 1Vi
ew.
cpp文件中)


oidCEx07_1View OnFo ntdlg()//字体设置
{ CFontDialog dlg;
if(dlg.DoModal()= = IDOK)
{ dlg.GetCurrentFont(&m_logfont1);
m_TextColor = dlg.GetColor();

(4)画笔、画刷、字体和位图的变换显示
① 手工添加枚举变量的声明(见清单 7 6)
② 在构造函数中给枚举变量 m_GdiShow 赋初值(见清单 7 8)

07_
清单7 8 构造函数(在 Ex 1Vi
ew.
cpp文件中)

07_
CEx 1View CEx07_
1Vi //构造函数
ew ()
{ m_GdiShow = Pens;//给枚举变量赋初值
//给 4 个 BOOL 变量赋初值,在(4)② 步添加
bPens = 1; //初始显示画笔
bBrushes = 0;
bFonts = 0;
bBitmaps = 0;

③ 在 OnDraw()函数中使用显示变量(见清单 7 9)
188
第七章 绘 图
……………………………………………………………………………………………………………………………………………

清单7 9 OnDr 07_


aw ()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx07_1V iewOnDr aw (CDC pDC )
{ CEx07_1Doc*pDoc = GetDocument();
ASSERT_VALID(pDoc);
switch(m_GdiShow)
{ case Pens:
ShowPens();
break;
case Brushes:
ShowBrushes();
break;
case Fonts:
ShowFonts();
break;
case Bitmaps:
ShowBitmaps(pDC);
break;

(5)添加和编写 4 个函数
① 添加 4 个函数:ShowPens(
)、ShowBrushes(
)、ShowFonts(
)和 ShowBitmaps(
CDC * pDC)
在 ClassView 中鼠标放在 CEx07_1View 类上,点击右键,选择 Add Member Function,在
Function Type 中输入 void,在 Function Declaration 中输入 ShowFonts,访问模式选择为
protected,单击 OK。按同样的方法定义其余三个函数,见清单 7 6。
② 编写 ShowPens()函数(见清单 7 10)
清单7 10 Sh
owPe
n 07_
s()函数(在 Ex 1Vi
ew.
cpp文件中)

oidCEx07_ 1View ShowPens()

CClientDC dc(this); //定义对象
CPen newPen1,newPen2,newPen3; //定义 3 个新画笔
CPen * pOldPen; //定义老画笔指针
newPen1.CreatePen(PS_SOLID,6,RGB(255,0,0)); //创建实线 6 个点宽的红色画笔
pOldPen = dc.SelectObject(&newPen1); //保存原画笔,选中新画笔
dc.MoveTo(20,20); //将画笔移动到(20,20)点
dc.LineTo(200,20); //画直线
dc.SelectObject(pOldPen); //恢复原画笔
newPen2.CreatePen(PS_DASH,1,RGB(0,255,0)); //创建画线 1 个点宽的绿色画笔
pOldPen = dc.SelectObject(&newPen2); //保存原画笔,选中新画笔
dc.MoveTo(20,80); //将画笔移动到(20,80)点
dc.LineTo(200,80); //画直线
dc.SelectObject(pOldPen); //恢复原画笔
newPen3.CreatePen(PS_DASHDOT,1,RGB(0,0,255)); //创建点画线 1 个点宽的蓝色画笔
pOldPen = dc.SelectObject(&newPen3); //保存原画笔,选中新画笔
dc.MoveTo(20,140); //将画笔移动到(20,140)点
dc.LineTo(200,140); //画点画线
dc.SelectObject(pOldPen); //恢复原画笔
} 189

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

③ 编写 ShowBrushes()函数(见清单 7 11)
清单7 11 Sh
owBr
ush
e 07_
s()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx07_ 1V i
ew ShowBr u
shes()//画刷
{ int y = 0;
CClientDC dc(this);
CPen newPen(PS_SOLID,1,RGB(0,0,0)); //创建新画笔
CPen * pOldPen;
pOldPen = dc.SelectObject(&newPen); //保存原画笔,选中新画笔
for(UINT i = 0;i<7;++i) //循环创建 7 个画刷,绘 7 个矩形
{ CBrush newBrush;
CBrush * pOldBrush;
if(i = = 6)
newBrush.CreateSolidBrush(RGB(255,0,0)); //创建实心画刷
else
newBrush.CreateHatchBrush(i,RGB(0,0,255));//创建带阴影的画刷
pOldBrush = dc.SelectObject(&newBrush); //保存原画刷,选中新画刷
y+ = 40;
dc.Rectangle(20,y,200,y+30); //绘矩形
dc.SelectObject(pOldBrush); //恢复原画刷

dc.SelectObject(pOldPen); //恢复原画笔

④ 编写 ShowFonts()函数(见清单 7 12)
清单7 12 Sh
owFo
nt 07_
s()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx07_1V iew ShowFon ts()//字体
{ CClientDC dc(this);
CFont newFont1,newFont2,newFont3;
CFont* pOldFont;
//由 CreateFont 函数创建字体
newFont1.CreateFont(25, 0, 0, 0,400,
false,false,false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH|FF_SWISS,
" 宋体");
pOldFont = dc.SelectObject(&newFont1); //保存原字体,选中新字体
dc.SetTextColor(RGB(0,0,0)); //设置字体颜色
dc.SetBkColor(RGB(255,255,0)); //设置背景颜色
dc.TextOut(10,20,"这是由 CreateFont 函数创建的字体"); //在 x = 10,y = 20 处输出文字
dc.SelectObject(pOldFont); //恢复老字体
//由 CreateFontIndirect 函数创建字体
m_logfont.lfHeight = 20;
m_logfont.lfWidth = 0;
m_logfont.lfEscapement = 0;
m_logfont.lfOrientation = 0;
190 m_logfont.lfWeight = FW_NORMAL;
第七章 绘 图
……………………………………………………………………………………………………………………………………………

m_logfont.lfItalic = FALSE;
m_logfont.lfUnderline = FALSE;
m_logfont.lfStrikeOut = FALSE;
m_logfont.lfCharSet = GB2312_CHARSET;
m_logfont.lfOutPrecision = OUT_STROKE_PRECIS;
m_logfont.lfClipPrecision = CLIP_STROKE_PRECIS;
m_logfont.lfQuality = DRAFT_QUALITY;
m_logfont.lfPitchAndFamily = VARIABLE_PITCH|FF_MODERN;
lstrcpy(m_logfont.lfFaceName, _T("楷体_GB2312" ));
newFont2.CreateFontIndirect(&m_logfont);
pOldFont = dc.SelectObject(&newFont2); //保存老字体,选择新字体
dc.SetTextColor(RGB(0,0,255)); //设置字体颜色
dc.SetBkColor(RGB(100,255,0)); //设置背景色
dc.TextOut(10,80,"这是由 CreateFontIndirect 函数创建的字体");//在 x = 10,y = 80 处输出文字
dc.SelectObject(pOldFont); //恢复老字体
//由字体对话框设置的字体
newFont3.CreateFontIndirect(&m_logfont1);
pOldFont = dc.SelectObject(&newFont3); //保存老字体,选择新字体
dc.SetTextColor(m_TextColor); //设置字体颜色
dc.SetBkColor(RGB(0,255,255)); //设置背景色
dc.TextOut(10,120,"CFontDialog 对话框"); //在 x = 10,y = 120 处输出文字
dc.SelectObject(pOldFont); //恢复老字体

⑤ 编写 ShowBitmaps()函数(见清单 7 13)
清单7 13 Sh
owB
itma
p 07_
s()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx07_ 1ViewShowB itma ps(CDC pDC) //位图
{ CBitmap Bitmap; //定义位图类对象
Bitmap.LoadBitmap(IDB_BITMAP1); //调用位图
CDC MemDC; //定义 CDC 类对象
MemDC.CreateCompatibleDC(pDC); //创建一个与指定设备一致的内存设备描述表
CBitmap * pOldBitmap = MemDC.SelectObject(&Bitmap);//保存老位图,选择新位图
BITMAP bm;
Bitmap.GetObject(sizeof(BITMAP),&bm); //获得位图的结构
pDC- >BitBlt(10,10,bm.bmWidth,bm.bmHeight,&MemDC,0,0,SRCCOPY); //显示位图
MemDC.SelectObject(pOldBitmap); //恢复老位图

(6)用菜单实现变换显示
① 在 CEx07_1View.h 中声明 4 个 BOOL 变量,用于更新菜单的操作(见清单 7 6)
② 在构造函数中给 4 个 BOOL 变量赋初值(见清单 7 8)
③ 编写菜单消息函数和菜单更新消息函数(见清单 7 14)
07_
清单7 14 菜单消息函数和菜单更新消息函数(在 Ex 1Vi
ew.
cpp文件中)

//菜单消息处理函数
vo
idCEx07_ 1V iew
OnGd
iPe //点击画笔菜单
n()
{ m_GdiShow = Pens;
bPens = 1;
191

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

bBrushes = 0;
bFonts = 0;
Invalidate();

vo
idCEx07_ 1V iew
OnGdi Bru
sh()//点击画刷菜单
{ m_GdiShow = Brushes;
bPens = 0;
bBrushes = 1;
bFonts = 0;
Invalidate();

vo
idCEx07_ 1V iew
OnGdi Fot()
n //点击字体菜单
{ m_GdiShow = Fonts;
bPens = 0;
bBrushes = 0;
bFonts = 1;
Invalidate();

vo
idCEx07_ 1V iew
OnGdi Bi
tmap()//点击位图菜单
{ m_GdiShow = Bitmaps;
bPens = 0;
bBrushes = 0;
bFonts = 0;
bBitmaps = 1;
Invalidate();

//菜单更新消息函数
vo
idCEx07_ 1V iew
OnUpd at
eGdiPen(CCmdUI pCmdUI)//更新画笔菜单
{ pCmdUI- >SetCheck(bPens);

vo
idCEx07_ 1V iew
OnUpd at
eGdiBrush(CCmdUI pCmdUI)//更新画刷菜单
{ pCmdUI- >SetCheck(bBrushes);

vo
idCEx07_ 1V iew
OnUpd at
eGdiFont(CCmdUI pCmdUI)//更新字体菜单
{ pCmdUI- >SetCheck(bFonts);

vo
idCEx07_ 1V iew
OnUpd at
eGdiBitmap(CCmdUI pCmdUI)//更新位图菜单
{ pCmdUI- >SetCheck(bBitmaps);

(7)编译和运行


52 鼠标绘图编程实例
【例7 2】 Ex07_2 鼠标绘图的实现,具体说明见表 7 8。
(1)新建一单文档应用程序 Ex07_2
(2)编辑绘图工具栏并将工具栏载入应用程序界面
① 绘图工具栏按钮的 ID 号依次为:ID_MDRAW_LINE、ID_MDRAW_RECTANGLE、ID_MDRAW_
ELLIPSE;
192
第七章 绘 图
……………………………………………………………………………………………………………………………………………

② 在 MainFrm.h 文件中声明一个 CToolBar 对象;


protected:
CToolBar m_DrawToolBar;//声明工具栏对象
表7 8 例7 2具体说明

项 目 图示和说明

程序界面

(1)点击菜单:鼠标绘图- >直线或工具栏的直线按钮,可在客户区绘制出直线
点击菜单:鼠标绘图- >矩形或工具栏的矩形按钮,可在客户区绘制出矩形
点击菜单:鼠标绘图- >椭圆或工具栏的椭圆按钮,可在客户区绘制出椭圆
程序功能
从键盘可以直接输入字符
(2)点击“保存”菜单或工具栏保存按钮,打开保存对话框,输入文件名可将绘制的图
形保存到磁盘

③ 在 MainFrm.cpp 文件中编写实现工具栏载入的代码(见清单 7 15)。


清单7 15 绘图工具栏载入(在 Ma
inF
rm.
cpp文件中)


ntCMa inF rame OnCreate(LPCREATESTRUCTl pCrea
teStruct)
{...
//绘图工具栏载入
if (! m_DrawToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
|CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)||
! m_DrawToolBar.LoadToolBar(IDR_DRAW_TOOLBAR))
{TRACE0("Failed to create toolbar\n" );
return-1;//创建失败

//绘图工具栏浮动
m_DrawToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_DrawToolBar);
return 0;

(3)添加和编写点击绘图工具栏按钮的消息函数
用 ClassWizard 添 加 3 个 工 具 栏 按 钮 消 息 函 数 和 3 个 更 新 消 息 函 数 (见 清
单 7 16 )。

193

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

07_
清单7 16 绘图工具栏按钮消息函数和更新消息函数(在 Ex 2Vi
ew.
cpp文件中)


oi 07_
dCEx 2View OnMd rawLine()//绘直线
{ m_DrawType = 1;//m_DrawType 表示绘图类型,在 Ex07_2View.h 文件中定义


oi 07_
dCEx 2View OnMd rawRectanl
g //绘矩形
e()
{ m_DrawType = 2;


oi 07_
dCEx 2View OnMd
rawE
lli
ps //绘椭圆
e()
{ m_DrawType = 3;


oi 07_
dCEx 2View OnMd
rawAn //任意
y()
{ m_DrawType = 4;


oi 07_
dCEx 2V i
ew OnUpd at
eMd rawL in e(CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(m_DrawType = = 1);


oi 07_
dCEx 2V i
ew OnUpd at
eMd rawRe ctange(
l CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(m_DrawType = = 2);


oi 07_
dCEx 2V i
ew OnUpd at
eMd rawE llipse(CCmdUI pCmdUI)
{ pCmdUI- >SetCheck(m_DrawType = = 3);

(4)鼠标操作与光标形状
① 在 CEx07_2View.h 中添加成员变量(见清单 7 17);
07_
清单7 17 添加成员变量(在 Ex 2Vi h文件中)
ew.

las
sCEx 07_2Viewpub li
cCS crol
lView

public:
//添加以下 5 个变量
UINT m_DrawType; //表示绘图类型
BOOL m_bMouseDown; //鼠标是否被按下
CPoint m_StartPt,m_OldPt; //m_StartPt 表示开始按下鼠标左键的位置,也即鼠标的初始位置。
//m_OldPt 用来表示鼠标移动过程中“上一次”的位置。
HCURSOR m_hCursor; //鼠标句柄类型,设置鼠标的形状。
//添加以下 2 个变量
CString m_strShow; //表示当前行字符串,
int m_nLine; //显示行数
...

② 在构造函数中初始化成员变量(见清单 7 18);
07_
清单7 18 构造函数(在 Ex 2Vi
ew.
cpp文件中)
07_
CEx 2View CEx07_2Vi ew ()
{ m_DrawType = 1;
m_bMouseDown = false; //鼠标未按下
m_hCursor = AfxGetApp()- >LoadStandardCursor(IDC_CROSS); //鼠标形状为十字形
m_nLine = 0; //手动添加
194 }
第七章 绘 图
……………………………………………………………………………………………………………………………………………

AfxGetApp()为全局函数用来得到指向主应用程序对象的指针 。
LoadStandardCursor()函数是 CWinApp 类的成员函数,其作用是取得鼠标形状句柄,其
函数原型为:
HCURSOR LoadStandardCursor(LPCTSTR lpszCursorName)const;
参数:lpszCursorName 是鼠标形状名称字符串,可取值见表 7 9。
表7 9 l
pszCu
rsrName可取值

lpszCursorName 取值 鼠标形状说明
IDC_ARROW 标准箭头形状
IDC_IBEAM I 形鼠标,用于标志文本编辑状态的鼠标形状
IDC_WAIT 用于标志程序等待状态的鼠标形状
IDC_CROSS 十字形
IDC_UPARROW 上箭头形
IDC_SIZEALL 四个方向都有箭头的十字形,用来标志移动
IDC_SIZENWSE 从左上到右下的双箭头
IDC_SIZENESW 从右上到左下的双箭头
IDC_SIZEWE 水平方向的双箭头
IDC_SIZENS 竖直方向的双箭头

③ 在 CEx07_2View 类中添加 WM_LBUTTONDOWN (鼠标左键按下 )、WM_MOUSEMOVE (鼠标移


动 和 WM_LBUTTONUP(鼠标左键松开)消息函数。

所添加的三个消息函数为:OnLButtonDown()、onLButtonUp()和 OnMouseMove()。
OnLButtonDown()函数的实现代码见清单 7 19。
清单7 19 OnLBu
tto 07_
nDown()函数(在 Ex 2Vi
ew.
cpp文件中)

vo
idCEx 07_2V iew OnLBut
tonDown(UINTnFlasCPo
g intp
oit)
n //鼠标左键按下
{ m_bMouseDown = true;
m_StartPt = point; //存放画线的起始位置
m_OldPt = point; //存放光标的当前位置
SetCapture(); //捕获鼠标
CRect rect; //定义一矩形类数据对象
GetClientRect(&rect); //获取视图窗口客户区的坐标
ClientToScreen(&rect); //转换客户区坐标为屏幕坐标
ClipCursor(&rect); //将光标限定在视图窗口客户区内
CScr
ollVi
ew OnLBu t
t nDown(
o nFla
gspo
int);

OnMouseMove()函数的实现代码见清单 7 20。
清单7 20 OnMo
useMo
v 07_
e()函数(在 Ex 2Vi
ew.
cpp文件中)


oidCEx07_2V i
ew OnMo us
eMoe(
v UINTnF
lasCPo
g intp
oit)
n //鼠标移动
{ SetCursor(m_hCursor);//设置鼠标形状为十字形
if(m_bMouseDown)
{ CClientDC dc(this);
switch(m_DrawType)
{ case 1:
195

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

dc.SetROP2(R2_NOT);
//以下两行代码擦除从起点(鼠标按下点)到上次鼠标移动到的位置之间的临时线
dc.MoveTo(m_StartPt);
dc.LineTo(m_OldPt);
//以下两行代码从起点到鼠标当前位置画线
dc.MoveTo(m_StartPt);
dc.LineTo(point);
break;
case 2:
dc.SetROP2(R2_NOTXORPEN);
dc.Rectangle(m_StartPt.x,m_StartPt.y,m_OldPt.x,m_OldPt.y);
dc.Rectangle(m_StartPt.x,m_StartPt.y,point.x,point.y);
break;
case 3:
dc.SetROP2(R2_NOTXORPEN);
dc.Ellipse(m_StartPt.x,m_StartPt.y,m_OldPt.x,m_OldPt.y);
dc.Ellipse(m_StartPt.x,m_StartPt.y,point.x,point.y);
break;

m_OldPt = point;//鼠标当前位置为下一次移动的旧位置

CScrollView::OnMouseMove(nFlags, point);

首先定义一个 CClientDC 类型的对象 dc,用 SetROP2()来设置当前绘图方式,SetROP2()函


数的原型为 CDC::SetROP2(int nDrawMode),参数 nDrawMode 可取值见表 7 10。
表7 10 nDr
awMo
de可取值

nDrawMode 取值 含 义
R2_BLACK 像素点颜色总是黑色
R2_WHITE 像素点颜色总是白色
R2_NOP 像素点颜色不改变
R2_NOT 像素点颜色为屏幕颜色的反色
R2_COPYPEN 像素点颜色为画笔颜色
R2_NOTCOPYPEN 像素点颜色为画笔颜色的反色
R2_MERGEPENNOT 像素点颜色为画笔颜色和屏幕颜色反色的或
R2_MASKPENNOT 像素点颜色为画笔颜色和屏幕颜色反色的与
R2_MERGENOTPEN 像素点颜色为画笔颜色反色和屏幕颜色的或
R2_MASKNOTPEN 像素点颜色为画笔颜色反色和屏幕颜色的与
R2_MERGEPEN 像素点颜色为画笔颜色和屏幕颜色的或的反色
R2_NOTMERGEPEN 像素点颜色为画笔颜色和屏幕颜色的或
R2_MASKPEN 像素点颜色为画笔颜色和屏幕颜色的与
R2_NOTMASKPEN 像素点颜色为画笔颜色和屏幕颜色的与的反色
R2_XORPEN 像素点颜色为画笔颜色和屏幕颜色的异或
R2_NOTXORPEN 像素点颜色为画笔颜色和屏幕颜色的异或的反色

OnLButtonUp()函数的实现代码见清单 7 21。
196
第七章 绘 图
……………………………………………………………………………………………………………………………………………

清单7 21 OnLBu
tto 07_
nUp()函数(在 Ex 2Vi
ew.
cpp文件中)


oidCEx07_2View OnLBu ttonUp(UINTnF lasCPo
g intp oi
nt)//释放鼠标
{ if(m_bMouseDown)

m_bMouseDown = false;
ReleaseCapture();//释放鼠标捕获并恢复正常输入
ClipCursor(NULL);//使光标可以移动到屏幕任意位置
CClientDC dc(this);
switch(m_DrawType)

case 1:
dc.SetROP2(R2_NOT);//设置绘图模式,以屏幕反转色为绘图颜色,即擦除“旧线”
dc.MoveTo(m_StartPt);
dc.LineTo(m_OldPt);
dc.SetROP2(R2_COPYPEN);//设置绘图模式,以当前画笔绘图,即绘“新线”
dc.MoveTo(m_StartPt);
dc.LineTo(point);
break;
case 2:
dc.SetROP2(R2_NOT);
dc.Rectangle(m_StartPt.x,m_StartPt.y,m_OldPt.x,m_OldPt.y);
dc.SetROP2(R2_COPYPEN);
dc.Rectangle(m_StartPt.x,m_StartPt.y,point.x,point.y);
break;
case 3:
dc.SetROP2(R2_NOT);
dc.Ellipse(m_StartPt.x,m_StartPt.y,m_OldPt.x,m_OldPt.y);
dc.SetROP2(R2_COPYPEN);
dc.Ellipse(m_StartPt.x,m_StartPt.y,point.x,point.y);
break;

//以下 4 行程序在第(8)步添加,其作用为把信息添加到文档类数据成员 m_LineList 中
CEx07_2Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CLine* pLine = new CLine(m_StartPt,point,m_DrawType);
pDoc- >m_LineList.AddTail((void*)pLine);

CScrollView::OnLButtonUp(nFlags, point);

(5)在视类中响应键盘输入与显示字符
用户可以用键盘输入字符并 在 视 图 窗 口 内 显 示 出 来,最 先 输 入 的 字 符 显 示 在 视 图
窗口最上面一行,按 Enter 键则换行输入,每行最多容纳 64 个字符,通过以下步骤完成 。
① 在 Ex07_2View.h 中添加 2 个成员变量 m_strShow 和 m_nLine(见清单 7 14)
② 在构造函数中初始化成员变量 m_nline(见清单 7 15)
③ 在 CEx07_2View 类中添加 WM_CHER 键盘消息函数
所添加的键盘消息函数名为:OnChar();具体代码见清单 7 22。

07_
清单7 22 键盘消息处理(在 Ex 2Vi
ew.
cpp文件中) 197

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidCEx07_2V i
ew OnCh ar(UINTnCharUINTnRe
pCn
tUINTnF
las)

{ //回车
if(nChar = = VK_RETURN)
{ m_strShow.Empty();//置空
m_nLine ++ ;//表示以后输入的字符应该在下一行

else if(m_strShow.GetLength()
<64)
{ m_strShow+= nChar;//将按下的键值赋给变量 m_strShow

CClientDC dc(this);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
int nLineHeight = tm.tmHeight+tm.tmExternalLeading;//计算每一行字的高度
dc.TextOut(0,m_nLine* nLineHeight,m_strShow);//输出字符
CScrollView::OnChar(nChar, nRepCnt, nFlags);

当用户在键盘上按下一个键时,程序运行函数 OnChar(),参数 nChar 是所按下键对应字


符的 ASCII 码值;nRepCnt 是重复次数;nFlags 是一些标志位。
(6)添加新类 CLine
编译上述程序发现程序存在两个问题:一是视图窗口重画时 (比如窗口最小化再最大
化,或窗口被别的窗口覆盖)所有在视图中显示的直线和文字都不见了;二是视图中直线和
文字的信息没有保存到磁盘。 为此首先添加和编写新类 CLine,然后再在 CEx07_2Doc 类中
实现数据存储。
① 在工程项目中新建一个 CLine 类
选择 Insert- > New Class 菜单项,在 New Class 对话框中的选择见图 7 4。

图7 4 新建 CL
ine类

② 在 Line.h 中添加成员变量和成员函数(见清单 7 23)


198
第七章 绘 图
……………………………………………………………………………………………………………………………………………

清单7 23 L
in h文件
e.


lassCLinepub licCOb jec

 DECLARE_SERIAL(CLine)
public:
CPoint m_StartPt,m_EndPt;//分别表示直线的起点和终点
public:
CLine();
CLine(int x1,int y1,int x2,int y2,int type);
CLine(CPoint pt1,CPoint pt2,int type);
virtual ~CLine();
void Draw(CDC* pDC);//绘图函数
virtual void Serialize(CArchive &ar);//系列化函数
};

③ 编写 Line.cpp 文件(见清单 7 24)


清单7 24 L
ine.
cpp文件
...
CLineCL ie()

{ m_StartPt.x = 0;
m_StartPt.y = 0;
m_EndPt.x = 0;
m_EndPt.y = 0;
m_nGraphType = 1;

CLi
neCL ie(
n i ntx 1in
ty 1
intx
2i
nty
2i
ntt
ype)
{ m_StartPt.x = x1;
m_StartPt.y = y1;
m_EndPt.x = x2;
m_EndPt.y = y2;
m_nGraphType = type;

CLi
neCL ine( CPointpt1CPo
intp
t2
intt
ype)
{ m_StartPt = pt1;
m_EndPt = pt2;
m_nGraphType = type;

CLin
e~CL in e()
{ }
vo
idCLineDr aw( CDCpDC)
{ switch(m_nGraphType)
{ case 1:
pDC- >MoveTo(m_StartPt);
pDC- >LineTo(m_EndPt);
break;
case 2:
pDC- >Rectangle(m_StartPt.x,m_StartPt.y,m_EndPt.x,m_EndPt.y);
break;
case 3:
pDC- >Ellipse(m_StartPt.x,m_StartPt.y,m_EndPt.x,m_EndPt.y);
break; 199

isu
alC++编程与项目开发 ……………………………………………………………………………………………………




oidCLine
Seria
lie(
z CAr ch
ive&a //序列化函数
r)
{ if(ar.IsStoring())
ar<<m_StartPt<<m_EndPt<<m_nGraphType;
else
ar>>m_StartPt>>m_EndPt>>m_nGraphType;

(7)在文档类中处理数据实现数据的磁盘操作
① 在 Ex07_2Doc.h 中声明成员变量和成员函数(见清单 7 25)
07_
清单7 25 声明成员变量和成员函数(在 Ex 2Do
c.h文件中)

#incl
ude"L i
ne.h"

lassCEx 07_2Docpub li
cCDo cume
nt
 public:
CPtrList m_LineList;//直线链表
CStringList m_strList;//保存除最后一行文字之外的各行文字
CString m_strLastLine;//最后一行
void ReInit();//重新初始化文档数据变量
void DrawLine(CDC*pDC);
void DrawText(CDC*pDC);
...}

② 编写 ReInit()函数(见清单 7 26)
清单7 26 Re
Ini 07_
t()函数(在 Ex 2Do
c.pp文件中)


oidCEx 07_2Do c ReInit()
{ CLine * pLine;
POSITION pos = m_LineList.GetHeadPosition();
for(;pos!= NULL;m_LineList.GetNext(pos))
{ pLine = (CLine* )m_LineList.GetAt(pos);
delete pLine;

m_LineList.RemoveAll();
m_strList.RemoveAll();
m_strLastLine.Empty();

函数 ReInit()首先释放在堆中为每个 CLine* 指针所分配的空间,然后清空链表。


③ 编写新建文档 OnNewDocument()函数(见清单 7 27)
清单7 27 OnNewDo
cume
n 07_
t()函数(在 Ex 2Do
c.pp文件中)

BOOLCEx 07_ 2DocOnNewDo cume t()



{ if (! CDocument::OnNewDocument())
return FALSE;
ReInit();
return TRUE;

200
第七章 绘 图
……………………………………………………………………………………………………………………………………………

新建的文档应该是空的,它的数据内容不应该受到前一文档的干扰和影响,为此新建文
档要初始化。
④ 程序结束时数据空间的释放,在析构函数中进行(见清单 7 28)
07_
清单7 28 析构函数(在 Ex 2Do
c.pp文件中)

07_
CEx 2Do 07_
c~CEx c()
2Do
{ ReInit();

⑤ 编写 serialize()函数,实现数据的磁盘读写(见清单 7 29)
清单7 29 S
eri
ali
z 07_
e()函数(在 Ex 2Do
c.pp文件中)


oidCEx07_2Do cSerial
ize( CArchiv
e&a r)

if (ar.IsStoring())
{ UINT nCount;
//保存直线
nCount = m_LineList.GetCount();
ar<<nCount;
CLine * pLine;
POSITION pos = m_LineList.GetHeadPosition();
for(; pos!= NULL; m_LineList.GetNext(pos))
{ pLine = (CLine*)m_LineList.GetAt(pos);
pLine- >Serialize(ar);

//保存文本
m_strList.Serialize(ar);//从 CObject 派生的链表类支持链表中各元素的序列化
ar<<m_strLastLine;

else
 UINT nCount;
ar>>nCount;
while(nCount - - )
{ CLine * pLine = new CLine;
pLine- >Serialize(ar);
m_LineList.AddTail(pLine);

m_strList.Serialize(ar);
ar>>m_strLastLine;

数据的磁盘读写有关内容见第 8 章的 83 节。
⑥ 重绘图形和文字(见清单 7 30)
清单7 30 Dr
awL
ine()和 Dr
awTe
x 07_
t()函数(在 Ex 2Do
c.pp文件中)


oidCEx 07_ 2DocDrawL
ine( //重绘图形
CDCpDC)
{ //取得需要重绘的区域
CRect rectClip; 201

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

pDC- >GetClipBox(&rectClip);
CLine * pLine;
POSITION pos = m_LineList.GetHeadPosition();
for(;pos!= NULL;m_LineList.GetNext(pos))
{ pLine = (CLine* )m_LineList.GetAt(pos);
if(rectClip.PtInRect(pLine- >m_StartPt)||rectClip.PtInRect(pLine- >m_EndPt))
{ pLine- >Draw(pDC);




oidCEx07_2Do cDrawTe xt(CDCpDC) //重绘文字
{ CRect rectClip,rect;
pDC- >GetClipBox(&rectClip);//取得需要重绘的区域
CSize size;
TEXTMETRIC tm;
pDC- >GetTextMetrics(&tm);
int nLineHeight = tm.tmHeight+tm.tmExternalLeading;
CString str;
int line = 0;
POSITION pos = m_strList.GetHeadPosition();
for(;pos!= NULL;m_strList.GetNext(pos))
{ str = m_strList.GetAt(pos);
size = pDC- >GetTextExtent(str);
rect.SetRect(0,line* nLineHeight,size.cx,line* nLineHeight+size.cy);
if(!(rect&rectClip).IsRectEmpty())pDC- >TextOut(0,line* nLineHeight,str);
line ++ ;

str = m_strLastLine;
size = pDC- >GetTextExtent(str);
rect.SetRect(0,line* nLineHeight,size.cx,line* nLineHeight+size.cy);
if(!(rect&rectClip).IsRectEmpty())
pDC- >TextOut(0,line* nLineHeight,str);

(8)文档数据更新,修改 OnLButtonUp()函数(见清单 7 21)


(9)绘图显示,在 OnDraw()函数中实现(见清单 7 31)
清单7 31 OnDr 07_
aw()函数(在 Ex 2Vi
ew.
cpp文件中)


oidCEx07_ 2Vi
ew OnDr aw(CDC pDC)
{ CEx07_2Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc- >DrawLine(pDC);
pDoc- >DrawText(pDC);

(10)编译和运行


53 动态绘图编程实例
202 【例7 3】 Ex07_3 动态绘图的实现,具体说明见表 7 10。
第七章 绘 图
……………………………………………………………………………………………………………………………………………

表7 10 例7 3具体说明

项 目 图示和说明

图(
b) 设置菜单
程序界面

图(
a) 主界面 图(
c) 动态菜单 图(
d) 颜色对话框

(1)点击菜单:设置- >背景颜色,可弹出图(d)所示的颜色对话框,选择颜色,然后点击确定,
可设置主界面的背景颜色
(2)点击菜单:设置- >画笔颜色,可弹出图(d)所示的颜色对话框,选择颜色,然后点击确定,
可设置画笔颜色
程序功能
(3)点击菜单:动态→动态绘正弦曲线,动态绘制正弦曲线
(4)点击菜单:动态→开始绘余弦曲线,动态绘制余弦曲线
点击菜单:动态→停止余弦曲线绘制,即停止余弦曲线绘制;如果又点击开始绘余弦曲
线,程序将继续绘制余弦曲线

(1)新建一单文档应用程序 Ex07_3
(2)编辑菜单和添加消息函数(见表 7 11)
表7 11 菜单消息函数

菜 单 名 ID 号 消 息 消 息 函 数
背景颜色... ID_SETUP_BKCOLOR COMMAND OnSetupBkcolor()
画笔颜色... ID_SETUP_PENCOLOR COMMAND OnSetupPencolor()
动态绘正弦曲线 ID_ADRAW_SIN COMMAND OnAdrawSin()
开始绘余弦曲线 ID_ADRAW_COS COMMAND OnAdrawCos()
停止余弦曲线绘制 ID_ADRAW_STOP COMMAND OnAdrawStop()

(3)背景颜色的设置与显示
① 定义变量 m_BKColor (见清单 7 32)
07_
清单7 32 定义变量(在 Ex 3Vi h文件中)
ew.


las
sCEx 07_3Viewpub licCView
 public:
COLORREF m_BKColor;//背景颜色
COLORREF m_PenColor;//画笔颜色,在(4)①标题处要求定义
CPen newPen,*pOldPen;//定义画笔对象,在(5)①标题处要求定义
void ShowBKColor();//显示背景颜色,在(4)④标题处要求定义

203

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

② 初始化变量 m_BKColor(见清单 7 33)


07_
清单7 33 定义变量(在 Ex 3Vi h文件中)
ew.

07_
CEx 3View CEx 07_3V ew()

{ m_BKColor = RGB(255,255,255);//背景颜色初始为白色
m_PenColor = RGB(0,0,0);//画笔颜色初始为黑色,在(4)②标题处要求赋初值

③ 编写背景颜色设置菜单的消息函数(见菜单 7 34)
清单7 34 OnS
etupBk
col
o 07_
r()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx07_3View OnSetupBk colr()
o //单击“背景颜色...
”菜单
{ CColorDialog colordlg;
if(colordlg.DoModal()= = IDOK)
{ m_BKColor = colordlg.GetColor();

Invalidate();

④ 定义和编写显示背景颜色的函数(分别见清单 7 32 和清单 7 35)


清单7 35 Sh
owBKCo
lo 07_
r()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx07_3V i
ew ShowBKCo lo //显示背景颜色
r()
{ CClientDC dc(this);
CRect rect;
GetClientRect(&rect);
CBrush newBrush,*pOldBrush;
newBrush.CreateSolidBrush(m_BKColor);
pOldBrush = dc.SelectObject(&newBrush);//保存原画笔,选中新画笔
dc.Rectangle(rect);
dc.SelectObject(pOldBrush);//恢复原画笔

⑤ 重载 OnDraw()函数(见清单 7 36)
清单7 36 OnDr 07_
aw()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx07_3View OnDraw(CDC pDC)
{ CEx07_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
ShowBKColor();//显示背景颜色

(4)画笔颜色的设置
① 定义变量 m_PenColor (见清单 7 32)
② 初始化变量 m_PenColor (见清单 7 33)
③ 编写背景颜色设置菜单的消息函数(见菜单 7 37)
204
第七章 绘 图
……………………………………………………………………………………………………………………………………………

清单7 37 OnS
etupPe
nco
lo 07_
r()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx07_3View OnSetupPen col
or()//单击“背景颜色...
”菜单
{ CColorDialog colordlg;
if(colordlg.DoModal()= = IDOK)
{ m_PenColor = colordlg.GetColor();

(5)动态绘制正弦曲线
其绘制动画的实现方法是:绘一段直线,用 Sleep()函数停止一下,再继续绘制。
① 定义变量 newPen、*pOldPen(见清单 7 32)
② 编写 OnAdrawSin()函数(见清单 7 38)
清单7 38 OnAd
rawS
i 07_
n()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx07_ 3V i
ew OnAd rawSi //单击“动态绘正弦曲线”菜单
n()
{ CClientDC dc(this);
newPen.CreatePen(PS_SOLID,3,m_PenColor);//创建新画笔
pOldPen = dc.SelectObject(&newPen);//选择新画笔,保存老画笔
dc.MoveTo(100,100);//移动到点(100,100)处
//动态绘直线
for(int i = 0;i<100;i ++ )
{ Sleep(100);
dc.LineTo(100+i,100);

dc.MoveTo(100,100);//移动到点(100,100)处
for(i = 0;i<100;i ++ )//动态绘正弦曲线
{ Sleep(100);
dc.LineTo(100+ i*1,(int)(100+ 50*sin(20*628*i)));

dc.SelectObject(pOldPen);//还原老画笔

(6)动态绘制余弦曲线
其绘制动画的实现方法是:利用 SetTimer()、OnTimer()函数。
① 编写 OnAdrawCos()函数(见清单 7 39)
清单7 39 OnAd
rawCo 07_
s()函数(在 Ex 3Vi
ew.
cpp文件中)


oi 07_
dCEx 3View OnAd r
awCo s()//单击“开始绘余弦曲线”菜单
{ SetTimer(1,50,0);//设置定时器
} newPen.CreatePen(PS_SOLID,3,m_PenColor);

② 添加和编写 OnTimer()函数(见清单 7 40)


用 ClassWizard 添加 WM_TIMER 消息的消息函数 OnTimer()。
清单7 40 OnT
ime 07_
r()函数(在 Ex 3Vi
ew.
cpp文件中)


oi 07_
dCEx 3View OnTimer(
UINTn
IDEv
ent)
{ CClientDC dc(this);
205

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

pOldPen = dc.SelectObject(&newPen);
static int i = 0;//设置静态变量
//动态绘水平直线
dc.MoveTo(100+i,300);
dc.LineTo(101+i,300);
//动态绘余弦曲线
dc.MoveTo(100+i,(int)(300+ 50*cos(20*628*(i))));
dc.LineTo(101+i,(int)(300+ 50*cos(20*628*(i+1))));
++i;
dc.SelectObject(pOldPen);
CView::OnTimer(nIDEvent);

(7)停止余弦曲线绘制(见清单 7 41)
清单7 41 OnAd
rawS
to 07_
p()函数(在 Ex 3Vi
ew.
cpp文件中)


oi 07_
dCEx 3View OnAd
rawS
to //单击“停止余弦曲线绘制”菜单
p()
{ KillTimer(1);//停止定时器

(8)编译和运行

206
第八章 文 件 操 作

在任何一个应用程序中都不希望其文档数据随着应用程序的关闭而全部消失 。 文档数
据的永久保存有两类方法,其一是保存在数据库中,其中的数据库可以随机存取。 其二是保
存为应用程序直接读写的磁盘文件 。本章将介绍采用第二类方法如何将文档数据存入磁盘
的永久性介质中,以及如何从磁盘文件中读取数据 。
CFile 是所有文件类的基类,它提供无缓冲二进制文件的输入输出服务。 CArchive 类
用来存储二进制字节流,对文件 I/O 提供缓冲,它广泛用于对象的序列化,它还提供了一些
函数使得对文件的操作更加容易 。
本章要点:
* 文件的读与写
* 文档数据的保存与打开


1 CF
ile类
在 MFC 中,使用 CFile 类来处理正常的文件 I/O 操作。 CFile 类直接提供了无缓冲、二
进制磁盘输入输出服务,并且它还通过它的派生类间接地支持文本文件和内存文件的操作 。
CFile 类经常和 CArchive 类共同来实现 MFC 对象的序列化支持。
CFile 类用于打开与关闭文件、读写文件、在文件中定位和管理文件。 其主要函数见表
8 1。
表8 1 CF
ile类主要函数

函 数 说 明
Constructor() 创建 CFile 对象。如果传递的是一个文件名,就打开该文件
Denstructor() 释放超出范围的 CFile 对象。如果文件已打开,则关闭它
Abort() 立即关闭文件而不管是否出错
Close() 关闭文件
Duplicate() 创建一个可复制的文件对象
Flush() 清除流中数据
GetFilename() 获取文件名
GetFilePath() 获取文件的全路径
GetFileTitle() 获取文件标题(不带扩展名的文件名)
GetLength() 获取文件长度
GetPosition() 获取文件当前的位置
GetStatus() 获取文件的状态
207

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
函 数 说 明
LockRange() 锁定文件的一部分
Open() 打开文件
Read() 从文件中读取数据
Remove() 删除文件
Rename() 重命名文件
Seek() 设置文件中位置
SeekToBegin() 设置至文件头的位置
SeekToEnd() 设置至文件尾的位置
SetFilePath() 设置文件路径
SetLength() 设置文件长度
SetStatus() 设置文件状态
UnlockRange() 对文件的一部分进行解锁
Write() 向文件中写数据


11 打开文件
可以直接通过 CFile 的构造函数来打开磁盘文件,同时可以用标志位指定打开方式 (只
读、只写、读写等)。

CFile(LPCTSTR lpszFfileName,UINT nOpenFlags);

参数lpszFileName:指定文件名称;
nOpenFlag:文件状态标志,定制文件打开方式的标志符;一次可以使用多个状态标
志,可以简单地将它们“或”在一起。这些状态标志描述了如何打开文件、何种操作
是有效的,其取值和含义见表 8 2。

表8 2 nOp
enF
lag(文件状态标志)

标 记 说 明
CFile::modeCreate 创建新的文件或把已存在的文件长度截为 0
CFile::modeNoInherit 不允许子过程继承
CFile::modeNoTruncate 创建文件时,如果文件已经存在则不对文件截断
CFile:: modeRead 只允许读操作
CFile:: modeReadWrite 允许读写操作
CFile:: modeWrite 只允许写操作
CFile::shareCompat 允许其他过程打开文件
CFile:: shareDenyNone 允许其他过程对文件进行读写
CFile:: shareDenyRead 不允许其他过程对文件进行读操作
CFile:: shareDenyWrite 不允许其他过程对文件进行写操作
CFile:: shareExclusive 不允许其他过程访问
CFile::typeBinary 设置文件为二进制模式
CFile:: typeText 设置文件为文本模式

208 例:CFile file(“readme.txt”,CFile::modeReadWrite);为利用构造函数以允许读写方


第八章 文 件 操 作
……………………………………………………………………………………………………………………………………………

式打开当前目录下的 readme.txt;
也可采用先构造一 CFile 类对象,然后用 Open 函数打开,Open 函数原型如下:

virtual BOOL Open (LPCTSTR lpszFileName, UINT nOpenFlags, CFileException *


pError = NULL)

参数:lpszFileName 和 nOpenFlag 所表意义与上同;


pError:指向文件 异 常 对 象 的 指 针,MFC 使 用 这 个 对 象 接 收 由 错 误 操 作 引 起 的
异常。
例:CFile file;
file.Open(“readme.txt”,CFile::modeRead|CFile::shareDenyWrite);为利用 Open 函
数以只读方式打开当前目录下的 readme.txt 并且允许其他进程读此文件。


12 关闭文件
函数 CFile::Close()关闭一个文件,一般一个文件使用完毕就应该关闭它。 当文件对
象被销毁时,析构函数会自动关闭文件;但通过 new 在堆中分配一个文件对象时,在 delete
这个对象之前必须关闭文件。


13 文件读写
可以用 CFile::Read()和 CFile:Write()对文件进行顺序读写,它们都从文件当前位置
开始,文件刚刚打开时,当前位置为文件头,文件当前位置随文件读写的字节数而移动。 函
数 Read()和 Write()的声明如下:
Read 函数原型:virtual UINT Read(void* lpBuf,UINT nCount);
参数:lpBuf:指向缓冲区的指针
nCount:要读取文件的字节数
Write 函数原型:virtual void Write(const void* lpBuf,UINT nCount);
参数:含义同上
例:对于一个已经打开的文件对象 file,下面的语句从文件中读 100 个字节存入字数组
pbuf 中(nBytesRead 是实际读到的字节数):

CFile file;//定义 CFile 类对象


char pbuf100;
UINT nBytesRead = cFile.Read(pbuf,100);

例:下面的语句把字符数组 pbuf 的内容写入文件 file(当前文件以可写的方式打开):

CFile file;//定义 CFile 类对象


char pbuf= "This file is Modified by li ";
file.Write(pbuf,sizeof(pbuf));


14 文件定位
函数 Read()和 Write()只能按顺序读写文件,如果想对文件数据进行随机存取,要用到
文件定位。通用的文件定位函数是 CFile::Seek(),它的作用是把文件位置指针相对于一个 209

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

基准移动一个偏量(即改变文件的当前位置)。
Seek 函数原型:virtual LONG Seek (LONG lOff,UINT nForm);
参数 nForm:指定移动的模式,可以在 CFile::begin,CFile::current 和 CFile::end 之
间取值,分别表示从文件的开始位置,指针的当前位置,以及文件的末尾位置开始移动指针 。
lOff:偏移量,其值可正可负,负值表示向前移;
如果指定的位置是合法的,函数将返回当前文件指针距离文件开头的字节偏移量;否则
将触发一个 CFileException 异常。另外,如果一个文件已经被打开,文件指针被定位于文
件的开始位置(偏移量为 0)。
例:下面的语句把文件位置指针相对于开头向后移动 1 000 个字节,而不论文件当前位
置在哪儿。

LONG lOffset = 1000,lfileposition;


Lfileposition = cfile.Seek(lOff,Cfile::begin);

另外,函数 SeekToBegin()是将文件指针移动到文件头;函数 SeekToEnd()把文件指针移


动到文件尾。函数 GetPosition()是得到文件指针的当前位置。

2 文件流f
8 str
eam
文件 流 允 许 对 文 件 或 具 有 文 件 行 为 的 外 部 设 备 进 行 输 入 或 输 出,如 果 采 用 文 件 流
fstream 处理 文 件 I/O, 程 序 中 必 须 包 含 fstream. h 文 件。 它 定 义 的 类 包 括 ifstream、
ofstream 和 fstream。流分为三类:输入、输出和输入/输出。 要创建一个输入流,必须说明
它为 ifstream;要创建一个输出流,必须说明它为类 ofstream;执行输入和输出操作的流必
须说明为 fstream。


21 打开文件
文件打开可以先构造一个流,然后用 open()函数打开文件,open()函数原型为:
void open(const char* szName, int nMode, int nProt = filebuf::openprot);
参数:szName 为文件名,它可以包含路径说明符;
nMode 决定文件的打开方式,取值见表 8 3;
nProt 决定存取文件的方式,缺省值为 filebuf::openprot。
表8 3 nMo
de的取值

nMode 取值 含 义
Ios::app 把文件的输出添加在文件尾,它只能用于输出文件
Ios::ate 文件打开时定位于文件尾
Ios::in 文件有输入能力
Ios::out 文件有输出能力
Ios::nocreate 导致函数 open()在文件不存在时失败
Ios::noreplace 导致函数在文件存在时失败
Ios::trun 导致已存在的同名文件的内容被破坏且长度被截断为 0
210 例:打开一个供输入/输出的流
第八章 文 件 操 作
……………………………………………………………………………………………………………………………………………

fstream mystream;
mystream.open(“readme.txt”,ios::in|ios::out);

如果 open()失败,则 mystream 为 0。
虽然使用 open ()函 数 打 开 一 个 文 件 完 全 合 适,但 在 大 多 数 情 况 下,由 于 ifstream、
ofstream、fstream 类包含自动打开文件的构造函数,所以还可用下面的方式打开文件 。
例:ifstream mystream("myfile ");


22 关闭文件
要关闭 一 个 文 件, 使 用 成 员 函 数 close (), 函 数 close ()不 带 任 何 参 数。 例 如 用
mystream.close()的语句关闭关于流 mystream 的文件。


23 编程实例
【例8 1】 Ex08_1 编写一程序,利用 CFile 类实现文件的读操作,利用文件流 fStream
类实现文件的写操作,具体说明见表 8 4。
表8 4 例8 1具体说明

项 目 图示和说明

程序界面
图(
b) “另存为”对话框

图(
a) 程序主界面

图(
c) 消息对话框 图(
d) 消息对话框

(1)运行程序 Ex08_1.exe 弹出程序主界面(a),在写入的文本编辑框中输入要写入的文字,然


后点击“写入到文件...”按钮,弹出另存为对话框(b)
(2)在另存为对话框中的选择为:在“保存在”组合框中选择文件保存的路径,在文件名中输
入要保存的文件名,如 Ex08string,在保存类型中选择为* .txt。点击保存按钮,程序将“
写入的文本”编辑框的内容保存到 Ex08string.txt 文件,然后弹出消息对话框(c)
程序功能
(3)点击“查看文件路径”按钮,即可将文件保存的路径显示在文件路径编辑框中
(4)点击“从文件读入...”按钮,弹出打开对话框,界面基本同图(b)
(5)在打开对话框中的选择为:在“保存在”组合框中选择文件被保存的路径,在文件名中输
入要打开的文件名,如 Ex08string。点击打开按钮,程序将 Ex08string.txt 文件的内容
显示到编辑框中,然后弹出文件读入完毕的消息对话框

(1)新建一基于对话框的应用程序 Ex08_1
(2)添加控件、成员变量和消息函数(见表 8 5)
表8 5 控件ID 号、成员变量和消息函数 211

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

控 件 标 题 ID 号 成员变量 变量类型 消息函数


编辑框
IDC_WRITE m_WriteString CString
(写入的文本)
编辑框
IDC_FILEPATH m_FilePath
(文件路径)
编辑框(从文件
IDC_READ m_strRead CString
中读入的文本)
IDC_BUTTON
下压按钮 写入到文件... OnButtonWrite()
_WRITE
IDC_BUTTON OnButtonFilepath
下压按钮 查看文件路径
_FILEPATH ()
IDC_BUTTON
下压按钮 从文件读入... OnButtonRead()
_READ

(3)写入文件
① 定义变量(见清单 8 1)
08_
清单8 1 定义变量(在 Ex 1Vi h文件中)
ew.


las
sCEx 08_1DlgpublicCD ialo

 public:
CString strFileName;//文件名
CString strFilePath;//文件路径
BOOL IsTextFile(CString& rFile);//判断文件类型是否正确(5)①标题处要求定义

② 编写下压按钮“写入文件”的消息处理函数(见清单 8 2)
清单8 2 OnBu
tto
nWr
it 08_
e()函数(在 Ex 1Dl
g.pp文件中)


oidCEx 08_ 1DlgOnBu ttonWr i
te()//单击"写入到文件…"按钮
{ UpdateData(true);
CFile file;
CfileDialog filedlg(0,//1-文件打开, 0-文件另存为
".txt|* .* " ,
NULL,
OFN_OVERWRITEPROMPT,
" 文本文件(*.txt)|*.txt|All Files (*.* )|* .* ||",
NULL);
if(filedlg.DoModal()= = IDOK)
strFileName = filedlg.GetFileName();//获得文件名
if(strFileName = ="" )
{ AfxMessageBox("请输入文件名!");
return;

file.Open(strFileName,CFile::modeCreate|CFile::modeWrite);//将数据写入文件
int length = m_strWrite.GetLength();//获取文件长度

212
第八章 文 件 操 作
……………………………………………………………………………………………………………………………………………


il
e.Write((LPCTSTR) m_s
trWri
te
length);//获取有关文件的信息 CS
tri
ng
AfxMessageBox(已保存到文件:+s t
rFil
eName+ //保存结束提示
);

trFi
lePath=f il
e.Ge
tFi
lePa
t //获得文件的路径
h();

il
e.Cloe();
s //关闭文件

(4)查看文件路径
编写下压按钮“查看文件路径”的消息函数(见清单 8 3)
清单8 3 OnBu
tto
nFi
lea
pt 08_
h()函数(在 Ex 1Dl
g.pp文件中)


oidCEx 08_1D l
g OnButtonFi
lepat //单击“查看文件路径”按钮
h()
{ m_strFilePath = strFilePath;
UpdateData(false);

(5)读入文件
① 定义判断文件类型是否正确的函数(见清单 8 1)
② 编写 IsTextFile 函数(见清单 8 4)
清单8 4 I
sTe
xtF
il 08_
e()函数(在 Ex 1Dl
g.pp文件中)

BOOLCEx 08_1D l
gIsText
Fie(
l CS
tring &rFile)//判断文件类型
{ CStringList strList;
CString str(rFile);
strList.AddHead(".TXT" );
strList.AddHead(".SYS" );
strList.AddHead(".BAT" );
strList.AddHead(".DAT" );
str = str.Right(4);//文件名的右边四位字符
str.MakeUpper();//转换成大写
return (strList.Find(str))? TRUE:FALSE;

③ 在 Ex08_1Dlg.cpp 文件开头添加# include" fstream.h" (见清单 8 5)


④ 编写下压按钮“读入文件”的消息函数(见清单 8 5)
清单8 5 OnBu
tto
nRe
a 08_
d()函数(在 Ex 1Dl
g.pp文件中)

...
#include"fstream.h"
...

oidCEx 08_ 1DlgOnButtonRea //单击“从文件读入...
d() ”按钮
{ fstream f1;//定义文件流对象
char s200;
CFileDialog filedlg(1,//1-文件打开, 0-文件另存为
".txt|*.* " ,
NULL,
OFN_OVERWRITEPROMPT,
"文本文件(. tx
t|.txt
|Al
lFi
les.|.||" 
NULL
if(fi
ledlg.DoMod al()==IDOK) 213

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

{ UpdateData(t
rue);

trFi
leName=f il
edlg.GetFi
leName();//获得文件名
i(!
f I sTe xtFi
l (
estrFi
leName ))//判断文件类型是否正确
{ Af xMe ssa
geBox("文件类型不正确!");
return


1.open(strFi
leNamei os
in|i
osnocr
eae);

whi
l (
e f! 1.eof())
{ f 1.getli
ne(s255);
ms_trRead=m_ strRe
ad+"\ r\n" +s//添加文件中的文本到编辑框
UpdateData(f
alse);

AfxMe ssa
geBox( s
trFi
leName+ " 文件读入完毕!");//保存结束提示
f1.close();//关闭文件流

(6)编译和运行


3 CAr
chve 类与序列化

CArchive(档案)类与C++的 iostream 类有些类似,不过 CArchive 类用来存储二进制字
节流,而 iostream 类只用于格式化的文本。
序列化(Serialize)是指将对象写入永久性存储媒体 (如磁盘文件 )或从其中读取对象
的过程。Serialize 成员函数并不直接使用 CFile 对象,而是通过 CArchive 类 的对象与
CFile 对象交互。MFC 将 CArchive 类的对象用作被序列化的对象和存储媒体之间的中介物。
如果使用序列化功能,首先要进行两项工作:创建可序列化的类和序列化对象 。


31 创建可序列化的类
(1)从 Object 类派生
一个类支持序列化是指此类能够用 Serialize()函数实现对类对象的流式存取。 要让
一个类支持序列化,它必须是 CObject 类或其派生类。
(2)使用 SERIAL 宏
为了支持序列化,在类的头文件说明中必须包含 DECLAR_SERIAL 宏调用。 在类的执行
文件(cpp 文件)中必须包含 IMPLEMENT_SERIAL 宏调用。这两个宏的格式为:
DECLAR_SERIAL(class_name)
IMPLEMENT_SERIAL(class_name,base_class_name)
其中 class_name 是要序列化的类名,base_class_name 是其基类的名字,注意类名不带
双引号。
(3)重载 Serialize 成员函数
(4)在类中定义不带参数的构造函数
从磁盘上加载序列化对象后,MFC 重新创建这些对象时,需要使用一个默认的不带参数
的构造函数。如果没有在使用 SERIAL 宏的类中定义不带参数的构造函数,编译器发出没有
214 可用的默认构造函数的警告。
第八章 文 件 操 作
……………………………………………………………………………………………………………………………………………


32 序列化对象
拥有了序列化的类后,就可以通过 CArchive 对象将该类的对象序列化到文件中或者从
文件序列化该类的对象。
(1)创建 CArchive 对象
创建 CArchive 对象,首先构造 CFile 对象,然后将 CFile 对象传递到 CArchive 类的构
造函数中。
例:构造一 CArchive 类的对象。
CFile MyFile;
MyFile.Open(...,CFile::ModeWrite);
CArchive MyArchive(&MyFile,Carchive::store);
可以采用下面的代码关闭 Carchive 对象。
MyArchive.Close();
MyFile.Close();
(2)使用运算符
CArchive 类提供了“ << ”和“
>> ”两个运算符来实现简单的串行化操作,“ << ”运算符用于
向文件中写入数据,“ >> ”用于向文件中读取数据。 MFC 类库将 CArchive 对象作为运算符的
第一操作数,第二操作数见表 8 6。
表8 6 CAr
chi
ve类的“
<<”和“>>”运算符支持的数据类型

数据类型 数据类型 数据类型 数据类型


CObject SIZE 和 CSize Float WORD
CString OINT 和 CPoint DWORD BYTE
RECT 和 CRect double LONG CTime 和 CTimeSpan
int COleCurrency COleVariant COleDateTime(Span)

例:运算符的应用
CArchive ar;
int myint;
BYTE myByte;
RECT myRect;
ar << myint << myByte << myRect;


33 CA
rch
ive类的数据成员和成员函数
CArchive 类的数据成员和成员函数见表 8 7。
表8 7 CAr
chi
ve类的主要数据成员和成员函数

数据成员和成员函数 作 用
m_pDocument 指向被序列化的 CDocument 对象的指针
Abort() 不发送异常的情况下直接关闭文档
Close() 关闭文档
215

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
数据成员和成员函数 作 用
Flush() 将等待的数据写入数据流
Read/Write() 读取/写入字节数据
Read/WriteString() 读取/写入一行文本字符
GetFile() 获取文档对象的 CFile 对象的指针
Get/SetObjectSchema() 读取/设置对象的版本号
IsBufferEmpty() 确定在套接字接受过程中是否清空缓冲区
IsLoading() 确定是否载入文档
IsStoring() 确定是否存储文档


34 Se
ria
li
ze函数串行化处理数据
文档数据的串行化是由文档类的成员函数 Serialize 处理的,在 AppWizard 创建的基本
框架中在文档类的* Doc.cpp(如 Ex07_2Doc.cpp)文件中已经定义了清单 8 6 的 Serialize
的基本结构。
清单8 6 S
eri
ali
ze()函数


oidCEx07_ 2DocSer
ial
ize(CAr chiv r)
e&a
{ if (ar.IsStoring())
//TODO:add storing code here


else
//TODO:add loading code here

Serialize 成员函数的参数是用于操作 CFile 类的对象的 CArchive 类的对象。 通过调


用该对象的成员函数完成读取文件数据或写数据到文件 。当准备写数据时,函数 IsStoring
返回 true,Serialize 函数执行文档数据的存储操作。 当准备读数据时,函数 IsStoring 返
回 false,Serialize 函数执行文档数据的加载操作。由程序员按需求编写存储操作和加载
操作的具体代码。
在应用程序执行中,Serialize 成员函数将响应 “File”下拉菜单中的 “Open”、“Save”、
“Save As”等完成文档数据的读写操作。
各种情况下的 Serialize 函数的编写如下:
(1)当要序列化的类的成员变量是标准的对象时,其 Serialize 函数按清单 8 7 编写。
清单8 7 S
eri
ali
ze()函数中序列化标准的对象


oid 类名:: Seri
alie(
z CAr
chi
ve&a r)
{ 基类名::Serialize(CArchive& ar)
if (ar.IsStoring())
{ //TODO:add storing code here
ar<<成员变量 1<<成员变量 2...;//存

else
216
第八章 文 件 操 作
……………………………………………………………………………………………………………………………………………

 //TODO:add loading code here


ar>>成员变量 1>>成员变量 2...;//取

其 中, 基 类 是 指 执 行 序 列 化 处 理 的 派 生 类 的 基 类, 但 当 基 类 是 CObject 或 者 是
CDocument 类时,因为其 Serialize 函数不执行任何操作,所以通常不予以调用。
(2)当要序列化的类的成员变量是非标准的对象时,其 Serialize 函数按清单 8 8
编写。
清单8 8 S
eri
ali
ze()函数中序列化非标准类的对象


oid 类名:: Seri
alie(
z CAr
chi
ve&a r)
{ 基类名::Serialize(CArchive& ar)
if (ar.IsStoring())
{ //TODO:add storing code here
ar<<成员变量 1<<成员变量 2...;//存

else
 //TODO:add loading code here
ar>>成员变量 1>>成员变量 2...;//取

非标准类对象.Serialize(ar);//非标准类对象的序列化

(3)如果类的成员变量含有非标准类对象的指针,则 Serialize 函数按清单 8 9 编写。


清单8 9 S
eri
ali
ze()函数中序列化非标准类的对象指针


oid 类名:: Seri
alie(
z CAr
chi
ve&a r)
{ 基类名::Serialize(CArchive& ar)
if (ar.IsStoring())
{ //TODO:add storing code here
ar<<成员变量 1<<成员变量 2...;//存

else
 //TODO:add loading code here
非标准类对象的指针= new 非标准类;
ar>>成员变量 1>>成员变量 2...;//取

//类中包含的不是非标准类的对象,而是其指针。因此必须在 CArchive 处理读写文档时,首先定义新
//的非标准类的对象,然后调用新建对象的 Serialize 成员函数。
//非标准类对象的指针- >Serialize(ar);

//另外,对于这种情况下的 Serialize 函数还可以用下面的简便写法

oid 类名:: Seri
alie(
z CAr
chi
ve&a r)
{ 基类名::Serialize(CArchive& ar)
if (ar.IsStoring())

217

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

{ //TODO:add storing code here


ar<<成员变量 1<<成员变量 2...<<非标准类对象的指针;//存

else
 //TODO:add loading code here
ar>>成员变量 1>>成员变量 2...>>非标准类对象的指针;//取

(4)如果类的成员变量中包含集合类(CObject 的直接或间接派生类 )的对象,其中包含


另一个类 2(CObject 的直接或间接派生类)的对象,则按清单 8 10 编写。
清单8 10 S
eri
ali
ze()函数中序列化集合类

vo
id 类名:: Seri
alie(
z CArch
ive&a r)
{ 基类名::Serialize(CArchive& ar)
if (ar.IsStoring())
{ //TODO:add storing code here
ar<<成员变量 1<<成员变量 2...;//存

else
 //TODO:add loading code here
ar>>成员变量 1>>成员变量 2...;//取

集合类的对象.Serialize(ar);//集合类对象的序列化

//说明:其中集合类对象的 Serialize 函数依次调用该对象中每个类 2 对象的 Serialize 函数,完成类 2
//对象组的存取。相应地,需在类 2 中定义其 Serialize 函数。
vo
id 类2名:: Seria
lie(
z CArch
ive&a r)
{ if (ar.IsStoring())
{ //TODO:add storing code here
ar<<类 2 成员变量 1<<类 2 成员变量 2...;//存

else
 //TODO:add loading code here
ar>>类 2 成员变量 1>>类 2 成员变量 2...;//取

Serialize()函数编写应用举例见 Ex07_2 的清单 7 29。

218
第九章 打 印

打印机和显示器一样,都是计算机图形或文字的输出设备。在 Windows 系统中,它们均


作为设备环境处理,因此无论是屏幕显示还是打印输出,均需要通过环境的对象完成。
Windows 提供了一组与打印相关的标准对话框,用于实现设置页面、打印机、打印份数、
打印预览和打印等操作,通常由应用程序 “文件”下拉菜单中的 “页面设置”、“打印机属性 ”、
“打印设置”、“打印预览”和“打印”等菜单选项激活。
本章要点:
* MFC 的基本打印及与打印有关的函数
* 打印缩放与多页打印

1 MFC 的基本打印和打印预览

利用 V C++实现打印和打印预览功能时,有时甚至不需要加入任何代码,下面以一个例
题来说明 V C++所带的打印功能。


11 缺省打印实例
【例9 1】 Ex09_1 建立一单文档应用程序,实现打印和打印预览文本与图形文件。 具
体说明见表 9 1。
表9 1 例9 1具体说明

项目 图示和说明

程序
界面

图(
a) 主界面 图(
b) 预览
程序 (1)启动程序,其界面见图(a)
功能 (2)点击菜单:文件- >打印预览,其界面见图(b)
219

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(1)新建一单文档应用程序 Ex09_1
1~5 步按缺省选择,在 step 6 of 6 选择视图类型为 CScrollView 类。
(2)编写 OnDraw 函数(见清单 9 1)

清单9 1 OnDr 09_


aw 函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx 09_ 1ViewOnDr aw( CDC pDC)
{ CEx09_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC- >TextOut(100,10,"这是 MFC 的打印功能!");//输出一行文字
pDC- >Rectangle(30,100,330,400);//绘制一个矩形

(3)编译和运行


12 视类中的打印函数
视类中自动添加与打印有关的函数见清单 9 2。

09_
清单9 2 视类中的打印函数(在 Ex 1Vi
ew.
cpp文件中)

BOOLCEx 09_1V i
ew OnPreparePrint
in g(
CPr
int
Inf
o p
Ino)

{ return DoPreparePrinting(pInfo);

vo
idCEx09_ 1ViewOnBe i
gnPrint
ing(CDC/pDC/CPr i
ntInf
o/pIn
fo/)


vo
idCEx09_ 1ViewOnEndPr int
ing(CDC/pDC/CPr intIn
fo/pI
nfo/)

除了清单 9 2 中的 3 个函数外,还有下面两个函数。
(1)OnPreparePrinting()
在一个打 印 任 务 开 始 时, 应 用 框 架 首 先 要 调 用 OnPreparePrinting ()成 员 函 数,
OnPreparePrinting()接 收 一 个 指 向 CPrintInfo 类 对 象 的 指 针 参 数。 表 9 2 列 出 了
CPrintInfo 类中重要的成员数据和成员函数 。
单击菜单:文 件 - > 打 印 时 将 显 示 如 图 9 1 所 示 的 打 印 对 话 框,图 9 1 的 显 示 是
DoPreparePrinting(
)函数实现的,用户可通过该对话框设置 CPrintInfo 类的许多数据成员。
表9 2 CPr
int
Ino类的成员

成员数据和成员函数 说 明
m_pPD 指向作为 Print 对话框的 CPrint Dialog 对象的指针
m_bDirect 指示文档是否正在直接打印的标志,它旁路 Print 对话框
m_bPreview 指示文档是否正在预览的标志
m_bContinuePrinting 指示 MFC 是否应该保持在打印循环内的标志
m_nCurPage 当前正在打印的页数
m_nNumPreviewPages 确定多少页应该显示在打印预览窗口:1 或 2
220 m_lpUserData 指向应用程序定义结构的指针
第九章 打 印
……………………………………………………………………………………………………………………………………………

续 表
成员数据和成员函数 说 明
m_rectDraw 定义打印机 DC 上的当前可用页区域的矩形框
m_strPageDesc 含有页号显示信息的格式字符串
SetMinPage() 指定文档的第一个页数(通常为 1)
SetMaxPage() 指定文档的最末页码
GetMinPage() 获取文档的第 1 页码
GetMaxPage() 获取文档的最末页码
GetFromPage() 获取待打印的第 1 页码
GetToPage() 获取待打印的最末页码

(2)OnBeginPrinting()
在“打印”对话框确定退出后,应用框架调用 OnBeginPrinting()成员函数。 该函数中可
以分配打印所需的 GDI 资源。
(3)OnPrepareDC()
应用程序框架在显示或打印之前调用 OnPrepareDC()成员函数,在该函数中可重新设置
显示或打印的逻辑映射模式,使之在随后的显示或打印中生效 。
(4)OnPrint ()
OnPrint()成员函数的参数中一个参数为指向设备环境的指针,另一个为指向整个打印
过程中均使用到的打印信息对象 (CPrintInfo)的指针。 在打印或打印预览之前,应用框架
创建打印信息对象。打印结束后,删除该对象,释放资源。 在打印信息对象中记录了打印页
码范围,打印作业的当前状态以及打印份数等信息 。在打印过程中,应用框架和视类之间交
换打印操作的信息主要通过处理同一打印信息对象,进而相互传递的。 因此控制了打印过
程中的打印信息对象,也就控制了打印。

图9 1 打印对话框 图9 2 打印过程函数的调用

(5)OnEndPrinting()
在打印文档最后一页结束后调用 OnEndPrinting()成员函数,用于释放打印时分配的资
源。如果在重写 OnBeginPrinting()时创建的 GDI 对象,也需要重写 OnEndPrinting()删除
该 GDI 对象。整个打印过程函数的调用见图 9 2。


13 打印控制过程
应用程序框架可自动在程序中实现简单的打印及打印预览功能,在控制上由应用程序 221

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

框架和视类共同完成,控制过程如下:
(1)视类设置“打印”对话框初始值
(2)应用框架调用 Windows 标准“打印”对话框,获取有关参数
(3)视类分配用于打印的 GDI 对象
(4)应用框架创建用于打印的设备环境 CDC 对象
(5)应用框架调用 CDC 类的 StartDoc()成员函数
(6)视类通知应用程序需打印文档的页数
(7)应用框架调用 CDC 类的 StartPage()成员函数
(8)应用框架通知视类将要打印的页号
(9)视类按通知准备该页内容并发送 escape 代码设置打印机模式
(10)视类打印指定页
(11)应用框架调用 CDC 类的 EndPage()结束该页打印
(12)返回第 7 步打印下一页,如此循环直到打印完全部文档
(13)应用框架调用 EndDoc()结束本次打印过程
(14)视类释放 GDI 资源

2 打印缩放 、映射模式选择及其编程实例

从表 9 1 中的图(a)和图(b)可以看出,所打印的文档与在屏幕上显示的不同,尽管屏幕
上的矩形占据了应用程序窗口相当大的一部分,打印出来却十分的小。 这是因为屏幕上的
像素和打印机上的点尺寸不同。 尽管在两处的矩形都是 300 点阵,但较小的打印机点打出
的矩形看起来更小。这是 Windows 缺省的图像映射模式 MM_TEXT 的工作原理。如果想把被
打印的图像缩放到一指定尺寸,就需要选择一种不同的映射模式(见表 7 1)。
例如选择 MM_LOMETRIC 模式,即一个像素点= 01 毫米长。
【例9 2】 Ex09_2 功能见例 9- 1,采用 MM_LOMETRIC 模式。
程序的建立过程同例 9 1,重新编写的 OnDraw 函数见清单 9 3。
清单9 3 OnDr 09_
aw 函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx 09_ 2ViewOnDr aw(CDC pDC)
{ CEx09_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC- >TextOut(100,10,"下面绘制矩形的映射模式为 MM_LOMETRIC" );//输出一行文字
pDC- >SetMapMode(MM_LOMETRIC);//设置映射模式
pDC- >Rectangle(30,-100,330,-400);//绘制矩形

在绘制矩形时 Y 轴为负值是因为 MM_LOMETRIC 模式下 X


轴向右增加,Y 轴向上增加。而且,缺省的窗口坐标是直角坐
标系的右下象限,如图 9 3 所示。

222
图9 3 MM_
LOMETER
缺省坐标系
第九章 打 印
……………………………………………………………………………………………………………………………………………

3 多页打印及其编程实例

如果上述例题中的矩形很多,不能在一页中完成,就要打印多页,要实现多页打印仅仅
用缺省的程序是不够的,为此要通过如下的步骤来实现。
(1)通知 MFC 有多少页需要打印(或预览)
在 OnBeginPrinting()函数中用 SetMaxPage()函数来实现。
(2)重载 OnPrepareDC()函数,设置原点
【例9 3】 Ex09_3 多页打印的实现,具体说明见表 9 3。
表9 3 例9 3具体说明

项 目 图示和说明

程序
菜单
与界面

图(
a) 屏幕显示界面 图(
b) 打印预览界面

(1)启动程序,界面上共有 6 个矩形
程序功能 (2)单击鼠标左键向下增加一个矩形,数字增加 1。如单击 3 次,共有 9 个矩形,见图(a)
(3)点击菜单:文件- >打印预览,其界面见图(b)

(1)新建一单文档应用程序 Ex09_3
1~5 步按缺省选择,在 step 6 of 6 选择视图类型为 CScrollView 类。
(2)绘制矩形
① 在文档类头文件中添加记录矩形个数的变量
public:
int m_iNumRects;//矩形数

② 在构造函数中给变量赋初值,见清单 9 4。
09_
清单9 4 在构造函数中给变量赋初值(在 Ex 3Do
c.pp文件中)

09_
CEx 3Doc CEx 09_ c()
3Do
{ m_iNumRects = 9;

③ 添加和编写单击鼠标左键的消息函数(见清单 9 5) 223

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单9 5 OnLBu
tto 09_
nDown ()函数(在 Ex 3Vi
ew.
cpp文件中)

oidCEx 09_ 3ViewOnLBu t t
onDown(UINTnF lagsCPo
intp
oit)

{ CEx09_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc- >m_inumRects ++;
Invalidate();
CScrollView::OnLButtonDown(nFlags, point);

④ 重写 OnDraw()函数来绘矩形(见清单 9 6)
清单9 6 OnDr 09_
aw()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx 09_ 3ViewOnLBu t t
onDown(UINTnF lagsCPo
intp
oit)

{ CEx09_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDoc- >m_inumRects ++;
Invalidate();
CScrollView::OnLButtonDown(nFlags, point);

(3)编译和运行程序
① 选择文件- > 打印预览,6 个矩形在第一页上正确显示,第二页为空白,见图 9 4。
② 在窗口内单击 1 次,变成 7 个矩形
③ 选择文件- >打印预览,7 个矩形在第一页上不能完全显示,而第二页为空白,见图 9 5。
为此要解决多页显示与打印的问题 。

图9 4 6个矩形能正确显示 图9 5 7个矩形不能正确显示

(4)实现多页正确预览第一步———获取多少页需要预览或打印
重写 OnBeginPrinting()函数,注意去掉对函数的两个参数的注释,见清单 9 7。
清单9 7 OnBe
ginPr
int
in 09_
g()函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx 09_ 3View OnBe ginPri
nting(CDC pDCCPr intI
nfo p
Ino)

{ CEx09_3Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
int pageHeight = pDC- >GetDeviceCaps(VERTRES);
int logPixelsY = pDC- >GetDeviceCaps(LOGPIXELSY);
224
第九章 打 印
……………………………………………………………………………………………………………………………………………

int rectHeight = (int)(22* logPixelsY);


int numPages = pDoc- > m_inumRects* rectHeight/pageHeight+ 1;
pInfo- > SetMaxPage(numPages);

(5)编译和运行程序
单击一次鼠标左键,然后操作菜单:文件- > 预览,其界面见图 9 6。 可以看出,第二页
显示与第一页完全相同,这是不正确的。第二页应该显示第一页剩余的部分,而不是从开始
处重新显示相同的数据。

图9 6 两页显示完全相同 图9 7 显示正确

(6)实现多页正确预览第二步———设置原点
为保证 第 二 页 和 后 继 页 打 印 正 常, 必 须 改 变 MFC 确 认 的 页 顶 部, 重 载 视 类 的
OnPrepareDC()函数,见清单 9 8。
清单9 8 OnPr
epa
r 09_
eDC()函数(在 Ex 3Vi
ew.
cpp文件中)

oidCEx09_3V iewOnPr epa eDC(
r CDC pDCCPr intInfo p
Ino)

{ if(pDC- >IsPrinting()) //应用程序是否正在调用设备环境类的成员函数
{ int pageHeight = pDC- >GetDeviceCaps(VERTRES);//获得打印页的高度
//新原点的 Y 轴坐标为当前页码减 1 乘以 1 页的大小
int originY = pageHeight* (pInfo- >m_nCurPage- 1);
pDC- >SetViewportOrg(0,- originY);//设置新原点

//CScrollView::OnPrepareDC(pDC, pInfo);

(7)编译和运行程序
单击一次鼠标左键,然后操作菜单:文件- > 预览,其界面见图 9 7。可以看出多页打印
可正确预览了。

225
第十章 异 常 处 理

在 程 序 运 行 中 总 会 发 生 各 种 各 样 的 不 可 预 知 的 情 形 ,如 除 数 为 0,不 能 够 分 配 内
存 ,本 应 该 打 开 的 文 件 现 在 无 法 打 开 等 。 一 个 好 的 软 件 不 仅 要 保 证 软 件 的 正 确 性 ,而
且 应 该 具 有 一 定 的 容 错 能 力 。 在 程 序 设 计 时 要 做 到 允 许 用 户 排 除 错 误 ,继 续 运 行 程
序 ,至 少 要 给 出 适 当 的 提 示 信 息 ,不 能 轻 易 死 机 ,更 不 能 出 现 灾 难 性 的 后 果 。 因 此 ,在
设 计 程 序 时 ,要 充 分 考 虑 各 种 可 能 出 现 的 意 外 情 况 ,并 给 予 适 当 的 处 理 ,这 就 叫 做 异
常处理。
本章要点:
* CException 类及其派生类
* CFileException 类的应用

10
1 CExcep
tion 类
10
11 CEx
cep
tin类的函数

MFC 异常类的基类是 CException 类,CException 类除了构造函数外,只包括三个函数,
具体见表 10 1。

表10 1 CEx
cet
pin类的函数

函 数 作 用
void Delete() 删除一个异常类对象

virtual BOOL GetErrorMessage(


LPTSTR lpszError,//指向将接收错误信息的缓冲区的指针
UINT nMaxError,//缓冲区中可容纳的最大字符数,包括 NULL 结束符
提供产生的错误文本
PUINT pnHelpContex = NULL //接收帮助文本 ID 的 UINT 的地址,若为 NULL,
//则返回无 ID

virtual int ReportError(


UINT nType = OK, //指定消息框的风格
UINT nNMssageID = 0//若异常对象无错误信息,则指定显示信息的资源 ID( 字 报告提交给用户的消息
//符串表项)。为 0,显示信息“No error message is 框中的错误文本
//available”

226
第十章 异 常 处 理
……………………………………………………………………………………………………………………………………………

10
12 CEx
cep
tin类的派生类

CException 类的派生类见表 10 2。
表10 2 CEx
cet
pin类的派生类

派 生 类 作 用
CMemoryException 内存不够异常
CNotSupportedException 请求一个不支持的操作
CArchiveException 档案专用异常
CFileException 文件专用异常
CResourceException Windows 资源未找到或不可创建
COleException OLE 异常
CDBException 数据库异常(由 ODBC 的数据库类引发)
COleDispatchException OLE 分配(自动化)异常
CUserException 指示某个资源不可发现的异常
CDaoException 数据访问对象异常(由 DAO 类引起)
CInternetException Internet 异常

2 文件异常 CF
10 ileExcep
tion 类
CFileException 用于通知应用程序出现了与文件系统有关的错误 。 CFileException
包含几个公共数据成员和方法,有助于判断哪个文件出了问题引起的异常事件,从而扩展了
CException 的基本功能。

10
21 CF
ileEx
cep
tin类数据成员

CFileException 类有三个数据成员:m_strFileName、m_cause、m_IosError。
公有成员 m_strFileName 包含与异常相联系的文件名。在故障产生的一刻把它们联系
起来是一种较好的解决方案。
公有成员 m_cause 提供一个独立于操作系统的错误代码,其取值见表 10 3。
表10 3 CF
ileEx
cet
pio m_
n c
aue的取值

m_cause 取值 含 义
CFileException::none 没有遇到错误
CFileException::generic 遇到未指定的错误
CFileException::fileNotFound 文件未能找到
CFileException::badPath 所有或部分路径不合法
CFileException::tooManyOpenFiles 打开文件超过最大量
CFileException::accessDenied 不能访问此文件
CFileException::invalidFile 试图使用一个无效句柄
CFileException::removeCurrentDir 不能删去当前工作目录
CFileException::directoryFull 不能建立任何附加目录项
CFileException::badSeek 不能设置文件指针
227

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
m_cause 取值 含 义
CFileException::hardIO 遇到一个软件错误
CFileException::sharingViolation 不能加载 SHARE.EXE,或者共享区被锁
CFileException::lockViolation 试图锁住一个已经加锁的区域
CFileException::diskFull 试图写整个盘
CFileException::endOfFile 到达文件结束

公有成员 m_IosError 是一个长整型,包含相关的操作系统错误号。此成员包含的值来


自于 error.h 或 errno.h。
建议用 AfxThrowFileException(
)来建立和激发 CFileException。AfxThrowFileException(
)的
取用独立于操作系统,IosError 以及文件名作为输 入,使 用 NEW 操 作 符 在 堆 上 建 立 一 个
CException 对象,然后激发此异常事件。

10
22 CF
ileEx
cep
tin类成员函数

CFileException 类成员函数见表 10 4。
表10 4 CF
ileEx
cet
pin类成员函数

成员函数名 作 用
构造一个 CFileException 对象,用于保存原因代码和操作
CFileException:: CFileException()
系统错误
将一给定运行库错误值转换为一个 CFileException 枚举错
CFileException:: ErrnoToException()
误值
返回给 定 参 数 值 所 对 应 的 一 个 枚 举 符。若 未 知,则 返 回
CFileException::OsErrnoToException()
CFileException::generic
首先构造一个对应给定 nErrno 值的 CFileException 对象,
CFileException::ThrowErrno
然后发布一个异常
发布一个对应于给定参数值的 CFileException 对象。若未
CFileException::ThrowOsErrno
知,则发布一个码值为 CFileException::generic 的异常

CFileException 构造函数,原型如下:

CFileException(int cause = CFileException::none, LONG lOsError = - 1);

参数:cause———一个枚举类型变量,用于表示异常的原因;
IosError———指示异常操作的操作系统专用原因(若可用)。

10
23 编程实例
【例10 1】 将文件异常应用于文件操作中,将清单 8 2 的 OnButtonRead 函数进行修
改,采用文件异常来对文件操作进行处理,修改后的 OnButtonRead 函数见清单 10 1。请读
者按此方法修改清单 8 1 中的 OnButtonWrite 函数。
清单10 1 修改后的 OnBu
tto
nRe
a 08_
d函数(在 Ex 1Dl
g.pp文件中)


oidCEx08_1D lg
OnBu
tto
nRe
ad()
228 { fstream f1;//定义文件流对象
第十章 异 常 处 理
……………………………………………………………………………………………………………………………………………

char s200;
CFileDialog filedlg(1,//1-文件打开, 0-文件另存为
".txt|* .* " ,
NULL,
OFN_OVERWRITEPROMPT,
" 文本文件(* .txt)|* .txt|All Files (* .* )|* .* ||",
NULL);
if(filedlg.DoModal()= = IDOK)
{ UpdateData(true);
TRY
 strFileName = filedlg.GetFileName();
if(! IsTextFile(strFileName))//判断文件类型是否正确
{ AfxMessageBox("文件类型不正确!");
return;

AfxThrowFileException(CFileException::generic);
f1.open(strFileName, ios::in|ios::nocreate);
strFilePath = filedlg.GetPathName();
if(! f1.good())
AfxThrowFileException(CFileException::fileNotFound);
while(! f1.eof())
{ f1.getline(s, 255);
m_strRead = m_strRead+ "\r\n" + s;//添加文件中的文本到编辑框
UpdateData(false);

AfxMessageBox(strFileName+ " 文件读入完毕!");//保存结束提示

CATCH(CFileException, e)
{ CString msg;
switch(e- >m_cause)
{ case CFileException::generic:
msg =" 不能打开此类文件: "+ strFileName;
break;
case CFileException::fileNotFound:
msg =" 文件打开错!"+ strFileName;
break;
default:msg =" 系统捕捉的错误!";

MessageBox(msg," 文件异常");

END_CATCH
f1.close();

3 数据库异常类
10
CDBException 类为数据库异常类,由 CException 类派生,以 3 个继承的成员变量反映
对数据库操作时的异常: 229

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

● m_mRetCode:以 ODBC 返回代码(SQL_RETURN)的形式表明造成异常的原因。


● m_strError:字符串,描述造成抛出异常的错误原因。
m_strStateNativeOrigin:字符串,描述以 ODBC 错误代码表示的异常。

有关数据库异常的应用见第十一章数据库编程 。

230
第十一章 数据库编程

简而言之,数据库就是一组记录的集合。 VC++提供了多种多样的数据库访问技术;如
ODBC(Open DataBase Connectivity )、DAO (Data Access Object )、OLE DB (Object Link and
Embedding DataBase)、ADO(ActiveX Data Object)等。 这些技术各有自己的特点,但都提供
了简单、灵活、访问速度快、可扩展性好的开发技术。 利用这些技术可以从程序中直接操作
各种各样的数据库,如 SQL Server,Microsoft Access,Microsoft FoxPro 和 Paradox 等。
本章要点:
* 数据库基本知识
* ODBC 数据库编程
* ADO 数据库编程

1 数据库概述
11
数据库主要包括两种模型:平面数据库模型和关系数据库模型。 平面数据库模型包括
一系列记录,每个记录由若干字段组成。关系数据库就是若干个平面数据库的连接,在关系
数据库中用户不仅能像在平面数据库中那样查询记录,而且能够建立一个记录集合和另一
个记录集合之间的关系,这样大大提高了数据库的效率。 在关系数据库中,每一个记录集合
称为一张表,而表之间的连接是通过关键字段来实现的 。

11
11 数据库对象
数据库对象是数据表中的物理现象。 这些对象拥有唯一的名字,并保存数据和数据关
系信息。
(1)表、字段和记录
表类似于日常生活中的表格,它按行和列方式将相关信息排列成逻辑组。 表中的每一
行称为记录,每一列称为字段。如表 11 1 为学生信息数据表。
表11 1 学生信息数据表

StuID StuName StuJg StuClass StuCollege


001 李明 上海 机械 042 机械与动力工程
002 刘小芳 湖南 信息 032 信息科学
003 张强 北京 化工 041 化学工程

(2)主键
一个记录中,可以代表整个记录的字段就可以将其设为主键。 该字段需要具备下面两 231

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

个条件:
① 字段中的每一个值都是唯一的(即不能重复)。
② 在意义上具有代表性。
如在表 11 1 中,“学号”———StuID 字段比“姓名”———StuName 字段更适合作为主键。

11
12 ODBC
ODBC(Open Database Connectivity)即开放式数据库链接,是客户应用程序访问关系数
据库时提供的一个统一的接口。ODBC 已经成为一种标准,目前所有的关系数据库都提供了
ODBC 驱动程序,这使 ODBC 的应用非常广泛,基本上可用于所有的关系数据库。 但是 ODBC 只
能访问各种关系数据库,另一端为应用程序和开发工具提供了一种统一的接口。 ODBC 是基
于 SQL 而设计的,而且它还定义了 C 语言与 SQL 数据库之间的程序设计接口。 在程序执行
过程中,ODBC32.DLL 会调用特定数据库的驱动程序,因而它允许单个程序同时访问多个数据
库管理系统中的数据。

11
13 DAO
和 ODBC 不同,DAO 通过数据库引擎实现对数据库的访问,DAO 具有比 ODBC 更多更强的功
能。
(1)ODBC 使用一组动态链接库来实现而 DAO 通过 OLE 对象来实现,因此 DAO 具有更新的
结构。
(2)DAO 类是 ODBC 类的超集,它包含了 ODBC 类的大部分功能并增加了许多自己的功能,
它提供了一个功能更强的方法集 。
(3)MFC ODBC 类对 ODBC API 的封装并不完全,它并不能对 ODBC 实现底层控制,是一种高
级用户接口。而 MFC DAO 类对 DAO 的封装已经相当完全,能够对 DAO 进行绝大多数的底层操
作,是一种底层用户接口。

11
14 OLEDB
OLE DB 是 Visual C++开发数据库应用中提供的新技术,它基于 COM 接口。 因此,OLE DB
对所有的文件系统包括关系数据库和非关系数据库都提供了统一的接口。 这些特性使得
OLE DB 技术比传统的数据库访问技术更加优越 。

11
15 ADO
ADO 技术是基于 OLE DB 的访问接口,它继承了 OLE DB 技术的优点,并且,ADO 对 OLE DB
的接口作了封装,定义了 ADO 对象,使程序开发得到简化。 ADO 技术属于数据库访问的高层
接口。

2 ODBC 类与 ODBC 数据库应用程序


11
11
21 ODBC 类
232 MFC 的 ODBC 主要包括的类见表 11 2。
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

表11 2 MFC 的 ODBC 主要包括的类

类 名 作 用
CDatabase 主要功能是建立与数据源的连接
CRecordset 代表从数据源选择的一组记录(记录集)
提供了一个表单视图与某个记录集直接相连,利用对话框数据交换机制(DDX)在
CRecordView
记录集与表单视图的控件之间传输数据
支持记录字段数据交换(DFX),即记录集字段数据成员与相应的数据库的表的字
CFileExchange
段之间的数据交换
CDBException 代表 ODBC 类产生的异常

(1)CDatabase 类
CDatabase 对象表示到一数据源的连接,通过它可以操作数据源。该类的成员函数见表
11 3。
表11 3 CDa
tab
ase类常用成员函数

成 员 函 数 函 数 功 能
BeginTrans() 开始事务
BindParameters() 允许在调用 CDatabase::ExecuteSQL 前绑定参数
Cancel() 删除一次异步操作
CanTransact() 检测数据库是否支持事务,如果支持则函数返回非零值
CanUpdate() 检测 CDaoDatabase 对象是否可更新,如果可更新则返回非零值
CDatabase() 构造一个 CDatabase 对象
Close() 关闭数据库连接
CommitTrans() 执行事务
ExecuteSQL() 执行 SQL 语句,不返回记录
GetBookmarkPersistence() 确定通过记录集对象中的哪一个书签操作
GetConnect() 获得数据库的连接参数
GetCursorCommitBehavior() 确定在一个打开的记录集对象中提交一项事务的效果
GetCursorRollbackBehavior() 确定在一个打开的记录集对象中退回一项事务的效果
GetDatabaseName() 获得当前使用的数据库的名称
IsOpen() 检测 CDatabase 对象是否正连接到数据库上,如果已连接则返回非零值
Open() 通过一个 ODBC 驱动程序创建到数据源的连接
OpenEx() 通过一个 ODBC 驱动程序创建到数据源的连接
Rollback() 回滚事务,数据源返回先前的状态
SetLoginTimeout() 设置数据库连接的超时数(秒为单位)
SetQueryTimeout() 设置数据库查询操作的超时数(秒为单位)

应用程序可使用多个 CDatabase 类型的对象。构造一个对象并调用 Open()成员函数打开


一个连接。接着构造 CRecordset 类型的对象以操作连接的数据源,构造时向记录集对象传递
CDatabase 类型的指针。完成使用后,用 Close(
)成员函数销毁 CDatabase 类的对象。
一般情况下并不需要直接使用 CDatabase 类型的对象,因为 CRecordset 类的对象可以
实现大多数的功能。但是在进行事务处理时,CDatabase 类型对象就起到关键作用。 事务
(Transaction)指的是将一系列对数据源的更新放在一起,同时提交或一个也不提交,为的
是确保多用户对数据源同时操作时的数据正确性 。 233

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(2)CRecordset 类
CRecordset 对象表示从数据源中选择的一组记录即 “记录集”,通过该类的方法实现对
数据库中记录的各种操作。该类的数据成员和常用成员函数见表 11 4。

表11 4 CRe
cor
dSt类的数据成员以及常用成员函数

数据成员和成员函数 函 数 功 能
m_hstmt() 包含记录集的 ODBC 陈述句柄,类型为 HSTMT
m_nFields() 从数据源中选入的记录集类的字段数目,类型为 UNIT
m_nParams() 包含记录集类中参数数据成员数量,类型为 UNIT
m_pDatabase() 包含一个 CDatabase 对象指针,通过它访问数据源
m_strFilter() 用于构造 SQL WHERE 语句的字符串
m_strSort() 用于构造 ORDER BY 语句的字符串
AddNew() 准备向记录集中添加一条新记录
CRecordset() 构造一个记录集
Close() 关闭记录集
Delete() 删除记录集中的当前记录
Open() 从一个表、动态集或快照创建一个新记录集
CanAppend() 如果新记录可通过 AddNew 成员函数增加到记录集中,则返回非零
CancelUpdate() 取消所有由 AddNew 或 Edit 操作引发的待决的更新操作
CanScroll() 如果可以在记录中滚动,返回非零
CanTransact() 如果数据源支持事务,返回非零
CanUpdate() 返回非零则记录集可以更新
DoFiledExchange() 在记录集的字段数据成员和数据源对应记录间交换数据(双向)
Edit() 准备改变记录集中的当前记录
GetDefaultConnect() 获取缺省连接串
GetDefaultSQL() 获取要执行的缺省 SQL 串
GetRecordCount() 获得记录集对象中记录数
GetSQL() 得到用于从记录集选择记录的 SQL 字符串
GetTableName() 获取记录集所基于的表名
IsBOF() 返回非零则当前记录为第一条记录
IsDeleted() 返回非零则当前记录被删除,如果被删除则函数返回 TRCE
IsEOF() 返回非零则当前记录为最后一条记录
IsFieldDirty() 返回非零则当前记录中指定字段发生变化
IsOpen() 返回非零则记录集已经被打开
Move() 移动到记录集中指定的记录号
MoveFirst() 在记录集中把当前记录置为第一条记录
MoveLast() 在记录集中把当前记录置为最后一条记录
MoveNext() 在记录集中把当前记录置为下一条记录
MovePrev() 在记录集中把当前记录置为上一条记录
Update() 在数据源存储新的或编辑过的数据,以完成 AddNew 或 Edit 操作

(3)CFieldExchange 类
CFieldExchange 类支持数据库类所使用的记录集字段交换 (RFX)程式。 如果使用自己
234 定义的数据类型写数据交换程式,会使用这个类,否则不会直接使用此类。 RFX 在记录集对
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

象的字段数据成员与数据源中当前记录的相应字段之间交换数据 。
(4)CRecordView 类
CRecordView 类的对象用于在控件中显示数据库记录的视图。 这种视图是一种直接连
接到一个 CRecordset 对象格式视图,它从一个对话框模板资源创建,并将 CRecordset 对象
的字段显示在对话框模板的控件里 。

11
22 注册数据库
创建数据库应用程序前,首先应将数据库注册为可由 ODBC 驱动程序访问的数据库。 例
如将 Access 数据库 Stumsg.mdb 注册为 ODBC 数据源 StuMsg 的操作步骤如下:
(1)根据需要创建文件夹,例如 d:\ExDatabase,将 StuMsg.mdb 文件拷贝到其中。
(2)打开控制面板- > 性能和维护- > 管理工具- > 数据源 (ODBC)。 注意不同的操作系统
数据源 ODBC 所在的位置略有区别。
(3)双击数据源(ODBC)图标。显示如图 11 1 所示的 ODBC 数据源管理器对话框。

图11 1 ODBC 数据源管理器对话框 图11 2 创建新数据源对话框

(4)选择用户 DSN 标签,单击 Add 按钮,显示如图 11 2 所示的创建新数据源对话框。


选择“Mocrosoft Access Driver(* .mdb)”选项。
(5)单击完成按钮,显示如图 11 3 所示的 ODBC Microsoft Access 安装对话框。 在数
据源名称 输 入 “StuMsg ”(也 可 根 据 自 己 的 喜 好 输 入 其 他 名 称 ); 在 说 明 中 输 入 “student
message”(可以不输入,也可输入其他的说明文本)。

图11 3 ODBC Mi
cro
sof
tAc
ces安装对话框
s 图11 4 选择数据库对话框

(6)单击选择按钮,显示如图 11 4 所示的选择数据库对话框。在数据库名中选择数据 235



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

库文件 StuMsg.mdb。
(7)单击图 11 4 的确定按钮、单击图 11 3 的确定按钮图、单击图 11 1 的确定按钮。
通过上述步骤,完成了设置使用 Microsoft Access ODBC 驱动程序访问 StuMsg.mdb 数据
库文件的特殊机制。

11
23 ODBC 数据库编程实例
【例11 1】 Ex11_1ODBC 应用程序,具体说明见表 11 5。

表11 5 例11 1具体说明

项目 图示和说明

程 图(
b) 图(
c) 图(
d)
序 记录菜单 排序菜单 过滤菜单

图(
a) 主界面 图(e) 图(f) 图(g)
过滤对话框 对话框 对话框

(1)程序带有一后台数据库 StuMsg.mdb
程 (2)通过操作工具栏和记录菜单可以实现对数据库的浏览
序 (3)通过操作记录菜单中的增加记录和删除记录可实现对数据库记录的增加和删除
功 (4)通过操作排序菜单下的各选项,可分别按学号、姓名、籍贯、班级和学院排序
能 (5)通过操作过滤菜单下的各选项,弹出查找对话框,可分别按学号、姓名、籍贯、班级和学院过滤
(或查找)。如果没有找到记录,则弹出图(f);如果找到记录,则弹出图(g)

第一部分:数据库部分
(1)用 Access 建立数据库 StuMsg.mdb,并建立数据库表 studenttab。
数据库表的设计见表 11 6。

表11 6 s
tud
ent
tab表

字段名称 数据类型 字段大小 必填字段 允许空字符 备 注


StuID 文本 10 是 否 学 号
StuName 文本 10 否 是 姓 名
StuSex 是/否 否 性 别
StuBirthday 日期/时间 短日期 否 出生年月日
StuNativePlace 文本 20 否 是 籍 贯
StuClass 文本 20 否 是 班 级
StuCollege 文本 50 否 是 学 院
236 注:上述表中性别和出生年月这两个字段在本章的编程中没有使用,这两个字段的应用可参见第 17 章。
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

(2)按上述步骤注册数据库
第二部分:VC++部分
(1)新建一单文档应用程序 Ex11_1,并建立与 studenttab 表交互的记录集类。
① 在 MFC AppWizard 的 step1 中,选中 single document。
② 在 MFC AppWizard 的 step2 中,选中 Database view without file support,如图 11 5
所示。
③ 单击 Data Source 按钮,建立应用程序与先前建立的数据库之间的连接,在打开的
Database Options 对话框的 ODBC 下拉列表框中选择“StuMsg”数据源,如图 11 6 所示。

图11 5 MFCAppWi
zar
dSt
ep2o
f6 图11 6 数据库选择对话框

④ 单击 OK 按钮,在打开的 Select Database Tables 对话框中,选择“studenttab”表,如


图 11 7 所示。
⑤ 在 MFC Appwizard 的 step3-step5 中默认选择。

图11 7 数据表选择对话框 图11 8 MFCAppWi


zar
dSt
ep6o
f6

⑥ 在 MFC Appwizard 的 step6 的选择见图 11 8。


(2)框架分析
完成上述 步 骤 后,系 统 自 动 建 立 了 CEx11_ 1Doc 类、CEx11_ 1View 类、CEx11_ 1App 类、
CEex11_1AboutDlg 类、CEx11_1MainFrame 类和 CEx11_1Set 类。
① CEx11_1Set 类:该类主要是建立与数据表 studenttab 的交互。 在 Ex11_1Set.h 头文
237

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

件中定义了 7 个变量(见清单 11 1),其初始化在构造函数中进行。 这 7 个变量主要用来与


数据库 studenttab 中的 7 个字段进行交互。

11_
清单11 1 Ex 1Se
t.c
pp


lassCEx 11_ 1Se
tpub licCRe cords
et
 public:
CEx11_1Set(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CEx11_1Set)
//Field/Param Data
//AFX_FIELD(CEx11_1Set, CRecordset)
CString m_StuID;
CString m_StuName;
BOOL m_StuSex;
CTime m_StuBirthday;
CString m_StuNativePlace;
CString m_StuClass;
CString m_StuCollege;
//AFX_FIELD
//Overrides
//ClassWizard generated virtual function overrides
//AFX_VIRTUAL(CEx11_1Set)
public:
virtual CString GetDefaultConnect(); //Default connection string
virtual CString GetDefaultSQL(); //default SQL for Recordset
virtual void DoFieldExchange(CFieldExchange* pFX); //RFX support
//AFX_VIRTUAL

;

GetDefaultConnect()函数主要返回所连接的数据库名;GetDefaultSQL()函数主要返回
数据表名;DoFieldExchange()函数主要表明 CEx11_1Set 类中定义的 7 个变量与数据表中 7
个字段交换的对应关系。这些函数体见清单 11 2。

11_
清单11 2 Ex 1Se
t.c
pp

# include" stdafx.h"
# include" Ex11_1.h"
# include" Ex11_1Set.h"
# ifdef _DEBUG
# define new DEBUG_NEW
# undef THIS_FILE
static char THIS_FILE= __FILE__;
# endif
/////////////////////////////////////////////////////////////////////////////
//CEx11_1Set implementation
IMPLEMENT_DYNAMIC(CEx11_1Set, CRecordset)
CEx 11_1S e
tCEx 11_1Set(CDa
tab
asepdb):CRecord
set(pdb)
{ //{{AFX_FIELD_INIT(CEx11_1Set)
m_StuID = _T("");
m_StuName = _T("");
238
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

m_StuSex = FALSE;
m_StuBirthday = 0;
m_StuNativePlace = _T("");
m_StuClass = _T("");
m_StuCollege = _T("");
m_nFields = 7;//AFX_FIELD_INIT
m_nDefaultType = snapshot;

CSt
rin
gCEx 11_1SetGetDefault
Co nne t()

{ return _T("ODBC;DSN = StuMsg" );

CSt
rin
gCEx 11_1SetGetDefaulSQL()

{ return _T("[studenttab" );

vo
idCEx 11_1SetDoFieldExchange( CFiel
dEx changepFX)
{ //{{AFX_FIELD_MAP(CEx11_1Set)
pFX ->SetFieldType(CFieldExchange::outputColumn);
RFX_Text(pFX, _T("[StuID" ), m_StuID);
RFX_Text(pFX, _T("[StuName" ), m_StuName);
RFX_Bool(pFX, _T("[StuSex" ), m_StuSex);
RFX_Date(pFX, _T("[StuBirthday" ), m_StuBirthday);
RFX_Text(pFX, _T("[StuNativePlace" ), m_StuNativePlace);
RFX_Text(pFX, _T("[StuClass" ), m_StuClass);
RFX_Text(pFX, _T("[StuCollege" ), m_StuCollege);
//}}AFX_FIELD_MAP

② Ex11_1View 类。
该类通过定义一个 CEx11_1Set 类的指针实现与 CEx11_1Set 类的交互,见清单 11 3。
清单11 3 定义指针 m_
pSe 11_
t(在 Ex 1Vi h文件中)
ew.


las
sCEx 11_1V iewpub li
cCRe cor
dV i
ew
 protected:// create from serialization only
CEx11_1View();
DECLARE_DYNCREATE(CEx11_1View)
public:
//AFX_DATA(CEx11_1View)
enum IDD = IDD_EX11_1_FORM;
CEx 11_1Set m_ pSt //定义记录集类的指针

//}}AFX_DATA

(3)编辑界面,添加控件资源和成员变量
本例中添加 5 个编辑框显示数据表中的学号 、姓名 、籍贯 、班级和学院等 5 个字段,代
表学号的编辑框在属性设置时设置为 “只读 ”,即将图 11 9 中的 Read Only 选中 。 5 个编
辑框的 ID 号和成员变量见表 11 7。 用 “MFC ClassWizard”添加成员变量,添加方法见图
11 10。 239

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表11 7 控件ID 和成员变量

控 件 ID 号 成 员 变 量 变量类型
编辑框(学号) IDC_STUID m_pSet ->m_StuID CString
编辑框(姓名) IDC _STUNAME m_pSet ->m_StuName CString
编辑框(籍贯) IDC _STUNATIVEPLACE m_pSet ->m_StuNativePlace CString
编辑框(班级) IDC _STUCLASS m_pSet ->m_StuClass CString
编辑框(学院) IDC _STUCOLLEGE m_pSet ->m_StuCollege CString

图11 9 选中 Re
adOn
ly复选框 图11 10 添加成员变量

studenttab表中的字段与 CEx11_1View 类中各控件关联是通过 CEx11_1Set 记录集来完


成的,其关系如图 11 11 所示。

图11 11 数据交互关系

控件 ID 号与控件变量建立的联系见 CEx11_1View.cpp 文件的 DoDataExchange 函数(见


清单 11 4)。
清单11 4 DoDa
taEx
cha
ng 11_
e函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx 11_1View DoDataExchange( CDataEx c
h an
ge pDX)
{ CRecordView::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CEx11_1View)
DDX_FieldText(pDX, IDC_STUID, m_pSet ->m_StuID, m_pSet);
DDX_FieldText(pDX, IDC_STUNAME, m_pSet ->m_StuName, m_pSet);
DDX_FieldText(pDX, IDC_STUNATIVEPLACE, m_pSet ->m_StuNativePlace, m_pSet);
DDX_FieldText(pDX, IDC_STUCLASS, m_pSet ->m_StuClass, m_pSet);
DDX_FieldText(pDX, IDC_STUCOLLEGE, m_pSet ->m_StuCollege, m_pSet);
//}}AFX_DATA_MAP

(4)编译和运行
得到如表 11 4 的界面,应用程序已具有修改记录的功能 。方法是修改记录中的任意字
240 段(除 STUID),然后移动到另一记录。应用程序记录菜单也提供了与工具栏功能相同的移动
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

记录选项。
(5)添加和删除记录
① 在记录菜单下增加两个子菜单,并添加相应的消息函数(见表 11 8)。
表11 8 增加的菜单ID 号和消息处理函数

菜单标题 ID 号 菜单消息函数
增加记录 ID_RECORD_ADD OnRecordAdd
删除记录 ID_RECORD_DELETE OnRecordDelete

② 在 Ex11_1View.h 文件中,声明一 BOOL 变量,以区分是否选择增加操作。


protected:
BOOL m_bAdding;

③ 在 Ex11_1View.cpp 文件的构造函数中,给 m_bAdd 赋初值。


m_bAdding = false;

④ 编写 OnRecordAdd 函数(见清单 11 5)。


⑤ 添加和编写 OnMove 函数。
在工程工作区中选择 Class View 面板。 在 CEx11_ 1View 类名,单击鼠标右键。 选择
AddVirtual Function 命令。滚动左边的 New Virtual Function 列表框,选择其中的 OnMove
函数,如图 11 12 所示。 然后单击 Add Handler 按钮,将该函数增加到 Existing virtual
function overrides 列表框中。OnMove 函数见清单 11 6。
清单11 5 OnRe
cor 11_
dAdd()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx11_1V i
ew OnRe cordAdd()//增加记录
{ m_pSet ->AddNew();//建立一个空记录
m_bAdding = true;
CEdit * pCtrl = (CEdit* )GetDlgItem(IDC_EDIT_STUID);//取得学号编辑控件的指针
int result = pCtrl ->SetReadOnly(false);//学号编辑框只读状态改为 false
UpdateData(false);//显示新的空白记录

图11 12 增加虚函数 OnMo


ve函数 241

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单11 6 OnMo
v 11_
e函数(在 Ex 1Vi
ew.
cpp文件中)

BOOLCEx 11_1V iewOnMo v e(UINTn IDMo veComma nd)


{ if(m_bAdding)

m_bAdding = FALSE;
UpdateData(TRUE);//窗口数据传给记录集
if(m_pSet ->CanUpdate()) //能否对数据源更新
m_pSet ->Update();//将记录集添加到数据源
m_pSet ->Requery();
UpdateData(false);//将数据传送给窗口控件
CEdit * pCtrl = (CEdit* )GetDlgItem(IDC_EDIT_STUID);
int result = pCtrl ->SetReadOnly(true);
return true;

else
return CRecordView::OnMove(nIDMoveCommand);

向数据库中增加一个新的记录,就必须移动一个新的记录,这一操作强制调用视窗的
OnMove()成员函数。
⑥ 编写 OnRecordDelete 函数(见清单 11 7)。
清单11 7 OnRe
cor
dDe
let 11_
e函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx11_1V i
ew OnRe cor
dDelee()
t //删除记录
{ m_pSet ->Delete();//删除记录集
m_pSet ->MoveNext();//移动到要显示的下一个记录
if(m_pSet ->IsEOF())//记录指针是否在数据库的末尾
m_pSet ->MoveLast(; /定位于记录集的最后一个记录
) /
if(m_pSet ->IsBOF())//记录指针是否在数据库的开头
m_pSet ->SetFieldNull(NULL);//设置当前记录字段为 NULL
UpdateData(FALSE);

到此程序可执行添加、删除和更新的操作。
(6)排序
① 增加一个“排序”菜单,它包含 5 个子菜单,并添加相应的消息函数(见表 11 9)。
表11 9 在排序菜单中增加5个菜单选项

菜单标题 ID 号 菜单消息函数

学 号 ID_SORT_STUID OnSortStuid()

姓 名 ID_SORT_STUNAME OnSortStuname()

籍 贯 ID_SORT_STUNATIVEPLACE OnSortStunativeplace()

班 级 ID_SORT_STUCLASS OnSortStuclass()

学 院 ID_SORT_STUCOLLEGE OnSortStucollege()

242 ② 编写排序菜单下的四个子菜单的消息函数(见清单 11 8)。


第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

11_
清单11 8 排序菜单下的5个菜单选项的消息函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx11_1V i
ew OnSortStuid()//按学号排序
{ m_pSet ->Close();
m_pSet ->m_strSort =" StuID" ;
m_pSet ->Open();
UpdateData(false);


oidCEx11_1V i
ew OnSortStuname() //按姓名排序
{ m_pSet ->Close();
m_pSet ->m_strSort =" StuName" ;
m_pSet ->Open();
UpdateData(false);


oidCEx11_1V i
ew OnSortStunativep lac //按籍贯排序
e()
{ m_pSet ->Close();
m_pSet ->m_strSort =" StuNativePlace" ;
m_pSet ->Open();
UpdateData(false);


oidCEx11_1V i
ew OnSortStuclass() //按班级排序
{ m_pSet ->Close();
m_pSet ->m_strSort =" StuClass" ;
m_pSet ->Open();
UpdateData(false);


oidCEx11_1V i
ew OnSortStucoll
e ge() //按学院排序
{ m_pSet ->Close();
m_pSet ->m_strSort =" StuCollege" ;
m_pSet ->Open();
UpdateData(false);

(7)过滤(查找)
① 增加一个“过滤”菜单,它包含 5 个选项,并添加相应的消息函数(见表 11 10)。
表11 10 在过滤菜单中增加5个菜单选项

菜单标题 ID 号 菜单消息函数

学 号 ID_FILTER_STUID OnFilterStuid()

姓 名 ID_FILTER_STUNAME OnFilterStuname()

籍 贯 ID_FILTER_STUNATIVEPLACE OnFilterStunativeplace()

班 级 ID_FILTER_STUCLASS OnFilterStuclass()

学 院 ID_FILTER_STUCOLLEGE OnFilterStucollege()

② 建立查找对话框资源与类。
查找对 话 框 界 面 如 表 11 5 的 图 所 示, 对 话 框 ID 为 IDD _ FILTERDLG, 类 名 为
CFilterDlg,控件 ID 号与变量见表 11 11。
243

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表11 11 过滤对话框各控件和成员变量

控 件 标题(Caption) ID 号 成员变量 变量类型


静态文本 输入要查询的内容: IDC_STATIC_FILTER m_strStaticFilter CString
编辑框 IDC_EDIT_FILTER m_strEditFilter CString

③ 定义和函数
在 Ex11_1View.h 文件中定义的变量和函数见清单 11 9。
11_
清单11 9 定义变量和函数(在 Ex 1Vi
ew.
cpp文件中)


lassCEx11_ 1Viewpub l
icCRe co
rdV i
ew
 
CString strStaticFilter;
void DoFilter(CString strFiltertype);//执行查询函数
};

变量 strStaticFilter 的值根据查找的字段不同而改变,如按学号查询,则 strStaticFilter =


" 输入要查询的学号:",这时查找对话框中的静态文本 “输入要查询的内容:”变为"输入要查
询的学号:"。
在 Ex11_1View.cpp 文件中编写 DoFilter()函数,见清单 11 10。
清单11 10 DoF
ilt
e 11_
r()函数(在 Ex 1Vi
ew.
cpp文件中)


oidCEx 11_1V iew DoFi
lter(CStr
ings tr
Fil
tertype)
{ CFilterDlg dlg;
dlg.m_strStaticFilter = strStaticFilter;
nt RecordCount;
char strRecordCount10;
if(dlg.DoModal()= = IDOK)
{ CString str = strFiltertype+ " = " + dlg.m_strEditFilter+ " ";
m_pSet ->Close();
m_pSet ->m_strFilter = str;
m_pSet ->Open();
RecordCount = m_pSet ->GetRecordCount();
if(RecordCount = = 0)
{ AfxMessageBox("没有此记录!");
m_pSet ->Close();
m_pSet ->m_strFilter ="" ;
m_pSet ->Open();

else
 sprintf(strRecordCount," 共有 %d 条记录", RecordCount);
AfxMessageBox(strRecordCount);

UpdateData(false);

④ 编写过滤菜单下的四个选项的消息函数(见清单 11 11)。
11_
清单11 11 过滤菜单下的四个菜单选项的消息函数(在 Ex 1Vi
ew.
cpp文件中)


oi 11_
dCEx 1V i
ew OnF il
ter
Stu
i //按学号查询
d()
244 { strStaticFilter =" 输入要查询的学号:";
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

DoFilter("StuID" );


oidCEx11_1View OnF i
lterSt
un ame() //按姓名查询
{ strStaticFilter =" 输入要查询的姓名:";
DoFilter("StuName" );


oidCEx11_1View OnF i
lterSt
un at
iveplac //按籍贯查询
e()
{ strStaticFilter =" 输入要查询的籍贯:";
DoFilter("StuNativePlace" );


oidCEx11_1View OnF i
lterSt
u c
lass()//按班级查询
{ strStaticFilter =" 输入要查询的班级:";
DoFilter("StuClass" );


oidCEx11_1View OnF i
lterSt
u c
oll
e e()
g //按学院查询
{ strStaticFilter =" 输入要查询的学院:";
DoFilter("StuCollege" );

⑤ 在 Ex11_1View.pp 文件开头添加包含语句# include “FilterDlg.h”。


(8)编译和运行

3 ADO 数据库编程
11
11
31 ADO 结构
ADO 的结构模型包含了 7 种对象和 4 种集合,其说明见表 11 12。
表11 12 ADO 模型包含的7种对象和4种集合

对象和集合名称 说 明
连接对象(Connection) 表示与数据源的连接,以及处理一些命令和事务
命令对象(Command) 用于处理传递给数据源的命令
记录集对象(RecordSet) 用于处理数据的表格集,如获取和修改数据
域对象(Field) 用于表示记录集中的列信息,包括列值以及其他信息
参数对象(Parameter) 用于对传送给数据源的命令赋参数值
属性对象(Property) 用于操作在 ADO 中使用的其他对象的详细属性
错误对象(Error) 用于获得连接对象所发生的错误的详细信息
Connection 对象具有 Errors 集合。包含为响应与数据源有关的单一错误
错误集合(Errors)
而创建的所有 Error 对象
Command 对象具有 Parameters 集合。包 含 了 应 用 于 命 令 对 象 的 所 有 的
参数集合(Parameters)
Parameters 对象
Recordset 对 象 具 有 Fields 集 合。包 含 了 所 有 代 表 记 录 集 中 每 列 的
域集合(Fields)
Fields 对象
Connection、Command、Recordset 和 Field 对 象 都 具 有 Properties 集 合。
属性集合(Properties)
Properties 对象的集合中包含了这些对象的所有特性 245

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

11
32 连接对象
Connection 对象代表了打开的、与数据源的连接,在使用 ADO 访问数据库之前,必须先
创建一个连接对象,然后通过它打开到数据库的连接。 其常用的函数和属性分别见表 11
13 和表 11 14。
表11 13 连接对象的函数

函 数 说 明
Open() 在创建了一个连接对象后,使用该函数打开到数据源的连接
打开了到数据源的连接后,使用该函数执行指定的查询、SQL 语句、存储过程或提
Execute()
供特定指定的文本等内容
Close() 关闭 Connection 对象以便释放所有关联系统的资源
BeginTrans() 启动新的事务
CommitTrans() 保存所有更改并结束当前事务。它也可以启动新事务
RollbackTrans() 取消当前事务中所做的任何更改并结束事务。它也可以启动新事务

表11 14 连接对象的属性

属 性 说 明
Attributes 可以读写,并且其值可能为 AdXactCommitRetaining 或为 AdXactAbortRetaining
指示在终止尝试和产生错误之前执行命令所等待的时间,为长整型,单位为秒(默
ConnectionTimeout
认值为 15 秒),连接关闭时该属性为读/写,而打开时为只读
ConnectionString 包含用来建立到数据源的连接的信息
DefaultDatabase 指示 Connection 对象的默认数据库
使用该属性可以设置 Connection 对象的隔离级别。该属性为读/写,直到下次调
IsolationLevel
用 BeginTrans 方法时,该设置才可以生效
使用该属性可设置或返回当前连接上提供者正在使用的访问权限。Mode 属性只
Mode
能在关闭 Connection 对象时方可设置
Provider 使用该属性可设置或返回提供连接者的名称
Version 使用该属性返回 ADO 执行的版本号

11
33 命令对象
Command 对象定义了将对数据源执行的指定命令。 其常用的函数和属性分别见表 11
15 和表 11 16。
表11 15 命令对象的函数

函 数 说 明
Execute() 执行在 CommandText 属性中指定的查询、SQL 语句或存储过程
用指定的名称、类型、方向、大小和值创建新的 Parameter 对象,在参数中传递的
CreateParameter()
所有值都将写入相应的 Parameter 属性
使用 Cancel 方法终止执行异步 Excute 方法调用。如果在试图终止的方法中没
Cancel()
有使用 adRunAsync,则 Cancel 将返回运行错误

246
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

表11 16 命令对象的属性

属 性 说 明
CommandText 可设置或返回 Command 对象的文本
ConnectionTimeout 指示在终止尝试和产生错误之前执行命令所等待的时间
CommandType 指定命令类型,可以是文本命令、表格名或者是一个存储过程
ActiveConnection 指示指定的 Command 对象当前所属的 Connection 对象
State 指定对象的当前状态
Prepared 指定执行前是否保存命令的编译版本

11
34 记录集对象
Recordset 对象表示的是来自基本表或命令执行结果的记录全集。 其常用函数和属性
分别见表 11 17 和表 11 18。
表11 17 记录集对象的函数

函 数 说 明
MoveFirst() 移动记录集到第一条记录处
MoveLast() 移动记录集到最后一条记录处
MovePrevious() 移动记录集到当前记录的前一条记录处
MoveNext() 移动记录集到当前记录的后一条记录处
Move() 移动记录集到指定的当前记录处
使用 NextRecordset 方法返回复合命令语句中下一条命令的结果,或者是返回多
NextRecordset()
个结果的已存储过程结果
Open() 直接打开一个记录集,而不是作为执行命令或者连接命令产生的记录集
Close() 关闭记录集
Delete() 删除记录集中的当前记录
Update() 将当前对记录集的改动保存到数据源
CancelUpdate() 取消 Update 更新前所做的改变

表11 18 记录集对象的属性

属 性 说 明
AbsolutePage 指定当前记录集所在的页
使用该属性可根据其在 Recordset 中的序号位置移动到记录,或确定当前记录的
AbsolutePosition
序号位置
ActiveConnection 指示指定的 Recordset 对象当前所属的 Connection 对象
指示当前记录位置是否位于 Recordset 对象的第一个记录之前。如果当前记录
BOF 位于第一个对象之前,BOF 属性返回 TRUE,如果当前记录为第一个记录或位于其
后,将返回 FALSE

指示当前记录位置是否位于 Recordset 对象的最后一个记录之后。如果当前记


EOF 录位于最后一个对象之后,EOF 属性返回 TRUE,如果当前记录为最后一个记录或
位于其前,将返回 FALSE
CursorType 获得或设置当前光标的类型
EditMode 获得当前的编辑状态
247

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
属 性 说 明
Filter 指定一个在行集中移动时使用的过滤器
LockType 获得或设置当前的访问状态
获得或设置一次操作中的记录集对象中返回的最大行的数目,默认值为 0,表示
MaxRecord
无限制
PageCount 获得记录集中使用的页数
PageSize 获得或者指定一页中的行数
RecordCount 获得记录集中包含的记录的数目
Source 获得记录集中记录的来源
Status 返回当前行的状态

11
35 域对象
Field 对象代表使用普通数据类型的数据列 。其常用的函数和属性分别见表 11 19 和
表 11 20。
表11 19 域对象的函数

函 数 说 明

可将长二进制或字符数据填写到域对象中。在系统内存有限的情况下,可以使
AppendChunk()
用 AppendChunk 方法对长整型值进行部分而非全部的操作

检索域对象部分或全部长二进制或字符数据。在系统内存有限的情况下,可以
GetChunk()
使用 GetChunk 方法处理部分而非全部的长整型

表11 20 域对象的属性

属 性 说 明
ActualSize 指示字段值的实际长度
Attribute 域的属性集合,可以用来判断此域是否有固定长度或是否可以为空
DefinedSize 确定 Field 对象的数据容量
Name 列的名称
NumericalScale 在浮点数中用于指出小数点右边多少位用于表示这个数
OriginalValue 发生任何更改前已在记录中存在的 Field 的值
Precision 表示数字 Field 对象值的精度
Type 列中的值的数据类型
UderlyingValue 反映了数据源中列的当前值,当在事务的同步时被使用
Value 列的值,用来查询或设置此列的值

11
36 参数对象
Parameter 对象代表参数与基于参数化查询或存储过程的 Command 对象的相关参数,代
表使用普通数据类型的数据列。其常用的函数和属性分别见表 11 21 和表 11 22。

248
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

表11 21 参数对象的函数

函 数 说 明
可将长二进制或字符数据填写到域对象中。在系统内存有限的情况下,可以使
AppendChunk()
用 AppendChunk 方法对长整型值进行部分而非全部的操作

表11 22 参数对象的属性

属 性 说 明
表示参数的特性,这个属性包含了几个字节标志的组合。标志用于指示参数是
Attribute
否接收有符号,0 或者长数据值
指示 Parameter 所指的是输入参数、输出参数还是既输入又输出参数,及该参数
Direction
是否为存储过程返回的值
Name 参数对象的名称
NumericalScale 在浮点数中用于指出小数点右边多少位用于表示这个值
Precision 表示参数对象值的精度
Type 列中的值的数据类型
Size 表示 Parameter 对象的最大长度(按字节或字符)
Value 包含分配给参数的实际值

11
37 错误对象
错误随时可在应用程序中发生,常见的有无法建立连接、无法执行命令或对某些状态的
对象无法进行操作(例如,试图使用没有初始化的记录集 )。 每当错误出现时,一个或多个
Error 对象将被放到 Connection 对象的 Errors 集合中。当另一个错误产生时,Errors 集合
将被清空,并在其中放入新的 Errors 对象集。其常用的属性见表 11 23。
表11 23 参数对象的属性

属 性 说 明
Description 包含错误的文本
NativeError 使用该属性可对特殊 Error 对象检索特定的错误信息
Number 包含错误常量的长整型整数值
标识产生错误的对象。在向数据源发出请求之后,Errors 集合中如有多个 Error
Source
对象,该属性将十分有用
使用该属性读取由提供者在处理 SQL 语句过程中出现错误返回的 5 个字符的错
SQLState
误代码
Value 包含分配给参数的实际值

11
38 集合
ADO 提供“集合”,这是一种可方便地包含其他特殊类型对象的对象类型 。使用集合方法
可按名称(文本字符串)或序号(整型数)对集合中的对象进行检索。 ADO 提供的四种集合分
述如下:
(1)Errors 集合
Errors 集合包含为响应涉及提供者的单个错误而创建的所有 Error 对象。 任何涉及 249

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

ADO 对象的操作都可能产生一个或多个提供者错误 。产生错误时,可以将一个或多个 Error


对象置于 Connection 对象的 Errors 集合中。 其他 ADO 操作产生错误时,将清空 Errors 集
合,并且将新的 Error 对象置于 Errors 集合中。Errors 集合的常用函数见表 11 24。
表11 24 Er
ros集合的函数

函 数 说 明
对 Errors 集合使用 Clear 方法,来删除集合中现有的全部 Error 对象。发生错误
Clear()
时,ADO 将自动清空 Errors 集合,并以基于新错误的 Error 对象填充集合
Item() 返回集合中的 Error 对象

Errors 集合的主要属性有 Count:使用该属性可确定给定集合中对象的数目 。


(2)Parameters 集合
Parameters 集合 包 含 Command 对 象 的 所 有 Parameter 对 象。 使 用 Command 对 象 的
Parameters 集合的 Refresh 方法,可以为在 Command 对象中指定的存储过程或参数化查询,
检索提供者的参数信息。某些提供者不支持存储的过程调用或参数化的查询,使用这样的
提供者时调用 Parameters 集合的 Refresh 方法将返回错误。如果调用 Refresh 方法前没有
定义自己的 Parameter 对象而访问 Parameters 集合,ADO 将自动调用该方法并充填该集合 。
Parameters 集合的常用函数见表 11 25。
表11 25 Pa
rame
tes集合的函数

函 数 说 明
Append() 添加新的参数对象到集合
Delete() 从集合中删除参数对象
Refresh() 用于进程或者参数化命令中,从有关参数的提供程序那里收集信息
Item() 返回集合中的参数对象

Parameters 集合的主要属性有 Count:使用该属性可确定给定集合中对象的数目 。


(3)Fields 集合
Fields 集 合 包 含 Recordset 对 象 的 所 有 Field 对 象。 其 中 每 个 Field 对 象 对 应
Recordset 中的一 列。 在 打 开 Recordset 前 通 过 调 用 集 合 上 的 Refresh 方 法 可 以 充 填
Fields 集合。
Fields 集合的常用函数见表 11 26。
表11 26 F
iel
ds集合的函数

函 数 说 明
Append() 添加新的 Field 对象到集合
Delete() 从集合中删除 Field 对象
Refresh() 更新集合中的 Field 对象
Item() 返回集合中的 Field 对象

Fields 集合的主要属性有 Count:使用该属性可确定给定集合中对象的数目 。


(4)Properties 集合
Properties 集合包 含 特 定 对 象 实 例 的 所 有 Property 对 象。 某 些 ADO 对 象 包 含 由
250 Property 对象组成的 Properties 集合。每个 Property 对象与特定于提供者的 ADO 对象的
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

特性相对应。
Properties 集合的常用函数见表 11 27。
表11 27 Pr
ope
rti
es集合的函数

函 数 说 明
Refresh() 更新集合中的 Property 对象
Item() 返回集合中的 Property 对象

Properties 集合的主要属性有 Count:使用该属性可确定给定集合中对象的数目 。

11
39 ADO 数据库编程实例
【例11 2】 Ex11_2 后台采用 ACCESS 建立 StuMsg.mdb 数据库(同例 11 1)。 使用 ADO
技术访问数据库,实现对数据库的浏览。具体说明见表 11 28。
表11 28 例11 2具体说明

项 目 图示和说明

程序界面

(1)程序带有一后台数据库 StuMsg.mdb
程序功能
(2)通过操作工具栏可以实现对数据库的浏览

(1)新建一单文档应用程序 Ex11_2
一直按“下一步”,到 step 6 of 6 选择 FormView 视图。
(2)编辑界面,添加控件资源(见例 11 1 的表 11 7)
(3)引入 ADO 动态链接库
在VC++中通过在程序中使用预编译指令 # import 来告诉编译器将此指令中指定的动
态链接库引入工 程 中,并 从 动 态 链 接 库 中 取 出 其 中 的 对 象 和 信 息,产 生 msado15.tlh 和
ado15.tli两个头文件来定义 ADO 库 。 在应用程序的文件 stdafx.h 中加入的语句见清单
11 12。
清单11 12 引入 ADO 动态链接库(在s
tda
f h文件中)
x.

# import" c:\program files\common files\system\ado\msado15.dll"


no_namespace rename("EOF" ," adoEOF" )

(4)初始化 OLE/COM 库环境 251



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

ADO 库是一组 COM 动态库,在调用 ADO 前,必须初始化 OLE/COM 库环境。 在 MFC 应用程
序里,一个比较好的方法是在应用程序类的 InitInstance 成员函数中初始化 OLE/COM 库环
境。见清单 11 14。
(5)创建 ADO 与数据源的连接
ADO 与 数 据 源 的 连 接 是 通 过 连 接 智 能 指 针 来 创 建 的。 首 先 需 要 添 加 一 个 指 向
Connection 对象的指针,然后调用 CreateInstance ()来创建一个连接对象的实例,再调用
Open()函数来创建与数据源的连接。
① 定义变量 pADOConnect(见清单 11 13)。
11_
清单11 13 定义变量(在 Ex 2.h文件中)


lassCEx 11_2Apppub l
icCWi nApp
 public:
CEx11_2App();
_ConnectionPtr m_pADOConn; //定义 ADO 数据库连接对象指针
_RecordsetPtr m_pADOSet; //在(6)②中使用
CString VariantToCString(VARIANT var); //数据类型转换函数,在(6)②中使用
};
extern CEx11_2App theApp; //在(6)②中使用

② 创建连接对象(见清单 11 14)。
11_
清单11 14 初始化 OLE/COM 库环境(在 Ex 2.pp文件中)

BOOLCEx 11_2App Ini


tInst
ance()
{...//初始化 com 环境
if(FAILED(::CoInitialize(NULL)))
{ AfxMessageBox("ADO Init failed" );
return false;

//创建连接对象
HRESULT hr;
try
 hr = m_pADOConn.CreateInstance(__uuidof(Connection));//创建 Connection 对象
if(SUCCEEDED(hr))
{ hr = m_pADOConn ->Open("Provider = Microsoft.Jet.OLEDB.40;Data Source = Stumsg.mdb" ,
"" ,"" ,adModeUnknown);//连接数据库


catch(_com_error e) //捕捉异常
{ CString errormessage;
errormessage.Format("连接数据库失败! \r\n 错误信息:%s" ,e.ErrorMessage());
AfxMessageBox(errormessage);//显示错误信息

catch(...)
{ AfxMessageBox("Unknown Error ..." );

252 (6)获得记录集,在界面上显示数据库数据
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

① 在 CEx11_2App 类中定义和编写数据类型转换函数(定义见清单 11 13),函数体见清


单 11 15。数据类型转换函数也可在 CEx11_2View 类中进行。

11_
清单11 15 数据类型转换函数(在 Ex 2.pp文件中)

CS
tri
n gCEx 11_2V i
ew Varian
tToCS tr
ing(VARIANTv a //数据类型转换
r)
{ CString strValue;
_variant_t var_t;
_bstr_t bst_t;
time_t cur_time;
CTime time_value;
COleCurrency var_currency;
switch(var.vt)
{case VT_EMPTY:strValue = _T("");break;
case VT_UI1:strValue.Format ("%d" ,var.bVal);break;
case VT_I2:strValue.Format ("%d" ,var.iVal);break;
case VT_I4:strValue.Format ("%d" ,var.lVal);break;
case VT_R4:strValue.Format ("%f" ,var.fltVal);break;
case VT_R8:strValue.Format ("%f" ,var.dblVal);break;
case VT_CY:
var_currency = var;
strValue = var_currency.Format(0);
break;
case VT_BSTR:
var_t = var;
bst_t = var_t;
strValue.Format ("%s" ,(const char* )bst_t);
break;
case VT_NULL:strValue = _T("");break;
case VT_DATE:
cur_time = var.date;
time_value = cur_time;
strValue = time_value.Format("%A,%B%d,%Y" );
break;
case VT_BOOL:strValue.Format ("%d" ,var.boolVal);break;
default:strValue = _T("");break;

return strValue;

② 声明外部变量:extern CEx11_2App theApp 见清单 11 13。


③ 在 CEx11_2View 类中定义和编写 ShowTable()函数实现数据表的显示 (定义见清单
11 16,函数体见清单 11 17)。

11_
清单11 16 定义函数(在 Ex 2Vi h文件中)
ew.


las 11_
sCEx 2Viewpub l
icCFormV
iew
 protected:
void ShowTable();//显示表中的数据

清单11 17 在界面上显示数据库数据(
Ex11_
2Vi
ew.
cpp文件中) 253

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidCEx 11_
2V iew ShowTa be()
l //显示表中的数据
{ _variant_t vstrSQL;
vstrSQL =" SELECT * FROM Studenttab" ;//从数据表中读数据
try
 HRESULT hTRes;
hTRes = theApp.m_pADOSet.CreateInstance(_T("ADODB.Recordset" ));
if (SUCCEEDED(hTRes))
{ hTRes = theApp.m_pADOSet ->Open(vstrSQL,
_variant_t((IDispatch * )(((CEx11_2App* )AfxGetApp())->m_pADOConn),
true),adOpenDynamic,adLockPessimistic,adCmdText);
if(SUCCEEDED(hTRes))
{ TRACE(_T("连接成功! \n" ));
if(theApp.m_pADOSet ->BOF)
{ return;

if(theApp.m_pADOSet ->MoveFirst()= = S_OK)
{ m_strStuID = theApp.VariantToCString( theApp.m_pADOSet ->GetCollect("StuID" ));
m_strStuName = theApp.VariantToCString( theApp.m_pADOSet ->GetCollect("StuName" ));
m_strStuNativePlace = theApp.VariantToCString( theApp.m_pADOSet ->GetCollect( "StuNativePlace" ));
m_strStuClass = theApp.VariantToCString( theApp.m_pADOSet ->GetCollect("StuClass" ));
m_strStuCollege = theApp.VariantToCString( theApp.m_pADOSet ->GetCollect("StuCollege" ));
UpdateData(false);



}end try
catch(_com_error e) //捕捉异常
{ CString errormessage;
MessageBox("ShowTable 创建 db 记录集失败 1! + strSQL" );



oidCEx11_2V i
ew OnIni
tial
Upd a
te()//初始化视图
{ CFormView::OnInitialUpdate();
GetParentFrame()->RecalcLayout();
ResizeParentToFit();
ShowTable();//显示数据表中的记录

④ 用 ClassWizard 添加和编写初始化视图函数(见清单 11 17)。


⑤ 编译和运行。
(7)遍历记录集
利用 MoveFirst、MoveLast、MovePrevious、MoveNext 函数实现遍历记录集。
① 在工具栏中添加 4 个按钮,并添加消息函数(见表 11 29)。
表11 29 工具栏按钮ID 号和消息函数

工具栏 ID 号 工具栏按钮消息函数
ID_RECORD_FIRST OnRecordFirst()

ID_RECORD_PREV OnRecordPrev()
254
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

续 表
工具栏 ID 号 工具栏按钮消息函数

ID_RECORD_NEXT OnRecordNext()

ID_RECORD_LAST OnRecordLast()

② 编写工具栏按钮消息函数(见清单 11 18)。
11_
清单11 18 遍历记录集(在 Ex 2Vi
ew.
cpp文件中)


oidCEx 11_ 2ViewOnRe cordF i
rst()//首记录
{ theApp.m_pADOSet ->MoveFirst();
try
 m_strStuID = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuID" ));
m_strStuName = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuName" ));
m_strStuNativePlace = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect
("StuNativePlace" ));
m_strStuClass = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuClass" ));
m _strStuCollege = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect
("StuCollege" ));
UpdateData(false);

catch(_com_error*e)
{AfxMessageBox(e ->ErrorMessage());



oidCEx 11_ 2ViewOnRe cordPr ev()//前一记录
{ theApp.m_pADOSet ->MovePrevious();
if(theApp.m_pADOSet ->BOF)
{ AfxMessageBox("已经是首记录!");
theApp.m_pADOSet ->MoveLast();

try
//以下同 OnRecordFirst()


oidCEx 11_ 2ViewOnRe cordNe xt()//下一记录
{ theApp.m_pADOSet ->MoveNext();
if(theApp.m_pADOSet ->EOF)
{ AfxMessageBox("已经是末记录!");
theApp.m_pADOSet ->MoveLast();

try
//以下同 OnRecordFirst()


oidCEx 11_ 2ViewOnRe cordLa st()//末记录
{ theApp.m_pADOSet ->MoveLast();
try
//以下同 OnRecordFirst()

(8)编译和运行
【例11 3】 Ex11_3 后台采用 SQL Server2000 建立 StuMsg 数据库,数据库表同例 11 1。 255

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

使用 ADO 技术访问数据库,实现对数据库的浏览。其余同例 11 2。
第一部分:数据库部分
(1)用 SQL Server 建立数据库 StuMsg
① 启动 SQL Server 服务管理器(见图 11 13)。
② 运行 SQL Server 企业管理器,并新建数据库 StuMsg(见图 11 14)。

图11 13 SQLS
erv
er服务管理器 图11 14 SQLS
erv
er企业管理器

③ 新建和设计数据表 studenttab(见图 11 15)。


第二部分:VC++部分
(1)新建一单文档应用程序 Ex11_3
一直按“下一步”,到 step 6 of 6 选择 FormView 视图。
(2)编辑界面,添加控件资源(见例 11 1 的表 11 7)
(3)引入 ADO 动态链接库(同例 11 2,见清单 11 12)
(4)初始化 OLE/COM 库环境(见清单 11 20)
(5)创建 ADO 与数据源的连接
图11 15 s b的设计
① 定义变量 pADOConnect(见清单 11 19)。 tud
ent
ta

11_
清单11 19 定义变量(在 Ex 3.h文件中)


lassCEx 11_2Apppub l
icCWi nApp
 public:
CEx11_2App();
_ConnectionPtr m_pADOConn; //定义 ADO 数据库连接对象指针
_RecordsetPtr m_pADOSet; //在(6)②中使用
Bool ADOExecute(_RecordsetPtr &pADOSet, _variant_t &strSQL); //在(6)②中使用
CString VariantToCString(VARIANT var); //数据类型转换函数,在(6)②中使用
};
extern CEx11_2App theApp; //在(6)② 中使用

② 创建连接对象(见清单 11 20)。
11_
清单11 20 初始化 OLE/COM 库环境和创建连接对象(在 Ex 3.pp文件中)

BOOLCEx 11_2App Ini


tIns
tance()
{...//初始化 OLE/COM 环境
256 if(FAILED(::CoInitialize(NULL)))
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

{ AfxMessageBox("ADO Init failed!" );


return false;

//创建连接对象
HRESULT hr;
try
 hr = m_pADOConn.CreateInstance(__uuidof(Connection));//创建 Connection 对象
if(SUCCEEDED(hr))
{ //连接数据库
m_pADOConn ->ConnectionString =" Provider = SQLOLEDB;server = MICROSOF-87DA1D;
UID = sa;PWD ="" ;database = StuMsg" ;
//打开连接
hr = m_pADOConn ->Open("","","",- 1);

catch(_com_error e) ///捕捉异常
//以下同清单 11 14

在清单 11 20 中注意:server = MICROSOF- 87DA1D 与图 11 13 中的服务器名相同;UID


为安装 SQL Server 时输入的用户名,本例为 sa;PWD 为安装 SQL Server 时输入的用户密码,
本例未设密码;database 为要连接的数据库名,本例为 StuMsg。
③ 添加和编写 ExitInstance 函数(添加见图 11 16,函数体见清单 11 21)。

图11 16 用 C
las
sWi
zad添加 Ex
r itI
nst
ane函数

清单11 21 Ex
itI
nst
anc 11_
e函数(在 Ex 3.pp文件中)

Bo
olCEx11_ 3AppEx itInst
ance()
{ if(adStateOpen = = m_pADOConn ->State)
m_pADOConn ->Close(); //释放 ADO Connection
m_pADOConn.Release();
if(adStateOpen = = m_pADOSet ->State)
m_pADOSet ->Close(); //释放 ADO RecordSet
m_pADOSet.Release();
return CWinApp::ExitInstance();

(6)获得记录集,在界面上显示数据库数据 257

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

① 编写数据类型转换函数(同例 11 2)。
② 定义和编写 ADOExecute 函数(定义见清单 11 13,函数体见清单 11 22)。
清单11 22 ADOEx
ecu
t 11_
e函数(在 Ex 3.pp文件中)

Bo
olCEx 11_
3App ADOEx ecute(_Recordse
tPtr&pADOS et_variant_t&s t
rSQL)
{ if (pADOSet ->State = = adStateOpen)pADOSet ->Close();
try
pADOSet ->Open (strSQL, m_pADOConn.GetInterfacePtr(), adOpenStatic,
adLockOptimistic, adCmdUnknown);
return true;

catch(_com_error &e)
{ CString err;
err.Format("ADO Error: %s" ,(char* )e.Description());
AfxMessageBox(err);
return false;

③ 声明外部变量:extern CEx11_3App theApp 参见清单 11 13。


④ 在 CEx11_3View 类中定义和编写 ShowTable()函数实现数据表的显示 (定义见清单
11 23,函数体见清单 11 24)。
11_
清单11 23 定义函数(在 Ex 3Vi h文件中)
ew.


las 11_
sCEx 3Viewpub l
icCFormV
iew
 protected:
void ShowTable();//显示表中的数据

清单11 24 在界面上显示数据库数据(
Ex11_
3Vi
ew.
cpp文件中)


oidCEx11_ 3View ShowTa be()
l //显示表中的数据
{ __variant_t vstrSQL;
vstrSQL =" Select * from studenttab" ;
theApp.ADOExecute(theApp.m_pADOSet,vstrSQL);
theApp.m_pADOSet ->MoveFirst();
m_strStuID = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuID" ));
m_strStuName = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuName" ));
m_strStuNativePlace = theApp.VariantToCString(
theApp.m_pADOSet ->GetCollect(
"StuNativePlace" )
);
m_strStuClass = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuClass" ; ))
m_strStuCollege = theApp.VariantToCString(theApp.m_pADOSet ->GetCollect("StuCollege" ));
UpdateData(false);


oidCEx11_ 3View OnInitialUpd ae()
t //初始化视图
{ CFormView::OnInitialUpdate();
GetParentFrame()->RecalcLayout();
ResizeParentToFit();
ShowTable();//显示数据表中的记录

258
第十一章 数 据 库 编 程
……………………………………………………………………………………………………………………………………………

⑤ 用 ClassWizard 添加和编写初始化视图函数(见清单 11 24)。


⑥ 编译和运行。
(7)遍历记录集(同例 11 2)
(8)编译和运行

259
第十二章 动态链接库

动态链接库(Dynamic Link Library, DLL)是作为共享函数库的可执行模块。 如果把几


个函数收集到一个 DLL 中,则这个 DLL 可以输出这些函数供其他程序使用。 在编写标准的
模块软件时,经常将动态链接库作为软件的一个模块 。
DLL 中一般定义有两种类型的函数:导出函数 (export function)和内部函数 (internal
function)。导出函数是可以被外部程序调用的函数,内部函数只能在 DLL 内部使用。
本章要点:
* 动态链接库的建立
* 动态链接库库函数的导出
* 动态链接库的使用

1 动态链接库的分类与创建
12
12
11 动态链接库分类
VC++可以建立以下四种类型的动态链接库:
(1)win32DLL,也称非 MFC
(2)使用“MFC DLL 向导”生成静态链接到 MFC 的常规 DLL
静态链接到 MFC 的 Regular DLL 是内部使用 MFC 的 DLL, DLL 中的导出函数可以被 MFC
和非 MFC 可执行程序调用。这种类型的 DLL 在建立时使用的是 MFC 静态链接库,导出函数使
用的是标准 C 接口。例如:

extern" C" EXPORT ExportedFunction();

其中,ExportedFunction 是导出函数名。
(3)使用“MFC DLL 向导”生成动态链接到 MFC 的常规 DLL
动态链接 MFC 的 Regular DLL 是内部使用 MFC 的 DLL,DLL 中的导出函数可以被 MFC 和
非 MFC 可执行程序调用。这种类型的 DLL 在建立时使用的是 MFC 动态链接库。 导出函数使
用的是标准 C 接口,但必须使用宏 AFX_MANAGE_STATE 来设置 MFC 模块状态。 为此,必须在
所有 DLL 导出函数的开始添加如下代码行:

AFX_MANAGE_STATE(AfxGetstaticModuleState());

其中,全局函数 AfxGetstaticModuleState 用于设置模块状态。


宏 AFX_MANAGE_STATE 不能在静态链接 MFC 的 Regular DLL 和动态链接 MFC 的扩展 DLL
260 中使用。
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

(4)使用“MFC DLL 向导”生成 MFC 扩展 DLL


MFC 扩展 DLL 是从已有的 MFC 类派生的新的可再用类的 DLL。扩展 DLL 在建立时使用的
是 MFC 动态链接库。扩展 DLL 中的导出函数只能由动态链接 MFC 的应用程序调用。 使用扩
展 DLL,可以从 MFC 派生新的自定义类,然后提供 MFC 扩展版给调用 DLL 的应用程序。
由于扩展 DLL 没有 CWinAPP 派生对象,因此必须添加初始化和终止代码到 AppWizard 生
成的 DllMain 函数中。扩展 DLL 的入口点函数是 DllMain,当应用程序加载 DLL 时自动执行
DllMain,DllMain 负责初始化并终止 DLL。DllMain 函数见清单 12 1。
清单12 1 D
llMa
in函数

static AFX_EXTENSION_MODULE MyExtDll =NULL,NULL;


extern" C" int APIENTRY
Dl
lMa in( HINSTANCh In
stance DWORDdwRe ason
LPVOIDl pReservd)

{ //Remove this if you use lpReserved
UNREFERENCED_PARAMETER(lpReserved);
if(dWReason = = DLL_PROCESS_ATTACH)
{ TRACE0("MyExtDLL Initializing!\n" )
//ExtenslonDllone- time Initializing
if(! AfxInitExtensionModule(MyExtDll,hlnstance))
return 0;
//Insert this DLL into the resource chain
new CDynLinkLibaray(MyExtDll);

else if(dwReasion = = DLL_process_DETACH)
{ TRACEO("MyExtDLL Terminating!\fi" );
//Terminate the library before destructors are called
AfxTermExtensionModule(MyExtDll);

return 1;

DllMain 函数的参数 hInstance 是 DLL 的模块句柄,lpReserved 是指定 DLL 初始化和清


除的某些特定内容。参数 dwReason 可以为以下值:
(1)DLL_PROCESS _ ATTACH:表 示 DLL 被 链 接 到 当 前 进 程 的 地 址 空 间 中。 这 时 要 调 用
AfxInitExtension—Module 进 行 DLL 初 始 化。 如 果 初 始 化 成 功, 则 返 回 TRUE, 否 则 返 回
FALSE。初始化期间,如果扩展 DLL 要导出 CRuntimeClass 对象或资源给应用程序,则必须创
建新的 CDynLinkLibrary 对象。
(2)DLL_THREAD_ATTACH:表示当前进程创建一个新线程,新线程要访问 DLL。 仅在进程
中的第二线程链接 DLL 时才使用这种调用。
(3)DLL_THREAD_DETACH:表示线程与 DLL 正常分离。
(4)DLL_PROCESS_DETACH:表示进程与 DLL 正常分离。
如果以后 要 从 一 个 或 多 个 Regular DLL 中 使 用 扩 展 DLL, 那 么 必 须 导 出 一 个 创 建
CDynLinkLibrary 对象的 初 始 化 函 数。 导 出 的 初 始 化 函 数 必 须 在 使 用 扩 展 DLL 的 每 个
Regular DLL 中调用,通常是在 Regular DLL 的 CWinApp 派生对象的 InitInstance 成员函数
中调用。
如果扩展 DLL 被显式链接到可执行程序中,那么必须在 DLL_PROCESS_DETACH 分支中添 261

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

加对 AfxTermExtensionModule 的 一 次 调 用。 每 个 进 程 从 扩 展 DLL 分 离 时, 必 须 调 用
AfxTermExtensionModule 函数来释放扩展 DLL。如果扩展 DLL 是隐式链接到可执行程序中,
则没有必要调用 AfxTermExtensionModule 函数。
如果显示链 接 到 扩 展 DLL 的 应 用 程 序 是 多 线 程 的,那 么 应 该 使 用 AfxLoadLibrary/
AfxFreeLibrary 代替 LoadLibrary/FreeLibrary 来加载/卸载扩展 DLL。

12
12 使用 AppWi
zad创建 MFCDLL

使用 Appwizard 来创建 MFC DLL 的基本结构,其步骤如下:
(1)从菜单“File”中选择“New”命令,弹出“New”对话框
(2)切换到“Projects”选项卡,选择项目类型为“MFC Appwizard(dll)”(见图 12 1)
(3)在“Project name”编辑框中键入名字,如“myDll”
(4)单击“OK”按钮,弹出图 12 2 所示的对话框
(5)选择要创建动态库的类型
单 选 按 钮,表 示 创 建 静 态 链 接 到 MFC 的
“Regular DLL with MFC statically linked ”
Regular DLL。
“Regular DLL using shared MFC DLL”单选按钮,表示创建动态链接到 MFC 的 Regular DLL。
“MFC Extension DLL(using shared MFC DLL)”单选按钮,表示要创建动态链接到 MFC 的
扩展 DLL。
(6)选择类型后,单击“Finish”按钮
(7)在随后的对话框中单击“OK”按钮,完成动态链接库框架的创建

图12 1 新建 MFCAppWi
zad(
r d
ll) 图12 2 “
MFCAppWi
zar
dS
te f1”对话框
p1o

2 从 DLL 中导出函数
12
DLL 文件与可执行文件非常相似,不同点在于 DLL 包含有导出表 (Export Table)。 导出
表包含 DLL 中每个导出函数的名字,这些函数是进入 DLL 的入口点。 只有导出表中的函数
可以被外部程序调用。
从 DLL 中导出函数有三种方法:
(1)创建模块定义文件(.DEF),并在建立 DLL 时使用此文件。
262 (2)在导出函数的定义中使用关键字_declspec(dllexport)。
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

(3)使用 AFX_EXT_CLASS 宏导出类,这种方法只能用于扩展 DLL。

12
21 使用.
DEF文件导出函数
模块定义文件(Export Definition File,DEF)是由一个或多个用于描述 DLL 属性的模
块语句组成的文本文件。每个.DEF 文件必须至少包含以下模块定义语句 。
(1)第一个语句必须是 LIBRARY 语句,这个语句指出 DLL 的名字,链接器将这个名字放
到 DLL 导入库(import library)中。DLL 导入库中包含了指向外部 DLL 的函数索引指针。
(2)EXPORTS 语句列出导出函数的名字,以及导出函数的序数值 (由@ 号与数字构成 )。
序数值可以省略,编译器(Compiler)会为每个导出函数指定一个,但这样指定的值不如自己
指定的明确。
(3)使用 DESCRIPTION 语句描述 DLL 的用途,这个语句可以省略。
例如:

LIBRARY BTREE
DESCRIPTION" 实现二叉搜索树"
EXPORTS
Insert@ 1
Delete@ 2
Member@ 3
Min @ 4

如果使用 AppWizard 来创建一个 MFC DLL,那么 AppWizard 将创建一个.DEF 文件的框架并自


动添加到项目中。对于非 MFC DLL 则必须自己创建 DEF 文件并将其添加到项目中去。
建立 DLL 时,链 接 器 使 用.DEF 文 件 来 创 建 一 个 导 出 文 件 (.EXP )和 一 个 导 入 库 文 件
(.LIB),然后使用导出文件来创建.DLL 文件。
如果建立的是 MFC 扩展 DLL 并使用.DEF 文件导出函数,那么必须放置以下代码到包含
被导出类的头文件的开始和末尾:
# undef AFX_DATA
# define AFX_DATA AFX_EXT_DATA
//<头文件体>
# undef AFX_DATA
# define AFX_DATA

12
22 使用关键字_
dec
lspec(
dl
lexpo
rt)
可以使用关键字_declspec(dllexport)从 DLL 导出数据、函数、类或类的成员函数。 如
果使用关键字_declspec(dllexport),则不需要.DEF 文件。
例如,可以使用以下方法导出函数:

void _declspec(dllexport)Function(void);

又如,可以使用以下方法导出类中的所有公有成员变量和成员函数:

class _declspec(dllexport)CSampleExport:public CObject 263



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

...类定义...}

12
23 使用 AFX_
EXT_
CLASS 导出和导入
MFC 扩展 DLL 使用宏 AFX_EXT_CLASS 来导出类,而链接到扩展 DLL 的可执行文件使用这
个宏来导入类。使用宏 AFX_EXT_CLASS,扩展 DLL 和链接到扩展 DLL 的可执行文件可以使用
相同的头文件。
如果要导出整个类,那么可以在 DLL 的头文件中添加 AFX_EXT_CLASS 关键字到类的声
明中。例如:

class AFX_EXT_CLASS CMyClass:public CDocument


 //<类体> = }

12
24 调用约定
默认时,VC++使用C++调用约定。如果要从 C 语言模块中使用C++编写的 DLL 函数,那么
应该在函数声明中用 extern" C" 来指定。例如:

extern" C" _declspec(dllexport)int MyFunc(int);

如果要从 C/C++语言模块中访问 C 编写的 DLL 函数,那么要使用_cplusplus 预处理宏,


并用 extern" C" 来声明这些函数。例如:
//MyFunc.h
ifdef _cplusplus
extern"C " # endif
_declspec(dllexport)void Myfuncl();
_declspec(dllexport)void MyFunc2();
# ifdef __cplusclpus

# endif

3 链接 DLL 到可执行程序
12
与应用程序不同,DLL 必须与可执行文件链接后才能使用 。链接 DLL 到可执行程序有两
种方式:隐式链接(Implicit linking)和显式链接(Explicit linking)。

12
31 隐式链接
隐式链接时,使用 DLL 的可执行文件链接到该 DLL 导入库 (.LIB 文件)中。 当加载使用
DLL 的可执行文件时,操作系统将加载 DLL。 客户端可执行文件调用 DLL 的导出函数,就好
像这些函数包含在可执行文件内部一样 。为了隐式链接 DLL,可执行文件需要从 DLL 提供者
获取以下三个文件。
(1)包含导出函数(或C++类)声明的头文件(.H)
264 (2)导入库文件(.LIB 文件)
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

(3)实际的 DLL(.DLL 文件)


可执行文件的每个使用导出函数的源文件中还需要用 # include 语句包含有导出函数
(或 C++类)的头文件。从编程角度讲,调用导出函数与调用其他函数的方法完全一样 。
建立可执行文件时,必须与导入库文件链接。 如果是在VC++集成环境中,那么请选择
“Projects”选单的“Settings”选项,然后从 “Project Settings”对话框的 “Link”选项卡的
“Object/Library Modules”编辑框中指定导入库的名字。

12
32 显式链接
显式链接时,使用 DLL 的可执行文件在运行时通过函数调用的方式来显式加载或卸载
DLL,并通过函数指针来调用 DLL 的导出函数。
要显式链接 DLL,应用程序必须:
(1)调用 LoadLibrary 或 AfxLoadLibrary 函数来加载 DLL 并获取模块句柄。
(2)调用 GetProcessAddress 来获取应用程序要调用的导出函数的指针。由于应用程序通
过指针调用 DLL 导出函数,因此编译器不用生成外部引用,从而不用链接到 DLL 的导入库中。
(3)使用完 DLL 后,调用 FreeLibrary 或 AfxFreeLibrary 函数来卸载 DLL。

4 动态链接库编程实例
12
12
41 编程实例一———采用关键字导出、隐式链接
【例12 1】 Ex12- 1 建 立 一 动 态 链 接 到 MFC 的 常 规 DLL, 采 用 关 键 字 _ declspec
(dllexport)导出,使用隐式链接方式,具体说明见表 12 1。
表12 1 例12 1功能说明

项目 图 示 和 说 明

程序
界面

本例创建两个工程:
工程 1:创建动态链接库 Ex12_01Dll,在动态库中实现两个数的平方和及两个数的平方差
程序
计算
功能
工程 2:使用动态链接库 Ex12_01App,点击使用动态链接库对话框中的平方和,则调用动态库
中的平方和计算函数;点击对话框中的平方差,则调用动态库中的平方差计算函数

工程 1:创建动态链接库
(1)动态链接库工程项目 Ex12_01Dll
选择 File - > New,然 后 单 击 Project 选 项,选 择 MFC AppWizard (dll ),注 意 不 是 MFC
AppWizard(exe),如图 12 1 所示,输入工程名 Ex12_01Dll。点击 OK,弹出创建 MFC 常规 DLL 265

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

向导的 step 1 of 1 对话框,如图 12 2 所示,选择 Regular DLL using shared MFC。


向导共产生 7 个文件,其中 4 个实现文件,3 个头文件,其中 Ex12_01.def 为模块定义
文件。
(2)编写动态链接库中的函数
本例题包括平方和及平方差的两个计算函数(见清单 12 2)。

12_
清单12 2 动态链接库库函数(在 Ex 01D
ll.
cpp文件中)

...
CEx12_01DllApp theApp;
//以下为添加的程序
# define DllExport __declspec(dllexport)
ex
t e
rn"C"D l
l Exportd oubeMyAdd(
l doub
lexdo
ubey)
l //平方和计算函数
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
return x* x+ y* y;


xte
rn"C"D l
l Exportd oubl
eMyMi
nus(doublexdoubley) //平方差计算函数
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
return x* x- y* y;

创建一个与 MFC 动态链接的常规 DLL 时,需要使用 AFX_MANAGE_STATE 宏命令来正确地


切换 MFC 模块的状态。MFC 进程中的每个模块都保存着状态信息,这意味着 DLL 和调用它的
应用程序有不同的状态信息。因此,任何直接被调用的函数都要告诉 MFC 它要使用的状态。
所以每个输出函数的开始处都要加上 AFX_MANAGE_STATE (AfxGetStaticModuleState ())语
句。这样函数在调用期间就可以设置正确的状态信息 。 如果 DLL 和 MFC 以静态方式链接,
则不能使用此宏命令。
(3)编译和链接
按 F7 或选择 Build 编译、链接 Ex12_01Dll 工程项目,如果成功,则生成 Ex12_01Dll.dll
及 DLL 的引入库 Ex12_01Dll.lib 文件。
工程 2:使用动态链接库
(1)新建一基于对话框的可执行程序项目 Ex12_01App
(2)添加控件
添加 3 个静态文本控件,3 个编辑控件和 3 个按钮控件。
(3)添加成员变量和消息函数(见表 12 3)

表12 3 成员变量和消息函数

控 件 标题(Caption) ID 号 成员变量 变量类型 消息函数


编辑框(x) IDC_EDIT m_x double
编辑框(y) IDC EDIT2 m_y double
编辑框(结果) IDC_EDIT3 m_result double
下压按钮 平方和 IDC_ADD OnAdd()
下压按钮 平方差 IDC_MINUS OnMinus()

266 (4)编写消息函数(见清单 12 3)
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

12_
清单12 3 消息函数(在 Ex 01AppD
lg.
cpp文件中)


oidCEx12_01AppD lgOnAdd() //平方和
{ UpdateData(true);
m_result = MyAdd(m_x,m_y);
UpdateData(false);


oidCEx 12_01AppD lgOnMi nus()//平方差
{ UpdateData(true);
m_result = MyMinus(m_x,m_y);
UpdateData(false);

(5)导入动态链接库函数的说明(见清单 12 4)

12_
清单12 4 导入动态链接库库函数 (在 Ex 01AppD
l h文件中)
g.

//在 Ex12_01AppDlg.h 文件的开头添加


extern" C" __declspec(dllimport)double MyMinus(double,double);
extern" C" __declspec(dllimport)double MyAdd(double,double);

(6)编译设置
选择 Project - > Settings,在弹出的
对话框 Project Settings 对话框 中 选 择
标签 link,在 Object/library modues 中
输入 Ex12_01Dll.lib,见图12 3。
(7)拷贝文件
将 Ex12_ 01Dll.dll、Ex12_ 01Dll.lib
文件拷贝到系统目录 (windows\system 或
windows\system32)下,也可以选择其他目
录,如使用该动态链接库的 Ex12_01App 工
图12 3 Pr
oje
ctS
ett
ins对话框

程目录下 (建议在做习题时选择此方法)。
(8)编译、链接和运行 Ex12_01App.exe
编译、链接 EX12_01App 工程生成 Ex12_01App.exe 文件,运行 Ex12_01App.exe,即可进行
平方和及平方差的计算。

12
42 编程实例二———采用.
DEF导出、显式链接
【例12 2】 建立一动态链接到 MFC 的常规 DLL,采用.DEF 导出,显式链接方式。 具体
功能与例 12 1 相同。
工程 1:创建动态链接库
(1)创建动态链接库工程项目 Ex12_02Dll
创建 Regular DLL using shared MFC 动态链接库。
(2)编写动态链接库中的函数(见清单 12 5)

267

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

12_
清单12 5 动态链接库库函数(在 Ex 02D
ll.
cpp文件中)

CEx12_02DllApp theApp;
//以下为添加的程序
doub
leMyAdd( do ub
lex do
ubl //平方和计算函数
ey)
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
return x*x+y*y;

do
ubl
eMyMi nus(doub
l ex
doubey)
l //平方差计算函数
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
return x*x-y*y;

(3)编写导出函数说明的.DEF 文件(见清单 12 6)

12_
清单12 6 导出函数说明(在 Ex 02D
ll.
def文件中)

; Ex12_02Dll def: Declares the module parameters for the DLL.


LIBRARY " Ex12_02Dll"
DESCRIPTION Ex12_02Dll Windows Dynamic Link Library
EXPORTS
;Explicit exports can go here
MyAdd @1
MyMinus @2

(4)编译、链接和生成
编译、链接 Ex12_02Dll 工程项目,生成 Ex12_02Dll.dll 和 Ex12_02Dll.lib 文件。
工程 2:使用动态链接库
(1)新建一基于对话框的可执行程序 Ex12_02App
(2)~(3)步同例 12 1 工程 2
(4)编写消息函数(见清单 12 7)

12_
清单12 7 消息函数(在 Ex 02AppD
lg.
cpp文件中)


oidCEx 12_02AppD lg OnAdd()//平方和
{ UpdateData(true);
//加载动态链接库
HINSTANCE hInstance = NULL;
hInstance = LoadLibrary("Ex12_02Dll.dll" );
if(hInstance = = NULL)
{ MessageBox("加载动态库失败!");
return;

//得到函数地址
typedef double(* MYADD)(double,double);
MYADD SquareAdd = NULL;
SquareAdd = (MYADD)GetProcAddress(hInstance," MyAdd" );
//计算平方和
m_result = SquareAdd (m_x,m_y);
UpdateData(false);
//卸载动态链接库
if(hInstance != NULL)
268
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

FreeLibrary(hInstance);


oidCEx 12_01AppD lg OnMinus()//平方差
{ UpdateData(true);
//加载动态链接库
HINSTANCE hInstance = NULL;
hInstance = LoadLibrary("Ex12_02Dll.dll" );
if(hInstance = = NULL)
{ MessageBox("加载动态库失败!");
return;

//得到函数地址
typedef double(* MYMINUS)(double,double);
MYMINUS SquareMinus = NULL;
SquareMinus = (MYMINUS)GetProcAddress(hInstance," MyMinus" );
//计算平方差
m_result = SquareMinus (m_x,m_y);
UpdateData(false);
//卸载动态链接库
if(hInstance != NULL)
FreeLibrary(hInstance);

(5)拷贝文件
将 Ex12_02Dll.dll 拷贝到 Ex12_02App 工程目录下。
(6)编译、链接和运行 Ex12_02App.exe
编译、链接 Ex12_02App 工程,生成 Ex12_02App.exe 文件;运行 Ex12_02App.exe,即可进
行平方和及平方差的计算。

12
43 编程实例三———扩展 MFC 的 DLL
【例12 3】 建立一扩展 MFC 的 DLL,采用 AFX_EXT_
CLASS 导出类,隐式链接方式。具体功能与例 12 1 相同。
工程 1:创建动态链接库 MFC AppWizard(dll)
(1)创建动态链接库工程项目 Ex12_03Dll
创建 MFC Extension DLL 动态链接库。
(2)新 建 编 写 动 态 平 方 和 及 平 方 差 的 计 算 函 数
类 CSquareCal
选择 Insert - > NewClass ...,新建 SquareCal 类,见图
12 4。
图12 4 新建类对话框
(3)在 CSquareCal 类导出与函数定义(见清单 12 8)
清单12 8 类导出与函数定义(在 Squ
areCa
l.h文件中)


lasAFX_
s EXT_ CLAS SCSqu
areCa
lpub
licCo
bje
ct//导出整个类
{ public:
CSquareCal();
269

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

virtual ~CSquareCal();
double MyAdd(double x,double y);//定义平方和计算函数
double MyMinus(double x,double y);//定义平方差计算函数
};

(4)编写动态链接库中的函数(见清单 12 9)

清单12 9 动态链接库库函数(在 Squ


areCa
l.pp文件中)

# include" stdafx.h"
# include" SquareCal.h"
# ifdef _DEBUG
# undef THIS_FILE
static char THIS_FILE= __FILE__;
# define new DEBUG_NEW
# endif
CSquareCal CSquareCal()//计算类构造函数
{ }
CSquareCal~CSqu a r
eCal()//计算类析构函数
{ }
doubleCSqu areCal MyAdd(d o
ubl
ex do
ubl //平方和计算函数
ey)
{ return x* x+ y* y;

do
ubl
eCSquareCal MyMi s(
nu d
oub
lex
doub
l //平方差计算函数
ey)
{ return x* x- y* y;

(5)编译、链接和生成
编译、链接项目,如果成功,则生成 Ex12_03Dll.dll 和 Ex12_03Dll.lib 文件。
工程 2:使用动态链接库
(1)新建一基于对话框的可执行程序 Ex12_03App
(2)~(3)同例 12 1
(4)编写消息函数(见清单 12 10)

12_
清单12 10 消息函数(在 Ex 03AppD
lg.
cpp文件中)


oidCEx12_03AppD lgOnAdd() //平方和计算
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyAdd(m_x,m_y);
UpdateData(false);


oidCEx12_03AppD lgOnMi nus()//平方差计算
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyMinus(m_x,m_y);
UpdateData(false);

270 (5)在 Ex12_03AppDlg.h 文件中添加包含语句


第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

# include" SquareCal.h"

(6)编译设置(方法同例 12 1)
选择 Project - > Settings,在 弹 出 的 Project Settings 对 话 框 中 选 择 标 签 link,在
Object/library modues 中输入 Ex12_03Dll.lib。
(7)拷贝文件
将 Ex12_03Dll.dll 和 Ex12_03Dll.lib 拷贝到 Ex12_03App 工程目录下。
(8)编译、链接和运行 Ex12_03App.exe
编译、链接 Ex12_03App 工程,生成 Ex12_03App.exe 文件,运行 Ex12_03App.exe,即可进
行平方和及平方差的计算。

12
44 编程实例四———动态链接库嵌套
【例12 4】 动态链接库的嵌套调用。 建立一扩展 MFC 的 DLL,采用 AFX_EXT_CLASS 导
出对话框类,隐式链接方式,具体说明见表 12 4。
表12 4 例12 4 功能说明

项目 图 示 和 说 明

程序
界面

图(
a) 可执行程序界面 图(
b) 动态链接库界面

本例创建两个工程:
工程 1:创建动态链接库 Ex12_04Dll,在动态库中实现动态库嵌套调用,实现两个数的平方和
及两个数的平方差计算,界面见图(b)
程序
工程 2:调用动态链接库 Ex12_04App,界面见图(a)。点击“调用动态库...”按钮,则显示右图
功能
的对话框,在对话框的 x 和 y 中输入数据,然后点击“平方和”或“平方差”按钮即可将
计算的结果显示在结果对编辑框中,按“确定”按钮,可将右边对话框中的结果导入左
边对话框的结果编辑框中

工程 1:创建动态链接库
(1)创建动态链接库工程 Ex12_04Dll
创建 MFC Extension DLL 动态链接库。
(2)添加对话框资源和创建对话框类
① 插入对话框资源 IDD_SQUARECALDLLDLG 到 Ex12_04Dll 工程中。
② 添加控件(按表 12 4 中图(b)所包含的控件添加)。
③ 为 IDD_SQUARECALDLLDLG 对话框创建 CSquareCalDllDlg 类,基类为 CDialog。
(3)平方和与平方差的实现
① 添加控件变量和消息函数(见例 12 1 表 12 3)。
② 编写消息函数(见清单 12 11)。 271

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单12 11 消息函数 (在 Squ


areCa
lDl
lDl
g.pp文件中)

# include" stdafx.h"
# include" resource.h"
# include" SquareCalDllDlg.h"
# include" SquareCal.h"//包含动态库计算类头文件

oi dCSqu areCalD l
lDlgOnAdd() //单击平方和按钮
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyAdd(m_x,m_y);
UpdateData(false);


oi dCSqu areCalD l
lDlgOnMi nus()//单击平方差按钮
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyMinus(m_x,m_y);
UpdateData(false);

③ 在 SquareCalDllDlg.cpp 文件中包含 SquareCal.h 文件(见清单 12 11)。


(4)在 SquareCalDllDlg.h 文件中添加资源定义和类导出的声明(见清单 12 12)
打开 Resource.h,将对话框和控件的资源定义拷贝到 SquareCalDllDlg.h 文件的开头。
使用 AFX_EXT_CLASS 导出 CSquareCalDllDlg 整个类。

清单12 12 资源定义和类导出的声明(在 Squ


areCa
lDl
lDl
g.h文件中)

//资源定义,下面的资源定义是从 Resource.h 文件中拷贝过来的


# define IDC_EDIT1 1000
# define IDC_EDIT2 1001
# define IDC_EDIT3 1002
# define IDC_MINUS 1003
# define IDC_ADD 1004
# define IDD_SQUARECALDLLDLG 11000
// 类声明
classAFX_ EXT_ CLAS SCSqu areCa
lDl
lDlgpubl
icCD
ial
og


(5)编译设置
选择 Project - > Settings,在 弹 出 的 Project Settings 对 话 框 中 选 择 标 签 link,在
Object/library modues 中输入 Ex12_03Dll.lib。
(6)拷贝文件
将 Ex12_03Dll.dll、
Ex12_03Dll.lib 和 SquareCal.h 文件拷贝到 Ex12_04Dll 工程目录下。
(7)编译、链接和生成
编译、链接 EX12_04Dll 工程项目,生成 Ex12_04Dll.dll 和 Ex12_04Dll.lib 文件。
工程 2:使用动态链接库
(1)新建一基于对话框的可执行程序 Ex12_04App
272 (2)动态链接库 Ex12_04Dll 的调用
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

① 添加控件、控件成员变量和消息函数(见表 12 5)。
表12 5 成员变量和消息函数

控 件 标题(Caption) ID 号 成员变量 变量类型 消息函数


静态文本 结果 IDC_STATIC
编辑框(x) IDC_EDIT1 m_result double
下压按钮 调用动态库 ... IDC_LOADDLL OnLoadDll()

② 编写消息函数(见清单 12 13)。
12_
清单12 13 消息函数(在 Ex 04AppD
l h文件中)
g.


#inc
lud
e"Squ areCalDl
lDlg. h"

oidCEx12_04AppD lgOnLo add
l //单击“调用库...”按钮
l()
{ CSquareCalDllDlg dlg;
if(dlg.DoModal()= = IDOK)
{ m_result = dlg.m_result;
UpdateData(false);

③ 在 Ex12_04AppDlg.cpp 文件中包含 SquareCalDllDlg.h 文件(见清单 12 13)。


(6)编译设置
选择 Project - > Settings,在 弹 出 的 对 话 框 Project Settings 中 选 择 标 签 link,在
Object/library modues 中输入 Ex12_04Dll.lib。
(7)文件拷贝
将 Ex12_03Dll.dll、Ex12_03Dll.lib、Ex12_04Dll.dll、Ex12_04Dll.lib 和 SquareCalDllDlg.h
文件拷贝到 Ex12_04App 工程目录下。
(8)编译、链接和运行 Ex12_04App.exe
编译、链接 Ex12_04App 工程项目,生成 Ex12_04App.exe 文件,运行该文件。

12
45 编程实例五———带数据库的动态链接库
【例12 5】 在上题中增加数据库,实现在动态链接库中使用数据库,具体见表 12 6。
表12 6 例12 5 功能说明

项目 图 示 和 说 明

程序
界面

图(
a) 可执行程序界面 图(
b) 带数据库的动态链接
273

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
项目 图 示 和 说 明

本例创建两个工程:
工程 1 为创建动态链接库 Ex12_04Dll,界面见图(b),有两个功能;功能一:在动态库中实现动
态库嵌套调用,实现两个数的平方和及两个数的平方差计算。功能二:动态库与数据库相结
程序 合,界面中 x、y 和结果编辑框可显示数据库中的数据
功能 工程 2 为调用动态链接库 Ex12_04App,界面见图(a),点击“调用动态库...”按钮,则显示右图
的对话框,在对话框的 x 和 y 中输入数据,然后点击平方和或平方差按钮即可将计算的结果
显示在结果对编辑框中,按确定按钮,可将右边对话框中的结果导入左边对话框的结果编辑
框中

第一部分:数据库的建立与注册
在 Access 平台下建立 Squaredb.mdb 数据库,创建 Squaredbtab 数据表,数据表的设计
见表 12 7。
表12 7 Squ
are
dbt
ab数据表
字段名 ID x y result
数据类型 int double double double

数据库建立完成后,注册数据库,注册数据库名为 Squaredb。
第二部分:VC++实现部分
工程 1:创建动态链接库
动态链接库与数据库采用 ODBC 链接方式。
(1)界面设计
修改上题动态链接库中的对话框资源,添加控件 (见表 12 8,修改后的界面见表 12 6
中的图(b))。
(2)添加单击 4 个下压按钮的消息函数(见表 12 8)
表12 8 各控件和成员变量

控件 标题(Caption) ID 号 消息函数
下压按钮 首记录 IDC_RECORD_FIRST OnRecordFirst()
下压按钮 上一条 IDC_RECORD_PRV OnRecordPre()
下压按钮 下一条 IDC_RECORD_NEXT OnRecordNext()
下压按钮 末记录 IDC_RECORD_LAST OnRecordLast()

(3)创建数据库记录集类
① 选择菜 单 Insert - > New Class ..., 弹 出 New Class 对 话 框 (见 图 12 5 ), 创 建
CSquaredbSet 类, 在 Name 编 辑 框 中 填 写 CSquaredbSet, 在 Base class 组 合 框 中 选 择
CRecordset,然后点击 OK。
② 在弹出的 Database Options 对话框(见图 12 6)中选择 ODBC 中的 Squaredb,然后点
击 OK。
③ 在弹出的 Select DataBase Tables 对话框(见图 12 7)中选择 Squaretab,然后点击
OK。这样就完成 CSquaredbSet 的建立。
274
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

图12 5 新建记录集类对话框

图12 6 数据库的选择 图12 7 数据库表的选择

(4)类导出的声明,定义记录集类的指针对象(见清单 12 14)
清单12 14 类导出的声明,定义记录集类的指针对象(在 Squ
areCa
lDl
lDl
g.h文件中)

# include" SquaredbSet.h"//包含记录集的头文件

lassAFX_ EXT_ CLAS SCSqu areCal
Dll
Dlgpub
licCD
ial
og
 ...
public:
CSquaredbSetm_ pSet//定义记录集类指针对象
...

(5)添加对话框初始化的函数、编写消息函数(见清单 12 15)
清单12 15 On
Ini
tDi
alg()(在 Squ
o areCa
lDl
lDl
g.pp文件中)

BOOLCSqu areCa lDl


lDlgOn In
itDial
o //初始化对话框
g()
{ CDialog::OnInitDialog();
m_pSet = new CSquaredbSet();
m_pSet ->Open();
275

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

m_pSet ->MoveFirst();
m_x = m_pSet ->m_x;
m_y = m_pSet ->m_y;
UpdateData(false);
return TRUE;//return TRUE unless you set the focus to a control


oidCSqua reCalDl
l D
lgOnAdd()
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyAdd(m_x,m_y);
UpdateData(false);


oidCSqua reCalDl
l D
lgOnMi nus()
{ UpdateData(true);
CSquareCal cal;
m_result = cal.MyMinus(m_x,m_y);
UpdateData(false);


oidCSqua reCalDl
l D
lgOnRe cordFi
rst()
{ m_pSet ->MoveFirst();
m_x = m_pSet ->m_x;
m_y = m_pSet ->m_y;
UpdateData(false);


oidCSqua reCalDl
l D
lgOnRe cordPre()
{ m_pSet ->MovePrev();
if(m_pSet ->IsBOF())
{ AfxMessageBox("已是首记录!");
m_pSet ->MoveFirst();

m_x = m_pSet ->m_x;
m_y = m_pSet ->m_y;
UpdateData(false);


oidCSqua reCalDl
l D
lgOnRe cordNext()
{ m_pSet ->MoveNext();
if(m_pSet ->IsEOF())
{ AfxMessageBox("已是末记录!");
m_pSet ->MoveLast();

m_x = m_pSet ->m_x;
m_y = m_pSet ->m_y;
UpdateData(false);


oidCSqua reCalDl
l D
lgOnRe cordLast()
{ m_pSet ->MoveLast();
m_x = m_pSet ->m_x;
m_y = m_pSet ->m_y;
UpdateData(false);

276
第十二章 动 态 链 接 库
……………………………………………………………………………………………………………………………………………

(6)编译、链接和生成
编译、链接 Ex12_04Dll 工程项目,生成 Ex12_04Dll.dll 和 Ex12_04Dll.lib 文件。
工程 2:使用动态链接库
与上题相同。但要注意将 SquareCalDllDlg.h 文件拷贝到 Ex12_04App 工程目录下后,
打开该 文 件 然 后 注 释 # include " SquaredbSet. h" 和 “CSquaredbSet * m _ pSet", 见 清 单
12 16。
清单12 16 注释#includ
e"Squa
redbS
e h" 和 CSqu
t. are
Setm_
pSe

(在 Squa
reCa
lDl
lDlg.h文件中)

//#include"Squ
aredbSe
t.h"//包含记录集的头文件
cl
assAFX_ EXT_CLAS SCSqu areCalDllDl
gpub
licCD
ial
og
 ...
public:
//CSquaredbSet * m_pSet;//定义记录集类指针对象
...

277
第十三章 Ac
tieX 控件

ActiveX 控件也称 OCX 控件,是一个直接插入到应用程序的软件模块,是 OLE (Object


Linking and Embedding)控件的扩展,它是为了适应 Internet 环境而对 OLE 控件作的扩充,
但这并不意味着 ActiveX 控件只能在 Internet 环境下使用。 其主要的作用是为其他程序
提供具有特定功能的组件,减少重复开发工作。
本章要点:
* ActiveX 控件的建立
* ActiveX 控件的使用

13
1 Ac
tiveX 的基本概念
为了方便快捷 地 在 同 时 运 行 的 多 个 应 用 程 序 中 交 换 信 息,Microsoft 发 展 了 OLE 与
ActiveX 技术。OLE 最初是对象的链接与嵌入(Object Linking and Embedding)技术,后来发
展为复合文档技术,不同类型的文件包括文字、图片、声音、动画和视频等可以共同存在于一
个文档中,它们可以由不同的应用程序产生,同时也可以在该文档中编辑。 ActiveX 技术来
源于 OLE 技术,ActiveX 技术主要是指在网络环境下基 于 组 件 对 象 模 型 (COM: Component
Object Model)的软件模块之间的通信与交互。OLE 和 ActiveX 的基本技术是 COM。

13
11 组件对象模型 COM
COM(Component Object Model)是 Windows 对象的一个二进制标准,是一种跨应用和语言
共享二进制代码的方法。COM 是一种协议,建立了一个软件模块同另一个软件模块之间的连
接,然后将其描述出来。 当这种连接建立起来后,两个软件模块之间就可以通过一个称为
“接口”的机制来进行通讯。
COM 对象也称为 COM 组件,它是 COM 类的特殊实例,它可以包含许多通过其接口来访问
的函数 (即 方 法 ), COM 对 象 至 少 必 须 拥 有 IUnknown 接 口。 CoClass (Component Object
Class)即 COM 类是包含在一个 DLL 或者一个 EXE 中,其代码隐藏在一个或多个接口背后,
CoClass 是接口的实现。
(1)接口(Interface)
接口就是两个不同对象间的一种连接方式。 计算机程序是通过一组函数进行连接的,
这组函数定义了程序中不同部分的接口 。 DLL 的接口就是它所输出的那些函数。 C++类的
接口就是该类的成员函数集。COM 中的接口是一组由组件实现的提供给客户使用的函数,这
些函数称为方法,用于控制应用程序对 COM 对象的访问。 接口名称带 “I ”字母前缀,例如
278 Iunknown、IShellLink 等等。C++中,接口类只包含纯虚函数,接口可以继承于其他接口,和
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

C++普通类的继承类似,但不允许多重继承。
GUID(Global Unqiue Identity Code,全局统一标识符 )是一个 128 位数字 (16 字节),它
作为 COM 语言 无 关 性 的 一 个 标 识,是 一 个 具 有 全 球 唯 一 性 的 标 识 符。 有 时 会 遇 到 UUID
(Universally Unique Identifier Code),作用和 GUID 是一样的。
在 Windows 平台上,GUID 应用非常广泛:类、接口标识、数据库甚至自动生成的机器名、
目录名等。Class ID(或者称 CLSID)是 CoClass 的 GUID;Interface ID(或者称 IID)是一个接
口的 GUID。
(2)IUnknown 接口
COM 技术规范规定,每个 COM 组件都必须支持 IUnknown 接口,为 COM 组件定义的每个接
口都必须是由 IUnknown 派生而来的。 IUnKnown 的定义在 WIN32 SDK 中的 UNKNOWN 头文件
中。IUnKnown 的定义见清单 13 1。
清单13 1 IUnKn
own的定义


nte
rfaceIUnKn own
 virtual HRESULT __stdcall QueryInterface(const IID& iid,void * * ppv)= 0;
virtual ULONG __stdcall AddRef()= 0;
virtual ULONG __stdcall Release()= 0;

IUnKnown 有三个方法(三个函数):
① AddRef()———告诉 COM 对象增加其引用计数,如果获取一个接口指针的副本,应该调
用这个方法增加引用记数;
② Release()———告诉 COM 对象减少其引用计数;
③ QueryInterface()———客户可以通过此函数来查询组件是否支持某个特定的接口 。
QueryInterface 函数返回一个指向组件支持的接口的指针 。 如果 QueryInterface 函数没
有找到组件支持的接口则返回的指针是 NULL。
QueryInterface()的原型是:
HRESULT IUnKnown::QueryInterface(REFIID iid, void* * ppv)
参数:iid,它是一个结构,接口标识符结构,iid(IID)标识了客户所需的接口,每一个接
口都有一个唯一的接口标识符,所以某个与 iid 相对应的接口绝对不会发生变化;
ppv,指向接口地址的指针。

13
12 对象链接与嵌入
对象的链接与嵌入是存储由另一个应用程序产生的 OLE 文档项目的两种方法。
(1)对象链接
如在 Word 中操作:菜单插入- > 对象- > 选定由文件创建选项卡- > 输入文件名 (VC 成绩
.xl)- > 选中复选框链接到文件,单击确定。整个文件将出现在 Word 文档中。
如果对磁盘上的文件“VC 成绩.xls”进行了修改,那么,其变化就会反映到 Word 文档中;
在 Word 文档中双击这个文件,将打开 Excel 来编辑 “VC 成绩.xls”。 如果在磁盘上删除了
“VC 成绩.xls”,那么 Word 文档仍然保持上次的结果,但是不能再编辑它。
链接并不动态地增加文档的大小,因为只有文件的位置和少量的描述信息需要保留在
文档中。 279

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(2)对象嵌入
如在 Word 中操作:菜单插入- > 对象- > 选定由文件创建选项卡- > 输入文件名 (VC 成绩
.xls)- > 不选中复选框链接到文件,单击“确定”。
整个文件将出现在 Word 文档中,那么与上述有什么区别呢? 当在 Word 文档中双击这
个对象时,Word 的菜单和工具栏消失并由 Excel 等同的菜单和工具栏替换,在这里修改不会
影响到“VC 成绩.xls”文件。
嵌入后的文档比其原始的文件要大得多,但是,如果出现空间不足的问题可以删除嵌入
的原始文件。

13
13 自动化服务器与自动化控制器
(1)容器和服务器
如果要将对象嵌入或链接到另一个之中,需要一个容器和一个服务器。 容器是嵌入或
链接对象的应用程序,如上述的 Word。
服务器是创建对象并且当对象被双击时,可以被启动的应用程序,如上述的 Excel。
(2)自动化服务器与自动化控制器
自动化技术(Automation)允许一个应用程序驱动另外一个应用程序 。驱动程序称为自
动化控制器(Automation Controller)或称自动化客户,另一个为自动化服务器 (Automation
Server)。一个自动化服务器至少包含一个,也可能多个 Idispatch 接口,其他应用程序 (驱
动程序)可以创建该接口或连接到该接口上 。

13
14 Ac
tiveX 控件
ActiveX 是在进程中装入的极小自动化服务器,ActiveX 由容器应用程序 (自动化控制
器)使用。ActiveX 控件的扩展名一般是 OCX。
ActiveX 控件是 32 位控件,等同于 OLE 控件。典型的控件由一个用户界面、一个单一的
IDispatch 接口和一个单一的 IConnectionPoint 接口组成,其中用户界面既表示设计时的
又表示运行时的,IDispatch 接口定义该控件的所有方法和属性,而 IConnectionPoint 接口
是用于该控件可以启动的事件的 。
ActiveX 控件通过事件告诉容器对该控件进行了操作 。 ActiveX 控件触发事件与它的
容器进行通信,容器也通过方法和属性与控件进行通信 。
ActiveX 控件的设计主要包括控件界面(外观)设计,控件的属性、方法和事件设计。
VC60 中创建 ActiveX 控件的方法一般有以下 3 种:
(1)使用 MFC ActiveXControlWizard
(2)使用 ActiveX 模板类库(ATL)
(3)使用 ActiveX 开发工具(BaseCtl Framework)
下面以 一 个 实 例 讲 述 利 用 MFC ActiveXControlWizard 开 发 ActiveX 控 件 的 方 法 和
步骤。

13
2 Ac
tiveX 控件编程实例
280 【例13 1】 开发 ActiveX 控件,具体说明见表 13 1。
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

表13 1 例13 1具体说明

项目 图 示 和 说 明

图(
a) 时钟控件界面

图(
b) 一般属性设置 图(
c) 是否显示数字时钟 图(
d) 颜色设置

图(
e) 字体设置 图(
f) 图片设置 图(
g) Al

(1)图(a)为 ActiveX 控件的界面,是一个时钟


(2)利用图(b)- (g)可以设置控件的属性

① 在图(c)中选中复选按钮“显示数字时钟”,在控件界面上将显示数字时钟

② 在图(d)中可以设置控件背景和前景的颜色

③ 在图(e)中可以设置数字时钟文本的字体

④ 图(f)中可以设置图片
⑤ 图(g)是属性的汇总

13
21 Ac
tiveX 控件框架的创建
利用 MFC ActiveXControlWizard 创建
ActiveX 控件的步骤如下:
(1)从菜单 “File”中选择 “New ”命令,
弹出“New”对话框,见图 13 1。
(2)切换到“ 选项卡,选择项目
Projects”
类型为“ MFC ActiveX ControlWizard”

(3)在“Project name”编辑框中键入名
字,如“Ex13_01ActiveX”。 图13 1 New 对话框

281

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(4)单击“OK”按钮,弹出图 13 2 所示的对话框。
(5)单击“Next”按钮,弹出图 13 3 所示的对话框,单击“Finish”即可。

图13 2 MFCAc
tiv
eXCo
ntr
olWi
zar
ds
tep1o
f2 图13 3 MFCAc
tiv
eXCo
ntr
olWi
zar
ds
tep2o
f2

13
22 控件的类
向导共产生 3 个类,12 个文件,其中 7 个实现文件,5 个头文件。3 个类分别为:控件模块
CEx13_01ActiveXApp 类、
控件 CEx13_01ActiveXCtrl 和属性页 CEx13_01ActiveXPropPage 类,文件
见表 13 2。
表13 2 AppWi
zar 13_
d为 Ex 01Ac
tieX 创建的文件

文 件 名 描 述 文 件 名 描 述
Ex13_01ActiveX.cpp 控件模块类源文件 Ex13_01ActiveX.h 控件模块类头文件
Ex13_01ActiveXCtl.cpp 控件类源文件 Ex13_01ActiveXCtl.h 控件类头文件
Ex13_01ActiveXPpg.cpp 属性页类源文件 Ex13_01ActiveXPropPage.h 属性页类头文件
Ex13_01ActiveX.rc 通用资源文件 Resource.h 资源头文件
Ex13_01ActiveX.def 模块定义文件 标准 应 用 程 序 结 构
StdAfx.h
Ex13_01ActiveX.odl 对象描述语言文件 头文件
标准 应 用 程 序 结 构
StdAfx.cpp
源程序文件

(1)控件模块类
控件 模 块 类 CEx13 _ 01ActiveXApp 类 从 COleControlModule 类 派 生, 而 不 是 常 见 的
CWinApp 类。在该类的源文件中实现了 InitInstance ()函数和 ExitInstance ()函数,在源
文件中有一个随机的数字串,称做控件标识。 该数字串唯一地表示了 “Ex13_ 01ActiveX ”
控件。
(2)控件类
控件类 CEx13_01ActiveXCtrl 类由 COleControl 类派生,代表了 “Ex13_01ActiveX”控件
对象,这是用户工作的重点,用户将在该类中完成了大部分的代码添加和修改工作 。
在 CEx13_01ActiveXCtrl 类中,除了必要的构造函数和析构函数外,另外还实现了其他
的几个函数:OnDraw()函数完成控件的自我绘制工作;DoPropExchange()函数实现控件的初
282 始化和串行化;OnResetState()函数重新设置控件的状态,即重新初始化;AboutBox()函数则
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

显示控件的版权信息对话框。
在 CEx13_01ActiveXCtrl 类中,更重要的是一些映射入口 (消息映射、调度映射、事件映
射)和属性页的设置。
(3)属性页类
属性页类 CEx13_01ActiveXPage 类由 COlePropertyPage 类派生,而 COlePropertyPage
类同时又是 CDialog 类的派生类,因此 COlePropertyPage 类的表现完全类似于一个对话框
类。用户将修改 COlePropertyPage 类的资源,并添加合适的成员变量以完成设置控件属性
的工作。

13
23 Ac
tiveX 控件的测试
在控件框架建 立 后,即 可 对 控 件 进 行 测 试,控 件 测 试 可 在 控 件 容 器 中 进 行,其 步 骤
如下:
(1)选择菜单 Tools - > ActiveX Control Test Container,显示图 13 4 所示的界面

图13 4 测试容器界面与插入控件对话框 图13 5 插入控件后的界面

(2)插入指定的 ActiveX 控件
选择菜单 Edit - > Insert New Control,弹出 Insert Control 对话框(见图 13 4),选中
CEx13_01ActiveX control,然后单击“OK”按钮,控件出现在测试界面中,见图 13 5。
图 13 5 可以看出,控件缺省的图形是一个椭圆,这一点阅读清单 13 2 的 OnDraw 函数
可见。
清单13 2 OnDr 13_
aw()函数 (在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oidCEx 13_
01Ac t
iveXCtrlOnDraw(CDC pd cc ons
tCRe ct&r cBound sconstCRe c
t&r cI
nva l
id)
{ pdc ->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
pdc ->Ellipse(rcBounds);

13
24 控件的外观设计
(1)定义时间成员变量和画时、分、秒钟的成员函数(见清单 13 3)
283

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

13_
清单13 3 定义有关成员变量和成员函数(在 Ex 01Ac
tiv
eXC
tr h文件中)
l.

# define PI 31415926536

lassCEx 13_01ActiveXCtrlpub l
icCO leControl
...
public:
CEx13_01ActiveXCtrl();
...
protected:
//声明画时、分、秒钟的成员函数
void MySec(CDC* pDC, double angle);//绘秒钟
void MyMin(CDC* pDC, double angle);//绘分钟
void MyHour(CDC* pDC, double angle);//绘时钟
//定义时间成员变量
double m_sec;//时间成员变量—秒
double m_min;//时间成员变量—分
double m_hour;//时间成员变量—时
char CurrentTime30;
long lTime;
long m_sethour;
long m_setmin;
long m_setsec;
BOOL m_IsTimer;
UINT m_timer;
~CEx13_01ActiveXCtrl();
...}

(2)在构造函数中初始化时间成员变量(见清单 13 4)

13_
清单13 4 构造函数(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)

13_
CEx 01Activ eXCt
r l
CEx13_ 01Acti
veXCtrl()//构造函数
{ InitializeIIDs(&IID_DEx13_01ActiveX, &IID_DEx13_01ActiveXEvents);
m_hour = 0;
m_min = 0;
m_sec = 0;
strcpy(CurrentTime," YYYY- MM- DD 00:00:00" ); //[0]= 0;
m_selectDispNumClock = TRUE;//见 1343
m_IsTimer = FALSE;
m_sethour = 25;
m_setmin = 0;
m_setsec = 0;
SetInitialSize(200, 200);//设定时钟初始大小

(3)编写画时、分、秒函数代码(见清单 13 5)

13_
清单13 5 画时、分、秒的函数(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oidCEx13_ 01Ac
tiveXC
trlMyHo
ur(
CDCpDCd
oub
lea
nge)
l //画时钟
{ CRect rect;
GetClientRect(&rect);
284 int r;
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

int x_center;
int y_center;
r = (rect.Width()> rect.Height())? (rect.Height()
/2- 20): (rect.Width()/2- 20);
x_center = rect.right/2;
y_center = rect.bottom/2;
pDC ->SetBkMode(TRANSPARENT);
pDC ->MoveTo(x_center, y_center);
pDC ->LineTo(x_center+ (r- 38)* sin(angle), y_center- (r- 38)* cos(angle));


oidCEx 13_01Ac t
iveXCtrlMyMi n(CDCpDCd o ub
lea n
gle)//画分钟
{ CRect rect;
GetClientRect(&rect);
int r;
int x_center;
int y_center;
r = (rect.Width()> rect.Height())? (rect.Height()
/2- 20): (rect.Width()/2- 20);
x_center = rect.right/2;
y_center = rect.bottom/2;
pDC ->SetBkMode(TRANSPARENT);
pDC ->MoveTo(x_center, y_center);
pDC ->LineTo(x_center+ (r- 30)* sin(angle), y_center- (r- 30)* cos(angle));


oidCEx 13_01Ac t
iveXCtrlMy Sc(
e CDCpDCd oubl
ea nl
ge)//画秒钟
{ CRect rect;
GetClientRect(&rect);
int r;
int x_center;
int y_center;
r = (rect.Width()> rect.Height())? (rect.Height()
/2- 20): (rect.Width()/2- 20);
x_center = rect.right/2;
y_center = rect.bottom/2;
pDC ->SetBkMode(TRANSPARENT);
pDC ->MoveTo(x_center, y_center);
pDC ->LineTo(x_center+ (r- 20)* sin(angle), y_center- (r- 20)* cos(angle));

(4)激活和撤消定时器
利用 ClassWizard 添加标准的 Windows 消息函数 OnCreate()函数和 OnDestroy()函数,
在 OnCreate()函数中用 SetTimer()函数激活定时器;在 OnDestroy()函数中添加 KillTimer
()函数撤消定时器(见清单 13 6)。

13_
清单13 6 激活和销毁定时器(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


ntCEx13_01Ac tiveXCtrlOnCr eae(
t LPCREATESTRUCTl pCr
eat
eSt
rut)

{ if (COleControl::OnCreate(lpCreateStruct)= = - 1)
return- 1;
m_timer = SetTimer(1,1000, NULL);//激活定时器
return 0;

285

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidCEx13_01ActiveXCtrl
OnDe s
tro //销毁定时器
y()
{ COleControl::OnDestroy();
KillTimer(m_timer);

(5)映射定时器标识的函数 OnTimer()(见清单 13 7)

清单13 7 OnT
ime 13_
r()函数(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oidCEx13_ 01Ac t
iveXC t
rlOnT imer(UINTn IDEvent)
{ struct tm* osTime;
CTime t = CTime::GetCurrentTime();
time(&lTime);
osTime = localtime(&lTime);
strcpy(CurrentTime, asctime(osTime));
CurrentTime24= ;
CurrentTime25= 0;
osTime = t.GetLocalTm(NULL);
m_hour = PI* (osTime ->tm_hour+osTime ->tm_min/600)
/60;
m_min = PI* (osTime ->tm_min+osTime ->tm_sec/600)/30;
m_sec = PI* (osTime ->tm_sec)/30;
if(osTime ->tm_sec % 30 = = 0)
FireIsTime();//见 13262
if(osTime ->tm_hour = = m_sethour&& osTime -> tm_min = = m_setmin&& osTime -> tm_sec = = m_
setsec)
FireIsTime();
m_IsTimer = TRUE;
InvalidateControl();
COleControl::OnTimer(nIDEvent);

(6)编写 OnDraw()函数画时钟,显示数字时钟(见清单 13 8)

清单13 8 OnDr 13_


aw()函数(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oidCEx13_ 01Ac t
iveXC t
rlOnDr aw( CDC pd cc ons
tCRe ct&r cBoundsconstCRe ct&r c
Inva
li d)
{ //设置,见 1342 和 1343
CBrush brush(TranslateColor(GetBackColor()));
pdc ->FillRect(rcBounds, &brush);
pdc ->SetTextColor(TranslateColor(GetForeColor()));
pdc ->SetBkMode(TRANSPARENT);
CFont* oldFont = SelectStockFont(pdc);
if(m_selectDispNumClock)
pdc ->ExtTextOut(rcBounds.left, rcBounds.top, ETO_CLIPPED, rcBounds, CurrentTime,
strlen(CurrentTime),NULL);
int r;
int x_center;
int y_center;
r =(rcBounds.Width(> rcBounds.Height(
) ))?(rcBounds.Height(
)/2- 20)
:(rcBounds.Height(
)/2- 20)

x_center = rcBounds.right/2;
y_center = rcBounds.bottom/2;
286
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

CPen pen(PS_SOLID, 2, RGB(0, 255, 255));


CPen* oldPen = pdc ->SelectObject(&pen);
//画时钟
for (int i = 0; i < 12; i+ + )
{ pdc->MoveTo(x_center+ (r-20)* sin(PI* i/60), y_center- (r- 20)* cos(PI* i/60));
pdc->LineTo(x_center+ (r-4)* sin(PI* i/60), y_center- (r- 4)* cos(PI* i/60));

pdc ->SelectObject(oldPen);
if(m_IsTimer)
{ MyHour(pdc, m_hour);
MyMin(pdc, m_min);
MySec(pdc, m_sec);

pdc ->SelectObject(oldFont);

(7)在构造函数中设定时钟初始大小(见清单 13 4)

13
25 设置属性
环境属性和时钟的高、宽已在构造函数中设置。 下面主要说明库存属性和自定义属性
的设置。

 定义库存属性

(1)添加库存属性
利用 ClassWizard 的 Automation 标签,如图 13 6 所示。 单击 “Add Property”按钮,弹
出如图 13 7 所示的对话框。
① 添加背景颜色 BkColor
在图 13 7 的 External name 中输入 BackColor,在 Implementation 中选择“Stock”。点
击“OK”。用同样的方法添加下面两项。

图13 6 Au
toma
tin标签
o 图13 7 添加库存属性

② 添加前景颜色 ForeColor
③ 添加字体 Font
(2)利用控件的属性页类来设置库存属性
在 Ex13_01ActiveXCtrl.cpp 文件中,定义容器所包含的页数(见清单 13 9)。 287

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

13_
清单13 9 属性页(在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)

BEGIN_PROPPAGEIDS(CEx13_01ActiveXCtrl, 4)
PROPPAGEID(CEx13_01ActiveXPropPage::guid)
PROPPAGEID(CLSID_CColorPropPage)//手工添加
PROPPAGEID(CLSID_CFontPropPage)//手工添加
PROPPAGEID(CLSID_CPicturePropPage)//手工添加
END_PROPPAGEIDS(CEx13_01ActiveXCtrl)

(3)响应属性,在 OnDraw()函数中实现(见清单 13 8)
字体:字体颜色应用于数字显示时钟;
背景颜色:用于时钟背景。

 自定义属性

用 Check Box 复选按钮选择是否显示数字时钟 。
(1)设计自定义属性资源(见图 13 8)

图13 8 添加自定义属性资源 图13 9 添加自定义属性

(2)定义自定义属性:BOOL selectDispNumClock
利用 Add Property 对话框来定义,见图 13 9。
(3)利用 ClassWizard 定义 IDC_CHK 控件变量
变量名 m_chk,变量类型 BOOL。
(4)设置默认的初始值,在构造函数中(见清单 13 10)

13_
清单13 10 构造函数(在 Ex 01Ac
tiv
eXPr
opPa
ge.
cpp文件中)

13_
CEx 01Ac tiveXPropPa
ge
CEx13_01Act
iveXPropPage()
:CO
lePr oe
p rtyPage(IDDIDS_EX13_01ACTIVEX_ PPG_ ION)
CAPT
{ //{{AFX_DATA_INIT(CEx13_01ActiveXPropPage)
m_chk = FALSE;
//AFX_DATA_INIT

(5)实现自定义属性(见清单 13 11)

13_
清单13 11 实现自定义属性 (在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oi 13_
dCEx 01ActiveXCtrl
OnS
ele
ctD
ispNumC
loc
kCh
ane
gd()
{ SetModifiedFlag();}
288
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

13
26 设置事件
事件包括库存事件和自定义事件 。

 添加库存事件

库存事件由 COleControl 类自动添加,COleControl 类包含常用行动( 如单击和双击控件、键盘
事件等)的驱动事件的预定义的成员函数。添加库存事件 Click,在 ClassWizard 的 ActiveX Events
标签中,单击“ 按钮(
Add Event” 见图 13 10)
,在 External name 组合框中选择“Click”

 自定义事件

自定义事件为当时钟达到某一时刻时发送标识 。
(1)定义自定义事件:事件名 IsTime(见图 13 11)

图13 10 添加库存事件 C
lic
k 图13 11 自定义事件I
sTime

(2)编写触发事件的代码
编写定时映射函数 OnTime(),见清单 13 7。在 OnTime()函数中调用 FireIstime()。

13
27 设置方法
方法包括库存方法和自定义方法 。

 定义库存方法

库存方法已经由 COleControl 实现,COleControl 支持 DoClick 和 Refresh 两个库存方
法。库存方法 DoClick 使用 Click 事件触发,选择 ClassWizard 的 Automation,单 击 Add
Method ...弹出 AddEvent 对话框,如图 13 12 所示定义库存方法。

289
图13 12 定义库存方法 图13 13 自定义方法

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

 自定义方法

(1)自定义方法
定义一个函数 SetTime(),能外部调用,定时触发 IsTime 事件,添加方法见图 13 13。
(2)实现参数设置
编写内部函数 SetTime()代码,见清单 13 12。
清单13 12 S
etT 13_
ime()函数 (在 Ex 01Ac
tiv
eXC
trl.
cpp文件中)


oidCEx13_01Ac t
iveXCtrl
Set
Time(
lon
gst_
e h
ourl
ongs
et_mi
nl
ongs
et_
sec)
{ m_sethour = set_hour;
m_setmin = set_min;
m_setsec = set_sec;

(3)自定义方法实现
在指定的时、分、秒 内 触 发 IsTime 事 件,完 善 OnTime ()函 数。 编 译 项 目 生 成 Ex13_
01ActiveX.ocx 文件,然后在 ActiveX 控件测试容器中测试。
完成上述测试后,点击菜单 Build - > Ex13_01ActiveX.ocx,系统自动编译项目生成Ex13_
01ActiveX.ocx 文件。

13
3 Ac
tiveX 控件的注册
ActiveX 控件必须注册后才能使用,可以采用下面两种方法注册。
方法一:将 ActiveX 控件的文件* .ocx 放入 windows/system 目录下。
方法二:菜单 Tools - > Register Control

13
4 Ac
tiveX 控件应用编程实例
下面主要讲述 ActiveX 控件的应用。
【例13 2】 使用 ActiveX 控件,具体说明见表 13 3。
表13 3 例13 2具体说明

程 序 界 面 程 序 功 能

图为使用 ActiveX 控件的界面,在时、分、秒中输入


数据(如在界面输入 22 时 30 分 37 秒),然后点击设置
报时按钮,到时间即可发出“嘟”一声报时

(1)创建一单文档的应用程序 Ex13_02ActiveXApp,选择视图类型 FormView


290 (2)在资源工具栏中添加刚创建好的 ActiveX 控件
第十三章 Act
iveX 控件
……………………………………………………………………………………………………………………………………………

图13 14 插入 Ac
tieX 控件到项目
v 13_
图13 15 插入 Ex 01Ac
tieX 控件到界面

选择菜单 Project - > Add to project - > Component and Controls,在弹出的 Components


and Controls Gallery 对话框中打开 Registered ActiveX Contorls 文件夹,选择刚注册好
的 ActiveX 控件,见图 13 14,单击 “Insert”按钮,即可将 Ex13_01ActiveX.ocx 控件插入到
Controls 中,并显示 OCX,像插入其他控件一样将控件拖到界面上,如图 13 15 所示。
(3)使用 ActiveX 控件的属性
使用控件的属性页设置本身的数字时钟 、颜色、字体等。 Ex13_01ActiveX 控件的属性设
置见表13 1中的图(b)~图(g)。
(4)使用 ActiveX 控件的事件
① 映 射 ActiveX 控 件 的 事 件 标 识 函 数, 在 视 图 类 中 用 ClassWizard 添 加
OnIsTimeActivex()函数,添加方法见图 13 16。

图13 16 添加 On
IsT
imeAc
tiv
ex()函数

② 编写 OnIsTimeActivex()函数(见清单 13 13)。
清单13 13 On
IsT
imeAc
tiv
e 13_
x()函数 (在 Ex 01Ac
tiv
eXApp.
cpp文件中)


oi 13_
dCEx 01ActiveXAppV i
ewOnI
sTimeAc
tiv
ex()
{ MessageBeep((WORD)- 1); //时间到发出一声“嘟”}

(5)使用 ActiveX 控件的方法 SetTime() 291



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

① 添加控件到界面。
添加 3 个静态文本控件、3 个编辑控件、1 个下压按钮和 1 个成组框。
② 添加成员变量和消息函数(见表 13 4)。
表13 4 各控件和成员变量

控 件 标 题 ID 号 成员变量 变量类型 消息函数


静态文本 设置时 IDC_STATIC
静态文本 设置分 IDC_STATIC
静态文本 设置秒 IDC_STATIC
编辑框(设置时) IDC_HOUR m_sethour Long
编辑框(设置分) IDC _MIN m_setmin Long
编辑框(设置秒) IDC_SEC m_setsec Long
下压按钮 设置报时 IDC_SET_CLOCK OnSetClock()
成组框 定时 IDC_STATIC
Ex13_01ActiveX IDC_ACTIVEX M_ctrlClock CEx13_01ActiveX

③ 编写单击设置报时的消息函数(见清单 13 14)。
清单13 14 OnS
etC
loc 13_
k()函数(在 Ex 01Ac
tiv
eXAppV
iew.
cpp文件中)


oidCEx13_01ActiveXAppV i
ew OnSetClock()
{ UpdateData();
m_ctrlClock.SetTime(m_sethour, m_setmin, m_setsec);//设置报时时间}

(6)编译和运行
运行 Ex13_01ActiveXApp.exe 程序,即可显示表 13 3 中所示的界面。

292
第十四章 多媒体技术

多媒体技术是综合图像、文字、
声音、动画和视频等多种媒体手段,用于传递和表达计算机
信息的技术。目前的计算机应用程序越来越多地将多媒体技术应用于应用程序设计之中。
开发多媒体应用程序,根据开发目的大致可分为三类:一是简单地调用媒体文件;二是
编辑处理媒体文件;三是控制多媒体硬件设备。本章主要讲述第一类,即调用媒体文件。
本章要点:
* 多媒体文件格式
* 多媒体播放器的制作

1 多媒体文件格式
14
多媒体文件中最具代表性的是音频文件和视频文件 。
音频文件主要格式有:波形音频文件 (.wav)、MIDI 音频文件 (.mid)、CD 音频和 MP3 文件
(.mp3)。
波形音频文件(.wav)是以数字化形式存储声音的文件,可以使用专用的工具软件编辑
序列化的波形文件,也可以使用过滤器修改。波形音频文件比较复杂,所占磁盘空间也比较
大,但几乎可以表达任何声音。
MIDI 音频文件即乐器数字接口,MIDI 文件实质上存储了一组指令,这些指令用于操作
微机的声音合成器(声卡的一部分),使用约定的乐器发出指定的声音。 因为 MIDI 文件中只
有一组指令,因此容易编辑,所占磁盘空间也较小。
CD 音频即通过计算机的 CD
ROM 驱动器来播放的音乐格式。 CD 音频代表高品质的数
字音频信息,但所需的存储空间大。
MP3 文件是一种压缩格式的声音文件,其结构复杂,所占磁盘空间较小,也可表达几乎所
有声音。
视频文件 的 文 件 格 式 也 有 多 种, 最 常 用 的 是 AVI (AudioVideo Interleaved )文 件
(.avi),它将视频信号以一系列的位图信息存储,文件中除了视频信号外还包括以波形声音
形式存储的数字化声音。

2 播放多媒体文件
14
在应用程序中播放多媒体文件时,必须在所有调用了多媒体函数的源文件中声明相应
的头文件。多媒体头文件包括:DIGITALV.H、MCIAVI.H、NMSYSTEM.H、MSACM.H、VCR.h、VFW.H。
此外,还需链接 MSACM32.LIB、WINMM.LIB 和 VFW32.LIB 等动态链接库。 在开发应用程序时, 293

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

可通过引入多媒体控件的方法在程序中自动添加所需要的声明 。
完成上述工作后,即可在程序中使用多媒体函数播放多媒体文件或操作多媒体设备 。
在程序中调用多媒体文件的主要方法有:
(1)使用 MCIWND 窗口类控控制波形文件、音频 CD 和视频等等。
(2)使用 MCI(Multimedia Control Interface,多媒体控制接口)。
(3)使用 API 函数。
此外,还可使用 PlaySound ()、SndPlaySound ()、MessageBeep ()等 函 数 控 制 播 放 音 频
文件。
在以上方法中,使用 API 函数需要程序员编写大量代码,所以比较简单有效的方法是使
用 MCIWND 窗口类或直接发送 MCI 命令。以下主要介绍 MCI 方法。

14 I控制方法
3 MC
14
3 I设备类型
1 MC
MCI(Multimedia Control Interface)是微软 Windows 定义的多媒体接口标准,提供了强
大的函数库来处理各种多媒体设备,而且由于 MCI 是一个可扩充库,所以如果出现了新的多
媒体设备,也能很方便地将其加入库中。表 14 1 列出了 MCI 所支持的设备类型。
表14 1 MCI支持的设备类型

设备类型 说 明 设备类型 说 明
animation 动画设备 scanner 图形扫描设备
caudio CD 音频设备 sequencer MIDI 设备
dat 数字音频设备 videodisc 影碟播放设备
digitalvideo 数字视频设备 vcr 录像机设备
other 未定义设备 waveaidio 波形音频设备
overlay 叠加视频设备

14
3 I函数接口
2 MC
MCI 为用户提供了控制不同多媒体设备的函数库,这些函数库允许用户以不同的方式访
问设备。表 14 2 列出了 MCI 主要函数接口。
表14 2 MCI主要函数接口

函数名称 说 明

原型:HANDLE mciGetCreatorTask(MCIEVICEID IDDevice)


功能:获得指定 MCI 设备的任务指针
mciGetCreatorTask
参数:IDDevice—任务所返回的设备 ID
返值:如果函数成功执行则返回所打开设备的任务指针,否则返回 NULL
原型:MCIDEVICEID micGetDeviceID (LPCTSTR lpszDevice)
功能:获得 MCI 设备的标识符
micGetDeviceID
参数:lpszDevice—指定设备名称或别名的字符串
294 返值:如果函数成功执行则返回指定设备的标识符,否则返回 0
第十四章 多 媒 体 技 术
……………………………………………………………………………………………………………………………………………

续 表
函数名称 说 明

原型:BOOL mciGetErrorString (DWORD fdwError, LPTSTR lpszErrorText, UNIT


cchErrorText)
功能:获得 MCI 错误信息
mciGetErrorString 参数:fdwError—mciSendCommand 或 micSendString 函数返回的错误码
lpszErrorText—描述错误码的字符串
cchErrorText—lpszErrorText 字符串的长度
返回值:如果函数成功执行则返回 TRUE,否则返回 FALSE

原型:YIELDPROC mciGetYieldPro(MCIDEVICEID IDDevice,LPDWORD lpdwYieldData)


功能:获得回调函数地址
mciGetYieldProc 参数:IDDevice—执行 MCI 命令的设备标识符
lpdwYieldData—将传给回调函数的输出数据缓冲区
返回值:如果函数成功执行则返回回调函数的地址,否则返回 NULL

原型:MCIERROR mciSendCommand (MCIDEVICEID IDDevice, UNIT uMsg, DWORD


fdwCommand,DWORD dwParam);
功能:向指定 MCI 设备发送命令消息
参数:IDDevice—接收 MCI 命令的设备标识符
mciSendCommand
uMsg—命令消息
fdwCommand—MCI 命令消息的标志位
dwParam—指向 MCI 命令消息数据结构的指针
返回值:如果函数成功执行则返回 0,否则返回非 0 值

原 型: MCIERROR micSendString (LPCTSTR lpszCommand, LPSTR lpszReturnString,


UINT cchReturn,HANDLE hwndCallback)
功能:向指定 MCI 设备发送命令字符串
参数:lpszCommand—包含 MCI 命令的字符集
mciSendString
lpszReturnString—接收返回信息的字符串
cchReturn—接收返回信息的字符串大小
hwndCallback—回调窗口句柄,该参数当指定了 notify 标志时有效
返回值:如果函数成功执行则返回 0,否则返回非 0 值

原型:UNIT mciSeYieldProc (MCIDEVICEID IDDevice, YIELDPROC yp, DWORD


dwYieldData)
功能:设置回调函数的地址
mciSetYieldProc 参数:IDDevice—回调函数作用的 MCI 设备标识符
yp—回调函数的地址
dwYieldData—传送给回调函数的数据
返回值:如果函数成功执行则返回 0,否则返回非 0 值

14
33 常用的 MC
I命令消息
常用的 MCI 命令消息,见表 14 3。
表14 3 常用的 MCI命令消息

命令消息 说 明 命令消息 说 明
MCI_OPEN 打开 MCI_STATUS 获取设备信息
MCI_SET 设置设备信息 MCI_PLAY 播放媒体文件 295

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

4 多媒体文件调用编程实例
14
本节以一个实例来说明多媒体文件的调用 。
【例 14 1】 Ex14 _ 1 设 计 一 个 简 易 的 Windows 媒 体 播 放 器。 利 用 动 画 控 件
ActiveMovie,实现媒体文件的播放,具体说明见表 14 4。
表14 4 例14 1具体说明

程序界面 程 序 功 能

(1)单击“OPEN”按钮,弹出打开文件对话框,选
择要打开的媒体文件。
(2)单击“PLAY”按钮,即可播放。
(3)单击“PAUSE”按钮,暂停播放;单击“PLAY”按
钮继续播放。
(4)单击“FULLSCREEN”按钮,全屏显示。
(5)单击“STOP”按钮,停止播放;单击“PLAY”按
钮从头开始播放

(1)新建一基于对话框的应用程序 Ex14_1
(2)添加控件
按照表 14 4 中的图添加控件,控件 ID 号等见表 14 5。
表14 5 控件ID 号、添加成员变量和消息处理函数

控 件 标 题 控件 ID 号 成员变量 消 息 消息函数名
按钮 OPEN IDC_OPEN BN_CLICKED OnPlay()
按钮 PLAY IDC_PLAY BN_CLICKED OnOpen()
按钮 PAUSE IDC_PAUSE BN_CLICKED OnPause()
按钮 STOP IDC_STOP BN_CLICKED OnStop()
按钮 FULLSCREEN IDC_FULLSCREEN BN_CLICKED OnFullScreen()
按钮 EXIT IDC_CANCEL
ActiveMovie m_ctrlActiveMovie

ActiveMovie 控件的插入方法如下:
在菜单中依次选择“Project/Add To Project/Components And Controls”菜单项,在出现
的“Components And Controls Gallery ”对话框中打开 “Registered Active Controls”文件
夹,选 中 “ActiveMovie Control Object ”选 项, 单 击 “Insert ”按 钮 后 关 闭 该 对 话 框,
ActiveMovie 控件便出现在控件面板中,调整好控件在对话框中的位置。为了能够控制控件
的操作,打开 ClassWizard,点击标签 Member Variable 选择 IDC_ACTIVEMOVIECONCROLl,然后
单击“Add Variable ...”按钮为它添加变量 m_ActiveMovie。
(3)编写消息函数(见清单 14 1)
296
第十四章 多 媒 体 技 术
……………………………………………………………………………………………………………………………………………

14_
清单14 1 消息函数 (在 Ex 1Dl
g.pp文件中)


oidCEx 14_1D l
g OnOp en() //显示“打开”对话框,选择要播放的文件
{ char szFileFilter=
" Mp3 File(*.mp3)|*.mp3|"
" Wma File(*.wma)|*.wma|"
" Video File(*.dat)|*.dat|"
" Wave File(*.wav)|*.wav|"
" AVI File(*.avi)|*.avi|"
" Movie File(*.mov)|*.mov|"
" Media File(*.mmm)|*.mmm|"
" Mid File(*.mid;* ,rmi)|*.mid;*.rmi|"
" MPEG File(*.mpeg)|*.mpeg|"
" All File(*.*)|*.* ||";
CFileDialog dlg(TRUE,NULL,NULL,OFN_HIDEREADONLY,szFileFilter);
if(dlg.DoModal()= = IDOK)
{ CString PathName = dlg.GetPathName();
PathName.MakeUpper();
m_ActiveMovie.SetFileName(PathName);



oidCEx 14_1D l
g OnPlay() //播放文件
{ m_ActiveMovie.Run(); //播放文件
SetTimer(0,20,NULL); //设置定时器


oidCEx 14_1D l
g OnPa use()//暂停播放
{ m_ActiveMovie.Pause();


oidCEx 14_1D l
g OnStop() //停止播放
{ m_ActiveMovie.Stop();//停止播放文件
KillTimer(0); //关掉定时器


oidCEx 14_1D l
g OnFullscreen()//全屏显示
{ m_ActiveMovie.Pause (); //暂停播放
m_ActiveMovie.SetFullScreenMode(true); //设置满屏模式
m_ActiveMovie.SetMovieWindowSize(SW_SHOWMAXIMIZED); //设置窗口为最大
m_ActiveMovie. Run(); //继续播放

(4)编译和运行

297
第十五章 多进程与多线程编程

当前流行的 Windows 操作系统能同时运行几个程序 (独立运行的程序又称之为进程 ),


对于同一个程序,它又可以分成若干个独立的执行流,称之为线程,线程提供了多任务处理
的能力。用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程概念的出现,
对提高软件的并行性有着重要的意义 。
本章要点:
* 进程与线程的概念
* 进程的创建、互斥与结束
* 线程管理(线程的创建与结束、线程的调度与优先级、线程间的通信)
* 线程同步

1 多进程编程
15
15
11 进程
进程(Process)是当前操作系统下一个被加载到内存的 、正在运行的应用程序实例。 每
一个进程都是由内核对象和地址空间所组成的,内核对象(内核对象在系统中相当于一种数
据结构,里面包含维护该对象的各种信息)可以让系统在其内存放有关进程的统计信息并使
系统能够以此来管理进程,而地址空间则包括了所有程序模块的代码和数据以及线程堆栈 、
堆分配空间等动态分配的空间。进程仅仅是一个存在,是不能独自完成任何操作的,必须拥
有至少一个在其环境下运行的线程,并由其负责执行在进程地址空间内的代码。 在进程启
动的同时即启动了一个线程,该线程被称做主线程或是执行线程,由此线程可以继续创建子
线程。如果主线程退出,那么进程也就没有存在的可能了,系统将自动撤消该进程并完成对
其地址空间的释放。
加载到进程地址空间的每一个可执行文件或动态链接库文件的映像都会被分配一个与之
相关联的全局唯一的实例句柄( 。该实例句柄实际是一个记录有进程加载位置的基
Hinstance)
本内存地址。 进 程 的 实 例 句 柄 在 程 序 入 口 函 数 WinMain ()中 通 过 第 一 个 参 数 HINSTANCE
hinstExe 传递,其实际值即为进程所使用的基本地址空间的地址。对于VC++链接程序所链接
产生的程序,其默认的基本地址空间地址为 0x00400000,如果没有必要一般不要修改该值。在
程序中,可以通过 GetModuleHandle(
)函数得到指定模块所使用的基本地址空间。

15
12 创建进程
298 (1)子进程的创建与 CreateProcess()函数
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

进程的创建通过 CreateProcess()函数来 实 现,CreateProcess()通 过 创 建 一 个 新 的


进程及在其地 址 空 间 内 运 行 的 主 线 程 来 启 动 并 运 行 一 个 新 的 程 序 。 具 体 的,在 执 行
CreateProcess()函数时,首 先 由 操 作 系 统 负 责 创 建 一 个 进 程 内 核 对 象,初 始 化 计 数 为
1,并立即为新进程创建一块虚拟地址空间 。 随后将可执行文件或其他任何必要的动态
链接库文件的代码和数据装 载 到 该 地 址 空 间 中 。 在 创 建 主 线 程 时,也 是 首 先 由 系 统 负
责创建一个线 程 内 核 对 象,并 初 始 化 为 1。 最 后 启 动 主 线 程 并 执 行 进 程 的 入 口 函 数
WinMain(), 完 成 对 进 程 和 执 行 线 程 的 创 建 。 CreateProcess ()函 数 的 原 型 , 见 清
单 15 1 。
清单15 1 Cr
eat
ePr
oce
ss()函数的原型

BOOL CreateProcess(
LPCTSTR lpApplicationName, //可执行模块名
LPTSTR lpComma ndLine, //命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程的安全属性
BOOL bInheritHandles, //句柄继承标志
DWORD dwCreationFlags, //创建标志
LPVOID lpEnvironment, //指向新的环境块的指针
LPCTSTR lpCurrentDirectory, //指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, //指向启动信息结构的指针
LPPROCESS_INFORMATION lpProcessInformation //指向进程信息结构的指针
);

在进程中通过 CreateProcess()函数去创建一个子进程,子进程在全部处理过程中只对
父进程地址空间中的相关数据进行访问,从而可以保护父进程地址空间中与当前子进程执
行任务无关的全部数据。子进程可以看成是父进程在运行期间的一个过程 。
父进程来掌握子进程的活动包括启动 、执行和退出。首先通过 CreateProcess()创建子
进程,子进程启动后父进程通过 WaitForSingleObject ()函数等待其执行的结束,在子进程
没有退出前父进程一直处于阻塞状态,一旦子进程退出,WaitForSingleObject ()函数所等
待的 pi. hProcess 对 象 将 得 到 通 知, 父 进 程 将 得 以 继 续, 如 有 必 要 可 以 通 过
GetExitCodeProcess()来获取子进程的退出代码。具体编程见清单 15 3。
(2)进程的互斥运行
正常情况下,一个进程的运行一般是不会影响到其他正在运行的进程的 。 但是对于某
些有特殊要求的如以独占方式使用串行口等硬件设备的程序就需要在其进程运行期间不允
许其他试图使用此端口设备的程序运行,同时此类程序通常也不允许运行同一个程序的多
个实例。这就引出了进程互斥的问题。
实现进程互斥的核心思想比较简单:进程在启动时首先检查当前系统是否已经存在
此进程的实例,如果没有,进程将成功创建并设置标识实例已经存在的标记 。 此后再创建
进程时将会通过该标记知晓实例已经存在,从而保证进程在系统中只存在一个实例 。 具
体可以采取内 存 映 射 文 件 、有 名 事 件 量 、有 名 互 斥 量 以 及 全 局 共 享 变 量 等 多 种 方 法 来
实现 。
CreateMutex()函 数 可 用 来 创 建 一 个 有 名 或 无 名 的 互 斥 量 对 象, 其 函 数 原 型 见 清
单15 2。 299

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单15 2 Cr
eat
eMu
tex()函数的原型

HANDLECreat
eMu
tex( LPSECURITY_ATTRIBUTES lpMutexAttributes,//指向安全属性的指针
BOOL bInitialOwner, //初始化互斥对象的所有者
LPCTSTR lpName //指向互斥对象名的指针
LPSECURITY_ATTRIBUTES lpMutexAttributes,//指向安全属性的指针
);

如果函数成功执行,将返回一个互斥量对象的句柄。如果在 CreateMutex()执行前已经
存在 有 相 同 名 字 的 互 斥 量, 函 数 将 返 回 这 个 已 经 存 在 互 斥 量 的 句 柄, 并 且 可 以 通 过
GetLastError()得 到 错 误 代 码 ERROR _ ALREADY _ EXIST。 可 见, 通 过 对 错 误 代 码 ERROR _
ALREADY_EXIST 的检测可以实现 CreateMutex()对进程的互斥。
使用全局 共 享 变 量 的 方 法 则 主 要 是 在 MFC 框 架 程 序 中 通 过 编 译 器 来 实 现 的。 通 过
# pragma data_seg预编译指令创建一个新节,在此节中可用 volatile 关键字定义一个变量,而
且必须对其进行初始化。volatile 关键字指定了变量可以为外部进程访问。最后,为了使该
变量能够在进程互斥过程中发挥作用,还要将其设置为共享变量,同时允许其具有读、写访问
权限。这可以通过# pragma comment 预编译指令来通知编译器,具体见清单 15 7。

15
13 结束进程
进程只是提供了一段地址空间和内核对象,其运行是通过在其地址空间内的主线程来
实现的。当主线程的进入点函数返回时,进程也就随之结束。 这种进程的终止方式是进程
的正常退出,进程中的所有线程资源都能够得到正确的清除。 除了这种进程的正常退出方
式外,有时还需要在程序中通过代码来强制结束本进程或其他进程的运行,如使用下面的两
个函数。
(1)使用 ExitProcess()函数结束进程
函数原型为:void ExitProcess(UINT uExitCode);
参数 uExitCode 为进程设置了退出代码。 该函数具有强制性,在执行完毕后进程即已
经被结束,因此位于其后的任何代码将不能被执行 。虽然 ExitProcess()函数可以在结束进
程的同 时 通 知 与 其 相 关 联 的 动 态 链 接 库, 但 是 由 于 它 的 这 种 执 行 的 强 制 性, 使 得
ExitProcess()函数在使用上存在安全隐患。 例如,如果在程序调用 ExitProcess ()函数之
前曾用 new 操作符申请过一段内存,那么将会由于 ExitProcess()函数的强制性而无法通过
delete 操作符将其释放,从而造成内存泄漏。 由于 ExitProcess ()函数的强制性和不安全
性,在使用时一定要引起注意。
(2)使用 TerminateProcess()函数结束进程
函数原型为:BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);
参数 hProcess 和 uExitCode 分别为进程句柄和退出代码。 如果被结束的是本进程,可
以通过 GetCurrentProcess()获取到句柄。
ExitProcess()只能强制执行本进程的退出,如果要在一个进程中强制结束其他的进程
就要用 TerminateProcess()来实现。与 ExitProcess()不同,TerminateProcess()函数执行
后,被终止的进程是不会得到任何关于程序退出的通知的 。 也就是说,被终止的进程是无法
在结束运行前进行退出前的收尾工作的。 所以,通常只有在其他任何方法都无法迫使进程
300 退出时才会考虑使用 TerminateProcess()去强制结束进程。
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

TerminateProcess (
)是异步执行的,在调用返回后并不能确定被终止进程是否已经真的退
出,如果调用 TerminateProcess (
)的进程对此细节关心,可以通过 WaitForSingleObject()来等
待进程的真正结束。

15
14 多进程编程实例
【例15 1】 Ex15_1 多进程编程,具体说明见表 15 1。
表15 1 例15 1具体说明

项目 图 示 和 说 明

程序界面

图(
a) 程序主界面 图(
b) 查看当前进程对话框
启动程序 Ex15_1.exe 后,如果程序不关闭,不能启动 Ex15_1.exe 第二个进程,也就是进程
互斥
程序功能 (1)点击菜单:进程->创建子进程->调用记事本,将打开记事本程序
(2)点击菜单:进程->创建子进程->调用 Word,将打开 Word 程序
(3)点击菜单:进程->查看当前进程,将显示当前进程的对话框

(1)新建一单文档应用程序 Ex15_1
(2)编辑菜单与添加菜单消息函数(见表 15 2)
表15 2 菜单消息函数(添加到视类)

菜 单 名 ID 号 消息函数
打开记事本 ID_PROCESS_CREAT_NOTPAD OnProcessCreatNotpad()
打开计算器 ID_PROCESS_CREAT_CAL OnProcessCreatCal()
查看当前进程… ID_PROCESS_NOWPROCESS OnProcessNowprocess()

(3)打开记事本菜单消息函数(见清单 15 3)

15_
清单15 3 打开记事本菜单消息函数 (在 Ex 1Vi
ew.
cpp文件中)


oidCEx15_1V i
ew OnPr o
cessCreatNotpa //打开记事本
d()
{ CString sCommandLine;//临时变量,记事本程序的路径+记事本程序名
char cWindowsDirectoryMAX_PATH;//windows 路径
char cCommandLineMAX_PATH;//记事本程序的路径+记事本程序名
DWORD dwExitCode;
PROCESS_INFORMATION pi;
STARTUPINFO si =sizeof(si)};
GetWindowsDirectory(cWindowsDirectory, MAX_PATH);//得到 Windows 目录
//记事本程序的路径+记事本程序名 301

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

sCommandLine = CString(cWindowsDirectory)+ "\\NotePad.exe" ;


::strcpy(cCommandLine, sCommandLine);
//启动"记事本"作为子进程
BOOL ret = CreateProcess(NULL, cCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
if (ret)
{ CloseHandle(pi.hThread);//关闭子进程的主线程句柄
WaitForSingleObject(pi.hProcess, INFINITE);//等待子进程的退出
GetExitCodeProcess(pi.hProcess, &dwExitCode);//获取子进程的退出码
CloseHandle(pi.hProcess);//关闭子进程句柄

else
 AfxMessageBox ("打开记事本失败! \r\n 请确认在 windows 目录下有 NotePad.exe 文件。",
MB_OK|MB_ICONERROR);

(4)打开计算器菜单消息函数(见清单 15 4)

15_
清单15 4 打开计算器菜单消息函数 (在 Ex 1Vi
ew.
cpp文件中)


oidCEx 15_1V iew OnPr oc
essCreat
Ca l()//打开计算器
{... //前 7 行见清单 15 3
//计算器程序的路径+ 记事本程序名
sCommandLine = CString(cWindowsDirectory)+ "\\system32" + "\\NotePad.exe" ;
::strcpy(cCommandLine, sCommandLine);
//启动"计算器"作为子进程
BOOL ret = CreateProcess(NULL, cCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
if (ret)
{...见清单 15 3

else
 AfxMessageBox( "打开计算器失败! \r\n 请确认在 windows\system32 目录下有 Calc.exe 文件。 ",
MB_OK|MB_ICONERROR);

(5)查看当前进程
① 编辑查看当前进程对话框的资源 。
对话框 ID 号为:IDD_PROCESSVIEWDLG;
添加控件:添加 1 个列表框,其 ID 号为:IDC_LISTB_PROCESS
② 为对话框建类,类名:CProcessViewDlg。
③ 用 ClassWizard 添加 IDC_LISTB_PROCESS 的成员变量:CListBox m_ctrlProcessList。
④ 用 ClassWizard 添加初始化对话框函数(见清单 15 5)。
清单15 5 On
Ini
tDi
alg()函数 (在 Pr
o oce
ssV
iewD
lg.
cpp文件中)

BOOLCPr oce
ssViewDlgOn In
itDial
og()
{ CDialog::OnInitDialog();
m_ctrlListProcess.ResetContent();
//创建快照句柄
302
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

HANDLE hSnapshot;
hSnapshot = CreateToolhelp32Snapshot(TH32 CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe;
Process32First(hSnapshot, &pe);//先搜索系统中第一个进程的信息
//下面对系统中的所有进程进行枚举,并保存其信息
do
 //把进程对应的文件路径名填入列表框
int index = m_ctrlListProcess.AddString(pe.szExeFile);
//设置列表框中该项的 Data 相应的进程的 ID 号,利于以后终止该进程
m_ctrlListProcess.SetItemData(index, pe.th32ProcessID);
}while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot);//关闭快照句柄
return TRUE;//return TRUE unless you set the focus to a control

⑤ 在 ProcessViewDlg.cpp 文件中添加包含语句:# include" tlhelp32.h" 。


⑥ 编写调用查看当前进程的菜单消息函数(见清单 15 6)。
15_
清单15 6 查看当前进程菜单消息函数 (在 Ex 1Vi
ew.
cpp文件中)


oidCEx15_1View OnProcessNowp
roc
es //查看当前进程
s()
{ CProcessViewDlg dlg;
dlg.DoModal();

(6)进程互斥运行(见清单 15 7)

15_
清单15 7 进程互斥运行(在 Ex 1.pp文件中)

# pragma data_seg("Shared" )
int volatile g_lAppInstance = 0;
# pragma data_seg()
# pragma comment(linker,"/section:Shared,RWS" )
BOOLCEx 15_1App Init
Instance()
{ //下面两行被注释的代码是使用编译器来实现进程的互斥运行,本例不用此方法
//if(+ +g_nAppInstance> 1)
//return FALSE;//通知进程结束
HANDLE m_hMutex = CreateMutex(NULL, FALSE," Sample05" );//创建互斥量
//检查错误代码
if (GetLastError()= = ERROR_ALREADY_EXISTS)
{ //如果已有互斥量存在则释放句柄并复位互斥量
CloseHandle(m_hMutex);
m_hMutex = NULL;
return FALSE;//程序退出

AfxEnableControlContainer();

(7)编译和运行

303

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

2 多线程编程
15
线程的基本思想很简单,它是一个独立的执行流,是进程内部的一个独立的执行单元,
相当于一个子程序,它对应于VC++中的 CWinThread 类对象。 单独一个执行程序运行时,缺
省地包含一个主线程,主线程以函数地址的形式出现,提供程 序 的 启 动 点,如 main ()或
WinMain()函数等。当主线程终止时,进程也随之终止。 根据实际需要,应用程序可以分解
成许多独立执行的线程,每个线程并行的运行在同一进程中 。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统
资源。操作系统给每个线程分配不同的 CPU 时间片,在某一个时刻,CPU 只执行一个时间片
内的线程,多个时间片中的相应线程在 CPU 内轮流执行,由于每个时间片时间很短,所以对
用户来说,仿佛各个线程在计算机中是并行处理的。 操作系统是根据线程的优先级来安排
CPU 的时间,优先级高的线程优先运行,优先级低的线程则排队等待。
线程被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来
处理用户的输入并响应各种事件和消息,其实,应用程序的主执行线程 CWinAPP 对象就是一个
用户界面线程,当应用程序启动时自动创建和启动,同样它的终止也意味着该程序的结束,进
程终止。工作线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等,它和
用户界面线程的区别是它不用从 CWinThread 类派生来创建,对它来说最重要的是如何实现工
作线程任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版
本;一个进程中的所有线程共享它们父进程的变量,但同时每个线程可以拥有自己的变量。

15
21 线程的创建与结束
(1)线程的创建
创建线程有多种方法,具体创建函数见表 15 3。
(2)线程的结束
线程结束可以采用线程函数返回的方法(这种方法较好)
,这是确保所有线程资源被正确地清
除的唯一办法。如果线程能够返回,就可以确保下列事项的实现:在线程函数中创建的所有 C 对
象均将通过它们的撤消函数被正确地撤消。操作系统将正确地释放线程堆栈使用的内存。系统
将线程的退出代码设置为线程函数的返回值。系统将递减线程内核对象的使用计数。
线程的结束函数,见表 15 4。
表15 3 线程的创建函数

函数来源 函 数 原 型
//创建工作者线程的函数:
CWinTh read Af xBe i
gnTh r
e ad(
AFX_THREADPROC pfnThreadProc,//指向工作者线程函数的指针
LPVOID pParam,//待传递给线程函数的参数
MFC 提供的
int nPriority = THREAD_PRIORITY_NORMAL,
函数
UINT nStackSize = 0,//线程栈 大小的指针
DWORD dwCreateFlags = 0,//线程栈创建标志的指针
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL//线程栈安全属性的指针
);
304
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

续 表
函数来源 函 数 原 型

//创建用户界面线程的函数:
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,//指向 CruntimesClass 类的指针
MFC 提供的 int nPriority = THREAD_PRIORITY_NORMAL,
函数 UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpSecurityAttrs,//指向安全属性的指针,如为 NULL
//则将使用默认的安全属性
DWORD dwStackSize,//线程空间的实际大小
Win32 API
LPTHREAD_START_ROUTINE lpStartAddress,//指线程的地址
函数
LPVOID lpParameter,//新线程参数
DWORD dwCreationFlags,//线程的创建方式,为 0 时创建的线程将立即运行,
//为 CREAT_SUSPENDED 时创建的线程将立即被挂起
LPDWORD lpThreadId//指向的地址将用来存放系统分配给新线程的 ID 号
);

表15 4 线程的结束函数

函 数 来 源 函 数 原 型
MFC 提供的函数 void AfxEndThread(UINT nExitCode);//nExitCode 是待结束线程的退出码
void ExitThread(DWORD dwExitCode);//dwExitCode 是待结束线程的退出码
BOOL TerminateThread(
Win32 API 函数 HANDLE hThread,//待结束线程的句柄
DWORD dwExitCode//待结束线程的退出码
);

① 对于 MFC 的 AfxBeginThread()创建的线程,可以通过 AfxEndThread()函数来实现线


程的结束。
② ExitThread()函数只能用在由 CreateThread()或 AfxBeginThread()创建的线程中;
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。 但是,C
资源(如 C 类对象)将不被撤消。
③ TerminateThread()结束能力要比 ExitThread ()强大,TerminateThread ()能撤消任
何线程。线程的内核对象的使用计数也被递减 。TerminateThread 函数是异步运行的函数。
如果要确切地知道该线程已经终止运行,必须调用 WaitForSingleObject ()或者类似的函
数。当使用返回或调用 ExitThread()的方法撤消线程时,该线程的内存堆栈也被撤消。 但
是,如果使用 TerminateThread(),那么在拥有线程的进程终止运行之前,系统不撤消该线程
的堆栈。
(3)线程函数
一个工作者的线程函数可以是一个静态成员函数,或者是在类外面声明的一个函数 。
它的原型为:UINT ThreadProcFunc(LPVOID pParam) 305

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

参数 pParam 是一个 32 位的值,它的值等于传递给 AfxBeginThread()的 pParam 值 。 通


常 pParam 是一个数据结构的地址,该结构是应用程序定义的,包含了传递该工作者进程的
信息,这些信息是由创建该工作者线程的那个线程传递的 。 该参数也可以是一个标量值 、
一个句柄,甚至是指向一个 MFC 对象的指针 。 对两个或更多的线程使用同一个线程函数
完全是合法的,但是要注意全局和静态变量产生的重入问题 。 只要一个线程使用的变量
(和对象 )是在堆栈上创建的,就不会出现重入问题,因为每个线程都获得它自己的堆栈 。
(4)线程与线程的创建举例
见例 15 2 的第(3)~(4)步。

15
22 线程的调度和优先级
(1)Windows 对线程的调度管理流程
Windows 操作系统是一种抢先式多任务的操作系统 。 这种操作系统虽然可以同时执行
多个线程,但实际上在一个极短的时间片上一个 CPU 只对开启的多个线程中的一个线程进
行处理。调度程序是操作系统的一个组成部分,它决定哪一个线程在什么时候运行以及运
行多长时间,它的目标是尽可能高效率地在执行的线程中间分配 CPU 时间,以便造成一种假
象,好像所有的线程都是立即运行的 。
线程是由线程内核对象和线程栈组成。 线程内
核对象的一个作用即是供系统存放线程的统计信息 。
图 15 1 为 Windows 对线程管理的流程图。
Windows 每隔 20 毫秒左右就要检查当前存在的
所有线程内核对象,并从可调度的一个线程内核对象
中加载环境结构中的值到 CPU 的寄存器。 此内核对
象中的环境结构保存了在上次运行线程时的 CPU 寄
存器状态。CPU 只对当前的处理线程操作 20 毫秒,20
毫秒之后 Windows 将把当前 CPU 的寄存器状态再次
保存到线程的环境结构中,停止本线程的执行,并以 图15 1 Wi
ndows对线程的调度管理
同样方式继续对下一个可调度线程进行相同处理 。
Windows 系统只能调度那些挂起计数为 0 的线程对象,对于那些暂停记数大于 0 的线程
或正在等待某些事情发生的线程将不为其分配 CPU 时间。线程在刚创建时其暂停计数均初
始化为 1,也就是刚创建的线程不可调度。 之后线程将花一定的时间去完成初始化工作,并
在初始化完成后查看是否使用了 CREATE_SUSPENDED 标志。如果该标志存在,则挂起计数依
然为 1,线程处于不可调度的挂起状态。如果未使用该标志,那么 CreateThread()函数将会
把计数器数设置为 0,使线程处于可调度状态。
(2)挂起和继续执行线程
一个运行的线程可以用 CWinThread::SuspendThread ()来挂起,并可用 CWindowThread::
ResumeThread(
)来使它重新执行。 一个线程可以在它自身上调用 SuspendThread (),或者让
另一个线程来调用它的 SuspendThread()。然而一个被挂起的线程不能用 ResumeThread()
来唤醒自己;必须有别的线程为它调用 ResumeThread()。 一个被挂起的线程几乎不占用处
理器的时间,并且基本上不增加系统的开销。
306 Windows 对每个线程保持一个挂起计数,每调用一次 SuspendThread (),就给该计数加
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

1,而调用一次 ResumeThread()则给该计数减 1。只有当一个线程挂起计数为 0 时,它才被分


配处理器时间。
(3)使线程睡眠
一个线程可以通过调用 API 函数::Sleep ()使它自己睡眠。 一个睡眠的线程不占用处
理器时间,并且在指定的一段时间后自动地苏醒 。 下面的语句暂停当前线程 10 秒钟。::
Sleep(10000);
(4)线程优先级设置
每一个线程随时都可以被赋予一个从 0 到 31 的优先级,数字越大,表示优先级越高。线程
的 32 个优先级别可划分为五个优先级范围,通过线程优先级设置函数 SetThreadPriority()还
可以设置两个特殊的优先级别,具体见表 15 5。
表15 5 线程优先级范围

优先级范围 说 明 备 注
THREAD_PRIORITY_HIGHEST 线程在标准优先级上两级运行
THREAD_PRIORITY_ABOVE_NORMAL 线程在标准优先级上一级运行
THREAD_PRIORITY_NORMAL 线程在标准优先级类运行
THREAD_PRIORITY_BLOW_NORMAL 线程在标准优先级下一级运行
THREAD_PRIORITY_LOWEST 线程在标准优先级下两级运行
在 15 优先级上运行,如果进程优先级类
SetThreadPriority()函
THREAD_PRIORITY_TIME_CRIT_ICAL 别为 REALTIME_PRIORITY_CLASS 则在优
数设置
先级 31 上运行

在 1 优先级上运行,如果进程优先级类
SetThreadPriority()函
THREAD_PRIORITY_IDLE 别为 REALTIME_PRIORITY_CLASS 则在优
数设置
先级 16 上运行

相同的优先级范围在不同的进程优先级类别下将会有不同的优先级取值,具体的优先
级映射关系由操作系统来负责。
需要注意的是,在改变线程优先级时必须先将线程挂起,设置完毕后再将其恢复。 具体
的,可以在开始创建线程时将其挂起,在设置完优先级后再将其恢复,也可将正在运行的线
程先通过 SuspendThread()挂起,然后再更改优先级设置。
(5)线程优先级设置举例
见例 15 2 的第(5)步。

15
23 线程间通信
由于进程本身隐含地创建有一个主线程,因此在程序中显式创建了线程后,实际上是对
多线程的处理与应用。在主线程中新创建的子线程一般是用来完成某种特定的任务,在执
行过程中总是或多或少地需要同主线程通信。 实现线程间通信的方法有很多,常用的主要
是通过全局变量、自定义消息函数和事件对象等来实现 。
(1)使用全局变量
将全局变量作为线程监视的对象,并通过在主线程对此变量值的改变而实现对子线程
的控制。由于这里的全局变量需要在使用它的线程之外对其值进行改变,这就需要通过
volatile(volatile 意思是不稳定的,用它修饰的成员变量在每次被线程访问时,都强迫从 307

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

共享内存中重读该成员变量的值)关键字对此变量进行说明。
(2)利用自定义消息
全局变量在线程通信中的应用多用在主线程对子线程的控制上,而从子线程向主线
程的信息反馈则多采用自定义消息的方式来进行 。 这里对自定义的消息的使用同使用普
通自定义消息非常相似,只不过消息的发送是在子线程函数中进行的 。 该方法的主体是
自定义消息函数,应首先定义自定义消息函数并添加对应的消息函数响应代码对此变量
进行说明 。
在子线程函数需要向主线程发送消息的地方调用 PostMessage()或 SendMessage()消
息传递函数将消息发送给主线程即可。 由于消息发送函数是在线程中被调用,因此需要指
出接收窗口句柄,可通过线程参数将其传递给线程函数 。
(3)使用事件内核对象
利用事件内核对象对线程的通信要复杂些,主要通过对事件对象的监视来实现线程间
的通信。事件对象 由 CreateEvent ()函 数 创 建,具 有 两 种 存 在 状 态:置 位 与 复 位,分 别 由
SetEvent ()和 ResetEvent ()来 产 生。 事 件 的 位 置 将 通 过 WaitForSingleObject ()或
WaitForMultpleObjects()之类的通知等函数继续执行。
(4)线程间通信举例
见例 15 2 第(6)~(8)步。

15
24 多线程编程实例
【例15 2】 Ex15_ 2 线程 创 建、线 程 管 理 和 线 程 间 通 信 的 应 用 与 编 程,具 体 说 明 见
表15 6。
表15 6 例15 2具体说明

项目 图示和说明




面 图(
a) 主界面和线程创建菜单 图(
b) 线程管理菜单 图(
c) 线程间通信菜单

图(
d) 消息对话框 图(
e) 消息对话框

308
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

续 表
项目 图示和说明

序 图(
f) 消息对话框
图(
i) 消息对话框

图(
h) 使用自定义消息

图(
g) 消息对话框 图(
j) 消息对话框
(1)点击菜单:线程创建->使用 AfxBeginThread(),弹出图(d)
(2)点击菜单:线程创建->使用 CreateThread(),弹出图(e)
(3)点击菜单:线程管理->线程启动时设置优先级,弹出图(e)

(4)点击菜单:线程管理->线程运行起来后改变优先级,弹出图(e)

(5)点击菜单:线程间通信->使用全局变量->执行,弹出图(f)

(6)点击菜单:线程间通信->使用全局变量->停止,弹出图(g)

(7)点击菜单:线程间通信->使用自定义消息,弹出图(h)
(8)点击菜单:线程间通信->使用事件内核对象->执行,弹出图(i)
(9)点击菜单:线程间通信->使用事件内核对象->停止,弹出图(j)

(1)新建一单文档应用程序 Ex15_2
(2)编辑菜单与添加菜单消息函数(见表 15 7)
表15 7 菜单消息函数(添加到视类)

菜 单 名 ID 号 消 息 函 数
使用 AfxBeginThread() ID_USE_AFXBEGINTHREAD OnUseAfxbeginthread()
使用 CreateThread() ID_USE_CREATETHREAD OnUseCreatethread()
线程启动时设置优先级 ID_SET_START_PRIORITY OnSetStartPriority()
线程运行起来后改变优先级 ID_SET_AFTERRUN_PRIORITY OnSetAfterrunPriority()
使用全局变量->执行 ID_COMMUNICATE_GLOBAL_START OnCommunicateGlobalStart()
使用全局变量->停止 ID_COMMUNICATE_GLOBAL_END OnCommunicateGlobalEnd()
使用自定义消息 ID_COMMUNICATE_USE_MESSAGE OnCommunicateUseMessage()
使用事件内核对象->执行 ID_COMMUNICATE_EVENT_START OnCommunicateEventStart()
使用事件内核对象->停止 ID_COMMUNICATE_EVENT_END OnCommunicateEventEnd()

(3)编写线程函数 (见清单 15 8)

15_
清单15 8 线程函数 (在 Ex 2Vi
ew.
cpp文件中)

DWORD WINAP ITh readFunc1(LPVOIDpPa


r //线程函数
am)
{ AfxMessageBox("CreateThread()函数创建的线程!");//显示消息对话框
return 0;
 309

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

UINTTh re
a dFun
c2( LPVOIDpPa ram)//线程函数
{ AfxMessageBox("AfxBeginThread()函数创建的线程!");//显示消息对话框
return 0;

(4)创建线程
编写菜单消息函数(见清单 15 9)。

15_
清单15 9 创建线程,编写菜单消息函数 (在 Ex 2Vi
ew.
cpp文件中)


oidCEx 15_2V i
ew OnUs eAf
xbeginthread()//使用 Af xbe
ginthread()创建
{ //创建工作者线程,并将视指针作为参数传递给线程
CWinThread* pWinThread = AfxBeginThread(ThreadFunc1, this);


oidCEx 15_2V i
ew OnUs eCr
eat
et hr
ead() //使用 Cr ea
tet
hread()创建
{ HANDLE hThread = CreateThread(NULL,//无安全属性
0,//使用默认线程栈大小
ThreadFunc2,//线程函数地址
this,//将视指针作为参数传递给线程
0,//线程创建后立即执行
NULL //不关心线程 ID
);
CloseHandle(hThread);//释放线程句柄

(5)线程优先级设置
编写菜单消息函数(见清单 15 10)。

15_
清单15 10 线程优先级设置菜单消息函数 (在 Ex 2Vi
ew.
cpp文件中)


oidCEx15_2V i
ew OnS etStar
tPrior
ity() //线程启动时设置优先级
{ //创建线程
HANDLE hThread = CreateThread (NULL,//无安全属性
0,//使用默认线程栈大小
ThreadFunc2,//线程函数地址
this,//将视指针作为参数传递给线程
CREATE_SUSPENDED,//线程创建后立即挂起
NULL);//不关心线程 ID
SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST);//设置线程优先级
ResumeThread(hThread);//恢复线程执行
CloseHandle(hThread);//关闭句柄


oidCEx15_2V i
ew OnS etAfter
runPr iori
ty()//线程运行起来后改变优先级
{ //创建线程
HANDLE hThread = CreateThread (NULL,//无安全属性
0,//使用默认线程栈大小
ThreadFunc2,//线程函数地址
this,//将视指针作为参数传递给线程
0,//线程创建后立即执行
NULL);//不关心线程 ID
310
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

SuspendThread(hThread);//挂起线程
SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST);//设置线程优先级
ResumeThread(hThread);//恢复线程执行
CloseHandle(hThread);//关闭句柄

(6)线程间通信—使用全局变量
① 编写线程函数(见清单 15 11)
15_
清单15 11 线程间通信的线程函数 (在 Ex 2Vi
ew.
cpp文件中)

volatile bool g_bDo = false;//线程通信用全局变量


UINTTh rea dFunc3(LPVOIDpPa r //线程函数
am)
{ while (g_bDo)
{ Sleep(1000);
AfxMessageBox("使用全局变量,线程正在运行!");

AfxMessageBox("使用全局变量,线程终止进行!");
return 0;

② 编写菜单消息函数(见清单 15 12)
15_
清单15 12 使用全局变量进行线程间通信的菜单消息函数 (在 Ex 2Vi
ew.
cpp文件中)


oidCEx15_2V i
ew OnCommun icateGlobalSt
art()//使用全局变量 执行
{ g_bDo = true;//通过全局变量通知线程执行
AfxBeginThread(ThreadFunc3, NULL);//启动线程


oidCEx15_2V i
ew OnCommun icateGlobalEnd()//使用全局变量 停止
{ g_bDo = false;//通过全局变量通知线程结束

(7)线程间通信—利用自定义消息
① 编写线程函数(见清单 15 13)
② 自定义消息(见清单 15 13)
15_
清单15 13 利用自定义消息进行线程间通信的线程函数(在 Ex 2Vi
ew.
cpp文件中)

#def
ineWM_ USER_MSG WM_ USER+101//自定义消息
UINTThreadFun 4(
c LPVOIDpPa ram)//线程函数
{ Sleep(3000);//延迟 3 秒
::PostMessage((HWND)pParam, WM_USER_MSG, 0, 0);//向主线程发送自定义消息
return 0;

③ 在 Ex15_2View.h 文件中声明自定义消息函数 afx_msg void OnUseMessage (WPARAM


wParam, LPARAM lParam);
④ 在消息映射表中手工添加映射关系:ON_MESSAGE(WM_USER_MSG, OnUseMessage)
⑤ 编写自定义消息函数和菜单消息函数(见清单 15 14) 311

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单15 14 使用全局变量进行线程间通信的菜单消息函数
15_
(在 Ex 2Vi
ew.
cpp文件中)


oidCEx15_2View OnUs eMessage(WPARAM wPa ramLPARAMl
Par //自定义消息函数
am)
{ AfxMessageBox("使用自定义消息 线程已退出!");//报告消息


oidCEx15_2View OnCommun icat
eUseMe ss
ag //菜单消息函数
e()
{ HWND hWnd = GetSafeHwnd();//获取窗口句柄
AfxBeginThread(ThreadFunc4, hWnd);//启动线程

(8)线程间通信—使用事件内核对象
① 编写线程处理函数(见清单 15 15)
清单15 15 使用事件内核对象进行线程间通信的线程处理函数
15_
(在 Ex 2Vi
ew.
cpp文件中)

HANDLE hEvent = NULL;//事件句柄


UINTTh rea
dFun c 5(LPVOIDpPa ram)//线程处理函数
{ while(true)
{ //等待事件发生
DWORD dwRet = WaitForSingleObject(hEvent, 0);
//如果事件置位则退出线程,否则将继续执行
if (dwRet = = WAIT_OBJECT_0)
break;
else
 Sleep(3000);//延迟 3 秒
AfxMessageBox("使用事件内核对象,线程正在运行!");


AfxMessageBox("使用事件内核对象,线程终止运行!");
return 0;

在清单 15 15 中,使用 WaitForSingleObject()对 hEvent 进行监视,该函数原型为:

Wa
itFo
rSi
ngl
eOb
jet(HANDLE hHandle,
c //等待对象的句柄
DWORD dwMilliseconds//超时时间间隔
);

② 编写菜单消息函数(见清单 15 16)
清单15 16 使用事件内核对象进行线程间通信的菜单消息处理函数
15_
(在 Ex 2Vi
ew.
cpp文件中)


oidCEx15_2V i
ew OnCommun i
cateEventStart()//使用事件内核对象 执行
{ hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);//创建事件
AfxBeginThread(ThreadFunc5, NULL);//启动线程


oidCEx15_2V i
ew OnCommun i
cateEventEnd() //使用事件内核对象 停止
{ SetEvent(hEvent);//事件置位

312
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

3 线程同步
15
15
31 线程同步的概念
因为各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的问题
是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。 保证各
个线程可以在一起适当的协调工作称为线程之间的同步 。
前面一节介绍的事件对象实际上就是一种同步形式 。 Visual C++中使用同步类来解决
由操作系统的并行性而引起的数据不安全问题,MFC 支持的七个多线程的同步类可以分成两
大类:同步对象 (CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步访
问对象(CMultiLock 和 CSingleLock)。本节主要介绍临界区 (Critical Section)、事件内核
对象(Event)、互斥(Mutex)、信号量 (Semaphore),这些同步对象使各个线程协调工作,程序
运行起来更安全。

15
32 临界区
临界区是一段以独占方式对某些共享资源访问的代码,在任意时刻只允许一个线程对
共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他
所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开 。
线程在使用临界区时,一般不允许其运行时间过长。 只要进入临界区的线程没有离开,
其他所有试图进入此临界区的线程都会被挂起而进入到等待状态 。 这在一定程度上会影响
程序的运行性能。需要注意的是不要将等待用户输入或是其他一些需要外界干预的操作包
含到临界区中。如果线程进入了临界区却一直没有释放,同样也会引起其他线程的长时间
等待。
(1)CCriticalSection 类
MFC 为临界区提供有一个 CCriticalSection 类,使用该类进行线程同步处理是非常简
单的,只需在线程处理函数中用 CCriticalSection 类成员函数 Lock()和 UnLock()标定出被
保护代码片段即可。
(2)临界区应用举例
见例 15 3 第(3)步。

15
33 事件内核对象
在前面讲述线程通信时曾提过使用事件内核对象来进行线程间的通信,除此之外,事件
内核对象也可以通过通知操作的方式来保持线程的同步 。使用临界区只能同步同一进程中
的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件内核
对象的访问权。
(1)CEvent 类
MFC 为事件处理提供 了 一 个 CEvent 类,共 包 含 有 除 构 造 函 数 外 的 4 个 成 员 函 数,见
表15 7。
313

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表15 7 CEv
ent类成员函数

函数原型和参数说明 作 用

CEvent(BOOL bInitiallyOwn = FALSE,//


BOOL bManualReset = FALSE,// 构造函数履行了原 CreateEvent()函数创建事件
LPCTSTR lpszName = NULL,// 对象的 职 责。按 照 缺 省 设 置 将 创 建 一 个 自 动 复
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL// 位、初始状态为复位状态的没有名字的事件对象
);

将事件设置为可用,释放任意等待的线程后再将
BOOL PulseEvent()
其重置为不可用

将事件设置为不可用,直到显示地调用成员函数
BOOL ResetEvent()
SetEvent 时设置为可用
BOOL SetEvent() 将事件设置为可用,并释放任意等待的线程
由当前拥有自动事件的线程调用,且在使用后释
Virtual BOOL UnLock()
放此事件对象

图 15 2 说明了 CEvent 类对 A、B 两线


程的同步过程:
B 线 程 在 执 行 到 CEvent 类 成 员 函 数
Lock()时将会发生阻塞,而 A 线程此时则可
以在没有 B 线程干扰的情况下对共享资源进
图15 2 CEv
ent类对线程的同步过程示意
行处 理, 并 在 处 理 完 成 后 通 过 成 员 函 数
SetEvent()向 B 发出事件,使其被释放,得以对 A 先前已处理完毕的共享资源进行操作 。
(2)管理事件内核对象应用举例
见例 15 3 第(4)步。

15
34 互斥内核对象
互斥(Mutex)是一种用途非常广泛的内核对象 。 互斥与临界区很相似,但是使用时相对
复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同
步,从而实现资源的安全共享。
(1)CMutex 类
MFC 为互斥提供了一个 CMutex 类,互斥与 CMutex 类的对象相对应,使用 CMutex 类的方
法是使用互斥对象,在构造 CMutex 类对象的同时可以指明待查询的互斥对象的名字,在构
造函数返回后即可访问此互斥变量 。该对象的 Lock()函数用于占有互斥,Unlock()用于释
放互斥。CMutex 类构造函数原型为:
CMutex(BOOL bInitiallyOwn = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);
(2)互斥内核对象举例,见例 15 3 第(5)步。

15
35 信号量内核对象
314 信号量 (Semaphore)内核对象的用法和互斥的 用 法 很 相 似,不 同 的 是 它 可 以 同 一 时
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

刻允许多个线程 访 问 同 一 个 资 源,但 是 需 要 限 制 在 同 一 时 刻 访 问 此 资 源 的 最 大 线 程
数目 。
(1)CSemaphore 类
MFC 为互斥提供了一个 CSemaphore 类,信号量需要用 CSemaphore 类声明一个对象,一
旦创建了一个信号量对象,就可以用它来实现对资源的访问技术 。 为实现计数处理,先创建
一个 CSingleLock 或 CMltiLock 对象,然后用该对象的 Lock ()函数减少这个信号量的计数
值,Unlock()反之。
CSemaphore 类构造函数原型为:

CSemaphore (LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL,


LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);

构造函数可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全
属性等进行初始化。
(2)信号量内核对象举例,见例 15 3 第(6)步。

15
36 线程同步编程实例
【例15 3】 Ex15_3 线程同步编程,具体说明见表 15 8。
表15 8 例15 3具体说明

项目 图 示 和 说 明


图(
a) 程序主界面 图(
b) 消息对话框

图(
c) 消息对话框 图(
d) 消息对话框 图(
e) 消息对话框

点击菜单:线程同步->临界区,弹出图(b)

序 点击菜单:线程同步->事件内核对象,弹出图(b)
功 点击菜单:线程同步->互斥内核对象,弹出图(b)

点击菜单:线程同步->信号量内核对象,弹出图(c)和(d),关闭(c)或(d),将弹出(e)

(1)新建一单文档应用程序 Ex15_3 315



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(2)编辑菜单与添加菜单消息函数(见表 15 9)

表15 9 菜单消息函数(添加到视类)

菜 单 名 ID 号 消 息 函 数

临界区 ID_SYN_CRITICALSECTION OnSynCriticalsection()


事件内核对象 ID_SYN _EVENT OnSynEvent()
互斥内核对象 ID_SYN _MUTEX OnSynMutex()
信号量内核对象 ID_SYN _SEMAPHORE OnSynSemaphore()

(3)临界区
① 编写线程函数(见清单 15 17)

15_
清单15 17 采用临界区的线程处理函数(在 Ex 3Vi
ew.
cpp文件中)

char cArray10;//共享资源
CCriticalSection criticalSection;//MFC 临界区类对象
UINTTh readCri
ticalFun c1( LPVOIDp a
ram)//线程函数
{ criticalSection.Lock();//进入临界区
//对共享资源进行写入操作
for (int i = 0; i < 10; i+ + )
{ cArrayi= a ;
Sleep(1);

criticalSection.Unlock();//离开临界区
return 0;

UINTThreadCr i
ticalFun c2( LPVOIDp a
r //线程函数
am)
{ criticalSection.Lock();//进入临界区
//对共享资源进行写入操作
for (int i = 0; i < 10; i+ + )
{ cArray10 i 1= b ;
Sleep(1);

criticalSection.Unlock();//离开临界区
return 0;

② 编写临界区菜单消息函数(见清单 15 18)
15_
清单15 18 临界区菜单消息函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx15_3V i
ew OnS ynCriti
calse
ction()//临界区
{ //启动线程
AfxBeginThread(ThreadCriticalFunc1, NULL);
AfxBeginThread(ThreadCriticalFunc2, NULL);
Sleep(300);//等待计算完毕
//报告计算结果
CString sResult = CString(cArray);
AfxMessageBox(sResult);

316
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

(4)事件内核对象
① 编写线程函数(见清单 15 19)
15_
清单15 19 采用临界区的线程处理函数(在 Ex 3Vi
ew.
cpp文件中)

CEvent event;//事件内核对象
UINTTh re
adEv ent Func 1(LPVOIDp a
r //线程函数
am)
{ //对共享资源进行写入操作
for (int i = 0; i < 10; i+ + )
{ cArrayi= a ;
Sleep(1);

event.SetEvent();//设置事件置位
return 0;

UINTThreadEv ent Func 2(LPVOIDp a
r //线程函数
am)
{ event.Lock();//等待事件
//对共享资源进行写入操作
for (int i = 0; i < 10; i+ + )
{ cArray10 i 1= b ;
Sleep(1);

return 0;

② 临界区菜单消息函数编写(见清单 15 20)
15_
清单15 20 临界区菜单消息函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx15_3V i
ew OnS y nEven //事件内核对象
t()
{ //启动线程
AfxBeginThread(ThreadEventFunc1, NULL);
AfxBeginThread(ThreadEventFunc2, NULL);
Sleep(300);//等待计算完毕
//报告计算结果
CString sResult = CString(cArray);
AfxMessageBox(sResult);

(5)互斥内核对象
① 线程函数编写(见清单 15 21)
15_
清单15 21 采用互斥内核对象的线程处理函数(在 Ex 3Vi
ew.
cpp文件中)

CMutex mutex(FALSE, NULL);//互斥类对象


UINTTh re
adMu te xFun c1( LPVOIDpa
r //线程函数
am)
{ mutex.Lock();//等待互斥对象通知
//对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{ cArrayi= a ;
Sleep(1);

317

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

mutex.Unlock();//释放互斥对象
return 0;

UINTThreadMu te xFun c2( LPVOIDpa
r //线程函数
am)
{ mutex.Lock();//等待互斥对象通知
//对共享资源进行写入操作
for (int i = 0; i < 10; i++)
{ cArray10 i 1= b ;
Sleep(1);

mutex.Unlock();//释放互斥对象
return 0;

② 互斥内核对象菜单消息函数编写(见清单 15 22)
15_
清单15 22 临界区菜单消息函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx15_3V i
ew OnS y nEven //事件内核对象
t()
{ //启动线程
AfxBeginThread(ThreadMutexFunc1, NULL);
AfxBeginThread(ThreadMutexFunc2, NULL);
Sleep(300);//等待计算完毕
//报告计算结果
CString sResult = CString(cArray);
AfxMessageBox(sResult);

(6)信号量内核对象
① 编写线程函数(见清单 15 23)
15_
清单15 23 采用信号量内核对象的线程处理函数(在 Ex 3Vi
ew.
cpp文件中)

CSemaphore semaphore(2, 2);//信号量类对象


UINTTh re
a dSemaphoreFunc1( LPVOIDp
ar //线程函数
am)
{ semaphore.Lock();//试图进入信号量关口
AfxMessageBox("线程一正在执行!");//线程任务处理
semaphore.Unlock();//释放信号量计数
return 0;

UINTTh re
a dSemaphoreFun2(
c LPVOIDp
ar //线程函数
am)
{ semaphore.Lock();//试图进入信号量关口
AfxMessageBox("线程二正在执行!");//线程任务处理
semaphore.Unlock(); //释放信号量计数
return 0;

UINTTh re
a dSemaphoreFunc3(
LPVOIDp
ar //线程函数
am)
{ semaphore.Lock();//试图进入信号量关口
AfxMessageBox("线程三正在执行!");//线程任务处理
semaphore.Unlock();//释放信号量计数
return 0;
318 
第十五章 多进程与多线程编程
……………………………………………………………………………………………………………………………………………

② 编写信号量内核对象菜单消息函数(见清单 15 24)
15_
清单15 24 信号量菜单消息函数(在 Ex 3Vi
ew.
cpp文件中)


oidCEx15_3View OnSynSema phoe()
r //信号量内核对象
{ //启动线程
AfxBeginThread(ThreadSemaphoreFunc1, NULL);
AfxBeginThread(ThreadSemaphoreFunc2, NULL);
AfxBeginThread(ThreadSemaphoreFunc3, NULL);

(7)编译和运行

319
第十六章 网络通信编程

网络通信是建立在通信协议上的,目前的 Internet 网主要是采用 TCP/IP 协议,并使用


Socket(套接字)编程实现 Internet 网的通信。串口端口是一种非常重要的通信资源,串行
编程方法主要有VC++提供的标准通信函数、使用相关的 Win32 API 函数和使用 MS Comm 串行
通信控件。
本章要点:
* TCP/IP 协议
* Socket 的概念与基本结构
* Windows Sockets API 函数与 Socket 编程流程
* MFC 中的 Winsock 编程
* 使用 Win32 API 函数和 MS Comm 控件实现串行端口通信编程

16
1 TCP/
IP 协议
16
11 TCP/
IP 协议的体系结构
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)
是一个四层协议,它的结构如图 16 1 所示。
链路层:包括系统中的设备驱动程序和网络接口
卡,它们和具体的物理媒介 (如电缆)一起,高层提供物
理链路。
网络层:主要任务是做出数据分组的路由选择 。
传输层:为相互通信的两主机提供端到端的通信
能力。其中,TCP 协议向应用层提供可靠的数据连接,
它保证进程间数据传输的正确、有序和不重复。UDP 协
议仅仅为应用层提供数据报的分组发送服务,数据传
输的可靠性只能通过应用层来保证 。
应用层:负责处理具体的 TCP/IP 协议的细节。 图16 1 TCP/
IP 体系结构图

16
1 P 地址和通信端口
2 I
(1)IP 地址
IP 地址的全称是互联网地址,它是唯一标识的网络接口,即一台主机。 IP 地址是一个
320 32 位二进制数,整个 IP 地址空间分为 5 类地址,用 A、B、C、D、E 表示。其中 A、B、C 为基本类,D
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

类用于多波传送,E 类属于保留类,现在不用。它们的格式如下 (其中* 代表网络号位数,X 代


表主机号):
A 类:0* * * * * * * xxxxxxxx xxxxxxxx xxxxxxxx
B 类:10* * * * * * ******** xxxxxxxx xxxxxxxx
C 类:110* * * * * ******** ******** xxxxxxxx
D 类:1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx
E 类:1111xxxx xxxxxxxx xxxxxxxx xxxxxxxx
IP 地址一般采用点分十进制的表示方法,例如:
10000001 00110100 00000110 00000000 表示 1295260
采用点分十进制表示方法的地址分类见表 16 1。
表16 1 各类I
P 地址的范围

类型 范 围 类型 范 围
A类 00.00~127255255255 D类 224000~239255255255
B类 128000~191255255255 E类 240000~247255255255
C类 192000~223255255255

此外,还需要特别注意以下几个特殊的 IP 地址:
网络地址:IP 中主机地址为 0 的地址表示网络地址。如 12821100
广播地址:网络号后跟一个所有位全是 0 或全是 1 的后缀,就是直接广播地址。 使用广
播地址可以向网络内的所有主机发送数据 。
组播地址:224000~239255255255
回送地址:12700.0~127255255255,用于测试,一般用 127001
(2)端口号
主机之间的通信实际上是主机之间进程的通信 。 与如上所述相同,IP 地址可以唯一地
确定互联网上的主机,而在系统内部,为了区分不同的进程,TCP/IP 引入了协议端口的概念,
用它来表示主机内的不同进程。
端口是一种抽象的软件结构,进程通过系统调用和端口绑定后,对通信端口的操作类似
于对本地文件的操作。 可以说端口是进程和运输层之间 I/O 操作的桥梁。 TCP/IP 协议用
16 位二进制数 (一个 WORD )表示端口号,因此理论上系统可以分配利用的端口号有 216 =
65 536个。端口号由 IANA(Internet Asigned Number Authority)控制和分配,端口可划分为
三类:
知名端口(wellknown ports):0~ 1023,它们由 IANA 分配,为固定服务保留。 如 FTP 协
议端口号为 21,Telnet 端口号为 23,Http 协议端口号为 80 等。

16
13 客户机/服务器模式
在 TCP/IP 网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式
(Client/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的
服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、
运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客
户请求服务这一非对等关系。其次,网间进程通信完全是异步的,相互通信的进程间既不存 321

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二
者的数据交换提供同步,这就是基于客户/服务器模式的 TCP/IP。

16 t概念与 Wi
2 Socke ndowsSocke
tAP

Socket 的英文原意是“孔”或 “插座 ”,中文译为套接字,起源于美国加州大学伯克利分
校,是伯克利开发的 BSD UNIX(Berkeley Software Distribution)操作系统中的网络通信接
口之一。可以将套接字看作不同主机间进程进行双向通信的端点 。 Windows Socket 是一套
开发的、支持多种协议的 Windows 下网络编程接口,是 Windows 网络编程事实上的标准。
Windows Socket 有 WinSock11 提供的原始 API 函数和 WinSock20 提供的一组扩展函数。
这两套函数虽有重复,但是 20 提供的函数功能更强大,函数数量也更多。两套函数可以灵
活混 用,分 别 包 含 在 头 文 件 Winsock.h,Winsock2.h,分 别 需 要 引 入 库 wsock32.lib、Ws2_
32.lib。

16
21 Soc
ket的类型
TCP/IP 的 Socket 提供下列三种类型套接字。流套接字、数据报套接字和原始套接字。
(1)流套接字(SOCK_STREAM)
其提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺
序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。 文件传送协
议(FTP)即使用流式套接字。
(2)数据报套接字(SOCK_DGRAM)
它提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢
失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报套接字。
(3)原始套接字(SOCK_RAW)
该接口允许对较低层协议,如 IP、ICMP 直接访问。 常用于检验新的协议实现或访问现
有服务中配置的新设备。常用的 DOS 程序 ping 可以用原始套接字实现。

16
22 阻塞和非阻塞
Socket 有阻塞(或同步)和非阻塞(或异步 )两种使用方式,阻塞和非阻塞往往都是针对
一个函数来说的,“阻塞”就是函数直到其要执行的功能全部完成时才返回;而 “非阻塞”则是
函数仅仅做一些简单的工作,然后马上返回,而它所要实现的功能留给别的线程或者函数去
完成。例如,SendMessage 就是“阻塞”函数,它不但发送消息到消息队列,还需要等待消息被
执行完才返回;相反 PostMessage 就是个非阻塞函数,它只管发送一个消息,而不管这个消
息是否被处理,就马上返回。

16
23 Wi
ndowsSoc
ke I主要函数
tAP
Windows Socket API 中大部分函数是在原有的 Berkeley Socket 基础上建立的,除此之
外,Windows 增加了一些套接字函数,这些函数以字母 WSA 开头。
322 (1)创建套接字函数 socket()
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

函数原型:SOCKET socket(int af,int type,int protocol);


参数:af 是协议通信域,如果该参数为 0,系统将自动选择。TCP/IP 应用取 AF_INET.
type 是套接字类型,TCP/IP 协议族仅支持 SOCK_STREAM (流套接字 )、SOCK_DGRAM (数据
报套接字)和 SOCK_RAW(原始套接字)。
protocol 指定网络协议,一般取为 0,表示默认为 TCP/IP 协议。
若函数调用成功,函数返回所创建的套接字的句柄,否则产生 INVALID_SOCKET 错误。
(2)将套接字与本地进程绑定的函数 bind()
函数原型:int bind(SOCKET s, const struct sockaddr * name, int namelen);
参数:s 是由 socket()调用返回的并且未作连接的套接字描述符(套接字号)。
name 是赋给套接字 s 的本地地址 (IP 地址+ 端口号),它由 struct sockaddr 结构表示,
具体说明见 1624 节。
namelen 表明了 name 的长度。
如果没有错误发生,bind()返回 0,否则返回值 SOCKET_ERROR。
bind()函数的作用是将一个 socket 号和本地进程 (用 IP 地址+ 端口号描述 )捆绑在一
起,即将名字赋予套接字,以指定本地半相关。
一个 IP 地址和一个端口号可以是互联网上的一个唯一的进程,它们连同通信协议一起
构成一个半相关。两个进程的通信必须使用同一种协议,它们各自的 IP 地址和端口号加上
协议,构成一个全相关,一个全相关可以表示一个连接。
(3)建立套接字连接的函数 connect()
函数原型:int connect(SOCKET s,const struct sockaddr FAR * name,int namelen);
参数:s 是(在客户端)由 socket()创建的套接字描述符(套接字号 ),sockaddr 结构指针
指向的地址实际上就是服务器进程 。
如果没有错误发生,connect()返回 0,否则返回值 SOCKET_ERROR。 在面向连接的协议
中,该调用导致本地系统和外部系统之间连接实际建立 。
(4)监听连接的函数 listen ()
函数原型:int listen(SOCKET s,int backlog);
参数:s 标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。
backlog 表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值
为 5。
如果没有错误发生,listen()返回 0,否则它返回 SOCKET_ERROR。
listen()用来监听客户机的连接,它只能由服务器进程调用。
(5)接受连接请求的函数 accept()
函数原型:SOCKET accept(SOCKET s,struct sockaddr * addr, int * addrlen);
参数: s 为 处 于 侦 听 模 式 的 套 接 字 描 述 符, 即 在 调 用 accept ()前, 应 该 先 调 用
过listen()。
addr 是指向客户机套接字地址结构的指针,包含发出连接请求的那个客户机的 IP 地址
信息。
addrlen 为客户机套接字地址的长度(字节数)。
如果没有错误发生,accept()返回一个 SOCKET 类型的值,表示接收到的套接字的描述 323

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

符。否则返回值 INVALID_SOCKET。
服务器程序调用 accept 函数从处于侦听的流套接字的客户连接请求队列中取出最靠
前的一个客户请求,并创建一个新的套接字来与客户套接字之间创建连接。 对于客户机后
续的操作,都应使用这个套接字,至于原来那个侦听套接字,它仍然用于接受其他客户机连
接,而且仍处于侦听模式。
(6)流套接字数据传输的函数 send()
/recv()
/recv()用于在参数 s 指定的已连接的流套接字上发送输出/接收输入数据。
send()
函数原型:int send(SOCKET s,const char FAR * buf,int len,int flags);
int recv(SOCKET s,char FAR * buf,int len, int flags);
参数:s 为已连接的套接字描述符。
buf 指向接收输入数据缓冲区的指针,其长度由 len 指定。
flags 指定传输控制方式,如是否接收带外数据等。
如果没有错误发生,send()
/recv()返回实际发送/接收的字节数。 如果连接被关闭,返
回 0。否则它返回 SOCKET_ERROR。
(7)数据报数据传输的函数 sendto()
/recvfrom()
sendto()的使用与 send()相同,recvfrom()的使用与 recv()相同。
函数原型:int sendto(SOCKET s,const char buf,int len,int flags,
const struct sockaddr FAR* to,int tolen);
参数:buf 为发送数据的缓冲,len 为发送数据的字节,to 参数为一个指向 sockaddr 结
构的指针,带有接收数据的套接字目标地址信息,其余参数同 send()
/recv()函数。
函数原型:int recvfrom(SOCKET s,char FAR * buf,int len,int flags,
struct sockaddr FAR* from,int * fromlen);
参数:from 是一个 sockaddr 结构指针,fromlen 参数是带有指向地址结构的长度的指
针。当它返回数 据 时,sockaddr 结 构 内 便 填 入 发 送 数 据 端 的 地 址,其 余 参 数 同 send ()

recv()函数。
(8)关闭套接字 shutdown()、closesocket()
在应用 closesocket()之前,应调用 shutdown()函数从容地中断连接,即发送端通知接
收端“不再发送数据”或接收端通知发送端“不再接收数据”。
函数原型:int shutdown(SOCKET s,int how);
int closesocket(SOCKET s);
参数:s 为要关闭的套接字号。
how 为关闭的方式,取值有 0(仅关闭数据接收),1(仅关闭数据发送),2(二者都关闭)
shutdown()调用成功返回 0,否则返回 SOCKET_ERROR(即返回 1)。
(9)WSAStartup()
函数原型:int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);
参数:wVersionRequested 用于指明应用程序请求使用的 Socket 版本。
lpWSAData 用于返回请求的 Socket 的版本信息。
当一个应用程序调用 WSAStartup 函数时,操作系统利用请求的 Socket 版本来搜索相应
324 的 Socket 库,然后 将 找 到 的 Socket 库 绑 定 到 应 用 程 序 中。 此 后,应 用 程 序 就 可 以 调 用
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

Socket 库中的成员函数。
(10)WSACleanup()
函数原型:int WSACleanup(void);
应用 程 序 在 完 成 对 请 求 的 Socket 库 使 用 后, 需 要 调 用 WSACleanup 函 数 来 解 除 与
Socket 库的绑定并且释放 Socket 库所占用的系统资源。

16
24 s
tru
cts
oc r结构
kadd
sockaddr 结构表示一个套接字地址,它定义如下:

struct sockaddr
 unsigned short sa_family;//表示地址族,对于 TCP/IP 应用而言,它只能取 AF_INET
char sa_data14; //表示地址、端口等信息
};

根据地址族的不同,sockaddr 被定义成几个其他的结构,例如,TCP/IP 所在的网际通信


域使用结构 sockaddr_in:

struct sockaddr_in
 short sin_family;//表示地址族,必须是 AF_INET
unsigned short sin_port; //表示端口号
struct in_addr sin_addr; //表示 32 位 IP 地址,用 in_add 结构表示
char sin_zero8;//表示全部填充 0,保证和 sockaddr 大小
//相同
};

in_addr 结构是一个 32 位的 IP 地址,它定义如下:

struct in_addr
 union  struct u_char s_b1,s_b2,s_b3,s_b4; S_un_b;
struct u_short s_w1,s_w2; S_un_w;
u_long s_addr;

S_un;

16
25 Wi
ndowsSoc
ke I辅助函数
tAP
端口号和 IP 地址都是以网络字节存放的。这是由于不同主机的字节顺序可能不同,当
它们通过网络互联时,需要以相同的字节顺序来通信。以 32 位的 IP 地址为例,它在网络上
的传输顺序是:0~7bit,8~15bit,16~23bit 和 24~ 31bit。 这种字节顺序被称为大头 (big
endian)字节序。对一些以小头(little endian)字节序存储的系统,必须进行相应的字节序
转换。
(1)字节序转换函数
字节序转换函数共有 4 个,具体见表 16 2。
325

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表16 2 字节序转换函数

字节序转换函数 说 明
u_short htons (u_short hostshort) 主机转换成网络字节顺序(短整型)
u_long htonl (u_long hostlong) 主机转换成网络字节顺序(长整型)
u_long ntohl (u_long netlong) 网络转换成主机字节顺序(长整型)
u_short ntohs (u_short netshort) 网络转换成主机字节顺序(短整型)

例:需要将一个名叫 PORT_NUM 的端口号转换成网络字节顺序并赋给 num 变量。


num = htons(PORT_NUM);
(2)IP 地址转换函数
IP 地址实际上是一个 32 位的无符号整数,但是通常用点分十进制法表示一个 IP 地址。
Socket 提供了 函 数 inet _ addr ()和 inet _ ntoa ()来 完 成 它 们 之 间 的 相 互 转 换。 具 体 见
表16 3。

表16 3 I
P 地址转换函数

IP 地址转换函数 说 明
unsigned long inet_addr (const char FAR * cp) 点分十进制转换成 32 位无符号整型
char FAR * inet_ntoa (struct in_addr in) 32 位无符号整型转换成点分十进制

例:unsigned long addr = inet_addr("1921884");


(3)数据库转换函数
socket 提供的数据函数大都是 getXbyY ()的形式,用于在文本数据库中查找信息,如
gethostbyname(),表示根据主机名查主机信息。
函数原型:struct hostent gethostbyname(const char name);
参数:name 是需要解析的主机名,例如 www.sina.com。函数返回一个 hostent 结构的指
针,hostent 是一个描述主机信息的结构,定义如下:

struct hostnet
 char * h_name; //主机的正式名称
char * * h_aliases; //主机的别名
int h_addrtype; //主机地址类型,对于 TCP/IP 协议而言取
//值为 AF_INET
int h_length; //地址长度,对于 TCP/IP 协议而言取值为 4
char * * h_addr_list; //存储主机地址的数组
# define h_addr h_addr_list0 //向后兼容
};

16
3 Wi t编程流程与编程实例
ndowsSocke
16
31 流套接字编程流程
326 流套接字的服务器进程和客户进程在通信前必须创建各自的套接字并建立连接,然后
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

才能对相应的套接字进行“读”、“写”操作,实现数据的传输。图 16 1 为流套接字编程流程
的时序图。
服务器端和客户端编程主要步骤如下:
(1)服务器进程创建套接字(socket)
服务进程总是先于客户进程启动,服务进程首先调用 socket 函数创建一个流套接字。
(2)将服务器本地地址绑定到所创建的套接字上以使在网络上标识该套接字 (bind)
调用 bind()函数来完成。通过 bind 函数,套接字就和 IP 地址及端口联系在一起了。
(3)开始监听连接(listen)
将套接字置于监听模式并准备接收连接请求,调用 listen(
)让一个套接字等候进入连接。
(4)准备接受来自客户端的连接请求(accept)
通过调用 accept()函数使套接字作好接收客户连接的准备 。 accept()函数返回后,其
中参数 addr 中会包含发出连接请求的那个客户机的 IP 地址信息,并返回一个新的套接字
描述字,它对应于已经接收的那个客户机的连接 。对于该客户机后续的所有操作,都应使用
这个新套接字,至于原来那个监听套接字,它仍然用于接收其他客户机连接,而且仍处于监
听模式。如无连接请求,服务进程被阻塞。
(5)创建客户机套接字(socket)
(6)与远程服务器进行连接(connect)
调用 connect()函数可以建立一个端的连接,在 connect()函数中,参数 s 标识一个未连
接的套接字的描述字。name 标识服务进程 IP 地址信息。
(7)连接请求
当连接请求到来后,被阻塞服务进程的 accept()函数生成一个新的套接字与客户套接
字建立连接,并向客户返回接收信号。

图16 1 流套接字编程时序图
327

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(8)数据收发(send、recv)
一旦客户机的套接字收到服务器的接收信号,则表示客户机与服务器已实现连接,则可
以进行数据传输了。用 send()、recv()函数进行数据收发。
(9)关闭套接字
调用 shoudown()、closesocket()函数。

16
32 数据报套接字编程流程
数据报套接字是无连接的,它的编程流程要简单些,见图 16 2。

图16 2 数据报套接字编程时序图

对于接收端(一般为服务器端),先用 socket 函数建立套接字,再通过 bind 函数把这个


套接字和准备接收数据的 IP 地址信息绑定在一起,然后等待接收数据即可。 由于它是无连
接的,因此它 可 以 接 收 网 络 上 任 何 一 台 机 器 所 发 送 的 数 据 报。 常 用 的 接 收 数 据 函 数 是
recvfrom。
对于发送端(一般为客户端 )的数据报套接字来说,先用 socket 函数建立套接字,再通
过 bind 函数把这个套接字和准备发送数据的 IP 地址信息绑定在一起,然后用 sendto 发送
数据。

16
33 Wi
ndowsSoc
ke I编程实例
tAP
【例16 1】 利 用 Windows Socket API 实 现 服 务 器 与 客 户 端 的 通 信, 具 体 说 明 见
表16 4。
服务器端:
(1)新建一基于对话框的应用程序 Ex16_1Server
(2)在 Ex16_1ServerDlg.h 文件中添加语句
# include" winsock.h"
# pragma comment(lib," wsock32" )
(3)添加控件、控件成员变量和消息函数(见表 16 5)
328
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

表16 4 例16 1具体说明

项目 图示和说明

程 图(
c 客户端主界面 图(
d 登录成功消
息对话框

界 图(
a 服务器端主界面

图(
e 服务器未启动或端口不对的消息对话框

图(
b 服务器已启动
消息对话框

图(
f 用户名不正确的 图(
g 用户密码不正确
消息对话框 的消息对话框

程 (1)启动程序 Ex16_1Server.exe,见图(a),点击“设定并启动服务器”按钮,弹出图(b)
(2)启动程序 Ex16_1Client.exe,见图(c),点击“登录”按钮,弹出图(d);若服务器没有启动则弹出

图(e);若服务器已启动,用户名错弹出图(f);若服务器已启动,用户密码错弹出图(g)
功 (3)若登录成功,在客户端输入要发送的信息,然后点击“发送”按钮,在服务器端点击“接收”按
能 钮,客户端的发送信息的编辑框的信息显示在服务器端的接收信息编辑框中

表16 5 控件ID 号、成员变量和消息函数

控 件 类 型 ID 号 成员变量 成员变量类型 消息函数


编辑框(端口) IDC_PORT m_nPort UINT
编辑框(接收客户端的信息) IDC_EDIT_RECV m_strRecvClient CString
下压按钮(设定并启动服务器) IDOK OnOK()
下压按钮(接收) IDC_RECV OnRecv()
下压按钮(关闭) IDCANCEL OnCancel()
OnDestroy()
(Windows 消息)
329

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(4)在 Ex16_1ServerDlg.h 文件定义变量和函数(见清单 16 1)

16_
清单16 1 定义变量和函数(在 Ex 1Se
rve
rDl
g.h文件)


lassCEx16_ 1ServerD
lgpub li
cCD
ial
og

public:

void CleanUp();//解除与 Socket 库的绑定并且释放 Socket 库所占用的系统资源
void StartUp();//搜索相应的 Socket 库,然后将找到的 Socket 库绑定到应用程序中
...
protected:
int ret;
int error;
SOCKET m_hSocket;//定义套接字对象,接收创建的套接字
SOCKET s;//定义套接字对象
SOCKADDR_IN m_addr;//定义结构对象
};

(5)在构造函数中初始化变量(见清单 16 2)

16_
清单16 2 构造函数(在 Ex 1Se
rve
rDl
g.pp文件)

CEx16_ 1S er
v erD
lgCEx16_1S
erverDg(
l CWnd pPar
en/=NULL/)

:CD ia
lo g(CEx16_1S
erv
erDlgIDDpPa
ren
t )
{...
m_hSocket = NULL;
s = NULL;
ret = 0;
error = 0;

(6)服务器启动、监听等功能的实现(见清单 16 3)
服务器启动、监听等功能是在消息函数 OnOK()中实现的,其编程过程按图 16 1 中服务
器的编程流程进行。

16_
清单16 3 OnOK()函数(在 Ex 1Se
rve
rDl
g.pp文件)


oidCEx16_1ServerDlg OnOK()
{ //创建服务器端的 socket
if(m_hSocket != NULL)
{ closesocket(m_hSocket);//如果已创建了套接字先关闭原来的
m_hSocket = NULL;

if(m_hSocket = = NULL)
{ m_hSocket = socket(AF_INET,SOCK_STREAM,0);
ASSERT(m_hSocket != NULL);

UpdateData();
//将服务器本地地址绑定到所创建的套接字
m_addr.sin_family = AF_INET;
330 m_addr.sin_addr.S_un.S_addr = INADDR_ANY;//任何 IP 地址
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

m_addr.sin_port = htons(m_nPort);
ret = bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr));
if(ret = = SOCKET_ERROR)
{ TRACE("Bind Error: %d \n" , (error = WSAGetLastError()));
return;

AfxMessageBox("服务器已启动!");
//开始监听
ret = listen(m_hSocket, 2);
if(ret = = SOCKET_ERROR)
{ TRACE("Listen Error: %d \n" , (error = WSAGetLastError()));
return;

//使套接字作好接受客户连接的准备
s = accept(m_hSocket, NULL, NULL);
if(s = = SOCKET_ERROR)
{ TRACE("Accept Error: %d \n" , (error = WSAGetLastError()));
return;

//一旦有用户连接,就等待用户发来的请求信息,接收客户端的数据
char buff256;
ret = recv(s,buff,256,0);
if(ret = = 0 || ret = = SOCKET_ERROR)
{ TRACE("Recv data error: %d\n" , WSAGetLastError());
return;

//下面是对客户发来的信息进行处理,属字符处理
char*name = NULL;
char*pass = NULL;
int len = 0;
len = buff0;
name = new charlen+1;
for(int i = 0; i < len; i++)
namei= buffi+1;
int len2 = bufflen+1;
pass = new charlen2+1;
for(i = 0; i < len2; i++)
passi= buffi+2+len;
passlen2= \0 ;
namelen= \0 ;
CString str;
if(strcmp(name," ware" )! = 0)
{ str = _T("用户名不正确!");
TRACE(_T("用户名不正确! \n" ));

else
 if(strcmp(pass," 11111" )! = 0)
{ str = _T("用户密码不正确!");
TRACE(_T("用户密码不正确! \n" ));


331

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

if(str.IsEmpty())
str = _T("登录成功!");
//将“登录成功!”字符串发送到客户端
char*buf = str.GetBuffer(0);
ret = send(s, buf, str.GetLength(), 0);
if(ret != str.GetLength())
{ TRACE("Send data error: %d\n" , WSAGetLastError());
return;

delete  name;
delete  pass;

(7)在 Ex16_1ClientDlg.cpp 文件中编写点击“接收”按钮消息函数 OnRecv()(见清单 16


4)
清单16 4 OnRe
c 16_
v()函数(在 Ex 1Se
rve
rDl
g.pp文件)


oidCEx16_ 1Ser verDlgOnRe cv()
{ char buff256;
ret = recv(s,buff,256,0);
if(ret = = 0 || ret = = SOCKET_ERROR)
{ TRACE("Recv data error: %d\n" , WSAGetLastError());
return;

char*strsend = NULL;
int len = 0;
len = buff0;
strsend = new charlen+1;
for(int i = 0; i < len; i+ + )
strsendi= buffi+1;
strsendlen= \0 ;
m_strRecvClient = strsend;
UpdateData(false);
delete  strsend;

(8)在 Ex16_1ServerDlg.cpp 文件中编写其他函数(见清单 16 5)

16_
清单16 5 其他函数(在 Ex 1Se
rve
rDl
g.pp文件)


oidCEx16_1S erverDlgStatUp()

{ WSADATA wsaData;
WORD version = MAKEWORD(2, 0);
int ret = WSAStartup(version, &wsaData);
if(ret != 0)
TRACE("Initialize Error!\n" );


oidCEx16_1S erverDlgCleanUp()
{ if (WSACleanup()! = 0)

332
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

{ TRACE("UnInitialize Error:%d\n" , WSAGetLastError());





oidCEx16_1Serve
rDlg OnDes
tro //用c
y() las
swi
zad添加

{ CDialog::OnDestroy();
CleanUp();


oidCEx16_1Serve
rDlg OnCanc
el()
{ if(m_hSocket != NULL)
{ closesocket(m_hSocket);
m_hSocket = NULL;

CDialog::OnCancel();

(9)初始化对话框函数(见清单 16 6)
清单16 6 On
Ini
tDi
alo 16_
g()函数(在 Ex 1Se
rve
rDl
g.pp文件)

BOOLCEx 16_1ServerD
lgOnInitDia
log()
{ CDialog::OnInitDialog();
...
StartUp();
return TRUE; //return TRUE unless you set the focus to a control

(10)编译与运行
客户端:
(1)新建一基于对话框的应用程序 Ex16_1Client
(2)在 Ex16_1ClinetDlg.h 文件中添加语句

# include" winsock.h"
# pragma comment(lib," wsock32" )
(3)添加控件、控件成员变量和消息函数(见表 16 6)
表16 6 控件ID 号、成员变量和消息函数

控 件 类 型 ID 号 成员变量 成员变量类型 消息函数

编辑框(服务器 IP) IDC_SERVERIP m_strServerIP CString


编辑框(服务器端口) IDC_PORT m_nPort CString
编辑框(用户名) IDC_USER m_strUser CString
编辑框(密码) IDC_PASS m_strPass CString
编辑框(要发送给服务器的信息) IDC_EDIT_SEND m_strSend CString
下压按钮(登录) IDOK OnOK()
下压按钮(关闭) IDCANCEL OnCancel()
OnDestroy()
(Windows 消息) 333

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(4)在 Ex16_1ClinetDlg.h 文件定义变量和函数(见清单 16 7)

16_
清单16 7 定义变量和函数(在 Ex 1Se
rve
rDl
g.h文件)


lassCEx16_ 1Clie
n tD
lgpubl i
cCD i
alog
 public:
CEx16_1ClientDlg(CWnd*pParent = NULL); //standard constructor
void CleanUp();
void StartUp();
...
protected:
SOCKET m_hSocket;
SOCKADDR_IN m_addr;
int ret;
int error;
;

(5)在构造函数中初始化变量(见清单 16 8)

16_
清单16 8 构造函数(在 Ex 1Cl
ien
trD
lg.
cpp文件)

CEx16_1C li
e n
tD l
g
CEx 16_1Cl
ien
tDlg(CWnd pPa
ren
t/=NULL/)
:CDi
alo (
g CEx _
161C l
ien
tDl
gIDDpPa
ret)

{ m_hSocket = NULL;
ret = 0;
error = 0;

(6)在 Ex16_ 1ClientDlg.cpp 文 件 中 编 写 点 击 “登 录 ”按 钮 消 息 函 数 OnOK ()(见 清


单16 9)

16_
清单16 9 OnOK()函数(在 Ex 1Cl
ien
tDl
g.pp文件)


o i
dCEx16_1Clien
tD lgOnOK()
{//创建客户端套接字
if(m_hSocket != NULL)
{ closesocket(m_hSocket);//如果已创建了套接字先关闭原来的
m_hSocket = NULL;

if(m_hSocket = = NULL)
{ m_hSocket = socket(AF_INET,SOCK_STREAM,0);
ASSERT(m_hSocket != NULL);

UpdateData();
//主动连接服务器
m_addr.sin_family = AF_INET;
m_addr.sin_addr.S_un.S_addr = inet_addr(m_strServerIP.GetBuffer(0));
m_addr.sin_port = htons(m_nPort);
ret = connect(m_hSocket, (LPSOCKADDR)&m_addr, sizeof(m_addr));
if(ret = = SOCKET_ERROR)
{ TRACE("Connect Error: %d \n" , (error = WSAGetLastError()));
if(error = = 10061)
334
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

AfxMessageBox(_T("请确认服务器已打开,并工作在同样的端口!"));
return;

//准备向服务器发送请求的数据包
CString str;
str+= char(m_strUser.GetLength());
str+= m_strUser;
str+= char(m_strPass.GetLength());
str+= m_strPass;
char*buf = str.GetBuffer(0);
ret = send(m_hSocket, buf, str.GetLength(), 0);//发送数据到服务器
if(ret != str.GetLength())
{ TRACE("Send data error: %d\n" , WSAGetLastError());
return;

char buff256;
ret = recv(m_hSocket, buff, 256, 0);//等待服务器的响应,该函数将堵塞
if(ret = = 0)
{ TRACE("Recv data error: %d\n" , WSAGetLastError());
return;

buffret= \0 ;
AfxMessageBox(buff);//显示收到的服务器回传的信息

(7 )在 Ex16 _ 1ClientDlg. cpp 文 件 中 编 写 点 击 “发 送 ”按 钮 OnSend ()函 数 (见 清


单16 10)
清单16 10 OnS
e 16_
nd()函数(在 Ex 1Se
rve
rDl
g.h文件)


oidCEx16_ 1ClientDl
g OnSend()
{ UpdateData(true);
CString str;
str+= char(m_strSend.GetLength());
str+= m_strSend;
char*buf = str.GetBuffer(0);
ret = send(m_hSocket, buf, str.GetLength(), 0);
if(ret != str.GetLength())
{ TRACE("Send data error: %d\n" , WSAGetLastError());
return;

(8)在 Ex16_1ClientDlg.cpp 文件中编写其他函数(与服务器端同)(见清单 16 5)


(9)初始化对话框函数,在该函数中添加 StartUp()(见清单 16 6)
(10)编译和运行

4 MFC 中的 Wi
16 nsock
MFC 提供了两个类用于网络编程,即 CAsyncSocket 类和 CSocket 类。 CAsyncSocket 类 335

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

在很低的层面上实现了对 WinSock API 的C++封装;CSocket 类从 CAsyncSocket 类派生,在更


高层面上实现了 socket 编程。

16
41 CAs
yncSoc
ket类及其主要成员函数
CAsyncSocket 类直接封装了 WinSock API。
(1)创建套接字函数 Create()
创建一个 CAsyncSocket 类对象,并调用 Create()函数创建套接字。
函数原型:BOOL Create (UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, long
lEvent = FD _ READ | FD _ WRITE | FD _ OOB | FD _ ACCEPT | FD _
CONNECT | FD_CLOSE, LPCTSTR lpszSocketAddress = NULL);
参数 nSocketPort 为套接字指定一个端口;lEvent 用于指定要生成的事件通知。 默认
时,所有事件都会生成通知。
lpszSocketAddress 是套接字的网络地址,如“12856228”或 ftp.microsoft.com;默认
值为 NULL,表示套接字地址限定为本地机。 对于服务器而言,调用 Create ()应该指明服务
端口号;而对于客户端,使用 Create()的默认值就行了,因为调用 Connect()时会给出端口号
的。

例一:CAsyncSocket socket;
socket.Create();
例二:CAsyncSocket * pSocket = new CAsyncSocket;
pSocket - > Create(nPort,SOCK_DGRAM);

(2)套接字与服务器连接函数 Connect()
客户机套接字可以调用 CAsyncSocket::Connect 来与服务器连接。如果连接成功,返回
TRUE,否则返回 FALSE。连接失败时,可以调用 GetLastError 来得到详细的错误报告。
无论成功与否,当 Connect 调用完成后将调用 OnConnect 消息函数来通知正在连接的套
接字。OnConnect 可以在 CAsyncSocke 的派生类中重载。
函数原型为:virtual void OnConnect(int nErrorCode);
参数:nErrorCode 为错误代码,如果 nErrorCode 为 0,表示连接成功,此时套接字可以准
备发送或接收数据。如果发生错误,那么将在 nErrorCode 中包含一个特定的错误码。
(3)接受与服务器连接函数 Listen()
如果创建了 服 务 器 套 接 字,那 么 可 以 调 用 CAsyncSocket::Listen 来 侦 听 连 接 请 求。
Listen 带有一个后备参数,用于确定等待队列中有多少个连接请求。 当套接字接受队列中
的连接请求后,将调用 OnAccept。可以在 CAsyncSocket 的派生类中重载 OnAccept。为了接
受客户机的请求,需要从 OnAccept 中调用 Accept。
函数原型为:BOOL Listen(int nConnectionBacklog = 5);
参数:nConnectionBacklog 表示请求连接队列的最大长度,有效值为 1 到 5。
(4)发送和接收流式数据函数 Send() /Receive()
要从流套接字中发送数据,可以调用 CAsyncSocket::Send。当套接字的缓存是空的,并
且套接字可以进行另一次发送时,将调用 OnSend 来通知套接字现在可以调用 Send 来发送
336 数据。
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

函数原型:virtual int Send(const void* lpBuf, int nBufLen, int nFlags = 0);
参数:lpBuf 指向接收输入数据缓冲区的指针,其长度由 nBufLen 指定。nFflags 指定传
输控制方式,如是否接收带外数据等。
套接字 接 收 到 数 据 后, 将 调 用 OnReceive。 可 以 在 CAsyncSocket 的 派 生 类 中 重 载
OnReceive 并调用 Receive 从套接字缓存中读取数据。 套接字接收的数据将一直保存在套
接字缓存中,除非调用 Receive 将其读走。
函数原型:virtual int Receive(const void* lpBuf, int nBufLen, int nFlags = 0);
参数含义同上。
若发送和接收数据报数据可用函数 SendTo()和 ReceiveFrom()。
(5)关闭套接字 Close()
使用完套接 字 后, 要 调 用 CAsyncSocket:: Close 来 释 放 与 套 接 字 有 关 的 系 统 资 源。
Close 通 常 在 套 接 字 析 构 函 数 中 调 用, 必 须 删 除 之; 不 必 关 心 套 接 字 的 关 闭, 因 为
CAsyncSocket 对象析构时会调用 Close()关闭套接字句柄。

16
42 CAs
yncSoc
ket类编程实例
【例16 2】 利用 CAsyncSocket 类实现信息的发送与接收,具体说明见表 16 6。
服务器端和客户端用同一程序,所以只要编写一个应用程序。
(1)新建一基于对话框的应用程序 Ex16_2
在 step 2 of 4 选中 Windows Sockets 复选按钮。
(2)添加控件、控件成员变量和消息函数(见表 16 7)
表16 6 例16 2具体说明

项目 图 示 和 说 明

序 图(
a) 选中服务器 图(
b) 消息对话 图(
c) 服务器端数据的发送与接收

图(
d) 选中客户端 图(
e) 消息对话 图(
f) 客户端数据的发送与接收
337

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
项目 图 示 和 说 明

(1)启动程序 Ex16_2.exe,选中服务器,见图(a):点击“启动服务器开始侦听”按钮,弹出图(b),图
(a)上半部分不可用
程 (2)启动程序 Ex16_2.exe,选中客户端,见图(d):点击“连接”按钮,弹出图(e),图(a)和图(d)变成
图(c)和图(f)

(3)在客户端:在发送的消息编辑框中输入文本,点击“发送”按钮,客户端的信息发送到服务器的
功 接收的信息列表框中
能 (4)在服务器端:在发送的消息编辑框中输入文本,点击“发送”按钮,服务器的信息发送到客户端
的接收的信息列表框中
(5)在客户端:点击“断开”按钮,客户端界面回到图(d),服务器界面回到图(a)

表16 7 控件ID 号、成员变量和消息函数

控 件 类 型 ID 号 成员变量 成员变量类型 消息函数


单选按钮(客户端) IDC_CLIENTTYPE m_iType int OnClienttype()
单选按钮(服务器) IDC_SERVERTYPE OnServertype()
静态文本(服务器 IP) IDC_STATICIP
编辑框(服务器 IP) IDC_SERVERIP m_strServerIP CString
静态文本(服务器端口) IDC_STATICPORT
编辑框(服务器端口) IDC_PORT m_nPort UINT
下压按 钮 (连 接/启 动 服 务 器
IDC_BCONNECT m_ctrlConnect CButton OnBConnect()
开始侦听)
下压按钮(断开) IDC_BCLOSE OnBClose()
静态文本(发送的信息) IDC_STATICMSG
编辑框(发送的信息) IDC_EMSG m_strMessage
下压按钮(发送) IDC_BSEND OnBsend()
列表框(接收的信息) IDC_LRECVD m_ctrlRecvd CListBox

(3)编写点击两个单选按钮的消息函数(见清单 16 11)

16_
清单16 11 点击两个单选按钮的消息函数(在 Ex 1Se
rve
rDl
g.pp文件)


oidCEx16_2DlgOnC li
enttype()//单击客户类型单选按钮
{ UpdateData(true);
m_ctrlConnect.SetWindowText("连接");
GetDlgItem(IDC_BCLOSE)->EnableWindow(true);//断开按钮可用


oidCEx16_2DlgOnS er
vertype()//单击服务器类型单选按钮
{ UpdateData(true);
m_ctrlConnect.SetWindowText("启动服务器开始侦听");
GetDlgItem(IDC_BCLOSE)->EnableWindow(false);//断开按钮不可用

(4)创建新类 CNewScoket
CNewScoket 类从 CAsyncSocket 类派生,点击菜单 Insert - > New Class。 在 New Class
对话框中,类的类型为 MFC Class;类名中输入 CNewSocket;基类选择 CAsyncSocket;单击 OK,
338 即可把这个新类添加到应用程序 CEx16_2 中。
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

(5)在新类 CNewScoket 中添加变量和函数声明(见清单 16 12)

清单16 12 在新类 CNewS


cok
et中添加变量和函数声明(在 NewS
cok
e h头文件中)
t.


las
sCNewS ocketpublicCAs yncSocke

 public:
CNewSocket();
virtual ~CNewSocket();
public:
void SetParent(CDialog *pWnd); //手工添加
...
protected:
void OnClose(int nErrorCode); //手工添加
void OnConnect(int nErrorCode); //手工添加
void OnAccept(int nErrorCode); //手工添加
void OnReceive(int nErrorCode); //手工添加
void OnSend(int nErrorCode); //手工添加
private:
CDialog* m_pWnd; //手工添加
};

(6)编写新类 CNewScoket 中添加的函数(见清单 16 13)

清单16 13 编写新类 CNewS


cok
et中添加的函数(在 NewS
cok
et.
cpp文件中)

# include" Ex16_2Dlg.h"


oi dCNewS ocketSetParet(
n CD
ial
ogpWnd)
{ m_pWnd = pWnd;


oidCNewSocketOnAc c e
pt(in
tnErrorCo e)

{ if(nErrorCode = = 0)
((CEx16_2Dlg* )m_pWnd)->OnAccept();


oidCNewSocketOnCo nnet(
c in
tnEr r
orCo e)

{ if(nErrorCode = = 0)
((CEx16_2Dlg* )m_pWnd)->OnConnect();


oidCNewSocketOnC lose(i
ntnErrorCode)
{ if(nErrorCode = = 0)
((CEx16_2Dlg* )m_pWnd)->OnClose();


oidCNewSocketOnRe c e
ive(
intnErrorCode)
{ if(nErrorCode = = 0)
((CEx16_2Dlg* )m_pWnd)->OnReceive();


oidCNewSocketOnSe nd(intnErr
orCo de)
{ if(nErrorCode = = 0)
((CEx16_2Dlg* )m_pWnd)->OnSend();

注意在文件开头要添加包含语句 # include "Ex16_2Dlg.h",见清单 16 13。 339



isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(7)在对话框类的头文件中定义变量和函数(见清单 16 14)

16_
清单16 14 在对话框类的头文件中定义变量和函数(在 Ex 2Dl
g.h文件中)

# include" NewSocket.h"

lassCEx 16_ 2DlgpublicCD ia
log
 public:
void OnAccept(); //手工添加
void OnConnect(); //手工添加
void OnSend(); //手工添加
void OnReceive(); //手工添加
void OnClose(); //手工添加
...
private:
CNewSocket m_sListenSocket; //手工添加,用于连接请求的侦听
CNewSocket m_sConnectSocket; //手工添加,用于来回发送消息
...
};

注意在文件开头要添加包含语句 # include “NewSocket.h”,见清单 16 14。


(8)编写初始化对话框 OnInitDialog()函数(见清单 16 15)
清单16 15 On
Ini
tDi
alo 16_
g()函数 (在 Ex 2Dl
g.pp文件中)

BOOLCEx 16_ 2D lgOn


In i
tDialg()

{ CDialog::OnInitDialog();
...
m_iType = 0;
m_strServerIP =" 127001" ;
m_uPort = 3000;
UpdateData(false);
GetDlgItem(IDC_EMSG)->EnableWindow(false);
GetDlgItem(IDC_STATICMSG)->EnableWindow(false);
GetDlgItem(IDC_BSEND)->EnableWindow(false);
GetDlgItem(IDC_BCLOSE)->EnableWindow(false);
m_sConnectSocket.SetParent(this);
m_sListenSocket.SetParent(this);
return TRUE;//return TRUE unless you set the focus to a control

(9)连接应用程序,编写单击连接按钮的消息函数(见清单 16 16)
清单16 16 OnBCo
nne
c 16_
t()函数 (在 Ex 2Dl
g.pp文件中)


oidCEx16_2DlgOnBc onnec //单击连接按钮
t()
{ UpdateData(true);
GetDlgItem(IDC_BCONNECT)->EnableWindow(false);
GetDlgItem(IDC_SERVERIP)->EnableWindow(false);
GetDlgItem(IDC_PORT)->EnableWindow(false);
GetDlgItem(IDC_STATICIP)->EnableWindow(false);
GetDlgItem(IDC_STATICPORT)->EnableWindow(false);
GetDlgItem(IDC_STATICTYPE)->EnableWindow(false);
340 GetDlgItem(IDC_CLIENTTYPE)->EnableWindow(false);
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

GetDlgItem(IDC_SERVERTYPE)->EnableWindow(false);
//客户端
if(m_iType = = 0)
{ m_sConnectSocket.Create();//客户端产生一个缺省的套接字
m_sConnectSocket.Connect(m_strServerIP,m_uPort);//客户端与服务器端连接

else//服务器端
{ m_sListenSocket.Create(m_uPort);//服务器端产生一个绑定端口的套接字
m_sListenSocket.Listen();//服务器监听
AfxMessageBox("服务器已启动,开始侦听");

注:在 执 行 m _ sConnectSocket.Connect (m _ strServerIP,m _ uPort)语 句 时, 程 序 的 运 行 过 程 是 调 用 CNewSocket::


OnConnect ()函 数, 而 CNewSocket:: OnConnect ()函 数 实 际 又 是 调 用 CEx16 _ 2Dlg:: OnConnect ()函 数。 执 行 m _
sListenSocket.Listen()语句过程也与此相似。

(10)完成连接,编写 CEx16_2Dlg 类中手工添加的函数(见清单 16 17)

16_
清单16 17 CEx 2Dl 16_
g类中手工添加的函数(在 Ex 2Dl
g.pp文件中)


oidCEx16_2D l
g OnAccept()
{ m_sListenSocket.Accept(m_sConnectSocket);
GetDlgItem(IDC_EMSG)->EnableWindow(true);
GetDlgItem(IDC_STATICMSG)->EnableWindow(true);
GetDlgItem(IDC_BSEND)->EnableWindow(true);
GetDlgItem(IDC_BCONNECT)->EnableWindow(false);
GetDlgItem(IDC_SERVERIP)->EnableWindow(false);
GetDlgItem(IDC_PORT)->EnableWindow(false);
GetDlgItem(IDC_STATICIP)->EnableWindow(false);
GetDlgItem(IDC_STATICPORT)->EnableWindow(false);
GetDlgItem(IDC_STATICTYPE)->EnableWindow(false);
GetDlgItem(IDC_CLIENTTYPE)->EnableWindow(false);
GetDlgItem(IDC_SERVERTYPE)->EnableWindow(false);


oidCEx16_2D l
g OnConnect()
{ GetDlgItem(IDC_EMSG)->EnableWindow(true);
GetDlgItem(IDC_STATICMSG)->EnableWindow(true);
GetDlgItem(IDC_BSEND)->EnableWindow(true);
GetDlgItem(IDC_BCLOSE)->EnableWindow(true);
AfxMessageBox("已连接! 可以发送消息了");

(11)编译、运行与测试
编译完成后,同时启动程序的两份拷贝,一个是在服务器端,单击启动服务器开始侦
听按钮,另一个在客户机端,单击连接按钮 。 连接后,连接控件被禁用,消 息 发 送 控 件 被
启用 。
(12)发送和接收
应用程序连接后,还需要添加消息收发功能,使用户能在对话框窗口的编辑框中输入文
本消息,然后单击发送按钮将消息发送到对方程序的列表框中。 编写单击发送按钮的消息
函数 OnBSend(),见清单 16 18。 341

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

清单16 18 OnBS
e 16_
nd()消息函数(在 Ex 2Dl
g.pp文件中)


oidCEx16_ 2Dlg OnB send()
{ int iLen;
int iSent;
UpdateData(true);
if(m_strMessage!="" )
{ iLen = m_strMessage.GetLength();
iSent = m_sConnectSocket.Send(LPCTSTR(m_strMessage),iLen);
if(iSent = = SOCKET_ERROR)
{ AfxMessageBox("不能发送!");

else



OnReceive 事件的激发表示消息已经送达。OnReceive()函数见清单 16 19。


清单16 19 OnRe
cei
v 16_
e()函数(在 Ex 2Dl
g.pp文件中)


oidCEx16_ 2D l
g OnRe ceie()

{ char *pBuf = new char1205;
int iBufSize = 1024;
int iRcvd;
CString strRecvd;
iRcvd = m_sConnectSocket.Receive(pBuf,iBufSize);
if(iRcvd = = SOCKET_ERROR)
{ }
else
 pBufiRcvd= NULL;
strRecvd = pBuf;
m_ctrlRecvd.AddString(strRecvd);
UpdateData(false);

(13)终止连接
为关闭两个程序之间的连接,客户机端的用户应单击断开按钮终止这次连接。 服务器
端的应用程序接着就会收到 OnClose Socket 事件。 OnClose(函数和 OnBClose)函数见清单
16 20。
清单16 20 OnC
loe()函数和 OnBC
s los 16_
e()消息函数(在 Ex 2Dl
g.pp文件中)


oidCEx16_2DlgOnC loe()

{ m_sConnectSocket.Close();
UpdateData(true);
GetDlgItem(IDC_EMSG)->EnableWindow(false);
GetDlgItem(IDC_STATICMSG)->EnableWindow(false);
GetDlgItem(IDC_BSEND)->EnableWindow(false);
342 GetDlgItem(IDC_BCLOSE)->EnableWindow(false);
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

GetDlgItem(IDC_BCONNECT)->EnableWindow(true);
GetDlgItem(IDC_SERVERIP)->EnableWindow(true);
GetDlgItem(IDC_PORT)->EnableWindow(true);
GetDlgItem(IDC_STATICIP)->EnableWindow(true);
GetDlgItem(IDC_STATICPORT)->EnableWindow(true);
GetDlgItem(IDC_STATICTYPE)->EnableWindow(true);
GetDlgItem(IDC_CLIENTTYPE)->EnableWindow(true);
GetDlgItem(IDC_SERVERTYPE)->EnableWindow(true);


oi 16_
dCEx 2DlgOnBc
loe()

{ OnClose();}

(14)编译和运行

16
43 CSoc
ket类
CSocket 类 派 生 自 CAsyncSocket 类, 它 实 现 了 对 WinSock API 的 更 高 层 的 抽 象。
CSocket 类通常和 CSocketFile 类、CArchive 类结合使用,通过 CArchive 类的序列化接口完
成数据的收发功能。CSocket 改写了 CAsyncSocket 的多个函数以支持同步操作。 它们包括
Accept()、Receive()、Send()等。实际上,这些函数由原来的一调用就返回改写成等待直到
调用完成才返回。
使用 CSocket 编程可以参照下面的步骤。
(1)参考 CAsyncSocket 的使用方法完成客户端和服务器端 Socket 的创建和监听 (或是
连接)工作。
(2)不要利用 Send ()、Receive ()、SendTo ()、ReceiveFrom ()发送和接收数据,而是以
Csocket 对象创建一个 CSocketFile 对象。
例如在服务器端:

CSocket servSocket;
...
CSocketFile servSocketFile(&servSocket);
(3)根据 CSocketFile 对象创建一个与之关联的 CArchive 对象,然后便可以在这个
CArchive 对象上收发数据了:

CArchive servSocketArchiveIn(&servSocketFile,CArchive::load);
CArchive servSocketArchiveIn(&servSocketFile,CArchive::store);
(4)调用 CAchive 的“<< ”和“>> ”方法收发数据。
(5)通信结束,清除 CSocket、CSocketFile 和 CArchive 对象数据。

5 串行端口通信编程
16
串行端口通常指的是计算机提供的 COM1、COM2、COM3、COM4 等 RS232 C 端口。串行端口的
本质是作为 CPU 与外部串行设备间的编码转换设备。即当数据从 CPU 经过串行端口发送出去
时,字节数据将被转换为串行的位;在接收数据时,串行的位将由串行端口转换成字节数据。 343

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

在进行串行通信编程时,其步骤如下:
(1)操作系统提出资源申请要求(打开串口)
(2)对端口进行参数配置
(3)同串口进行数据交换并完成数据从串口的发送与接收
(4)在通信完成时应及时释放资源(关闭串口)
用VC++开发串行通信目前通常有如下几种方法:一是利用 Windows API 通信函数;二是
利用VC++的标准通信函数_inp、_inpw、_inpd、_outp、_outpw、_outpd 等直接对串口进行操
作;三是使用 Microsoft Visual C++的通信控件(MSComm)。

16
51 Wi
n I串行端口通信函数及编程实例
dowsAP
与通信有关的 Windows API 函数共有 26 个,本节主要介绍下面几个主要函数:
CreateFile()用“comn”(n 为串口号)作为文件名就可以打开串口;SetCommState()函数
负责完成串口的普通设置;ReadFile()读串口;WriteFile()写串口;CloseHandle()关闭串口
句柄。
(1)CreateFile()函数打开串口
函数原型:HANDLE CreateFile(
LPCTSTR lpFileName, //指向一个通信资源的对象名,如“COM1”
DWORD dwDesiredAccess, //串口的存取方式
DWORD dwShareMode, //串口的共享方式,设为 0
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//串口的安全属性结构
DWORD dwCreationDisposition, //创建方式,在此只能取 OPEN_EXISTIN
DWORD dwFlagsAndAttributes, //文件属性,取 FILE_FLAG_OVERLAPPED
HANDLE hTemplateFile //设置为 NULL
);
函数成功返回一个句柄,该句柄标识了由 lpFileName 指定的串口。在此后对串口的所
有操作均通过该句柄来进行。 在串口不使用时,应通过 CloseHandle()函数将其关闭,以释
放资源。
(2)SetCommState()函数设置串口
函数原型:BOOL SetCommState(
HANDLE hFile, //串口句柄
LPDCB lpDCB //指向 DCB 结构对象的指针
);
(3)I/O 读写数据
如果用 CreateFile()打开串口时,没有使用 FILE_FLAG_OVERLAPPED 标志,则表示此端
口进行的是同 步 I/O 读 写, 在 同 步 方 式 下 进 行 数 据 发 送 与 接 收 分 别 由 WriteFile ()和
ReadFile()函数完成。其函数原型为:

发送:BOOL WriteFile(
HANDLE hFile, //串口句柄
LPCVOID lpBuffer, //发送缓存
344 DWORD nNumberOfBytesToWrite, //要发送的字节
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

LPDWORD lpNumberOfBytesWritten, //实际发送的字节数


LPOVERLAPPED lpOverlapped //重叠操作结构指针
);
接收:BOOL ReadFile(
HANDLE hFile, //串口句柄
LPVOID lpBuffer, //接收缓存
DWORD nNumberOfBytesToRead, //要接收的字节
LPDWORD lpNumberOfBytesRead, //实际接收送的字节数
LPOVERLAPPED lpOverlapped //重叠操作结构指针
);
如果用 CreateFile()打开串口时,使用 FILE_FLAG_OVERLAPPED 标志,则表示此端口进
行的是异步 I/O 读写,上述两个函数的最后一个参数为指向 OVERLAPPED 结构的指针,通过该
参数,可以使数据的读写操作在后台完成 。
(4)CloseHandle()关闭串口
函数原型:BOOL CloseHandle(
HANDLE hObject //串口句柄
);
(5)其他函数(表 16 8)
表16 8 其他函数

函 数 作 用
BOOL GetCommTimeOuts() 获得当前的超时设置
BOOL SetCommTimeOuts() 完成超时设置
BOOL GetCommState() 获得串口的设置
BOOL SetupComm() 缓冲区大小的设置
BOOL SetCommMask() 建立事件屏蔽
BOOL GetCommMask() 查询得到串口当前的时间屏蔽设置情况
BOOL GetCommProperties() 获取系统支持的各项串口设置
BOOL ClearCommError() 确定缓冲区中处于等待的字节数
BOOL WaitCommEvent() 监视发生在串行口中的各种串口通信事件
BOOL GetOverlappedResult() 后台异步执行的操作是否已经完成
HANDLE CreatEvent() 创建事件句柄
BOOL ResetEvent() 复位

(6)有关结构(见表 16 9)
表16 9 有关结构

结 构 作 用

该结构包括 28 个成员,这些成员是对串口的各基本控制参数
的描述,其中 BaudRate、ByteSize、Parity 和 StopBits 这 4 个
typedef struct_DCBDCB;
成员最基本,通过这几个成员即指定了串口通信时所采取的
波特率、数据位、奇偶校验方式和停止位 345

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
结 构 作 用
typedef struct _ COMMTIMEOUTS  
该结构包括 5 个参数,为超时设置结构
COMMTIMEOUTS,*LPCOMMTIMEOUTS;
Typedef Struct_COMMPROP OMMPROP 该结构包括 18 个成员,各成员记载了系统支持的各项配置
typedef struct _ COMM _ CONFIG  该结构包括 8 个成员,其中包含有一个 DCB 结构对象,各成员
COMMCONFIG,*LPCOMMCONFIG; 主要是关于通信设备的配置信息
typedef struct _ OVERLAPPED   该结构包括 5 个成员,对于串口通信只有 hEvent 成员是有效
OVERLAPPED; 的,其他 4 个成员必须设置为 0
typedef struct_COMSTATCOMSTAT, 该结构包括 10 个成员,对于串口通信只有 hEvent 成员是有效
*LPCOMSTAT; 的,其他 4 个成员必须设置为 0

【例16 3】 利用 Windows API 通信函数实现串行端口通信,具体说明见表 16 10。

表16 10 例16 3具体说明

项目 图 示 和 说 明

程序
菜单

界面

图(
a) 程序主界面 图(
b) 串口设置 图(
c) 接收到数据

(1)将两台计算机用 COM1 口连接,都启动程序 Ex16_3.exe 见图(a),点击菜单:串口通讯-> 串


口设置,弹出对话框(b)
(2)在图(b)串口设置对话框中进行各项设置,点击“确定”按钮
程序功能 (3)在发送数据编辑框中输入要发送的数据,点击“发送数据”按钮,这时数据将通过 COM1 口
发送到另一台计算机的数据接收数据列表框中。见图(c)
(4)如果只有一台计算机,首先将计算机上的 COM1 口的 2、3 针短接(收与发形成回路),然后
运行 Ex16_3.exe,按上面的步骤即可在一台计算机上实现数据的收与发

(1)新建一基于单文档的应用程序 Ex16_3
在 step 4 of 6 选中 Windows Sockets 复选按钮,在 step 6 of 6 中选择 FormView 视图。
(2)添加串口设置对话框和有关控件,并建立串口设置对话框类
对话框 ID 号:IDD_COMSETDLG。串口设置对话框类名:CComsetDlg。有关控件 ID 号和控
件成员变量见表 16 11。
表16 11 串口设置对话框控件ID 号和控件成员变量

控 件 类 型 ID 号 成员变量 成员变量类型
组合框(端口) IDC_COMBO_COM m_nPort int
组合框(波特率) IDC_COMBO_BAUD m_nBaud int
组合框(数据位) IDC_COMBO_DATABIT m_nDataBit int
346 组合框(停止位) IDC_COMBO_STOPBIT m_nStopBit int
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

续 表
控 件 类 型 ID 号 成员变量 成员变量类型
组合框(校验) IDC_COMBO_CHECK m_nCheck int
静态文本(流控制) IDC_STATIC

(3)编辑调用串口设置对话框的菜单,添加和编写该菜单的消息函数
菜单 ID 号:ID_COMM_SET;消息函数:OnCommSet(),消息函数见清单 16 21。
清单16 21 OnCommS
e 16_
t()消息函数(在 Ex 2Vi
ew.
cpp文件中)


oidCEx16_ 3V i
ew OnCommS et()//串口设置菜单消息函数
{ //超时结构对象
COMMTIMEOUTS CommTimeOuts;
//设备控制块结构对象
DCB dcb;
//配置串口参数
CComsetDlg dlg;
if (dlg.DoModal()= = IDOK)
{ //打开串口
m_hCom = CreateFile(m_ComPortdlg.m_nPort,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (m_hCom = = INVALID_HANDLE_VALUE)
{ AfxMessageBox("打开通讯口失败!",MB_OK);
return;

//得到当前串口配置
dcb.DCBlength = sizeof(dcb);
GetCommState(m_hCom, &dcb);
//更改串口配置
//基本配置
dcb.BaudRate = m_ComBauddlg.m_nBaud;
dcb.ByteSize = m_ComDataBitdlg.m_nDataBit;
dcb.StopBits = m_ComStopBitdlg.m_nStopBit;
dcb.Parity = m_ComParitydlg.m_nCheck;
//硬件流控制
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fOutxCtsFlow = FALSE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
//软件流控制
dcb.fInX = FALSE;
dcb.fOutX = FALSE;
dcb.XonChar = (char)0xFF;
dcb.XoffChar = (char)0xFF;
dcb.XonLim = 100;
dcb.XoffLim = 100;
dcb.EvtChar = 0x0d;
347

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

dcb.fBinary = TRUE;
dcb.fParity = TRUE;
//配置串口设置
SetCommState(m_hCom, &dcb);
//超时设置
CommTimeOuts.ReadIntervalTimeout = MAXDWORD;
CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
CommTimeOuts.ReadTotalTimeoutConstant = 0;
CommTimeOuts.WriteTotalTimeoutMultiplier = 2 *9600/dcb.BaudRate;
CommTimeOuts.WriteTotalTimeoutConstant = 25;
SetCommTimeouts(m_hCom, &CommTimeOuts);
//设置缓冲区大小
SetupComm(m_hCom, 4096, 4096);
//开启数据接收线程
AfxBeginThread(ReadProc, this);

注意在 Ex16_2View.cpp 文件开头包含头文件: # include" ComsetDlg.h"


(4)定义串口设置的有关变量(见清单 16 22)
16_
清单16 22 有关变量的定义(在 Ex 2Vi
ew.
cpp文件中)

char *m_ComPort=" COM1" ," COM2" ," COM3" ," COM4"; //串口


DWORD m_ComBaud=CBR_1200,CBR_2400,CBR_4800,CBR_9600,CBR_14400, CBR_19200,
CBR_38400,CBR_57600; //波特率
BYTE m_ComDataBit=7,8; //数据位
BYTE m_ComStopBit=ONESTOPBIT, ONE5STOPBITS,TWOSTOPBITS; //停止位
BYTE m_ComParity=NOPARITY,ODDPARITY,EVENPARITY; //校验方式
HANDLE m_hCom = INVALID_HANDLE_VALUE; //串口句柄

(5)编写接收数据线程函数(见清单 16 23)
清单16 23 接收数据线程函数 Re
adPr
o 16_
c()(在 Ex 2Vi
ew.
cpp文件中)

UINTRe adProc( LPVOIDl pVo


i //数据接收线程函数
d)
{ CEx16_3View* pView = (CEx16_3View* )lpVoid;//得到视指针
if (pView = = NULL)
return 1;
//中间变量
DWORD dwErrorMask;
DWORD dwEvtMask = 0;
DWORD dwActRead = 0;
DWORD dwPoint = 0;
char Buf4096;
//串口状态结构对象
COMSTAT comstat;
//异步结构对象
OVERLAPPED ov;
ov.Internal = 0;
ov.InternalHigh = 0;
ov.Offset = 0;
348
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

ov.OffsetHigh = 0;
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
//设置事件驱动的类型
if (!SetCommMask(m_hCom, EV_RXCHAR))
return 1;
while (true)
{ WaitCommEvent(m_hCom, &dwEvtMask, NULL);//等待接收事件
if ((dwEvtMask & EV_RXCHAR)= = EV_RXCHAR)
{ClearCommError(m_hCom, &dwErrorMask, &comstat);//确定接收缓冲中处于等待的字节数
if (comstat.cbInQue> 0)
{ //异步读取数据
if (!ReadFile(m_hCom, Buf+dwPoint, comstat.cbInQue, &dwActRead, &ov))
{ if (GetLastError()= = ERROR_IO_PENDING)
{ //检测数据是否接收完毕
while (!GetOverlappedResult(m_hCom, &ov, &dwActRead, FALSE))
{ //数据尚未接收完毕
if (GetLastError()= = ERROR_IO_INCOMPLETE)
continue;

//对接收到的数据进行处理
if (dwActRead> 0)
{ //判断一条数据是否接受完毕
if (BufdwPoint+dwActRead 1= = 13)
{ //将接收到的数据插入到列表控件
BufdwPoint+dwActRead 1= 0;
pView ->m_ctrlListRecvText.InsertString(
0, CString(
Buf)
);
memset(Buf, 0, sizeof(Buf));//复位
dwPoint = 0;

else
 dwPoint+= dwActRead;//修正接收缓存指针




else
//对接收到的数据进行处理
if (dwActRead> 0)
{ //判断一条数据是否接受完毕
if (BufdwPoint+dwActRead 1= = 13)
{ //将接收到的数据插入到列表控件
BufdwPoint+dwActRead 1= 0;
pView ->m_ctrlListRecvText.InsertString(0, CString(Buf));
memset(Buf, 0, sizeof(Buf));//复位
dwPoint = 0;

else
 dwPoint+= dwActRead;//修正接收缓存指针


ResetEvent(ov.hEvent);//手动复位
349

isu
alC++编程与项目开发 ……………………………………………………………………………………………………





return 1;

(6)在 FormView 视图中添加控件、控件成员变量和消息函数(见表 16 12)


表16 12 Fo
rmV
iew 视图中控件ID 号、控件成员变量和消息函数

控件类型 ID 号 成员变量 成员变量类型 消息函数


编辑框(发的数据) IDC_ESENDTEXT m_strSendText CString
下压按钮(类型) IDC_BSEND OnBSend()单击
列表框(接收数据) IDC_LRECVTEXT m_ctrlListRecvText CListBox

(7)编写单击发送消息按钮的消息函数 OnBSend()(见清单 16 24)


清单16 24 OnBS
e 16_
nd()消息函数(在 Ex 2Vi
ew.
cpp文件中)


oidCEx 16_ 3V i
ew OnBS end()//按发送数据按钮
{ if (m_hCom = = INVALID_HANDLE_VALUE);//确保串口已打开
return;
UpdateData(TRUE);
OVERLAPPED ov;//异步结构对象
ov.Internal = 0;
ov.InternalHigh = 0;
ov.Offset = 0;
ov.OffsetHigh = 0;
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
//发送数据
DWORD dwActWrite;
! WriteFile(
if( m_hCom, m_strSendText+ CString(
13), m_strSendText.GetLength(
)+ 1,&dwActWrite, &ov)


if (GetLastError()= = ERROR_IO_PENDING)
{ //检测数据是否发送完毕
while (!GetOverlappedResult(m_hCom, &ov, &dwActWrite, FALSE))
{ //数据尚未发送完毕
if (GetLastError()= = ERROR_IO_INCOMPLETE)
continue;



//手动复位异步事件
ResetEvent(ov.hEvent);
//清除发送数据
m_strSendText = "" ;
UpdateData(FALSE);

(8)编译和运行
350
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

16
52 利用端口函数实现串行端口通信编程
这种方式主要是采用两个端口函数_inp(), _outp()实现对串口的读写,
读端口的函数原型为:int _inp(unsigned shot port)
该函数从端口读取一个字节,端口号为 0~65535。
写端口的函数原型为:int _outp(unsigned shot port, int databyte)
该函数向指定端口写入一个字节。 不同的计算机串口地址可能不一样,通过向串口的
控制及收发寄存器进行读写,可以实现灵活的串口通信功能,由于涉及具体的硬件电路讨论
比较复杂,在此不加赘述。

16
53 MSComm 控件及其编程实例
MSComm 控件是微软开发的专用通信控件,封装了串口的所有功能,使用很方便。 在工程
中通过操作菜单“Project - > Add To Project - > Components and Controls”可以弹出一个组
件和控件 的 对 话 框,进 入 “Registered ActiveX Controls ”目 录 下 并 选 择 添 加 “Microsoft
Communications Control,Version60”控件到工程。
(1)MSComm 控件的属性(表 16 13)

表16 13 MSComm 控件的属性

属性名称 说 明
CommPort 设置端口号,类型 short:1 comm1 2 comm2
设置串口通信参数,类型 CString:B 波特率,P 奇偶性(N 无校验,E 偶校验,O 奇校验),D
Settings
字节有效位数,S 停止位
PortOpen 设置或返回串口状态,类型 BOOL:TRUE 打开,FALSE 关闭
InputMode 设置从接收缓冲区读取数据的格式,类型 long: 0 Text 1 Bin
Input 从接收缓冲区读取数据,类型 VARIANT
InBufferCount 接收缓冲区中的字节数,类型:short
InBufferSize 接收缓冲区的大小,类型:short
Output 向发送缓冲区写入数据,类型:VARIANT
OutBufferCount 发送缓冲区中的字节数,类型:short
OutBufferSize 发送缓冲区的大小,类型:short
InputLen 设置或返回 Input 读出的字节数,类型:short
CommEvent 串口事件,类型:short

(2)MSComm 控件对文本信息的传输
在使用时,每个 MS Comm 控件只能对应一个串口。 如果需要访问多个串口,就必须相应
地使用多个 MS Comm 控件。
① 设置与打开端口
该类提供了 SetCommPort()函数设置要操作的端口号,用 SetSetting()函数配置串口,
最后通过 SetPortOpen()函数打开端口。
② 发送数据
用 SetOutput()函数发送数据,该函数需要将要发送的数据以 VARIANT 类型参数传入。 351

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

③ 接收数据
用 GetInput()函数从串口接收到数据,并将其保存到一个 COleSafeArray 类型的临时
变量。
【例16 4】 利用 MS Comm 控件实现串口通信,具体功能同例 16 3。
(1)项目名为 Ex16_4,其余同上题(1)
(2)同上题(1)
(3)编辑调用串口设置对话框的菜单,添加和编写该菜单的消息函数
菜单 ID 号:ID_COMM_SET;消息函数:OnCommSet(),消息函数见清单 16 25。
清单16 25 OnCommS
e 16_
t()消息函数(在 Ex 4Vi
ew.
cpp文件中)


oidCEx16_4V i
ew OnCommS et()//串口设置菜单消息函数
{ CComSetDlg dlg;
if (dlg.DoModal()= = IDOK)
{ m_ctrlComm.SetCommPort(m_ComPortdlg.m_nPort); //设置端口号
CString sSetting;
sSetting.Format( "%d,%s,%d,%s" , m_ComBauddlg.m_nBaud, m_ComParitydlg.m_nCheck,
m_ComDataBitdlg.m_nDataBit, m_ComStopBitsdlg.m_nStopBit);
m_ctrlComm.SetSettings(sSetting); //配置串口
m_ctrlComm.SetPortOpen(TRUE); //打开串口

(4)定义串口设置的全局变量(见清单 16 26)

16_
清单16 26 串口设置全局变量的定义(在 Ex 4Vi
ew.
cpp文件中)

END_MESSAGE_MAP()//结束消息映射后添加以下定义
short m_ComPort=1, 2, 3, 4; //串口
int m_ComBaud=1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600; //波特率
int m_ComDataBit=7, 8; //数据位
CString m_ComStopBit=" 1" ," 15" ," 2"; //停止位
CString m_ComParity=" n" ," o" ," e"; //校验方式

(5)在 Ex16_4View.h 文件中定义两个成员变量(见清单 16 27)

16_
清单16 27 变量的定义(在 Ex 4Vi h文件中)
ew.


las
sCEx 16_4V i
ewpub l
icCFo
rmV
iew

public:
DWORD m_dwPoint;
char m_Buf4096;

(6)在 FormView 视图中添加控件、控件成员变量和消息函数,见表 16 12。

除了表 16 12 包含的控件外再加上一个 MS Comm 控件 ,ID 号为 IDC_MSCOMM1,用

ClassWizard 给该控件类定义一个对象 m_ctrlComm。


352 (7)编写单击发送消息按钮的消息函数 OnBSend()(见清单 16 28)
第十六章 网络通信编程
……………………………………………………………………………………………………………………………………………

清单16 28 OnBS
e 16_
nd()消息函数(在 Ex 4Vi
ew.
cpp文件中)


oidCEx16_4View OnBS e //按发送数据按钮
nd()
{ if (m_ctrlComm.GetPortOpen()= = FALSE)
return;
UpdateData(TRUE);
m_ctrlComm.SetOutput(COleVariant(m_strSendText+CString(13))); //发送数据
m_strSendText ="" ; //清空发送数据
UpdateData(FALSE);

(8)添加和编写 OnCommMscomm1()函数接收数据
用 ClassWizard 添加 OnComm 消息函数 OnCommMscomm1(),该函数程序见清单 16 29。
清单16 29 OnCommMs
c 16_
omm1()消息函数(在 Ex 4Vi
ew.
cpp文件中)


oidCEx16_4V i
ewOnCommMs comm1()
{ //接收到数据
if (m_ctrlComm.GetCommEvent()= = 2)
{ COleSafeArray oleInput = m_ctrlComm.GetInput(); //从串口读取数据
long dwActRead = oleInput.GetOneDimSize(); //得到数据长度
for (long i = 0; i < dwActRead; i++)
oleInput.GetElement(&i, m_Buf+m_dwPoint+i);
oleInput.Clear(); //释放 OLE 对象
//判断一条数据是否接收完毕
if (m_Bufm_dwPoint+dwActRead-1= = 13)
{ //将接收到的数据插入到列表控件
m_Bufm_dwPoint+dwActRead-1= 0;
m_ctrlListRecvText.InsertString(0,"dfg" );
m_ctrlListRecvText.InsertString(0, CString(m_Buf));
memset(m_Buf, 0, sizeof(m_Buf)); //复位
m_dwPoint = 0;

else
 m_dwPoint+= dwActRead; //修正接收缓存指针


(9)编译和运行

353
第十七章 项目开发实例 ——— 学生
管理信息系统
管理信息系统 MIS(Management Information System)是一个由人、计算机等组成的能进
行信息的收集、传送、储存、维护和使用的系统。
本章以学生管理信息系统的开发为例,说明项目的开发过程、项目帮助文件的建立、项
目的集成与打包等内容。本系统使用 ACCESS 或 SQLServer 为后台数据库,提供学生信息管
理的一些常用基本功能。

1 管理信息系统设计原则
17
管理信息系统的设计与开发是一项系统工程,为了保证系统的质量,设计人员必须遵守
共同的原则,尽可能地提高系统的各项指标。具体包括的设计原则如下:
(1)实用性原则
(2)可扩展性与可维护性原则
(3)安全可靠性原则
(4)用户界面设计原则
MIS 人机界面必须遵循以下一些基本原则:
① 以通信功能作为界面的设计核心 ;
② 界面必须始终一致;
③ 界面必须使用户随时掌握任务的进展情况 ;
④ 界面必须能够提供帮助;
⑤ 界面友好,使用方便;
⑥ 输入画面尽可能接近实际;
⑦ 具有加强的容错性。
(5)数据库设计原则

2 需求分析
17
17
21 系统主要功能
在该系统中主要对学生的基本信息 、学生成绩等进行管理,系统主要功能如下:
(1)学生信息管理
354 用来管理学生基本信息,包括浏览、查询、添加、修改和删除。 学生基本信息包括:学号、
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

姓名、性别、出生年月、籍贯、自然班级、学院。
(2)课程信息管理
一门课程有一个上课班级,也可能有几个上课班级,因此课程信息里的授课教师是不确
定的,所以把授课教师添加到了班级信息中。 课程信息管理包括:浏览、查询、添加、修改和
删除。课程基本信息包括:课程编号、课程名称、课程类型、课程学时、课程学分、所属院系。
(3)班级信息管理
对于学分制管理下的选课而言,学生选择上课教师,上课不再采用自然班上课,而是由学
生选择教师上课后组成上课班级。因此,这里的上课班级和自然班级是不同的。这里主要是
对班级基本信息进行管理,包括浏览、
查询[按班级号查询,按课程号查询]、添加、修改和删除。
班级基本信息包括:班级编号、
所属课程、授课教师、
开始日期、结束日期、班级人数。
(4)成绩信息管理
用来管理学生成绩,包括浏览、查询[按成绩自动编号、课程号、班级号查询 ]、添加、修改
和删除。成绩基本信息包括成绩自动编号 、学生学号、上课班级编号、课程成绩。
(5)成绩统计分析
统计一个上课班级的成绩分段百分比 、平均成绩、均方差。 可以分别统计每个课程班级
的成绩和整个课程的成绩。
(6)权限管理
对于不同层次的使用者应该开放不同的权限 。
管理者:维护学生基本信息,管理院系、课程、成绩各种数据。
教师:可以录入学生成绩信息,查询学生基本信息和成绩统计信息 。
学生:可以查询成绩信息和成绩统计信息 。
(7)使用要求
需要提供方便灵活的数据查询功能,友好的人机界面,满足繁杂、多样的用户需求。
总之,通过该系统的建设来提高学生的管理效率,使得学校的发展能够适应当前教育信
息化建设的总体发展趋势。

17
22 数据流
数据流图是一种面向数据流的分析方法,主要采用自顶向下逐层分解的分析思想和原
则。图 17 1(a)为顶层数据流图,由于成绩管理数据流较为复杂,因此再给出成绩管理数


a)系统数据流顶层图 (
b)成绩管理数据流图
图17 1 数据流图 355

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

据流图,见图 17 1(b),部分数据流向将在数据库中的关系中有所体现,限于篇幅其他数据
流程模块略。

3 系统设计
17
通过上述需求分析,出于对执行部门的单一性和系统的安全考虑,此系统初步设计成单
机、多机环境两种工作模式。 单机模式推荐使用 ACCESS 数据库;分布式多机环境下使用
SQLServer 数据库,并且所有客户端和数据库都必须在一个局域网内 。

17
31 系统的功能模块
根据需求分析的结果,系统的总体功能模块如图 17 2 所示:

图17 2 系统功能图

356
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

17
32 业务流程设计
业务流程是为实现业务的某一特定目的所采取的一系列有控制的步骤 。 如不能给某门
不存在的课程添加班级,不能删除下挂有授课班级的课程等 。
图 17 3 为本系统的业务流程图,它是建立关系数据库的又一重要依据 。

图17 3 业务流程图

17
33 数据库设计
针对学生管理信息系统的功能图、数据流图以及业务流程图,采用 ACCESS 或 SQLServer
作为后台数据库,数据库命名为 StuMIS,整个系统包括 6 张数据表,分别如下:
表17 1 系统管理员表:
Use
rTa

字段命名 字段说明 字段类型 备 注

Username 用户名 nvarchar(20) 主 键


Password 密码 nvarchar(50)
Power 用户权限 int

表17 2 院系信息表:
Col
leeTa
g b

字段命名 字段说明 字段类型 备 注

CollegeID 院系编号 nvarchar(20) 主 键


CollegeMemo 院系备注 nvarchar(255)

表17 3 课程表:
Cou
rseTa

字段命名 字段说明 字段类型 备 注

CourseID 课程编号 nvarchar(20) 主 键


CourseName 课程名称 nvarchar(50)
CourseType 课程类型 nvarchar(10)
CourseHours 课程学时 tinyint
CourseCredit 课程学分 real
CollegeID 所属院系 nvarchar(20) 外键约束:CollegeTab. CollegeID
357

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表17 4 课程班级表:
Cla
ssTa

字段命名 字段说明 字段类型 备 注


ClassID 班级编号 nvarchar(20) 主 键
CourseID 所属课程 nvarchar(20) 外键约束:CourseTab. CourseID
ClassTeacher 班级教师 nvarchar(20)
ClassDateStart 开始日期 nvarchar(20)
ClassDateEnd 结束日期 nvarchar(20)
ClassNum 班级人数 int

表17 5 学生表:
Stud
ent
Tab

字段命名 字段说明 字段类型 备 注


StuID 学号 nvarchar(20) 主 键
StuName 姓名 nvarchar(10) 有索引
StuSex 性别 nvarchar(10)
StuBirthday 出生日期 nvarchar(50)
StuNativePlace 籍贯 nvarchar(20)
StuClass 自然班级 nvarchar(20)
CollegeID 所属院系 nvarchar(20) 外键约束:CourseTab. CourseID

表17 6 成绩表:
Sco
reTa

字段命名 字段说明 字段类型 备 注


ScoreID 成绩编号 int 主键:自动编号
ScoreStuID 学生学号 nvarchar(20) 外键约束:StudentTab. StuID
ClassID 上课班级编号 nvarchar(20) 外键约束:ClassTab. ClassID
Score 课程成绩 real

外键也称为关系,外键约束是关系数据库的一个重要特征,建立外键是减少数据冗余的
重要手段,可以有效地保证数据库的完整性与有效性 。比如在 ScoreTab 表 ScoreStuID 字段
中,不能输入 StudentTab 表 StuID 字段中没有的值;同样如果 StudentTab 表 StuID 字段中
的某个值被引用了,则不能随意地更改或删除它 (取决于在设计数据库关系时的设置 )。 在
ACCESS 中建立外键的方法非常简单:
(1)点击进入数据库,在表视图的空白区域内点击鼠标右键
(2)点击选择[关系]
(3)单击右键选择[显示表],选择要设计的外键 [关
系]的表
(4)单击选中需要受到约束的字段,拖放到外键查
询的字段,此时弹出图 17 4
实施参照完整性有两个选项:
① 级联更新相关字段:即如果外键中的值发生了改
变,则受它约束的相关字段会相应地更新。 实际操作中
图17 4 编辑关系[设计外键]
我们一般选择此项,以简化数据库更新操作。
358 ② 级联删除相关记录:即如果外键中的某个值被删除,则受它约束的相关字段会自动地
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

删除。为了确保数据安全,防止误操作,一般不选此项。
选择完这项即完成了一个外键[关系]的制作。
在 SQLServer 中操作大同小异,这里不再叙述。 在 SQLServer 中制作完成的关系如图
17 5 所示。

图17 5 数据库关系图(
SQLS
erv
er)

关系的引用,在减小了数据库的冗余的同时,也带来了查询与更新上的麻烦,比如要查
询 ScoreTab 中的某项成绩的课程是哪个院系开的,就需要依据外键进行三次连接查询,即:
ScoreTab - > ClassTab - > CourseTab - > CollegeTab。 为了降低这种查询所带来的书写 SQL
语句的难度和提高执行效率,在 SQLServer 中一般会采用通过建立视图或存储过程的方法
来实现,但为了确保能支持 ACCESS 数据库,本项目使用了直接写 SQL 语句的方法。

4 详细设计
17
依据系统需求分析和功能模块的设计结果,采用 Visual C++60 来实现各个模块的功
能。本系统采用基于对话框的设计方法来实现,在刚开始会尽可能详细地介绍每一步的实
现,到后面对已经介绍过的内容则会简化,只给出关键代码,请读者注意。
先使用 MFC AppWizardexe创建一个新项目,命名为 ExMIS,单击确定后在创建类型中
选择 Dialog based (基于对话框)的应用程序,其他选默认即可。

17
41 主体框架模块
主程序界面是应用程序与其他功能模块的连接平台,并且包含了权限控制,根据实际使
用的需要,学生管理信息系统采用了传统的 “菜单/工具栏/状态栏 ”风格。 下面分别介绍菜
单、工具栏与状态栏的添加与制作。

1 添加菜单
(1)选择菜单中 Insert - > Resource - > Menu 项,然后单击 New 按钮,创建一个新菜单,
将其 ID 更改为 IDR_MENU。 359

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图17 6 主窗体界面

(2)在菜单中创建如下菜单选项后 再 保 存,之 后 在 合 适 的 地 方 添 加 菜 单 分 隔 符 (见
表17 7)。
(3)在资源视图中打开 Dialog 中 IDD_EXMIS_DIALOG 对话框,在窗体上单击右键后选择
Properties 弹出属性设置对话框,在 MENU 的下拉选项中选择 IDR_MENU。
表17 7 菜单项,及其ID

菜 单 菜 单 项 菜单 ID 号
重新登录 ID_MENUITEM_RELOGIN
登录管理
退出系统 ID_MENUITEM_QUIT
学生基本信息数据维护 ID_MENUITEM_STUDENT
院系数据维护 ID_MENUITEM_COLLEGE
数据管理
课程数据维护 ID_MENUITEM_COURSE
班级数据维护 ID_MENUITEM_CLASS
成绩管理 ID_MENUITEM_SCORE
成绩管理
成绩统计 ID_MENUITEM_STAT
用户管理 系统用户维护 ID_MENUITEM_USER
记事本 ID_MENUITEM_NOTEPAD
附属工具
计算器 ID_MENUITEM_CAL
关于 帮助文档 ID_MENUITEM_HELP

 添加 犆犜狅狅
2 犾犅犪狉犆狋
狉犾工具栏
在 MFC 中,提供了两个工具栏类:CToolBar 和 CToolBarCtrl。 CToolBar 具有代码编写
简单、可装载工具栏资源等优点,但它只支持 16 色图标,而 CToolBarCtrl 却可以弥补这个缺
点,但 CToolBarCtrl 需配合 CImageList,代码也略为复杂,为了综合美观和实效,本文在主
360 窗体中使用 CToolBarCtrl,而在其他模块中使用 CToolBar 实现,读者也可以先参看后文,先
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

使用比较简单的 CToolBar 来实现。


(1)在 CExMISDlg 类的声明文件中添加两个成员变量的声明

protected:
CToolBarCtrl m_ToolBar;
CImageList m_ImageList;
(2)选择菜单中 Insert - > Resource - > Import 按钮,导入先制作好的 12 个 ICON 图标,
为减小编写代码的工作量,注意此时必须按顺序依次导入,以保证它们的 ID 值连续 [会自动
命名为 ICON1~ICON12。然后在 String Table 中添加图标下的提示文字,同样也必须依次
输入,以使它们 ID 的值连续,分别命名为 IDS_STRING102~IDS_STRING113。
(3)在 OnInitDialog()初始化函数的 return TRUE 代码前添加代码,见清单 17 1。
清单17 1 On
Ini
tDi
alg()函数(在 ExMI
o SDl
g.pp文件中)

BOOLCExMI SDlgOn InitDi


alog()
{//添加 3:工具栏
TBBUTTON button12;
int i = 0,nStringLength;
CString string;
TCHAR * pString;
m_ImageList.Create(42,42,ILC_COLOR32|ILC_MASK,0,0); //创建图像列表框,32 色,48×48
m_ToolBar.EnableAutomation();
m_ToolBar.Create(WS_CHILD|WS_VISIBLE,CRect(0,0,0,0),this,IDR_TOOLBAR);
//向 IMAGELIST 对象添加资源图标
for(i = IDI_ICON1;i<= IDI_ICON12;i+ +)
{ m_ImageList.Add(::LoadIcon(::AfxGetResourceHandle(),MAKEINTRESOURCE(i)));

m_ToolBar.SetImageList(&m_ImageList);
//如果此处循环错误则不能正常显示文字,为工具栏添加图标,文字
for(i = 0;i<12;i+ + )
{ buttoni.dwData = 0;
buttoni.fsState = TBSTATE_ENABLED;
buttoni.fsStyle = TBSTYLE_BUTTON;
buttoni.iBitmap = i;
string.LoadString(i+ IDS_STRING102);
//与 String Table 中的字符相关联;
nStringLength = string.GetLength()+ 1;
pString = string.GetBufferSetLength(nStringLength);
buttoni.iString = m_ToolBar.AddStrings(pString);
string.ReleaseBuffer();

//将工具栏的命令 ID 与菜单绑定,这样按钮和菜单的不同事件就会响应同一个消息函数
button0.idCommand = ID_MENUITEM_RELOGIN;
button1.idCommand = ID_MENUITEM_STUDENT;
button2.idCommand = ID_MENUITEM_COLLEGE;
button3.idCommand = ID_MENUITEM_COURSE;
button4.idCommand = ID_MENUITEM_CLASS;
button5.idCommand = ID_MENUITEM_SCORE;
button6.idCommand = ID_MENUITEM_STAT;
button7.idCommand = ID_MENUITEM_USER; 361

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

button8.idCommand = ID_MENUITEM_NOTEPAD;
button9.idCommand = ID_MENUITEM_CAL;
button10.idCommand = ID_MENUITEM_HELP;
button11.idCommand = ID_MENUITEM_QUIT;
m_ToolBar.AddButtons(12,button);//装载入 12 个制作好的工作栏按钮
button0.fsStyle = TBSTYLE_SEP;
//添加分隔条,这里从后向前添加,这样不用考虑添加后对序号的影响
m_ToolBar.InsertButton(10,&button0);
m_ToolBar.InsertButton(7,&button0);
m_ToolBar.InsertButton(6,&button0);
m_ToolBar.InsertButton(4,&button0);
m_ToolBar.InsertButton(1,&button0);
m_ToolBar.AutoSize;
m_ToolBar.SetStyle(TBSTYLE_FLAT|CCS_TOP);//更改风格

 添加状态栏

添加状态栏的方法较为简单:
(1)在 CExMISDlg 类的声明文件中添加一个状态栏的成员变量
CStatusBar m_wndStatusBar;
(2)在指示器数组中添加状态栏窗格的 ID 号,见清单 17 2。

清单17 2 i
ndi
cat
ors(在 ExMI
SDl
g.pp文件中)

static UINT indicators=


 IDS_STATUS_DATA, //添加的自定义字符串
ID_SEPARATOR, //系统中已定义
ID_INDICATOR_CAPS, //系统中已定义
ID_INDICATOR_NUM, //系统中已定义
ID_INDICATOR_SCRL, //系统中已定义
IDS_STATUS_SPACE, //添加的空格
};

将 ID 号定义到系统,并在 String Table 中添加的两个字符串,其方法见第 5 章。


(3)在 OnInitDialog ()函 数 的 工 具 栏 实 现 代 码 后 再 添 加 工 具 栏 初 始 化 代 码, 见 清
单17 3。

清单17 3 On
Ini
tDi
alg()函数(在 ExMI
o SDl
g.pp文件中)

//状态栏,获取客户区大小
CRect rect;
GetClientRect(rect);
//定义状态栏
m_wndStatusBar.Create(this);
m_wndStatusBar.SetIndicators(indicators,sizeof(indicators)/sizeof(UINT));
//在底部显示状态栏
m_wndStatusBar.MoveWindow(0,rect.bottom- 20,rect.right,20);
m_wndStatusBar.SetPaneStyle(2,SBPS_STRETCH);

362
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

 主窗体布局

完成上述 3 个步骤后,再为主窗体添加欢迎画面,并调整窗体大小布局,以使菜单、工具
栏和状态栏以及欢迎画面显得比较紧凑 。
(1)在资源视图中虽然其自带的画图板只支持 256 色(8 位),但依然可以导入 24 位以上
的图片。再次选择菜单 Insert - > Resource - > Import 按钮,在文件类型中选择选“所有文件
(* .* )”,然后导入事先制作好的真彩色位图,此时会弹出因位图多于 256 色而无法编辑的
英文提示,即表示导入成功。
(2)删除 IDD_EXMIS_DIALOGCExMISDlg对话框上的所有项目,添加三个 Picture 控件,
一个用来装载图片,另外两个用来做分隔线。将要做成分隔线的两个 Picture 控件属性更改
为,Type:Frame; Color: Etched。然后分别拉伸成一条线即可。将用来装载图片的 Picture
控件的 Type 属性更改为 Bitmap 然后在 Image 选项中选择刚才导入位图的 ID 即可。
(3)最后调整窗口的大小和三个 Picture 控件的位置,上下留出菜单、工具栏、状态栏的
显示空间,可反复编译调整到最佳位置,最后得到的效果如图 17 7 所示。

图17 7 调整后的主窗体界面

17
42 登录权限验证模块
 登录窗体设计

登录窗体如图 17 8 所示,并在主窗体中调用它,其制作过程如下:
(1)界面制作
① 单击菜单中的 Insert - > New Form 选项,在新建窗
体对 话 框 中, 输 入 类 名 称 CLoginDlg, 基 类 选 择 默 认 的
CDialog,此时 会 自 动 生 成 对 话 框 ID 为 IDD _ LOGINDLG _
DIALOG,并导入登录位图 IDB_BITMAP_LOGIN。
② 单击资源视图中新生成的 IDD_LOGINDLG_DIALOG
图17 8 登录窗体
对话框窗体,在窗体上单击右键,在 Caption 属性中输入
“学生管理信息系统_登录”更改标题,之后添加控件,见表 17 8。
363

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

表17 8 登录框控件列表

控件类型 控件 ID 控件变量 控 件 属 性
Picture IDC_STATIC Type: Bitmap, Image: IDB_BITMAP_LOGIN
Static Text IDC_STATIC Caption:用户名:
Static Text IDC_STATIC Caption:密 码:
Static Text IDC_STATIC Caption:数据库:
Edit Box IDC_EDIT_USERNAME m_strUsername
Edit Box IDC_EDIT_PASSWORD m_strPassword 勾选 Styles 中的 Password 属性
Data: ACCESS, SQLServer
Combo Box IDC_COMBO_TYPE m_strDataType
分成两行,Ctrl+ Enter 换行
Button IDC_BTN_LOGIN Caption:登录
Button IDC_BTN_CANCEL Caption:取消
注:其中控件变量 m_str 代表是 CString 类型的。下文中还会出现的一些常用表示:m_ctrl 代表是 Control 类型的,
m_b代表是布尔型的,m_n 代表是 int 型的,m_f 代表是 float 型的。

(2)在 ExMisDlg.h 头文件中添加权限控制变量

public:
int m_nPower; //指示登录用户的权限
(3)在 ExMisDlg.cpp 源文件中添加# include" LoginDlg.h"
(4)使用 MFC ClassWizzard(创建类向导),为 CExMisDlg 的 ID_MENUITEM_RELOGIN(重新
登录菜单选项),添加消息响应函数 OnMenuitemRelogin(),并添加代码,见清单 17 4。
清单17 4 OnMe
nui
temRe
loi
gn()菜单消息函数(在 ExMI
SDl
g.pp文件中)


oidCExMI SDlg OnMe nuit
emRe l
ogn()

{ //打开登录窗口,以下为添加
m_nPower = 0; //重新或第一次登录时,初始化权限为零
CLoginDlg dlg;
dlg.DoModal();
m_nPower = dlg.m_nPower; //获取登录用户的权限

(5)在主窗体显示之前弹出登录窗口,如图 17 8 所示。

 使用 犛犙犔 语言

接下来就要直接操作数据库,SQL 是英文 Structured Query Language 的缩写,意思为结
构化查询语言。SQL 语言的主要功能就是同各种数据库建立联系,进行沟通。 在实际应用
中,用的最多的无非是增、删、改、查四种操作,其中以 SELECT 语句最为灵活,下面简要举例
介绍一下其用法。
(1)INSERT 命令:在数据库中创建新行。用法如下:

INSERT INTO tablename ([fieldname1,fieldname2,  ) VALUES (filedvalue1,


fieldvalue2,)

364 如为用户表添加一个新的用户,因为 username 与 password 都是数据库中的保留字,所


第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

以必须加上方框号,power 为数值型,所以其值加单引号:

INSERT INTO UserTab([username,password,power)VALUES(zcl , 1 ,0)


(2)UPDATE 命令:在数据库中修改指定记录。用法如下:

UPDATE tablename
SET fieldname1 = fieldvalue1, fieldname2 = fieldvalue2
WHERE condition
如将刚才添加的 zcl 用户的密码更改为“123”:

UPDATE UserTab SET password= 123 WHERE username= zcl


(3)DELETE 命令:在数据库中删除指定记录。用法如下:

DELETE FROM tablename WHERE serarch_condition


(4)SELECT 命令:在数据库中查询指定记录。用法如下:

SELECT fieldname1,fieldname2
FROM tablename1,tablename2
WHERE search_conditions
ORDER BY order_conditions
例 1:查询密码为“1”的用户,并按用户名排序

SELECT * FROM UserTab WHERE password= 1


ORDER BY username

例 2:查询课程编号为“10001”的所有成绩记录

SELECT * FROM ScoreTab


WHERE ClassID IN (SELECT ClassID FROM ClassTab WHERE CourseID = 10001 );

例 3:查询汇总用户表的权限均值和权值总和,并将总和重命名为 total

SELECT avg(power), sum(power)AS total


FROM UserTab;

例 4:连接查询所有班级,并显示这些班级所属的课程名称 。

SELECT ClassTab.* ,CourseTab.coursename


FROM ClassTab,CourseTab
WHERE ClassTab.courseID = CourseTab.courseID

在 ACCESS 中验证 SQL 语句的方法十分简单,只要进入查询选项,然后新建一个查询,选


择 SQL 视图,写入我们自己的 SQL 语句后,单击运行即可验证是否正确。

 使用 犃犇犗 连接数据库

ADO 是基于 OLE DB 接口而设计的,是一个易于使用,速度快,内存支出小的应用程序接
口。使用 ADO 通过 SQL 语言连接 ACCESS 数据库实现用户登录。 365

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

(1)在 StdAfx.h 文件中引入相应的库文件(在文件最后添加即可):

# import" c:\program files\common files\system\ado\msado15.dll" no_namespace


rename("EOF" ," adoEOF" )(这是一整行,不能分开写)
(2)在 CExMISApp 源文件的 CExMISApp()函数中添加初始化 OLE 环境语句:

if(! AfxOleInit())
{ //初始化 OLE 环境
AfxMessageBox("OLE 初始化失败");

(3)在 CLoginDlg 头文 件 中 定 义 使 用 ADO 的 两 个 接 口 对 象 和 SQL 命 令 字 符 串,及 权


限变量。

_ConnectionPtr m_pConn; //定义一个连接对象


_RecordsetPtr m_pRst; //定义一个数据集
CString m_strSQL; //定义查询语句的字符串
int m_nPower; //用户权限
(4)使 用 MFC ClassWizard 为 CLoginDlg 类 的 IDC _ BTN _ LOGIN 添 加 消 息 响 应:
OnBtnLogin(),并编写代码,见清单 17 5。
清单17 5 OnB
tnLo
gin()消息函数(在 Lo
ginD
lg.
cpp文件中)


oidCLo ginD l
g OnB
tnLog n()

{ //检查用户名是否输入
UpdateData(true);
if(m_strUsername.IsEmpty()||m_strPassword.IsEmpty()||m_strDataType.IsEmpty())
{ AfxMessageBox("请将资料填写完整再登录!");
return;

_variant_t Holder,SQL; //添加的
//建立连接
m_pConn.CreateInstance(__uuidof(Connection));
m_pConn ->Open( "Provider = Microsoft.Jet.OLEDB.40;Data Source = StuMIS.mdb" ,"" ,"" ,- 1);
//建立数据集
m_pRst.CreateInstance(__uuidof(Recordset));
//声明对象
m_strSQL = " SELECT password,power FROM UserTab WHERE username= "+
m_strUsername+ " " ;//以上两行为一行,因为 username 是数据库中保留字,所以加上方框号
_variant_t sql = m_strSQL;
m_pRst ->Open( sql,m_pConn.GetInterfacePtr( ),adOpenDynamic,adLockOptimistic,adCmdText) ;
bool r = false;//判断是否登录成功
if(! m_pRst ->adoEOF)
{ _variant_t Holder = m_pRst ->GetCollect((long)0);
CString password = (char* )_bstr_t(Holder);
if(password = = m_strPassword)
{ //获得用户权限
Holder = m_pRst ->GetCollect((long)1);
m_nPower = (int)V_I2(&Holder);
366
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

r = true;

m_pRst ->Close();

if(! r) AfxMessageBox("错误的用户名或密码!");
else CDialog::OnOK();//相当于单击了对话框的 OK 按钮,返回一个模态,结束调用。

至此,完成了登录模块的设计与制作。

17
43 重新封装 ADO
由上文中的示例可以看出,ADO 的使用还是比较复杂的,而且极易发生异常,因为 VC 中
使用 ADO 的本质还是调用外部的 DLL(动态链接库 )来实现的,所以时序很重要,如果打乱了
装载 DLL、初始化 OLE 环境、建立连接接口的顺序或给的时间间隔不够,就会引发异常,所以
通常的解决方法是加上 trycatch异常处理块。 而且 ADO 的返回类型是_variant_t(多
种类型的集合),并不能直接使用,因此对 ADO 的重新封装就显得十分必要。

1 创建 犕狔犚犲犮狅狉犱犛犲狋类
(1)在工程中添加一个新类 MyRecordSet,并在头文件中添加成员函数和方法,见清单
17 6。
清单17 6 MyRe
cor
dSe
t.h文件


las
sMyRecordSe

 public:
CString GetFieldName(int nCol);
bool ADOEOF();
void ADOConnectionClose();
//数据集的列数,字段数
int nFieldCols;
//数据集的行数
int nFieldRows;
//返回数据集中的某个列值
CString GetFieldString(int nCol); //以字符串形式返回值
CString GetFieldString(CString strFieldName); //以字符串形式返回值
int GetFieldNumber(int nCol); //以整型返回值,参数为列序号
int GetFieldNumber(CString strFieldName); //1 次重载,参数为字段名
float GetFieldFloat(int nCol); //以浮点返回值,参数为列序号
float GetFieldFloat(CString strField); //以浮点返回值,参数为字段名
CString VariantToCString(VARIANT var); //类型转换
//类型转换
CString TimeToString(CTime time);
CTime StringToTime(CString s);
//数据集指针操作
HRESULT MoveNext();
HRESULT MovePrev();
HRESULT MoveLast();
HRESULT MoveFirst();
367

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

MyRecordSet(); //构造函数
//数据集操作
bool ADOOpen(CString strDataType);
bool ADOOpen();
bool ADOExcute(); //执行 SQL 语句:不带参数
bool ADOExcute(CString strSQL); //执行 SQL 语句:带参数
//执行 SQL 语句,返回受影响的行数,主要针对增删改
int ADOExcuteNoQuery(CString strSQL);
void ADOClose(); //手动关闭数据集
_ConnectionPtr m_pConn; //定义一个连接对象
_RecordsetPtr m_pRst; //定义一个数据集
CString m_strSQL; //查询语句
CString m_strDBType; //数据库类型
bool isConn; //是否已经打开连接
bool isOpen; //是否已经打开数据集
virtual ~MyRecordSet(); //析构造函数
};

(2)MyRecordSet 类的源文件,清单 17 7。
清单17 7 MyRe
cor
dSe
t.pp文件

MyReco r
d S etMyRe cor
dSet()
{ isConn = false;
isOpen = false;
nFieldRows = 0;
nFieldCols = 0;
try
 m_pConn.CreateInstance(__uuidof(Connection));
m_pRst.CreateInstance(__uuidof(Recordset));

catch(...)
{ }

MyReco r
d S et~MyRe cor
dSet()//析构函数
{ if(isOpen)m_pRst ->Close(); //如果数据集打开则关闭
if(isConn)m_pConn ->Close(); //如果连接打开则关闭

//建立数据库连接
boo
lMyRe cord S
et ADOOpen(CS t
rings
trDa
taType)
{ m_strDBType = strDataType;
return ADOOpen();

boo
lMyRe cord S
et ADOOpen()
{ ADOConnectionClose();
isConn = false;
try
//选择不同的数据库连接,ACCESS 或 SQLServer
if(m_strDBType = =" ACCESS" )
{m_pConn ->Open("Provider = Microsoft.Jet.OLEDB.40;Data Source = StuMIS.mdb" ,"
" ,"" ,-1);
368 }
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

else
m_pConn ->Open( "Provider = SQLOLEDB.1;Password = ;Persist Security Info = True;User
;//此处为一行
ID = sa;Initial Catalog = StuMIS;Data Source = MICROSOF 87DA1D" ,"" ,"" ,- 1)

//使游标在客户端,才能取出受影响的行列数
m_pRst ->CursorLocation = adUseClient;
isConn = true; //连接成功

catch(...)
{AfxMessageBox("与数据建立连接失败!");}
return isConn;

boolMyRe cordSetADOEx cute(CStringstrSQL) //执行 SQL 语句,返回数据集,主要针对查询
{ m_strSQL = strSQL;
return ADOExcute();

//执行 SQL 语句,返回受影响的行数,主要针对增删改
in
tMyRe c o
rdSe tADOEx cuteNoQu ery(CStringstrSQL)
{ _variant_t var;
_bstr_t sql = strSQL;
try
 m_pConn ->Execute(sql,&var,adCmdText);
return (int)V_I2(&var);

catch(...)
{return 0;

boolMyRe cordSetADOEx cute()//执行 SQL 语句,必须先对 m_ strSQL 赋值
{ //声明对象
_variant_t sql = m_strSQL;
nFieldRows = 0;
nFieldCols = 0;
ADOClose();
isOpen = false;
try
 m_pRst ->Open( sql,m_pConn.GetInterfacePtr(
),adOpenDynamic,adLockOptimistic,adCmdText)

if(m_pRst ->State)
{ nFieldRows = m_pRst ->RecordCount;
nFieldCols = m_pRst ->GetFields()->GetCount();
isOpen = true;


catch(...)
{ }
return isOpen;

voi
dMyRe co
r dSetADOC lose() //关闭数据集
{ if(isOpen)
{ m_pRst ->Close();
isOpen = false;

 369

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidMyRe c
ordSetADOCo nne
ctio
nCl
ose() //关闭数据连接
{ //只有先关闭数据集才能再关闭连接
ADOClose();
if(isConn)
{ m_pConn ->Close();
isConn = false;


bo
olMyRe c
o rdSe
tADOEOF() //返回是否数据集结尾
{ if(isConn&&isOpen)
return m_pRst ->adoEOF;
return true;

HRESULTMyRe c ordSetMoveFir
st()
{ return m_pRst ->MoveFirst(); }
HRESULTMyRe c ordSetMoveLast()
{ return m_pRst ->MoveLast(); }
HRESULTMyRe c ordSetMovePrev()
{ return m_pRst ->MovePrevious(); }
HRESULTMyRe c ordSetMoveNext()
{ return m_pRst ->MoveNext();}
CSt
ringMyRe cord Se
t GetFi
eldName(intnCo l) //返回字段名称,参数为列序号
{ CString sValue;
_variant_t vValue;
vValue = m_pRst ->GetFields()->GetItem((long)nCol)->Name;
if(vValue.vt = = VT_EMPTY||vValue.vt = = VT_NULL)
sValue ="" ;
else
sValue = (char* )(_bstr_t)vValue;
sValue.TrimLeft();
sValue.TrimRight();
return sValue;

//以字符串类型返回数据集中的值,参数是字段名称
CSt
ringMyRe cordSetGetFie
ldStri
ng( CSt
ringstrF
iedName)

{ _variant_t strField = strFieldName;
_variant_t var;
try
 var = m_pRst ->GetCollect(strField);

catch(...)
{AfxMessageBox("读取数据失败");
return"" ;

return VariantToCString(var);

//以字符串类型返回数据集中的值,传入的是列的序号
CSt
ringMyRe cordSetGetFie
ldStr
ing(intnCol)
{ _variant_t var;
try
var = m_pRst ->GetCollect((long)nCol);
370
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………


catch(...)
{ AfxMessageBox("读取数据失败");
return"" ;

return VariantToCString(var);


ntMyRe cordSet Ge
tFi
eldNumber(i
ntnCo l)//以整型类型返回数据集中的值,传入的是字段序号
{ _variant_t var;
try
 var = m_pRst ->GetCollect((long)nCol);

catch(...)
{ AfxMessageBox("读取数据失败");
return 0;

return (int)V_I2(&var);

//以整型类型返回数据集中的值,传入的是字段名
in
tMyRe cordSetGet
Fie
ldNumb er(CStringst
rF i
eldName)
{ _variant_t strField = strFieldName;
_variant_t var;
try
 var = m_pRst ->GetCollect(strField);

catch(...)
{ AfxMessageBox("读取数据失败");
return 0;

return (int)V_I2(&var);


loa
tMyRe cordSetGet
FieldFl
oat(intnCol)//以浮点类型返回数据集中的值,传入的是字段序号
{ _variant_t var;
try
 var = m_pRst ->GetCollect((long)nCol);

catch(...)
{AfxMessageBox("读取数据失败");
return 0;

return (float)V_R4(&var);


loa
tMyRe cord Se
tGe tF
ieldFl
oat(CStri
ngs t
rFie
l //以浮点类型返回数据集中的值,传入的是字段名
d)
{ _variant_t vField = strField;
_variant_t var;
try
var = m_pRst ->GetCollect(vField);}
catch(...)
{AfxMessageBox("读取数据失败");
return 0;

371

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

return (float)V_R4(&var);

//类型转换
CSt
ringMyRe cordSe
t Va r
iantToCString(VARIANTv a //将数据库中的返回值变为字符串输出
r)
{ CString strValue;
_variant_t var_t;
_bstr_t bst_t;
time_t cur_time;
CTime time_value;
COleCurrency var_currency;
switch(var.vt)
{ case VT_EMPTY:strValue = _T("");break;
case VT_UI1:strValue.Format ("%d" var.bVal);break;
case VT_I2:strValue.Format ("%d" var.iVal);break;
case VT_I4:strValue.Format ("%d" var.lVal);break;
case VT_R4:strValue.Format ("%f" var.fltVal);break;
case VT_R8:strValue.Format ("%f" var.dblVal);break;
case VT_CY:
var_currency = var;
strValue = var_currency.Format(0);
break;
case VT_BSTR:
var_t = var;
bst_t = var_t;
strValue.Format ("%s"  (const char*)bst_t);
break;
case VT_NULL:strValue = _T("");break;
case VT_DATE:
cur_time = var.date;
time_value = cur_time;
strValue = time_value.Format("%Y-%m-%d" );
break;
case VT_BOOL:strValue.Format ("%d" var.boolVal);break;
default:strValue = _T("");break;

return strValue;

CSt
ringMyRe c
ordSetTimeTo
Str
ing(
CTimet //时间到字符串
ime)

CString str;
str = time.Format("%Y-%m-%d" );
return str;

CTimeMyRe cordSetStri
n ime(
gToT CSt
rin
gsr)
t //字符串到时间
{ if(str.IsEmpty())
str = " 1990-1-1" ;
int ymd;
y = atoi(str);
int ij;
i = str.Find(- ,0);
j = str.Find(- ,i+1);
372
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

CString temp;
while(i<j-1)
temp+= str.GetAt(++i);
m = atoi(temp);
temp.Empty();
i = str.GetLength();
while(j<i-1)
temp+= str.GetAt(++j);
d = atoi(temp);
CTime tmp(ymd000);
return tmp;

由代码可看出此封装可实现以下内容:
① 用 ACCESS 和 SQLServer 两种数据库连接
② 使用时只需声明一个实例,指定连接类型就能初始化数据连接 ,执行 SQL 语句时无需
打开或关闭数据库。
③ 有多种返回值可供选择,封装了一些常用的函数和操作。
使用此类可极大地简化对数据库的操作,加快开发速度,在使用此类前,同样需要引用
ADO 的 DLL 和初始化 OLE 环境。

 使用 犕狔犚犲犮狅狉犱犛犲狋类

下面说明如何使用 MyRecordSet 对数据库进行操作,以登录验证为例,过程如下。
(1)在 CLoginDlg 类的头文件中添加 MyRecordSet 对象的实例:
MyRecordSet m_pRS; //定义数据集
(2)在 CLoginDlg 类 的 源 文 件 中 改 写 登 录 按 钮 IDC _ BTN _ LOGIN 的 消 息 响 应 函 数
OnBtnLogin(),见清单 17 8。
清单17 8 OnB
tnLo
gin()消息函数(在 Lo
ginD
lg.
cpp文件中)


oidCLoginDlgOnB tnLo i
gn()
{ //检查用户名是否输入
UpdateData(true);
if(m_strUsername.IsEmpty()||m_strPassword.IsEmpty()||m_strDataType.IsEmpty())
{ AfxMessageBox("请将资料填写完整再登录!");
return;

m_pRS.ADOOpen(m_strDataType); //打开所选数据连接
m_strSQL =" SELECT passwordpower FROM UserTab WHERE username= "+m_strUsername+ " " ;
m_pRS.ADOExcute(m_strSQL);
if(m_pRS.nFieldRows = = 1) //判断是否找到 1 个用户
{ if(m_pRS.GetFieldString(0)= = m_strPassword) //判断密码是否正确
{ m_nPower = m_pRS.GetFieldNumber(1); //取得权限
CDialog::OnOK();

else
 AfxMessageBox("错误的密码!"); }
} 373

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

else
 AfxMessageBox("错误的用户名!");}

这样就完成了登录查询和取得权限的操作,为了将登录时选的数据库类型通过主窗体
传给其他模块,有必要在主窗体中添加如下操作:
(3)在 CExMISDlg 类的头文件中添加 MyRecordSet 对象的实例:

MyRecordSet m_pRS; //定义数据集


(4)在 CExMISDlg 的源文件的 OnMenuitemRelogin()中添加代码:

dlg.DoModal();//在登录窗口取得返回值后添加
m_pRS.m_strDBType = dlg.m_pRS.m_strDBType;//将登录时选择的数据库类型传到主
//窗体

17
44 院系数据管理模块
因为院系数据管理模块涉及控件较少 ,操作的数据库字段也少,所以先行介绍。 院系管
理模块其实就是对 CollegeTab 表的管理,这个
表不受外键约束,处在控制的最高层,从关系图
可以看出,除用户表外,其他表都直接或间接受
他的约束。

 界面设计

图 17 9 即为院系管理界面,其实现步骤
如下:
(1)新建一个名为 CCollegeDlg 的对话框
类 (继 承 自 CDialog 类 ),ID 自 动 命 名 为 IDD_
COLLEGEDLG_ DIALOG并 更 改 标 题 (Caption )为
图17 9 院系管理界面
“院系管理”。
(2)按图 17 9 为院系管理界面添加控件如下表:

表17 9 院系管理窗口控件列表

控 件 类 型 控件 ID 控件变量 控 件 属 性
List Control IDC_LIST m_ctrlList
Static Text IDC_STATIC Caption:院系名称:
Static Text IDC_STATIC Caption:院系备注:
Edit Box IDC_EDIT_ID m_strID
Edit Box IDC_EDIT_MEMO m_strMemo 勾选 Styles 中的 Multiline 属性
Button IDC_BUTTON_CLEAR Caption:清空输入
Button IDC_BUTTON_ADD Caption:保存新增
Button IDC_BUTTON_EDIT Caption:保存修改
Button IDC_BUTTON_DEL Caption:删除记录
374
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

 院系浏览与显示

(1)在 CCollegeDlg 头文件中添加 MyRecordSet 的实例和成员函数
MyRecordSet m_pRS;//数据库类型会在主窗体调用时被主窗体指定
void ShowListItems();//在列表中显示院系
(2)为 CCollegeDlg 类添加(重载)WM_INITDIALOG 的消息映射 OnInitDialog(),编写成
员函数实现代码,以便显示院系列表,见清单 17 9。
清单17 9 On
Ini
tDi
alg()和 Sh
o owL
ist
Items()函数(在 Co
lle
geD
lg.
cpp文件中)

BOOLCCo l
legeDlgOnI n
itDia
log()
{ CDialog::OnInitDialog();
//添加的代码
m_ctrlList.InsertColumn(0,"院系名");
m_ctrlList.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES);//设备风格
m_ctrlList.SetColumnWidth(0,160);
m_pRS.ADOOpen();//打开 ADO 的连接
ShowListItems();//在列表中显示院系
return TRUE;


oidCCol
legeD lg
ShowList
Items()
{ m_ctrlList.DeleteAllItems();//清空列表项
m_pRS.ADOExcute("SELECT CollegeID FROM CollegeTab" );
int i = 0;
while(!m_pRS.ADOEOF())
{ m_ctrlList.InsertItem(i++m_pRS.GetFieldString(0));
m_pRS.MoveNext();

m_ctrlList.SetRedraw(TRUE);

(3 )为 CCollegeDlg 类 中 的 IDC _ LIST 控 件 添 加 单 击 事 件 NM _ CLICK 的 消 息 映 射


OnClickList(),以实现单击显示院系信息列表,并添加代码,见清单 17 10。
清单17 10 OnC
lic
kLi
st()消息函数(在 Co
lle
geD
lg.
cpp文件中)

CCo
llegeDlg OnC l
ickLit()

{ CString strSQL;
int i = m_ctrlList.GetSelectionMark();
CString str = m_ctrlList.GetItemText(i0);
strSQL.Format("SELECT * FROM CollegeTab WHERE CollegeID = %s " str);
m_pRS.ADOExcute(strSQL);
if(m_pRS.nFieldRows!= 1)
{ AfxMessageBox("数据库查找错误!");
return;

m_strID = m_pRS.GetFieldString("CollegeID" );
m_strMemo = m_pRS.GetFieldString("CollegeMemo" );
UpdateData(false);

375

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

 院系数据操作

(1)清空输入:为 IDC_BUTTON_CLEAR 控件添加 OnButtonClear()事件,见清单 17 11
(2)保存添加:为 IDC_BUTTON_ADD 控件添加 OnButtonAdd()事件,见清单 17 11
(3)保存修改:为 IDC_BUTTON_EDIT 控件添加 OnButtonEdit()事件,见清单 17 11
(4)删除记录:为 IDC_BUTTON_DEL 控件添加 OnButtonDel()事件,见清单 17 11
清单17 11 院系数据操作消息函数(在 Co
lle
geD
lg.
cpp文件中)

CCo l
leg eD lg OnBu ttonClear()//清空输入
{ m_strID ="" ;
m_strMemo ="" ;
UpdateData(false);

CCo l
leg eD lg OnBu ttonAdd() //保存添加
{ UpdateData(true);
CString sql;
sql.Format( "INSERT INTO CollegeTab(
CollegeID
CollegeMemo)Values(%s %s )
",m_strID
m_strMemo)

if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("增加记录成功!");
ShowListItems();//增加完记录后,马上刷新显示;

else
AfxMessageBox("增加记录失败,可能是重复的院系名,可选择保存修改!");

CCo l
leg eD lg OnBu ttonEdit()//保存修改
{ UpdateData(true);
CString sql;
sql.Format("UPDATE CollegeTab SET CollegeID = %s CollegeMemo = %s WHERE
CollegeID = %s " m_strIDm_strMemom_strID);//此处为一行
if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("编辑记录成功!");
ShowListItems(); //编辑完记录后,马上刷新显示;

else
AfxMessageBox("编辑记录失败!");

CCo l
leg eD lg OnBu ttonDel()//删除记录
{ if(m_ctrlList.GetSelectionMark() <0)
{ AfxMessageBox("请先选中要删除的项!");
return;

//此操作不能恢复,一般都需要确认一下
if(AfxMessageBox("您确认要删除吗?",MB_OKCANCEL)= = IDOK)
{ UpdateData(true);
CString sql;
sql.Format("DELETE * FROM CollegeTab WHERE CollegeID = %s " m_strID);
if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("删除记录成功!");
ShowListItems();//刷新显示;

else
376
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

 AfxMessageBox("删除记录失败,当其他表存在有依赖此记录的项时则不能删除!");


 调用院系管理窗口

下面说明在主窗体中调用院系窗体,设定只有权限值为 “3”的管理员才能调用,因为权
限验证代码会被多次使用,所以设计了一个权限判断函数来实现:
(1)在 CExMISDlg 头文件中添加函数声明。

bool PowerValidate(int nPower); //权限管理

(2)在 CExMISDlg 源文件中添加函数代码,见清单 17 12。


(3)为 CExMISDlg 的 ID_MENUITEM_COLLEGE 添加消息响应,和院系窗体的头文件引用,
见清单 17 12。
清单17 12 Powe
rVa
lid
ate()和 OnMe
nui
temCo
lle
ge()函数(在 ExMI
SDl
g.pp文件中)

# include "CollegeDlg.h"
boolCExMI SDlgPowe rVal
ida
te(i
ntnPowe //权限管理
r)
{ if(m_nPower<nPower) //m_nPower 已经在登录时被赋用户权限值
{ AfxMessageBox("对不起,您的权限不够,无法执行此项操作!");
return false;

return true;


oidCExMI SDlgOnMe nui
temCo l
le ge()//院系管理,三级以上权限可进
{ if(PowerValidate(3))
{ CCollegeDlg dlg;
//将使用的数据库类型传给学院管理子窗体
dlg.m_pRS.m_strDBType = m_pRS.m_strDBType;
dlg.DoModal();

17
45 学生数据管理模块
由前面数据库关系图可以看出 ,因为学生表 [StudentTab只 受 院 系 表 [CollegeTab
的约束 ,所以制作完院系模块 ,并 录 入 一 些 院 系 信 息 后 ,就 可 以 开 始 制 作 学 生 数 据 管 理
模块 。

 界面设计

设计好的学生数据管理界面如图 17 10。
此窗体的设计略有些复杂,同样使用了包含菜单、工具栏和状态栏的窗体,因为数据管
理的功能项都大同小异,因此我们设计了两个通用的菜单栏和工具栏 ,只是在不同的窗口中
响应不同的消息响应而已。窗体制作过程如下。 377

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图17 10 学生数据管理界面

(1)添加窗体和控件
新建一对话框窗体,ID 号为 IDD_DATADLG_DIALOG,添加控件如下,并按图 17 10 布局以
便给菜单、工具、状态栏显示空间。
表17 10 学生数据管理窗口控件列表

控 件 类 型 控件 ID 控件变量 控 件 属 性
Group Box IDC_STATIC Caption:详细信息:
Static Text IDC_STATIC Caption:姓名:
Static Text IDC_STATIC Caption:籍贯:
Static Text IDC_STATIC Caption:性别:
Static Text IDC_STATIC Caption:院系:
Static Text IDC_STATIC Caption:出生年月:
Static Text IDC_STATIC Caption:班级:
Static Text IDC_STATIC Caption:学号:
Edit Box IDC_EDIT_NAME m_strName
Edit Box IDC_EDIT_NATIVEPLACE m_strNativePlace
Edit Box IDC_EDIT_CLASS m_strClass
Edit Box IDC_EDIT_ID m_strID
Combo Box IDC_COMBO_SEX m_strSex Data:男、女;分两行
Combo Box IDC_COMBO_COLLEGE m_strCollege 不赋初值,由程序填充
Data Time Picker IDC_DATE_BIRTHDAY m_timeBirthday
Button ID_ITEM_SEARCH Caption:查找
Button ID_ITEM_DELETE Caption:删除记录
Button ID_ITEM_SAVE Caption:保存添加
Button ID_ITEM_EDIT Caption:保存编辑
勾选 Styles 中:Has buttons;
Tree Control IDC_TREE m_ctrlTree
Has lines; Lines at root;
List Control IDC_LIST m_ctrlList
378
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

(2)建类并定义控件变量
为新建的对话框建类,类名为 CDataDlg基类为 CDialog;添加的控件变量见表 17 10。

 添加菜单栏和状态栏

因为菜单里部分菜单项和界面上的四个按钮的功能相同 ,功能相同的菜单项和按钮以
相同的 ID 命名,这样一来,触发它们就会响应同一个消息函数 ,从而减小代码编写量。
(1)添加菜单
在 Insert Resource 中添加一个新的 Menu设置菜单 ID 号为 IDR_MENU_DATA并添加表
17 11 的菜单项。
表17 11 通用菜单项列表

查找记录:ID_ ITEM_ SEARCH清空输入:ID_ ITEM_ ADD保 存 添 加:ID_ ITEM_ SAVE修 改 记 录:


数据操作
ID_ITEM_EDIT删除记录:ID_ITEM_DELETE刷新所有:ID_ITEM_REFRESH
关于 帮助:ID_ITEM_HELP
退出 退出程序:ID_ITEM_QUIT

(2)关联菜单
在学生数据维护窗体 IDD_DATADLG _DIALOG 上点击右键,
更选 MENU 为 IDR_MENU_DATA 即可。
(3)添加状态栏 状态栏的添加见 17413

 添加 犆犜狅狅
3 犾犅犪狉工具栏
下面将使用比较简单的 CToolBar 来实现在对话框窗体上显示工具栏 。
(1)添加工具栏资源
在 Insert Resource 中添加一个新的 Toolbar 并将工具栏的 ID 重命名为:IDR_TOOLBAR_
并添加如图 17 11 所示的图标,其 ID 从左至右依次为:ID_ITEM_ADD、ID_ITEM_SAVE、ID
DATA 
_ITEM_SEARCH、ID_ITEM_EDIT、ID_ITEM_DELETE、ID_ITEM_REFRESH、ID_ITEM_HELP、ID_ITEM_
QUIT。

图17 11 学生数据管理窗口工具栏

(2)装载工具栏资源
工具栏的显示并不能像菜单那样直接在窗口上通过可视化的操作实现 ,通过下面的步
骤可实现在界面的显示。
① CDataDlg 的头文件中添加变量声明
CToolBar m_wndToolBar;
② CDataDlg 类添加 OnInitDialog 初始化消息函数,见清单 17 13
清单17 13 On
Ini
tDi
alg()函数(在 Da
o taD
lg.
cpp文件中)


oidCDataD
lgOn I
nitD
ial
o //初始化对话框函数
g()
{ ...
CRect rect;
379

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

GetClientRect(rect);
m_wndToolBar.Create(this);
m_wndToolBar.LoadToolBar(IDR_MENU_DATA); //装载工具栏资源
m_wndToolBar.MoveWindow(0,0,rect.right48);//调整工具栏位置

 添加数据到树形及列表控件

树形控件(Tree Control)和列表控件(List Control)是程序中常用的,也是比较难使用
的两个控件,下面将使用树形控件来制作一个索引目录,并使用列表控件来实现对数据的
浏览。
(1)首先在 CDataDlg 的头文件中添加一些成员数据和函数的声明 ,见清单 17 14。
清单17 14 CDa
taD
lg类的成员数据和函数声明(在 Da
taD
l h文件中)
g.

Cl
a s
sCDa taDlg

CImageList m_TreeBootImage;//树形控件图标
CString m_strSQL; //保存上一次操作的 SQL 语句,以便刷新或排序用
void AddTreeNodes(); //添加数据到树形控件
void AddListItems(); //添加数据到列表控件
HTREEITEM hRootItem; //根节点
//如果该值[strValue在指定节点[hParent的子节点中不存在,则添加一个为此值的节点;
HTREEITEM AddDistinctNode(HTREEITEM hParentCString strValue);
};

(2)在使用树形控件前,需先指定其图标,先导入制作好的图标 (Insert Resource - >


Import),命名为 IDB_BITMAP_TREE添加代码见清单 17 15。
清单17 15 创建图标(在 Da
taD
lg.
cpp文件中)

CDataDlgCDataDlg(CWnd pPa rent/=NULL/):CD ia


log(CDa taD
lg
IDDpPa
ret)

{...
//导入的图标是横向连到一起的,创建 CImageList 时指定 20 像素为宽截取出来
m_TreeBootImage.Create (IDB_BITMAP_TREE201ILC_COLOR32);

(3)为 CDataDlg 的 ID_ITEM_REFRESH 添加消息函数,并在初始化函数中添加代码,见清


单 17 16。
清单17 16 On
Ini
tDi
alg()函数(在 Da
o taD
lg.
cpp文件中)

BOOLCDa t
aDlgOnInitDi
alog()//初始化对话框函数
{ ......
m_ctrlTree.SetImageList(&m_TreeBootImageTVSIL_NORMAL); //初始化树形控件,设置图标
//设置列表控件的样式
m_ctrlList.ModifyStyle(0,LVS_REPORT|LVS_SHOWSELALWAYS|LVS_SINGLESEL);
m_ctrlList.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|
LVS_EX_HEADERDRAGDROP);
380 m_pRS.ADOOpen();//打开数据连接
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

CDataDlg::OnItemRefresh();//调用刷新记录事件全部重新填充
return TRUE;


oidCDataDlgOn ItemRe f
resh()//刷新所有记录,主要是让树控件刷新
{ m_strSQL =" SELECT * FROM StudentTab" ; //暂存 SQL 语句,供排序时使用
m_pRS.ADOExcute(m_strSQL);
AddTreeNodes();//添加院系、班级到树控件中
AddListItems();//添加学生到列表中
//往院系下拉框中填充院系表已经有的院系,以便用户选择。
MyRecordSet rs;//为院系控件添加院系列表,此处建立了一个临时的 RecordSet 实例
rs.ADOOpen(m_pRS.m_strDBType);//打开数据连接
rs.ADOExcute("SELECT CollegeID FROM CollegeTab" );//提取数据集
while(!rs.ADOEOF())
{ m_ctrlCollege.AddString(rs.GetFieldString(0));
rs.MoveNext();

rs.ADOConnectionClose();//释放资源

(4)向控件中添加数据,见清单 17 17。
AddTreeNodes()为 向 树 形 控 件 中 添 加 数 据,在 填 充 树 形 控 件 时 编 写 了 一 个
AddDistinctNode()函数。这个函数十分巧妙,只遍历了一遍,即完成了分类的功能,减小了
很多代码编写量,读者可仔细体会。AddListItems()为向列表控件中添加数据。
清单17 17 向控件中填充数据的函数(在 Da
taD
lg.
cpp文件中)

vo
idCDataDlgAddTr eeNo des()
{ m_ctrlTree.DeleteAllItems();//删除所有节点控件中的项目
//添加根节点即学生表的信息
hRootItem = m_ctrlTree.InsertItem("学生基本信息",0,2,TVI_ROOTTVI_LAST);
m_pRS.MoveFirst(); //指向第一条
while(!m_pRS.ADOEOF())
{ //如此院系不在根节点中,则添加院系到根节点中
HTREEITEM hCollege = AddDistinctNode(hRootItem
m_pRS.GetFieldString(
"CollegeID" )
);
//如此院的班级不在院系节点的子节点中,则添加
HTREEITEM hClass = AddDistinctNode(hCollegem_pRS.GetFieldString("StuClass" ));
m_pRS.MoveNext();


//如果该值[strValue在指定节点的子节点中不存在,则将此值添加到其子节点中;
HTREEITEM CDa taDlgAddD is
tinctNode( HTREEI TEMhPa rentCS t
rings t
rValue)
{ HTREEITEM hItem = m_ctrlTree.GetChildItem(hParent);
while(hItem)
{ CString strItemText = m_ctrlTree.GetItemText(hItem);
if(strItemText = = strValue)
return hItem; //已经存在该值的子节点,则返回
hItem = m_ctrlTree.GetNextSiblingItem(hItem);

//遍历了所有子节点,没有找到该值,则添加并返回
return m_ctrlTree.InsertItem(strValue12hParentTVI_LAST);
} 381

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


oidCDataDlg AddL is
tIems()

{ //清列表框的头,列,行
int nCount = 0;
CHeaderCtrl *pHeaderCtrl = m_ctrlList.GetHeaderCtrl();
if(pHeaderCtrl!= NULL)
nCount = pHeaderCtrl ->GetItemCount();
for(int i = 0;i<nCount;i++)
m_ctrlList.DeleteColumn(0);
m_ctrlList.DeleteAllItems();
//取出字段名显示到列表头中
for(i = 0;i< m_pRS.nFieldCols;i++)
m_ctrlList.InsertColumn(im_pRS.GetFieldName(i),LVCFMT_LEFT110);
//取出字段值放到列表行中
int nItem = 0;
while(!m_pRS.ADOEOF())
{ m_ctrlList.InsertItem(nItemm_pRS.GetFieldString(0));
for(int i = 1;i<m_pRS.nFieldCols;i++)
m_ctrlList.SetItemText(nItemim_pRS.GetFieldString(i));
m_pRS.MoveNext();

 添加事件到树形及列表控件

下面将添加一些事件到树形及列表控件中 ,如单击树形控件的某个节点,会在列表控件
中显示对应的数据;单击列表控件的某个行,会有详细信息框中显示;单击列表控件的某列
会按列 值 重 新 排 序 等。 在 CDataDlg 中 添 加 IDC _ TREE 的 TVN _ SELCHANGED 消 息 映 射
OnSelchangedTree (); 添 加 IDC _ LIST 的 NM _ CLICK 和 LVN _ COLUMNCLICK 的 消 息 映 射
OnClickList()和 OnColumnclickList();见清单 17 18。
清单17 18 向控件中填充数据的函数(在 Da
taD
lg.
cpp文件中)

//单击树形控件的某个节点后

oidCDa taDl
g OnS e
lch
a n
gedTr e(
e NMHDR pNMHDRLRESULT pRe s
ult)
{ NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW* )pNMHDR;
* pResult = 0;
HTREEITEM hSelItem = m_ctrlTree.GetSelectedItem(); //取得当前选中的节点
HTREEITEM hParentItem = m_ctrlTree.GetParentItem(hSelItem); //取得选中节点的父节点
//通过父节点判断选中的的是根节点,还是学院二级子节点,按不同情况写 SQL 语句的条件
CString strSqlstrstrCondition; //SQL 语句
strSql = " SELECT *FROM StudentTab" ;
if(hParentItem!= NULL)
{ HTREEITEM hOldParent = m_ctrlTree.GetParentItem(hParentItem);
if(hOldParent!= NULL)
{ //选中的是班级节点,添加院系条件和班级条件
strCondition.Format(" Where CollegeID = %s AND StuClass = %s " 
m_ctrlTree.GetItemText(hParentItem),m_ctrlTree.GetItemText(hSelItem));

else
//选中的是院系节点,添加院系条件
382
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

strCondition.Format(
" Where CollegeID = %s " 
m_ctrlTree.GetItemText(
hSelItem)
);


else
 strCondition ="" ; //选中的是根节点,没有过滤条件

strSql+= strCondition; //为 SQL 加上筛选条件,
m_strSQL = strSql; //将 SQL 语句暂存,以供排序(单击列表控件列时)使用;
m_pRS.ADOExcute(strSql); //提取数据集
AddListItems(); //重新填充列表控件

//当单击列表控件某一行后,在详细信息中显示
vo
idCDa t
aD l
g OnC l
ickL
ist(NMHDR pNMHDRLRESULT pRe su
lt)
{ int i = m_ctrlList.GetSelectionMark(); //取得该行
//将值赋给控件变量
m_strID = m_ctrlList.GetItemText(i0);
m_strName = m_ctrlList.GetItemText(i1);
m_strSex = m_ctrlList.GetItemText(i2);
m_timeBirthday = m_pRS.StringToTime(
m_ctrlList.GetItemText(
i3)); //字符类型转化为时间类型
m_strNativePlace = m_ctrlList.GetItemText(i4);
m_strClass = m_ctrlList.GetItemText(i5);
m_strCollege = m_ctrlList.GetItemText(i6);
UpdateData(false); //将变量显示到界面上
*pResult = 0;

//当单击列表控件某一列后按列排序
vo
idCDa t
aD l
g OnCo l
umncli
ckL i
st(NMHDRpNMHDRLRESULTpRe su
lt)
{ NM_LISTVIEW*pNMListView = (NM_LISTVIEW*)pNMHDR;
* pResult = 0;
//单击表头列时排序,可有两种方法,一种是比较调整所有对应行的值冒泡排序,较为麻烦
//这里使用了获取列名并重新填充的方法
//获取表头列中的文字,由于没有直接的函数,所以需要分两步
//取得选中的列索引:Info.iSubItem行索引为:Info.iItem
CPoint pt;
::GetCursorPos(&pt);
m_ctrlList.ScreenToClient(&pt);
LVHITTESTINFO Info;
Info.pt = pt;
Info.flags = LVHT_ABOVE;
m_ctrlList.SubItemHitTest(&Info);
//取得列头中的值即:col.pszText
LVCOLUMN col;
col.mask = LVCF_TEXT;
col.cchTextMax = 20;
char buff20;
col.pszText = buff;
m_ctrlList.GetColumn(Info.iSubItem&col);
//取得值之后,为 SQL 语句增加排序条件,并重新填充
CString strSQL = m_strSQL+ " ORDER BY" +col.pszText;
m_pRS.ADOExcute(strSQL);//提取数据集
AddListItems(); //重新填充列表控件

383

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

6 学生信息数据管理
下面将对学生信息数据进行增 、删 、改 、查的操作 ,与院系数据操行的步骤相同 ,先用
ClassWizard 为 CDataDlg 的各个菜单项添加事件 。 在新增和编辑记录中使用了两种方法
实现对列表控件的刷新显示 ,一种是通过暂存的 SQL 语句重新填充数据集刷新显示列表 ,
这种方法操作简单 ,但需查询数据库;一种是直接对列表进行操作 ,更改列表中的数据 ,这
种操作不需要查询数据库 ,但在字段较多时方法略显复杂 ,读者可自行体会 。 具体实现代
码见清单 17 19。
清单17 19 学生数据库增、删、改、查的操作(在 Da
taD
lg.
cpp文件中)

oidCDa taDlg OnItemS ea
r h()
c //查找数据事件
{ UpdateData(true);
//查找指定学号的项,并放入列表框中
m_pRS.ADOExcute("SELECT * FROM StudentTab WHERE StuID = "+ m_strID+" ");
AddListItems(); //刷新列表数据
UpdateData(false);


oidCDa taDlg OnItemAdd() //清空输入框的值,准备增加记录
{ m_strID ="" ;
m_strName ="" ;
m_strSex ="" ;
m_strNativePlace ="" ;
m_strClass ="" ;
m_strCollege ="" ;
UpdateData(false);


oidCDa taDlg OnItemS ave()//保存增加的记录
{ UpdateData(true);
CString sql;
sql.Format("INSERT INTO StudentTab(StuIDStuNameCollegeIDStuNativePlace
StuClassStuSex
StuBirthday)Values(%s %s %s %s %s %s %s ) ",
m_strIDm_strNamem_strCollegem_strNativePlacem_strClassm_strSex
m_pRS.TimeToString(m_timeBirthday)); //从 sql 开始到此处为一行
if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("增加记录成功!");
//增加完记录后,马上在列表中显示;
int n = m_ctrlList.GetItemCount();
n = m_ctrlList.InsertItem(nm_strID);
m_ctrlList.SetItemText(n1m_strName);
m_ctrlList.SetItemText(n2m_strSex);
m_ctrlList.SetItemText(n3m_pRS.TimeToString(m_timeBirthday));//需类型转换
m_ctrlList.SetItemText(n4m_strNativePlace);
m_ctrlList.SetItemText(n5m_strClass);
m_ctrlList.SetItemText(n6m_strCollege);

else
 AfxMessageBox("增加记录失败,可能是重复的学号!");



oidCDa taDlg OnItemEd it()//保存编辑事件
384 { UpdateData(true);
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

CString sql;
sql.Format("UPDATE StudentTab SET StuID = %s StuName = %s CollegeID = %s 
StuNativePlace = %s 
StuClass = %s 
StuSex = %s 
StuBirthday = %s WHERE StuId = %s " 
m_strIDm_strNamem_strCollegem_strNativePlacem_strClassm_strSex
m_pRS.TimeToString(m_timeBirthday),m_strID);//此处为一行
if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("编辑记录成功!");
m_pRS.ADOExcute(m_strSQL);//编辑完记录后,用暂存的 SQL 的语句刷新显示
AddListItems();

else
 AfxMessageBox("编辑记录失败!");



oidCDataDlgOnItemDelee()
t //删除事件,由于删除操作不可恢复,因此要谨慎实现
{ if(m_ctrlList.GetSelectionMark()
<0)
{ AfxMessageBox("请先选中要删除的项!");
return;

if(AfxMessageBox("您确认要删除吗?",MB_OKCANCEL)= = IDOK)
{ UpdateData(true);
CString sql;
sql.Format("DELETE * FROM StudentTab WHERE StuID = %s " m_strID);
if(m_pRS.ADOExcuteNoQuery(sql)= = 1)
{ AfxMessageBox("删除记录成功!");
//删除列表项中造中的记录
m_ctrlList.DeleteItem(m_ctrlList.GetSelectionMark());

else
 AfxMessageBox("删除记录失败!");


 调用学生管理窗口

调用学生管理窗口与在主窗体中调用其他模块的方法差不多 ,仅仅是权限的不同,设置
2 级以上权限可管理,具体过程略,关键代码见清单 17 20。
清单17 20 调用学生管理窗口(在 ExMI
SDl
g.pp文件中)


oidCExMI SDlgOnMe nuitemS
tuden t()
{ if(PowerValidate(2))//学生管理,二级以上权限可进
{ CDataDlg dlg;//准备打开数据管理窗口
dlg.m_pRS.m_strDBType = m_pRS.m_strDBType;//将数据库类型传到数据管理窗体
dlg.DoModal();

385

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

17
46 课程成绩管理模块
由于数据管理模块间的功能和形式都差不多 ,所以这里不再详细介绍,现在只简要介绍
一下与学生数据管理区别较大的课程成绩管理模块 。 由于使用了关系模块,并且要与功能
相对较弱的 ACCESS 数据库相兼容 (无法创建储存过程与视图 ),因而数据的查询就略显复
杂。因此有时在设计数据库时也会增加一些冗余字段来简化查询操作 。

17
4 1 成绩的分类显示
6
成绩数据维护界面见图 17 12。 这里使用了按院系- > 课程- > 课程班级的分级来分类
显示分库。由于课程名称可能出现重复,所以使用 “课程编号:课程名称 ”的格式显示,需要
使用时只需做一下分离即可(以便查询使用),其关键代码见清单 17 21。

图17 12 课程成绩管理窗口

清单17 21 树型控件和列表控件显示有关成绩内容(在 S
cor
eDl
g.pp文件中)


oidCScor
eD l
g On It
emRe f
resh()
{ //利用查询新组建一个包含院系、课程、班级的表,使用了连接查询
m_pRS.ADOExcute("SELECT ClassIDCourseTab.CourseIDCourseTab.CourseName
CollegeTab.CollegeID FROM ClassTabCourseTabCollegeTab
WHERE ClassTab.CourseID = CourseTab.CourseID AND
CourseTab.CollegeID = CollegeTab.CollegeID" );//此处为一行
AddTreeNodes();
m_strSQL =" SELECT * FROM ScoreTab" ;//添加分数到列表中,并暂存 SQL 语句
m_pRS.ADOExcute(m_strSQL);
AddListItems();


oidCScor
eD l
g AddTr eeNo ds()

{ m_ctrlTree.DeleteAllItems(); //删除所有节点控件中的项目
hRootItem = m_ctrlTree.InsertItem("学生成绩基本信息",0,2,TVI_ROOTTVI_LAST);
m_pRS.MoveFirst(); //指向第一条
386 CString nodeCourse; //声明一个临时字符串变量
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

while(! m_pRS.ADOEOF())
{ //添加院系到根节点中
HTREEITEM hCollege = AddDistinctNode(hRootItemm_pRS.GetFieldString("CollegeID" ));
//添加课程名称到院系节点中, 因为可能有重复的课程名, 所有将课程编号也加到节点值中
nodeCourse = m_pRS.GetFieldString(
"CourseID" )
+ ":"+ m_pRS.GetFieldString("CourseName" )

HTREEITEM hCourse = AddDistinctNode(hCollegenodeCourse);
HTREEITEM hClass = AddDistinctNode(hCoursem_pRS.GetFieldString("ClassID" ));
m_pRS.MoveNext();

虽然图中的树形控件中的部分节点值在成绩表找不到相应字段 ,不过因为有关系的存
在,所以可以使用连接查询来实现树形控件的点击刷新 ,关键代码见清单 17 22。
清单17 22 改变树形控件选项的消息函数(在 S
cor
eDl
g.pp文件中)


oidCScoreDlg OnS e
lchan ge
dTr ee( NMHDR pNMHDRLRESULT pRe sult)
{ NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW* )pNMHDR;
* pResult = 0;
HTREEITEM hSelItem = m_ctrlTree.GetSelectedItem();//取得当前选中的节点
HTREEITEM hParentItem = m_ctrlTree.GetParentItem(hSelItem);//取得选中节点的父节点
CString strSqlstrstrCondition;//SQL 语句
strSql =" SELECT * FROM ScoreTab" ;//
m_strClassID ="" ;//清空班级,防止误操作
if(hParentItem!= NULL)
{ HTREEITEM hOldParent = m_ctrlTree.GetParentItem(hParentItem);
if(hOldParent!= NULL)
{ HTREEITEM hOlder = m_ctrlTree.GetParentItem(hOldParent);
if(hOlder!= NULL)
{ //选中的是班级节点,添加班级条件即可
strCondition.Format (" WHERE ClassID = %s " m_ctrlTree.GetItemText(hSelItem)
);
m_strClassID = m_ctrlTree.GetItemText(hSelItem);

else
 //选中的是课程节点,添加课程条件即可(选先分离出课程编号)
CString s = m_ctrlTree.GetItemText(hSelItem);//提取课程编号
int nPos;
nPos = s.Find(_T(":"),0);
if(nPos>0)
{ str = s.Mid(0,nPos);
strCondition.Format( " WHERE ClassID IN (SELECT ClassID FROM
ClassTab WHERE CourseID = %s ) ",str);//生成 SQL 语句的 Where 条件



else
 //选中的是院系节点,添加院系条件
strCondition.Format(" WHERE ClassID IN (SELECT ClassID FROM ClassTab WHERE
CourseID IN (SELECT CourseID FROM CourseTab WHERE CollegeID = %s ))",
m_ctrlTree.GetItemText(hSelItem));//院系的连接查询较复杂

387

isu
alC++编程与项目开发 ……………………………………………………………………………………………………


else
 strCondition ="" ;//选中的是根节点,没有过滤条件

UpdateData(false);
strSql+ = strCondition;//加上筛选条件,
//将 SQL 语句暂存,以供排序使用;
m_strSQL = strSql;
m_pRS.ADOExcute(strSql);//提取数据集
AddListItems();//刷新列表数件

17
4 2 成绩的多种查询
6
在学生成绩查询的界面上,提供了三种查询分数的方式,按
学号、班级号、课程号,以方便教师对学生的管理。 学号可在界面
上直接输入,但班级号和课程号却必须依靠一个弹出的对话框
(见图 17 13)来实现,具体步骤如下: 图17 13 输入参数窗口
(1)首先新建一个名为 CInputDlg 的对话框,并将出现的两
个默认的按钮上的文字分别更名为确认和取消 ,然后添加一个编辑 ID 为 IDC_EDIT_INPUT
的编辑控件。
(2)在 CInputDlg 类的头文件中添加一个公共的变量:

public: CString m_strValue; //控件中的值


(3)为 IDOK 添加消息响应,其实现代码见清单 17 23。
清单17 23 OnOK()函数(在I
npu
tDl
g.pp文件中)


oidCInputDl
g OnOK()
{ //将窗口上的变量传给 m_strValue 后返回 IDOK 的模态值。
GetDlgItem(IDC_EDIT_INPUT)->GetWindowText(m_strValue);
CDialog::OnOK();

这样就完成了参数输入对话框的制作。 下面再以按课程号查询为例,说明如何使用这
个对话框。
(4)首先在 CScoreDlg 的源文件中添加输入参数对话框的引用 。

# include" InputDlg.h"

(5)为 CScoreDlg 类的“按课程查”按钮添加消息响应,实现代码见清单 17 24。


清单17 24 “按课程查”按钮消息函数(在 S
cor
eDl
g.pp文件中)


oidCScor
eD l
g OnB t
nCo urse()
{ //弹出输入框,按输入的课程号查找所有成绩
CInputDlg dlg;
if(dlg.DoModal()= = IDOK)
388
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

{ CString course = dlg.m_strValue;


if(!course.IsEmpty()) //只有当课程号不为空时才执行查询
{ CString sql;
sql.Format("SELECT * FROM ScoreTab WHERE ClassID IN (
SELECT ClassID FROM
ClassTab WHERE CourseID = %s )",course);//此处为一行
m_pRS.ADOExcute(sql);//重新填充列表控件
AddListItems();


 成绩的统计分析

这里的成绩统计分析是针对班级的 ,其实现和算法都比 1747 的课程的成绩统计分析
要简单。不过在调用之前需要将班级编号传递过去,即在班级成绩统计分析中添加一个公
共的字符型传递变量即可,具体实现不再累述。

17
47 课程成绩统计模块
课程成绩统计模块与院系、用户管理
模块的难度相当,不过增加了很多与窗口
的数据交互传递工作。

 界面设计

图 17 14 为 课 程 成 绩 统 计 界 面 ,将
课程 成 绩 统 计 类 命 名 为: CReportDlgID
号为:IDD_ REPORTDLG_ DIALOG。 课 程 成 绩
统计模 块 的 左 侧 是 一 个 树 形 控 件 ,右 侧
是各种统计信息 ,分隔线是用 Picture 控
件制作的 ,外框是 Group Box表 17 12 是
图17 14 课程成绩统计窗口
关键控件列表 ,其中所有的 EDIT 控件都
是只读的 。
表17 12 课程成绩统计窗口控件列表

控件类型 控件 ID 控件变量 控 件 说 明
Edit Box IDC_EDIT_NAME m_strName 课程名称
Edit Box IDC_EDIT_CREDIT m_fCredit 课程学分
Edit Box IDC_EDIT_COLLEGE m_strCollege 所属院系
Edit Box IDC_EDIT_COUNT m_nCount 课程人数
Edit Box IDC_EDIT_HIGH m_fHigh 最高分
Edit Box IDC_EDIT_LOW m_fLow 最低分
Edit Box IDC_EDIT_AVG m_fAvg 平均分
Edit Box IDC_EDIT_DELTA m_fDelta 标准差
Edit Box IDC_EDIT_50 m_n50 50 分以下人数
389

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

续 表
控件类型 控件 ID 控件变量 控 件 说 明
Edit Box IDC_EDIT_R50 m_str50 50 分以下百分比
Edit Box IDC_EDIT_60 m_n60 50—59 分人数
Edit Box IDC_EDIT_R60 m_str60 50—59 分百分比
Edit Box IDC_EDIT_70 m_n70 60—69 分人数
Edit Box IDC_EDIT_R70 m_str70 60—69 分百分比
Edit Box IDC_EDIT_80 m_n80 70—79 分人数
Edit Box IDC_EDIT_R80 m_str80 70—79 分百分比
Edit Box IDC_EDIT_90 m_n90 80—89 分人数
Edit Box IDC_EDIT_R90 m_str90 80—89 分百分比
Edit Box IDC_EDIT_100 m_n100 90—100 分人数
Edit Box IDC_EDIT_R100 m_str100 90—100 分百分比
选 Styles 中:Has buttons;Has lines;
Tree Control IDC_TREE m_ctrlTree
Lines at root;

 课程分类显示

课程成绩统计模块的课程分类显示与 1746 的成绩显示模块差不多,甚至更加容易,
因为没有了班级一级的子节点,关键代码见清单 17 25。
清单17 25 课程分类显示(在 Re
por
tDl
g.pp文件)

BOOLCRe portDlgOnIni
tDialog()
{ CDialog::OnInitDialog();
//初始化树形控件,设置图标
m_ctrlTree.SetImageList(&m_TreeBootImageTVSIL_NORMAL);
m_pRS.ADOOpen();//打开数据集
m_pRS.ADOExcute("SELECT * FROM CourseTab" );//添加院系、课程到树控件中
AddTreeNodes();//增加树节点
return TRUE;


oidCReportD l
g AddTreeNo des()
{ //删除所有节点控件中的项目
m_ctrlTree.DeleteAllItems();
//添加根节点即学生表的信息
hRootItem = m_ctrlTree.InsertItem( "所有课程列表", 0,2,TVI_ROOTTVI_LAST);
m_pRS.MoveFirst();//指向第一条
while(!m_pRS.ADOEOF())
{ //添加院系到根节点中
HTREEITEM hCollege = AddDistinctNode(
hRootItemm_pRS.GetFieldString("CollegeID" )
);
//添加课程名称到院系节点中, 因为可能有重复的课程名, 所有将课程编号也加到节点值中
CString nodeCourse = m_pRS.GetFieldString("CourseID" )+ ":"+
m_pRS.GetFieldString("CourseName" );
HTREEITEM hClass = AddDistinctNode(hCollegenodeCourse);//此函数前文清单已列出
m_pRS.MoveNext();


390
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

 课程成绩统计

下面说明树形控件的单击节点事件和成绩统计函数 ,在成绩统计中要计算标准差,标准

差的数学公式为:标本标准差= ∑(样本值-样本平均值)。

样本个数-1
使用树形控件的单击节点事件来触发执行统计计算函数 ,关键代码见清单 17 26,注意
在程序开头要添加# include" Math.h" 。
清单17 26 改变树形控件选项得到课程成绩统计(在 Re
por
tDl
g.pp文件)

# include" Math.h"//引用数学函数库

oi dCRe por
tD lgOnS el
ch angedTr e(
e NMHDR pNMHDRLRESULT pRe s u
lt)
{ NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW* )pNMHDR;
* pResult = 0;
HTREEITEM hSelItem = m_ctrlTree.GetSelectedItem();//取得当前选中的节点
HTREEITEM hParentItem = m_ctrlTree.GetParentItem(hSelItem);//取得选中节点的父节点
//判断选中的是根节点,还是学院二级子节点。并取回根节点
CString strstrSqlstrCondition;//SQL 语句
//只有当选中课程节点时才开始计算
if(hParentItem!= NULL)
{ HTREEITEM hOldParent = m_ctrlTree.GetParentItem(hParentItem);
if(hOldParent!= NULL)
{ //选中的是课程节点,添加课程条件即可
//先分离出课程编号
CString s = m_ctrlTree.GetItemText(hSelItem);
int nPos;
nPos = s.Find(_T(":"),0);
if(nPos>0)
{ str = s.Mid(0,nPos);
//查询课程信息的 SQL 命令
strSql.Format("SELECT * FROM CourseTab WHERE CourseID = %s " str);
m_pRS.ADOExcute(strSql);
m_strName = m_pRS.GetFieldString("CourseName" );
m_fCredit = m_pRS.GetFieldFloat("CourseCredit" );
m_strCollege = m_pRS.GetFieldString("CollegeID" );
Calculate(str);//调用统计函数
UpdateData(false);//更新界面数据





oi dCRe por
tD lgCa l
culate(CStringc our
seID)
{ //先清空所有
m_fLow = 00f;
m_fHigh = 00f;
m_fDelta = 00f;
m_fCredit = 00f;
m_fAvg = 00f;
m_n90 = 0;
m_n80 = 0;
m_n70 = 0;
m_n60 = 0; 391

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

m_n50 = 0;
m_n100 = 0;
m_nCount = 0;
CString sql;
//先提出某门课程的平均分,使用了 SQL 中的 Avg 关键字。
sql.Format("SELECT Avg(Score)FROM ScoreTab WHERE ClassID IN (SELECT ClassID FROM
ClassTab WHERE CourseID = %s )",courseID);//此处为一行
m_pRS.ADOExcute(sql);
//因为 ACCESS 中提取的平均数是字符类型,需要转化为浮点型
m_fAvg = atof(m_pRS.GetFieldString(0));
m_fHigh = m_fAvg;//初始化最高分和最低分
m_fLow = m_fAvg;
//找出最高分、最低分,并计算出标准差
float sumscore;
sum = 0;
sql.Format("SELECT score FROM ScoreTab WHERE ClassID IN (SELECT ClassID FROM
ClassTab WHERE CourseID = %s )",courseID);//此处为一行
m_pRS.ADOExcute(sql);
m_nCount = m_pRS.nFieldRows;
while(!m_pRS.ADOEOF())
{ score = m_pRS.GetFieldFloat(0);
if(score>m_fHigh) m_fHigh = score;
if(score<m_fLow) m_fLow = score;
sum+ = (scorem_fAvg)* (scorem_fAvg); //方差的和
//各个分数段人数统计
if(score<50) m_n50+ + ;
else if(score<60) m_n60+ + ;
else if(score<70) m_n70+ + ;
else if(score<80) m_n80+ + ;
else if(score<90) m_n90+ + ;
else if(score<= 100) m_n100+ + ;
m_pRS.MoveNext();

if(m_pRS.nFieldRows>1)
{ m_fDelta = sqrt(sum/(m_pRS.nFieldRows-1));//计算标准差

else
 m_fDelta = 0;

//计算出各分数段所占人数比例,因为整型相除还是整型,所以先把其中一个转化为浮点型即可
if(m_nCount>0)
{ m_str100.Format("%31f%%" (float)m_n100/m_nCount* 100);
m_str90.Format("%31f%%" (float)m_n90/m_nCount* 100);
m_str80.Format("%31f%%" (float)m_n80/m_nCount* 100);
m_str70.Format("%31f%%" (float)m_n70/m_nCount* 100);
m_str60.Format("%31f%%" (float)m_n60/m_nCount* 100);
m_str50.Format("%31f%%" (float)m_n50/m_nCount* 100);

else
 m_str100 =" 0%" ;
m_str90 =" 0%" ;
392
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

m_str80 =" 0%" ;
m_str70 =" 0%" ;
m_str60 =" 0%" ;
m_str50 =" 0%" ;

17
48 帮助的制作
在一个软件项目中帮助文件是必不可少的 ,帮助文件一般有.chm 和.hlp 两种类型。
VC++使用的 MSDN 联机帮助,是一个非常典型的 HtmlHelp 联机帮助系统,HtmlHelp 文件的扩
展名为.chm。在VC++安装盘中的 HtmlHelp 目录下可以找到该工具的软件安装包,但在安装
VC++时 HtmlHelp Workshop 工 具 并 不 会 被 一 同 安 装。 因 此 首 先 要 在 VC++ 安 装 盘 中 的
HtmlHelp 目录下找到 Setup.exe 文件安装 HtmlHelp Workshop 工具。
另外 Microsoft Visual Studio 60 自 带 了 一 个 制 作 系 统 帮 助 文 档 的 工 具 Help
Workshop一般可以在安装目录下的 Microsoft Visual Studio 60 Tools 中找到。 用 Help
Workshop 制作的帮助文件的扩展名为.hlp并且可以将 RTF 文件制作成帮助。 因此可以先
用 WORD 将帮助写好,再另存为 RTF 文件格式。 下面主要说明用 Help Workshop 制作帮助文
件的过程。

 制作系统帮助的具体步骤

(1)新建一个帮助工程
打开主界面后,选择 File - > New新建,弹出如图 17 15 所示
的界面,选择 Help Project 新建一个帮助工程,按 OK 按钮,然后在
另存为对话框中输入工程保存的目录 ,保存文件名为 Help。
图17 15 选择帮助类型
(2)导入 RTF 帮助文件
接下来弹出如图 17 16 所示的窗口。单击 Fiels 按钮,将准备要制作成帮助的 RTF 文
件导入工程。如图 17 17 所示,单点 ADD 按钮以后,在文件对话框中选择要导入的文件,
然后单击 OK 即可。

图17 16 工程主窗口 图17 17 导入文件窗口

(3)设定帮助窗口的属性 393

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

单击工程主窗口上的 Window 按钮,建立一个窗口名为 Main基于 Procedure 的标准窗


口即可。在弹出的窗口中按图 17 18 设定属性,然后单击确定。
这样就完成了一个简单帮助文档的制作,单击主窗口下方的 Save and Compile 按钮就
会生成一个名为 help.hlp 的帮助文件,帮助界面如图 17 19 所示。

图17 18 设计属性窗口 图17 19 制作完成的帮助预览

 调用帮助及附属工具

帮助的调用和计算器/记事本的调用一样,我们分别使用了两种方式实现,读者可比较
二者的区别。首先将 help.hlp 文件拷贝到项目的文件夹中,然后编写在主界面上调用帮助
文件及计算器的消息函数,见清单 17 27。
清单17 27 帮助及附属工具(计算器)的调用(在 ExMI
SDl
g.pp文件)


oidCExMI SDlgOnMe nuitemHelp()//调帮助文档
{ ShellExecute(NULLNULL_T("help.hlp" ),NULLNULLSW_SHOW);


oidCExMI SDlgOnMe nuitemCal()
{ //调用计算器
WinExec("calc.exe" SW_SHOW);

5 项目包装和项目打包
17
17
51 项目包装
 图标的改变

AppWizard 会为用户生成一个默认的 MFC 图标。如何修改此图标呢?
在 ResourceView 中,单击 Icon 项左边的“+ ”号,然后将 AppWizard 为程序生成的 IDR_
394 MAINFRAME 删除,再插入用户所选择的新图标,该图标的 ID 号仍然设置为 IDR_MAINFRAME绘
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

制新图标即可。

 启动闪屏的制作

为应用程序添加闪屏启动封面可选
择菜 单 Project - > Add To Project - >
Components and Controls这 时 弹 出
Components and Controls Gallery 对话
框。 双 击 其 中 的 Develper Studio
Components 文件夹,选择 SplashScreen
组件,单击 Insert 按钮即可。 如图 17
20 所示。
这 时,VC++ 会 为 应 用 程 序 添 加
CSplahWnd 类,该类专门用于管理闪屏。
同时还 会 向 应 用 程 序 中 插 入 ID 号 为
IDB_SPLASH 的位图 (在项目目录下自动
图17 20 插入 Sp
las
hSc
ren组件

生成了 Splh16.bmp 位图文件 ),以作为
闪屏窗口的显示封面。用户可以编辑 IDB_SPLASH 位图,也可用其他专用图形处理软件设计
好一幅图形,将文件名保存为 Splsh16.bmp然后将其拷贝到应用程序目录下以替换原来的
Splsh16.bmp 文件。本项目没有将闪屏加入,读者可自行加入。

17
52 项目打包
项目开发后要进行打包和发布 (也可以说是安装盘的制作 ),VC++自带 InstallShield
for VC(60)来打包并发布程序。InstallShield 其实是一款第三方工具,以其功能强大、灵
活性好、容易扩展和强大的网络支持而著称 。这个软件一般放在 VC 安装盘的 ISHIELD 目录
下,直接安装即可使用。下面说明安装盘的制作。

 建立安装项目

(1)打开的 InstallShield 启动画面如图 17
21 所示,右击鼠标,然后点击 Project Wizard 即可新
建一个安装项目。
(2)接着出现如图 17 22 所示的 Welcome 向导
页,按要求输入应用程序的一些基本信息。 这里输
入的内容也可以通过工作区窗口进行修改,单击下
一步。
(3)接下来是 Choose Dialogs 向导页。 用来让
用户从列表中选择将要在安装过程中 出 现 的 对 话
图17 21 I
nst
all
Shi
eld启动界面
框。单击向导页的左下角的 Preview 按钮可预览该
对话框,选择默认设置(图略),然后单击下一步。
(4)出现如图 17 23 所示的 Choose Target Platforms 向导页。 该向导页让用户选择
一个或多个操作系统平台,用鼠标全面选中,然后单击下一步。 395

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图17 22 We
lcome界面 图17 23 选择平台界面

(5)出现 Specify Languages 向导页。 该向导页指出了安装程序所支持的语系。 点击


English然后单击下一步。
(6)接着出现的是 Specify Setup Types 向导页。在其中设置一个或多个应用程序的安
装类型,常用的有 Compact 紧凑型、Typical 典型和 Custom 自定义类型等,使用默认,然后单
击下一步。
(7)出现如图 17 24 所示的 Specify Components 向导页和图 17 25 所示 Specify File
Groups 向导页,分别用来设置安装工程中的组件和文件组。 可以通过 Add/Delete 按钮添
加/删除组件或文件组,在此仅保留 Program Files 和 Help FilesSpecify File Groups 向导
页中也做同样的设置。

图17 24 指定组件画面 图17 25 指定文件组

(8)最后一步将出现 Summary 向导页,该向导页显示了用户在前面的向导过程中所选定


的信息摘要,单击完成即可。至此已经完成了一个安装项目的制作 。

2 完善安装工程
完成项目创建的 InstallShield 的工程界面如图 17 26 所示。 框架左侧为工程工作
区,由 7 个选项卡构成。右侧显示的视图与工作区中当前选中的选项卡有关。 下方则是信
息输出区。
396 下面主要对工程工作区中的选项卡进行介绍 。
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

Scripts 页: 该 选 项 卡 页 面 用 来
管理安装程序的脚本文件。
Components 页:管理安装程序的
各组件项。
Setup Types 页:用来管理提供给
用户的安装类型。
Setup Files 页:该页面用来管理
在安装过程中将要用到的安装文件 。
File Groups 页:负责管理安装工
程所需要的文件组。
Resources 页: 用 来 管 理 安 装 工
17 26 I
nst
all
Shi
eld工程界面
程所需要的安装资源。
Media 页:用来管理安装程序的发布媒介。
下面介绍如何修改安装工程以便完成安装 。
(1)为安装工程添加文件
切换到 File Groups 页,如图 17 27 所示。 在 File Groups 中添加文件,在右侧空白区
域点击鼠标右键然后选 Insert Files  即可,如图所示将主程序和数据库添加到 Program
Executable Files 中,将帮助添加到 Help Files 中。

图17 27 F
ileGr
o s选项卡
up

(2)切换到 Components 选项卡(如图 17 28 所示)。


在选中某组件项后,右侧列表视图将显示该组件项 的 相 关 属 性,双 击 Include File
Groups 项(右侧最后一项),将弹出 Properties 属性页,点击窗口上的 ADD 按钮后将弹出 Add
File Group 对话框,分别为 Help 和 Program 添加同名的文件组即可。 组件项的 Overwrite
属性也是比较关键的,它决定了在安装文件时的覆盖属性。 缺省设置 ALWAYSOVERWRITE 将
始终用安装文件去覆盖安装目录中已存在的文件 。如果是 Shared DLLs 组件项,建议修改成
对版本进行区分覆盖,否则有可能会影响其他程序的运行 。这里使用了默认。
(3)制作快捷方式
397

isu
alC++编程与项目开发 ……………………………………………………………………………………………………

图17 28 Comp
one
nts选项卡

切换到如图 17 29 所示的 Resources 选项页。 先在 Programs 上右击鼠标,选 NEW - >


Folder 添加一个“华东理工大学”的文件夹,然后用同样的方法,在该文件夹上右击鼠标,选
NEW - > Shortcut新建两个快捷方式。 然后单击右栏中的 Target 属性,将弹出如图 17 29
所示的 Shortcut Properties 快捷方式设置窗口,在 Target 中输入:<TARGETDIR>\ExMIS.EXE
即代表指向主程序的一个快捷方式 ,< TARGETDIR > 即程序安装的文件夹,用同样的方式,再
制作帮助的快捷方式。

图17 29 添加快捷方式

(4)发布制作好的安装工程
398
第十七章 项目开发实例———学生管理信息系统
……………………………………………………………………………………………………………………………………………

切换到 Media 选项卡,右击鼠标单击 Media Build Wizard然后输入一个新的媒体名称


如 New Media”。在进行到“Disk Type”一项时(见图 17 30),选择 CD-ROM 介质,其他选默认

即可,这 样 就 完 成 了 一 个 安 装 工 程 的 发 布,发 布 好 的 文 件 会 自 动 存 放 在 C:\ My
Installations 目录中。图 17 31 即是打包完成的安装文件,双击 SETUP 即可安装。

图17 30 选择媒体类型 图17 31 打包好的安装文件

399
参 考 文 献

[1 ]官章全,刘加明.Visual C++60 类库大全.北京:电子工业出版社,2000.


[2 ]Eugene OlafsenKenn ScribnerK. David White.MFC Visual C++6 编程技术内幕.王
建华等译.北京:机械工业出版社,2000.
[3 ]木林森,高峰霞.Visual C++60 使用与开发.北京:清华大学出版社,1998.
[4 ]武装,张碧霞.Visual C++60 项目开发指南.北京:国防工业出版社,2000.
[5 ]李鑫,白雪.Visual C++60 编程基础与范例.北京:电子工业出版社,2000.
[6 ]官章全,唐晓卫.Visual C++60 编程实例详解.北京:电子工业出版社,2000.
[7 ]张志学,等.Visual C++项目开发指南.北京:清华大学出版社,2000.
[8 ]唐克.MFC 程序设计 Using Visual C++60.北京:北京希望电子出版社,2002.
[9 ] Kate Gregory. Visual C++ 6 开 发 使 用 手 册. 前 导 工 作 室 译. 北 京: 机 械 工 业 出 版
社,1999.
[10]求实科技.Visual C++60 数据库技术与工程实践.北京:人民邮电出版社,2004.
[11]郎锐,罗发根.Visual C++网络通信程序开发指南.北京:机械工业出版社,2004.
[12]邓人杰.软件工程.北京:清华大学出版社,1999.
[13]赛奎春,等.Visual C++工程应用与项目实践.北京:机械工业出版社,2005.
[14]黄明,等.Visual C++信息系统设计与开发实例.北京:机械工业出版社,2005.

400

You might also like