趣味程序导学 (Visual C++)
 7900643079, 9787900643070

Table of contents :
趣味程序导学Visual C++......Page 1
版权页......Page 2
目录......Page 3
丛书总序......Page 8
1.2 C++的新特性......Page 10
1.3 面向对象简介......Page 12
1.4 VC++集成开发环境简介......Page 21
1.5 创建第一个工程......Page 23
1.6 运行工程文件......Page 29
1.7 Microsoft基本类库与应用程序框架......Page 32
1.8 本章知识点回顾......Page 36
2.1 “幸运52”游戏简介......Page 38
2.2 设计初始界面......Page 40
2.3 编写程序代码......Page 44
2.4 完善游戏界面......Page 49
2.5 本章知识点回顾......Page 54
第3章 “速算24”游戏......Page 56
3.1 设计初始界面......Page 57
3.2 编写程序代码......Page 58
3.3 完善游戏界面......Page 67
3.4 本章知识点回顾
......Page 68
4.2 创建初始界面......Page 70
4.3 位图的读入和显示......Page 73
4. 4 用Static控件显示位图......Page 80
4.5 图格的移动......Page 91
4.6 游戏的启动代码......Page 98
4.7 游戏完成条件的判断......Page 100
4.8 游戏的进一步完善......Page 102
4.9 本章知识点回顾......Page 111
5.1 程序效果说明......Page 115
5.2 创建初始界面程序......Page 116
5.3 媒体播放类的创建......Page 121
5.4 MIDI文件的播放和控制......Page 126
5.5 Wave文件的播放和控制......Page 137
5.6 CD的播放和控制......Page 140
5.7 AVI文件的播放和控制......Page 142
5.8 其他媒体文件简介......Page 144
5.9 媒体播放类的使用......Page 145
5.10 音响效果显示和音量控制
......Page 149
5.11 用ActiveMovie控件制作媒体播放器......Page 157
5.12 DirectSound简介......Page 160
5.13 本章知识点回顾......Page 161
6.1 系统使用说明......Page 165
6.2 数据库基础知识......Page 166
6.3 使用Micosoft Access创建数据库......Page 167
6.4 VC与数据库接口......Page 171
6.5 记录集操作......Page 181
6.6 MFC基本控件消息响应与系统完善......Page 189
6.7 主要部分源代码......Page 200
6.8 本章知识点回顾......Page 203
7.1 游戏效果说明......Page 205
7.2 创建界面的主框架......Page 206
7.3 显示背景......Page 213
7.4 方块的显示和控制......Page 227
7.5 显示成绩和排名......Page 238
7.6 制作图形的按钮......Page 242
7.7 数字的特殊效果显示......Page 250
7.8 用ActiveX美化界面......Page 254
7.9 游动字幕About Box和说明的制作......Page 257
7.10 本章知识点回顾
......Page 266
8.1 程序效果说明......Page 268
8.2 生成动态链接库(DLL)......Page 269
8.3 创建基于TCP协议的Socket类......Page 271
8.4 两人聊天的OICQ......Page 289
8.5 本章知识点回顾......Page 296

Citation preview

 

科海电脑技术丛书

趣味程序导学 Visual C++ 董未名





编著

清 华 大 学 出 版 社  http://www.tup.tsinghua.edu.cn

 

(京)新登字 158 号 内 容 提  要 本书通过编写趣味游戏程序来引导读者学习 Visual C++编程的方法和技巧,形式 新颖活泼,别具一格。 全书从 Visual C++语言基础知识和编制简单的程序入手,将 Visual C++编程的知识 点有机地分散在“幸运 52”,“速算 24”,“俄罗斯方块” , “拚图游戏”,属于你的 OICQ 等多个趣味游戏的程序设计示例中,引导读者轻松学习 Visual C++编程的相关知识、编 程技术及技巧,其中包括 Visual C++中消息处理、多媒体、图形图像、数据库处理以及 网络编程等内容。 本书以示例教学方式来组织内容,集趣味性、直观性和可操作性于一体,适用于 Visual C++初学者及对游戏程序感兴趣的电脑爱好者。

版权所有,盗版必究。 本书封面贴有清华大学出版社激光防伪标签,无标签者不得销售。



名:趣味程序导学 Visual C++



者:董未名





出版者:清华大学出版社(北京清华大学校内,邮编 100084) 印刷者:北京市耀华印刷有限公司(原门头沟胶印厂) 发行者:新华书店总店北京科技发行所 开

本:787×1092 1/16



次:2002 年 2 月第 1 版



数:0001~5000



号:ISBN 7-0



价:00.00 元

印张:00.00

字数:000 千字

2002 年 2 月第 1 次印刷

目 第1章



初识Visual C++.................................................................................................. 1

1.1

什么是Visual C++ ...................................................................................................................................1

1.2

C++的新特性 ..........................................................................................................................................1

1.3

面向对象简介..........................................................................................................................................3 1.3.1

基本概念.........................................................................................................................................3

1.3.2. 继承和多态....................................................................................................................................10 1.4 VC++集成开发环境简介......................................................................................................................12 1.4.1

AppWizard工具............................................................................................................................12

1.4.2

工程和工程工作区.......................................................................................................................12

1.4.3 Class Wizard工具..........................................................................................................................13 1.4.4 Wizard Bar工具栏 ........................................................................................................................14 1.5

1.6

创建第一个工程....................................................................................................................................14 1.5.1

生成一个基于文本框的工程.......................................................................................................14

1.5.2

生成一个基于对话框的工程文件...............................................................................................18

运行工程文件........................................................................................................................................20 1.6.1

基于文本框的程序.......................................................................................................................20

1.6.2

基于对话框的程序.......................................................................................................................22

1.7 Microsoft 基本类库与应用程序框架....................................................................................................23 1.7.1

什么是Application Framework.....................................................................................................23

1.7.2

为什么要用Application Framework.............................................................................................24

1.7.3 Microsoft Foundation Class(MFC)与VC++.................................................................................24

1.8

第2章 2.1 2.2

1.7.4

纵观MFC.......................................................................................................................................24

1.7.5

怎样才能学好MFC.......................................................................................................................24

1.7.6

用Application Wizard生成的程序的结构 ...................................................................................26

本章知识点回顾....................................................................................................................................27

“幸运52”游戏— — Visual C++ 初步应用....................................................... 29 “幸运52”游戏简介............................................................................................................................29 设计初始界面........................................................................................................................................31 2.2.1

生成源代码基本框架...................................................................................................................31

2.2.2

添加控件并设置其属性...............................................................................................................31

2.2.3

生成管理对话框的类,定义成员变量.......................................................................................34

2.2.3

定义消息处理函数.......................................................................................................................35

2.2.4

引入图片资源...............................................................................................................................35



II



2.3

编写程序代码........................................................................................................................................35

2.4

完善游戏界面........................................................................................................................................40

2.5

第3章 3.1

2.4.1

焦点控制:SetFocus方法 ............................................................................................................40

2.4.2

对用户的意外操作进行响应.......................................................................................................42

本章知识点回顾....................................................................................................................................45

“速算24”游戏............................................................................................... 46 设计初始界面........................................................................................................................................47 3.1.1

生成基本框架源代码...................................................................................................................47

3.1.2

生成管理对话框的类,定义成员变量.......................................................................................48

3.1.3

定义消息处理函数.......................................................................................................................48

3.1.4

引入图片资源...............................................................................................................................48

3.2

编写程序代码........................................................................................................................................48

3.3

完善游戏界面........................................................................................................................................56

3.4

3.3.1

不同时期在按钮上显示不同文字...............................................................................................56

3.3.2

增加计时功能...............................................................................................................................57

本章知识点回顾....................................................................................................................................58

第4章  拼图游戏——Visual C++位图操作.................................................................... 59 4.1

游戏效果说明........................................................................................................................................59

4.2

创建初始界面........................................................................................................................................59

4.3

位图的读入和显示................................................................................................................................62 4.3.1 Windows位图的基本结构............................................................................................................62

4.4

4.3.2

位图资源的读入...........................................................................................................................64

4.3.3

自定义位图文件的读入...............................................................................................................66

用Static控件显示位图...........................................................................................................................69 4.4.1

设置Static控件的初始位置..........................................................................................................69

4.4.2

图格的显示...................................................................................................................................74

4.5

图格的移动............................................................................................................................................80

4.6

游戏的启动代码....................................................................................................................................87

4.7

游戏完成条件的判断............................................................................................................................88

4.8

4.9

第5章

游戏的进一步完善................................................................................................................................91 4.8.1

添加帮助画面...............................................................................................................................91

4.8.2

用Status Bar显示提示信息 ..........................................................................................................94

4.8.3

游戏计时器的加入.......................................................................................................................98

本章知识点回顾....................................................................................................................................99

媒体播放器——多媒体程序设计..................................................................... 103

5.1

程序效果说明......................................................................................................................................103

5.2

创建初始界面程序..............................................................................................................................104





III

5.2.1

在按钮上显示位图.....................................................................................................................105

5.2.2

菜单项位图的显示.....................................................................................................................107

5.2.3

对话框背景图的添加.................................................................................................................108

媒体播放类的创建..............................................................................................................................109

5.3

5.3.1

高级音频函数.............................................................................................................................109

5.3.2 Windows MCI与多媒体软件开发.............................................................................................111 5.4

MIDI文件的播放和控制.....................................................................................................................114 5.4.1

MIDI简介....................................................................................................................................114

5.4.2

MIDI文件格式............................................................................................................................115

5.4.3

MIDI文件的播放........................................................................................................................116

5.5 Wave文件的播放和控制 ....................................................................................................................125 5.5.1 Wave文件格式简介....................................................................................................................125 5.5.2 Wave文件的播放和录音............................................................................................................127 5.6

CD的播放和控制................................................................................................................................128

5.7 AVI文件的播放和控制.......................................................................................................................130 5.7.1 AVI数字视频的格式..................................................................................................................130 5.7.2

AVI数字视频的特点..................................................................................................................131

5.7.3 AVI文件的播放..........................................................................................................................132 5.8

其他媒体文件简介..............................................................................................................................132

5.9

媒体播放类的使用..............................................................................................................................133

5.10

音响效果显示和音量控制................................................................................................................137

5.10.1

音响效果的显示.......................................................................................................................137

5.10.2

音量的控制...............................................................................................................................143

5.11

用ActiveMovie控件制作媒体播放器...............................................................................................145

5.11.1

建立工程...................................................................................................................................146

5.11.2

添加代码...................................................................................................................................146

5.12 DirectSound简介 ...............................................................................................................................148 5.13

第6章

本章知识点回顾................................................................................................................................149

北京市公交查询系统——数据库编程基础....................................................... 153

6.1

系统使用说明......................................................................................................................................153

6.2

数据库基础知识..................................................................................................................................154 6.2.1

6.3

简介.............................................................................................................................................154

使用Micosoft Access创建数据库.......................................................................................................155 6.3.1

初识Access..................................................................................................................................156

6.3.2

选择关系并定义字段.................................................................................................................157

6.3.3

添加数据.....................................................................................................................................158

6.4 VC与数据库接口 ................................................................................................................................159 6.4.1

用户DSN设置.............................................................................................................................159



IV



6.4.2

ODBC标准..................................................................................................................................162

6.4.3

接口实现.....................................................................................................................................163

记录集操作..........................................................................................................................................169

6.5

6.5.1

使用ODBC记录集......................................................................................................................169

6.5.2

用SELECT打开一个ODBC记录集...........................................................................................174

6.6

MFC基本控件消息响应与系统完善.................................................................................................177 6.6.1

在组合框内选择车次并显示路线信息.....................................................................................177

6.6.2

在编辑框内输入需要查询的车站并显示路线信息.................................................................184

6.6.3

完善界面.....................................................................................................................................187

6.6.4

其他.............................................................................................................................................188

6.7

主要部分源代码..................................................................................................................................188

6.8

本章知识点回顾..................................................................................................................................191

第7章

俄罗斯方块游戏——Visual C++应用深入 ....................................................... 193

7.1

游戏效果说明......................................................................................................................................193

7.2

创建界面的主框架..............................................................................................................................194 7.2.1

用ClassWizard生成CPropertySheet ...........................................................................................195

7.2.2 CPropertySheet类成员................................................................................................................196 7.2.3

成员函数.....................................................................................................................................197

显示背景..............................................................................................................................................201

7.3

方块的显示和控制..............................................................................................................................215

7.4

7.4.1

显示窗口.....................................................................................................................................215

7.4.2

定义方块的数据结构.................................................................................................................216

7.4.3

方块的显示.................................................................................................................................222

7.4.4

截获键盘操作.............................................................................................................................223

7.4.5

计时器.........................................................................................................................................225

7.5

显示成绩和排名..................................................................................................................................226

7.6

制作图形的按钮..................................................................................................................................230

7.7

数字的特殊效果显示..........................................................................................................................238

7.8

用ActiveX美化界面............................................................................................................................242

7.9

游动字幕About Box和说明的制作....................................................................................................245

7.10

本章知识点回顾................................................................................................................................254

第8章

属于你的OICQ— — Visual C++ 网络编程 ....................................................... 256

8.1

程序效果说明......................................................................................................................................256

8.2

生成动态链接库(DLL)..................................................................................................................257

8.3

创建基于TCP协议的Socket类 ...........................................................................................................259 8.3.1 WinSock介绍 ..............................................................................................................................259 8.3.2

在DLL中添加CTCPSocket类 ....................................................................................................264

8.3.3

成员变量及其说明.....................................................................................................................264



8.4

8.5



V

8.3.4

成员函数及其说明.....................................................................................................................266

8.3.5

建立连接.....................................................................................................................................267

8.3.6

连接方连接函数.........................................................................................................................274

两人聊天的OICQ................................................................................................................................277 8.4.1

用AppWizard建立工程..............................................................................................................277

8.4.2

生成用户界面.............................................................................................................................279

8.4.3

加入所需变量.............................................................................................................................280

8.4.4

编写初始化函数.........................................................................................................................281

8.4.5

进行函数映射.............................................................................................................................281

本章知识点回顾..................................................................................................................................284

丛 书 总 序 电脑游戏 “我喜欢游戏!” “游戏是我生命中的一部分” “我是游戏的一部分” 这是许多玩家从开始玩电脑游戏,到喜欢,直到痴迷的三段自我写照。 当计算机技术给游戏提供了强有力的支持后,一个陌生而又似曾相识的新奇世界展示在 人们面前:这里有逝去的童年梦想,有心头压抑已久的情感,有疯狂、神秘,有脑力和技巧 的挑战,也有可以轻松获得的志得意满的“虚拟”成就感。游戏里有一个别样的人生,有一 个神奇的世界。 娱乐、游戏是人的天性。无论关于游戏的各种观点怎样碰撞,年轻一代对电脑游戏的痴 迷已经无法逆转。在不久的将来,我们将面对“玩游戏长大的一代”,甚至人们的思维方式 也将受到游戏的很大影响。 程序设计 Java,JavaScript,Delphi,VB,VC,C++Builder……窗口,图形界面,事件驱动,数 据库,多媒体,网络编程……当我们编写的代码通过编译运行(或解释执行)产生奇妙的动 态效果,当我们成功地编写了一个窗口程序,当我们亲自编写了一个哪怕是很粗糙的聊天工 具,那一刻的成功、喜悦、振奋和激动都会让人无以言表。 计算机程序设计给我们带来了另一个精彩的别样世界。掌握和使用新的程序设计语言, 学习和操作新的程序设计工具,认识和思考新的“信息世界”,不断吸收信息新知,是信息 时代弄潮儿永远不知疲倦的一件赏心乐事。 熟悉一些流行的程序开发工具,掌握一定的程序设计方法,已经成为年轻一代所必须的 素质,也是时代的要求。也许你还是一名中学生,也许你是一名大学生,或许你已经就业工 作,作为一个跨世纪的现代人、21世纪的主人翁,我们有必要了解、掌握、驾驭一定的程序 设计工具和程序设计语言。 通过趣味游戏程序学习程序设计 学习程序设计,并不是一件艰苦、枯燥的事情,它能像电脑游戏那样让你充满好奇、富 有乐趣。这正是本丛书的编写目的! 本丛书面向初、中级用户,精选了目前全球最流行、最常用的程序设计语言和程序开发 工具,通过趣味游戏示例,以目标式教学为主,引导读者学习、掌握程序设计思想和编程技 巧。 本丛书努力做到如下几点: ・ 趣味性:以趣味游戏程序为例,形式新颖活泼,读者在学习的过程中能自己动手设 计电脑游戏,感受学习的乐趣,保持学习的兴趣。本丛书均带有光盘,在光盘中给 出了全部示例的源代码和各种资源文件,读者可以分析、参考和学习。 ・ 直观性:将程序设计的知识点有机地分散在多个趣味游戏的设计示例中,使得程序 设计语言众多的对象、属性、方法以及程序开发工具的各种设置和操作都变得具体、

形象、直观,通俗易懂,深入浅出。 ・ 可操作性:以示例教学、目标式学习来组织内容,将程序设计的思路、操作步骤、 知识点和方法的讲解紧密结合,互相映证。本套丛书力求做到结构明晰,容易理解, 便于操作,读者可以跟随书本,一边思考、体会程序设计的思路,一边一步步进行 实际的操作,并及时从操作情况和程序执行的效果中得到反馈,带着目的学习,带 着问题学习,有的放矢,从实际的操作、具体的设计中体会、领悟、积累程序设计 的知识、技能和经验,这将极大地提高学习效率,达到更好的学习效果。 ・ 循序渐进:本丛书尤其注意由浅入深,循序渐进,让读者的学习是一个轻松渐进、 平衡上升的过程。每本书首先都从基础讲起,读者在一开始可以是一个完全的门外 汉;随着学习的深入,将被一步步领进门,登堂入室,渐入佳境,最后从入门达到 提高的目的。 我们将电脑游戏和程序设计这两个精彩世界有机地嫁接在一起,希望读者能在充满趣味 的编程过程中,掌握程序设计语言,领悟程序设计的方法和技巧。 学习建议 本丛书以示例为主,注重操作性,将程序设计各方面的知识点有机地分散在各游戏的设 计步骤中,在使用本书时,最好使用如下方法: (1)在实际的操作中学习 本丛书实战性非常强,读者最好一边阅读,一边上机,两者紧密结合。一定要亲自动手, 体会实际的操作过程,查看程序运行的效果反馈,并及时思考、总结。每学完一章,我们应 该有自己的收获,动手编制出自己的游戏作品,同时理解、掌握程序设计过程中所用到的知 识和技能。 (2)发挥主观能动性,积极思考 本书循序渐进,每个游戏侧重于程序设计的一个方面,在实际的设计过程中,又分为很 多步骤。在每一步,读者应充分发挥自己的主观能动性,积极思考,尽量先有自己的思路, 甚至给出自己的解决方法,然后再看书中的实现方法,并进行分析和比较,深入理解程序设 计的精髓。 (3)借助于网络结成学习共同体 21世纪是一个信息社会,学习者不再是封闭、孤立的个体,而应该尽量借助网络来和其 他学习者、专家进行沟通、协作,以积极寻求帮助和互助,提高学习效率。 本套丛书由北京高校计算机图书创作联盟策划、创作和编写。联盟主要由清华大学、北 京大学等高校的研究生组成,成员有很强的计算机技术背景和丰富的实践经验。以团队协作、 大胆创新的精神为宗旨,以认真负责、严谨细致的态度,努力创作真正切合广大电脑应用学 习者需要的计算机精品图书。 最后,感谢科海培训中心夏非彼老师对联盟的关心。从联盟的最初构想、初创时起,夏 老师就给予了积极的支持、热心的帮助和非常有价值的指导。感谢科海培训中心张红编辑对 本套丛书提出的修改意见和建议,使我们的工作能够得以顺利地进展。

北京高校计算机图书创作联盟  2001年12月 

第1章

初识 Visual C++

本章我们将简单介绍Visual C++,让读者对它有一个初步认识,为以后进一步学习和 使用Visual C++打下良好的基础。本章是全书的基础,所介绍的内容比较多,但都是学习 Visual C++编程所必备的知识。

1.1

什么是Visual C++

Visual C++是指由Microsoft公司开发的可视化集成编程软件Microsoft Visual Studio成 员之一,目前最高版本是6.0。Microsoft Visual C++以C++语言为基础,并结合MFC进行编 程。 长期以来Microsoft Windows操作系统一直占据着个人计算机操作系统的主导地位,因 此Microsoft Visual C++受到越来越多的编程爱好者的青睐。

1.2

C++的新特性

Visual C++是以C++语言为基础的,很多读者可能都学过C语言,但是对C++并不是很 熟悉。下面我们简单介绍C++的新特性。 1. 注释语句:除了可以用/*和*/外,行注释还可以用//。 2. 声明语句:在C中变量的声明只能在程序块开头,但是在C++中,局部变量的声明 可以放在程序中的任何位置,只要是变量的首次声明即可。 3. 作用域操作符(∷):在C中作用域内的变量将覆盖同名的作用域外的变量,但是 在C++中在也可以访问同名的作用域外的变量,只要加上作用域操作符∷即可。例 如: double a;//全局变量a void main() { int a ; //局部变量a a=5; //局部变量赋值 ∷a=10 //全局变量赋值 }

4. 默认参数值:C++在定义函数时可以定义一些参数的默认值来简化编程。例如下面

2

Visual C++趣味程序导学

代码行: void ShowMessage (char *Text,int Length = -1,int Color = 0)

中就定义了参数Length,Color的默认值。 5. 引用类型:声明为引用的变量是另一变量的别名。可用&操作符声明引用,例如: int count = 0; int &rencount = count;

在这段代码中rencount声明为int型引用,并初始化为int型变量count,这个定义使 rencount成为count的别名,即rencount和count指向同一内存地址。 6. 函数和引用:引用类型也可以用于函数,例如如下代码: FuncA(int &parm) { ++parm; } FuncB(int parm) { ++parm; } void main() { int N = 0; FuncA(N); //N equals 1; FuncB(N); //N still equals 1 }

函数A中的变量parm是int型引用,所以函数A中的语句++parm将修改实际变量N的 值,因为其实parm只是N的一个别名,而在函数B中参数parm只是一个新创建的内 部变量,由函数将变量N的值传给它,所以++parm语句并不修改实际变量N的值。 7. 常量:C++中可以用const定义常量,例如: const int a = 100;

8.new和delete操作符:在C++中可以用new来为一个变量分配内存空间,用delete来释 放一个不再使用的变量的内存空间。 9. 面向对象机制:C++是既面向过程又面向对象的编程语言,所以C++具有所有面向 对象语言的特性。

第1章

1.3 1.3.1

初识 Visual C++

3

面向对象简介

基本概念

下面我们介绍类、对象、构造函数、析构函数等基本概念。 1. 类和对象 在介绍C++中的类之前,先介绍C语言中的结构(struct),因为C++中的类是从C语言 中的结构演化而来的。在C语言中,可以如下所示定义一个结构: struct TPoint { int x; int y; }

上述代码中struct TPoint是一种自定义数据类型。事实上,它和其他的数据类型一样, 现在编译器并没有给它分配任何空间,它仅仅是一种与int、long等类似的数据类型,可以 用它来定义数据实例。例如: struct TPoint pLeftTop={0,0}; struct TPoint pBottomRight={800,600};

如果使用typedef,那么代码就会简单明了: typedef struct TPoint { int x; int y; }POINT;

这时可以使用POINT来代替struct TPoint。例如: POINT pLeftTop={0,0}; POINT pBottomRight={800,600};

pLeftTop,pBottomRight称为实例(instance),它们相当于C++中的对象,编译时编译 器会给它们分配一定的内存空间。 在C++中用类和对象(object)来代替上面的结构和实例。请看下面的例子: class TPoint { public; int x; int y; };

4

Visual C++趣味程序导学

可以用下面的代码来创建和使用对象: TPoint pl;//没有初始化 pl.x=0; pl.y=0; coutGetSubMenu(0); pSubMenu->CheckMenuItem(ID_HARD,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_EASY,MF_CHECKED); Easy=TRUE;IsRnd=FALSE; count=0; CanCount=FALSE; SetPos(); IsWin(); m_wndStatusBar.SetText("加油!",0,0); } void CPictureDlg::OnHard() { pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->CheckMenuItem(ID_EASY,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_HARD,MF_CHECKED); Easy=FALSE;IsRnd=FALSE; count=0; CanCount=FALSE; SetPos(); IsWin(); m_wndStatusBar.SetText("有点难度,呵呵",0,0); } BOOL CPictureDlg::IsWin() //判断是否完成的函数 { WINDOWPLACEMENT wp; INT con,move; INT win=0; if(IsLong) move=70; else move=0; if(Easy==TRUE) con=2; else if(!Easy) con=3; for(int a=0;a m_nUpper) nPos = m_nUpper; if (nPos < m_nLower) nPos = m_nLower; UINT nOld = m_nPos; m_nPos = nPos; DrawSpike(); Invalidate(); return nOld; } void CHistogramCtrl::DrawSpike() { //CClientDC dc(this); UINT nRange = m_nUpper - m_nLower; CRect rcClient; GetClientRect(rcClient); if (m_MemDC.GetSafeHdc() != NULL) { m_MemDC.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &m_MemDC, 4, 0, SRCCOPY); CRect rcTop(rcClient.right - 4, 0, rcClient.right - 2, rcClient.bottom); rcTop.top = (long) (((float) (m_nPos - m_nLower) / nRange) * rcClient.Height()); rcTop.top = rcClient.bottom - rcTop.top; // draw scale CRect rcRight = rcClient; rcRight.left = rcRight.right - 4; m_MemDC.SetBkColor(RGB(0,0,0)); CBrush bkBrush(HS_HORIZONTAL,RGB(0,128,0)); m_MemDC.FillRect(rcRight,&bkBrush); // draw current spike CBrush brush(RGB(0,255,0)); m_MemDC.FillRect(rcTop, &brush);

第5章

媒体播放器——多媒体程序设计

141

} }

CHistogramCtrl类的完整声明如下: class CHistogramCtrl : public CWnd { // Construction public: CHistogramCtrl(); UINT m_nVertical; // Attributes public: UINT SetPos(UINT nPos); void SetRange(UINT nLower, UINT nUpper); void InvalidateCtrl(); void DrawSpike(); // Operations public: void StepIt(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CHistogramCtrl) public: virtual BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL); //}}AFX_VIRTUAL // Implementation public: virtual ~CHistogramCtrl(); // Generated message map functions protected: //{{AFX_MSG(CHistogramCtrl) afx_msg void OnPaint(); //}}AFX_MSG DECLARE_MESSAGE_MAP() UINT UINT UINT

m_nLower; m_nUpper; m_nPos;

CDC

m_MemDC;

// lower bounds // upper bounds // current position within bounds

趣味程序导学 Visual C++

142 CBitmap m_Bitmap; };

下面,我们就可以在程序中使用这个类了。为对话框添加一个CStatic 控件,其ID设为 IDC_STATIC_HISTOGRAM,在属性对话框中将其Type属性设为Rectangle,Color属性设 为Black,如图5.16所示。

图 5.16 Static 控件属性设置

为对话框类CMultimediaDlg添加一个CHistogramCtrl类型的变量,相应代码为: CHistogramCtrl m_HistogramCtrl;

在OnInitDialog函数中对控件进行初始化,相应代码如下: CRect rect; GetDlgItem(IDC_STATIC_HISTOGRAM)->GetWindowRect(rect); ScreenToClient(rect); m_HistogramCtrl.Create(WS_VISIBLE | WS_CHILD, rect, this, 100); m_HistogramCtrl.SetRange(0,100);

在对话框的OnTimer事件中添加代码如下,每250ms显示一个音响效果: void CMultimediaDlg::OnTimer(UINT nIDEvent) { // TODO: Add your message handler code here and/or call default CTime t = CTime::GetCurrentTime(); int nRandom; srand(t.GetSecond()); do { nRandom = rand(); } while (nRandom < 0 || nRandom > 100); m_HistogramCtrl.SetPos(nRandom); nIDEvent=10; CString TimeCon,Min,Sec; char temp[10]; count ++; _itoa(count/4/60,temp,10);

第5章

媒体播放器——多媒体程序设计

143

Min = (CString)temp; TimeCon = "时间 " + Min + " 分"; _itoa(count/4%60,temp,10); Sec = (CString)temp; TimeCon = TimeCon + Sec + " 秒"; m_wndStatusBar.SetText(TimeCon,1,0); CDialog::OnTimer(nIDEvent); }

m_wndStatusBar是一个CStatusBarCtrl类型的变量,用来在对话框的状态栏中显示相应 的信息。 如图5.17所示是程序播放一个MIDI文件时的柱状音响效果显示。

图 5.17

5.10.2

柱状音响效果显示

音量的控制

为对话框添加一个CSlider控件,在其Style属性页设置如图5.18所示。

图 5.18

设置 CSlider 控件的 Style 属性

在程序中,我们利用这个滑块控件来调整系统音量的大小。由于系统音量的控制很复 杂,所以我们在此只给出一个相应的C++类,其源代码请有兴趣的读者参阅本书的配套光 盘。下面我们简要介绍这个类的使用方法。 首 先 将 “ VolumeOutMaster.h ” 包 含 到 StdAfx.h 中 , 然 后 将 IVolume.h , VolumeInXXX.h和VolumeInXXX.cpp 3个文件添加到工程中。添加回调函数和声音设定函 数如下: void CALLBACK MasterVolumeChanged( DWORD dwCurrentVolume, DWORD dwUserValue ) {

144

趣味程序导学 Visual C++

} void CMultimediaDlg::SetVolume(DWORD dwValue) { if ( !pMasterVolume || !pMasterVolume->IsAvailable() ) { // handle error } pMasterVolume->Enable(); pMasterVolume->RegisterNotificationSink( MasterVolumeChanged, dwValue ); pMasterVolume->SetCurrentVolume( dwValue ); //DWORD dwCurrentVolume = pMasterVolume->SetCurrentVolume(dwValue); }

在OInitDialog中进行初始化,相应代码为: pMasterVolume = (IVolume*)(new CVolumeOutMaster()); m_ctrlVolume.SetRange(pMasterVolume->GetMinimalVolume(),30000); m_ctrlVolume.SetLineSize(1000); m_ctrlVolume.SetPageSize(5000); m_ctrlVolume.SetTicFreq(1000); m_ctrlVolume.SetPos(3000); SetVolume(3000); return TRUE; // return TRUE unless you set the focus to a control

在ClassWizard中为对话框加入OnVScroll事件,并添加相应代码如下: void CMultimediaDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { // TODO: Add your message handler code here and/or call default int nNewPos = 0; switch(nSBCode) { case SB_TOP: //Scroll to far left. nNewPos = m_ctrlVolume.GetRangeMin(); break; case SB_ENDSCROLL: //end scroll. return; case SB_LINELEFT: //Scroll left. nNewPos = m_ctrlVolume.GetPos() - m_ctrlVolume.GetLineSize(); break; case SB_LINERIGHT: //Scroll right. nNewPos = m_ctrlVolume.GetPos() + m_ctrlVolume.GetLineSize(); break;

第5章

媒体播放器——多媒体程序设计

145

case SB_PAGELEFT: //Scroll one page left. nNewPos = m_ctrlVolume.GetPos() - m_ctrlVolume.GetPageSize(); break; case SB_PAGERIGHT: //Scroll one page right. nNewPos = m_ctrlVolume.GetPos() + m_ctrlVolume.GetPageSize(); break; case SB_BOTTOM: //Scroll to far right. nNewPos = m_ctrlVolume.GetRangeMax(); break; case SB_THUMBPOSITION: // Scroll to absolute position. The current // position is specified by the nPos parameter. nNewPos = nPos; break; case SB_THUMBTRACK: // rag scroll box to specified position. // The current position is specified by the nPos // parameter nNewPos = nPos; break; } nNewPos = max(nNewPos, m_ctrlVolume.GetRangeMin()); nNewPos = min(nNewPos, m_ctrlVolume.GetRangeMax()); SetVolume(nNewPos); CDialog::OnVScroll(nSBCode, nPos, pScrollBar); }

运行程序,现在我们就可以通过拖动滑块来调整音量大小了。 到此为止,我们的媒体播放器程序就基本完成了,现在,便可以用自制的媒体播放器 欣赏音频或视频文件了。 MCI调用简单,功能强大,可以满足日常多媒体编程的基本需要。但是,MCI一次只 能播放一个文件,使用DirectSound技术可以实现8个以上媒体文件的同时播放。

5.11

用ActiveMovie控件制作媒体播放器

在上面的几节里,我们为读者介绍了用Windows MCI制作媒体播放器的方法。在这一 节里,我们为读者介绍另外一种比较简单的方法,即利用ActiveMovie控件来制作媒体播 放器。 可视动画控件ActiveMovie是Microsoft公司开发的ActiveX控件,从开始的1.0版、2.0版 到现在的3.0版,功能上已经有了很大的改进。由于该控件内嵌了Microsoft MPEG音频解

趣味程序导学 Visual C++

146

码器和Microsoft MPEG视频解码器,所以能够很好地支持音频文件和视频文件,用其播放 的VCD效果就很好。另外,播放时若用鼠标右键单击画面,可以直接对画面的播放、暂 停、停止等进行控制,读者还可以自行在属性栏中设置影片播放控制,用起来非常方便。 在VC++6.0中已经包含了ActiveMovie 控件的3.0版,下面我们就利用这个控件自制了一个 简易的媒体播放器,该媒体播放器除了全屏显示功能外,还可以对音量进行控制。 5.11.1

建立工程

利用VC++6.0的AppWizard生成一个基于对话框的工程 Player ,去掉对话框上的 “确 定”和“取消”按钮,并加入ActiveMovie控件(通常情况下ActiveMovie控件并不出现在 控件面板中,可在菜单中依次选择Project|Add To Project|Components And Controls,在出现 的Components And Controls Gallery对话框中打开Registered Active Controls文件夹,选中 ActiveMovie Control Object选项,如图5.19所示。按Insert后关闭该对话框,ActiveMovie控 件便出现在控件面板中),调整好控件在对话框中的位置。为了能够控制控件的操作,应 为对话框设计一个菜单,菜单项可以定为“文件”、“屏幕控制”和“音量控制”。

图 5.19

5.11.2

为工程加入 ActiveMovieControl Object 控件

添加代码

首先利用ClassWizard为ActiveMovie控件声明一个变量m_ActiveMovie。然后为菜单文 件添加两个菜单项“打开文件”和“退出”,并分别添加函数OnOpen和OnExit ,代码如 下: void CPlayer::OnOpen() { // TODO: Add your command handler code here char szFilter[] = " 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

第5章

媒体播放器——多媒体程序设计

147

(*.mpeg)∣(*.mpeg)∣All File (*.*)∣*.* ";//用于设置FileDialog的文件类型 CFileDialog FileDlg( TRUE, NULL, NULL, OFN_HIDEREADONLY, szFilter ); if( FileDlg.DoModal() == IDOK ) { CString PathName = FileDlg.GetPathName(); PathName.MakeUpper(); m_ActiveMovie.SetFileName(PathName); } } void CPlayer::OnExit() { // TODO: Add your command handler code here OnOK();//退出应用程序 }

OnOpen()函数的作用是显示打开对话框,通过该对话框选择要执行的文件。 为菜单“屏幕控制”添加菜单项“满屏”,其响应函数为OnFully,具体代码如下: void CPlayer::OnFully() { // TODO: Add your command handler code here m_ActiveMovie.Pause (); //暂停播放 m_ActiveMovie.SetFullScreenMode(true); //设置满屏模式 m_ActiveMovie. Run(); //继续播放 m_ActiveMovie.SetMovieWindowSize(SW_SHOWMAXIMIZED); //设置窗口为最大 }

ActiveMovie 控件还提供了控制音量的两个函数GetVolume和SetVolume,只要在程序 中调用这两个函数,便可以达到控制音量的目的。为“音量控制”添加“增加”和“减 小”两个菜单项,其响应函数分别为: void CPlayer::OnAdd() { // TODO: Add your command handler code here long m_valume= m_ActiveMovie.GetVolume (); //获取当前音量 m_ActiveMovie.Pause (); m_ActiveMovie.SetVolume(m_valume+100); //用于增加音量 m_ActiveMovie.Run (); } void CPlayer::OnReducing() { // TODO: Add your command handler code here long m_valume= m_ActiveMovie.GetVolume (); m_ActiveMovie.Pause (); m_ActiveMovie.SetVolume(m_valume-100);

趣味程序导学 Visual C++

148 //用于减小音量 m_ActiveMovie.Run (); }

在声卡的控制菜单上给出了静音操作,那么能否为我们自己制作的媒体播放器加上静 音功能呢?回答是肯定的。虽然CActiveMovie3控件并没有直接提供静音函数,但可以通 过控制函数SetVolume的参数来实现静音的效果。笔者经过反复试验,当 SetVolume的参 数设为-4000时效果比较理想。要实现静音功能,应先为音量控制加入菜单项“静音”, 并添加消息响应函数OnMute,代码如下: void CVcdDlg::OnMute() { // TODO: Add your command handler code here m_ActiveMovie.Pause (); m_ActiveMovie.SetVolume(-4000); m_ActiveMovie.Run (); }

编译运行本程序,便可以用自制的媒体播放器欣赏光盘上的音频或视频节目了。

5.12 DirectSound简介 在本章最后,我们为读者简单介绍一下DirectSound的有关知识。 如果应用程序中只有单一的背景声音,或只有偶尔在按键时才发出的“咔嚓”声的 话,那么用微软的Win32 API函数就可以很好地实现,比如用PlaySound函数。但是更好的 多媒体环境需要更强大的开发工具。设想在一个充满机器的轰鸣声,步话机断断续续的喊 叫声,痛苦的呻吟声的环境里开枪射击的情况,这时就应该引入DirectSound。 DirectSound提供了进行音频处理所需要的两个特性:速度快,可控制性强。以下是它 优于Win32多媒体API函数的关键特性: ・ ・ ・ ・ ・

当硬件空闲时自动启用硬件加速 不受数量限制的声源混音 声音重现延迟时间短暂 与Direct3D接口简单的3D声音定位效果 自动将输入的Wave数据转换成与输出匹配的格式——即使输入数据为复杂格式

・ 支持属性设置,利用硬件的新特性而不改变API函数 下 面 我 们 简 单 了 解 一 下 DirectSound 是 如 何 工 作 的 。 首 先 从 “ 从 声 音 缓 冲 区 (Secondary Sound Buffer)”对象说起。一个从声音缓冲区对象代表一个声源,这个声源 既 可 以 是 静 态 的 声 音 对 象 ( StaticSound ) , 也 可 以 是 动 态 的 声 音 对 象 ( Streaming Sound)。静态的声音对象是指声音数据一次性读入内存,它一般适用于较短的声音。动 态的声音对象是指声音数据必须隔一段时间传送一部分到缓冲区中。所有缓冲区都含有脉

第5章

媒体播放器——多媒体程序设计

149

冲编码调制(PCM)格式的声音样本数据。 播放从声音缓冲区对象时,DirectSound从每个缓冲区中取出数据,然后在主缓冲区 (primary buffer)中进行混音。混音时,它会执行所有必要的格式转换——例如,将采样率 从44KHz转换到22KHz。同时,它会处理所有特殊效果,例如,3D空间中的声源定位等。 在主缓冲区中混音后,声音即送往输出设备。 当硬件缓冲区和硬件混音设备空闲时,DirectSound自动将尽可能多的声音对象送入硬 件内存中。留在主机系统内存中的声音对象由DirectSound进行软件混音,并以流的方式与 硬件缓冲区中的声音对象一起送入硬件混音器。 在应用程序中使用DirectSound系统的操作步骤如下所示: 1. 2. 3. 4.

为声音设备获得一个“全局惟一标志符”(GUID)(可选)。 生成DirectSound对象。 设置协作优先级。 设置主缓冲区对象的格式(可选)。

5.13 在按钮上显示位图

本章知识点回顾

加载位图资源: HBITMAP LoadBitmap(HINSTANCE hInstance, LPCTSTR lpBitmapName ); 为按钮设置位图: HBITMAP CButton::SetBitmap( HBITMAP hBitmap );

菜单项位图的显示

得到对话框主菜单的指针: pMainMenu = GetMenu(); 得到菜单项的指针: CMenu* CMenu::GetSubMenu( int nPos ); 加载位图资源: BOOL CBitmap::LoadBitmap( UINT nIDResource ); 设置菜单项的位图: BOOL CMenu::SetMenuItemBitmaps( UINT nPosition, UINT nFlags,const CBitmap* pBmpUnchecked, const CBitmap* pBmpChecked );

趣味程序导学 Visual C++

150

续表 系统报警声音的播

系统报警声音是由用户在控制面板中的声音(Sounds)程序中定义的,或者在



WIN.INI的[sounds]段中指定。该函数的声明为: BOOL MessageBeep(UINT uType); 参数uType说明了报警声音的类型。

高 级 音 频 函 数

PlaySound函数的原型为:

PlaySound

BOOL WINAPI PlaySound

MIDI文件格式

Wave文件格式

( LPCSTR

pszSound,

HMODULE

hmod,

DWORD );

fdwSound

MIDI文件大体分为两个区块: (1)文件头区块MThd (2)音轨区块MTrk Wave文件作为多媒体中使用的声波文件格式之一,它是以RIFF格式为标准的。 每个Wave文件的头四个字节便是RIFF。Wave文件由文件头和数据体两大部分组 成。其中文件头又分为RIFF/Wave文件标识段和声音数据格式说明段两部分。

AVI文件格式

AVI(Audio Video Interleave)是一种音频图像交叉记录的数字视频文件格式。 构成一个AVI文件的主要参数包括图像参数、伴音参数和压缩参数等。

MCI 接口的传送命

mciSendCommand方法:

令方式

MCIERROR mciSendCommand ( MCIDEVICEID IDDevice, UINT uMsg, DWORD fdwCommand, DWORD dwParam ); mciSendString方法

MCI设备类型

MCIERROR mciSendString ( LPCTSTR lpszCommand, LPTSTR lpszReturnString, UINT cchReturn, HANDLE hwndCallback ); ・ animation:动画播放设备 ・ cdaudio:CD音响播放设备 ・ digitalvideo:Windows用视频 ・ other:未定义的MCI设备 ・ overlay:窗口中的模拟设备 ・ sequencer:乐器数字接口(MIDI)音序器 ・ videodisk:视频演播设备 ・ waveaudio:数字波形音频设备

第5章

媒体播放器——多媒体程序设计

151 续表

用MCI 播放和控制 媒体文件

打开设备: mciSendCommand(NULL, MCI_OPEN,

MCI_WAIT|MCI_OPEN_TYPE|

MCI_OPEN_ELEMENT,(DWORD)(LPMCI_OPEN_PARMS)&mciOpenP arms); 关闭设备: mciSendCommand(m_nDeviceID,MCI_CLOSE,MCI_WAIT,NULL); 播放: mciSendCommand(m_nDeviceID,MCI_RESUME,MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) &mciGenericParms); 停止: mciSendCommand(m_nDeviceID,MCI_STOP, MCI_WAIT,(DWORD) (LPMCI_GENERIC_PARMS) &mciStopParms); 暂停: mciSendCommand (m_nDeviceID, MCI_PAUSE, MCI_WAIT, (DWORD)(LPMCI_GENERIC_PARMS) &mciGenericParms); 跳转: mciSendCommand (m_nDeviceID, MCI_SEEK,MCI_TO|MCI_WAIT, (DWORD) (LPMCI_SEEK_PARMS)&mciSeekParms); 察看状态和信息: mciSendCommand (m_nDeviceID, MCI_STATUS,MCI_WAIT| MCI_STATUS_ITEM,(DWORD)( LPMCI_STATUS_PARMS) &mciStatusParms); 录音: mciSendCommand (m_nDeviceID, MCI_RECORD, NULL, (DWORD)(LPMCI_RECORD_PARMS)&mciRecordParms); 保存录音: mciSendCommand (m_nDeviceID, MCI_SAVE, MCI_SAVE_FILE | MCI_WAIT,(DWORD)(LPMCI_SAVE_PARMS) &mciSaveParms); 开光驱门: mciSendCommand (m_nDeviceID, MCI_SET,MCI_SET_DOOR_OPEN, NULL); 关光驱门: mciSendCommand (m_nDeviceID, MCI_SET,MCI_SET_DOOR_CLOSED, NULL);

趣味程序导学 Visual C++

152

续表 ActiveMovie控件的 使用

设置播放文件的路径: m_ActiveMovie.SetFileName(PathName); 暂停播放: m_ActiveMovie.Pause (); 设置全屏模式: m_ActiveMovie.SetFullScreenMode(true); 继续播放: m_ActiveMovie. Run(); 得到当前音量大小: m_ActiveMovie.GetVolume (); 设置音量大小: m_ActiveMovie.SetVolume(m_valume-100);

第6章

北京市公交查询系统——数据库编程基础

在信息时代,常常要利用网络与数据库来查询相应信息。本章主要是通过Visual C++ 来实现ODBC数据库管理,并借助于一个简单的北京市公交查询系统来介绍数据库基础知 识。本章主要讲述了以下内容:数据库基础知识、使用Microsoft Access构建数据库、基于 对话框的MFC程序设计、VC与数据库的接口、Windows 2000下数据源(ODBC)中用户 DSN的设置、Microsoft开放数据互连(ODBC)标准与数据库的打开、记录集的打开、记 录集相应操作、SQL查询语句对应的过滤操作、基本控件操作、组合框内容的加入和选 取、编辑框内容的获取等。

6.1

系统使用说明

“北京市公交查询系统”的核心是打开已有的公交车次路线数据库,并对选择好的车 次进行路线(即停靠站)的查询,或者在编辑框内输入所要查询的车站名,显示路线中含 有该站的车次及相应停靠站。 作为一个简单的查询系统,需要设定一些基本要素,如下所示: ・ ・ ・ ・ ・ ・

给定一个已有的公交路线数据库(自己构建)。 含有所有车次的下拉组合框,供用户选择所需查询的车次。 能够读取用户输入的编辑框,供用户输入需要查询的车站。 “查询”按钮,当用户选择好相应工程后,通过这个按钮显示匹配的查询内容。 静态文本框,用于显示相应的内容。 “退出”按钮,退出本查询系统。

该系统具体的使用规则如下: 1. 单击下拉组合框,系统将显示出初始化读入的公交车次。 2. 选择好车次,单击“查询”按钮,即可在右边的显示区显示相应车次的停靠站 (路线)。 3. 在车站查询栏目中,输入需要查询的车站名 4. 单击“查询”按钮,即可在后边的显示区显示所有通过这个车站的车次和该车次 对应的路线。 这个北京市公交查询系统如图6.1所示,它只是一个简单的系统,数据库的进一步扩 充和界面的美化可以由读者自己完成。

趣味程序导学 Visual C++

154

图 6.1

6.2

6.2.1

查询系统界面

数据库基础知识

简介

数据库是计算机应用系统中的一种专门管理数据资源的系统。数据有多种形式,如文 字、数码、符号、图形、图像以及声音等。数据是所有计算机系统所要处理的对象。人们 所熟知的一种处理办法是制作文件,即将处理过程编成程序文件,将所涉及的数据按程序 要求组织成数据文件,用程序文件来调用。数据文件与程序文件保持着一定的对应关系。 在计算机应用迅速发展的情况下,这种文件式方法便显出不足。比如,它使得数据通用性 差,不便于移植,在不同文件中存储大量重复信息,浪费存储空间,而且更新不便。数据 库系统便能解决上述问题。数据库系统不从具体的应用程序出发,而是立足于数据本身的 管理,它将所有数据保存在数据库中,进行科学的组织,并借助于数据库管理系统,以它 为中介,与各种应用程序或应用系统接口,使之能方便地使用数据库中的数据。就好像医 院中的药房一样,面向所有科室,不论哪个科开的药都可到药房去拿药,药品的进出、更 新、保存均由药房来做。有了数据库系统,所有应用程序都可以通过访问数据库的办法来 使用所需的数据,实现了数据资源的共享。数据库管理系统负责各种数据的维护、管理工 作,如大批数据的更新、保存、交流等也很方便,数据的查询、检索等操作也变得十分容 易。 一个数据库系统通常由3部分组成: (1)数据库(DB):是按照某种规范格式存放在一起的相关数据的集合。简言之, 数据库是集中存放的大批数据文件。 (2)数据库管理系统(DBMS):是操纵和管理数据库的大型软件,是用户的个别 应用与整个数据库之间的接口。当用户向数据据库发出访问请求后,DBMS接受,分析该 用户的请求,并根据用户请求去操纵(查询、存储、更新)数据库中的有关数据。 (3)用户应用:指用户根据自身的需要,利用DBMS提供的相关命令编制的一组实

第5章

北京市公交查询系统——数据库编程基础

155

用程序。例如在一个饭店管理的数据库系统中,可能会存在着多个用户应用,包括预订房 间、顾客登记、订购机票等。 严格来说,数据库系统(Database System)是一个实际可运行的存储、维护和应用数据 的软件系统,是存储介质、处理对象和管理系统的集合体。它通常由软件、数据库和数据 库管理员组成。其软件主要包括操作系统、各种宿主语言,实用程序以及数据库管理系 统。而数据库(Database)是依照某种数据模型组织起来并存放在存储器中的数据集合。 这些数据为多个应用服务,独立于具体的应用程序。而数据库由数据库管理系统统一管 理,数据的插入、修改和检索均要通过数据库管理系统进行。数据库管理系统是一种系统 软件,它的主要功能是维护数据库并有效地访问数据库中的任意数据。对数据库的维护包 括保持数据的完整性、一致性和安全性。数据库管理员负责创建、监控和维护整个数据 库,使数据能被任何有权使用的人有效使用。数据库系统具有以下特性: ・ 数据独立性:也就是数据能独立于应用程序之外,我们修正数据不需修改相应的 应用程序。 ・ 数据安全性:能防止无关人员获取他不应该知道的数据,这是由用户自己负责 的。 ・ 数据完整性:指数据的正确性、客观性和真实性。因为破坏数据完整性的因素很 多,所以应尽可能减少这类情况的发生。 ・ 数据一致性:指同一事物的数据,不管出现在何时何处都是一致的。 ・ 数据共享:是数据库系统的主要功能特色之一。它指多个应用程序可以使用同一 数据文件;多个用户可存取同一数据;可向社会开放,成为社会的一种信息资 源。 ・ 控制冗余:它对于节省空间和减少开销及防止数据不一致有重要的作用。 ・ 集中管理:指不仅对文件的结构、数据的装入和文件的各种操作要集中管理,而 且对文件的内容、数据的类型、长度、大小等都要检查。 ・ 并发控制:因数据库系统实现了多个用户共享数据,所以就可能多个用户同时要 存取数据,这时就需要对这种并发操作进行控制。 ・ 故障恢复:当数据库系统运行时出现故障,如何尽快将它恢复正常,就是数据系 统的故障恢复功能。 一般来说,我们平时所说的数据库系统是代指数据库管理系统(Database Management System),而不是指某个具体的数据库。以下均沿用这个约定。

6.3

使用Micosoft Access创建数据库

在学习了有关数据库的基础知识后,我们开始为这个查询系统的数据设计车次车站间 的关系,并借助于工具Microsoft Access创建一个简单的数据库,为后面程序的访问提供数 据。本节就简单地介绍这个强大的数据库创建管理工具的基本使用方法。

趣味程序导学 Visual C++

156

6.3.1

初识Access

Access 2000是Microsoft强大的桌面数据库平台的划时代产品,也是32位Access的第三 代产品。和传统的dBASE和FoxPro相比,Access不但使用简单,容易操作,而且和Office 家族的其他产品紧密结合,并采用了Office 2000 VBA(Visual Basic for Application)代码 来自动操作Access 应用。同时,Access 2000还共享了Office 2000新的超文本标记语言 (HTML)的帮助系统。 当我们第一次打开Access 2000时,Office助手将显示相应的提示信息及初始选项,如 图6.2和图6.3所示。

图 6.2 Office 助手

图 6.3

初始选项

.

当你从Office 2000助手中选择“开始使用Microsoft Access”后,我们可以通过选择初 始项来开始创建数据库,如新建空的Access数据库,或者打开一个已有文件。这里我们建 立一个简单的公交路线数据库。我们选中“新建数据库”中的“空Access数据库”,如图 6.4所示:

图 6.4

选择新建空数据库

单击“确定”按钮后,系统将弹出“文件新建数据库”对话框,选择该数据库保存的 位置并输入新建数据库的名字bus.mdb如图6.5所示:

第5章

北京市公交查询系统——数据库编程基础

图 6.5

157

保存数据库

单击“创建”后,系统将弹出数据库的结构,并提供创建向导,帮助创建数据库。在 创建表的过程中,系统提供了设计器,向导和输入数据三种方式。用户可以根据自己的需 要进行选择。如图6.6所示:

图 6.6

选择设计方式

考虑到我们将要创建的公交系统的关系模型,我们选择使用设计器来创建表。 6.3.2

选择关系并定义字段

双击使用设计器来创建表,将出现设计器的对话框,在这里我们可以选择这个数据库 的关系模型,并输入相应的字段名称和数据类型。考虑到后面程序的实现,我们加入两个 字段,分别定义为bus_number和bus_station,作为车次和停靠站,对数据类型的选择,系 统提供了数字、文本、自动编号等,单击下拉菜单即可作出选择。在此我们选择了数字和 文本,如图6.7所示。

趣味程序导学 Visual C++

158

图 6.7

图 6.8

定义字段

选择数据类型并保存

然后,可以利用组合键Ctrl+s来保存该表,也可以通过单击右上角的关闭按钮,然后 选择保存来存储。键入bus作为这个表的名称,单击确定保存。在后面的访问中,我们将 利用这个bus表来访问数据库的内容,如图6.8所示。 在单击确定后,系统将弹出一个“尚未定义主键”的提示框。如图6.9所示。主键主 要是用来定义多个表间的关系,在这里我们仅用到了一个 bus 表,故可以选择不定义主 键。

图 6.9

6.3.3

主键定义提示框

添加数据

保存bus 表后,我们可以开始向其中加入相应的数据。双击 bus 图标,系统将弹出表 格。可以看到,bus 表包含两个字段,一个是刚才定义的bus_number 字段,另一个是 bus_station字段。在这两个字段下我们可以加入相应的数据,构建成一个数据库。为方便 起见,只添加一些简单的数据作为例子,如图6.10所示。

第5章

北京市公交查询系统——数据库编程基础

159

需要注意的是,这里将公交系统的关系简单的定义为车次与停靠车站。在bus_station 字段中,各停靠站之间以逗号隔开,这主要是方便以后的程序。当然,定义多个车站字段 将各站分开可以方便显示,但是查询时需要更多的判断。

图 6.10

添加数据

6.4 VC与数据库接口 在前面几节里,我们着重介绍了如何利用Microsoft Access构建一个简单的数据库,在 第2章和第3章中曾讲述过利用VC++的AppWizard创建一个基于对话框的MFC应用程序。 如何将二者联系在一起,即数据库和VC之间的接口问题,是这一节里我们要解决的主要 问题。首先,让我们来设置用户方的公交系统数据源(Data Source Name,DSN)。 6.4.1

用户DSN设置

安装Visual C++后,系统将自动地在硬盘上安装所需要的ODBC(将在后面介绍)驱 动程序。在 Windows 2000下,单击“开始”菜单,选择“设置”中的“控制面板” (control panel),选择“管理工具”,如图6.11所示:

图 6.11

在“控制面板”中选择“管理工具”

趣味程序导学 Visual C++

160

双击“管理工具”,从打开的窗口中选择“数据源(ODBC)”(见图6.12),下面 的一系列设置均是基于此项。双击该项,系统将出现数据源的设置,包括用户DSN,系统 DSN,文件DSN,驱动程序,跟踪,连接池和关于等项。本程序中需要处理的只是用户 DSN部 分 , 选 择 “ 添 加 ” 。 系 统 弹 出 添 加 新 数 据 源 对 话 框 ( 见 图 6.13 ),从中选择 “Microsoft Access Driver(*.mdb)”项,单击“完成”按钮,进而设置“ODBC Microsoft Access安装”。如图6.14所示。

图 6.12

图 6.13

选择数据源

选择“Micorsoft Access Driver(*.mdb)”

第5章

北京市公交查询系统——数据库编程基础

161

图 6.14 ODBC Microsoft Access 安装

单击“选择”按钮,选择将要打开的数据库,在此我们选择bus.mdb,如图6.15所 示:

图 6.15

选择数据库

单击“确定”按钮,然后在弹出的对话框中定义这个数据库的数据源名(DSN)为 bus,如图6.16所示。

图 6.16

定义数据源名

单击“确定”按钮,可以看到用户DSN项中有一项 bus : Microsoft Access Driver (*.mdb),如图6.17所示。

趣味程序导学 Visual C++

162

图 6.17

设置完毕

单击“确定”按钮后,用户DSN就设定好了。这时候,我们开始创建的bus.mdb已经 与用户的一个设定为bus的数据源对应起来。下面我们将脱离原始的数据库bus.mdb,直接 对这个数据源进行操作。 6.4.2

ODBC标准

Microsoft开放数据库互连(ODBC)标准不但定义了SQL语法规则,而且还定了C (C++)语言和SQL数据库之间的编程接口。所以,后面的程序可以访问带有ODBC驱动 程序的DBMS。 ODBC 结构 ODBC 有一个非常独特的基于 DLL的结构,从而使系统完全模块化。这个 DLL,即 ODBC32.DLL,定义了应用程序编程接口(API)。在程序的执行过程中,这个DLL会装 入特定数据库的DLL,也就是人们常说的驱动程序(driver)。借助于控制面板中的ODBC 管理模块,ODBC32.DLL能够取得将要得到的数据库的DLL,因而可以使得应用程序能够 访问指定的DBMS中的数据。具体结构图如图6.18所示。 MFC ODBC 基本类 借助于MFC类,我们可以使用C++对象来代替窗口句柄和设备环境句柄;而通过MFC ODBC 类 , 我 们 可 以 用 对 象 来 代 替 连 接 句 柄 和 语 句 句 柄 。 两 个 最 主 要 的 ODBC 类 是 CDatabase和CRecordset。CDatabase类的对象代表了和数据源的ODBC连接,而CRecordset 类的对象则代表了可以访问并可以滚动的记录集。用户很少从CDatabase类中派生出新的 类,而总是要从CRecordset中派生出一系列的类,以便和数据库表中的字段相匹配。它们 之间的关系如图6.19所示。 需要注意的是,CRecordset类指的是派生出的CxxxxSet,其对象中所包含的数据成员 只代表了表中的一行,即是当前记录。CRecordset类的主要函数将在后面介绍。下面我们 将探讨如何利用上述结构来实现这个数据库的接口。

第5章

北京市公交查询系统——数据库编程基础

MFC数据

ODBC32.DLL

库应用程序

驱动程序管理器

ODBCJT32.DLL

ODBCCR32.DLL

Jet控制器

光标库

MSJT3032.DLL

SQL Server

Jet数据库引擎

ODBC驱动动程序

MSXB3032.DLL

163

SQL Server

Xbase驱动程序

远程共享数据库

本地MDB文件

本地DBF文件

图 6.18

CDatabase对象

ODBC连接

ODBC 结构

CRecordset对象

ODBC记录集

动态集或快照

数据库 图 6.19

6.4.3

C++对象和 ODBC 的关系

接口实现

有了前面的知识,我们可以开始着手进行数据库和VC框架之间的连接。当前我们的 目标是在应用程序中访问ODBC兼容的数据库,而采取的策略是根据刚才分析的MFC ODBC 主 要 的 CDatabase类 和 CRecordset 类 来 处 理 。 CDatabase类 用 来 打 开 数 据 库 , 而 CRecordset类用来打开数据库的表(就是前面创建数据库时定义两个字段 bus_number 和

趣味程序导学 Visual C++

164

bus_station,并添加几个简单数据的bus表)并滚动记录。下面将这个接口实现的过程分为 两步: 1. 设置应用程序 首先我们打开VC++ Developer Studio中工作区的文件视图(File View),选择其中的 头文件夹(Hearder Files),找到头文件StdAfx.h,如图6.20所示。

图 6.20

设置应用程序

确保其中包含以下代码,如果没有,请加入: #ifndef _AFX_NO_DB_SUPPORT #include

// MFC ODBC database classes

#endif // _AFX_NO_DB_SUPPORT #ifndef _AFX_NO_DAO_SUPPORT #include #endif // _AFX_NO_DAO_SUPPORT

// MFC DAO database classes

其中第一个包含文件是为了打开 ODBC数据库类,而第二个是为了打开DAO(Data Access Objects)数据库。 2. 用 ClassWizard 创建 CBusSet 记录集类 这个类派生于CRecordset基类,用于打开bus 表形成的记录集类。单击ClassWizard, 选择Add Class下拉列表框中的New,如图6.21所示。 单击OK按钮,弹出如图6.22所示的对话框,在Base class中选择CRecordset作为该派生 类的基类。同时在第一项Name中输入派生类的名称,例如CBusSet。

第5章

北京市公交查询系统——数据库编程基础

图 6.21

图 6.22

165

创建 CBusSet 记录集

New Class 对话框

单击OK按钮,系统将出现数据库的相应选项。这里的设置将直接决定我们的应用程 序所要打开的数据库和数据库中的表。请选择bus数据源作为相应的数据库,如图6.23所 示:

图 6.23

选择 bus 作为数据源

单击OK按钮,将出现包含在此数据源中的表。其中选择多表将对多个表进行操作。

趣味程序导学 Visual C++

166

这里我们只有一个表bus供选择,选定后单击OK按钮,如图6.24所示:

图 6.24

选择其中的 bus 表作为数据库表

最后,ClassWizard将创建派生的记录集类CBusSet。这时,表中的每一个字段都对应 着一个成员变量。打开工作区的ClassView,我们发现在bus Class中多了一项CBusSet,这 就是刚才利用Class Wizard创建的派生于CRecordset的记录集类。同时,原来数据库表中的 字段名分别对应了成员变量m_bus_number,m_bus_station,如图6.25所示:

图 6.25

创建的 CBusSet 类和成员变量 m_bus_number,m_bus_station

双击m_bus_number,在右边的对应窗口中将显示如下的代码: class CBusSet : public CRecordset { public: CBusSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CBusSet) // Field/Param Data //{{AFX_FIELD(CBusSet, CRecordset) long m_bus_number; CString m_bus_station; //}}AFX_FIELD

从代码第一行我们可以看出,CBusSet是派生于CRecordSet基类的,而从m_bus_number,m_bus_station的声明: long

m_bus_number;

CString

m_bus_station;

第5章

北京市公交查询系统——数据库编程基础

167

不难看出,它们和字段的数据类型:数字和文本也是分别对应的。后面对相应字段的处理 和操作均是基于这两个变量进行的。 3. 打开数据库和表,形成记录集 经过以上两个步骤我们已经完成了打开数据库bus.mdb和bus表的准备工作。在这一步 里,我们将学习剩下的操作。 首先,单击ClassView中的CBusDlg类,在右边窗口出现的对应的BusDlg.h中加入对类 CBusSet的引用说明,即: #include "BusSet.h"

并在随后的类声明中声明两个成员变量作为CDatabase和CBusSet的实例,如图6.26所 示。

图 6.26

代码窗口

代码如下: CBusDlg(CWnd* pParent = NULL);

// standard constructor

CDatabase m_database; CBusSet m_set;

图中阴影部分是加入的代码。其中m_set是类CBusSet的对象,然后在CBusDlg 类的实 现文件头部加入包含文件: #include "BusSet.h"

单击CBusDlg类中的OnInitDialog()函数,对应右边窗口显示其代码。初始化查询系统 对话框的工作将在这个函数内完成。我们计划在这个函数内打开数据库并形成记录集,代 码如下:

趣味程序导学 Visual C++

168

图 6.27

添加包含头文件

BOOL CBusDlg::OnInitDialog() { CDialog::OnInitDialog(); // Add "About..." menu item to system menu. // IDM_ABOUTBOX must be in the system command range. ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < 0xF000); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // Set the icon for this dialog.The framework does this automatically //

when the application's main window is not a dialog

SetIcon(m_hIcon, TRUE); SetIcon(m_hIcon, FALSE);

// Set big icon // Set small icon

if(!m_database.Open(NULL,FALSE,FALSE,"ODBC;DSN=bus")) {

第5章

北京市公交查询系统——数据库编程基础

169

AfxMessageBox("Failed to open database"); } m_set.Open(); // TODO: Add extra initialization here return TRUE;

// return TRUE

unless you set the focus to a control

}

黑体部分是我们加入的代码。其中m_database调用Open函数通过ODBC驱动程序打开 数据库bus,各参数说明如下: if(!m_database.Open(NULL, FALSE, //通常为FALSE

//数据源说明,若为NULL,则默认表示为后面的设定

FALSE, //定义为TRUE时表示只读 "ODBC;DSN=bus"))//连接由DSN表示的数据源

而m_set指针也调用了Open()函数。 至此,我们已经完成了VC应用程序和数据库之间的连接。作为本程序的重点,我们 再一次回顾整个接口过程: (1)设定用户DSN,将数据库bus.mdb与Microsoft Access Driver对应。 (2)设置应用程序,确保包含文件的存在。 (3)在应用程序中派生CRecordset类,形成CBusSet类作为记录集类。 (4)在对话框类中声明CDatabase和CBusSet类的对象。 (5)分别利用二者的Open函数打开相应的数据库和表,形成记录集。 经过上面的过程,数据库已经打开。下面我们将对记录集中的记录进行相应的操作, 完成整个查询系统。

6.5

记录集操作

在这一节里,我们主要熟悉数据库中记录集的各项操作。包括如何滚动记录集、加 入、删除、编辑和查找记录。这主要是通过上面定义的CBusSet类的对象m_set的成员函数 完成。通过对这些函数的认识,我们将在后面的查询系统中更好地对记录集进行操作。首 先,我们来看一下ODBC记录集的基本操作。 6.5.1

使用ODBC记录集

在这一节里,我们主要介绍ODBC几个最基本的操作。并通过实际运行结果来观察处 理机制。 在上一节里我们已经提到了m_set的一个成员函数Open()以及记录集的打开方式。这 里仅介绍如何显示记录集中的记录。

趣味程序导学 Visual C++

170

回到Developer Studio的工作区,在资源编辑器中找到对话框IDD_BUS_DIALOG,依 前面的介绍选择静态文本框的属性,删除原来的字符串,按回车键确定,如图6.28所示:

图 6.28

静态文本框

这里,需要记住的是这个静态文本框在资源中对应的ID:IDC_STATIC。在后面的程序 中,我们将通过这个ID来对静态文本框做相应的处理。下面我们通过显示第一条记录来说 明如何在对话框中显示相应的记录。 显示第一条记录 使用函数MoveFirst()即可显示第一条记录,单击Developer Studio 中的ClassView 中 CBusDlg中的OnInitDialog()函数,在刚才加入的代码中继续加入如下的代码: CString str; m_set.MoveFirst(); str.Format("%ld",m_set.m_bus_number); SetDlgItemText(IDC_STATIC,str);

其中CString str声明了一个字符串类型的变量str来作为显示缓冲区。m_set.MoveFirst() 返回的是第一条记录,而str.Format("%ld",m_set.m_bus_number)则将类型为long的m_bus_ number格式化为字符串,便于输出。SetDlgItemText(IDC_STATIC,str)是将已经格式化好的 字符串str以文本形式显示在ID为IDC_STATIC即刚才我们设定的静态框内。回忆最早设定 的数据库,第一个记录是“345;清华西门”,如果程序正确的话,将在原来hello world的 地方显示第一个记录的车次“345”,编译运行结果如图6.29所示。

图 6.29

运行结果

第5章

北京市公交查询系统——数据库编程基础

171

滚动记录集 利用循环语句和函数MoveNext()可以对记录集进行滚动。比如,可以利用如下方式进 行滚动: while(!m_set.IsEOF()) { m_set.MoveNext(); }

考虑到显示问题,我们在程序中做如下处理: m_set.Open(); CString str; m_set.MoveFirst(); for(int i=0;im_pErrorInfo->m_strDescription); e->Delete(); } //str.Format("%ld",m_set.m_bus_number); //SetDlgItemText(IDC_STATIC,str);

上面try-catch是异常处理。其中显示输出的语句我们暂时注释掉。下面我们打开原有 的数据库bus.mdb,再打开表bus,显示如图6.31所示。

图 6.31

添加记录

需要注意的是,必须在加入代码的最后调用Update()才能把记录加入记录集。 编辑记录 可以编辑一条记录,我们假设将刚才加入的记录“395;王府井”改成“335;西单”, 代码如下: if(!m_database.Open(NULL,FALSE,FALSE,"ODBC;DSN=bus")) {

第5章

北京市公交查询系统——数据库编程基础

173

AfxMessageBox("Failed to open database"); } m_set.Open(); CString str; m_set.MoveLast(); try { m_set.Edit(); m_set.m_bus_number=335; m_set.m_bus_station="西单 "; m_set.Update(); } catch (CDaoException *e) { AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); }

其中修改的代码部分用黑体表示。再次打开bus表,可以看到原先的记录已经变成我 们修改后的“335;西单”,如图6.32所示。

图 6.32

编辑记录

和加入记录一样,这里仍需更新后方能完成编辑功能。 删除一条记录 通过以下代码,可以删除记录集中最后一条记录: if(!m_database.Open(NULL,FALSE,FALSE,"ODBC;DSN=bus")) { AfxMessageBox("Failed to open database"); }

趣味程序导学 Visual C++

174 m_set.Open(); CString str; m_set.MoveLast(); try { m_set.Delete(); } catch (CDaoException *e) {

AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); }

其中黑体部分是在原来程序代码中作了修改的部分。m_set.MoveLast()是返回最后一 条记录。现在我们再次打开bus表,可以看到结果如图6.33所示。

图 6.33

删除记录

关闭记录集 在程序的最后,我们用如下方式来关闭这个记录集,释放它所占用的内存空间。 m_set.Close();

6.5.2

用SELECT打开一个ODBC记录集

上面介绍的几个操作都是ODBC记录集中比较基本的操作。下面将介绍基于SQL查询 方式的ODBC记录集的打开方式。

第5章

北京市公交查询系统——数据库编程基础

175

完全匹配查询 我们可以利用m_set的变量m_strFilter和简单SQL WHERE语句打开一个数据库,如查 找路线为“双清路”的记录(注意,是路线,若想根据停靠站来搜索,则需要用到下面的 模糊查询),可以借助于如下代码: if(!m_database.Open(NULL,FALSE,FALSE, "ODBC;DSN=bus")) { AfxMessageBox("Failed to open database"); } m_set.m_strFilter="[bus_station]='双清路'"; m_set.Open(); CString str; m_set.MoveFirst(); while(m_set.IsEOF()) { m_set.MoveNext(); } str.Format("%ld",m_set.m_bus_number); SetDlgItemText(IDC_STATIC,str);

这里最关键的部分已经以黑体表示,即: m_set.m_strFilter="[bus_station]='双清路'";

记录集在打开时将按照这个描述语句进行判断。匹配的记录将逐一打开。bus表中仅 有“365;双清路”符合,所以我们只是简单的直接输出。最后运行结果如图6.34所示:

图 6.34

完全匹配

趣味程序导学 Visual C++

176

模糊查询 如果在查询时只给出了停靠站,而不是整条路线,我们需要对记录集进行模糊查询。 如,我们需要查询所有经过天坛车站的公交路线。这里用到的关键语句是: m_set.m_strFilter=" [bus_station] like '%天坛%' ";

原有的bus表中仅有375满足要求,所以我们可以简单处理如下: if(!m_database.Open(NULL,FALSE,FALSE, "ODBC;DSN=bus")) { AfxMessageBox("Failed to open database"); } m_set.m_strFilter=" [bus_station] like '%天坛%'"; m_set.Open(); CString str; m_set.MoveFirst(); while(m_set.IsEOF()) { m_set.MoveNext(); } str.Format("%s",m_set.m_bus_station); SetDlgItemText(IDC_STATIC,str);

这里我们利用如下语句选择输出匹配的路线: str.Format("%s",m_set.m_bus_station);

其中“%s”是将str格式化为m_set中的m_bus_station的字符串类型,从而正确输出公 交路线。需要注意的是,如果有多条记录满足查询条件时,我们可以在循环语句中用多个 静态文本框输出。在后面完善程序时,我们将设置6个静态文本框以输出多条匹配记录。 运行程序,得到正确结果,如图6.35所示。

图 6.35

模糊匹配

第5章

北京市公交查询系统——数据库编程基础

177

6.6 MFC基本控件消息响应与系统完善 前面几节已经完成了构建数据库、生成基本框架、应用程序和数据库接口以及记录集 操作等知识的介绍。在这一节里我们主要学习MFC基本控件的使用方法和消息响应,并最 终完善该程序。 6.6.1

在组合框内选择车次并显示路线信息

为了提供给用户更好的交互性,我们可以选择组合框(Combo box)作为车次选择的 控件,在对话框初始化时读入记录集中的车次信息,通过下拉选项选择所要查询的车次, 响应查询按钮,并显示出查询出的信息。下面首先介绍组合框的使用。 打开bus数据库,单击菜单中的Tools项,选择其中的Customize...项,如图6.36所示。

图 6.36

选择 Customize

在弹出的对话框中选择第二个标签(Toolbars),选中Build MiniBar ,Dialog和控件 (Controls)三个选项,如图6.37所示: Developer Studio将会弹出含有控件的控制板。单击Customize中的Close按钮,关闭此 对话框,含有控件的控制板如图6.38所示:

趣味程序导学 Visual C++

178

图 6.37

图 6.38

其中组合框的按钮为

Toolbars 标签

含有控件的控制板



打开IDD_BUS_DIALOG,然后拖动组合框按钮到对话框上,如图6.39所示。

图 6.39

放置组合框

第5章

北京市公交查询系统——数据库编程基础

179

单击组合框周围的8个小方框标记,可以改变组合框的大小,如图6.40所示:

图 6.40

改变组合框大小

在组合框上单击右键,选择属性,将ID改为IDC_COMBONUM,按回车键确定。下 面我们将介绍一种新的消息传递机制——设置控制变量。 打开ClassWizard,选择Member Variables,在原来添加CBusSet类时选择的Add Class 下面有一个Add Member Variable选项,单击该按钮,系统弹出设定对话框。我们输入变量 名m_num,同时在类别(Category)项中选择control,表示控制变量,而不是值变量,系 统自动设定此变量类型为CComboBox,如图6.41所示:

图 6.41

添加控制变量

单击OK按钮,对应于这个组合框我们有一个控制变量m_num,后面的操作都将通过 这个变量来进行。在工作区中选择ClassView,再选择CBusDlg类,找到变量m_num,双击 该变量,可以在头文件busDlg.h中看到该变量的声明: CComboBox m_num;

趣味程序导学 Visual C++

180

找到前面加入代码的OnInitDialog()函数,接下来我们将在这个函数里加入相应的代码 完成初始化时读入记录集中所有车次信息的操作。在6.5.1节中我们介绍了记录集的滚动方 式。只要在滚动每条记录的时候,将其中的车次信息,即m_bus_number的值压入组合框 内,即可完成要求。代码如下: if(!m_database.Open(NULL,FALSE,FALSE, "ODBC;DSN=bus")) { AfxMessageBox("Failed to open database"); } m_set.Open(); CString str; m_set.MoveFirst(); while(!m_set.IsEOF()) { str.Format("%ld",m_set.m_bus_number); m_num.AddString(str); m_set.MoveNext(); } m_set.Close();

其中黑体 m_num.AddString(str) ; 是这个操作的关键部分。控制变量m_num调用函数 AddString()来完成字符串的填充功能。字符串以数字形式格式化后填入组合框内。运行程 序,输出结果如图6.42所示:

图 6.42

初始化时装入车次信息

定制显示区 设定好组合框后,下面来设计整个查询系统的显示区。由于本系统只是一个简单的公 交查询系统,所以我们静态设定了6行输出,即在默认情况下,含有同一停靠站的公交路 线不超过6条。 前面进行记录集操作时我们曾经采用了静态文本框来作为输出载体,现在我们依然采

第5章

北京市公交查询系统——数据库编程基础

181

用这种风格,但在文本框的扩展风格设定时对边缘进行了处理,以便于显示。首先在控件 控制板上拖动按钮 代表的静态文本框,拖入对话框内,改变其大小,并单击右键,选 择属性,如图6.43所示。

图 6.43

选择扩展属性

在Extended Styles标签中,选择客户和静态边缘(Client edge+Static edge)组合的方式, 则静态框显示为 。 为了方便后面的程序处理,将ID设为IDC_BUSNUM1,作为车次输出的区域。选定该 静态文本框,按住ctrl键移动鼠标则可以在指定位置复制同样大小和风格的静态文本框, 如图6.44所示:

图 6.44

静态框的快速复制

查 看 这 6 个 静 态 文 本 框 的 属 性 , 发 现 其 ID 依 次 排 列 为 IDC_BUSNUM1~IDC_ BUSNUM6。以同样方式设置路线显示区,其中扩展风格选择为客户边缘(Client edge) 并将ID设定为IDC_STANUM1~IDC_STANUM6,如图6.45所示: 响应组合框中选定的数据 绘制好输出显示区后,接下来的工作就是如何将选定车次的信息显示到这个区域内。 为了更加方便程序的处理,我们加入图标为 的按钮,用于在选择好车次后确认查询对 象。单击右键选择属性,在一般属性里设定ID为IDC_NUM,Caption设定为“查询”,如 。

182

趣味程序导学 Visual C++

图 6.45

定制显示区

打开MFC ClassWizard,在Message Maps标签中设定类名(Class name)为CbusDlg, 在ObjectIDs列表中找到IDC_NUM,并在消息(Messages)一栏中选定BN_CLICKED,作 为单击该按钮的消息映射,如图6.46所示:

图 6.46

映射单击的消息

单击Add Function按钮,在弹出的对话框内设定该消息映射对应的函数名,默认为 OnNum,如图6.47所示:

图 6.47

设定消息映射函数名

第5章

北京市公交查询系统——数据库编程基础

183

单击OK按钮,ClassWizard将在类CBusDlg 中生成OnNum函数的主体部分。代码如 下: void CBusDlg::OnNum() { // TODO: Add your control notification handler code here }

在其中,我们加入如下的代码: CString str; int i=m_num.GetCurSel(); m_set.m_strFilter=" [bus_station] "; if(m_set.IsOpen()){ m_set.Close(); } m_set.Open(); m_set.MoveFirst(); for (int j=0;jAppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); } } // Set the icon for this dialog. The framework does this // automatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE);

// Set small icon

if(!m_database.Open(NULL,FALSE,FALSE,"ODBC;DSN=bus")) { AfxMessageBox("Failed to open database"); } m_set.Open(); CString str; m_set.MoveFirst(); while(!m_set.IsEOF()) { str.Format("%ld",m_set.m_bus_number); m_num.AddString(str); m_set.MoveNext(); } m_set.Close(); // TODO: Add extra initialization here return TRUE;

// return TRUE

unless you set the focus to a control

} void CBusDlg::OnNum() { // TODO: Add your control notification handler code here CString str; int i=m_num.GetCurSel();

189

趣味程序导学 Visual C++

190

m_set.m_strFilter="[bus_station]"; if(m_set.IsOpen()){ m_set.Close(); } m_set.Open(); m_set.MoveFirst(); for (int j=0;jm_pErrorInfo->m_strDescription); e->Delete(); }

趣味程序导学 Visual C++

192

续表 编辑一条记录

try { m_set.Edit(); m_set.m_bus_number=335; m_set.m_bus_station="西单"; m_set.Update(); } catch (CDaoException *e) { AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); }

删除一条记录

代码表示为 try { m_set.Delete(); } catch (CDaoException *e) { AfxMessageBox(e->m_pErrorInfo->m_strDescription); e->Delete(); }

关闭记录集 SQL WHERE查 询语句

m_set.Close(); 完全匹配 m_set.m_strFilter=" [bus_station]='双清路'"; 模糊查询 m_set.m_strFilter=" [bus_station] like '%天坛%'";

组合框的 AddString()函数

向组合框内加入字符串,代码为 m_num.AddString(str); 其中m_num为利用ClassWizard定义的控制变量,声明如下: CComboBox m_num;

AfxMessageBox ()函数

系统提示对话框弹出函数,可以根据参数设定不同风格的对话框 AfxMessageBox("没有找到匹配数据,请重新输入! ";

第7章

俄罗斯方块游戏——Visual C++应用深入

“俄罗斯方块”是一个家喻户晓的游戏。由于它对显示分辨率的要求不高,又充满了 趣味性,曾经在掌上型游戏机上风靡一时。如今在PC机普及的时代,其吸引力仍然不减 当年。本章通过实现“俄罗斯方块”这个游戏,巩固前面所学知识,同时在此基础上引入 一些深入的技巧。

7.1

游戏效果说明

“俄罗斯方块”的规则很简单,就是在下列四种图形从屏幕上方掉下来时,巧妙的安 排布置,达到充分利用屏幕空间的目的。每当屏幕的一整行被方块积木排满时,作为奖 赏,该行从屏幕上消失,剩余的积木依次往下降一行。当积木堆积达到屏幕顶端的时候, 游戏结束,如图7.1所示:

图 7.1

俄罗斯方块

游戏的操作方法介绍: ・ 开始和暂停按钮。按Start按钮游戏开始(重新开始),此时Start按钮变成Stop按 钮。同时原来灰色禁用的Pause按钮可以使用。按下Pause按钮可以暂停和继续游戏 的运行。 ・ 游戏的主界面上有三个键分别控制方块向下、向左、向右移动。还有一个键控制 方块的顺时针旋转。 ・ 屏幕的右上角有三行数字分别显示成绩(Score)、累计消去的行数(Lines)、游 戏所处级别(Level)。 ・ 游戏结束后可以将自己的名字加入排行榜。 游戏的主界面如图7.2所示:

趣味程序导学 Visual C++

194

图 7.2

7.2

主界面

创建界面的主框架

程序的主框架建立在MFC的CPropertySheet和CPropertyPage两个基本类上。 CPropertySheet 类 对 象 表 示 属 性 表 , 或 者 说 是 标 签 对 话 框 。 一 个 属 性 表 由 一 个 CPropertySheet对象和一个或多个CPropertyPage对象构成。一个属性表由框架来显示,就 像是一个具有一系列标签索引的窗口。用户通过这些标签索引来选择当前标签,和一块用 于当前所选标签的区域。虽然CPropertySheet不是从CDialog派生而来的,但是管理一个 CPropertySheet对象类似于管理一个CDialog对象。例如,一个属性表的创建需要分两步: 首先调用构造函数,然后对模式属性表调用 DoModal,或对非模式属性表调用Create。 CPropertySheet 有 两 种 类 型 的 构 造 函 数 : CPropertySheet ∷ Construct和 CPropertySheet ∷ CPropertySheet。在一个CPropertySheet对象和某个外部对象之间交换数据,类似于与一个 CDialog对象交换数据。两者之间的主要差别是:一个属性表的设置通常是CPropertyPage 对象的成员变量,而不是CPropertySheet对象本身。此类属性表的外观可以参看图7.2,或 是打开Windows的“开始”|“控制面板”|“显示”对话框。 你可以创建一种被称为向导的标签对话框,这种对话框包括一个属性表,该表有一系 列属性标签来引导用户进行某项操作的每一个步骤,比如说设置一个设备或创建一个时事 通讯。大家比较熟悉的例子是Windows的“控制面板”中的“添加/删除硬件向导”对话框 (见图7.3)。

第7章

俄罗斯方块游戏──Visual C++应用深入

图 7.3

195

向导类型的标签对话框

我们游戏中使用到的,也将是我们主要介绍的是第一种:标签索引式的属性表。 7.2.1

用ClassWizard生成CPropertySheet

首先新建一个MFC基于Dialog的Project。在菜单的Project项下,选择Add To Project中 的Components and Controls… 。在列表中选择Visual C++ Components,得到VC控件列表 (见图7.4)。然后根据提示选择属性表上标签的个数。此时CProperySheet已经成功的导 入。在VC的类管理窗口中可以看到CMyPropertySheet和CMyPropertyPage类;在资源管理 窗口中可以看到PropertyPage的页面,可以像Dialog一样在上面添加各种控件。

图 7.4 Visual C++ 控件列表

完成了所有的操作之后,我们仍然无法看到PropertySheet。我们还需要在合适的地方 声明CPropertySheet类对象,并使用CPropertySheet∷DoModal()来显示,如图7.5所示。

趣味程序导学 Visual C++

196

图 7.5

7.2.2

由 ClassWizard 生成的 Property Sheet

CPropertySheet类成员

CPropertySheet类成员如表7.1所示。 表7.1 Data Members(数据成员)

CPropertySheet类成员

m_psh Windows PROPSHEETHEADER结构,提供对基本属性表参 数的访问

Construction(构造方式)

构造一个CPropertySheet对象

Construct Attributes(属性)

构造一个CPropertySheet对象

CPropertySheet

GetActiveIndex 获取属性表的活动页的索引 GetPageIndex 获取属性表指定页的索引 获取属性表中的页数

GetPageCount

GetPage 获取指向指定页的指针 GetActivePage 获取活动页对象 SetActivePage 设置活动页对象 SetTitle 设置属性表的标题 GetTabControl

获取指向一个标签控件的指针

SetFinishText 设置Finish按钮的文本 使向导按钮有效

SetWizardButtons SetWizardMode

使向导模式有效

EnableStackedTabs Operations(操作)

DoModal

代码属性表是使用分页方式还是滚动方式

显示一个模式属性表

Create 显示一个无模式属性表 AddPage 向属性表中添加一页 RemovePage 从属性表中移去一页 PressButton

在一个属性表中模拟对指定按钮的选择

EndDialog 终止属性表

第7章

7.2.3

俄罗斯方块游戏──Visual C++应用深入

197

成员函数

下面介绍CpropertySheet类的成员函数。 成员函数 1 CPropertySheet∷AddPage void Addpage(CPropertyPage *pPage); 参数pPage:指向要被添加到属性表中去的页。不能是NULL。 此成员函数用来将所提供的页添加到属性表中,并使它具有最右边的标签。该函数按 你所希望的从左至右的顺序来添加页。 AddPage将CPropertyPage对象添加到CPropertySheet对象的页列表中,但是并不为这些 页实际地创建窗口。直到用户选择了一页,框架才为此页创建窗口。当你用AddPage来添 加一个属性页时,CPropertySheet就是CPropertyPage的父类。为了从属性页对属性表进行 访问,可以调用CWnd∷GetParent。如果你在显示属性页之后调用AddPage,则标签行将 反映新添加的页。 成员函数 2 CPropertySheet∷Construct void Construct(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0); void Construct(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0); 其中: ・ nIDCaption:用于属性表的标题的ID。 ・ pParentWnd:指向此属性表的父窗口的指针。如果是NULL,则其父窗口就是应 用程序的主窗口。 ・ iSelectPage:最初在最顶上的页的索引。默认值是添加到表中的第一页的索引。 ・ pszCaption:指向一个字符串的指针,该字符串包含了用于属性表的标题。不能是 NULL。 此成员函数用来构造一个CPropertySheet对象。如果还没有调用一个类构造函数,则 调用此成员函数。例如,当你声明或分配CPropertySheet对象数组时,就调用Construct。 在有数组的情况下,你必须为数组中的每一个成员调用Construct。 下面的示例说明了在什么情况下你要调用Construct。 int i; CPropertySheet

grpropsheet[4];

CPropertySheet

someSheet;

//

不需要为它调用Construct

UINT rgID[4] = {IDD_SHEET1, IDD_SHEET2, IDD_SHEET3, IDD_SHEET4}; for (i= 0; i < 4; i++) grpropsheet[i].Construct(rgID[i]);

趣味程序导学 Visual C++

198

成员函数 3 CPropertySheet∷CPropertySheet CPropertySheet(); CPropertySheet(UINT nIDCaption, CWnd* pParentWnd = NULL, UINTiSelectPage = 0); CPropertySheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL,UINT iSelectPage = 0); 其中: ・ nIDCaption:用于属性表的标题的ID。 ・ pParentWnd:指向此属性表的父窗口的指针。如果是NULL,则其父窗口就是应 用程序的主窗口。 ・ iSelectPage:最初在最顶上的页的索引。默认值是添加到表中的第一页的索引。 ・ pszCaption:指向一个字符串的指针,该字符串包含了用于属性表的标题。不能是 NULL。 此函数用来构造一个CPropertySheet对象。要显示此CPropertySheet,调用DoModal或 Create。第一个参数中包含的字符串将被放置在属性表的标题栏中。如果你有多个参数 (例如,你使用的是数组),则使用Construct来代替CPropertySheet。 成员函数 4 CPropertySheet∷Create BOOL Create(CWnd* pParentWnd = NULL, DWORD dwStyle = (DWORD)-1, DWORD dwExStyle = 0); 如果成功地创建了属性表则该函数返回非零值;否则返回0。 其中: ・ pParentWnd:指向一个父窗口。如果是NULL,则父窗口是桌面。 ・ dwStyle:属性表窗口的风格。 ・ dwExStyle :属性表的扩展窗口风格。若此成员函数用来显示一个无模式的属性 表,可以在构造函数的内部调用Create,也可以在激活构造函数之后调用它。当传 递给参数dwStyle的值是-1时,表示默认风格,这种风格实际是:WS_SYSMENU |WS_POPUP | WS_CAPTION|DS_MODALFRAME | DS_CONTEXT_HELP | WS_ VISIBLE。当传递给参数dwExStyle的值是0时,表示默认扩展风格,实际是: WS_EX_DLGMO- DALFRAME。 在创建属性表之后,Create成员函数立即返回。要撤消这个属性表,可以调用CWnd ∷DestroyWindow。 调用Create来显示的无模式属性表,没有像模式属性表那样的OK,Cancel, Apply Now和Help按钮。必须由用户来创建所需要的按钮。要显示一个模式属性表,可以调用 DoModal。

第7章

俄罗斯方块游戏──Visual C++应用深入

199

成员函数 5 CPropertySheet∷DoModal virtual int DoModal(); 如果函数成功,则返回IDOK或IDCANCEL;否则返回0或-1。如果此属性表是作为一 个向导建立的,则DoModal返回ID_WIZFINISH或IDCANCEL。 此成员函数用来显示一个模式属性表。其返回值对应于用来关闭属性表的控件的ID。 此函数返回后,Windows响应这个属性表,所有的属性页都会被撤消。而这些对象本身仍 然存在。通常,你将在DoModal返回IDOK之后从CPropertyPage对象检索数据。 要显示一个无模式属性表,请调用Create来代替此函数。

M

注意:在第一次从对话框资源创建一个属性页时,它有可能引发first- chance异常。这是由于在创建此页之前属性页将对话框资源的风格改变成了 所需的风格。因为资源通常来说是只读的,所以这导致了一个异常。这个异 常由系统处理,系统会自动拷贝修改后的资源。这样,first-chance异常就 被忽略了。  由于这个异常必须由操作系统来处理,所以不要用一个C++ try/catch块来 隐藏调用 CPropertySheet∷DoModal,因为在一个 C++ try/catch 块中catch 会处理所有的异常,比如, catch(...),它将处理那些属于操作系统的异 常,这将导致不可预料的行为发生。但是,通过指定异常类型或结构化异常 处理来处理C++异常就是安全的,在结构化异常处理中,访问非法异常被传 递给操作系统。 

成员函数 6 CPropertySheet∷EnableStackedTabs void EnableStackedTabs(BOOL bStacked); 参 数 bStacked 表 明 在 属 性 表 中 分 页 式 方 式 是 否 是 有 效 的 。 通 过 设 置 bStacked 为 FALSE,可以使分页式方法无效。 此成员函数用来表明在一个属性表中是否使用分页式方式。默认的,如果一个属性表 有很多标签,属性表的宽度不足以把它们放在一行中,则这些标签将按多行分页。要使用 滚动方式来代替分页方式,请在调用DoModal或Create之前将bStacked设置为FALSE来调 用EnableStackedTabs。 当你创建一个模式或无模式的属性表时,你必须调用EnableStackedTabs。为了在一个 CPropertySheet派生类中混合这种风格,请为 WM_CREATE写一个消息句柄。在 CWnd∷ OnCreate的重载版本中,在调用基类实现之前调用EnableStackedTabs(FALSE)。 例如: int CMyPropertySheet∷OnCreate(LPCREATESTRUCT lpCreateStruct) {

趣味程序导学 Visual C++

200 // 设置为滚动标签风格

EnableStackedTabs(FALSE); // 调用基类 if(CPropertySheet∷OnCreate(lpCreateStruct) == -1) return ?; // TODO:在此添加你的指定创建代码 return 0; }

成员函数 7 CPropertySheet∷EndDialog void EndDialog(int nEndID); 其中: nEndID:用来作为属性表的返回值的标识符。 此成员函数用来终止属性表。当按下OK,Cancel或Close按钮时,框架调用这个成员 函数。如果发生了一个要改变此属性表的事件,也调用此成员函数。 成员函数 8 CPropertySheet∷GetActiveIndex GetActiveIndex() const; 此成员函数用来获取属性表窗口中的活动页的索引号,然后用这个返回的索引号作为 GetPage的参数。 成员函数 9 CPropertySheet∷GetActivePage CPropertyPage* GetActivePage() const; 此成员函数用来获取属性表窗口中的活动页。使用这个函数可以对活动页执行某些动 作。 成员函数 10 CPropertySheet∷GetPage CPropertyPage* GetPage(int nPage) const; 参数nPage为所希望的页的索引,从0开始。其值必须在0和属性表的页数之间。 此成员函数返回一个指向此属性表中的指定页的指针。

第7章

俄罗斯方块游戏──Visual C++应用深入

7.3

201

显 示 背 景

在VC中要使用CBitmap类必须将BMP位图装入资源中,然后通过类 CBitmap的成员 函数使用它,再通过CDC类的成员函数操作它。这样做有两点缺陷:将位图装入资源导致 可执行文件增大,不利于软件发行;只能使用资源中有限的位图,无法选取其他位图。而 且BMP位图文件是以DIB(设备无关位图)方式保存,BMP位图装入资源后被转换为DDB (设备相关位图),类CBitmap就是对一系列DDB操作的API函数进行了封装,使用起来 有一定的局限性,不如DIB可以独立于平台。 要弥补使用资源位图的两点不足,可以直接使用BMP位图文件。VC自带的示例中提 供了一种方法读取并显示 BMP位图文件。首先使用API函数GlobalAlloc 分配内存并创建 HDIB位图句柄,所有操作直接读写内存,然后通过StrechDIBits及SetDIBsToDevice函数来 显示于屏幕上。 总之,要显示一幅位图,首先应得到该图的有关信息,通过位图的颜色表创建一个逻 辑调色板,然后将这个调色板选入设备环境,实现这个调色板,最后将位图用BitBlt函数 拷贝到设备环境就可以了。 具体实现步骤如下: (1)首先装入一幅位图,该位图既可以以资源的形式与程序绑在一起,也可以以文 件的形式从外部装入。然后将该位图与一个CBitmap对象联系(Attach)起来。在此使用 API函数LoadImage(),而不是CBitmap类的成员函数CBitmap∷LoadBitmap(),因为我们需 要得到该位图的DIBSECTION结构,从这个结构中我们可以得到该位图的色彩信息,从而 建立一个与这些色彩相匹配的逻辑调色板。使用CBitmap∷LoadBitmap()将会失去我们所 需的位图的色彩信息。 (2)得到位图后,接下来要取得该位图的色彩信息。通过CBitmap:GetObject()函 数,我们可以访问DIBSECTION结构,从中得到位图的色彩数。一般来说,这些信息存在 于BITMAPINFOHEAD 结构中,不过,作为DIBSECTION结构的一部分,BITMAPINFOHEAD有时并未说明图像用了多少种颜色;碰到这种情况,我们可以看看图像的每一像素 用了几位(Bit)来描述颜色,如果是8位的话,因为8位二进制数可以表示 256种不同的 值,所以该图像是256色的;同理,16位表明是64K色。得到了位图所用的颜色数,就可 以创建逻辑调色板了。超过256色的位图是没有颜色表(Color Table)的,这时我们只需简单 地创建一个和设备环境兼容的半色调调色板(Halftone Palette)就行了,在半色调调色板中包 含着所有不同颜色的样本。这显然不是最佳解决方案,但却是最简单的。 而对于小于或等于256色的位图,我们就要从头建立一个新的调色板。先分配足够的 内存空间来装入图像的颜色表,颜色表可以利用API函数GetDIBColorTable 获得;然后再 分配足够的内存给新建的逻辑调色板,将刚才得到的颜色表信息拷入新建调色板中的 PalEntry域,并将PalVersion域设为0X300。创建了调色板后,应将窗口刷新重画。 具体的实现过程如下:

趣味程序导学 Visual C++

202

DIB.h class CDIBitmap { friend class CBmpPalette; BITMAPINFO *m_pInfo; BYTE *m_pPixels; CBmpPalette * m_pPal; BOOL m_bIsPadded; public://构造函数 CDIBitmap(); virtual ~CDIBitmap(); private: CDIBitmap(const CDIBitmap& dbmp); public://图像属性 BITMAPINFO *GetHeaderPtr() const; BYTE *GetPixelPtr() const; RGBQUAD *GetColorTablePtr() const; int GetWidth() const; int GetHeight() const; CBmpPalette * GetPalette() { return m_pPal; } public:// 操作 BOOL void BOOL BOOL BOOL BOOL BOOL void BOOL

CreatePalette(); ClearPalette(); // 清除图像的调色板 CreateFromBitmap(CDC *, CBitmap *); LoadResource(LPCTSTR ID); LoadResource(UINT ID) { return LoadResource(MAKEINTRESOURCE(ID)); } LoadBitmap(UINT ID) { return LoadResource(ID); } LoadBitmap(LPCTSTR ID) { return LoadResource (ID); } DestroyBitmap(); DeleteObject() { DestroyBitmap(); return TRUE; }

public: // 在指定的位置显示位图 virtual void DrawDIB(CDC * pDC, int x=0, int y=0); // 画出位图并拉伸或压缩 virtual void DrawDIB(CDC * pDC, int x, int y, int width, int height);

第7章

俄罗斯方块游戏──Visual C++应用深入

203

// 在DC的某个区域中显示位图 virtual int DrawDIB(CDC * pDC,CRect & rectDC, CRect & rectDIB); // 从文件读入位图 virtual BOOL Load(CFile * pFile); virtual BOOL Load(const CString &); // 存储文件到位图 virtual BOOL Save(CFile * pFile); virtual BOOL Save(const CString &); protected: int int DWORD DWORD DWORD BOOL BOOL WORD

GetPalEntries() const; GetPalEntries(BITMAPINFOHEADER& infoHeader) const; GetBitsPerPixel() const; LastByte(DWORD BitsPerPixel, DWORD PixelCount) const; GetBytesPerLine(DWORD BitsPerPixel, DWORD Width) const; PadBits(); UnPadBits(); GetColorCount() const;

};

DIB.c #define PADWIDTH(x)

(((x)*8 + 31)

CDIBitmap ∷ CDIBitmap() : m_pInfo(0) , m_pPixels(0) , m_pPal(0) , m_bIsPadded(FALSE) { } CDIBitmap ∷ ~CDIBitmap() { delete [] (BYTE*)m_pInfo; delete [] m_pPixels; delete m_pPal; } void CDIBitmap ∷ DestroyBitmap() { delete [] (BYTE*)m_pInfo; delete [] m_pPixels; delete m_pPal; m_pInfo = 0;

& (~31))/8

趣味程序导学 Visual C++

204 m_pPixels = 0; m_pPal = 0; }

BOOL CDIBitmap ∷ CreateFromBitmap(CDC * pDC, CBitmap * pSrcBitmap) { ASSERT_VALID(pSrcBitmap); ASSERT_VALID(pDC); try { BITMAP bmHdr; // 得到位图信息头 pSrcBitmap->GetObject(sizeof(BITMAP), &bmHdr); // 为图像重新分配空间 if(m_pPixels) { delete [] m_pPixels; m_pPixels = 0; } DWORD dwWidth; if (bmHdr.bmBitsPixel > 8) dwWidth = PADWIDTH(bmHdr.bmWidth * 3); else dwWidth = PADWIDTH(bmHdr.bmWidth); m_pPixels = new BYTE[dwWidth*bmHdr.bmHeight]; if(!m_pPixels) throw TEXT("could not allocate data storage\n"); // 根据位图头信息确定大致的颜色数 WORD wColors; switch(bmHdr.bmBitsPixel) { case 1 : wColors = 2; break; case 4 : wColors = 16; break; case 8 : wColors = 256; break; default :

第7章

俄罗斯方块游戏──Visual C++应用深入

205

wColors = 0; break; } // 重新分配BITMAPINFO 结构 if(m_pInfo) { delete [] (BYTE*)m_pInfo; m_pInfo = 0; } m_pInfo = (BITMAPINFO*)new BYTE[sizeof(BITMAPINFOHEADER) + wColors*sizeof(RGBQUAD)]; if(!m_pInfo) throw TEXT("could not allocate BITMAPINFO struct\n"); // 拼装BITMAPINFO 头信息 m_pInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); m_pInfo->bmiHeader.biWidth = bmHdr.bmWidth; m_pInfo->bmiHeader.biHeight = bmHdr.bmHeight; m_pInfo->bmiHeader.biPlanes = bmHdr.bmPlanes;

if(bmHdr.bmBitsPixel > 8) m_pInfo->bmiHeader.biBitCount = 24; else m_pInfo->bmiHeader.biBitCount = bmHdr.bmBitsPixel; m_pInfo->bmiHeader.biCompression = BI_RGB; m_pInfo->bmiHeader.biSizeImage = ((((bmHdr.bmWidth * bmHdr.bmBitsPixel) + 31) & ~31) >> 3) * bmHdr.bmHeight; m_pInfo->bmiHeader.biXPelsPerMeter = 0; m_pInfo->bmiHeader.biYPelsPerMeter = 0; m_pInfo->bmiHeader.biClrUsed = 0; m_pInfo->bmiHeader.biClrImportant = 0; // 得到点阵信息 int test = ∷GetDIBits(pDC->GetSafeHdc(),(HBITMAP)pSrcBitmap>GetSafeHandle(),0, (WORD)bmHdr.bmHeight, m_pPixels, m_pInfo,DIB_RGB_COLORS); if(test != (int)bmHdr.bmHeight) throw TEXT("call to GetDIBits did not return full number

趣味程序导学 Visual C++

206

of requested scan lines\n"); CreatePalette(); m_bIsPadded = FALSE; #ifdef _DEBUG } catch(TCHAR * psz) { TRACE1("CDIBitmap∷CreateFromBitmap(): %s\n", psz); #else } catch(TCHAR *) { #endif if(m_pPixels) { delete [] m_pPixels; m_pPixels = 0; } if(m_pInfo) { delete [] (BYTE*) m_pInfo; m_pInfo = 0; } return FALSE; } return TRUE; } BOOL CDIBitmap ∷ LoadResource(LPCTSTR pszID) { HBITMAP hBmp = (HBITMAP) ∷LoadImage( AfxGetInstanceHandle(), pszID, IMAGE_BITMAP, 0,0, LR_CREATEDIBSECTION ); if(hBmp == 0) return FALSE; CBitmap bmp; bmp.Attach(hBmp); CClientDC cdc(CWnd∷GetDesktopWindow()); BOOL bRet = CreateFromBitmap(&cdc, &bmp); bmp.DeleteObject(); return bRet; }

第7章

俄罗斯方块游戏──Visual C++应用深入

207

BOOL CDIBitmap ∷ Load(CFile* pFile) { ASSERT(pFile); BOOL fReturn = TRUE; try { delete [] (BYTE*)m_pInfo; delete [] m_pPixels; m_pInfo = 0; m_pPixels = 0; DWORD dwStart = pFile->GetPosition(); // 确定得到位图,位图头文件中的前两个字节必须是“B”或“M” BITMAPFILEHEADER fileHeader; pFile->Read(&fileHeader, sizeof(fileHeader)); if(fileHeader.bfType != 0x4D42) throw TEXT("Error:Unexpected file type, not a DIB\n"); BITMAPINFOHEADER infoHeader; pFile->Read(&infoHeader, sizeof(infoHeader)); if(infoHeader.biSize != sizeof(infoHeader)) throw TEXT("Error:OS2 PM BMP Format not supported\n"); // 存储 DIB 结构的大小 int cPaletteEntries = GetPalEntries(infoHeader); int cColorTable = 256 * sizeof(RGBQUAD); int cInfo = sizeof(BITMAPINFOHEADER) + cColorTable; int cPixels = fileHeader.bfSize - fileHeader.bfOffBits; m_pInfo = (BITMAPINFO*)new BYTE[cInfo]; // 为位图的信息头结构分配存储 // 控件 memcpy(m_pInfo, &infoHeader, sizeof(BITMAPINFOHEADER)); // 从文件中拷贝信息头 pFile->Read(((BYTE*)m_pInfo) + sizeof(BITMAPINFOHEADER), cColorTable); // 从文件中读取调色板信息 m_pPixels = new BYTE[cPixels]; // 为像素分配控件 pFile->Seek(dwStart + fileHeader.bfOffBits, CFile∷begin); pFile->Read(m_pPixels, cPixels); // 从文件读取像素信息 CreatePalette(); m_bIsPadded = TRUE; #ifdef _DEBUG } catch(TCHAR * psz) { TRACE(psz); #else } catch(TCHAR *) { #endif fReturn = FALSE;

趣味程序导学 Visual C++

208 } return fReturn; }

BOOL CDIBitmap ∷ Load(const CString & strFilename) { CFile file; if(file.Open(strFilename, CFile∷modeRead)) return Load(&file); return FALSE; } BOOL CDIBitmap ∷ Save(const CString & strFileName) { ASSERT(! strFileName.IsEmpty()); CFile File; if(!File.Open(strFileName, CFile∷modeCreate|CFile∷modeWrite)) { TRACE1("CDIBitmap∷Save(): Failed to open file %s for writing.\n", LPCSTR(strFileName)); return FALSE; } return Save(&File); }

// 在这里不打开和关闭文件,假设调用者会完成该操作 BOOL CDIBitmap ∷ Save(CFile * pFile) { ASSERT_VALID(pFile); ASSERT(m_pInfo); ASSERT(m_pPixels); BITMAPFILEHEADER bmfHdr; DWORD dwPadWidth = PADWIDTH(GetWidth()); // 确认位图的格式 PadBits(); bmfHdr.bfType = 0x4D42; //初始化位图信息头大小 DWORD dwImageSize= m_pInfo->bmiHeader.biSize; // 加入调色板的大小

第7章

俄罗斯方块游戏──Visual C++应用深入

WORD wColors = GetColorCount(); WORD wPaletteSize = (WORD)(wColors*sizeof(RGBQUAD)); dwImageSize += wPaletteSize; // 加入实际的点阵数组大小 dwImageSize += PADWIDTH((GetWidth())*DWORD(m_pInfo ->bmiHeader.biBitCount)/8) * GetHeight(); m_pInfo->bmiHeader.biSizeImage = 0; bmfHdr.bfSize = dwImageSize + sizeof(BITMAPFILEHEADER); bmfHdr.bfReserved1 = 0; bmfHdr.bfReserved2 = 0; bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + m_pInfo ->bmiHeader.biSize + wPaletteSize; pFile->Write(&bmfHdr, sizeof(BITMAPFILEHEADER)); pFile->Write(m_pInfo, sizeof(BITMAPINFO) + (wColors-1) *sizeof(RGBQUAD)); pFile->WriteHuge(m_pPixels,DWORD((dwPadWidth*(DWORD)m_pInfo-> bmiHeader.biBitCount*GetHeight())/8)); return TRUE; } BOOL CDIBitmap ∷ CreatePalette() { if(m_pPal) delete m_pPal; m_pPal = 0; ASSERT(m_pInfo); // 颜色数小于256时需要调色板.否则内存将溢出 if(m_pInfo->bmiHeader.biBitCount GetSafeHdc(); CPalette * pOldPal = 0; if(m_pPal) { pOldPal = pDC->SelectPalette(m_pPal, FALSE); pDC->RealizePalette(); // 确认使用最佳的拉伸模式 pDC->SetStretchBltMode(COLORONCOLOR); } if(m_pInfo) StretchDIBits(hdc, x, y, width, height, 0, 0, GetWidth(), GetHeight(), GetPixelPtr(), GetHeaderPtr(), DIB_RGB_COLORS, SRCCOPY); if(m_pPal) pDC->SelectPalette(pOldPal, FALSE); } int CDIBitmap ∷ DrawDIB(CDC * pDC, CRect & rectDC, CRect & rectDIB) { ASSERT(pDC); HDC

hdc = pDC->GetSafeHdc();

CPalette * pOldPal = 0; if(m_pPal) { pOldPal = pDC->SelectPalette(m_pPal, FALSE); pDC->RealizePalette();

第7章

俄罗斯方块游戏──Visual C++应用深入

211

// 确认用最佳的拉伸模式 pDC->SetStretchBltMode(COLORONCOLOR); } int nRet = 0; if(m_pInfo) nRet = SetDIBitsToDevice( hdc,

// Device

rectDC.left,

// DestX

rectDC.top, rectDC.Width(),

// DestY // DestWidth

rectDC.Height(),

// DestHeight

rectDIB.left, // SrcX GetHeight() -rectDIB.top -rectDIB.Height(), // SrcY 0, GetHeight(),

// StartScan // NumScans

GetPixelPtr(),

// color data

GetHeaderPtr(), DIB_RGB_COLORS

// header data // color usage

); if(m_pPal) pDC->SelectPalette(pOldPal, FALSE); return nRet; } BITMAPINFO * CDIBitmap ∷ GetHeaderPtr() const { ASSERT(m_pInfo); ASSERT(m_pPixels); return m_pInfo; } RGBQUAD * CDIBitmap ∷ GetColorTablePtr() const { ASSERT(m_pInfo); ASSERT(m_pPixels); RGBQUAD* pColorTable = 0; if(m_pInfo != 0) { int cOffset = sizeof(BITMAPINFOHEADER); pColorTable = (RGBQUAD*)(((BYTE*)(m_pInfo)) + cOffset); }

趣味程序导学 Visual C++

212 return pColorTable; }

BYTE * CDIBitmap ∷ GetPixelPtr() const { ASSERT(m_pInfo); ASSERT(m_pPixels); return m_pPixels; } int CDIBitmap ∷ GetWidth() const { ASSERT(m_pInfo); return m_pInfo->bmiHeader.biWidth; } int CDIBitmap ∷ GetHeight() const { ASSERT(m_pInfo); return m_pInfo->bmiHeader.biHeight; } WORD CDIBitmap ∷ GetColorCount() const { ASSERT(m_pInfo); switch(m_pInfo->bmiHeader.biBitCount) case 1: return 2; case 4:

return 16;

case 8: default:

return 256; return 0;

{

} } int CDIBitmap ∷ GetPalEntries() const { ASSERT(m_pInfo); return GetPalEntries(*(BITMAPINFOHEADER*)m_pInfo); } int CDIBitmap ∷ GetPalEntries(BITMAPINFOHEADER& infoHeader) const { int nReturn; if(infoHeader.biClrUsed == 0) nReturn = (1 bmiHeader.biBitCount; } DWORD CDIBitmap ∷ LastByte(DWORD dwBitsPerPixel, DWORD dwPixels) const { register DWORD dwBits = dwBitsPerPixel * dwPixels; register DWORD numBytes = dwBits / 8; register DWORD extraBits = dwBits - numBytes * 8; return (extraBits % 8) ? numBytes+1 : numBytes; }

DWORD CDIBitmap ∷ GetBytesPerLine(DWORD dwBitsPerPixel, DWORD dwWidth) const { DWORD dwBits = dwBitsPerPixel * dwWidth; if((dwBits % 32) == 0) return (dwBits/8);

// 像素以32位为单位存储

DWORD dwPadBits = 32 - (dwBits % 32); return (dwBits/8 + dwPadBits/8 + (((dwPadBits % 8) > 0) ? 1 : 0)); } BOOL CDIBitmap ∷ PadBits() { if(m_bIsPadded) return TRUE; // 像素所占的存储空间大于1字节时作调整 DWORD dwAdjust = 1, dwOffset = 0, dwPadOffset=0; BOOL bIsOdd = FALSE; dwPadOffset = GetBytesPerLine(GetBitsPerPixel(), GetWidth()); dwOffset = LastByte(GetBitsPerPixel(), GetWidth()); if(dwPadOffset == dwOffset) return TRUE; BYTE * pTemp = new BYTE [GetWidth()*dwAdjust]; if(!pTemp) {

趣味程序导学 Visual C++

214

TRACE1("CDIBitmap ∷ PadBits(): could not allocate row of width %d.\n", GetWidth()); return FALSE; } // 已经为像素数组分配了足够的空间,包括必要的填充,以满足每行都是32位的倍数,填充 // 是以4字节(32位)为单位的 for(DWORD row = GetHeight()-1 ; row>0 ; --row) { CopyMemory((void *)pTemp, (const void *)(m_pPixels + (row*dwOffset)), dwOffset); CopyMemory((void*)(m_pPixels+(row*dwPadOffset)), (const void *)pTemp, dwOffset); } delete [] pTemp; return TRUE; } BOOL CDIBitmap∷UnPadBits() { if(! m_bIsPadded) return TRUE; DWORD dwAdjust = 1; BOOL bIsOdd = FALSE; DWORD dwPadOffset = GetBytesPerLine(GetBitsPerPixel(), GetWidth()); DWORD dwOffset = LastByte(GetBitsPerPixel(), GetWidth()); BYTE * pTemp = new BYTE [dwOffset]; if(!pTemp) { TRACE1("CDIBitmap∷UnPadBits() could not allocate row of width %d.\n", GetWidth()); return FALSE; } for(DWORD row=1 ; row < DWORD(GetHeight()); ++row) { CopyMemory((void *)pTemp, (const void *)(m_pPixels + row*(dwPadOffset)), dwOffset); CopyMemory((void *)(m_pPixels + (row*dwOffset)), (const void *)pTemp, dwOffset); }

第7章

俄罗斯方块游戏──Visual C++应用深入

215

delete ∷ pTemp; return TRUE; }

7.4 7.4.1

方块的显示和控制

显示窗口

为了显示当前运动中的方块,同时显示下一个方块,我们需要在面板上加入两个显示 区域。游戏的主界面利用的是CPropertySheet中的一个CPropertyPage,我们称之为“面 板”,而面板上用于显示的区域我们选用Windows Custom Control,如图7.6所示。 当把Custom Control拖放到CPropertyPage上后,尝试运行发现带有Custom Control的那 页不能正常显示。原因是Custom Control控件句柄需要赋予一个由CWnd类派生的类,并且 注册窗口。

图 7.6

加入 Custom Control 用于显示

由CWnd类我们派生两个类: CGameBoard和CPiecePreview 。它们的注册函数如下所 示: BOOL CPiecePreview ∷ Register() { WNDCLASS wc; wc.style = CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = PiecePreviewWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0;

趣味程序导学 Visual C++

216

wc.hInstance = 0; wc.hIcon = 0; wc.hCursor = 0; wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.lpszMenuName = 0; wc.lpszClassName = TEXT("TetrisPreview"); VERIFY(RegisterClass(&wc)); return TRUE; } BOOL CGameBoard ∷ Register() { WNDCLASS wc; wc.style = CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = GameBoardWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = 0; wc.hIcon = 0; wc.hCursor = 0; wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.lpszMenuName = 0; wc.lpszClassName = TEXT("TetrisGameBoard"); VERIFY(RegisterClass(&wc)); return TRUE; }

此时可以像操作普通窗口那样操作如图7.6所示的两个显示区域了。有了画板,就可 以在上面绘制方块了。 7.4.2

定义方块的数据结构

定义一个4×4的方形区域,共16个方格。用“0”和“1”来表示每个方格是绘制还是 空着。由此可以组合出多种图形:

图 7.7

绘制方块

第7章

俄罗斯方块游戏──Visual C++应用深入

217

方块的旋转是通过绘制四个方向的方块,在不同时刻显示一个方向的方块来完成的。 所以程序控制方块的旋转方向,只要控制显示哪幅图就可以了。从下列代码中可以清楚地 看到这一点: class CPiece { protected: enum { NoOfSquares = 4, NoOfDirections = 4 }; char

m_chFigure[NoOfDirections] [NoOfSquares][NoOfSquares];

short

m_sDirection;

protected: CPiece() : m_sDirection(0) { for(register short d=0 ; d < NoOfDirections ; d++) for(register short l=0 ; l < NoOfSquares ; l++) for(register short c=0 ; c < NoOfSquares ; c++)m_chFigure[ d ][ l ][ c ] = 0 ; } public: virtual ~CPiece() {} public: int int

GetLines() const { return NoOfSquares ; } GetColumns() const { return NoOfSquares ; }

BOOL

IsSquare(int nLine, int nCol) const { return m_chFigure[ m_sDirection ][ nLine] [ nCol] ? TRUE : FALSE ;

} void

Rotate() { --m_sDirection ; m_sDirection &= NoOfDirections - 1 ;

} void

BackRotate() { --m_sDirection ; m_sDirection &= NoOfDirections - 1 ;

趣味程序导学 Visual C++

218 }

virtual short GetPoints() const = 0; };

class CLongPiece : public CPiece { public: CLongPiece() { for(register short c=0 ; c < NoOfSquares ; c++) m_chFigure[0][1][c]=m_chFigure[2][1][c] = 1; for(register short l = 0 ; l < NoOfSquares ; l++) m_chFigure[1][l][1]=m_chFigure[3][l][1] = 1; } virtual short GetPoints() const { return 2; } }; class CSquarePiece : public CPiece { public: CSquarePiece() { for(register short d=0 ; d < NoOfDirections ; d++) m_chFigure[d][1][1]=m_chFigure[d][1][2]= m_chFigure[d][2][1]=m_chFigure[d][2][2]=1; } virtual short GetPoints() const { return 1; } };

class CLShapePiece : public CPiece { public: CLShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][2][3]=1; m_chFigure[1][1][2]=m_chFigure[1][2][2]=m_chFigure[1][3][2]=1; m_chFigure[1][3][1]=1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][1][1]=1; m_chFigure[3][1][1]=m_chFigure[3][2][1]=m_chFigure[3][3][1]=1; m_chFigure[3][1][2] = 1; }

第7章

俄罗斯方块游戏──Visual C++应用深入

219

virtual short GetPoints() const { return 3; } }; class CRevLShapePiece : public CPiece { public: CRevLShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][2][1]=1; m_chFigure[1][1][2]=m_chFigure[1][2][2]=m_chFigure[1][3][2]=1; m_chFigure[1][1][1]=1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][1][3]=1; m_chFigure[3][1][1]=m_chFigure[3][2][1]=m_chFigure[3][3][1]=1; m_chFigure[3][3][2]=1; } virtual short GetPoints() const { return 3; } };

class CTeeShapePiece : public CPiece { public: CTeeShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][2][2]= 1; m_chFigure[1][1][1]=m_chFigure[1][2][1]=m_chFigure[1][3][1]=1; m_chFigure[1][2][2]= 1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][1][2]= 1; m_chFigure[3][1][2]=m_chFigure[3][2][2]=m_chFigure[3][3][2]=1; m_chFigure[3][2][1] = 1; } virtual short GetPoints() const { return 4; } }; class CSShapePiece : public CPiece { public: CSShapePiece() { m_chFigure[0][1][2]=m_chFigure[0][2][2]=m_chFigure[0][2][1]=1; m_chFigure[0][3][1]=1; m_chFigure[1][1][0]=m_chFigure[1][1][1]=m_chFigure[1][2][1]=1; m_chFigure[1][2][2]=1;

趣味程序导学 Visual C++

220

m_chFigure[2][1][2]=m_chFigure[2][2][2]=m_chFigure[2][2][1]=1; m_chFigure[2][3][1]=1; m_chFigure[3][1][0]=m_chFigure[3][1][1]=m_chFigure[3][2][1]=1; m_chFigure[3][2][2]=1; } virtual short GetPoints() const { return 5; } }; class CRevSShapePiece : public CPiece { public: CRevSShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][2][1]=m_chFigure[0][2][2]=1; m_chFigure[0][3][2]=1; m_chFigure[1][2][1]=m_chFigure[1][2][2]=m_chFigure[1][1][2]=1; m_chFigure[1][1][3]=1; m_chFigure[2][1][1]=m_chFigure[2][2][1]=m_chFigure[2][2][2]=1; m_chFigure[2][3][2]=1; m_chFigure[3][2][1]=m_chFigure[3][2][2]=m_chFigure[3][1][2]=1; m_chFigure[3][1][3]=1; } virtual short GetPoints() const { return 5; } }; // extended figure set from here on : class CCrossShapePiece : public CPiece { public: CCrossShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][2][2]=m_chFigure[0][0][2]= 1; m_chFigure[1][1][1]=m_chFigure[1][1][2]=m_chFigure[1][1][3]=1; m_chFigure[1][2][2]=m_chFigure[1][0][2]= 1; m_chFigure[2][1][1]=m_chFigure[2][1][2]=m_chFigure[2][1][3]=1; m_chFigure[2][2][2]=m_chFigure[2][0][2]= 1; m_chFigure[3][1][1]=m_chFigure[3][1][2]=m_chFigure[3][1][3]=1; m_chFigure[3][2][2]=m_chFigure[3][0][2]= 1; } virtual short GetPoints() const { return 7; } }; class CUShapePiece : public CPiece {

第7章

俄罗斯方块游戏──Visual C++应用深入

221

public: CUShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][2][1]=m_chFigure[0][2][3]= 1; m_chFigure[1][1][2]=m_chFigure[1][2][2]=m_chFigure[1][3][2]=1; m_chFigure[1][1][1]=m_chFigure[1][3][1]= 1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][1][3]=m_chFigure[2][1][1]= 1; m_chFigure[3][1][1]=m_chFigure[3][2][1]=m_chFigure[3][3][1]=1; m_chFigure[3][3][2]=m_chFigure[3][1][2]= 1; } virtual short GetPoints() const { return 6; } }; class CZShapePiece : public CPiece { public: CZShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][0][1]=m_chFigure[0][2][3]= 1; m_chFigure[1][1][2]=m_chFigure[1][2][2]=m_chFigure[1][3][2]=1; m_chFigure[1][1][3]=m_chFigure[1][3][1]= 1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][3][3]=m_chFigure[2][1][1]= 1; m_chFigure[3][1][1]=m_chFigure[3][2][1]=m_chFigure[3][3][1]=1; m_chFigure[3][3][0]=m_chFigure[3][1][2]= 1; } virtual short GetPoints() const { return 5; } };

class CRevZShapePiece : public CPiece { public: CRevZShapePiece() { m_chFigure[0][1][1]=m_chFigure[0][1][2]=m_chFigure[0][1][3]=1; m_chFigure[0][0][3]=m_chFigure[0][2][1]= 1; m_chFigure[1][1][2]=m_chFigure[1][2][2]=m_chFigure[1][3][2]=1; m_chFigure[1][3][3]=m_chFigure[1][1][1]= 1; m_chFigure[2][2][1]=m_chFigure[2][2][2]=m_chFigure[2][2][3]=1; m_chFigure[2][3][1]=m_chFigure[2][1][3]= 1; m_chFigure[3][1][1]=m_chFigure[3][2][1]=m_chFigure[3][3][1]=1; m_chFigure[3][1][0]=m_chFigure[3][3][2]= 1;

趣味程序导学 Visual C++

222 }

virtual short GetPoints() const { return 5; } };

7.4.3

方块的显示

根据当前的数据,我们可以方便的在自定义窗口中绘制方块。运用MFC的消息映射机 制,通过重载CWnd类的 CWnd∷OnPaint()函数,在窗口中可画出具有立体效果的方块。 void CPiecePreview∷OnPaint() { if(m_pPiece) { CPaintDC dc(this);

//得到当前视图类的画布

CRect rect; GetClientRect(rect); //得到当前窗口的大小、位置 COLORREF clrTopLeft = ∷GetSysColor(COLOR_BTNHILIGHT); COLORREF clrBottomRight = ∷GetSysColor(COLOR_BTNSHADOW);

// 计算位置 register const int nLines = m_pPiece->GetLines(); register const int nCols

= m_pPiece->GetColumns();

register const int nSquareWidth=(rect.Width() / nCols)-1; register const int nSquareHeight=(rect.Height() / nLines)-1; register const int nHOffset=(rect.Width()(nSquareWidth* nCols))/2; register const int nVOffset = (rect.Height()(nSquareHeight * nLines)) / 2; for(register int l = nLines-1 ; l >= 0 ; --l) for(register int c = 0 ; c < nCols ; ++c) if(m_pPiece->IsSquare(nLines-1-l, c))

{

dc.FillSolidRect(nHOffset+(c*nSquareWidth), nVOffset+(l*nSquareHeight), nSquareWidth, nSquareHeight, m_clrPiece); dc.Draw3dRect(nHOffset+(c*nSquareWidth), nVOffset+(l*nSquareHeight), nSquareWidth,

第7章

俄罗斯方块游戏──Visual C++应用深入

223

nSquareHeight, clrTopLeft, clrBottomRight); } } else CWnd∷OnPaint(); }

7.4.4

截获键盘操作

程序主要由键盘控制,由键盘来操纵方块的运动,包括左、右移动,顺时针、逆时针 的转动,急速下降到底部等。 键盘的输入实际是两步过程。Windows用虚拟键码把WM_KEYDOWN和WM_KEYUP 发送到一个窗口中,但是,在它们到达窗口之前,已经过转换。如果键入一个ANSI字符 (导致产生WM_KEYDOWN消息),转换函数检查键盘 shift状态,然后用正确的代码发送 WM_CHAR消息,这个代码可以大写,也可以小写。光标键和功能键没有代码,所以不需 要进行转换。窗口只获得WM_KEYDOWN和WM_KEYUP消息。 可以使用ClassWizard把所有这些消息映射到视图上。如果希望处理字符,就映射 WM_CHAR;如果希望处理其他的按键,则映射WM_KEYDOWN,MFC库把字符代码或 者虚拟键码作为处理函数的参数。 下面构造消息映射函数,截获键盘消息,并且得到当前按键的值。 首先声明消息映射函数: afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);

其中: ・ nChar:描述了按键的键码。 ・ nRepCnt:表示用户持续按键多少次后程序认为是持续按键。 ・ NFlags:包含了按键的扫描码、上次按键的情况等。 每当键盘上的一个键被按下的时候,Windows产生一个WM_KEYDOWN消息,该消 息提醒执行消息映射函数OnKeyDown(),由它来处理对应的操作: void CGameBoard∷OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if(m_pCurPiece) { switch(nChar) { case VK_LEFT: case VK_NUMPAD4: MoveLeft();//向左移动 break; case VK_RIGHT:

趣味程序导学 Visual C++

224

case VK_NUMPAD6: MoveRight();//向右移动 break; case VK_UP: case VK_NUMPAD8: case VK_NUMPAD5: Rotate();// 方块的旋转 break; case VK_DOWN: case VK_NUMPAD2: BackRotate();//反向旋转 break; case VK_SPACE: case VK_NUMPAD0: if(!(nFlags & (1 it->m_uScore) break; CString strName; TCHAR name[256]; DWORD dwSize = 255; ∷ZeroMemory(PVOID(name), 256); if(∷GetUserName(name, &dwSize)) { name[dwSize] = TEXT('\0'); strName = name; } m_ScoreArray.insert(it, ScoreTag(strName, uScore, uLevel)); if(m_ScoreArray.size() >= MAXSCORE) m_ScoreArray.resize(MAXSCORE); VERIFY(AddHiScore(index, strName, uScore, uLevel) == index); m_bCanEditName = TRUE;

趣味程序导学 Visual C++

230 CEdit * pEdit =

m_ctrlScore.EditLabel(index);

ASSERT(pEdit != 0); pEdit->LimitText(30); }

界面显示如图7.8所示。

图 7.8

7.6

显示成绩和排名

制作图形的按钮

制作带图形的按钮很简单,利用MFC类库中的CButton类构造可以显示icon的按钮, 然后利用CButton的方法SetIcon就可以把资源中的icon显示在Button上。具体的构造方法如 下: CButton myButton; // 创建一个按钮 myButton.Create(_T("My button"), WS_CHILD|WS_VISIBLE|BS_ICON, CRect(10,10,60,50), pParentWnd, 1); // 将系统的问号图标显示在按钮上 myButton.SetIcon(∷LoadIcon(NULL, IDI_QUESTION));

其中WS_VISIBLE使得按钮在创建时可见,而BS_ICON则表示该按钮是一个可以显示 图标的按钮,LoadIcon()函数负责转换系统图标。 但游戏的界面不满足于这一点,需要具有圆形边缘的按钮,使得整个界面更加协调、

第7章

俄罗斯方块游戏──Visual C++应用深入

漂亮。通过自己绘制按钮的边缘,我们可以达到这个效果。 首先基于CButton类,制作新的CRoundButton类: class CRoundButton : public CButton { // 构造函数 public: CRoundButton(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CRoundButton) public: virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); protected: virtual void PreSubclassWindow(); //}}AFX_VIRTUAL // Implementation public: virtual ~CRoundButton(); CRgn

m_rgn;

CPoint m_ptCentre; CPoint m_ptLeft; CPoint m_ptRight; int

m_nRadius;

BOOL BOOL

m_bDrawDashedFocusCircle; m_bStretch;

// Generated message map functions protected: //{{AFX_MSG(CRoundButton) //}}AFX_MSG DECLARE_MESSAGE_MAP() }; CRoundButton∷CRoundButton() { m_bDrawDashedFocusCircle = TRUE; }

231

趣味程序导学 Visual C++

232

CRoundButton∷~CRoundButton() { m_rgn.DeleteObject(); } BEGIN_MESSAGE_MAP(CRoundButton, CButton) //{{AFX_MSG_MAP(CRoundButton) //}}AFX_MSG_MAP END_MESSAGE_MAP() //////////////////////////////////////////////////////////////////////// // CRoundButton message handlers void CRoundButton∷PreSubclassWindow() { CButton∷PreSubclassWindow(); ModifyStyle(0, BS_OWNERDRAW); CRect rect; GetClientRect(rect); // 如果按钮不是正方形,设置拉伸属性 m_bStretch = rect.Width() > rect.Height() ? TRUE : FALSE; // 将窗口设置成正方形的 if(!m_bStretch) rect.bottom = rect.right = min(rect.bottom, rect.right); //得到窗口的统计信息 m_ptCentre = m_ptLeft = m_ptRight = rect.CenterPoint(); m_nRadius

= rect.bottom/2-1;

m_ptLeft.x = m_nRadius; m_ptRight.x = rect.right - m_nRadius - 1; // 重新设置窗口范围,使得鼠标只在按钮的圆角范围之内响应 m_rgn.DeleteObject(); SetWindowRgn(NULL, FALSE); // 简单的构造一个椭圆型的区域是不够的,如果按钮被拉伸过,区域的形状就更复杂了 if(m_bStretch)

第7章

俄罗斯方块游戏──Visual C++应用深入

233

CreateButtonRgn(m_rgn, rect, m_ptLeft, m_ptRight, m_nRadius); else m_rgn.CreateEllipticRgnIndirect(rect); SetWindowRgn(m_rgn, TRUE); // 把当前客户坐标转化为父窗口的客户坐标 ClientToScreen(rect); CWnd* pParent = GetParent(); if (pParent) pParent->ScreenToClient(rect); // 重新设置窗口大小 if(!m_bStretch)MoveWindow(rect.left,rect.top,rect.Width(), rect.Height(), TRUE); } void CRoundButton∷ { ASSERT(lpDrawItemStruct != NULL); CDC* pDC

= CDC∷FromHandle(lpDrawItemStruct->hDC);

CRect rect = lpDrawItemStruct->rcItem; UINT state = lpDrawItemStruct->itemState; UINT nStyle = GetStyle(); int nRadius = m_nRadius; int nSavedDC = pDC->SaveDC(); pDC->SelectStockObject(NULL_BRUSH); if(m_bStretch) { // 由于俄罗斯方块游戏使用了图像背景,我们不能简单的填充一个长方形的区域,必须按照 // 按钮的形状填充 CRect rc(rect); ++rc.top; --rc.bottom; CRgn rgn; if(m_bStretch) CreateButtonRgn(rgn, rc, m_ptLeft, m_ptRight, m_nRadius); else m_rgn.CreateEllipticRgnIndirect(rc);

趣味程序导学 Visual C++

234

CBrush brush; brush.CreateSolidBrush(∷GetSysColor(COLOR_BTNFACE)); pDC->FillRgn(&rgn, &brush); } // 在按钮上绘制表示得到焦点的虚线框 if ((state & ODS_FOCUS) && m_bDrawDashedFocusCircle && !m_bStretch) DrawCircle(pDC, m_ptCentre, nRadius--, RGB(0,0,0)); // 绘制按钮的突出或凹陷的边界 if (nStyle & BS_FLAT) { if(m_bStretch) { CPen* oldpen; CRect LeftBound(0,0,nRadius*2,nRadius*2); CRect RightBound(m_ptRight.x-nRadius,0,m_ptRight. x+nRadius,nRadius*2); oldpen = pDC->SelectObject(new CPen(PS_SOLID,1, ∷GetSysColor(COLOR_3DDKSHADOW))); pDC->Arc(LeftBound, CPoint(m_ptLeft.x,0), CPoint(m_ptLeft.x,nRadius*2)); pDC->Arc(RightBound, CPoint(m_ptRight.x,nRadius*2), CPoint(m_ptRight.x,0)); pDC->MoveTo(m_ptLeft.x,0); pDC->LineTo(m_ptRight.x,0); pDC->MoveTo(m_ptLeft.x,nRadius*2-1); pDC->LineTo(m_ptRight.x,nRadius*2-1); nRadius--; LeftBound.DeflateRect(1,1); RightBound.DeflateRect(1,1); delete pDC->SelectObject(new CPen(PS_SOLID, 1, ∷GetSysColor(COLOR_3DHIGHLIGHT))); pDC->Arc(LeftBound, CPoint(m_ptLeft.x,1),CPoint (m_ptLeft.x,nRadius*2)); pDC->Arc(RightBound, CPoint(m_ptRight.x,nRadius*2), CPoint(m_ptRight.x,0));

第7章

俄罗斯方块游戏──Visual C++应用深入

pDC->MoveTo(m_ptLeft.x,1); pDC->LineTo(m_ptRight.x,1); pDC->MoveTo(m_ptLeft.x,nRadius*2); pDC->LineTo(m_ptRight.x,nRadius*2); delete pDC->SelectObject(oldpen); } // 没有拉伸的按钮画两个圈 else { DrawCircle(pDC, m_ptCentre, nRadius--,∷GetSysColor (COLOR_3DDKSHADOW)); DrawCircle(pDC, m_ptCentre, nRadius--,∷GetSysColor (COLOR_3DHIGHLIGHT)); } } else { if ((state & ODS_SELECTED))

{

// 绘制弧形部分 DrawCircleLeft(pDC, m_ptLeft, nRadius, ∷GetSysColor(COLOR_3DDKSHADOW), ∷GetSysColor(COLOR_3DHIGHLIGHT)); DrawCircleRight(pDC, m_ptRight, nRadius, ∷GetSysColor(COLOR_3DDKSHADOW), ∷GetSysColor(COLOR_3DHIGHLIGHT)); DrawCircleLeft(pDC, m_ptLeft, nRadius-1, ∷GetSysColor(COLOR_3DSHADOW), ∷GetSysColor(COLOR_3DLIGHT)); DrawCircleRight(pDC, m_ptRight, nRadius-1, ∷GetSysColor(COLOR_3DSHADOW), ∷GetSysColor(COLOR_3DLIGHT)); // 为拉伸的按钮绘制连接部分 if (m_bStretch) { CPen* oldpen; //CPen* mypen; oldpen = pDC->SelectObject(new CPen(PS_SOLID, 1, ∷GetSysColor(COLOR_3DDKSHADOW))); pDC->MoveTo(m_ptLeft.x, 1); pDC->LineTo(m_ptRight.x, 1);

235

趣味程序导学 Visual C++

236

delete pDC->SelectObject(new CPen(PS_SOLID, 1, ∷GetSysColor(COLOR_3DSHADOW))); pDC->MoveTo(m_ptLeft.x, 2); pDC->LineTo(m_ptRight.x, 2); delete pDC->SelectObject(new CPen(PS_SOLID, 1, ∷GetSysColor(COLOR_3DLIGHT))); pDC->MoveTo(m_ptLeft.x, m_ptLeft.y + nRadius-1); pDC->LineTo(m_ptRight.x, m_ptLeft.y + nRadius-1); delete pDC->SelectObject(new CPen(PS_SOLID, 1, ∷GetSysColor(COLOR_3DHIGHLIGHT))); pDC->MoveTo(m_ptLeft.x, m_ptLeft.y + nRadius); pDC->LineTo(m_ptRight.x, m_ptLeft.y + nRadius); delete pDC->SelectObject(oldpen); } } else { // 绘制弧形部分 DrawCircleLeft(pDC, m_ptLeft, nRadius, ∷GetSysColor(COLOR_3DHIGHLIGHT), ∷GetSysColor(COLOR_3DDKSHADOW)); DrawCircleRight(pDC, m_ptRight, nRadius, ∷GetSysColor(COLOR_3DHIGHLIGHT), ∷GetSysColor(COLOR_3DDKSHADOW)); DrawCircleLeft(pDC, m_ptLeft, nRadius - 1, ∷GetSysColor(COLOR_3DLIGHT), ∷GetSysColor(COLOR_3DSHADOW)); DrawCircleRight(pDC, m_ptRight, nRadius - 1, ∷GetSysColor(COLOR_3DLIGHT), ∷GetSysColor(COLOR_3DSHADOW)); // 为拉伸的按钮绘制连接部分 if (m_bStretch) { CPen* oldpen; oldpen = pDC->SelectObject(new CPen(PS_SOLID, 1, pDC->GetPixel(m_ptLeft.x, 1))); pDC->MoveTo(m_ptLeft.x, 1); pDC->LineTo(m_ptRight.x, 1);

第7章

俄罗斯方块游戏──Visual C++应用深入

237

delete pDC->SelectObject(new CPen(PS_SOLID, 1, pDC->GetPixel(m_ptLeft.x, 2))); pDC->MoveTo(m_ptLeft.x, 2); pDC->LineTo(m_ptRight.x, 2); delete pDC->SelectObject(new CPen(PS_SOLID, 1, pDC->GetPixel(m_ptLeft.x, m_ptLeft.y + nRadius))); pDC->MoveTo(m_ptLeft.x, m_ptLeft.y + nRadius); pDC->LineTo(m_ptRight.x, m_ptLeft.y + nRadius); delete pDC->SelectObject(new CPen(PS_SOLID, 1, pDC->GetPixel(m_ptLeft.x, m_ptLeft.y + nRadius - 1))); pDC->MoveTo(m_ptLeft.x, m_ptLeft.y + nRadius - 1); pDC->LineTo(m_ptRight.x, m_ptLeft.y + nRadius - 1); delete pDC->SelectObject(oldpen); } } } // 绘制必要的文字 CString strText; GetWindowText(strText); if (!strText.IsEmpty()) { CRgn rgn; if (m_bStretch){ rgn.CreateRectRgn(m_ptLeft.x-nRadius/2, m_ptCentre.y-nRadius, m_ptRight.x+nRadius/2, m_ptCentre.y+nRadius);} else{ rgn.CreateEllipticRgn(m_ptCentre.x-nRadius, m_ptCentre.y-nRadius, m_ptCentre.x+nRadius, m_ptCentre.y+nRadius);} pDC->SelectClipRgn(&rgn);

趣味程序导学 Visual C++

238

CSize Extent = pDC->GetTextExtent(strText); CPoint pt = CPoint(m_ptCentre.x - Extent.cx/2, m_ptCentre.y - Extent.cy/2); if (state & ODS_SELECTED) pt.Offset(1,1); pDC->SetBkMode(TRANSPARENT); if (state & ODS_DISABLED) pDC->DrawState(pt, Extent, strText,DSS_DISABLED,TRUE, 0, (HBRUSH)NULL); else { // 3D效果的文字 COLORREF oldcol = pDC->SetTextColor(∷GetSysColor (COLOR_3DHIGHLIGHT)); pDC->TextOut(pt.x, pt.y, strText); pDC->SetTextColor(∷GetSysColor(COLOR_3DDKSHADOW)); pDC->TextOut(pt.x-1, pt.y-1, strText); pDC->SetTextColor(oldcol); } pDC->SelectClipRgn(NULL); rgn.DeleteObject(); } // 为未拉伸的按钮绘制表示得到焦点的框 if ((state & ODS_FOCUS) && m_bDrawDashedFocusCircle && !m_bStretch) DrawCircle(pDC, m_ptCentre, nRadius-2, RGB(0,0,0), TRUE); pDC->RestoreDC(nSavedDC); }

7.7

数字的特殊效果显示

为了美化界面,成绩和游戏相关信息的显示采用模拟7段数码管的外观。在程序中, 并不是真的像数码管那样通过数字的每一笔横竖线条来控制显示内容,而是将各个数字作 为一个整体来显示。在这里用到的是0~9这10个数字以及没有显示时的状态——BLANK。 所以,可以根据自己的喜好来更改数字显示的界面,只需要将11张图片换成另一种风格就 可以了。 首先,我们将数字图片作为资源引入VC工程。在“WorkSpace”窗口中单击右键,选

第7章

俄罗斯方块游戏──Visual C++应用深入

239

择“Import”(导入),然后选择对应的位图文件。出于程序结构的考虑,专门构造了 CDigiDisplay 这 样 一 个 类 , 它 是 从 MFC 的 CStatic 基 类 派 生 的 , 用 来 显 示 位 图 。 CDigiDisplay类的主要任务是完成位图资源的导入和显示。作为参数,当前需要显示的整 型数值内容被传递给CDigiDisplay,然后通过重载OnPaint函数来显示数值。 CDigiDisplay∷CDigiDisplay(int NumOfDigits /* = 8 */) : m_nNumOfDigits(NumOfDigits) , m_strNumber("0") { VERIFY(m_bmpDigit[0].LoadBitmap(IDB_Digit0)); VERIFY(m_bmpDigit[1].LoadBitmap(IDB_Digit1)); VERIFY(m_bmpDigit[2].LoadBitmap(IDB_Digit2)); VERIFY(m_bmpDigit[3].LoadBitmap(IDB_Digit3)); VERIFY(m_bmpDigit[4].LoadBitmap(IDB_Digit4)); VERIFY(m_bmpDigit[5].LoadBitmap(IDB_Digit5)); VERIFY(m_bmpDigit[6].LoadBitmap(IDB_Digit6)); VERIFY(m_bmpDigit[7].LoadBitmap(IDB_Digit7)); VERIFY(m_bmpDigit[8].LoadBitmap(IDB_Digit8)); VERIFY(m_bmpDigit[9].LoadBitmap(IDB_Digit9)); VERIFY(m_bmpDigit[BLANK].LoadBitmap(IDB_DigitBlank)); //假设所有的位图都是一样大小的 m_bmpDigit[0].GetObject(sizeof(BITMAP), &m_BM); } CDigiDisplay∷~CDigiDisplay() { }

BEGIN_MESSAGE_MAP(CDigiDisplay, CStatic) //{{AFX_MSG_MAP(CDigiDisplay) ON_WM_PAINT() //}}AFX_MSG_MAP END_MESSAGE_MAP() //////////////////////////////////////////////////////////////////////// // CDigiDisplay message handlers void CDigiDisplay∷SetNumber(UINT uNumber) { m_strNumber.Format("%u", uNumber); Invalidate();

趣味程序导学 Visual C++

240 }

void CDigiDisplay∷GetNumber(UINT & uNumber) { GetWindowText(m_strNumber); uNumber = UINT(_ttol(m_strNumber)); } void CDigiDisplay∷OnPaint() { CPaintDC dc(this); CDC dcMem; VERIFY(dcMem.CreateCompatibleDC(0)); CRect rect; GetClientRect(rect); CString str(m_strNumber); const int nHeight = rect.Height(); const int nWidth

= rect.Width() / m_nNumOfDigits;

const BOOL bLeft = !(GetStyle() & SS_RIGHT); register const int len = str.GetLength(); for(register int i = 0; i < m_nNumOfDigits; ++i) { CBitmap * pBmp = 0; if(bLeft) { // 左对齐 if(i < len && str[i] >= '0' && str[i] = nSpaceLeft && str[i-nSpaceLeft] >= '0' && str[inSpaceLeft] PrepareCtrl(nIDC); ASSERT(hWndCtrl); CDigiDisplay * pCtrl = (CDigiDisplay*) CWnd∷FromHandle(hWndCtrl); ASSERT(pCtrl); if(pDX->m_bSaveAndValidate) { pCtrl->GetNumber(uNumber); } else { pCtrl->SetNumber(uNumber); } }

在用来显示数字的Static 窗口的父窗口 CTetrisDlg 中重载DoDataExchange函数,调用 DDX全 局 函 数 。 这 里 要 注 意 , 类 的DoDataExchange 函数不能直接调用,而应该调用 UpDateData来发送消息。UpDateData(FALSE) 表示初始化对话框信息,UpDateData(True) 是用来取得当前对话框的内容。 void CTetrisDlg∷DoDataExchange(CDataExchange* pDX) { CBitmapPropPage∷DoDataExchange(pDX); //{{AFX_DATA_MAP(CTetrisDlg) DDX_Text(pDX, IDC_TxtLevel, m_strLevel); DDX_Text(pDX, IDC_TxtLines, m_strLines); DDX_Text(pDX, IDC_TxtScore, m_strScore); //}}AFX_DATA_MAP

趣味程序导学 Visual C++

242

DDX_Control(pDX, IDC_Pause, m_btnPauseResume); DDX_Control(pDX, IDC_BtnStart, m_btnStartStop); DDX_Control(pDX, IDC_MoveLeft, m_btnMoveLeft); DDX_Control(pDX, IDC_MoveRight, m_btnMoveRight); DDX_Control(pDX, IDC_Rotate, m_btnRotate); DDX_Control(pDX, IDC_Place, m_btnPlace); DDX_Control(pDX, IDC_Score, m_ctrlScore); DDX_DigiDisplay(pDX, IDC_Score, m_uScore); DDX_Control(pDX, IDC_Lines, m_ctrlLines); DDX_DigiDisplay(pDX, IDC_Lines, m_uLines); DDX_Control(pDX, IDC_Level, m_ctrlLevel); DDX_DigiDisplay(pDX, IDC_Level, m_uLevel); if(0 == m_pGameBoard) m_pGameBoard = (CGameBoard*) GetDlgItem(IDC_GameBoard); ASSERT(0 != m_pGameBoard); if(0 == m_pPiecePreview) m_pPiecePreview = (CPiecePreview*) GetDlgItem(IDC_PiecePreview); ASSERT(0 != m_pPiecePreview); if(! pDX->m_bSaveAndValidate) { // display correct text m_btnPauseResume.SetWindowText(m_bPaused ? m_strResume:m_strPause); m_btnStartStop.SetWindowText(m_bInGame ? m_strStop : m_strStart); } }

7.8

用ActiveX美化界面

通常,程序在完成初始化的时候,需要显示一些标志来提醒用户。避免单调的界面和 乏味的等待。这样的标志最简单的是Windows 的鼠标小沙漏,复杂一点的是Adobe公司 PhotoShop这样的欢迎界面。 俄罗斯方块的初始化并不复杂,所需的时间也不多。但为了增加趣味和美化界面,采 用了VC通用控件列表中的Splash screen来显示一张位图。通过介绍位图显示,大家可对 VC程序如何引入控件,如何进一步引入ActiveX控件有所了解。 如图7.9所示,选中其中的Splash screen,单击Insert按钮将控件插入工程。 在VC的资源管理窗口中,会多出一个IDB_SPLASH的位图,那就是在程序初始化时 前台要显示的位图。可以修改或自己载入新的位图。

第7章

俄罗斯方块游戏──Visual C++应用深入

图 7.9

243

控件列表

ActiveX控件最好从客户端实现,因此本节一开始是讨论包容器是如何通过现有的 ActiveX控件扩展其能力,图7.10所示为几个ActiveX控件Visual C++和Internet Explorer都附 带一个可免费使用的ActiveX控件集(见表7.3),其中的一部分在Gallery对话框中列出。 如果想打开Gallery对话框,请从Project菜单中选择Add To Project命令,并单击次级菜单上 的Components And Controls ,然后双击Registered ActiveX Controls 文件夹来显示控件列 表。如果在Gallery中选中了一个控件图标,则More Info(其他信息)按钮可用,那么,单 击More Info按钮可以查看该控件的资料。 表7.3

Microsoft中提供的ActiveX控件

控件名

说明

AniBtn32.ocx

Animated button(动画按钮):使用位图或元文件来创建一个可变化的图像按钮

BtnMenu.ocx

Menu(菜单):显示一个按钮和一个弹出式菜单

DBGrid32.ocx

Grid(网格):以标准网格样式显示单元格的电子表控件。用户可以选定单元格(与 老的Grid32控件不同),并直接将数据输入单元格。还可以由包容器通过编程来填充 单元格,或为了自动更新而捆绑到记录集

IELabel.ocx

label(标签):以一定角度或沿指定的曲线显示文字

IEMenu.ocx

Pop-up menu(弹出式菜单):显示一个弹出式菜单

EPopWnd.ocx

Pop-up window(弹出窗口):在弹出式窗口中显示HTML文档

IEPrld.ocx

Preloader(预加载器):下载指定URL的内容,并将它存储在高速缓存中。完成下 载之后,该控件将触发一个事件

IEStock.ocx

stock ticker(股市自动接收机):以固定周期下载和显示URL的内容。顾名思义, 该控件对于显示连续变化的数据特别有用

IETimer.ocx

Timer(计时器):一个不可见的控件,以一定的周期触发一个事件

趣味程序导学 Visual C++

244

续表 控件名

说明

KeySta32.ocx

Key(键)状态:显示并有选择地修改Cap Lock,Num Lock,Insert和Scroll Lock键 的状态

Marquee.ocx

Marquee(大屏幕):水平或垂直方向滚动HTML文件中的文字,可以通过配置来改 变滚动的数量和延迟时间

MCI32.ocx

Multimedia(多媒体):管理Media Control Interface(MCI,媒体控制接口)设备的 多媒体文件的录音和播放。该控件可以显示一套用于将MCI命令传向设备的下压式 按钮,这些设备包括音频板、MIDI序列发生器、CD-ROM 驱动器、音频CD播放 器 、 视 频 光 盘 播 放 器 以 及 视 频 磁 带 录 音 机 和 播 放 器 。 该 控 件 还 支 持Video for Windows AVI文件的播放

MSCal.ocx

Calendar(日历):在屏幕上显示日历,用户可以从中选择日期。

MSChart.ocx

Chart(图表):一个高级的图表控件,它接收数字数据,然后显示几种图表类型之 一,包括线、条和栏形图表。该控件的绘制以两维或三维形式显示

MSComm32.oc

Comm(通信):为串行通信提供支持,处理发送到串行口和从串行口接收的数据 传输

MSMask32.ocx

Masked edit(掩码编辑):一个增强的编辑控件,它可以确保输入符合预先定义好 的格式。例如,一个“##:##??”的约束将输入限制为时间格式。例如“11:18 AM”

PicCLP32.ocx

Picture clip(图片剪辑):显示位图中剪下的矩形区域,可以根据指定的行数和列 数将位图划分为网格

图 7.10

几个在包容器上出现的 Microsoft ActiveX 控件

当你从配套光盘或另一个来源向自己的硬盘复制控件文件的时候,请使用VC98\Bin子 文件夹中的RegSvr32.exe实用程序对其进行注册。RegSvr32调用该控件的自注册函数,该 函数将有关这个控件的信息写入系统注册表。在注册控件之前,包容器应用程序一般是没

第7章

俄罗斯方块游戏──Visual C++应用深入

245

有办法通过嵌入来定位的。单击Start按钮,并从Run对话中执行RegSvr32,在命令行中指 定一个ocx文件:regsvr32\windows\occache\anibtm32.ocx,如果你的系统中PATH语句没有 包括 VC98\Bin文 件 夹 , 那 么 , 请 在 键 入 RegSvr32时 指 定 正 确 的 路 径 。 如 果 想 解 除 对 ActiveX控件的注册(也就是说,从注册表中删除它的条目),请按照相同的方式再运行一遍 RegSvr32,但是要在文件名前包括开关“/u”。解除对控件的注册并不从磁盘上删除它的 ocx文件,你还可以通过单击 Tools 菜单上的Register Control命令来从Visual C++中运行 RegSvr32。不过,在默认状态下,该命令假定你想注册一个控件,因此,它仅仅注册工程 目标文件。

7.9

游动字幕About Box和说明的制作

说明性的文字如果用静态方式显示,会显得比较呆板。如果能够做成电影字幕一样自 动更新,就生动有趣多了。 这里,将用到前面介绍过的SetTimer等一系列计时器函数。每隔固定的时间间隔调用 OnTimer函数,绘制当前的状态。绘制是在Static 上完成的,所以我们继承了CStatic 类并构 造了CCrediteStatic 。 前面已经介绍过一种位图的显示方式,称之为与设备无关的位图(DIB)。还有一种 适用性较差,但比较简单的方法是GDI位图。About Box的游动显示就是通过巧妙运用GDI 位图的特性实现的。 Windows不允许直接访问显示硬件,但是,可以通过与窗口关联的名为“设备环境” (DC)的抽象层进行通信。用户可以直接操作DC,但对DC的直接操作会导致速度的下 降。所以在绘制复杂的画面时,采用虚拟 DC的概念。也就是建立一个虚拟的CDC对象 ( CompatibleDC ) , 还 需 要 将 该 DC 与 内 存 中 的 一 块 位 图 区 域 相 联 系 (CompatibleBitmap),所有对虚拟DC的操作都在内存中完成,最后需要显示的时候再一 次性显示出来,这样可以大大提高显示效率,降低显示迟滞。 函数MoveCrdit 的主要任务是移动前景中的文字。首先选择合适的字体,然后绘制在 虚拟的DC上,并计算绘制文字的大小、位置,以方便控制卷屏。 void CCreditStatic∷MoveCredit(CDC* pDC, CRect& m_ScrollRect, CRect& m_ClientRect, BOOL bCheck) { CDC memDC,memDC2; memDC.CreateCompatibleDC(pDC); memDC2.CreateCompatibleDC(pDC); COLORREF BackColor = (m_bTransparent && m_bitmap.m_hObject != NULL)? RGB(192,192,192) : m_Colors[BACKGROUND_COLOR]; CBitmap *pOldMemDCBitmap = NULL; CBitmap

*pOldMemDC2Bitmap = NULL;

趣味程序导学 Visual C++

246

CRect r1; if(m_BmpMain.m_hObject == NULL) { m_BmpMain.CreateCompatibleBitmap(pDC, m_ScrollRect.Width(), m_ScrollRect.Height()); pOldMemDCBitmap = (CBitmap*)memDC.SelectObject(&m_BmpMain); if(m_Gradient && m_bitmap.m_hObject == NULL) FillGradient(&memDC, &m_ScrollRect, &m_ScrollRect,m_Colors[BACKGROUND_COLOR]); else memDC.FillSolidRect(&m_ScrollRect,BackColor); } else pOldMemDCBitmap = (CBitmap*)memDC.SelectObject(&m_BmpMain); if(m_ClientRect.Width() > 0) { CRgn RgnUpdate; memDC.ScrollDC(0,m_ScrollAmount,(LPCRECT)m_ScrollRect, (LPCRECT)m_ClientRect,&RgnUpdate, (LPRECT)r1); } else { r1 = m_ScrollRect; r1.top = r1.bottom-abs(m_ScrollAmount); } m_nClip = m_nClip + abs(m_ScrollAmount); //字体的选择 CFont m_fntArial; CFont* pOldFont = NULL; BOOL bSuccess = FALSE; BOOL bUnderline; BOOL bItalic; int rmcode = 0; if (!m_szWork.IsEmpty()) { char c = m_szWork[m_szWork.GetLength()-1]; if(c == m_Escapes[TOP_LEVEL_GROUP]) { rmcode = 1; bItalic = FALSE; bUnderline = FALSE; m_nCurrentFontHeight = m_TextHeights

第7章

俄罗斯方块游戏──Visual C++应用深入 [TOP_LEVEL_GROUP_HEIGHT];

bSuccess = m_fntArial.CreateFont(m_TextHeights [TOP_LEVEL_GROUP_HEIGHT], 0, 0, 0, FW_BOLD, bItalic, bUnderline,0, ANSI_CHARSET, OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS, PROOF_QUALITY, VARIABLE_PITCH | 0x04 | FF_DONTCARE, (LPSTR)"Arial"); memDC.SetTextColor(m_Colors[TOP_LEVEL_GROUP_COLOR]); if (pOldFont != NULL) memDC.SelectObject(pOldFont); pOldFont = memDC.SelectObject(&m_fntArial); } else if(c == m_Escapes[GROUP_TITLE]) { rmcode = 1; bItalic = FALSE; bUnderline = FALSE; m_nCurrentFontHeight=m_TextHeights[GROUP_TITLE_HEIGHT]; bSuccess = m_fntArial.CreateFont(m_TextHeights [GROUP_TITLE_HEIGHT], 0, 0, 0, FW_BOLD, bItalic, bUnderline, 0, ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,PROOF_QUALITY, VARIABLE_PITCH | 0x04 | FF_DONTCARE, (LPSTR)"Arial"); memDC.SetTextColor(m_Colors[GROUP_TITLE_COLOR]); if (pOldFont != NULL) memDC.SelectObject(pOldFont); pOldFont = memDC.SelectObject(&m_fntArial); } else if(c == m_Escapes[TOP_LEVEL_TITLE]) { rmcode = 1; bItalic = FALSE; bUnderline = FALSE; m_nCurrentFontHeight =m_TextHeights [TOP_LEVEL_TITLE_HEIGHT]; bSuccess = m_fntArial.CreateFont (m_TextHeights[TOP_LEVEL_TITLE_HEIGHT], 0, 0, 0, FW_BOLD, bItalic, bUnderline, 0,ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, VARIABLE_PITCH | 0x04 | FF_DONTCARE, (LPSTR)"Arial"); memDC.SetTextColor(m_Colors[TOP_LEVEL_TITLE_COLOR]);

247

趣味程序导学 Visual C++

248

if (pOldFont != NULL) memDC.SelectObject(pOldFont); pOldFont = memDC.SelectObject(&m_fntArial); } else if(c == m_Escapes[DISPLAY_BITMAP]) { if (!m_bProcessingBitmap) { CString szBitmap=m_szWork.Left(m_szWork.GetLength()-1); if(m_bmpWork.LoadBitmap((const char *)szBitmap)) { BITMAP

m_bmpInfo;

m_bmpWork.GetObject(sizeof(BITMAP), &m_bmpInfo); m_size.cx = m_bmpInfo.bmWidth; // width

of dest rect

m_size.cy = m_bmpInfo.bmHeight; m_pt.x = (m_ClientRect.right ((m_ClientRect.Width())/2) – (m_size.cx/2)); m_pt.y = m_ClientRect.bottom; m_bProcessingBitmap = TRUE; if (pOldMemDC2Bitmap != NULL) memDC2.SelectObject(pOldMemDC2Bitmap); pOldMemDC2Bitmap = memDC2.SelectObject(&m_bmpWork); } else c = ' '; } else { if (pOldMemDC2Bitmap != NULL) memDC2.SelectObject(pOldMemDC2Bitmap); pOldMemDC2Bitmap = memDC2.SelectObject( &m_bmpWork); } } else { bItalic = FALSE; bUnderline = FALSE; m_nCurrentFontHeight=m_TextHeights[NORMAL_TEXT_HEIGHT]; bSuccess = m_fntArial.CreateFont

第7章

俄罗斯方块游戏──Visual C++应用深入

(m_TextHeights[NORMAL_TEXT_HEIGHT], 0, 0, 0, FW_THIN, bItalic, bUnderline, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, VARIABLE_PITCH | 0x04 | FF_DONTCARE, (LPSTR)"Arial"); memDC.SetTextColor(m_Colors[NORMAL_TEXT_COLOR]); if (pOldFont != NULL) memDC.SelectObject(pOldFont); pOldFont = memDC.SelectObject(&m_fntArial); } } if(m_Gradient && m_bitmap.m_hObject == NULL) FillGradient(&memDC,&m_ScrollRect, &r1,m_Colors[BACKGROUND_COLOR]); else memDC.FillSolidRect(&r1,BackColor); memDC.SetBkMode(TRANSPARENT); if (!m_bProcessingBitmap) { if(bCheck) { CSize size = memDC.GetTextExtent((LPCTSTR)m_szWork, m_szWork.GetLength()-rmcode); if(size.cx > n_MaxWidth) { n_MaxWidth = (size.cx > m_ScrollRect.Width())? m_ScrollRect.Width():size.cx; m_ClientRect.left = (m_ScrollRect.Width()-n_MaxWidth)/2; m_ClientRect.right = m_ClientRect.left+ n_MaxWidth; } } CRect r(m_ClientRect); r.top = r.bottom-m_nClip; int x = memDC.DrawText((const char *)m_szWork, m_szWork.GetLength()-rmcode,&r,DT_TOP|DT_CENTER| DT_NOPREFIX | DT_SINGLELINE); m_bDrawText=FALSE; } else { if(bCheck) {

249

趣味程序导学 Visual C++

250

CSize size = memDC.GetTextExtent((LPCTSTR)m_szWork, m_szWork.GetLength()-rmcode); if(m_size.cx > n_MaxWidth) { n_MaxWidth = (m_size.cx > m_ScrollRect.Width())? m_ScrollRect.Width():m_size.cx; m_ClientRect.left=(m_ScrollRect.Width()n_MaxWidth)/2; m_ClientRect.right = m_ClientRect.left + n_MaxWidth; } } CRect r(m_pt.x, m_pt.y-m_nClip, m_pt.x+ m_size.cx, m_pt.y); DrawBitmap(&memDC, &memDC2, &r); if (m_nClip >= m_size.cy) { m_bmpWork.DeleteObject(); m_bProcessingBitmap = FALSE; m_nClip=0; m_szWork.Empty(); m_nCounter=1; } } if (pOldMemDC2Bitmap != NULL) memDC2.SelectObject(pOldMemDC2Bitmap); if (pOldFont != NULL) memDC.SelectObject(pOldFont); memDC.SelectObject(pOldMemDCBitmap); }

在前景经过精确的计算并绘制出来之后需要加到背景上,最终显示出来。这里使用的 是多个DC的叠加技术: void CCreditStatic ∷ AddBackGround(CDC* pDC, CRect& m_ScrollRect, CRect& m_ClientRect) { CDC memDC; memDC.CreateCompatibleDC(pDC); if(m_bitmap.m_hObject == NULL) { CBitmap* pOldBitmap = memDC.SelectObject(&m_BmpMain); pDC->BitBlt(0, 0, m_ScrollRect.Width(), m_ScrollRect.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); return;

第7章

俄罗斯方块游戏──Visual C++应用深入

} //在背景上绘制位图,首先建立一个掩膜 CBitmap bitmap; bitmap.CreateCompatibleBitmap(pDC, m_ClientRect.Width(), m_ClientRect.Height()); CBitmap* pOldMemDCBitmap = memDC.SelectObject(&bitmap); CDC tempDC; tempDC.CreateCompatibleDC(pDC); CBitmap* pOldTempDCBitmap = tempDC.SelectObject(&m_BmpMain); memDC.BitBlt(0, 0, m_ClientRect.Width(), m_ClientRect.Height(), &tempDC, m_ClientRect.left, m_ClientRect.top, SRCCOPY); CDC maskDC; maskDC.CreateCompatibleDC(pDC); CBitmap maskBitmap; // 建立单色位图掩膜 maskBitmap.CreateBitmap(m_ClientRect.Width(), m_ClientRect.Height(), 1, 1, NULL); CBitmap* pOldMaskDCBitmap = maskDC.SelectObject(&maskBitmap); memDC.SetBkColor(m_bTransparent? RGB(192,192,192): m_Colors[BACKGROUND_COLOR]); // 为内存中的DC建立掩膜 maskDC.BitBlt(0, 0, m_ClientRect.Width(), m_ClientRect.Height(), &memDC, 0, 0, SRCCOPY); tempDC.SelectObject(pOldTempDCBitmap); pOldTempDCBitmap = tempDC.SelectObject(&m_bitmap); CDC imageDC; CBitmap bmpImage; imageDC.CreateCompatibleDC(pDC); bmpImage.CreateCompatibleBitmap(pDC, m_ScrollRect.Width(), m_ScrollRect.Height()); CBitmap* pOldImageDCBitmap = imageDC.SelectObject(&bmpImage); if(pDC->GetDeviceCaps(RASTERCAPS) & RC_PALETTE && m_pal.m_hObject != NULL) {

251

趣味程序导学 Visual C++

252

pDC->SelectPalette(&m_pal, FALSE); pDC->RealizePalette(); imageDC.SelectPalette(&m_pal, FALSE); } CRect rc; GetClientRect(rc); ClientToScreen(rc); CWnd * pParent = GetParent()->GetParent(); ASSERT(pParent != 0); pParent->ScreenToClient(rc); int xSrc = rc.left; int ySrc = rc.top; // 得到x和y的偏移地址 // 用平铺的方式显示位图 for(int i = 0; i < m_ScrollRect.right; i += m_cxBitmap) for(int j = 0; j < m_ScrollRect.bottom; j += m_cyBitmap) imageDC.BitBlt(i, j, m_cxBitmap, m_cyBitmap, &tempDC, xSrc, ySrc, SRCCOPY); // 将背景设成黑色的 //用SRCPAINT和其他颜色制造透明的效果 memDC.SetBkColor(RGB(0,0,0)); memDC.SetTextColor(RGB(255,255,255)); memDC.BitBlt(0,0,m_ClientRect.Width(),m_ClientRect.Height(), &maskDC, 0, 0, SRCAND); // 设置前景颜色 imageDC.SetBkColor(RGB(255,255,255)); imageDC.SetTextColor(RGB(0,0,0)); imageDC.BitBlt(m_ClientRect.left, m_ClientRect.top, m_ClientRect.Width(), m_ClientRect.Height(), &maskDC, 0, 0, SRCAND); // 将前景和背景合并 imageDC.BitBlt(m_ClientRect.left, m_ClientRect.top, m_ClientRect.Width(), m_ClientRect.Height(), &memDC, 0, 0,SRCPAINT); // 将最终的图形显示出来

第7章

俄罗斯方块游戏──Visual C++应用深入

253

pDC->BitBlt(0,0,m_ScrollRect.Width(),m_ScrollRect.Height(),&imageDC,0, 0, SRCCOPY); imageDC.SelectObject(pOldImageDCBitmap); maskDC.SelectObject(pOldMaskDCBitmap); tempDC.SelectObject(pOldTempDCBitmap); memDC.SelectObject(pOldMemDCBitmap); }

在CCreditStatic 的OnTimer函数中分别进行前景的绘制和背景的叠加,就可以达到滚动 字幕的效果了。 void CCreditStatic∷OnTimer(UINT nIDEvent) { if (nIDEvent != DISPLAY_TIMER_ID) { CStatic∷OnTimer(nIDEvent); return; } BOOL bCheck = FALSE; if (!m_bProcessingBitmap) { if (m_nCounter++ % m_nCurrentFontHeight == 0) // every x timer events, show new line { m_nCounter=1; m_szWork = m_ArrCredit.GetNext(m_ArrIndex); if(m_bFirstTurn) bCheck = TRUE; if(m_ArrIndex == NULL) { m_bFirstTurn = FALSE; m_ArrIndex = m_ArrCredit.GetHeadPosition(); } m_nClip = 0; m_bDrawText=TRUE; } } CClientDC dc(this); CRect m_ScrollRect; GetClientRect(&m_ScrollRect); CRect m_ClientRect(m_ScrollRect); m_ClientRect.left = (m_ClientRect.Width()-n_MaxWidth)/2; m_ClientRect.right = m_ClientRect.left + n_MaxWidth; MoveCredit(&dc, m_ScrollRect, m_ClientRect, bCheck);

趣味程序导学 Visual C++

254

AddBackGround(&dc, m_ScrollRect, m_ClientRect); CStatic∷OnTimer(nIDEvent); }

7.10

本章知识点回顾

标签对话框

显示对话框:

(CProperty

CPropertySheet∷DoModal()

Sheet) 增加新页: CPropertySheet∷AddPage(CPropertyPage *pPage) 客户自定义控 件(Custom Control)

键盘事件

BOOL CPiecePreview ∷ Register() { WNDCLASS wc; … // wc赋值 … VERIFY(RegisterClass(&wc)); return TRUE; } 键按下时: void CGameBoard∷OnKeyDown(UINT nChar,UINT nRepCnt,UINT nFlags) 按键抬起: void CGameBoard∷OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)

计时器 (Timer)

每隔固定的时间间隔调用函数指针lpfnTimer指向的函数,计时器的设置: UINT SetTimer(UINT nIDEvent, UINT nElapse, void (CALLBACK EXPORT* lpfnTimer)(HWND, UINT, UINT, DWORD)); 每隔固定时间间隔调用的成员函数: void CGameBoard∷OnTimer(UINT nIDEvent)

第7章

俄罗斯方块游戏──Visual C++应用深入

255 续表

列表视图控件 (CListCtrl)

设置列属性 CListCtrl∷SetColumn() 对列表中的项进行操作: InsertItem DeleteItem DeleteAllItems FindItem SortItems

虚拟DC的使用

// 在列表视图控件中插入一个新项 // 从控件中删除一项 // 从控件中删除所有项 // 查找具有指定字符的列表视图项 // 使用比较函数排序列表视图项

创建虚拟的DC并在内存中为其提供空间: CDC∷CreateCompatibleDC() 创建位图并与DC相关联: CBitmap∷CreateCompatibleBitmap() CDC∷SelectObject() 将显示内存中的位图贴到pSrcDC指向的DC: BOOL BitBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop);

第8章

属于你的 OICQ——Visual C++ 网络编程

随着Internet的迅猛发展,网络软件的开发与设计显得越来越重要。最初的网络软件主 要是以UNIX操作系统为开发环境的,随着Windows个人操作系统的流行,传统的编程界 面向这一新的平台转换变得极为迫切。作为一名VC++程序员,不可避免会接触到网络编 程。这就要求程序员要了解Internet的组织结构。幸运的是, MFC(Microsoft Foundation Class)已经为我们封装了基本的操作,本章将结合一个聊天程序介绍如何编写Socket 程 序。

8.1

程序效果说明

我们的聊天程序可以供两个人进行聊天,其中一个作为等待方(Server端),一个作 为连接方(Client端),程序的初始界面如图8.1所示。

图 8.1

聊天程序的初始界面

作为等待方的程序启动之后不必做任何事情,只要等待连接方的连接请求即可。作为 连接方的程序启动之后首先选中“连接”单选框,输入要连接的主机的 IP地址(见图 8.2),然后单击“连接”按钮,若连接成功,程序弹出对话框提示已连接主机,如图8.3 所示,否则提示连接失败,如图 8.4所示。联机成功之后,双方便可以利用程序进行聊 天,程序提供两个文本编辑控件,上面的文本框用来输入想要发送的句子,下面的文本框 用来显示对方传送过来的信息,如图8.5所示。利用这个程序,两个人就可以在完全没有 外界干扰的情况下聊天。

第8章

属于你的 OICQ——Visual C++ 网络编程

图8.2

图 8.3

输入等待方主机的IP地址

连接成功的提示

图 8.5

8.2

257

图 8.4

连接失败的提示

用属于自己的 OICQ 聊天

生成动态链接库(DLL)

由于Windows为微机提供了前所未有的标准用户界面、图形处理能力和简单灵活的操 作,绝大多数编程人员都已转向或正在转向Windows编程。在编写应用系统时,常常要实 现软件对硬件资源和内存资源的访问,例如端口I/O,DMA,中断,直接内存访问等等。 若是编制DOS程序,很容易实现这些功能,但要是编制Windows 程序,尤其是Windows NT环境下的程序,就会比较困难。 因为Windows具有“与设备无关”的特性,不提倡与机器的底层结构通信,如果直接 用Windows的 API函数或I/O读写指令进行访问和操作,程序运行时往往就会产生保护模 式错误甚至死机,更严重的情况会导致系统崩溃。那么在Windows下怎样方便地解决上述

趣味程序导学 Visual C++

258

问题呢?用DLL(Dynamic Link Libraries)技术即可。 DLL是Windows最重要的组成要素,Windows中的许多新功能、新特性都是通过DLL 来实现的,因此掌握它、应用它是非常重要的。其实Windows本身就是由许多DLL组成 的,它最基本的三大组成模块Kernel、GDI和User 都是DLL,它所有的库模块也都设计成 DLL。凡是以.DLL,.DRV,.FON,.SYS和许多以.EXE为扩展名的系统文件都是DLL,要 是打开Windows\System目录,就可以看到许多DLL模块。尽管DLL在Ring3优先级下运 行,它仍是实现硬件接口的简便途径。DLL可以有自己的数据段,但没有自己的堆栈,使 用与调用它的应用程序相同的堆栈模式,减少了编程的不便;同时,一个DLL在内存中只 有一个实例,使之能高效经济地使用内存;DLL实现的代码封装性,使得程序简洁明晰; 此外还有一个最大的特点,即DLL的编制与具体的编程语言及编译器无关,只要遵守DLL 的开发规范和编程策略,并安排正确的调用接口,不管用何种编程语言编制的DLL都具有 通用性。 DLL的引用一般有两种方式:隐式连接和显式连接。 隐式连接要在DLL代码中用extern “C”__declsped(dllexport)int MyFunction(int n)声明 导出函数并在客户程序中用extern “C”__declsped(dllimport)int MyFunction(int n)声明导 入函数,当然DLL文件编译产生的*.Lib也要被加到应用程序的Project中去。 也可显式用LoadLibrary(“DLLname”)加载DLL。 简单的说,使用DLL要遵循如下步骤: (1)把编译后的*.DLL文件、*.LIB和所有的头文件复制到应用程序的目录。 (2)用Project | Add to Project | Files加入*.LIB文件。 (3)主程序的头文件中包含所有的头文件。 我们用AppWizard创建一个基于DLL的工程,如图8.6所示。

图 8.6

M

创建动态链接库

注意:要选中Windows Sockets的复选框和MFC Extension DLL,如图8.7所 示。最后的新工程创建信息如图8.8所示。 

第8章

属于你的 OICQ——Visual C++ 网络编程

图 8.7

创建 Windows Sockets 程序

图 8.8

8.3

259

新工程创建信息

创建基于TCP协议的Socket类

网络上的各计算机之间进行通信时有一种约定,这就是网络协议。不同的计算机之间 必须使用相同的网络协议才能进行数据交换。网络协议有很多种,具体选择哪一种协议则 要看情况而定。Internet上的计算机使用的是TCP/IP协议。 Winsock库支持很多种网络协议,当然包括Internet Protocol(IP)协议和TCP协议,为 了熟悉库中的重要函数,了解如何建立Socket,笔者分析了一个基于TCP协议的Socket类 定义及其实现,并对其中的数据结构和关键函数(尤其是Winsock的基本操作)进行了详 细分析。 8.3.1

WinSock介绍

Windows下网络编程的规范——Windows Sockets是Windows下得到广泛应用的、开放

趣味程序导学 Visual C++

260

的、支持多种协议的网络编程接口。从1991年的1.0版到1995年的2.0.8版,经过不断完善 并在Intel,Microsoft,Sun,SGI,Informix,Novell等公司的全力支持下,已成为Windows 网络编程的事实上的标准。 Windows Sockets规范以U.C. Berkeley大学BSD UNIX中流行的Socket接口为范例定义 了一套Micosoft Windows下网络编程接口。它不仅包含了人们所熟悉的Berkeley Socket风 格的库函数,也包含了一组针对 Windows的扩展库函数,以使程序员能充分地利用 Windows消息驱动机制进行编程。Windows Sockets规范本意在于提供给应用程序开发者一 套简单的API,并让各家网络软件供应商共同遵守。此外,在一个特定版本Windows的基 础上,Windows Sockets也定义了一个二进制接口(ABI ),以此来保证应用Windows Sockets API的应用程序能够在任何网络软件供应商的符合Windows Sockets协议的实现上 工作。因此这个规范定义了应用程序开发者能够使用,并且网络软件供应商能够实现的一 套库函数调用和相关语义。遵守这套 Windows Sockets规范的网络软件,我们称之为 Windows Sockets 兼容的,而 Windows Sockets 兼容实现的提供者,我们称之为Windows Sockets提供者。任何能够与Windows Sockets兼容实现协同工作的应用程序就被认为是具 有Windows Sockets接口,我们称这种应用程序为Windows Sockets 应用程序。Windows Sockets规范定义并记录了如何使用API与Internet协议族(IP,通常我们指的是TCP/IP)连 接,尤其要指出的是所有的Windows Sockets实现都支持流套接口和数据报套接口。应用 程序调用Windows Sockets的API实现相互之间的通信。Windows Sockets又利用下层的网络 通信协议功能和操作系统调用实现实际的通信工作。它们之间的关系如图8.9所示。 应用程序1

应用程序2

网络编程接口,例如Windows Sockets

网络通信协议服务接口,例如TCP/IP

操作系统,例如Windows

物理通信介质 图 8.9

网络协议间的关系

通信的基础是套接口(Socket),一个套接口是通信的一端,在该端上你可以找到与 其对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接口存 在于通信域中。通信域是为了处理一般的线程通过套接口通信而引进的一种抽象概念。套 接口通常和同一个域中的套接口交换数据(数据交换也可能穿越域的界限,但这时一定要

第8章

属于你的 OICQ——Visual C++ 网络编程

261

执行某种解释程序)。Windows Sockets规范支持单一的通信域,即Internet域。各种进程 使用这个域用Internet协议来进行通信(Windows Sockets 1.1以上的版本支持其他的域,例 如Windows Sockets 2)。套接口可以根据通信性质分类;这种性质对于用户是可见的。应 用程序一般仅在同一类的套接口间通信。不过只要底层的通信协议允许,不同类型的套接 口间也照样可以通信。用户目前可以使用两种套接口,即流套接口和数据报套接口。流套 接口提供了双向的,有序的,无重复并且无记录边界的数据流服务。数据报套接口支持双 向的数据流,但并不保证是可靠,有序,无重复的。也就是说,一个从数据报套接口接收 信息的进程有可能发现信息重复了,或者和发出时的顺序不同。数据报套接口的一个重要 特点是它保留了记录边界。对于这一特点,数据报套接口采用了与现在许多包交换网络 (例如以太网)非常类似的模型。 在建立分布式应用时最常用的便是客户机/服务器模型。在这种方案中客户应用程序 向服务器程序请求服务。这种方式隐含了在建立客户机/服务器间通信时的非对称性。客 户机/服务器模型工作时要求有一套为客户机和服务器所共识的约定来保证服务能够被提 供(或被接受)。这一套约定包含了一套协议。它必须在通信的两端都被实现。根据不同 的实际情况,协议可能是对称的或是非对称的。 在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变 地认为是主机,而另一方则是从机。一个对称协议的例子是Internet中用于终端仿真的 TELNET。而非对称协议的例子是Internet中的FTP。无论具体的协议是对称的或是非对称 的,当服务被提供时必然存在“客户进程”和“服务进程”。 一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一 直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程 序被“唤醒”并且为客户提供服务——对客户的请求作出适当的反应。这一请求/响应的 过程可以简单的用图8.10表示。虽然基于连接的服务是设计客户机/服务器应用程序时的标 准,但有些服务也是可以通过数据报套接口提供的。 客户机

服务器 进行通信设施

请求

请求 响应 响应

图 8.10

请求/响应过程

Intel处理器的字节顺序是和DEC VAX处理器的字节顺序一致的,但它与68000型处理 器以及Internet的顺序是不同的,所以用户在使用时要特别小心以保证正确的顺序。任何从 Windows Sockets函数对IP地址和端口号的引用和传送给Windows Sockets函数的IP地址和 端口号均是按照网络顺序组织的,这也包括了sockaddr_in结构这一数据类型中的IP地址域 和端口域(但不包括sin_family域)。考虑到一个应用程序通常用与“时间”服务对应的 端口来和服务器连接,而服务器提供某种机制来通知用户使用另一端口。因此

趣味程序导学 Visual C++

262

getservbyname()函数返回的端口号已经是网络顺序了,可以直接用来组成一个地址,而不 需要进行转换。然而如果用户输入一个数,而且指定使用这一端口号,应用程序则必须在 使用它建立地址以前,把它从主机顺序转换成网络顺序(使用htons()函数)。相应地,如 果应用程序希望显示包含于某一地址中的端口号(例如从getpeername()函数中返回的), 这一端口号就必须在被显示前从网络顺序转换到主机顺序(使用ntohs()函数)。由于Intel 处理器和Internet的字节顺序是不同的,上述的转换是无法避免的,应用程序的编写者应该 使用Windows Sockets API标准的转换函数,而不要使用自己的转换函数代码。只有使用标 准的转换函数的应用程序才是可移植的。 在MFC中微软为套接口提供了相应的类 CAsyncSocket和CSocket,CAsyncSocket提供 基于异步通信的套接口封装功能,CSocket则是由CAsyncSocket派生,提供更高级的功 能,例如可以将套接口上发送和接收的数据和一个文件对象(CSocketFile)关联起来,通 过读写文件来达到发送和接收数据的目的,此外CSocket提供的通信为同步通信,数据未 接收到或是未发送完之前调用不会返回。此外通过MFC,开发者可以不考虑网络字节顺序 和忽略掉更多的通信细节。 在一次网络通信/连接中有以下几个参数需要被设置:本地IP地址,本地端口号,对 方端口号,对方IP地址。前两页称为半关联,当与后两项建立连接后就称为一个全关联。 在这个全关联的套接口上可以双向交换数据。如果是使用无连接的通信,则只需要建立半 关联,在发送和接收时指明另一半参数就可以了,所以可以说无连接的通信是将数据发送 到另一台主机的指定端口。此外不论是有连接还是无连接的通信都不需要双方的端口号相 同。 在创建CAsyncSocket对象时通过调用 BOOL CAsyncSocket::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 )

通过指明lEvent所包含的标记来确定需要异步处理的事件,对于指定的相关事件的相 关函数调用都不需要等待完成后才返回,函数会马上返回,然后在完成任务后发送事件通 知,并利用重载以下成员函数来处理各种网络事件,如表8.1所示。 表8.1

处理网络事件的重载函数

标记

事件

需要重载的函数

FD_READ

有数据到达时发生

void OnReceive( int nErrorCode );

FD_WRITE

有数据发送时产生

void OnSend( int nErrorCode );

FD_OOB

收到带外数据时发生

void OnOutOfBandData( int nErrorCode );

FD_ACCEPT

作为服务端等待连接成功时发生

void OnAccept( int nErrorCode );

FD_CONNECT

作为客户端连接成功时发生

void OnConnect( int nErrorCode );

FD_CLOSE

套接口关闭时发生

void OnClose( int nErrorCode );

我们看到重载的函数中都有一个参数nErrorCode,为0则表示正常完成,非0则表示错

第8章

属于你的 OICQ——Visual C++ 网络编程

263

误。通过int CAsyncSocket::GetLastError()可以得到错误值。 下面我们看看套接口类所提供的一些功能,通过这些功能我们可以方便的建立网络连 接和发送数据: ・ BOOL CAsyncSocket::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为使用的端口号,为0则表示由系统自动选择,通 常在客户端都使用这个选择。nSocketType为使用的协议族,SOCK_STREAM表明 使 用 有 连 接 的 服 务 , SOCK_DGRAM 表 明 使 用 无 连 接 的 数 据 报 服 务 。 lpszSocketAddress为本地的IP地址,可以使用点分法表示。 ・ BOOL CAs yncSocket::Bind ( UINT nSocketPort, LPCTSTR lpszSocketAddress = NULL)等待连接方时产生网络半关联,或者是使用UDP协议时产生网络半关联。 ・ BOOL CAsyncSocket::Listen(int nConnectionBacklog = 5)等待连接方时指定同时 可以接受的连接数,请注意不是总共可以接受的连接数。 ・ BOOL CAsyncSocket::Accept ( CAsyncSocket& rConnectedSocket, SOCKADDR* lpSockAddr = NULL, int* lpSockAddrLen = NULL)等待连接建立,当连接建立后 将创建一个新的套接口,该套接口将用于通信。 ・ BOOL CAsyncSocket::Connect(LPCTSTR lpszHostAddress, UINT nHostPort);连 接方发起与等待连接方的连接,需要指定对方的IP地址和端口号。 ・ void CAsyncSocket::Close( );关闭套接口。 ・ int CAsyncSocket::Send(const void* lpBuf, int nBufLen, int nFlags = 0) int CAsyncSocket::Receive(void* lpBuf, int nBufLen, int nFlags = 0);在建立连接 后发送和接收数据,nFlags为标记位,双方需要指定相同的标记位。 ・ int CAsyncSocket::SendTo ( const void* lpBuf, int nBufLen, UINT nHostPort, LPCTSTR lpszHostAddress = NULL, int nFlags = 0)对无连接通信发送数据 ・ int CAsyncSocket::ReceiveFrom(void* lpBuf, int nBufLen, CString& rSocketAddress, UINT& rSocketPort, int nFlags = 0);对于无连接通信接收数据,需要指定对方的 IP地址和端口号,nFlags为标记位,双方需要指明相同的标记位。 我们可以看到大多数函数都返回一个布尔值表明是否成功。如果发生错误可以通过int CAsyncSocket::GetLastError()得到错误值。 由于CSocket由CAsyncSocket 派生,所以它拥有CAsyncSocket 的所有功能,此外你可 以通过BOOL CSocket::Create(UINT nSocketPort = 0, int nSocketType = SOCK_STREAM, LPCTSTR lpszSocketAddress = NULL)来创建套接口,这样创建的套接口没有办法异步处 理事件,所有的调用都必需完成后才会返回。 在上面的介绍中我们看到MFC提供的套接口类屏蔽了大多数细节,我们只需要做很少 的工作就可以开发出利用网络进行通信的软件。

趣味程序导学 Visual C++

264

8.3.2

在DLL中添加CTCPSocket类

选择Insert | New Class加入一个新类,如图8.13所示:

图 8.11

加入 CTCPSocket 类

单击OK按钮,生成类的基本定义。 由于CTCPSocket类要求是一个可以从DLL中导出的类,所以我们必须修改类的声明 部分,具体操作是在class与CTCPSocket之间加入AFX_EXT_CLASS,如下所示: class AFX_EXT_CLASS CTCPSocket

8.3.3

成员变量及其说明

在这个类中加入以下成员变量: protected: HWND m_hWnd; HANDLE m_hListenThread; SOCKET m_sockUse[2]; WORD m_wPort; WORD m_wFlag; BYTE m_bMaxListen; UINT m_uAccept; UINT m_uRead;

其中: ・ m_hWnd是一个CWnd的句柄,一般由 GetSafeHwnd()函数产生以保证用户得到一个 安全的窗口,有了安全的窗口句柄才可以使用消息映射机制使用户和程序进行交 互。 ・ m_hListenThread表示一个线程的句柄(我们假定用户已经对VC++的多线程操作

第8章

属于你的 OICQ——Visual C++ 网络编程

265

有一定的了解)。以后,我们会启动一个Listen线程以保证运行应用程序的两台计 算机可以保持通信。具体应用方法如下: m_hListenThread=CreateThread( NULL, 0,(LPTHREAD_START_ROUTINE) ListenThread,pstrListen,0,&dwRet);

其中,ListenThread是一个线程函数,也就是m_hListenThread这个线程的线程体。 而 pstrListen 是 一 个 指 向 我 们 自 定 义 结 构 strListen 的 指 针 , 它 是 用 来 为 函 数 ListenThread传递参数的,dwRet变量是用来存放新线程的标识的。 ・ m_sockUse[2]是一个SOCKET描述符的数组,m_sockUse[0]用来和另一台计算机建 立SOCKET联系,而m_sockUse[1]则是在收到m_sockUse[0]请求后,建立的一个新 的SOCKET,它和m_sockUse[0]有同样的属性,并将取代m_sockUse[0]实际操作建 立的连接。具体应用方法如下: m_sockUse[0]=socket(AF_INET,SOCK_STREAM,0); m_sockUse[1]=accept(m_sockUse[0], (struct sockaddr FAR *)&sinClient, (int FAR*)&iLength);

・ m_wPort指的是我们创建的Socket端口号。我们在程序里定义一个常量: #define ESTSOCKET

0X2022

・ m_wFlag是一个很重要的标志,用它来区分客户程序的状态,并决定要采用的处 理方法。初始: #define WSA_NOTIFY

0X0100

・ m_bMaxListen定义了Socket所能响应的最大连接数。 ・ m_uAccept 是我们定义的一个消息,当客户程序被连接到Socket 时,返回这个消 息。 #define ID_THREADACCEPT

WM_USER + 0X0F00

#define WSA_ACCEPT

ID_THREADACCEPT

・ m_uRead是我们定义的另一个消息,当Socket连接结束或者收到另一方数据时返 回。 #define WSA_READ

WM_USER + 0X0F01

我们上面提到一个传递给线程ListenThread的结构strListen,它由以下几个变量组成: struct strListen { HWND hWnd; WORD wPort; BYTE bMaxListen; UINT uMessage; BOOL fDelAuto;

趣味程序导学 Visual C++

266 SOCKET *psockUse; };

StrListen中的大部分变量的意义与上面相应变量相同,稍有区别的是uMessage和 fDelAuto。UMessage和上面的m_uAccept对应,而fDelAuto是一个是否自动清除所传递参 数的标志。 8.3.4

成员函数及其说明

另外,我们还需要一些基本函数进行最基本的操作: 类的构造函数和析构函数 首先创建CTCPSocket类的构造函数,相应代码如下: CTCPSocket::CTCPSocket(void) { m_sockUse[0]=m_sockUse[1]=-1; m_hListenThread=NULL; }

构造函数使我们定义的两个SOCKET描述符和线程句柄无效(等待以后创建)。 析构函数代码如下: CTCPSocket::~CTCPSocket(void) { Close(); } void CTCPSocket::Close(void) { if(m_sockUse[1]>0) closesocket(m_sockUse[1]); if(m_sockUse[0]>0) closesocket(m_sockUse[0]); m_sockUse[0]=m_sockUse[1]=-1; }

析构函数调用Close()函数,closesocket()函数关闭由Socket描述符描述的Socket。 Socket 的初始化 我们创建初始化过程InitData(),以便根据客户应用程序的需要对类变量赋值,相应代 码如下: void CTCPSocket::InitData(HWND hWndOwner, WORD wPort, BOOL fListen, WORD wFlag, BYTE bMax, UINT uAccept)

第8章

属于你的 OICQ——Visual C++ 网络编程

267

{ Close(); m_hWnd=hWndOwner; m_uAccept=uAccept; m_wFlag=wFlag; m_wPort=wPort; m_bMaxListen=bMax; m_wFlag |=( fListen )? LISTEN_SIDE:CONNECT_SIDE; }

LISTEN_SIDE和CONNECT_SIDE是我们定义的两个常量,他们与m_wFlag连用可以 判断程序运行时到底处在什么地位,也就是说判断我们的应用程序是作为被动还是主动连 接的一方。相应代码为: #define LISTEN_SIDE

0X1000

#define CONNECT_SIDE

0X0000

8.3.5

建立连接

下面我们创建建立连接用的Establish()函数,相应代码如下: BOOL CTCPSocket::Establish(LPCSTR lpszOtherHost) { ASSERT(m_sockUse[0]==-1); if(IsListenSide()) return ListenSide(); else { ASSERT(lpszOtherHost); return ConnectSide(lpszOtherHost); } }

函数先是判断我们的客户程序究竟是属于被连接还是主动连接其他机器的一方,如果 是 被 动 的 一 方 则 调 用 ListenSide() 函 数 , 否 则 调 用 ConnectSide() 函 数 。 这 个 判 断 是 由 IsListenSide()完成的,相应代码为: BOOL CTCPSocket::IsListenSide(void) { return m_wFlag&LISTEN_SIDE;// 当m_wFlag和LISTEN_SIDE相等时返回TRUE }

如果是被连接一方,接下来调用ListenSide()函数,相应代码为: BOOL CTCPSocket::ListenSide(void) {

趣味程序导学 Visual C++

268

WORD wFlag=m_wFlag; wFlag&=0X0111; //取m_wFlag的后三位 switch(wFlag) { case(BLOCKING_NOTIFY): return BSDListen(); break; case(THREAD_NOTIFY): return ThreadListen(); break; case(WSA_NOTIFY): return WSAListen(); break; default: ASSERT(0); break; } return FALSE; }

BLOCKING_NOTIFY,THREAD_NOTIFY和WSA_NOTIFY是类定义的三个常量,用 来区分三种不同的等待方式,并使用三个不同的函数分别进行处理: #define BLOCKING_NOTIFY

0X0001

#define THREAD_NOTIFY

0X0010

#define WSA_NOTIFY

0X0100

(1)异步等待 WSAListen()和 WSAAccept() BOOL CTCPSocket::WSAListen(void) { SOCKADDR_IN sinLocal; //这是一个Socket的完整说明 PHOSTENT pHost; //用来存放主机的信息 char szHostName[60]; int iStatus; m_sockUse[0]=socket(AF_INET,SOCK_STREAM,0); //建立一个基于Internet //TCP协议的Socket if(m_sockUse[0]h_addr, pHost->h_length); iStatus=bind(m_sockUse[0],(struct sockaddr FAR*)&sinLocal, sizeof(sinLocal)); //把m_sockUse[0]绑定在本地主机上,为以后的listen,connect作准备 if(iStatus(iStatus=listen(m_sockUse[0],m_bMaxListen)))

// 把m_sockUse[0] //设为被动监听模式

{ closesocket(m_sockUse[0]); //失败 m_sockUse[0]=-1; return FALSE; } iStatus=WSAAsyncSelect(m_sockUse[0],m_hWnd,m_uAccept,FD_ACCEPT); //当m_sockUse[0]收到信息的时候,产生一个m_uAccept的消息。 if(iStatus>0) { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } else return TRUE; }

函数中的注释已经相当清楚,不过有几点还要强调一下:首先这种等待是异步的。这 也是它与其他两种等待方式的不同;其次,SOCKADDR_IN的family 必须是AF_INET;另 外,建立连接的顺序是: socket()->bind()->listen()/connect()->closesocket()

趣味程序导学 Visual C++

270

(2)同步函数 BOOL CTCPSocket::WSAAccept(void) { SOCKADDR_IN sinClient; int iLength; ASSERT(m_sockUse[0]!=-1); ASSERT(m_wFlag&WSA_NOTIFY); iLength=sizeof(sinClient); m_sockUse[1]=accept(m_sockUse[0], //函数将被阻塞,m_sockUse[0]有信号,建立新的socket,并将请求信息保存。 (struct sockaddr FAR *)&sinClient, (int FAR*)&iLength); if(SOCKET_ERROR==m_sockUse[1]) { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } else return TRUE; }

(3)同步等待 BSDListen() BOOL CTCPSocket::BSDListen(void) { SOCKADDR_IN sinLocal,sinClient; PHOSTENT pHost; char szHostName[60]; int iStatus; int iLength; m_sockUse[0]=socket(AF_INET,SOCK_STREAM,0); if(m_sockUse[0]h_addr, pHost->h_length); iStatus=bind(m_sockUse[0],(struct sockaddr FAR*)&sinLocal, sizeof(sinLocal)); if(iStatus(iStatus=listen(m_sockUse[0],m_bMaxListen))) { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } iLength=sizeof(sinClient); m_sockUse[1]=accept(m_sockUse[0], (struct sockaddr FAR *)&sinClient, (int FAR*)&iLength); if(SOCKET_ERROR==m_sockUse[1]) //失败 { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } else return TRUE; }

该函数的结构基本上和WSAListen()+WSAAccept()相同,所不同的是:由于该函数是 一个同步函数,所以它的输入和输出必须在一次操作中完成,而不能像WSAListen()和 WSAAccept()那样分成两个函数。也就是说,在该函数中必须要收到另一方的回应后才能 继续运行,这就是同步的意义所在。

趣味程序导学 Visual C++

272

(4)线程监听 ThreadListen() 第三种等待方式是设置一个单独的线程来监听可能到达的消息,线程设置函数如下 (指向strListen的指针pstrListen是我们传给线程体的参数): BOOL CTCPSocket::ThreadListen(void) { strListen* pstrListen=new (strListen); pstrListen->hWnd=m_hWnd; pstrListen->bMaxListen=m_bMaxListen; pstrListen->uMessage=m_uAccept; pstrListen->fDelAuto=TRUE; pstrListen->wPort=m_wPort; pstrListen->psockUse=m_sockUse; m_hListenThread=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ListenThread, pstrListen, 0, &dwRet); return (m_hListenThread!=NULL); } // 使用一个外部函数ListenThread()作为线程体: DWORD ListenThread(strListen *pstrListen) { SOCKADDR_IN sinLocal,sinClient; PHOSTENT pHost; char szHostName[60]; int iStatus; int iLength; strListen strListens; memcpy((void*)&strListens,(void*)pstrListen,sizeof(strListen)); //把参数复制,防止可能产生的互斥问题 if(pstrListen->fDelAuto) delete pstrListen;

//由参数决定是否清除原参数

strListens.psockUse[0]=socket(AF_INET,SOCK_STREAM,0); //建立一个基于 //InternetTCP协议的Socket if(strListens.psockUse[0]h_addr, pHost->h_length); iStatus=bind(strListens.psockUse[0],(structsockaddr FAR*)&sinLocal, sizeof(sinLocal)); if(iStatus(iStatus=listen(strListens.psockUse[0],strListens.bMaxListen))) { closesocket(strListens.psockUse[0]); strListens.psockUse[0]=-1; SendMessage(strListens.hWnd, strListens.uMessage, 0, WSAMAKESELECTREPLY(FD_ACCEPT,1)); return 0; } iLength=sizeof(sinClient); strListens.psockUse[1]=accept(strListens.psockUse[0], (struct sockaddr FAR *)&sinClient, (int FAR*)&iLength); if(SOCKET_ERROR==strListens.psockUse[1])

273

趣味程序导学 Visual C++

274 {

closesocket(strListens.psockUse[0]); strListens.psockUse[0]=-1; SendMessage(strListens.hWnd, strListens.uMessage, 0, WSAMAKESELECTREPLY(FD_ACCEPT,1)); return 0; } else { SendMessage(strListens.hWnd, strListens.uMessage, 0, WSAMAKESELECTREPLY(FD_ACCEPT,0)); return TRUE; } }

这个线程的操作基本和BSDListen()相同,也是一种同步等待方式。更确切的说,只是 这个子线程是同步的。另外我们之所以选择“消息传递”而不是“共享数据”的方式进行 父子线程之间的通信,是因为消息传递方式更加安全,也基本没有互斥与同步的问题。 8.3.6

连接方连接函数

以上是作为等待方的处理过程(也可以理解为Server方式)下面我们创建连接方连接 函数ConnectSide()。如果作为主动连接方(Client),就要用ConnectSide()函数,相应代码 如下: BOOL CTCPSocket::ConnectSide(LPCSTR lpszServer) //要指定Server的地址 { SOCKADDR_IN sinServer; PHOSTENT pHost; int iStatus; sinServer.sin_family=AF_INET; pHost=gethostbyname(lpszServer); //得到可以识别的地址 if(pHost==NULL) return FALSE; sinServer.sin_port=htons(m_wPort); //使用定义的端口 memcpy(&sinServer.sin_addr,pHost->h_addr,pHost->h_length); m_sockUse[0]=socket(AF_INET,SOCK_STREAM,0); if(m_sockUse[0]0) closesocket(m_sockUse[0]); m_sockUse[0]=SOCKET_ERROR; }

(4)调用Close()函数,关闭所有连接: void CTCPSocket::Close(void) {

第8章

属于你的 OICQ——Visual C++ 网络编程

277

if(m_sockUse[1]>0) closesocket(m_sockUse[1]); if(m_sockUse[0]>0) closesocket(m_sockUse[0]); m_sockUse[0]=m_sockUse[1]=-1; }

M

注意:CTCPSocket类是我们定义的Socket类,由于采用了DLL方式,它可以 方便的应用到我们以后要编写的应用程序中,这就是DLL方式最大的优势。 CTCPSocket类只支持TCP/IP协议,如果读者要使用其他的网络协议,请参照 本例进行相应的修改。另外,强烈建议读者在编写程序时参照MSDN,MSDN是 微软最权威的帮助文档,既然 VC++都是微软开发的, MSDN的重要性不言而 喻! 

8.4

两人聊天的OICQ

在前面几节里,我们创建了一个基于Windows Socket的DLL(动态链接库),用它来 进行网络通信,下面我们将利用它来创建网络聊天程序。 8.4.1

用AppWizard建立工程

创建一个基于对话框的程序结构,详细步骤如下: (1)首先创建一个基于MFC的工程,如图8.12所示。

图 8.12

创建工程 myICQ

(2)单击OK按钮,在出现的对话框中选中Dialog based选项,建立一个基于对话框

趣味程序导学 Visual C++

278

的应用程序,如图8.13所示。

图 8.13

建立基于对话框的应用程序

(3)单击Next按钮,选中About box,3D controts,ActiveX Controts,由于是Socket 编程,所以也要选中Windows Sockets复选框,如图8.14所示。

图 8.14

选择 Windows Sockets 支持

最后的两个步骤如图8.15和8.16所示。

第8章

8.4.2

属于你的 OICQ——Visual C++ 网络编程

图 8.15

工程创建的第四步

图 8.16

工程创建的第五步

279

生成用户界面

如图8.17 所示,即是我们应用程序的用户界面,用户可以选择作为等待方还是连接 方,若是连接方,就要指定主机地址。 图中两个文本框是用来输入和输出对话内容的,各个组件的ID都已经添加。

趣味程序导学 Visual C++

280

图 8.17

8.4.3

用户界面

加入所需变量

图 8.18

加入所需变量

我们为图中3个需要接收用户输入的文本框分别添加了3个CString类型的变量 另外,我们要为我们的对话框类添加一个CTCPSocket的成员变量myTS(看,有用了 吧!)。 别忘了#include "TCPSocket.h"!,我们还要加入一个成员变量BOOL m_IsListen,以此 决定是否为等待连接的一方(当然,在此之前要按照上面所说的顺序把DLL的LIB文件和 头文件包含在Project中!)。

第8章

8.4.4

属于你的 OICQ——Visual C++ 网络编程

281

编写初始化函数

在OnInitDialog()函数中加入如下代码,设置初始对话框状态: ((CButton*)GetDlgItem(IDC_Listen))->SetCheck(1); ((CButton*)GetDlgItem(IDC_connect))->SetCheck(0); ((CEdit*)GetDlgItem(IDC_Addr))->EnableWindow(FALSE); m_IsListen=TRUE;

8.4.5

进行函数映射

为了能够对我们的操作进行响应,我们要对可能产生的用户动作加入相应的处理函 数: (1)为IDC_con添加Click事件,如图8.19所示。

图 8.19

添加 Click 事件

编写过程代码如下: void CMyICQDlg::Oncon() { // TODO: Add your control notification handler code here BOOL m_IsListen=(1==((CButton*)GetDlgItem(IDC_Listen))->GetCheck()); CString szOtherName; ((CEdit*)GetDlgItem(IDC_Addr))->GetWindowText(szOtherName); HWND hwndParent=GetSafeHwnd(); if(!m_IsListen&&(0==szOtherName.GetLength())) {

趣味程序导学 Visual C++

282

AfxMessageBox("You need input the other PC's name!",MB_OK); return; } myTS->InitData(hwndParent,ESTSOCKET,m_IsListen,WSA_NOTIFY); if(myTS->Establish(szOtherName)) { if(!m_IsListen) CDialog::OnOK(); else { SetTimer(1,500,NULL); m_iTimer=1; } } else AfxMessageBox("Connect Fail",MB_OK); if(!m_IsListen) GetDlgItem(IDC_con)->EnableWindow(); }

该函数主要用来判断用户的选择,如果是等待方就调用等待的过程,如果是连接方就 调用连接函数。 (2)为消息WSA_ACCEPT定义OnWSAAccept()函数: 还记得定义在CTCPSocket里的WSA_ACCEPT消息吗?这里定义了OnWSAAccept()函 数。有了它我们就可以通过WSA_ACCEPT消息响应连接方的信息了。 首 先 , 在 BEGIN_MESSAGE_MAP(CEstDlg,CDialog) 与 END_MESSAGE_MAP() 中 加 入ON_MESSAGE ( WSA_ACCEPT, OnWSAAccept)。然后在类声明中加入afx_msg LONG OnWSAAccept(UINT uP,LONG lP);这样我们就可以继续编写相应函数代码了: void CMyICQDlg::OnWSAAccept(UINT uP,LONG lP) { AfxMessageBox("Connected!"); if(m_iTimer) KillTimer(1); GetDlgItem(IDC_con)->EnableWindow(TRUE); if(WSAGETSELECTERROR(lP)==0) { myTS->WSAAccept(); CDialog::OnOK(); } else { myTS->CancelListen();

第8章

属于你的 OICQ——Visual C++ 网络编程

283

AfxMessageBox("Connect Failed !",MB_OK); } return ; }

通过这个函数等待方就可以得到连接方的信息,并最终建立可以传输数据的Socket。 (3)映射发送函数 映射IDC_send消息(同时也完成接收的操作),如图8.20所示。

图 8.20

映射 IDC_send 消息

相应代码如下: void CMyICQDlg::Onsend(void) { myTS->Write((WORD*)sizeof(m_Smesg),2); myTS.->rite((LPCSTR)&m_Smesg,sizeof(m_Smesg)); WORD wSize; MyTS->Read((LPSTR)&wSize,sizeof(wSize)); LPSTR lpszRead=new char[wSize+1]; myTS->Read(lpszRead,wSize); lpszRead[wSize]=”\0”; ((CEdit*)GetDlgItem(IDC_R_message))->SetWindowText(lpszRead); delete lpszRead; }

(4)退出函数: 由IDCANCEL映射而得,如图8.21所示。

趣味程序导学 Visual C++

284

图 8.21

映射 IDCANCEL

相应代码如下: void CMyICQDlg::OnCancel() { // TODO: Add extra cleanup here myTS->Close(); CDialog::OnCancel(); }

大功告成,你终于拥有自己的聊天软件了,不过稍微简单了点,读者可以进一步去完 善它,希望本章内容对你有所帮助!

8.5

本章知识点回顾

下面给出Winsock库核心函数和宏,如表8.3所示。 表8.3

Winsock库核心函数和宏

定义

说明

SOCKET socket(

返回Socket描述符

);

int af, int type,

af为Socket的地址族 type一般有两种类型SOCK_STREAM和

int protocol

SOCK_DGRAM 分别对应TCP和UDP协议

第8章

属于你的 OICQ——Visual C++ 网络编程

285 续表

定义

说明

int bind(

把Socket描述符s绑定到指定的计算机

SOCKET s,

s就是Socket描述符

const struct sockaddr FAR *name, int namelen

name是用来描述对象计算机的 namelen是name的长度

); int connect(

把Socket描述符连接到指定计算机

SOCKET s,

参数函数和bind完全相同

const struct sockaddr FAR *name, int namelen

不同的是connect函数要调用bind函数 而且bind一般是与本地计算机相连

); int listen(

使Socket描述符等待一个即将到来的连接

SOCKET s,

s是Socket描述符

int backlog

backlog是可以等待的最大连接数

); int send( SOCKET s,

通过Socket描述符发送数据 s是Socket描述符

const char FAR *buf,

buf是要发送的数据区的指针

int len, int flags

len是要发送的数据长度 flags是发送模式,一般为0

); int recv(

通过Socket描述符接收数据

SOCKET s,

s是Socket描述符

char FAR *buf, int len,

buf是接收到数据存放的数据区 len是数据长度

int flags

flags同上

);

在本章的最后,笔者再次强调MSDN的重要性。熟读MSDN是对程序员的基本要求!