科海电脑技术丛书
趣味程序导学 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; cout<<"x*y="<
上面的x和y称为类的元素或成员,类的成员可以是其他的对象甚至它本身的对象(这 时必须是指针),也可以是函数。事实上,类是对象和用来建立、操作、撤消这些对象的 函数集合,这种函数常常称为方法,这些方法可以访问所有的数据成员。也就是说C++中 的类可以包含函数这样的数据成员。不论是函数还是数据成员,默认都是公有的( public ), 即可以被外部直接访问。 2.构造函数 从上面可以看出,通过赋值语句来初始化对象的数据成员使很繁琐的,实际上,对于 简单的类,可以用同结构实例相同的初始化方法来初始化类的对象。例如: TPoint pl={0,0};
但是大多数情况下类不是这么简单的,它可能需要根据不同的情况来进行初始化。C++ 中的类可以包含成员函数和数据成员,成员函数可以访问类的所有数据成员。这些函数中 包含一个或多个特定成员函数——构造函数,它们被用作初始化对象,构造函数定义前没 有返回值(其实构造函数是有返回值的,这个返回值就是构造完整后的对象的引用),构 造函数可以重载,即类的构造函数可以有多个,他们通过不同的参数表来区分。如果类的 定义中不带任何构造函数,那么编译器将产生一个不带任何参数的默认构造函数,对于任 何一个作为类的数据成员的对象来说,编译器会调用默认的构造函数。当编译器使用默认 构造函数时,会给其对应的类对象分配空间,但并不对其内部类型的值进行初始化。例如 上面的例子,可以按照下面的方法加入构造函数: //--------------------------------------------------#include
#include //--------------------------------------------------class TPoint { public: int X; int Y; TPoint(int x,int y) { X=x; Y=y; } };
第1章
初识 Visual C++
5
//---------------------------------------------void main() { TPoint pl(10,10); cout<<"pl: "<
运行结果如下: p1: 10*10
如果把构造函数去掉,让编译器调用默认的构造函数,看看能得到什么结果: //-------------------------------------------------#include #include //--------------------------------------------------class TPoint { public: int X; int Y; /* TPoint(in x, int y) { X=x; Y=y; } */ }; //--------------------------------------------------------void main() { TPoint pl; cout<<"pl: "<
运行结果为: pl:256*1
显然这个结果是随机的,这说明编译器并没有给类赋初值。上例中,把构造函数声明
6
Visual C++趣味程序导学
为内联函数,有这样的约定:编译器默认认为在类内定义的函数为内联函数,所以认为没 有必要或不宜成为内联函数的成员函数不要在类内定义。当然在类外定义的成员函数只要 加上inline关键字也可以成为内联函数。 当定义了自己的构造函数以后,就不能再用像结构那样的简单赋值方式,也不能调用 默认构造函数,除非定义了自己的默认构造函数(也就是不带参数的构造函数)。下面的 语句将是非法的: TPoint pl(0,0); TPoint pl; //除非定义了自己的默认构造函数
3. 析构函数 析构函数是另外一种特殊的类成员函数,它的名字也是确定的——在类名字前加符号 “~”。有构造函数,就有析构函数。构造函数负责分配和申请系统资源,而析构函数负责 在对象超出作用域以后,释放系统资源。析构函数有且只有一个,它不带任何参数,也没 有任何返回值。下面举例说明: //-------------------------------------------------#include #include //--------------------------------------------------class TPoint { public: int X; int Y; TPoint();//默认构造函数 TPoint(int x, int y); ~TPoint(); //析构函数 }; //--------------------------------------------------void main() { { TPoint p1; TPoint p2(800,600); cout<<"pl: "<
第1章
初识 Visual C++
7
{ cout<<" 调用了默认构造函数"<<"\n"; X=0; Y=0; } //------------------------------------------------------Tpoint:: Tpoint(in x, int y) { cout<<" 调用了非默认构造函数"<<"\n"; X=x; Y=y; } //--------------------------------------------------------TPoint:: ~TPoint() { cout<<" 调用了析构函数"<<"\n"; } //-------------------------------------------------
运行结果如下: 调用了默认构造函数 调用了非默认构造函数 p1:0*1 p2:800*600 调用了析构函数 调用了析构函数
程序中main函数中的“{}”是用来限定两个对象的生命周期的,以便在程序退出之前 调用析构函数。在上例中不管在构造函数还是在析构函数的定义前面都有“ TPoint::”前缀, 它告诉编译器这个函数是TPoint类的成员函数。事实上,如果在函数外面定义成员函数都 要加这样的前缀。其中“∷”是域操作符,代表一种所属关系。 从上例可看出析构函数是不能人为调用的,它由编译器根据某对象是否已超出它的作 用域来决定是否调用它的析构函数。上面的析构函数没有做任何有实际意义的动作,仅仅 说明它已经被调用了,这是因为编译器会自动释放该对象所占用的空间,如果在构造函数 中使用new或其他方法在堆中申请了内存,这时就要在析构函数中使用delete或相应的释放 方法去释放内存,否则系统是不会释放内存的,这样就会产生内存漏洞。 如果没有编写自己的析构函数,那么编译器将产生一个默认析构函数。对于本身就是 C++对象的数据成员来说,默认析构函数会调用这些对象的析构函数。 4. 其他的类成员函数 类除了包含构造函数和析构函数外,还可以包含其他成员函数。类不但要有建立和撤 消对象的功能,更主要的是还要具有操作和处理对象的功能以便完成特定的任务。这些功
8
Visual C++趣味程序导学
能不宜在构造函数和析构函数中实现,所以需要其他成员函数来补充。对上面的例子作如 下改动: //-------------------------------------------------#include #include //--------------------------------------------------class TPoint { public: int X; int Y; TPoint();//默认构造函数 TPoint(int x, int y); ~TPoint(); //析构函数 int ReadX() const {return X;} int ReadY() const {return Y;} int WriteX(int x) const {return X=x;} int WriteY(int y) const {return Y=y;} }; //------------------------------------------------------void main() { { TPoint p1; TPoint p2(800,600); cout<<"pl: "<
第1章
初识 Visual C++
9
cout<<" 调用了非默认构造函数"<<"\n"; X=x; Y=y; } //--------------------------------------------------------TPoint:: ~TPoint() { cout<<" 调用了析构函数"<<"\n"; } //---------------------------------------------------------
此时,这个类可以说是比较完善了。像其他成员函数一样,ReadX、ReadY、WriteX 和WriteY可以直接访问类中所有的成员。这里在两个读函数定义之后加了关键字const,意 味着在这些函数内不允许对数据成员进行赋值操作,如果在这些函数中有下面类型的语句 如“X=800”,编译器就会报错。 5.类成员的三个区 上面的类虽然比较完善了,但是并不实用。因为这个时候还可以随意访问对象的数据 成员,这是不符合数据封装准则的。可以对类定义作如下的修改: //--------------------------------------------------class TPoint { private: int X; int Y; public: TPoint();//默认构造函数 TPoint(int x, int y); ~Tpoint(); //析构函数 int ReadX() const {return X;} int ReadY() const {return Y;} int WriteX(int x) const {return X=x;} int WriteY(int y) const {return Y=y;} }; //-------------------------------------------------------
修改之后,除非调用读写成员函数,否则在类之外将无法直接访问数据成员X、Y。这 是因为类的成员分三个区,分别用private、public 、protected来说明。凡是定义在public 内的 成员可以被类外程序直接访问;凡是定义在private区的类成员,只能被本类的成员函数直 接访问,类外的程序则必须通过类区的成员函数间接访问。例如在上例中,对于由TPoint 定义的对象,不能通过对象名直接对X、Y进行访问,必须通过4个读写函数间接访问。
10
Visual C++趣味程序导学
具体编程时,必须结合使用public 与private两个区,把需要保护的类成员放到private区, 使之封装起来不易受到外界的干扰。但是建立类是必须使之与外界发生关系的,否则就毫 无意义,为此,需要public 成员以便与外界通信。这清楚地说明了C++的封装性质。在复杂 的类中,封装显得尤为重要,因为类越复杂,对类内部的访问控制就越严格。 1.3.2. 继承和多态 1. 继承的引出 自然界中很多客观事物具有很多共性,比如人与猿之间、火车与汽车之间、大炮与机 枪之间明显地有很多共性,但是他们之间又有很多的不同。C++解决 “类似但有不同”问 题的方法是——允许类从一个或多个其他类(在这里称为基类)继承其特性和行为,参看 下面的例子: //------------------------------------------------------------class PrintedDocument { //成员列表 }; //Book类从PrintedDocument类派生出来 class Book: public PrintedDocument { //成员列表 }; //---------------------------------------------------------------
我们称从其他类继承而来的类为派生类,而一个派生类本身也可以被其他类继承,必 须以适当的方式来对继承得到的成员进行访问。为了保证基类数据封装的安全,无论何种 继承方式得到的派生类都不能直接访问基类的private区成员。派生类的语法格式为: class类名:<访问权限>基类名列表{类定义体};
基类名列表中各基类用逗号隔开。其中访问权限可以是public 或private,它们表示两种 不同的继承方式:以public 方式继承得到的成员属性与其基类中的属性相同;以private方式 继承得到的成员属性将全部成为private属性。前面提到的protected区成员在使用上与private 区成员完全一样,惟一不同只是在派生时,protected 区成员可以被派生类直接访问,即对 派生类来说是可见的。例如: //---------------------------------------------class Document { public: char *Name; void PrintNameOf(); };
第1章
初识 Visual C++
11
//----------------------------------------------void Document::PrintNameOf() { cout<
2. 类派生引出的成员覆盖问题 在继承中,派生类包含所有的基类成员,同时加入自己的新成员。因此派生类可以根 据派生时的成员访问机制访问基类的任何成员(除非这些成员在派生类中重新进行了定 义)。当基类的成员在派生类里面被重新定义时,可以用域操作符“∷”来强制调用基类 成员。在上面的例子中,如果在Book中重新定义了PrintNameOf函数,而又要调用基类的 PrintNameOf函数,只能通过域操作符“∷”强制调用,如下所示: //----------------------------------------------------------class Book: public Document { public: Book(char *name, long pagecount); private: long PageCount; }; //----------------------------------------------------------void Book:: PrintNameOf() { cout<<"Name of book: "; Document:: PrintNameOf();
12
Visual C++趣味程序导学 } //-------------------------------------------------------------
1.4
VC++集成开发环境简介
Visual C++集成开发环境Developer Studio提供了大量支持可视化编程特性的实用工 具,它们包括:工程工作区、Class Wizard、AppWizard、Wizard Bar等。下面我们分别介 绍它们。 1.4.1
AppWizard工具
AppWizard应用程序向导是Visual C++提供的一个高级编程工具,它可以产生应用程序 的C++源代码框架。通过与另一个工具Class Wizard一起配合使用,可大大节省开发应用程 序的时间和精力。 AppWizard是一个标准的C++源代码生成器。它通过一系列的对话框来提示用户输入所 需创建的程序的信息,如它的名字和位置。用户还可以指定它是否具有一些特性,如是否 是多文档界面或是否带工具栏,是否支持数据库、OLE等。设置完成后AppWizard生成一些 构成应用程序框架的文件。由AppWizard生成的应用程序是一个基本的Windows应用程序, 用户可以对它进行编译并运行。但是,这个程序并不能完成任何工作,它只是为你继续增 加代码提供了一些框架性的代码,目的是节省用户设计应用程序框架的时间和精力。 1.4.2
工程和工程工作区
AppWizard生成的应用程序框架是通过工程工作区来管理的。工程工作区是一个包含 用户的所有相关工程和配置的实体。 工程则是与应用程序相关的一组文件及其配置,用以生成最终的程序或二进制文件。 一个应用程序对应一个工程。一个工程工作区可以包含多个工程,这些工程可以是同一类 型的工程,也可以是不同类型的工程。 工程工作区是Developer Studio 的一个最重要的组成部分,程序员的大部分工作都在 Developer Studio中完成。Developer Studio使用工程工作区来组织工程、元素以及工程信息 在屏幕上出现的方式。在一个工程工作区中,可以处理: ・ ・ ・ ・
一个工程和它所包含的文件 一个工程的子工程 多个相互独立的工程 多个相互依赖的工程
一个工程工作区可包含由不同的开发工具包,如Visual C++和Visual J++生成的工程。 在桌面上,工程工作区以窗口方式组织工程、文件和工程设置。工程工作区窗口一般位于 屏幕左侧,如图1.1所示。工程工作区窗口底部有一组标签,用于查看工程工作区中的各个
第1章
初识 Visual C++
图 1.1
工程工作区窗口
13
工程。
工程工作区信息以.dsw(以前为.mdp)为后缀的文件保存,工程文件以.dsp (以前为.mak) 为后缀的文件保存。工程和工程工作空间是通过工程工作区来管理的。要打开一个工程, 只需打开对应的工程工作区文件( *.dsw )即可。 工程工作区窗口由视图组成。每个视图都有一个相应的文件夹,包含了关于该工程的 各种元素。展开文件夹可以显示所选视图的详细信息。一个Windows应用程序的工程工作 区一般包含图1.1所示的3种视图: ・ FileView(文件视图):显示所创建的工程。展开文件夹可以查看工程中所包含的文 件。 ・ ClassView(类视图):显示工程中定义的C++类,展开文件夹可以显示工程中定义的 所有类,展开类可查看类的数据成员和成员函数以及全局变量、函数和类定义。 ・ ResourceView(资源视图):显示工程中所包含的资源文件。展开文件夹可显示所有 类型的资源。 1.4.3
Class Wizard工具
Class Wizard是一个交互式工具,用来建立新的类,把消息映射成类成员函数,或者把 控制框映射为类变量成员。在程序开发过程中,可用ClassWizard建立程序所需要的类,包 括消息处理和消息映射函数。Class Wizard可以完成下列事情: ・ ・ ・ ・
支持从许多应用程序框架基类中派生新类; 为类添加消息映射函数; 查看和编辑消息处理函数; 创建新类时,自动加入方法和属性等。
14
Visual C++趣味程序导学
1.4.4 Wizard Bar工具栏 Wizard Bar是一个可停泊的工具栏,可与Class Wizard配合使用。它主要用于快速访问 一些Developer Studio最实用的功能,比如Class Wizard或Class View的一些功能。 Wizard Bar工具栏包含了3个相关的下拉列表框,从左到右依次为:类(Class)、成员 (Member)、过滤器(Filter),如图1.2所示。类列表框包含了应用程序定义的所有类。 当前所选择的类决定可用的过滤器;所选的过滤器决定Member 列表中显示的内容。选择 Member中的一项,可以跳到相应的成员定义。Wizard Bar最右边是一个Action Control图标, 单击Action Control 图标的向下箭头符号会弹出一个菜单,用于执行跳到函数定义、增加消 息处理函数等操作。
图 1.2
1.5 1.5.1
Wizard Bar 工具栏
创建第一个工程
生成一个基于文本框的工程
(1)启动Visual C++。 使用File|New命令显示如图1.3所示的New对话框。在Projects 标 签 中 我 们 选 择 MFC AppWizard(exe) , 对 于 初 学 者 来 说 , 一 般 都 是 使 用 MFC AppWizard(exe)来编写一个可执行的Windows应用程序。
图 1.3
New 对话框
在Project name:文本框中填写工程的名称,在Location:文本框中填入工程文件保存 路径。Platforms:文本框中应选择Win32,表示我们编写的是Win32应用程序。然后单击OK 按扭出现如图1.4所示的对话框:
第1章
图 1.4
初识 Visual C++
15
MFC AppWizard-Step1 对话框
(2)MFC AppWizard将要求你选择工程文件的类型和语言支持,工程类型分为单文档 程序(Single document), 多 文 档 程 序(Multiple documents)和基于对话框的程序(Dialogbased)。单文档程序和多文档程序都是基于文本框的程序。基于对话框的程序的主界面是对 话框。下面的选择框将提示你选择要加入的多语言支持的动态链接库,在中文Windows环 境下默认为简体中文支持。 (3)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.5的对话框。
图 1.5 MFC AppWizard-Step2 对话框
从这一步开始MFC AppWizard将提示你选择功能支持,如果你对程序没有特殊要求, 可以单击Finish按钮,跳到最后一步生成工程文件。 (4)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.6所示的对话框: 这一步将提示您对复合文档以及Automation和ActiveX Controls的支持。 (5)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.7所示的对话框。 这一步将提示你选择菜单和工具栏的某些属性以及所支持的文件数。 (6)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.8所示的对话框。
16
Visual C++趣味程序导学
图 1.6
MFC AppWizard-Step3 对话框
图 1.7 MFC AppWizard-Step4 对话框
图 1. 8 MFC AppWizard-Step5 对话框
第1章
初识 Visual C++
17
这一步是对MFC的属性进行设置,初学者利用默认选项即可。 (7)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.9所示的对话框。
图 1.9 MFC AppWizard-Step6 对话框
在图1.9中,第一个文本框中会列出将要生成的工程中要包含的类,在下面的几个文本 框中将显示相应类的一些信息。单击Finish按钮将出现如图1.10所示的对话框。
图 1. 10 New Project Information 对话框
单击OK按钮将生成工程文件,如图1.11就是一个空的多文档工程文件。
18
Visual C++趣味程序导学
图 1.11
新生成的工程文件
左下方的三个工作区,分别是ClassView,ResouceView,Fileview。ClassView模式可 以显示工作区内的所有类以及类成员函数,方便你查找及添加类代码。ResouceView模式可 以显示工程中的所有资源文件,包括对话框,文本框,位图,工具栏,按钮等,方便程序 员编辑。FileView模式将显示工作区内的所有文件。 1.5.2
生成一个基于对话框的工程文件
(1)我们现在要新建一个工程(见图1.3),参数的选择参见1.5.1节。 按要求选择完 毕后,单击OK按钮出现如图1.12的对话框。
图 1.12 MFC AppWizard-Step1 对话框
(2)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.13的对话框。
第1章
初识 Visual C++
19
图 1.13 MFC AppWizard-Step2 对话框
基于对话框的程序将直接对对话框的一些特性支持进行初始化,包括是否显示3D对话 框,是否加入About box,对Automation和ActiveX Controls的支持以及是否加入Windows Socket的支持,最后给主对话框命名,默认是工程名。 (3)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.14的对话框,在 其中设置MFC的一些属性:
图 1.14
MFC AppWizard-Step3 对话框
(4)对上一步的对话框按要求选择完毕后,出现如图1.15的对话框。
20
Visual C++趣味程序导学
图 1.15 MFC AppWizard-Step4 对话框
(5)对上一步的对话框按要求选择完毕后,单击OK按钮出现如图1.16的对话框。
图 1.16
New Project Information 对话框
单击OK按钮系统将自动生成代码。
1.6
运行工程文件
下面我们将讲述如何使用工程。 1.6.1
基于文本框的程序
编译运行1.5.1节所生成的代码,结果如图1.17所示:
第1章
初识 Visual C++
图 1.17
21
运行结果
外框是程序主界面, project1是其中的一个文档界面,如果是基于单文档的将只会有 一个文档界面。基于文档的程序是靠菜单来完成对程序的操作的,所以编程的入口自然也 就是在对菜单的定义和操作。具体定义菜单的方法如下: 打开ResouceView工作区,单击Menu|IDR_PROJECTYPE将会在主工作窗口出现如图 1.18所示界面:
图 1.18
菜单栏
如图所示,每一个菜单项均可进行自定义。要定义或修改菜单项,只需双击虚线框或 者原有菜单项即会出现如图1.19对话框:
图 1.19
M enu Item Properties 对话框
22
Visual C++趣味程序导学
在ID中将定义菜单的ID,ID是一个菜单和程序内置函数的惟一接口,当使用者对菜单 进行操作时,程序将通过菜单的ID将消息映射到相应的函数以完成相应操作。 在Caption中将定义菜单项的名称,用于人机交互,下面列出了一些菜单的属性,可根 据程序员的不同需要加以定义,例如选中Separator将加入菜单分隔线,选中Pop-up将定义 可弹出菜单项,选中Checked将定义一个可选菜单项,选中Grayed将定义一个暂时不可用的 菜单项等。 定义了菜单项后,我们要做的工作就是给这个菜单项的相应操作赋予相应的处理,选 择View|ClassWizard或者直接按Ctrl+w键打开MFC ClassWizard,如图1.20所示:
图 1.20
Message Maps 标签
在上述对话框中可选择Object IDs及Messages并执相应的操作。 1.6.2
基于对话框的程序
基于对话框的程序与基于文本框的程序的工作原理基本相同,都是通过对程序外部接 口组件进行操作,然后将消息映射给相应的函数并进行相应的处理。不同的是,基于文本 框的程序的操作对象是比较简单的菜单,基于对话框的程序的操作对象是相对复杂的各种 控件(当然一般的基于文本框的程序也可以调用对话框)。首先编译并运行1.5.2j节所生成的 工程代码,结果如图1.21所示: 程序初设了一个静态文本:“TODO:在这里设置对话控件”,及“确定”和“取消” 两个按钮,并在在单击两个按钮时,定义了OnCancle函数,用于关闭程序。 打开ResouceView工作区,单击Dialog|IDD_PROJECT2_DIALOG将会在主工作窗口出 现如图1.22界面:
第1章
初识 Visual C++
图 1.21
Project2 运行结果
图 1.22
23
设置对话框控件
程序员可以用鼠标点击工具栏上的相应控件,然后在主对话框上绘制相应控件,用MFC ClassWizard来定义控件的唯一ID并为控件操作定义各种不同的函数来实现相应的功能,方 法和对菜单项的操作类似,这里就不重复。 基于对话框的程序最基本的就是对各种控件的应用,本书后面的示例中还将对相应的 控件进行介绍。
1.7
Microsoft基本类库与应用程序框架
本节将介绍Microsoft基本类库(简称MFC)应用程序框架,从中可以了解什么是MFC 及 应 用 程 序 框 架 (Application Framework) 的 原 理 。 因 为 本 书 中 的 例 子 都 是 基 于 MFC Framework的。 1.7.1
什么是Application Framework 通俗的说,Application Framework是一个完整的程序模型,它具备了标准应用软件所
24
Visual C++趣味程序导学
需的一切基本功能(像文件存取、打印预览等),以及这些功能的使用接口。以术语来讲, Application Framework就是一组类构造起来的大模型。它的出现具有革命性的意义。因为从 此以后,开发人员再也不需要把精力浪费在程序外观的构建上了,这项工作的初期代码可 以由Application Framework自动完成。 1.7.2
为什么要用Application Framework
使用Application Framework的最直接的原因是,Windows API函数非常复杂,很难直 接用它来编写程序。而用MFC,一个类就可以帮我们解决以前一大堆API才能解决的事情, 何乐而不为? 当然,Application Framework的引入决不仅仅是为了减少我们花在Windows API上的 大量时间。它带来的是一种全新的面向对象程序设计的思想和方法。 1.7.3
Microsoft Foundation Class(MFC)与VC++
目前,主要有三套Application Framework,它们分别是:Microsoft的MFC(Microsoft Foundation Class),Borland的OWL(Object Window Library),以及IBM 的OCL(Open Class Library)。至于其他的C++编译器厂商(如Watcom,Symantec)只是提供集成开发环境(IDE), 其Application Framework用的都是MFC。 Windows操作系统是Microsoft的产品,而MFC也是Microsoft的产品,这样我们就应该 知道,没有人会在Windows接口方面比Microsoft做得更好了,那我们还有什么理由不选用 MFC呢? 我们经常听人说,学习VC++编程。其实这种说法并不准确。VC++仅仅是Microsoft提 供的集成开发环境(IDE)。通过VC++这个工具,我们可以利用MFC或者其他Windows API 编写Windows程序。比较准确的说法是,我们正在学习的是MFC/C++程序设计。 1.7.4
纵观MFC
MFC非常巨大,各种类加起来足有几百个,令人望而生畏。不要害怕,总起来看MFC 只不过分为下列几大类: ・ General Purpose Class:提供字符串类、数据处理类(数组和链表)、异常情况处理类、 文件类等。 ・ Windows API Class:用来封装Windows API,如窗口类、对话框类等。 ・ Application Framework Class:这些类是组成应用程序的骨干,包括文档/视图、消 息驱动、消息映射、消息传递、动态创建、文件读写等。 ・ High Level Abstractions:包括工具栏、状态栏、拆分窗口、滚动窗口等。 ・ Operation System Extensions:包括OLE,ODBC,DAO,MAPI,WinSock,ISAPI 等。 1.7.5
怎样才能学好MFC Application Framework替我们解决了使用C语言进行程序设计时的许多细节问题,因
第1章
初识 Visual C++
25
此我们能够在很短的时间内就可以掌握MFC的程序设计方法。 1.C++是基础 熟悉C++程序设计是学习MFC的基础,特别是对于其中的继承、重载、多态,我们更 是应该知其所以然的。要知道MFC本身就是一个巨大的类库,连类的继承都不知道的人, 不要来学MFC程序设计。基础不打牢,就如浮沙上筑高台,永远不会很高。 2.熟记 MFC 的层次结构 要学习MFC程序设计,第一步就是熟记MFC的层次结构(见图1.22) CObject CCmdTarget CWnd
CWinThread CWinApp
CFrameWnd CMyFrameWnd
CMyWinApp
CView CMyView
CDocument CMyDoc CMyDoc CColorDialog CFileDialog CFirelReplaceDialog CFrontDialog CPintDialog 图 1.22
MFC 的层次结构
3.理解 MFC 的宏机制和消息循环机制 从MFC的层次图上,我们可以看到,常用的类都是继承自Cobject,因此它们也就具
26
Visual C++趣味程序导学
备了CObject的一些特性。如运行时类型识别(RTTI)、序列化(Serialization)、动态对象创建 (Dynamic object creation)等。不过MFC并不是通过虚函数来实现这些功能的。它是巧妙的利 用了一系列宏,如BEGIN_MESSAGE_MAP()、DECLARE_DYNCREATE()等,来将特定的 消息映射到派生类中相应的成员函数上。 1.7.6
用Application Wizard生成的程序的结构
1.5节中,利用Application Wizard,没有写一行代码,仅仅点几下鼠标,我们就得到 了一个可以运行的Windows程序(当然它只是个空架子,什么也干不了)。现在,让我们来看 看这个程序的结构。它由几个头文件(.H)、几个与头文件对应的程序文件(.CPP)和一些资源 文件(.RC)构成。一般来说,MFC遵循这样的规则:类的声明放在头文件中,对象的实现放 在相对应的程序文件中,各种资源(如各种对话框、位图、图标、字体等)放在资源文件中, 在编译的时候由编译器把它们组装起来。 1.MFC 程序的来龙去脉 以前我们用传统的C/SDK编写Windows程序,最大的好处就是可以看清楚整个程序的 来龙去脉和消息动向。然而在用MFC编写的应用程序中,这些东西都变得隐晦不明了。我 们都知道,C/SDK编写的Windows程序是从WinMain()函数开始执行的。可是我们查遍用 Application Wizard生成的整个程序,根本找不到WinMain()函数的影子,程序的入口到底在 哪里呢?下面让我来告诉你。 2.CwinApp——程序的诞生 Windows程序总要有WinMain()函数,在MFC的程序中看不到它,是因为它被隐藏在 Application Framework中了。 MFC会 根 据 你 的 程 序 名 ( 假 设 我 们 的 程 序 名 是 MyApp) 建 立 一 个 程 序 的 主 类 CMyWinApp,这个类继承自CWinApp。在程序中,你需要建立一个在整个程序中惟一的 CMyWinApp对象theApp,我们称之为程序对象。因为我们没有编写CMyWinApp的构造函 数,由于虚函数的作用,MFC是调用CWinApp的构造函数来产生对象theApp。程序对象 theApp产生后,也就相应分配了内存,程序也就诞生了。 3.CMyApp::InitInstance——主框架的构建 这是CMyWinApp定义的一个虚函数,我们需要自己改写这个函数。一般情况下,这个 函数应该这样来改写: CMyApp::InitInstance() { m_pMainWnd->new CMyFrameWnd(); m_pMainWnd->ShowWindow(m_nCmdShow); m_pMainWnd->UpdateWindow(); Return Ture; }
第1章
初识 Visual C++
27
在这个函数里我们首先新建了一个CMyFrameWnd对象。CMyFrameWnd的构造函数会 产 生 主 窗 口 。 接 着 , 我 们 调 用 ShowWindows() 函 数 来 显 示 这 个 窗 口 , 然 后 调 用 UpdateWindows()发出WM_PAINT消息。 4.CMyApp::Run——程序的发动机 CMyApp::Run承担着整个程序的消息驱动任务,它利用Message Map机制,借助于一 些宏(如DECLARE_MESSAGE_MAP)等,将消息循环建立起来,把不同的消息映射到不同 的 消 息 处 理 函 数 。 例 如 : WM_PAINT 消 息 被 映 射 到 OnPaint 函 数 , WM_COMMAND(IDM_ABOUT)消息被映射到OnAbout函数。消息循环建立起来后,程序 就这样不断的等待着外界消息的输入,映射消息,处理消息直到程序的结束。 5.CMyApp::ExitInstance——程序的死亡 当使用者单击了File菜单中的Close命令时。发出了WM_CLOSE消息,这个消息进而 触发WM_DESTROY消息,然后触发WM_QUIT消息,遵循Message Map的原理,WM_QUIT 由CMyApp::ExitInstance来处理,如果我们没有改写这个函数,根据虚函数的概念,将由 CWinApp::ExitInstance来处理这个消息,程序结束。以上的这些消息驱动,都已经由MFC Framework封装了起来,我们初学者知道其然就行了,不需要知道其所以然。
1.8 什么是Visual C++
本章知识点回顾
Visual C++ 是 指 由 Microsoft 公 司 开 发 的 可 视 化 集 成 编 程 软 件 Microsoft VisualStudio成员之一,目前的最高版本是6.0。
C++的新特性
(1)支持用“//”注释行 (2)局部变量的声明可以放在程序中的任何位置,只要是变量的首次声明即可 (3)在函数作用域内也可以访问同名的作用域外的变量,但是要加上作用域操 作符∷ (4)定义函数时可以定义一些参数的默认值来简化编程 (5)可用&操作符声明引用,声明为引用的变量是另一变量的别名 (6)引用类型也可以运用到函数当中 (7)可以用const定义常量类型 (8)new 和 delete操作符 (9)面向对象机制
面向对象简介
类和对象 构造函数 析构函数 继承和多态
28
Visual C++趣味程序导学 续表
Visual C++集成开发
(1)AppWizard: Visual C++提供的一个高级编程工具,它可以产生应用程序
环境Developer
的C++源代码框架
Studio
(2)Class Wizard:一个交互式工具,用来建立新的类,把消息映射成类成员函 数,或者把控制框映射为类变量成员 (3)Wizard Bar:一个可停泊的工具栏,可与Class Wizard配合使用。它主要用 于快速访问一些Developer Studio最实用的功能,比如Class Wizard或Class View的 一些功能
用
VC++
编
写
(1)生成一个基于文本框的工程(project):
Windows 程 序 的 基
・ 定义菜单
本框架――project
・ 定义消息 ・ 处理函数 (2)生成一个基于对话框的工程(project): ・ 添加控件 ・ 定义消息 ・ 处理函数
第2章
“幸运 52”游戏
——Visual C++ 初步应用 在第1章中我们简单介绍了Visual C++开发环境,并且介绍了如何生成一个工程及如何 开始编写Windows程序。本章我们将通过“幸运52”游戏程序来详细介绍如何编写Widows 程序以及一些控件的基本应用。
2.1
“幸运52”游戏简介
“幸运52”是一个流行的电视综艺节目。“幸运52”游戏的核心规则是在给出商品之 后,要求用户迅速猜中该商品的价格,在估价的过程中,系统会提示所估价格相对于实际 价格是高了还是低了。 游戏的初始界面如图2.1所示。
图 2.1
游戏的初始界面
当用户选择“开始”按钮时,应用程序将随机显示一商品的图像和名称,在输入框中 输入对该商品的估价,然后单击“确定”按钮,这时应用程序会给出反馈,弹出一个警示 对话框,如果用户输入的估价比商品的实际价格低,则提示你的估价“低了低了!”,如
2
趣味程序导学 Visual C++
图2.2所示,如果用户输入的估价比商品的实际价格要高,会弹出一个警示对话框,提示你 的估价“高了高了!”,如图2.3所示。在关闭对话框之后,用户可以再次输入对商品的估 价,直到猜中商品的实际价格为止,这时系统就会恭喜用户猜对了,如图2.4所示。
图2.2 估价偏低
图2.3
估价偏高
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
图2.4
3
猜对了
这个游戏具体的规则如下: 1. 单击“开始”按钮,游戏开始,系统将给出商品信息。 2. 请迅速在输入框中输入您估计的商品价格,然后单击“确定”按钮。 3. 这时系统会提示您估计的商品价格是高了还是低了,在弹出的对话框中单击“确定” 按钮,再次输入新的估计值,重复上一步。直到您的估计值正确,这时系统会恭喜 您中奖了! 下面,我们开始循序渐进地创建这个小游戏。
2.2
设计初始界面
本节首先用应用程序向导(AppWizard)生成一个基于对话框的简单框架。AppWizard 会自动生成基本框架的源代码,然后我们对生成的源代码进行修改,得到完整的“幸运52” 模拟游戏程序。在这里,我们主要介绍生成模态对话框程序的方法,以及有关对话框控件 的知识。 2.2.1
生成源代码基本框架
我们将建立一个基于对话框的应用程序,以使读者了解编写对话框程序的方法。几乎 每一个Windows程序都使用对话框与用户进行交互。对话框可以是一个简单的OK消息框, 也可以是一个复杂的数据输入表单。实际上,对话框是一个接受消息的窗口,它可以移动 和关闭,并且,它在客户区可以绘图。 对话框有两种:模态的和非模态的。本章将利用最常用的类型,即模态对话框。CDialog 基类支持模态和非模态对话框。使用模态对话框,比如Open File对话框,在对话框关闭之
趣味程序导学 Visual C++
4
前,用户不能在同一个应用程序的其他地方工作。使用非模态对话框,当对话框还在屏幕 上时,用户仍然可以在应用程序的另一个窗口中工作。Microsoft Word的查找和替换对话框 就是非模态的对话框。当这些对话框打开的同时,我们还可以编辑文档。 下面我们首先生成程序源代码的基本框架:按照上一章我们介绍的生成基于对话框的 工程的方法生成一个名字为xingyun的工程,其中所有设置均使用默认项。 2.2.2
添加控件并设置其属性
如图2.5所示就是Visual C++的一个对话框编辑窗口。开始,对话框里只有“确定”按 钮和“取消”按钮。对话框编辑器激活时,通常显示控件和对话工具栏(如果两者都未出 现,可通过单击鼠标右键并从快捷菜单中选择相应命令使其显示)。控件工具栏包含可以 加入对话框的每类控件的按钮。
图 2.5
对话框编辑窗口
1. 加入控件 设计界面的第一步也是重要的一步是往窗体上加入控件。 有以下几种方法来加入控件: (1)在控件选项板上,单击想要加入的控件,然后在窗体上单击鼠标。这时该控件就 被放置在鼠标单击的位置处。 (2)在控件选项板上,双击想要加入的控件,这时该控件就被放置在窗体的正中间。 如果用此方法连续加入了几个控件,后面加入的控件把原先的遮住了,这时要把上面的控 件拖到其他位置。 (3)如果希望连续加入几个某一类型的控件,可以在单击此控件时,按住Shift健,这 时此控件凹陷下去,并且周围有蓝色的边框。然后再在窗体所希望放置控件的位置处单击 鼠标,每单击一次鼠标,加入一个控件。最后,单击控件选项板上最左边的箭头,这时控 件恢复正常状态。否则,每在窗体上单击一次,就会增加一个控件。 现在就来设计界面。共需要加入static 文本控件, edit控件和picture控件,如图2.6所示。
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
图 2.6
5
加入控件后的初始界面
为了给用户提供一个美观的界面,常需要在窗体上重新设置控件的尺寸、位置。利用 一些快捷方式可以非常容易地剪切、复制、粘贴和删除控件。 2. 重设控件大小 重新设置控件的大小有几个方法,看自己喜欢哪种方法: (1)利用鼠标拖动来改变控件的大小。方法是选择希望重设大小的控件,然后根据需 要将鼠标放置在控件的8个控制点中的一个上,等到鼠标变成双箭头时,就可以拖动鼠标来 改变控件的大小了。 (2)利用对话框。方法是先选择该控件,然后选择菜单“Edit”,再选择“Size”菜 单项,或在该控件上单击鼠标右键,在弹出菜单中选择“Size”菜单项,这时出现Size对话 框。在此对话框中根据需要选择相应的项或输入控件的宽度和高度。 (3)利用Shift键和方向键的组合来改变控件的大小。方法是选择希望重新设置大小的 控件,按住Shift键,如果想要拉长控件,则同时击向右的方向键;如果想要缩短控件,则 同时击向左的方向键;同时击向下的方向键可以使控件变高,向上的方向键则可以减小高 度。 (4)利用Scale来改变。方法是选择菜单“ Edit”然后选择“Scale ”菜单项,或在窗体 上单击右键后选择“Scale”菜单项,打开Scale对话框,在Scale factor中输入放大的比例。 要注意的是,这种方法改变了窗体中所有控件的大小,并且后面加入的控件的大小也按此 比例来显示。 3. 控件的移动和删除 移动控件是设计界面中经常要做的事情。 如果需要移动的距离比较大,可以用鼠标来拖动。如果只希望稍微移动控件,可以用 Ctrl键和方向键的组合来实现。即一直按住Ctrl键,然后击方向键,即可移动控件。 删除控件的方法更简单,选择该控件后,单击Delete键即可,或选择菜单“Edit”后, 再选择“Delete”菜单项。
趣味程序导学 Visual C++
6
4. 控件的剪切、复制和粘贴 控件的剪切、复制和粘贴完全和一般的文字、代码编辑一样,可以选择“Edit”菜单 中的“Cut”,“Copy”和“Paste”实现控件的剪切、复制和粘贴,也可以用Ctrl+X、Ctrl+C 和Ctrl+V热键来剪切、复制和粘贴控件。 5. 设置控件的属性 我们可以对各个控件的属性进行必要的修改。首先选中控件,单击右键,选择Properties, 弹出如图2.7所示的对话框。将该对话框中的Caption属性改为“幸运52”模拟小游戏。然后 再以此方法修改各个static 控件和Button1的Caption属性,其他的属性均使用默认设置。
图 2.7 Dialog Properties 对话框
(1)将图片正上方的static 控件的ID属性设为IDC_STATIC1,因为我们在游戏中希望 它的显示随着下面的图片变化。而其他的static 控件不用修改ID,因为我们后面并不需要对 它们进行操作。 (2)在修改edit1的属性时,选中其styles属性中的Number属性,这样,用户就只能输 入数字了,以避免发生用户意外输入的问题。 修改后的游戏界面如图2.8所示。
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用 图 2.8
2.2.3
7
修改后的游戏界面
生成管理对话框的类,定义成员变量
这一步要用ClassWizard生成管理刚刚设计的对话框的类。单击对话框编辑器窗口并选 择View菜单的ClassWizard命令或按Ctrl+W键。弹出MFC ClassWizard对话框。这时,可用 ClassWizard给CxingyunDlg类加入数据成员。ClassWizard可以定义对话框中包含的每个控 件的一个或几个对话类数据成员。运行程序和显示对话框时,MFC自动将每个数据成员的 值传送给相应的控件。 选择Member Variables标签,我们可以在Control Ids下面看到可以添加数据成员的控件。 首先选中IDC_BUTTON1,然后单击Add Variable…按钮,弹出Add Member Variable对话框, 在Member Variable Name的文本框中输入数据成员名m_BUTTON1(ClassWizard会插入名 称的m_部分)。其他保留默认选择,完成后单击OK按钮。 使用同样的方法,可以定义其他控件的数据成员。注意在定义IDC_EDIT1数据成员时, 将其Variable type属性改为int,这样在后面处理时将会带来很大的方便。其他使用取默认值。 2.2.3
定义消息处理函数
和任何窗口一样,对话框接收各种消息来告知重大事件,必须在对话类中加入消息处 理函数来处理这些事件。 为此,打开MFC ClassWizard对话框中的Message Maps 标签,选择Object Ids 中的 CxingyunDlg,然后在Messages框中选择WM_INITDIALOG并单击Add Function按钮定义这 个消息的处理函数,ClassWizard生成OnInitDialog函数。WM_INITDIALOG消息在对话框首 次生成显示之前发出。再选择IDC_BUTTON1,在Messages中选择BN_CLICKED并单击Add Function按钮定义这个消息的处理函数OnButton1。最后用同样的方法定义IDOK的消息处理 函数ONOK。 2.2.4
引入图片资源
本节我们要引入程序中各种商品的图片。打开Insert菜单中的Resource选项或按Ctrl+R 打开Insert Resource对话框,选择Bitmap项,并单击Import按钮引入图片,浏览并选中要引 入的图片,按回车键即可。这样在Resource项中的 Bitmap中即可含有系统自动命名的 IDB_BITMAP1到IDB_BITMAP10的图片资源。
2.3 编写程序代码 1. 首先打开XingyunDlg.h文件,在CxingyunDlg的类定义中加入三个私有变量,代码如 下: private: CString cmmdty[10]; //用于保存商品名称 int CurrentCommodityIndex;//用于表示当前商品的序号 int price[10]; //用于保存各种商品的价格
趣味程序导学 Visual C++
8
再定义一个公有变量: public: CBitmap Bitmap[10]://用于存放Bitmap图
在上面的代码中,我们用到了一个名为CString的数据类型。Visual C++将CString作为 一个类来实现。 CSting 类 CSting类提供了下列字符串处理特性: ・ ・ ・ ・
引用记数 字符串长度 数据 空字符串终止符
如果未提供一个初始值,CString变量初始化为NULL。 下面是使用CString的示例。代码如下所示: //-------------------------------------------------#include #include<stdio.h> //--------------------------------------------------class Famille { private: CString FNames[10]; CString GetNames[int Index]; void SetName(int, CString); public: _property CString Names[int Index]={read=GetName, write=SetName}; Famille(){} ~Famille(){} }; //--------------------------------------------------Cstring Famille::GetName(int i) { return FNames[i]; } //--------------------------------------------------void Famille::SetName(int i, const CString s) { FNames[i]=s; } //---------------------------------------------------
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
9
void main() { Famille C; C.Names[0]=”Steve”; //调用Famille::SetName() C.Names[1]=”Amy”; C.Names[2]=”Sarah”; C.Names[3]=”Andrew”; For (int i=0; i<=3; i++) { //调用Famille::GetName() puts(C.Names[i].c_str()); } } //---------------------------------------------------
因为CString是一个数据类型,就像int、float一样,在程序中使用非常频繁,是个非常 重要的类,因此读者应该熟练掌握它的使用方法。 2. 然后打开XingyunDlg.cpp文件,在 CxingyunDlg类的构造函数中对上面这些变量进行 初始化: CXingyunDlg::CXingyunDlg(CWnd*pParent /*=NULL*/) :CDialog(CXingyunDlg::IDD, pParent) { cmmdty[0]=”康佳29寸纯平彩电”; cmmdty[1]=”松下掌上电脑”; cmmdty[2]=”JNC MP3播放器891”; cmmdty[3]=”捷视可视电话机2000T”; cmmdty[4]=”索尼随身听EX2000”; cmmdty[5]=”索尼数码相机DSC-P1”; cmmdty[6]=”松下剃须刀ES365A”; cmmdty[7]=”日本ESP电吉它”; cmmdty[8]=”Nokiya 8210手机”; cmmdty[9]=”奔驰500”; CurrentCommodityIndex=-1; Price[0]=4390; Price[1]=5230; Price[2]=2079; Price[3]=5380; Price[4]=1224; Price[5]=7140;
趣味程序导学 Visual C++
10 Price[6]=273; Price[7]=5230; Price[8]=2810; Price[9]=120000;
Bitmap[0].LoadBitmap(IDB_BITMAP10) Bitmap[1].LoadBitmap(IDB_BITMAP9) Bitmap[2].LoadBitmap(IDB_BITMAP8) Bitmap[3].LoadBitmap(IDB_BITMAP7) Bitmap[4].LoadBitmap(IDB_BITMAP6) Bitmap[5].LoadBitmap(IDB_BITMAP5) Bitmap[6].LoadBitmap(IDB_BITMAP4) Bitmap[7].LoadBitmap(IDB_BITMAP3) Bitmap[8].LoadBitmap(IDB_BITMAP2) Bitmap[9].LoadBitmap(IDB_BITMAP1)
上面的代码是对话框的构造函数,一般在类的构造函数完成初始化成员变量。在此函 数中,我们初始化了用于保存商品名称的cmmdty数组以及商品对应的价格Price数组,最后 将CurrentCommodityIndex设置为-1。另外,我们将位图资源装入Bitmap数组中。我们这里 需要将CurrentCommodityIndex初始化成小于0而大于9的数,这样可以在程序中方便我们判 断用户是否选择了“开始”按钮来随机选择一商品。 注意:Bitmap和cmmdty,Price的数值对应关系。 3.“开始”按钮的消息处理函数 下面,我们要完成“开始”按钮的消息处理函数。打开XingyunDlg.cpp文件,找到 OnButton1的函数,加入以下的代码: void CXingyunDlg::OnButton1() { //T0D0: Add your control notification handler code here m_EDIT1=0; //清空Edit1 stand(time (NULL)); CurrentCommodityIndex=rand()%10;//产生一个0到9的随机数 m_STATICI.Format(“%s”, cmmdty[CurrentCommodityIndex]); UpdateDate(FALSE); if(CurrentCommodityIndex==0) ((CStatic*)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[0])); else if (CurrentCommodityIndex==1) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[1])); else if (CurrentCommodityIndex==2) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[2]));
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
11
else if (CurrentCommodityIndex==3) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[3])); else if (CurrentCommodityIndex==4) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[4])); else if (CurrentCommodityIndex==5) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[5])); else if (CurrentCommodityIndex==6) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[6])); else if (CurrentCommodityIndex==7) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[7])); else if (CurrentCommodityIndex==8) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[8])); else if (CurrentCommodityIndex==9) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[9])); }
这段代码首先产生一个随机数,然后根据这个随机数确定商品的种类,并且在程序界 面上显示出它的名称和图片。 4.“确定”按钮的消息处理函数 最后我们来完成“确定”按钮的消息处理函数。 单击“确定”按钮时,系统对用户输入的商品估价进行确认,并判断是否正确,然后 给出提示信息: 当用户的估价相对实际价格偏高时,弹出对话框,提示用户“您想用这么多的钱买这 个破东西?!高了高了!!”,并清除用户刚才的估价以方便用户再次输入。 当用户的估价相对实际价格偏低时,弹出对话框,提示用户“您想用这么少的钱买这 个好东西?!低了低了!!”,并清除用户刚才的估价以方便用户再次输入。 当用户的估价与实际价格相等时,弹出对话框,提示用户“恭喜恭喜!!”“猜对了”。 仍然打开XingyunDlg.cpp文件,找到OnOK函数,并加入以下的代码: void CXingyunDlg::OnOK() { //T0D0: Add extra validation here UpdateDate(); int priceTemp=m_EDIT1; if(prictTemp>price[CurrentCommodityIndex])00 { MessageBox(“您想用这么多的钱买这个破东西?高了高了!!”,“猜错了”,MB_OK); } else if(priceTemp<price[CurrentCommodityIndex]) { MessageBox(“您想用这么少的钱买这个好东西?低了低了!!”,“猜错了”,MB_OK);
趣味程序导学 Visual C++
12
} else { MessageBox(“恭喜恭喜!!”, ”猜对了”,MB_OK); } }
在这段代码中,将用户输入的价格和程序中存储的价格作比较,并且做出相应的判断, 发出相应的消息。
2.4 完善游戏界面 2.4.1
焦点控制:SetFocus方法
1. 效果 按钮被单击或者使用tab键选中,即为按钮获得焦点(focus);按钮获得焦点后,还会 再失去焦点。 在上述游戏的操作过程中有一些不便之处,主要是“焦点”问题,如下: (1)程序启动后,光标在编辑框中,而此时用户需要做的是选择“开始”按钮来开始 游戏。如果此时焦点在“开始”按钮上,用户只需按回车键,即可代替用鼠标单击“开始” 按钮来开始游戏,简洁方便。 (2)当选择“开始”按钮后,并不能立即开始输入对商品的估价,因为此时“焦点” 不在输入框,还必须将鼠标光标移到输入框中,或者使用Tab键将“焦点”移到输入框,然 后才能输入。 (3)开始游戏之后,单击“确定”按钮,弹出判断正误的对话框,单击对话框中的“确 定”按钮之后,“焦点”不在输入框,这时也不能直接再次输入估价,而先要将“焦点” 移到输入框。 下面我们将通过焦点控制来解决上述问题。 2. 实现方法 当我们在窗体上加入控件时,Visual C++按照控件加入顺序,自动将那些能用Tab键切 换的控件的TabOrder属性赋予一个整数。例如在我们的程序中,我们先在窗体中加入Edit1 控件,然后在窗体中分别加入Button1控件、Button2控件和Button3控件(其他的Label控件 不能用Tab键来切换选择,所以没有TabOrder属性),此时Visual C++将Edit1控件的TabOrder 属性设置成0,将Button1、Button2、Button3控件的TabOrder属性分别设置成1、2和3。因此
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
13
在程序启动的时候,焦点在Edit1控件中,要将焦点切换到Button1控件当中,只需要将Button1 的TabOrder设置成0即可。在这里,我们可以充分利用上面讲到的设置移动顺序的办法。 好了,第一个不方便用户的地方到此就圆满地解决了,下面来解决其他两个问题。 获得焦点可以使用相应的SetFocus方法来实现。要使得输入框Edit1获得焦点,使用如 下代码即可: GetDlgItem(IDC_Edit1)->SetFocus();
将“开始”按钮控件的消息处理函数修改成如下所示的代码: void CXingyunDlg::OnButton1() { //T0D0: Add your control notification handler code here m_EDIT1=0; //清空Edit1 stand(time (NULL)); CurrentCommodityIndex=rand()%10;//产生一个0到9的随机数 m_STATICI.Format(“%s”, cmmdty[CurrentCommodityIndex]); UpdateDate(FALSE); If(CurrentCommodityIndex==0) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[0])); else if (CurrentCommodityIndex==1) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[1])); else if (CurrentCommodityIndex==2) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[2])); else if (CurrentCommodityIndex==3) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[3])); else if (CurrentCommodityIndex==4) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[4])); else if (CurrentCommodityIndex==5) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[5])); else if (CurrentCommodityIndex==6) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP( Bitmap[6])); else if (CurrentCommodityIndex==7) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[7])); else if (CurrentCommodityIndex==8) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[8])); else if (CurrentCommodityIndex==9) ((CStatic *)GetDlgItem(IDC_PICTURE))->SetBitmap(HBITMAP(Bitmap[9])); GetDlgItem(IDC_EDIT1)->SetFocus(); }
趣味程序导学 Visual C++
14
将“确定”按钮的事件处理函数代码修改成如下所示: void CXingyunDlg::OnOK() { //T0D0: Add extra validation here UpdateDate(); int priceTemp=m_EDIT1; if(priceTemp>price[CurrentCommodityIndex]) { MessageBox(“您想用这么多的钱买这个破东西?高了高了!!”,“猜错了”,MB_OK); GetDlgItem(IDC_EDIT1)->SetfOCUS(); } else if(priceTemp<price[CurrentCommodityIndex]) { MessageBox(“您想用这么少的钱买这个好东西?低了低了!!”,“猜错了”,MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); } else { MessageBox(“恭喜恭喜!!”, ”猜对了”,MB_OK); GetDlgItem(IDC_BUTTON1)->SetFocus(); } }
上述代码中,我们判断用户输入的价格是否和商品本身的价格相等,如果不相等,则 将焦点切换到Edit1中,以便用户重新输入新的估计价格。如果相等,则将焦点切换到 Button1,以便用户重新开始游戏。 从上面的程序代码中,我们可以看出作为程序员应该处处为用户做想,一切以方便用 户为原则。 2.4.2
对用户的意外操作进行响应
1. 对用户没有选择商品时的响应 如果用户没有选择商品而单击了“确定”按钮,这时的运行结果报告说用户猜错了, 显然这是个错误的结果。因此在程序中,我们应该先判断用户是否选择了商品,如果没有 选择,则要求用户选择一商品。这时代码如下所示: void CXingyunDlg::OnOK() { //T0D0: Add extra validation here UpdateDate(); //保证用户先选择一商品 if(CurrentCommodityIndex==-1)
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
15
{ MessageBox("您必须先选择一商品!","错误",MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); return; } Int priceTemp=m_EDIT1; if(priceTemp>price[CurrentCommodityIndex]) { MessageBox("您想用这么多的钱买这个破东西?高了高了!!","猜错了",MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); } else if(priceTemp<price[CurrentCommodityIndex]) { MessageBox("您想用这么少的钱买这个好东西?低了低了!!","猜错了",MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); } else { MessageBox("恭喜恭喜!!", "猜对了",MB_OK); } GetDlgItem(IDC_BUTTON1)->SetFocus(); } }
2. 对用户的意外输入进行响应 用户在输入框输入对商品价格的估计值,这个值显然应该是个正整数,大多数用户也 会这么输入,但是这种情况也是可能出现的:用户意外的输入了非正整数,如输入了 “12345a6”或者干脆是个空值。 在此我们在Edit1的Styles属性里,选中Number属性,这样它就只响应用户的数字输入 了,而对于其他的输入,它都不响应。 另外如果用户没有任何输入,我们应该给出响应。 当用户没有任何输入时,将弹出警示对话框提示“您必须为商品输入价格!”, 如图 2.9所示,代码如下: if(priceTemp==0) { MessageBox("您必须为商品输入价格!", "错误", MB_OK); return; }
趣味程序导学 Visual C++
16
图2.9
输入错误时的界面
这时“确定”按钮的事件处理函数为: void CXingyunDlg::OnOK() { //T0D0: Add extra validation here UpdateData(); //保证用户选选择一商品 if(CurrentCommodityIndex==-1) { MessageBox("您必须先选择一商品!","错误", MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); return; } int priceTemp=m_EDIT1; if(priceTemp==0) { MessageBox("您必须为商品输入价格!", "错误", MB_OK); return; } else if(prictTemp>price[CurrentCommodityIndex]) { MessageBox("您想用这么多的钱买这个破东西?高了高了!!","猜错了",MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); }
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
17
else if(priceTemp<price[CurrentCommodityIndex]) { MessageBox("您想用这么少的钱买这个好东西?低了低了!!","猜错了",MB_OK); GetDlgItem(IDC_EDIT1)->SetFocus(); } else { MessageBox("恭喜恭喜!!", "猜对了",MB_OK); GetDlgItme(IDC_EDIT1)->SetFocus(); } }
上面通过一个简单的例子,讲述了生成基于对话框程序的步骤,介绍了Micosoft基础类 和应用程序向导应用程序生成工具。其中着重介绍了对话框界面的设计,一些基本控件的 使用,消息的定义以及消息映射的处理等内容,这些都是编写Windows程序的基础。下面 再给出一个应用实例:游戏“速算24”。
2.5 本章知识点回顾
18
在窗体上加入 控件的方法
重新设置控件 大小的方法
移动控件
删除控件的方 法 定义消息处理 函数 CSting类
趣味程序导学 Visual C++
(1)在控件选项板上,单击想要加入的控件,然后在窗体上单击鼠标。 (2)在控件选项板上,双击想要加入的控件,这时该控件就被放置在窗体 的正中间。 (3)如果希望连续加入几个某一类型的控件,可以在单击此控件时,按住 Shift键,然后再在窗体所希望的位置处单击鼠标,每单击一次鼠标, 加入一个控件。最后,单击控件选项板上最左边的箭头,这时控件恢 复正常状态。 (1)利用鼠标拖动来改变控件的大小。 (2)利用Size对话框。在Size对话框中根据需要选择相应的项或输入控件 的宽度和高度。 (3)利用Shift键和方向键的组合来改变控件的大小。 (4)利用Scale来改变控件的大小。 如果需要移动的距离比较大,可以用鼠标来拖动。如果只希望稍微移动控 件,可以用Ctrl键和方向键的组合来实现。即一直按住Ctrl键,然后击方向 键,即可移动控件。 选择该控件后,单击Delete键即可,或选择菜单Edit后,再选择Delete菜单 项。 打开MFC ClassWizard对话框中的Message Maps选项卡,选择Object Ids中的 CxingyunDlg , 然 后 在 Messages 框 中 选 择 WM_INITDIALOG 并 单 击 Add Function按钮定义这个消息的处理器 CSting提供了下列的字符串处理特性: (1)引用记数 (2)字符串长度 (3)数据 (4)空字符串终止符
焦点控制: 按钮被单击或者使用tab键选中,即为按钮获得焦点(focus);按钮获得 SetFocus方法 焦点后,还会再失去焦点
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
19
第3章 “速算24”游戏 “速算24”扑克游戏是个很流行的数学运算游戏。本章通过“速算24”游戏,来加深 对对话框编程的认识和理解,并介绍Visual C++在数学计算方面的知识,以及在按钮上设 置位图和设置定时器的方法。 速算24扑克游戏的规则是由系统发出4张扑克牌,要求用户利用扑克牌显示的数字 JQKA和“王”算做1,通过加减乘除运算得出24(可以使用括号)。 当启动应用程序后,游戏界面如图3.1所示。
图 3.1
游戏的初始界面
单击“开始”按钮,游戏开始,系统将发出4张扑克牌。这时用户利用扑克牌显示的数 字,通过加减乘除运算,以最快的速度得出24(可以使用括号)。然后在文本框中写好表 达式,接着单击“计算”按钮。这时系统会计算输入表达式的结果,并告诉用户是对还是 错了。如果用户输入的表达式计算结果不等于24,则弹出一对话框,告诉用户他的表达式 的结果错误,如图3.2所示,如果您的表达式正确,这时系统会恭喜算对了!如图3.3所示。
20
趣味程序导学 Visual C++
图3.2
计算结果错误
图3.3
计算结果正确
因为本章的目的是通过这个游戏来介绍如何用Visual C++进行数学运算,因此游戏的 界面很简单,后面我们会逐步完善和美化界面。
3.1
设计初始界面
本节将生成程序Susuan, 它是用应用程序向导生成的一个基于对话框的简单框架。 我们首先生成基本框架源代码,然后对生成的源代码进行修改,得到完整的“速算24”游 戏。
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
21
生成基本框架源代码
3.1.1
生成Susuan程序时,首先像 “幸运52”游戏一样,使用应用程序向导生成源代码文件。 在New Project Workspace对话框中,Name文字框输入Susuan,Location文本框输入工程文件 夹路径。在应用程序向导对话框,选择与“幸运52”游戏中相同的选项。 具体的操作在这里就不再赘述,不清楚的地方请参见“幸运52”游戏的创建部分,即 2.2节。 生成管理对话框的类,定义成员变量
3.1.2
在这里,也同样采用和“幸运52”游戏中同样的方法。我们只需要定义一个成员变量, 那就是IDC_EDIT1的成员变量m_EDIT1。 定义消息处理函数
3.1.3
在这个程序中,需要同“幸运52”游戏中一样,加入“开始”按钮和“计算”按钮的 消息处理函数。 引入图片资源
3.1.4
采用和“幸运52”游戏同样的方法引入扑克牌的bmp图片。在此,为了简单起见,我 们只引入了13张扑克(从IDB_BITMAP1到IDB_BITMAP13),和一种扑克背面的bmp图 (IDB_BITMAP14)。 3.2
编写程序代码
1.首先打开SusuanDlg.h文件,在CSusuanDlg 的类定义中加入以下的变量和函数,代 码中添加了详细的注释,请对照阅读、揣摩: class CSusuanDlg: public CDialog { // Construction public: char *stopstring; //定位最后一个算术符号的位置 int AynLastPos(CString Str); //定位第一个算术符号的位置 int AynFirstPos(CString Str); //判断最先出现的符号是+号、-号、*号还是/号 char AnyFirstF(CString Str); //此计算用于计算不带()号的加、减、乘、除运算 int SubCompute(CString Str); //用于计算表达式的结果 int TotalCompute(CString Str);
趣味程序导学 Visual C++
22
HBITMAP m_hBackGround; void RandomDisplay();//随机显示4张牌的函数 int card[14]; CButton* m_Button[4]; CBitmap BackGroundBitmap; CBitmap Bitmap[13]; CSusuanDlg(CWnd* pParent=NULL);
//standard constructor
由上面的代码和注释可以看出,实现游戏的难点之一是对用户输入的表达式进行计算。 因为用户的输入是一行文本,所以必须对它进行识别,并按照加减乘除运算的优先级进行 处理。 2.然后打开SusuanDlg.cpp文件,找到OnInitDialog函数,对上面定义的变量进行初始 化。输入代码如下: BOOL CSusuanDlg:: OnInitDialog() { CDialog::OnInitDialog(); m_Button[0]=(CButton*)GetDlgItem(IDC_BUTTON1); m_Button[1]=(CButton*)GetDlgItem(IDC_BUTTON2); m_Button[2]=(CButton*)GetDlgItem(IDC_BUTTON3); m_Button[3]=(CButton*)GetDlgItem(IDC_BUTTON4); Bitmap[0].LoadBitmap(IDB_BITMAP1); Bitmap[1].LoadBitmap(IDB_BITMAP2); Bitmap[2].LoadBitmap(IDB_BITMAP3); Bitmap[3].LoadBitmap(IDB_BITMAP4); Bitmap[4].LoadBitmap(IDB_BITMAP5); Bitmap[5].LoadBitmap(IDB_BITMAP6); Bitmap[6].LoadBitmap(IDB_BITMAP7); Bitmap[7].LoadBitmap(IDB_BITMAP8); Bitmap[8].LoadBitmap(IDB_BITMAP9); Bitmap[9].LoadBitmap(IDB_BITMAP10); Bitmap[10].LoadBitmap(IDB_BITMAP11); Bitmap[11].LoadBitmap(IDB_BITMAP12); Bitmap[12].LoadBitmap(IDB_BITMAP13); m_hBackGround=::LoadBitmap(AfxGetInstanceHandle(),MAKEINTRESOURCE (IDB_BITMAP14)); for (int {
i=1; i<5; i++)
m_Button[i-1]->SetBitmap(m_hBackGround);
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
23
}
在这个初始化过程中,主要是对bmp资源进行赋值,并且使得程序刚打开的时候,4个 按钮都显示扑克的背面图。 3.另外我们还必须对上面定义的数学函数进行重新定义。在 SusuanDlg.cpp文件中加入 以下的代码,注意阅读代码中的注释: //定位最后一个算术符号的位置 Int CSusuanDlg::AnyLastPos(CString Str) { int SubPos=Str.ReverseFind(’-’)+1; int PluPos=Str.ReverseFind(’+’)+1; int MulPos=Str.ReverseFind(’*’)+1; int DivPos=Str.ReverseFind(’/’)+1; int Pos=SubPos; if(Pos
//如果没有-号 //将SubPos设置成一个不可能的值 //如果没有+号 //将PluPos设置成一个不可能的值 //如果没有*号 //将MulPos设置成一个不可能的值
DivPos=200; if(ForPos==0)
//如果没有/号 //将DivPos设置成一个不可能的值 //如果没有^号
ForPos=200;
//将ForPos设置成一个不可能的值
if(DivPos==0)
趣味程序导学 Visual C++
24
if(Pos>SubPos) Pos=SubPos; if(Pos>PluPos) Pos=PluPos; if(Pos>MulPos) Pos=MulPos; if(Pos>DivPos) Pos=DivPos; if(Pos>ForPos) Pos=ForPos; return Pos; } //------------------------------------------------------------------//判断最先出现的符号是+号、-号、*号还是/号 char CSusuanDlg::AnyFirstF(CString Str) { int SubPos=Str.Find(’-’)+1; int PluPos=Str.Find(’+’)+1; int MulPos=Str.Find(’*’)+1; int DivPos=Str.Find(’/’)+1; if(SubPos==0) SubPos=200; if(PluPos==0) PluPos=200; if(MulPos==0) MulPos=200; if(DivPos==0) DivPos=200; char Result=’-’; int tempPos=SubPos; if(PluPos
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用 if(DivPos
25
趣味程序导学 Visual C++
26
int DivPos=Str.Find(’/’)+1; First=MulPos; if(MulPos>DivPos) First=DivPos; if(DivPos==&&MulPos!=0) { First=MulPos; DivPos=2000; //将除号所在位置设置成一个大于MulPos但又不可能的值 } if(DivPos!=0
&& Mulpos==0)
{ First=DivPos; //将乘号所在位置设置成一个大于DivPos但不可能的值 MulPos=2000; } while(First)
//循环计算乘、除
{ CString tempStr=Str.Mid(0,First-1); int temp=AnyLastPos(tempStr); CString Left=Str.Mid(0,temp); CString Mull=Str.Mid(temp, First-temp-1); tempStr=Str.Mid(First, Str.GetLength()-First); temp=AnyFirstPos(tempStr); if(temp==200) { Mul2=tempStr; Right=” ”; } else { Mul2=tempStr.Mid(0,temp-1); Right=tempStr.Mid(temp-1,tempStr.GetLength()-temp+1); } if(MulPos>DivPos) Middle.Format(“%d”,(int)(strtod(Mull.GetBuffer(Mull.GetLength()), &stopstring)/strtod(Mul2.GetBuffer(Mul2.GetLength()), &stopstring))); else Middle.Format(“%d”,(int)(strtod(Mull.GetBuffer(Mull.GetLength()), &stopstring)*strtod(Mul2.GetBuffer(Mul2.GetLength()), &stopstring)));
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用 Str=Left+Middle+Right; MulPos=Str.Find(’*’)+1; DivPos=Str.Find(’/’)+1; First=MulPos; if(MulPos>DivPos) First=DivPos; if(DivPos==0
&& MulPos!=0)
{ First=MulPos; DivPos=2000; // 将除号所在位置设置成一个大于MulPos但又不可能的值 } if(DivPos!=0 {
&& MulPos==0)
First=DivPos; // 将乘号所在位置设置成一个大于DivPos但不可能的值 MulPos=2000; } } //定位+、-号首先出现的位置 First=AnyFirstPos(Str); if(First=200)//如果没有+、-号,则可以直接返回结果 return (int)strtod(Str.GetBuffer(Str.GetLength()),&stopstring); char Fuhao=AnyFirstF(Str); //确定首先出现的符号是+号还是-号 while(First) {//如果找到+号或-号 CString tempStr=Str.Mid(0,First-1); int temp=AnyLastPos(tempStr); CString Left=Str.Mid(0,temp); CString Mull=Str.Mid(temp,First-temp-1); tempStr=Str.Mid(First.Str.GetLength()-First); temp=AnyFirstPos(tempStr); if(temp==200) { Mul2=tempStr; Right=” ”; } else { Mul2=tempStr.Mid(0,temp-1); Right=tempStr.Mid(temp-1,tempStr.Getlength()-temp+1); }
27
趣味程序导学 Visual C++
28 if(Fuhao==’+’)
Middle.Format(“%d”,(int)(strtod(Mull.GetBuffer(Mull.GetLength()), &stopstring)+strtod(Mul2.GetBuffer(Mul2.GetLength()), &stopstring))); else Middle.Format(“%d”,(int)(strtod(Mull.GetBuffer(Mul2.GetLength()), &stopstring)-strtod(Mul2.GetBuffer(Mul2.GetLength()), &stopstring))); Str=Left+Middle+Right; First=AnyFirstPos(Str); if(First==200) break; Fuhao=AnyFirstF(Str); } return(int)strtod(Middle.GetBuffer(Middle.GetLength()),&stopstring); } //---------------------------------------------------------------------//用于计算表达式的结果 int CSusuanDlg::TotalCompute(CString Str) { int First=Str.ReverseFind(’(’)+1; //定位最后一个(号位置 while(First) { CString SubStr=Str.Mid(First,(Str.GetLength()-First)); int Last=SubStr.Find(’)’)+1; Last+=First; //定位最后一个(号以后的最开始的)号位置 CString LeftStr=Str.Mid(0,First-1); //(号左边的字符串 CString Middle=Str.Mid(First,Last-First-1); // ( )号右边的字符串
// ( )号中间的字符串
CString Right=Str.Mid(Last, Str.GetLength()-Last); int Result=SubCompute(Middle); Middle.Format(“%d”, Result); Str=LeftStr+Middle+Right; First=Str.ReverseFind(’(’)+1; } int Result=SubCompute(Str); return Result;
//进入下面的计算
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
29
}
因为这里要用到很多Visual C++的数学库函数,所以请在SusuanDlg.cpp文件的开始部 分加入以下两行,否则编译时会出错。 #include “math.h” #include “stdlib.h”
4.下面定义“开始”按钮的消息处理函数,仍然打开上面的文件,找到OnButton5函 数,加入下面的代码: viod CSusuanDlg::OnButton5() { RandomDisplay(); for(int i=1; i<5; i++) {int j; j=card[i+5]; m_Button[i-1]->SetBitmap(HBITMAP(Bitmap[j-1]));} //T0D0:Add your control notification handler code here }
在这个消息处理函数中,首先是取得一个随机数组,然后取其中4个数,根据这4个数, 发出4张随机扑克,并且在4个Button上显示出。 5.下面定义“计算”按钮的消息处理函数,在上面的文件中找到OnOK函数,并加入 下面的代码: void CSusuanDlg::OnOK() { //T0D0:Add extra validation here UpdateData(); int Result=TotalCompute(m_EDIT1); if(Result==24) { MessageBox(“您真行,我服了您!”, “对了”, MB_OK); } else { CString temp; temp.Format(“%d”,Result); CString Answer= “您输入的表达的计算结果为”+temp+”!”; MessageBox(Answer, “错了”, MB_OK); } //CDialog::OnOK(); }
趣味程序导学 Visual C++
30
此函数首先对用户输入的表达式进行计算,然后与24作比较,并且根据结果显示相应 的消息。 至此,这个游戏的程序已经基本上完成了,下面我们将进一步完善游戏的界面。我们 将看到,虽然这只是一些小技巧,但是效果却是明显的。
3.3 3.3.1
完善游戏界面
不同时期在按钮上显示不同文字
当程序运行后,界面上出现“开始”按钮是应该的,但是当用户计算一个表达式以后, 该控件不应该再显示成“开始”,而应该显示成“重新开始”。 要完成此功能非常的简单,只需要在“开始”按钮的OnButton5消息处理函数中加入下 面一行代码即可: GetDlgItem(IDC_BUTTON5)->SetWindowText(“重新开始”);
虽然只添加了一行代码,但是却改善了游戏效果。 3.3.2
增加计时功能
1. 效果 显示用户在计算时所花费的时间。当单击“计算”按钮后,显示的时间就是用户所花 费的总时间。 2. 实现方法 在最初的界面设计时,已经向界面中加入了一个“使用时间”的static 控件。在这里, 只需再向对话框中加入一个计时器即可。 为此,打开View菜单,并单击MFC ClassWizard选项,弹出MFC ClassWizard对话框。 然后打开Message Maps标签,在Object Ids 中选中CSusuanDlg类,并且在Messages文本框中 选中WM_TIMER函数,并单击Add Function按钮,如单击OK,系统会自动生成一个OnTimer 函数。 首先我们要定义一个计数器,以便对使用时间进行计数。为此,在 SusuanDlg.h文件中, 给CSusuanDlg类加入一个公共变量。 int SpendTime;
然后在“开始”按钮的消息处理函数中加入以下两行代码: SetTimer(1, 1000, NULL); SpendTime=0;
SetTimer函数是对计时器进行设置,1是定时器的ID号,1000表示计时器的计时时间是
第 2 章“幸运 52”与“速算 24”游戏——Visual C++ 初步应用
31
1000ms,也就是1s。这样,每隔1s,定时器就会发出一个WM_TIMER消息,并且调用OnTimer 函数。 下面我们就编写OnTimer函数的代码。打开SusuanDlg.cpp文件,并找到OnTimer函数, 在其中加入以下的代码: void CSusuanDlg::OnTimer(UINT nIDEvent) { SpendTime++ CString SSpendTime; SSpendTime.Format(“%d”,SpendTime); GetDlgItem(IDC_STATIC1)->SetWindowText(“使用时间:” “+SSpendTime+”秒”); //T0D0: Add your message handler code here and/or call default CDialog::OnTimer(nIDEvent); }
此代码的处理过程是,首先对时间计数器进行赋值计数,然后在static1上显示出使用时 间。 最后,在用户输入结果后,还要撤消计时器,以便下次重新计数。为此,在OnOK函 数中,加入下面一行代码: KillTimer(1);
括号中的1代表的是该计时器的ID号。 至此,该游戏已完成。通过这个大家常见的游戏程序,加深了我们对于对话框编程的 认识和理解,而且在其中还讲述了Visual C++在数学计算方面的应用,以及在按钮上设置 位图和计时器的方法。
3.4
添加Visual C++的数学库函数
本章知识点回顾
#include “math.h” #include “stdlib.h”
不同时期在按钮上显示不同文 字 增加计时功能—— OnTimer 函数,SetTimer函数
GetDlgItem(IDC_BUTTON5)->SetWindowText(“重新开始”);
OnTimer函数 void CSusuanDlg::OnTimer(UINT nIDEvent) { SpendTime++ CString SSpendTime;
趣味程序导学 Visual C++
32
SSpendTime.Format(“%d”,SpendTime); GetDlgItem(IDC_STATIC1)->SetWindowText(“ 使 用时间:” “+SSpendTime+”秒”); //T0D0: Add your message handler code here and/or call default CDialog::OnTimer(nIDEvent); }
SetTimer函数 SetTimer(1, 1000, NULL);
撤消计时器
在按钮上设置位图
KillTimer(1);
m_Button[ ]=(CButton*)GetDlgItem(IDC_BUTTON);
第 4 章 拼图游戏——Visual C++位图操作 “拼图”是一个非常有趣的益智游戏。本章通过实现一个简单的拼图游戏,来实践对 Visual C++中位图的简单应用。本章主要讲述以下内容:在基于对话框的工程中加入菜单 操作、用代码操纵菜单、Windows 位图文件的基本结构、Visual C++中对位图资源的操 作、Visual C++中对自定义位图文件的操作、设备相关位图(DDB)的概念、用Static 控件 显示位图以及用Status Bar显示提示信息和Visual C++随机函数。
4.1
游戏效果说明
“拼图”游戏的核心规则是将一张整图分成N小块,随机打乱,让用户拼回原图,根 据用户所花费时间的多少来评价其玩游戏的水平。 该游戏的具体规则如下: 1. 游戏开始,由用户选择应用程序提供的位图资源或自定义的位图作为游戏使用的 图片。 2. 用户选择游戏的难度,若选择“容易”,程序将图片分成9块;若选择“难”则分 成16块。 3. 单击“重置”菜单项初始化各图格的位置。 4. 单击任一图格,图像重新排列,并开始计时,用户可单击空格周围的图格来改变 其位置。 5. 拼图成功,程序给出提示信息和所花费的时间,用户可选择另外一幅图片重新开 始游戏。 游戏界面如图4.1所示,由于本章的目的是通过这个游戏来介绍Visual C++中位图的操 作,所以对游戏的界面不做过多的渲染,只是在后面会做简单的美化。
4.2
创建初始界面
新建一个工程,将其命名为Picture,注意在第一步选择Dialog based,如图4.2所示。 在Workspace的Resource标签中加入菜单资源,如图4.3所示设定菜单项,并将其ID号设为 IDD_PICTURE_DIALOG。设置方法如下是,打开Dialog Properties对话框,为Menu属性对 应的下拉框选择IDR_MENU1,如图4.4所示,这样菜单就可在程序运行时显示在主对话框 的顶部了。
2
趣味程序导学 Visual C++
图 4.1
图 4.2
游戏界面
创建 Dialog based 工程
图 4.3
设定程序菜单项
第4章
拼图游戏——Visual C++位图操作
图 4.4
3
设定主对话框的属性
为CPictureDlg类添加两个CMenu类型的成员变量如下: CMenu *pMainMenu; CMenu *pSubMenu;
CMenu类为Windows HMENU的封装类。它提供了成员函数以用于创建、跟踪、更新 及撤消菜单。在CPictureDlg的OnInitialDialog事件中加入代码: pMainMenu=GetMenu();
可以得到指向对话框主菜单的指针,这样,我们就可以通过代码来操纵菜单了。这里先简 单介绍通过代码对菜单进行动态修改所涉及到的几个函数,具体操作放在后面几节中讲。 在MFC中用CMenu类来处理和菜单有关的功能。在创建一个CMenu对象时需要从资源 中装入菜单,通过调用BOOL CMenu::LoadMenu(UINT nIDResource)进行装入,然后就可 以对菜单进行动态修改,所涉及到的函数有: (1)CMenu* GetSubMenu(int nPos) 若弹出菜单位于指定的位置,则返回CMenu 对 象的指针,其中CMenu对象要包含弹出菜单的句柄;否则返回NULL。如果CMenu 对象 不存在,那么将创建临时CMenu对象,但返回的CMenu指针不应被存储。nPos指定包含在 菜单中的弹出菜单的位置。对于第一个菜单项,开始位置值为0。 (2)BOOL AppendMenu(UINT nFlags, UINT nIDNewItem=0, LPCTSTR lpszNewItem = NULL) 在末尾添加一项,若nFlags为MF_SEPARATOR表示增加一个分隔条,这样其他 两个参数将会被忽略;若nFlag为MF_STRING表示添加一个菜单项。nIDNewItem为该菜单 的ID命令值;若nIDNewItem为MF_POPUP 表示添加一个弹出菜单项,这时nIDNewItem为 另一菜单的句柄HMENU。lpszNewItem为菜单文字说明。 (3)BOOL InsertMenu(UINT nPosition, UINT nFlags ,UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL) 用于在指定位置插入一菜单,变量nPosition指定插入位 置 。 如 果 nFlags 包 含 MF_BYPOSITION 则 表 明 插 入 在 nPosition 位 置 , 如 果 包 含 MF_BYCOMMAND表示插入在ID为nPosition的菜单处。 ( 4 ) BOOL ModifyMenu(UINT nPosition, UINT nFlags, UINT nIDNewItem=0 , LPCTSTR lpszNewItem = NULL) 用于修改某一位置的菜单,如果nFlags包含MF_BYPOSITION,则表明修改nPosition位置的 菜单,如果包含 MF_BYCOMMAND表示修改ID为 nPosition处的菜单。 (5)BOOL RemoveMenu(UINT nPosition, UINT nFlags) 用于删除某一位置的菜单。
趣味程序导学 Visual C++
4
如 果 nFlags 包 含 MF_BYPOSITION 则 表 明 删 除 nPosition 位 置 的 菜 单 , 如 果 包 含 MF_BYCOMMAND表示删除ID为nPosition处的菜单。 (6)BOOL AppendMenu(UINT nFlags, UINT nIDNewItem, const CBitmap* pBmp) 和 BOOL InsertMenu(UINT nPosition, UINT nFlags, UINT nIDNewItem, const CBitmap* pBmp) 可以添加位图菜单,但这样的菜单在选中时是反色显示,并不美观。 (7)UINT CheckMenuItem(UINT nIDCheckItem, UINT nCheck) 返回菜单项以前的状 态 : MF_CHECKED 或 MF_UNCHECKED 。 如 果 该 菜 单 项 不 存 在 , 那 么 将 返 回 0xFFFFFFFF。nIDCheckItem指定由nCheck确定的将要选择的菜单项。nCheck指定是否选 中 菜 单 项 , 并 决 定 菜 单 中 各 菜 单 项 的 位 置 。 参 数 nCheck可 以 是 MF_CHECKED 或 MF_UNCHECKED与MF_BYPOSITION或MF_BYCOM MAND的组合。这些标志可通过使 用位与运算进行组合。其中MF_CHECKED与MF_UNCHECKED用来进行状态转换,在菜 单项之前放置默认的选中标记。 另外,视图中是没有菜单的,在框架窗口中才有菜单,所以只有用AfxGetApp()->m_p MainWnd->GetMenu()才能得到菜单指针。
4.3
位图的读入和显示
在程序中,需要由用户来选择游戏中使用的图片,同时还需要将整幅图片分成不同的 小块。因此,采用Static 控件作为位图的载体,分别显示图片的不同部分。 4.3.1
Windows位图的基本结构
Windows位图文件即BMP文件,它由位图文件头、位图信息头、颜色表和位图数据4 部分组成。 位图文件头 位图文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。其结构 定义如下: typedef struct tagBITMAPFILEHEADER { // 位图文件的类型,必须为BM // 位图文件的大小,以字节为单位 WORD bfReserved1; // 位图文件保留字,必须为0 WORD bfReserved2; // 位图文件保留字,必须为0 // 位图数据的起始位置,以相对于位图文件头的偏移量表示,以字节为单位 WORD DWORD
bfType; bfSize;
DWORD
bfOffBits;
} BITMAPFILEHEADER;
第4章
拼图游戏——Visual C++位图操作
5
位图信息头 位图信息头数据用于说明位图的大小。其结构定义如下: typedef struct tagBITMAPINFOHEADER { DWORD biSize; // 所占用字节数 LONG biWidth; // 位图的宽度,以像素为单位 LONG biHeight; // 位图的高度,以像素为单位 WORD biPlanes; // 目标设备的级别,必须为1 WORD biBitCount // 每个像素所需的位数,必须是1(双色), // 4(16色),8(256色)或24(真彩色)之一 DWORD biCompression; // 位图压缩类型,必须是 0(不压缩), // 1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一 DWORD biSizeImage; // 位图的大小,以字节为单位 LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数 LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数 DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数 DWORD biClrImportant; // 位图显示过程中重要的颜色数 } BITMAPINFOHEADER;
颜色表 颜色表用于说明位图的颜色,它有若干个表项,每个表项定义一种颜色,其结构为 RGBQUAD类型,其结构定义如下: typedef struct tagRGBQUAD { BYTE rgbBlue; // BYTE rgbGreen; // BYTE rgbRed; // BYTE rgbReserved; // } RGBQUAD;
蓝色的亮度(值范围为0-255) 绿色的亮度(值范围为0-255) 红色的亮度(值范围为0-255) 保留,必须为0
颜色表中项数由biBitCount来确定: ・ 当biBitCount=1,4,8时,分别有2,16,256个表项。 ・ 当biBitCount=24时,没有颜色表项。 位图信息头和颜色表组成位图信息,BITMAPINFO结构定义如下: typedef struct tagBITMAPINFO { BITMAPINFOHEADER bmiHeader; RGBQUAD bmiColors[1]; } BITMAPINFO;
// 位图信息头 // 颜色表
趣味程序导学 Visual C++
6
位图数据 位图数据记录了位图的每一个像素值,记录顺序是在扫描行内从左到右,扫描行之间 是从下到上。位图的一个像素值所占的字节数: ・ ・ ・ ・
当biBitCount=1时,8个像素占1个字节。 当biBitCount=4时,2个像素占1个字节。 当biBitCount=8时,1个像素占1个字节。 当biBitCount=24时,1个像素占3个字节。
Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以 0填充。 Windows用位图来显示和保存图像,从单色图像到24位真彩色图像都可以存储到位图 中。 位图实际上是一个像素阵列,像素阵列存储在一个字节数组中,每个像素的位数可以 是1、4、8或24。单色位图的字节数组中的每一位存储一个像素,16色位图的字节数组中 每一个字节存储两个像素,256色位图的每一个字节存储一个像素,而真彩色位图中每个 像素用3个字节来表示。在256色以下的位图中存储的像素值实际上是调色板索引,在真彩 色位图中存储的则是像素的RGB颜色值。 位图分为设备相关位图(DDB)和与设备无关位图(DIB),二者有不同的用途。 4.3.2
位图资源的读入
在Insert Resource对话框中选择Bitmap,如图4.5所示,单击Import… 按钮,在打开的 文件对话框中选择一幅位图,将其作为资源加入到工程中,ID号为IDB_BITMAP1。同样 将 另 外 三 幅 位 图 也 加 入 到 工 程 中 , ID 号 分 别 为 ,IDB_BITMAP2 , IDB_BITMAP3 和 IDB_BITMAP4。
图 4.5
将位图资源加入到工程中
为CPictureDlg类添加一个CBitmap类型的成员变量Bitmap,CBitmap封装了Windows图 形设备接口(GDI)中的位图,并且提供了操纵位图的成员函数。 添加如图4.6所示菜单 项 , 以 用 来 选 择 不 同 的 位 图 资 源 进 行 游 戏 , 其 ID 号 分 别 为 ID_PICTURE1 ,
第4章
拼图游戏——Visual C++位图操作
7
ID_PICTURE2,ID_PICTURE3,ID_PICTURE4。
图 4.6
添加选择位图资源的菜单项
为方便起见,我们用一个函数来实现不同位图资源的加载。为CPictureDialog类添加 一个成员函数 void OnRun(UINT nBitmapID,UINT nMenuID);
参数nBitmapID指定位图资源的ID号,nMenuID指定菜单项的ID号。在函数中,我们 首先用GetSubMenu得到指向弹出菜单对象的指针,然后用CheckMenuItem方法为nMenuID 所指定的菜单项放置选中标记。接下来用CBitmap对象的LoadBitmap方法将nBitmapID所指 定的位图载入。相应代码如下: void CPictureDlg::OnRun(UINT nBitmapID,UINT nMenuID) { pSubMenu=pMainMenu->GetSubMenu(1); pSubMenu->CheckMenuItem(ID_PICTURE1,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE2,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE3,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE4,MF_UNCHECKED); pSubMenu->CheckMenuItem(nMenuID,MF_CHECKED); bitmap.DeleteObject(); bitmap.LoadBitmap(nBitmapID); }
载入位图时,首先调用函数CGdiObject::DeleteObject删除由LoadBitmap加载过的位 图,然后用LoadBitmap方法载入新的位图对象。LoadBitmap函数的原型为: BOOL LoadBitmap(LPCTSTR lpszResourceName); BOOL LoadBitmap(UINT nIDResource);
参数lpszResourceName指向一个包含了位图资源名字的字符串(该字符串以null结 尾),而nIDResource指定位图资源的ID号。该函数从应用程序的可执行 文件中加载由 lpszResourceName指定名字或者由nIDResource指定的ID号标志的位图资源。加载的位图被 附在CBitmap对象上。如果由lpszResourceName指定名字的对象不存在,或者没有足够的 内存加载位图,函数将返回0。 用 ClassWizard 为 菜 单 项 ID_PICTURE1 , ID_PICTURE2 , ID_PICTURE3 , ID_PICTURE4添加响应函数,并调用OnRun函数加载位图。相应代码如下:
趣味程序导学 Visual C++
8
void CPictureDlg::OnPic1() { OnRun(IDB_BITMAP1,ID_PICTURE1); } void CPictureDlg::OnPic2() { OnRun(IDB_BITMAP2,ID_PICTURE2); } void CPictureDlg::OnPic3() { OnRun(IDB_BITMAP3,ID_PICTURE3); } void CPictureDlg::OnPic4() { OnRun(IDB_BITMAP4,ID_PICTURE4); }
4.3.3
自定义位图文件的读入
添加“自定义”菜单项,如图4.7所示,将其ID号设为ID_ADVAN,并在Class Wizard 中加入相应的事件处理函数。
图 4.7
加入“自定义”菜单项
为CPictureDlg添加一个HBITMAP类型的成员变量hBitmap,表示一个指向位图资源的 句柄。接下来我们为“自定义”菜单项添加事件处理函数,相应代码如下: CFileDialog dlg(TRUE,NULL,NULL,NULL,"位图文件(*.bmp)|*.bmp"); INT Result=dlg.DoModal(); if(Result==IDOK) { count=0;
CanCount=FALSE;
IsRnd=FALSE; Advan=TRUE; hBitmap=(HBITMAP)::LoadImage(NULL,dlg.GetFileName(),IMAGE_BITMAP, 0,0,LR_LOADFROMFILE); BITMAP bm; ::GetObject(hbitmap,sizeof(BITMAP),&bm); if(bm.bmWidth>=bm.bmHeight) {
第4章
拼图游戏——Visual C++位图操作
9
hBitmap=(HBITMAP)::LoadImage(NULL,dlg.GetFileName(), IMAGE_BITMAP,350,280,LR_LOADFROMFILE); Width=350; Height=280; IsLong=FALSE; } else if(bm.bmWidth
在上面的程序中,我们用LoadImage方法来加载自定义文件中的位图资源,然后用 GetObject方法得到其BITMAP对象,函数LoadImage的原型为: HANDLE LoadImage ( HINSTANCE hinst,
// handle to instance
LPCTSTR lpszName,
// name or identifier of the image
UINT uType, int cxDesired,
// image type // desired width
int cyDesired, UINT fuLoad );
// desired height // load options
它可以给图标、光标或位图资源返回一个一般的句柄。参数hinst处理包含被加载图像 模块的实例。若要加载OEM图像,则此值设为0。参数lpszName处理图像加载。如果参数 hinst为非空,而且参数fuLoad不包括LR_LOADFROMFILE的值时,那么参数lpszName是 一个指向保留在hinst模块中加载的图像资源名称,并以NULL为结束符的字符串。如果参 数hinst为空,并且LR_LOADFROMFILE被指定,那么这个参数低位字一定是被加载的 OEM图像所标识的。OEM图像标识符是在WINUSER.H头文件中定义的,下面给出其前缀 的含义: ・ OBM_ OEM:位图 ・ OIC_OEM:图标 ・ OCR_OEM:光标 如果参数fuLoad包含LR_LOADFROMFILE值,则参数lpszName为包含有图像的文件 名。
10
趣味程序导学 Visual C++
参数uType指定被加载图像类型。此参数可以为下列值,其含义如下: ・ IMAGE_BITMAP:加载位图 ・ IMAGE_CURSOR:加载光标 ・ IMAGE_ICON:加载图标 参数cxDesired指定图标或光标的宽度,以像素为单位。如果此参数为0 并且参数 fuLoad值 为LR_DEFAULTSIZE, 那 么 函 数 使 用 SM_CXICON或SM_CXCURSOR 设定宽 度;如果此参数为0并且没有使用LR_DEFAULTSIZE,那么函数使用目前的资源宽度。 参数cyDesired意义同上,只不过它指定图标或光标的高度。 参数fuLoad根据下面复合值列表指定函数返回值,值含义如下: ・ LR_DEFAULTCOLOR : 默 认 标 志 ; 它 不 做 任 何 事 情 。 它 的 含 义 是 “ 无 LR_MONOCHROME”。 ・ LR_CREATEDIBSECTION:当参数uType指定为IMAGE_BITMAP 时,该函数返 回一个DIB位图,而不是一个兼容的位图。这个标志在加载一个位图,而不是映射 它的颜色到显示设备时非常有用。 ・ LRDEFAULTSIZE:若cxDesired或cyDesired未被设为0,使用系统指定的公制值标 识光标或图标的宽和高。如果不设置这个参数且cxDesired或cyDesired被设为0,函 数使用实际资源大小。如果资源包含多个图像,则使用第一个图像的大小。 ・ LR_LOADFROMFILE:根据参数 lpszName的值加载图像。若标记未被给定, lpszName的值为资源名称。 ・ LW_LOADMAP3DCOLORS:查找图像的颜色表并且按下面相应的3D颜色表的灰 度进行替换。3D颜色表:DkGrayRGB(128,128,128)COLOR_3DSHADOW; Gray RGB(192,192,192)COLOR_3DFACEjLt Gray RGB(223,223,223)。 ・ COLOR_3DLIGHT LR_LOADTRANSPARENT:找到图像中的一个像素颜色值并 且根据颜色表中系统的默认颜色值替代其相应接口的值。图像中所有使用这种接 口的像素的颜色都变为系统的默认窗体颜色。此值仅用来申请相应的颜色表。若 fuLoad 包 括 LR_LOADTRANSPARENT 和 LR_LOADMAP3DCOLORS 两 个 值 , 则 LR_LOADTRANSPARENT优先。但是,颜色表接口由COLOR_3DFACE替代,而 不是COLOR_WINDOW。 ・ LR_MONOCHROME:加载黑白图。 ・ LR_SHARED:若图像将被多次加载则共享。如果LR_SHARED未被设置,则再向 同一个资源第二次调用这个图像时就会再次加载,以便这个图像能够返回不同的 句柄。不要对不同标准大小的图像使用LR_SHARED,加载后图像可能会有改 变,或是从文件中被加载。 在此,我们用CFileDialog类的GetFileName方法取得位图文件的完整路径,并将其赋 给 参 数 lpszName ; hinst 的 值 设 为 NULL , 表 示 加 载 OEM 图 像 ; 参 数 uType 设 为 IMAGE_BITMAP,表示加载的资源是位图;第一次调用时,参数cxDesired和cyDesired的 值都设为0,表示保持位图资源的原大小,然后根据它的大小做相应的处理;布尔型变量
第4章
拼图游戏——Visual C++位图操作
11
IsLong用来表示图像的宽度是否大于长度;参数fuLoad的值设为LR_LOADFROMFILE, 这样就可根据参数lpszName的值从位图文件中加载图像。
4. 4
用Static控件显示位图
在游戏程序中,我们采用Static Text即静态文本控件来显示位图。静态文本控件的功 能比较简单,可显示字符串,图标,位图。创建一个窗口可以使用成员函数: BOOL CStatic::Create(LPCTSTR lpszText, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID = 0xffff);
其 中 dwStyle 将 指 定 该 窗 口 的 风 格 , 除 了 子 窗 口 常 用 的 风 格 WS_CHILD , WS_VISIBLE外,可以针对静态控件指定特殊风格: ・ SS_CENTER,SS_LEFT,SS_RIGHT :指定字符显示的对齐方式。 ・ SS_GRAYRECT:显示一个灰色的矩形。 ・ SS_NOPREFIX:如果指定该风格,对于字符&将直接显示,否则 &将作为转义 符,&将不显示,而在其后的字符将有下划线,如果需要直接显示&,必须用&& 表示。 ・ SS_BITMAP:显示位图。 ・ SS_ICON:显示图标。 ・ SS_CENTERIMAGE:图像居中显示。 控制显示的文本,可利用成员函数SetWindowText/GetWindowText设置/得到当前显示 的文本。 控制显示的图标,可利用成员函数SetIcon/GetIcon设置/得到当前显示的图标。 控制显示的位图,可利用成员函数SetBitmap/GetBitmap设置/得到当前显示的位图。 下面一段代码演示如何创建一个显示位图的静态窗口并设置位图: CStatic* pstaDis=new CStatic; pstaDis->Create("",WS_CHILD|WS_VISIBLE|SS_BITMAP|SSCENTERIMAGE, CRect(0,0,40,40),pWnd,1); CBitmap bmpLoad; bmpLoad.LoadBitmap(IDB_TEST); pstaDis->SetBitmap(bmpLoad.Detach());
4.4.1 设置 Static 控件的初始位置 为图4.3中的菜单项添加相应的事件响应,添加布尔型变量Easy来标识游戏的难度 (分为9格或16格),同时添加布尔变量IsRnd用来标识图格是否进行了随机位置的初始 化,相应代码如下: void CPictureDlg::OnEasy() //菜单函数,选择Easy(容易)为9格,选择Hard(难)则为16格 {
趣味程序导学 Visual C++
12
pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->CheckMenuItem(ID_HARD,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_EASY,MF_CHECKED); Easy=TRUE;IsRnd=FALSE; CanCount=FALSE; } void CPictureDlg::OnHard() //菜单函数,选择Easy(容易)为9格,选择Hard(难)则为16格 { pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->CheckMenuItem(ID_EASY,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_HARD,MF_CHECKED); Easy=FALSE;IsRnd=FALSE; CanCount=FALSE; }
图4.8和图4.9分别显示了选择Easy(容易)和Hard(难)时的显示效果。
图 4.8
选择 Easy(容易)时的显示效果
第4章
拼图游戏——Visual C++位图操作
图 4.9
13
选择 Hard(难)时的显示效果
为CPictureDlg对话框添加16个Static 控件,并将它们的Type属性设为Bitmap,如图4.10 所示。
图 4.10
设定 CStatic 的 Type 属性
添加一个CStatic 类型的数组与这16个Static 控件相关联,用来对图像进行分格显示。 为CPictureDlg添加两个INT型的成员变量x和y,用来计算Static 控件的位置。添加一个成员 函数SetPos,以对Static 控件的位置进行初始化。由于我们的主要目的是通过游戏来学习 Windows下的位图操作,所以在这里对具体的算法不做过多的叙述,只给出相应的代码, 如下所示: void CPictureDlg::SetPos() //初始化Staic 控件的位置 { BITMAP bm; INT con,move; if(Advan==FALSE) { bitmap.GetObject(sizeof(BITMAP),&bm); Width=bm.bmWidth; Height=bm.bmHeight; } if(Easy) {x=Width/3; y=Height/3; con=2;} else if(!Easy) {x=Width/4;
y=Height/4; con=3;}
if(IsLong) move=70; else move=0; for(int i=0;i<=con;i++) { m_Image[i].SetWindowPos(NULL,x*i+move,0,0,0,SWP_NOSIZE); m_Image[i].SetWindowPos(NULL,0,0,x,y,SWP_NOMOVE); } for(int j=0;j<=con;j++)
趣味程序导学 Visual C++
14 {
m_Image[con+1+j].SetWindowPos(NULL,x*j+move,y,0,0,SWP_NOSIZE); m_Image[con+1+j].SetWindowPos(NULL,0,0,x,y,SWP_NOMOVE); } for(int k=0;k<=con;k++) { m_Image[(con+1)*2+k].SetWindowPos(NULL,x*k+move,2*y,0,0,SWP_NOSIZE); m_Image[(con+1)*2+k].SetWindowPos(NULL,0,0,x,y,SWP_NOMOVE); } if(!Easy) for(int l=0;l<=3;l++) { m_Image[l+12].SetWindowPos(NULL,x*l+move,3*y,0,0,SWP_NOSIZE); m_Image[l+12].SetWindowPos(NULL,0,0,x,y,SWP_NOMOVE); CClientDC dc(&m_Image[l+12]); } }
在这里,我们使用Win32 API SetWindowPos来设置Static 控件的位置,这是一个很有 用的函数,下面我们对它作简单介绍。函数SetWindowPos的原型为: BOOL SetWindowPos ( HWND hWnd,
// handle to window
HWND hWndInsertAfter, // placement-order handle int X, // horizontal position int Y,
// vertical position
int cx, int cy,
// width // height
UINT uFlags
// window-positioning options
);
该函数改变一个子窗口、弹出式窗口或顶层窗口的尺寸、位置和z序。子窗口,弹出 式窗口及顶层窗口根据它们在屏幕上出现的顺序排序,顶层窗口设置的级别最高,并且被 设置为z序的第一个窗口。各参数的含义如下: (1)hWnd:窗口句柄。 (2)hWndInsertAfter:在z序中位于被置位的窗口前的窗口句柄。该参数必须为一个 窗口句柄或下列值之一: ・ HWND_BOTTOM:将窗口置于z序的底部。如果参数hWnd标识了一个顶层 窗口,则窗口失去顶级位置,并且被置在其他窗口的底部。 ・ HWND_DOTTOPMOST:将窗口置于所有非顶层窗口之上(即在所有顶层 窗口之后)。如果窗口已经是非顶层窗口,则该标志不起作用。
第4章
拼图游戏——Visual C++位图操作
15
・ HWND_TOP:将窗口置于z序的顶部。 ・ HWND_TOPMOST:将窗口置于所有非顶层窗口之上。即使窗口未被激 活,它也将保持顶级位置。 (3)X:以客户坐标指定窗口新位置的左边界。 (4)Y:以客户坐标指定窗口新位置的顶边界。 (5)cx:以像素为单位指定窗口的新宽度。 (6)cy:以像素为单位指定窗口的新高度。 (7)uFlags:窗口尺寸和定位的标志。该参数可以是下列值的组合: ・ SWP_ASNCWINDOWPOS:如果调用进程不拥有窗口,系统会向拥有窗口 的线程发出需求。这就防止调用线程在其他线程处理需求的时候发生死锁。 ・ SWP_DEFERERASE:防止产生WM_SYNCPAINT消息。 ・ SWP_DRAWFRAME:在窗口周围画一个边框(其定义在窗口类描述中)。 ・ SWP_FRAMECHANGED:给窗口发送 WM_NCCALCSIZE消息,即使窗口 大小没有改变也会发送该消息。如果未指定这个标志,只有在改变了窗口大 小时才发送WM_NCCALCSIZE。 ・ SWP_HIDEWINDOW:隐藏窗口。 ・ SWP_NOACTIVATE:不激活窗口。如果未设置该标志,则窗口被激活,并 被设置到其他顶级窗口或非顶级窗口的顶部(根据参数hWndlnsertAfter 设 置)。 ・ SWP_NOCOPYBITS:清除客户区的所有内容。如果未设置该标志,客户区 的有效内容被保存并且在窗口大小更新和重定位后拷贝回客户区。 ・ SWP_NOMOVE:维持当前位置(忽略X和Y参数)。 ・ SWP_NOOWNERZORDER:不改变z序中的所有者窗口的位置。 ・ SWP_NOREDRAW:不重画改变的内容。如果设置了这个标志,则不发生 任何重画动作。适用于客户区和非客户区(包括标题栏和滚动条)及任何由 于窗口移动而露出的父窗口的所有部分。如果没有设置这个标志,应用程序 必须显示使窗口无效,并重画窗口的任何部分和父窗口需要重画的部分。 ・ SWP_NOREPOSITION:与SWP_NOOWNERZORDER标志相同。 ・ SWP_NOSENDCHANGING:防止窗口接收WM_WINDOWPOSCHANGING 消息。 ・ SWP_NOSIZE:维持当前大小(忽略cx和cy参数)。 ・ SWP_NOZORDER:维持当前z序(忽略hWndInsertAfter参数)。 ・ SWP_SHOWWINDOW:显示窗口。 如果函数成功,返回值为非0;如果函数失败,则返回值为0。 如果设置了SWP_SHOWWINDOW和SWP_HIDEWINDOW标志,则窗口不能被移动 和 改 变 大 小 。 如 果 使 用 SetWindowLog 改 变 了 窗 口 的 某 些 数 据 , 则 必 须 调 用 函 数 SetWindowPos来进行改变,要使用下列组合标志:
趣味程序导学 Visual C++
16
SWP_NOMOVEISWP_NOSIZEISWP_FRAMECHANGED
有 两 种 方 法 将 窗 口 设 为 最 顶 层 窗 口 : 一 种 是 将 参 数 hWndInsertAfter 设 置 为 HWND_TOPMOST并确保没有设置SWP_NOZORDER标志;另一种是设置窗口在z序中的 位置。当一个窗口被置为最顶层窗口时,属于它的所有窗口均为最顶层窗口,而它的所有 者的z序并不改变。 如果HWND_TOPMOST和HWND_NOTOPMOST标志均未指定,即应用程序要求窗口 在激活的同时改变其在z序中的位置时,在参数hWndInsertAfter中指定的值只有在下列条 件中才使用: ・ 在hWndInsertAfter 参数中没有设定 HWND_NOTOPMOST和HWND_TOPMOST标 志。 ・ 由hWnd参数标识的窗口不是激活窗口。 如果未将一个非激活窗口设定到z序的顶端,则应用程序不能激活该窗口。应用程序 可以无任何限制地改变被激活窗口在z序中的位置,或激活一个窗口并将其移到顶级窗口 的顶部或非顶级窗口的顶部。 如果一个顶层窗口被重定位到z序的底部(HWND_BOTTOM)或在任何非顶级窗口 之后,该窗口就不再是最顶层窗口。当一个最顶层窗口被置为非最顶级,则它的所有者窗 口和所属者窗口均为非最顶层窗口。 一个非最顶层窗口可以拥有一个最顶层窗口,但反之则不可以。任何属于顶层窗口的 窗口(例如一个对话框)本身就被置为顶层窗口,以确保其所属窗口都在它们的所有者之 上。
4.4.2 图格的显示 下面我们首先介绍设备相关位图(DDB)的基本概念。 DDB(Device-dependent bitmap)与设备相关,这主要体现在以下两个方面: ・ DDB的颜色模式必须与输出设备相一致。例如,如果当前的显示设备是256色模 式,那么DDB必然也是256色的,即一个像素用一个字节表示。 ・ 在256色以下的位图中存储的像素值是系统调色板的索引,其颜色依赖于系统调色 板。 由于DDB高度依赖输出设备,所以DDB只能存在于内存中,它要么在视频内存中, 要么在系统内存中。 DDB的主要用途是保存位图。要保存的位图可以来自资源位图,也可以是一个绘制位 图。 前面说过,在256色以下的显示模式中,DDB中的像素值是系统调色板的索引。一般 在系统调色板中除了保留的20种静态颜色外,其他表项都有可能被应用程序改变。如果 DDB中有一些像素值是指向20种静态颜色以外的颜色,那么该位图的颜色将是不稳定的。 因此,DDB不能用来长期存储色彩丰富的位图。如果位图使用的大部分颜色都是20种保留
第4章
拼图游戏——Visual C++位图操作
色,则该位图可以用CBitmap对象保存在内存中。 设定了Static 控件的初始位置之后,就可以将图像拷贝到上面,相应代码如下: void CPictureDlg::SetImage() //把图像拷贝到Static控件 { INT con; HANDLE picture; CRect rect(0,0,x,y); if(Advan==TRUE) picture=hbitmap; else if(Advan==FALSE) picture=bitmap; if(Easy) con=2; else if(!Easy)
con=3;
for(int i=0;i<=con;i++) { CDC *pDC=new CDC; CClientDC dc(&m_Image[i]); pDC->CreateCompatibleDC(&dc); pDC->SelectObject(picture); dc.BitBlt(0,0,x,y,pDC,x*i,0,SRCCOPY); if(Style3d) dc.DrawEdge(rect,EDGE_RAISED,BF_RECT); else if(!Style3d)
dc.Draw3dRect(rect,RGB(0,0,0,),RGB(0,0,0,));
delete pDC; UpdateWindow(); } for(int j=0;j<=con;j++) { CClientDC dc(&m_Image[con+1+j]); CDC *pDC=new CDC; pDC->CreateCompatibleDC(&dc); pDC->SelectObject(picture); dc.BitBlt(0,0,x,y,pDC,x*j,y,SRCCOPY); if(Style3d) dc.DrawEdge(rect,EDGE_RAISED,BF_RECT); else if(!Style3d)
dc.Draw3dRect(rect,RGB(0,0,0,),RGB(0,0,0,));
delete pDC; } for(int k=0;k<=con;k++) { CClientDC dc(&m_Image[(con+1)*2+k]); CDC *pDC=new CDC; pDC->CreateCompatibleDC(&dc); pDC->SelectObject(picture);
17
趣味程序导学 Visual C++
18
dc.BitBlt(0,0,x,y,pDC,x*k,2*y,SRCCOPY); if(Style3d) dc.DrawEdge(rect,EDGE_RAISED,BF_RECT); else if(!Style3d) dc.Draw3dRect(rect, RGB(0,0,0,),RGB(0,0,0,)); delete pDC; } if(!Easy) for(int l=0;l<=3;l++) { CClientDC dc(&m_Image[l+12]); CDC *pDC=new CDC; pDC->CreateCompatibleDC(&dc); pDC->SelectObject(picture); dc.BitBlt(0,0,x,y,pDC,x*l,3*y,SRCCOPY); if(Style3d) dc.DrawEdge(rect,EDGE_RAISED,BF_RECT); else if(!Style3d) dc.Draw3dRect(rect,RGB(0,0,0,),RGB(0,0,0,)); delete pDC; } if(!IsRnd) { pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->EnableMenuItem(ID_FORHELP, MF_DISABLED|MF_GRAYED); } }
在 显 示 时 , 首 先 构 造 一 个CClientDC 对 象 , 调 用 其 构 造 函 数CClientDC(CWnd* pWnd),参数pWnd为设备上下文将要存取的客户区所在的窗口。由于我们要使用Static 控 件来显示位图,所以将 m_Image[x]的指针传给它。接下来,新建一个指向 CDC对象的指 针,然后调用其CreateCompatibleDC方法,在内存中准备图像,其函数原型为: virtual BOOL CreateCompatibleDC(CDC*pDC)
其参数pDC为设备上下文的指针。如果pDC为NULL,函数将产生与系统显示兼容的 内存设备上下文,即产生与pDC指定设备兼容的设备上下文内存。设备上下文内存包含显 示表面的信息,它用于在向实际的兼容设备表面发送图像之前在内存中作好准备。当创建 设备内存上下文时,GDI自动选择单色存储位图格式。只有在位图已被创建并被选入设备 上下文之中时,才使用GDI输出函数。在使用时要注意,该函数仅用于创建与支持光栅操 作的设备上下文。 然后,我们调用CDC的SelectObject函数来选择绘图对象,这里,我们选择读入的位 图作为绘图对象。 最后,利用函数BitBlt来绘制位图。该函数把源设备上下文中的位图复制到本身的设 备上下文中,两个设备上下文可以是内存设备上下文,也可以是同一个设备上下文。 BitBlt也是一个很重要的函数,下面我们对它进行详细介绍。
第4章
拼图游戏——Visual C++位图操作
19
函数CDC::BitBlt的原型为: BOOL BitBlt(int x,int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc,DWORD dwRop);
各参数的含义如下: (1)x:指定目标矩形左上角的逻辑x坐标。 (2)y:指定目标矩形左上角的逻辑y坐标。 (3)nWidth:指定目标矩形和源位图的宽度(逻辑单位)。 (4)nHeight:指定目标矩形和源位图的高度(逻辑单位)。 (5)pSrcDC:指向CDC对象的指针,标识待拷贝位图的设备上下文。如果dwRop指 定不包括源的光栅操作,则它必须为NULL。 (6)xSrc:指定源位图左上角的逻辑x坐标。 (7)ySrc:指定源位图左上角的逻辑y坐标。 (8)dwRop:指定要执行的光栅操作。光栅操作代码定义CDC如何合并输出操作中 的颜色,包括当前画刷、可能的源位图和目标位图。下面对dwRop列出光栅操作代码及其 描述: ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・
BLACKNESS:所有输出变黑 DSTINVERT:反转目标位图 MERGECOPY:使用布尔运算符AND合并特征与源位图 MERGEPAINT:使用布尔运算OR符合并特征与源位图 NOTSRCCOPY:拷贝反转源位图到目标 NOTSRCERASE:反转使用布尔运算符OR合并源和目标位图的结果 PATCOPY:拷贝特征到目标位图 PATINVERT:使用布尔运算符XOR合并目标位图和特征 PATPAINT:使用布尔运算符OR合并反转源位图和特征或用布尔OR运算符 合并这项操作结果与目标位图 SRCAND:使用布尔运算符AND合并目标像素和源位图 SRCCOPY:拷贝源位图到目标位图 SRCERASE:反转目标位图并用布尔AND运算符合并这个结果和源位图 SRCINVERT:使用布尔运算符XOR合并目标像素和源位图 SRCPAINT:使用布尔运算符OR合并目标像素和源位图 WHITENESS:所有输出变白
BitBlt函数从源设备上下文拷贝位图到当前设备上下文。应用该函数可以在字节边界 上对齐窗口或客户区域,但要保证BitBlt的操作发生在以字节方式对齐的矩形上(注册窗 口类时设置设备CS_BYTEALLGNWINDOW或CS_BYTE_ALIGHCLIENT 标记)。在以字 节方式对齐矩形时的BitBlt操作比未经字节对齐矩形时的BitBlt操作要快。如果想对自己的 设备上下文指定字节对齐风格,必须注册窗口类而不要依赖Microsoft基本类。可使用全局
趣味程序导学 Visual C++
20
函数AfxRegisterWndClass。 一旦使用目标设备上下文和使用源设备上下文,GDI变为nWidth和nHeight。如果结果 不匹配,必要时GDI使用Windows StretchBlt函数压缩或拉伸源位图。如果目标、源和特征 位图颜色格式不同,BitBlt转换源和特征位图以匹配目标位图。转换中使用目标位图的前 景色和背景色。 BitBlt函数把单色位图转换为彩色时,它设置白色(1)为背景色,黑色 (0)作为前景色,并使用目标设备上下文的背景色和前景色。要把彩色转换为单色, BitBlt把与背景色匹配的像素设置为白色,其余所有像素设置为黑色。在从彩色到单色的 转换中,BitBlt使用彩色设备上下文的前景和背景色。 我们采用函数DrawEdge来绘制图格的边框。该函数可绘制指定风格和类型的矩形。 其原型为: BOOL DrawEdge(LPRECT lpRect,UNIT nEdge,UNIT nFlags);
参数lpRect指向包含有逻辑坐标矩形的RECT结构的指针。nEdge指定矩形内外边界的 类型,该参数是内边界标志和外边界标志的集合。如下所示: (1)内边界标志 ・ BDR_RAISEDINNER:内边界凸出 ・ BDR_SUNKENINNER:内边界凹下 (2)外边界标志 ・ BDR_RAISEDOUTER:外边界凸出 ・ BDR_SUNKENOUTER:外边界凹下 (3)nEdge参数必须是内边界标志和外边界标志的组合可以为以下值之一: ・ ・ ・ ・
EDGE_BUMP,BDR_RAISEDOUTER和BDR_SUNKENINNER的组合 EDGE_ETCHED,BDR_SUNKENOUTER和BDR_RAISEDINNER的组合 EDGE_RAISED,BDR_RAISEDOUTER和BDR_RAISEDINNER的组合 EDGE_SUNKEN,BDR_SUNKENOUTER和BDR_SUNKENINNER的组合
nFlags指定绘制边界的类型,其参数的类型如下: ・ ・ ・ ・ ・ ・ ・ ・ ・
BF_RECT:矩形的四周边界 BF_LEFT:矩形的左边界 BF_BOTTOM:矩形的底部边界 BF_RIGHT :矩形的右边界 BF_TOP:矩形的顶部边界 BF_TOPLEFT:矩形的左底部边界 BF_TOPRIGHT :矩形的右顶部边界 BF_BOTTOMLEFT:矩形的左底部边界 BF_BOTTOMRIGHT :矩形的右底部边界
第4章
拼图游戏——Visual C++位图操作
21
对于对角线,BF_RECT标志指定了矢量终点: ・ BF_DIAGONAL_ENDBOTTOMLEFT:对角线边界。终点为矩形的左下角,始点 为右上角。 ・ BF_DIAGONAL_ENDBOTTOMRIGHT :对角线边界。终点为矩形的右下角,始 点为左上角。 ・ BF_DIAGONAL_ENDTOPLEFT:对角线边界。终点为矩形的左上角,始点为右 下角。 ・ BF_DIAGONAL_ENDTOPRIGHT :对角线边界。终点为矩形的右上角,始点为左 下角。 在程序中,我们提供接口将图格的边框设为平面风格或3D风格两种形式。添加两个 菜单项,如图4.11所示,使得用户可以选择所希望的边框风格。
图 4.11
添加选择边框风格的菜单项
相应的事件处理代码如下: void CPictureDlg::On3D() //3D风格 { pSubMenu=pMainMenu->GetSubMenu(2); pSubMenu->CheckMenuItem(ID_FLAT,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_3D,MF_CHECKED); Style3d=TRUE; SetImage(); } void CPictureDlg::OnFlat() //平面风格 { pSubMenu=pMainMenu->GetSubMenu(2); pSubMenu->CheckMenuItem(ID_FLAT,MF_CHECKED); pSubMenu->CheckMenuItem(ID_3D,MF_UNCHECKED); Style3d=FALSE; SetImage(); }
图4.12和图4.13分别是图格边框的3D风格和平面风格的显示效果。
趣味程序导学 Visual C++
22
图 4.12
图格边框的 3D 风格显示
图 4.13
图格边框的平面风格显示
4.5 图格的移动 在游戏正式开始之前,我们首先要对各图格进行随机排列。为CPictureDlg添加两个成 员函数MapRand和Rnd,分别用来作为随机函数和设定随机图格的位置,相应代码如下: UINT CPictureDlg::MapRand(UINT nMax) //随机函数 { int nRand=rand(); float fMap=(float)nMax/RAND_MAX; float fRetVal=(float)nRand*fMap+0.5f; return (UINT)fRetVal;
第4章
拼图游戏——Visual C++位图操作
23
} void CPictureDlg::Rnd() //随机图格的位置 { int xnum,ynum,level; UINT rand; if(Easy) level=8; else if(!Easy) level=15; WINDOWPLACEMENT wpnum,wp15;
//如果是9格 //如果是16格
for(int a=0;a<=600;a++) { rand=MapRand(4); if(rand==1) for(int b=0;b<=level-1;b++) { m_Image[level].GetWindowPlacement(&wp15); m_Image[b].GetWindowPlacement(&wpnum); xnum=wpnum.rcNormalPosition.left; ynum=wpnum.rcNormalPosition.top; if(wpnum.rcNormalPosition.top==wp15.rcNormalPosition.top && wpnum.rcNormalPosition.left==wp15. rcNormalPosi-tion.left-x) { m_Image[b].SetWindowPos(NULL,xnum+x,ynum,0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0,SWP_NOSIZE); } } if(rand==2) for(int c=0;c<=level-1;c++) { m_Image[level].GetWindowPlacement(&wp15); m_Image[c].GetWindowPlacement(&wpnum); xnum=wpnum.rcNormalPosition.left; ynum=wpnum.rcNormalPosition.top; if(wpnum.rcNormalPosition.top==wp15.rcNormalPosition.top && wpnum.rcNormalPosition.left==wp15. rcNormalPosition.left+x) { m_Image[c].SetWindowPos(NULL,xnum-x,ynum,0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum, ynum,0,0,SWP_NOSIZE); } } if(rand==3) for(int d=0;d<=level-1;d++)
趣味程序导学 Visual C++
24 {
m_Image[level].GetWindowPlacement(&wp15); m_Image[d].GetWindowPlacement(&wpnum); xnum=wpnum.rcNormalPosition.left; ynum=wpnum.rcNormalPosition.top; if(wpnum.rcNormalPosition.left==wp15.rcNormalPosition.left && wpnum.rcNormalPosition.top==wp15. rcNormalPosition.top+y) { m_Image[d].SetWindowPos(NULL,xnum,ynum-y, 0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum, ynum,0,0,SWP_NOSIZE); } } if(rand==4) for(int e=0;e<=level-1;e++) { m_Image[level].GetWindowPlacement(&wp15); m_Image[e].GetWindowPlacement(&wpnum); xnum=wpnum.rcNormalPosition.left; ynum=wpnum.rcNormalPosition.top; if(wpnum.rcNormalPosition.left==wp15. rcNormalPosition.left && wpnum.rcNormalPosition.top==wp15. rcNormalPosition.top-y) {m_Image[e].SetWindowPos(NULL,xnum,ynum+y, 0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0, SWP_NOSIZE); } } } if(Easy) m_Image[8].ShowWindow(SW_HIDE); else if(!Easy) { m_Image[8].ShowWindow(SW_SHOW); m_Image[15].ShowWindow(SW_HIDE); } pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->EnableMenuItem(ID_FORHELP,MF_ENABLED); IsRnd=TRUE; CanCount=TRUE;
第4章
拼图游戏——Visual C++位图操作
}
函数rand用来产生一个伪随机数,它在头文件<stdlib.h>中定义。 用来移动随机图格的代码如下: void CPictureDlg::MoveImage(int num) //移动图格的函数 { int xnum,ynum,level; if(Easy) level=8; else if(!Easy) level=15; WINDOWPLACEMENT wpnum,wp15; m_Image[level].GetWindowPlacement(&wp15); m_Image[num].GetWindowPlacement(&wpnum); xnum=wpnum.rcNormalPosition.left; ynum=wpnum.rcNormalPosition.top; if(num!=level) if(wpnum.rcNormalPosition.top==wp15.rcNormalPosition.top && wpnum.rcNormalPosition.left==wp15. rcNormalPosition.left-x) { m_Image[num].SetWindowPos(NULL,xnum+x,ynum,0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0,SWP_NOSIZE); } if(wpnum.rcNormalPosition.top==wp15.rcNormalPosition.top && wpnum.rcNormalPosition.left==wp15. rcNormalPosition.left+x) { m_Image[num].SetWindowPos(NULL,xnum-x,ynum,0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0,SWP_NOSIZE); } if(wpnum.rcNormalPosition.left==wp15.rcNormalPosition.left && wpnum.rcNormalPosition.top==wp15. rcNormalPosition.top+y) { m_Image[num].SetWindowPos(NULL,xnum,ynum-y,0,0,SWP_NOSIZE); m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0,SWP_NOSIZE); } if(wpnum.rcNormalPosition.left==wp15.rcNormalPosition.left && wpnum.rcNormalPosition.top==wp15. rcNormalPosition.top-y) { m_Image[num].SetWindowPos(NULL,xnum,ynum+y,0,0,SWP_NOSIZE);
25
趣味程序导学 Visual C++
26
m_Image[level].SetWindowPos(NULL,xnum,ynum,0,0,SWP_NOSIZE); } }
现在,我们就可以为Static 控件添加鼠标响应事件了,相应代码如下: void CPictureDlg::OnImage0() //单击Static控件 { if(IsRnd) MoveImage(0); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage1() { if(IsRnd) MoveImage(1); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage2() { if(IsRnd) MoveImage(2); else Rnd(); IsWin(); } void CPictureDlg::OnImage3() { if(IsRnd) MoveImage(3); else Rnd(); IsWin(); } void CPictureDlg::OnImage4() { if(IsRnd) MoveImage(4); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage5() {
第4章
拼图游戏——Visual C++位图操作
if(IsRnd) MoveImage(5); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage6() { if(IsRnd) MoveImage(6); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage7() { if(IsRnd) MoveImage(7); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage8() { if(IsRnd) MoveImage(8); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage9() { if(IsRnd) MoveImage(9); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage10() { if(IsRnd) MoveImage(10); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage11() { if(IsRnd)
27
趣味程序导学 Visual C++
28 MoveImage(11);
else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage12() { if(IsRnd) MoveImage(12); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage13() { if(IsRnd) MoveImage(13); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage14() { if(IsRnd) MoveImage(14); else Rnd(); if(IsWin()) CanCount=FALSE; } void CPictureDlg::OnImage15() { }
单击Static 控件时,程序首先判断图格是否进行了随机数初始化,若是,则移动图 格,否则的话先进行初始化。最后判断游戏是否结束。如图4.14所示是图格进行了随机数 初始化后的图像显示。
第4章
图 4.14
拼图游戏——Visual C++位图操作
进行随机数初始化后的图像显示
4.6 游戏的启动代码 为CPictureDlg添加函数OnBegin作为整个程序的入口函数,相应代码如下: void CPictureDlg::OnBegin() //程序入口函数 { IsRnd=FALSE; CanCount=FALSE; count=0; SetPos(); IsWin(); }
为使程序能够启动,为CPictureDlg的OnInitDialog方法添加代码如下: BOOL CPictureDlg::OnInitDialog() { CDialog::OnInitDialog(); // 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 srand((unsigned)time(NULL)); // TODO: Add extra initialization here bitmap.LoadBitmap(IDB_BITMAP1); pkDC=new CDC; pMainMenu=GetMenu(); SetWindowPos(NULL,0,0,358,346,SWP_NOMOVE);
29
趣味程序导学 Visual C++
30
// TODO: Add extra initialization here OnBegin(); return TRUE; // return TRUE unless you set the focus to a control }
最后,我们给出初始化函数OnRun的完整代码,如下所示: void CPictureDlg::OnRun(UINT nBitmapID,UINT nMenuID) { pSubMenu=pMainMenu->GetSubMenu(1); pSubMenu->CheckMenuItem(ID_PICTURE1,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE2,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE3,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_PICTURE4,MF_UNCHECKED); pSubMenu->CheckMenuItem(ID_ADVAN,MF_UNCHECKED); pSubMenu->CheckMenuItem(nMenuID,MF_CHECKED); count=0; IsLong=FALSE; CanCount=FALSE; IsRnd=FALSE; Advan=FALSE; bitmap.DeleteObject(); bitmap.LoadBitmap(nBitmapID); SetPos(); SetImage(); IsWin(); }
当窗口进行重绘时,我们调用SetImage函数对所有的图格进行重新显示,相应代码如 下: void CPictureDlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangle int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; // Draw the icon dc.DrawIcon(x, y, m_hIcon); }
第4章
拼图游戏——Visual C++位图操作
31
else { CDialog::OnPaint(); SetImage(); } if(pkDC) pkDC->DeleteDC(); }
4.7 游戏完成条件的判断 接下来,添加函数IsWin用来判断游戏是否完成,只要所有的Static 控件都回到了相应 的位置,就可认为用户已完成了拼图,游戏完成,相应代码如下: 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<=con;a++) { m_Image[a].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==0 && wp.rcNormalPosition. left==a*x+move) win+=1; } for(int b=0;b<=con;b++) { m_Image[con+1+b].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==y && wp.rcNormalPosition. left==x*b+move) win+=1; } if(win<3) return FALSE; for(int c=0;c<=con;c++) { m_Image[2*(con+1)+c].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==2*y && wp.rcNormalPosition. left==x*c+move)
趣味程序导学 Visual C++
32 win+=1;
} if(win<5) return FALSE; if(!Easy) for(int d=0;d<=3;d++) { m_Image[12+d].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==3*y && wp.rcNormalPosition. left==x*d+move) win+=1; } if(Easy==TRUE && win==9)
//判断是否完成
{ m_Image[8].ShowWindow(SW_SHOW); m_wndStatusBar.SetText("祝贺你,拼出来了!",0,0); return TRUE; } else if(!Easy && win==16)
//判断是否完成
{ m_Image[8].ShowWindow(SW_SHOW); m_Image[15].ShowWindow(SW_SHOW); m_wndStatusBar.SetText("祝贺你,拼出来了!",0,0); return TRUE; } else return FALSE; }
WINDOWPLACEMENT包含窗口在屏幕上的位置信息,其原型为: typedef struct _WINDOWPLACEMENT { UINT length; UINT flags; UINT showCmd; POINT ptMinPosition; POINT ptMaxPosition; RECT rcNormalPosition; } WINDOWPLACEMENT;
在这里我们主要用到了它的rcNormalPosition属性,它指定了窗口所处的位置。 我们利用API函数GetWindowPlacement来获取Static 控件所处的位置,该函数返回指定 窗口的显示状态及其位置。其原型如下: BOOL GetWindowPlacement
第4章
拼图游戏——Visual C++位图操作
33
( HWND hWnd, // handle to window WINDOWPLACEMENT *lpwndpl // position data );
参数hWnd为窗口的句柄。参数lpwndpl则为指向WINDOWPLACEMENT结构的指针, 该结构存储显示状态和位置信息。 在调用GetWindowPlacement函数之前,要先将WINDOWPLACEMENT 结构的长度设 为 sizeof(WIDNOWPLACEMENT) 。 如 果 lpwndpl->length 设 置 不 正 确 , 则 函 数 GetWindowPlacement将失败。由该函数获得的WINDOWPLACEMENT结构的flag单元总为 0。如果hWnd的窗口被最大化,则showCmd单元为SHOWMAXMIZED,如果窗口被最小 化,则showCmd单元为SHOWMINIMIZED,除此之外为SHOWNORM,WINDOWPLACEMENT长度单元必须置为sizeOf(WINDOWPLACEMENT),如果参数设置不正确,函数 返回FALSE。
4.8
游戏的进一步完善
在上一节中,我们已编写了游戏的主要功能代码,本节我们将进一步完善游戏的界面 和功能。 4.8.1
添加帮助画面
在用户进行游戏的过程中,可能需要查看原图中各图格的位置,以便能够更快地完成 拼图。因此,我们为游戏添加一个帮助画面,使得用户可以在游戏的过程中查看原图的缩 略图。运行后的效果如图4.15所示。
图 4.15
帮助界面的效果图
首先为对话框加入一个GroupBox控件,设置其Caption属性为Help,如图4.16所示。然
34
趣味程序导学 Visual C++
后再加入一个Static 控件(picture control),将其Type属性设为Bitmap。
图 4.16
设置 GroupBox 控件的属性
为图4.3中的“帮助”菜单项添加事件处理,相应代码如下: void CPictureDlg::OnForHelp() //显示略缩图 { ShowItem(); if(!pkDC) pkDC=new CDC; if(pkDC) pkDC->DeleteDC(); SetPre(); } void CPictureDlg::ShowItem() //显示帮助对话框 { INT count; if(Easy) count=7; else count=14; if(!IsHelp) { GetDlgItem(IDC_ENDHELP)->ShowWindow(SW_SHOW); GetDlgItem(IDC_DLGABOUT)->ShowWindow(SW_SHOW); GetDlgItem(IDC_PREVIEW)->ShowWindow(SW_SHOW); GetDlgItem(IDC_WIN)->ShowWindow(SW_SHOW); GetDlgItem(IDC_HIDE)->ShowWindow(SW_SHOW); GetDlgItem(IDC_RECT)->ShowWindow(SW_SHOW); GetDlgItem(IDC_VERSION)->ShowWindow(SW_SHOW); for(int i=0;i<=count;i++) m_Image[i].ShowWindow(SW_HIDE); pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->EnableMenuItem(ID_BEGIN,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_EASY,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_HARD,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_FORHELP,MF_DISABLED|MF_GRAYED); pSubMenu=pMainMenu->GetSubMenu(1); pSubMenu->EnableMenuItem(ID_PICTURE1,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_PICTURE2,MF_DISABLED|MF_GRAYED);
第4章
拼图游戏——Visual C++位图操作
pSubMenu->EnableMenuItem(ID_PICTURE3,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_PICTURE4,MF_DISABLED|MF_GRAYED); pSubMenu->EnableMenuItem(ID_ADVAN,MF_DISABLED|MF_GRAYED); IsHelp=TRUE; } else if(IsHelp) { GetDlgItem(IDC_ENDHELP)->ShowWindow(SW_HIDE); GetDlgItem(IDC_DLGABOUT)->ShowWindow(SW_HIDE); GetDlgItem(IDC_PREVIEW)->ShowWindow(SW_HIDE); GetDlgItem(IDC_WIN)->ShowWindow(SW_HIDE); GetDlgItem(IDC_HIDE)->ShowWindow(SW_HIDE); GetDlgItem(IDC_RECT)->ShowWindow(SW_HIDE); GetDlgItem(IDC_VERSION)->ShowWindow(SW_HIDE); for(int i=0;i<=count;i++) m_Image[i].ShowWindow(SW_SHOW); pSubMenu=pMainMenu->GetSubMenu(0); pSubMenu->EnableMenuItem(ID_BEGIN,MF_ENABLED); pSubMenu->EnableMenuItem(ID_EASY,MF_ENABLED); pSubMenu->EnableMenuItem(ID_HARD,MF_ENABLED); pSubMenu->EnableMenuItem(ID_FORHELP,MF_ENABLED); pSubMenu=pMainMenu->GetSubMenu(1); pSubMenu->EnableMenuItem(ID_PICTURE1,MF_ENABLED); pSubMenu->EnableMenuItem(ID_PICTURE2,MF_ENABLED); pSubMenu->EnableMenuItem(ID_PICTURE3,MF_ENABLED); pSubMenu->EnableMenuItem(ID_PICTURE4,MF_ENABLED); pSubMenu->EnableMenuItem(ID_ADVAN,MF_ENABLED); IsHelp=FALSE; } } void CPictureDlg::SetPre() //生成略缩图 { BITMAP bm; CClientDC dc(&m_Preview); pkDC->CreateCompatibleDC(&dc); if(!Advan) { m_Preview.SetWindowPos(NULL,0,0,136,112,SWP_NOMOVE); pkDC->SelectObject(bitmap); bitmap.GetObject(sizeof(BITMAP),&bm); dc.StretchBlt(0,0,136,112,pkDC,0,0, bm.bmWidth,bm.bmHeight,SRCCOPY); } else if(Advan)
35
趣味程序导学 Visual C++
36 {
INT wei,hei; if(IsLong) { m_Preview.SetWindowPos(NULL,0,0,85,120,SWP_NOMOVE); wei=85; hei=120; } else { wei=136; hei=112; m_Preview.SetWindowPos(NULL,0,0,136,112,SWP_NOMOVE); } ::GetObject(hbitmap,sizeof(BITMAP),&bm); pkDC->SelectObject(hbitmap); dc.StretchBlt(0,0,wei,hei,pkDC,0,0, bm.bmWidth,bm.bmHeight,SRCCOPY); } }
这里,我们使用CDC的StretchBlt函数来实现缩略图的显示,其原型为: BOOL StretchBlt(int x ,int y ,int nWidth,int nHeight,CDC*pSrcDC, Intx Src,int ySrc, intnSrcWidth, int nSrcHeight.DWORD dwRop)
该函数将源矩形中的位图拷贝到目标矩形中,如果有必要,可以扩展或压缩该位图使 其与目标矩形尺寸吻合。函数使用目标设备上下文(由SetStretchBltMode设置)的扩展模 式来决定如何扩展或压缩位图。StretchBlt 函数将pSrcDC源设备中的位图移动到目标矩 形。xSrc,ySrc,nSrcWidth和nSrcHeight 参数定义了源矩形的左上角和尺寸。x,y, nWidth和nHeight参数定义了目标矩形的左上角和尺寸。dwRop指定的光栅操作模式说明了 源位图与目标设备上已经存在的位图是如何组合的。如果nSrcWidth和nWidth或nSrcHeight 和nHeight的符号不同,StretchBlt 将为位图创建一个镜像。如果nSrcWidth和nWidth符号不 同,函数沿X轴创建镜像。如果 nSrcHeight和nHeight符号不同,函数沿Y轴创建镜像。 StretchBlt函数在内存中对源位图进行扩展或压缩,然后将结果拷贝到目标矩形中。如果模 板要与结果组合,则在扩展后的位图拷贝到目标矩形后才进行组合。如果用到画刷,应使 用目标设备上下文中选定的画刷。目标坐标根据目标设备上下文来转换,源坐标根据源设 备上下文来转换。如果目标位图、源位图和模板位图的格式不一致,StretchBlt把模板与源 位图转换成模板位图格式,转换中会使用到目标设备上下文中的前景色和背景色。如果要 将黑白位图转换为彩色位图,它将背景色设置为白(1),前景色设置为黑(0)。如果要 将彩色位图转换为黑白位图,函数设置与背景色匹配的像素为白( 1),其他像素为黑 (0),其中用到了带颜色的设备上下文中的前景色和背景色。 4.8.2
用Status Bar显示提示信息
为了使用户能够按照正确的步骤完成游戏,我们利用Status Bar (状态栏控件)来显 示系统的提示信息。
第4章
拼图游戏——Visual C++位图操作
37
一个“状态栏控件”是一个水平窗口,通常显示在父窗口的底部,在其中应用程序可 以显示不同类型的状态信息。可以将此状态栏控件分割为几部分,用来显示多种类型的信 息。MFC中使用CStatusBar类来封装状态栏控件的各种操作。而CStatusBarCtrl类则提供了 Windows通用状态栏控件的性能。 为 CPictureDlg 添 加 一 个 CStatusBarCtrl 类 型 的 变 量 m_wndStatusBar , 并 为 OnInitialDialog添加相应的代码如下: BOOL CPictureDlg::OnInitDialog() { CDialog::OnInitDialog(); // 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 srand((unsigned)time(NULL)); // TODO: Add extra initialization here bitmap.LoadBitmap(IDB_BITMAP1); pkDC=new CDC; pMainMenu=GetMenu(); SetWindowPos(NULL,0,0,358,346,SWP_NOMOVE); SetTimer(10,1000,NULL); // TODO: Add extra initialization here m_wndStatusBar.Create(WS_CHILD|WS_VISIBLE|CCS_BOTTOM|CCS_NODIVIDER, CRect(0,0,0,0),this,102); m_wndStatusBar.SetParts(3,strPartDim); m_wndStatusBar.SetText(TimeCon,1,0); m_wndStatusBar.SetText("Computer74",2,0); OnBegin(); return TRUE;// return TRUE unless you set the focus to a control }
构造一个CStatusBarCtrl对象可以分两步。首先调用构造函数,然后调用Create来创建 状态栏控件并将它与CStatusBarCtrl对象连接。CStatusBarCtrl::Create函数的原型为: BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);
(1)参数dwStyle指定状态栏控件的风格。状态栏控件风格的任意组合都适用于这个 控件。这个参数必须包括WS_CHILD风格,它也必须包括WS_VISIBLE风格。 (2)rect指定状态栏控件的大小和位置,它可以是一个CRect对象或一个RECT结构。 (3)pParentWnd指定状态栏控件的父窗口,通常是一个CDialog,它不能是NULL。 (4)nID指定状态栏控件的ID。 其中,dwStyle参数可以是下列值的任意组合: ・ CCS_BOTTOM:使控件将它自己定位在父窗口的客户区的底端,并将宽度设置为 与父窗口的宽度一样。状态栏控件将此作为它的默认风格;
38
趣味程序导学 Visual C++
・ CCS_NODIVIDER:禁止在控件的顶部绘制两个像素的高亮区; ・ CCS_NOHILITE:禁止在控件的顶部绘制一个像素的高亮区; ・ CCS_NOMOVEY:使控件响应WM_SIZE消息,调整其本身的大小并水平方向移 动,但不是垂直移动。如果使用了CCS_NORESIZE风格,则此风格不能使用; ・ CCS_NOPARENTALIGE:禁止控件自动移动到父窗口的顶部或底部。不管控件 的父窗口的尺寸怎么改变,控件都保持它在父窗口中的位置。如果也使用了 CCS_TOP和CCS_BOTTOM风格,则高度被调整为默认值,但位置和宽度仍然保 持不变; ・ CCS_NORESIZE:当控件设置它本身的初始尺寸和新尺寸时,禁止控件使用默认 的宽度和高度,而使用在创建或调整大小的请求中指定的宽度和高度; ・ CCS_TOP:使控件将自己定位在其父窗口的顶部,并将自己的宽度设置为与父窗 口的宽度一样。一个状态窗口的默认位置是沿着父窗口的底部,但是你也可以指 定 CCS_TOP 风 格 来 使 它 显 示 在 父 窗 口 客 户 区 的 顶 部 。 还 可 以 指 定 SBARS_SIZEGRIP风格来使它包括一个位于状态窗口右端的调整大小的句柄。并 不建议组合CCS_TOP和SBARS_SIZEGRIP风格,因为这样获得的调整大小句柄是 没有用的,尽管系统将它绘制在了状态栏窗口中。 状态栏对象成功创建之后,我们调用CStatusBarCtrl的SetParts函数来设置要把控件分 为几部分,以及每一部分的右边缘的坐标。其原型为: BOOL SetParts(int nParts, int* pWidths);
参数nParts为要设置的部分的数目,不能大于255。pWidths是一个整数数组的地址, 该数组的元素个数与nParts指定的数目一样。数组中的每一个元素以客户区坐标指定了相 应部分的右边缘位置。如果某一个元素是-1,则相应部分的右边缘位置扩展到了该控件的 右边缘。 在这里,我们使用数组strPartDim来指定每一部分的右边缘坐标,其定义如下: int static strPartDim[4]= {160, 260, 350, -1};
最后,我们使用SetText函数来设置状态栏每一部分的文字。此成员函数用来在一个状 态栏控件的给定部分中设置文本。当控件下一次接收到WM_PAINT消息时,表示控件的 这一部分应该被改变,这时将导致该部分显示新的文本。函数原型为: BOOL SetText(LPCTSTR lpszText, int nPane, int nType);
其中,参数lpszText是一个以空字符结尾的字符串的地址,该字符串指定了要设置的 文本。如果nType是SBT_OWNERDRAW,则lpszText表示32位的数据。 相应的,更新另外几个函数,以在状态栏显示提示信息,代码如下: void CPictureDlg::OnBegin() { IsRnd=FALSE; CanCount=FALSE;
第4章
拼图游戏——Visual C++位图操作
count=0; SetPos(); IsWin(); m_wndStatusBar.SetText("加油!",0,0); } void CPictureDlg::OnEasy() { pSubMenu=pMainMenu->GetSubMenu(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<=con;a++) { m_Image[a].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==0 && wp.rcNormalPosition.left==a*x+move) win+=1; } for(int b=0;b<=con;b++) {
39
40
趣味程序导学 Visual C++ m_Image[con+1+b].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==y&&wp.rcNormalPosition.left ==x*b+move) win+=1; } if(win<3) return FALSE; for(int c=0;c<=con;c++) { m_Image[2*(con+1)+c].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==2*y &&wp.rcNormalPosition.left ==x*c+move) win+=1; } if(win<5) return FALSE; if(!Easy) for(int d=0;d<=3;d++) { m_Image[12+d].GetWindowPlacement(&wp); if(wp.rcNormalPosition.top==3*y &&wp.rcNormalPosition.left ==x*d+move) win+=1; } if(Easy==TRUE && win==9) //判断是否完成 { m_Image[8].ShowWindow(SW_SHOW); m_wndStatusBar.SetText("祝贺你,拼出来了!",0,0); return TRUE; } else if(!Easy && win==16) //判断是否完成 { m_Image[8].ShowWindow(SW_SHOW); m_Image[15].ShowWindow(SW_SHOW); m_wndStatusBar.SetText("祝贺你,拼出来了!",0,0); return TRUE; } else return FALSE; } void CPictureDlg::OnPic1() //显示图片 { OnRun(IDB_BITMAP1,ID_PICTURE1); m_wndStatusBar.SetText("Duck(小鸭)",0,0); } void CPictureDlg::OnPic2() //显示图片 { OnRun(IDB_BITMAP2,ID_PICTURE2); m_wndStatusBar.SetText("HelloKitty(My Love!)",0,0); } void CPictureDlg::OnPic3()
第4章
拼图游戏——Visual C++位图操作
41
//显示图片 { OnRun(IDB_BITMAP3,ID_PICTURE3); m_wndStatusBar.SetText("加菲猫",0,0); } void CPictureDlg::OnPic4() //显示图片 { OnRun(IDB_BITMAP4,ID_PICTURE4); m_wndStatusBar.SetText("美丽的女孩(I Love)",0,0); }
4.8.3
游戏计时器的加入
下面我们为程序加入一个计时器来记录用户完成游戏的时间。 为CPictureDlg加入一个CString类型的变量TimeCon,用来显示用户所花费的时间。并 为其加入OnTimer事件响应,相应代码如下: void CPictureDlg::OnTimer(UINT nIDEvent) { nIDEvent=10; CString m_temp = "计时:"; if(CanCount==TRUE) count+=1; TimeCon=IntToString(count); TimeCon =m_temp+" "+TimeCon+" " "秒"; m_wndStatusBar.SetText(TimeCon,1,0); }
这样,在用户进行游戏的过程中,就可同时记录其所花费的时间了,如图4.17所示。
图 4.17
显示显示用户完成游戏所用时间
趣味程序导学 Visual C++
42
本章知识点回顾
4.9 用代码操纵菜单
pMainMenu = GetMenu();//得到对话框主菜单的指针 CMenu* GetSubMenu(int nPos);// 得到菜单项的指针 BOOL AppendMenu(UINT nFlags, UINT nIDNewItem = 0, LPCTSTR lpszNewItem = NULL);// 在末尾添加菜单项 BOOL InsertMenu(UINT nPosition, UINT nFlags, UINT nIDNewItem =0, LPCTSTR lpszNewItem = NULL); // 在指定位置添加菜单项 BOOL ModifyMenu(UINT nPosition, UINT nFlags, UINT nIDNewItem =0, LPCTSTR lpszNewItem = NULL); // 修改某一位置的菜单项 BOOL RemoveMenu(UINT nPosition, UINT nFlags); // 删除某一位置的菜单项 UINT CheckMenuItem(UINT nIDCheckItem, UINT nCheck); // 返回菜单项以前的状态
续表 Windows位图文件的基
位图文件头:
本结构
typedef struct tagBITMAPFILEHEADER { WORD bfType; // 位图文件的类型,必须为BM DWORD bfSize; // 位图文件的大小,以字节为单位 WORD bfReserved1; // 位图文件保留字,必须为0 WORD bfReserved2; // 位图文件保留字,必须为0 DWORD bfOffBits; // 位图数据的起始位置,以相对于位图 // 文件头的偏移量表示,以字节为单位 } BITMAPFILEHEADER; 位图信息头: typedef struct tagBITMAPINFOHEADER { DWORD biSize; // 本结构所占用字节数 LONG biWidth; // 位图的宽度,以像素为单位 LONG biHeight; // 位图的高度,以像素为单位 WORD biPlanes; // 目标设备的级别,必须为1 WORD biBitCount // 每个像素所需的位数,必须是1(双色), // 4(16色),8(256色)或24(真彩色)之一 DWORD biCompression; // 位图压缩类型,必须是 0(不压缩), // 1(BI_RLE8压缩类型)或2(BI_RLE4压缩 // 类型)之一
第4章
拼图游戏——Visual C++位图操作
43
DWORD biSizeImage; // 位图的大小,以字节为单位 LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数 LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数 DWORD biClrUsed; // 位图实际使用的颜色表中的颜色数 DWORD biClrImportant; // 位图显示过程中重要的颜色数 } BITMAPINFOHEADER; 颜色表: typedef struct tagRGBQUAD { BYTE rgbBlue; // 蓝色的亮度(值范围为0-255) BYTE rgbGreen; // 绿色的亮度(值范围为0-255) BYTE rgbRed; // 红色的亮度(值范围为0-255) BYTE rgbReserved;// 保留,必须为0 } RGBQUAD; 位图资源的读入
可通过LoadBitmap方法载入新的位图对象。LoadBitmap函数的原型为: BOOL LoadBitmap(LPCTSTR lpszResourceName); BOOL LoadBitmap(UINT nIDResource); 参数lpszResourceName指向一个包含了位图资源名字的字符串(该字符串以 null结尾),而nIDResource指定位图资源的ID号。
续表 自定义位图文件的读
可用LoadImage方法来加载自定义文件中的位图资源,然后用GetObject方法
入
得到其BITMAP对象,函数LoadImage的原型为: HANDLE LoadImage ( HINSTANCE hinst,
// handle to instance
LPCTSTR lpszName,
// name or identifier of the image
UINT uType, int cxDesired, int cyDesired, UINT fuLoad ); SetWindowsPos函数
// image type // desired width // desired height // load options
可用API函数 SetWindowsPos来设置控件的位置,其原型为: BOOL SetWindowPos ( HWND hWnd,
// handle to window
HWND hWndInsertAfter,
// placement-order handle
int X, int Y,
// horizontal position // vertical position
int cx,
// width
趣味程序导学 Visual C++
44
int cy, UINT uFlags ); DDB位图的显示
// height // window-positioning options
在窗口中显示DDB的方法有些特别,其过程分以下几步: (1)构建一个CDC对象,然后调用CDC::CreateCompatibleDC创建一个兼容 的内存设备上下文: virtual BOOL CreateCompatibleDC(CDC*pDC) (2)调用CDC::SelectObject将DDB选入内存设备上下文中。 (3)调用CDC::BitBlt或CDC::StretchBlt将DDB从内存设备上下文中输出到窗 口的设备上下文中: BOOL BitBlt(int x,int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, intySrc, DWORD dwRop); BOOL StretchBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop); (4)调用CDC::SelectObject 把原来的 DDB 选入到内存设备上下文中并使新 DDB脱离出来。 (5)调用CDC::DrawEdge来绘制边框: BOOL DrawEdge(LPRECT lpRect,UNIT nEdge,UNIT nFlags); 续表
WINDOWPLACEMEN
结构WINDOWPLACEMENT包含了一个窗口在屏幕上的位置信息,其原型
T结构
为: typedef struct _WINDOWPLACEMENT { UINT length; UINT flags; UINT showCmd; POINT ptMinPosition; POINT ptMaxPosition; RECT rcNormalPosition; } WINDOWPLACEMENT;
GetWindowPlacement
可用API函数GetWindowPlacement来取得Static控件所处的位置,该函数返回
函数
指定窗口的显示状态及其位置。其原型如下: BOOL GetWindowPlacement ( HWND hWnd, WINDOWPLACEMENT *lpwndpl );
随机函数
随机数初始化:
// handle to window // position data
第4章
拼图游戏——Visual C++位图操作
45
srand((unsigned)time(NULL)); 产生伪随机数: rand(); 用StatusBar显示提示信
调用CStatusBarCtrl::Create来创建状态栏控件并将它与CStatusBarCtrl对象连
息
接: BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID); 调用CStatusBarCtrl::SetParts来设置要把控件分为几部分,以及每一部分的右 边缘的坐标: BOOL SetParts(int nParts, int* pWidths); 调用CStatusBarCtrl::SetText 来设置状态栏每一部分的文字: BOOL SetText(LPCTSTR lpszText, int nPane, int nType);
第5章
媒体播放器——多媒体程序设计
媒体一词的英文为“media”,意思为媒介、方法和手段。所谓多媒体即是以多种媒 体形式——文字、图形、声音、动画和图像来传播信息。随着多媒体技术的迅猛发展和 PC性能的大幅度提高,在P C机上运行的应用程序越来越多地采用了多媒体技术。如果你 编写的应用程序能够发出美妙的声音,播放有趣的动画,无疑将会给人留下深刻的印象。 特别是音乐,可以使用户心情愉快,在合适的场合播放恰当的音乐能够使程序员和他编写 的VC++程序焕发光彩。Windows 9x/NT提供了丰富的多媒体服务功能,包括大量从低级 到高级的多媒体API函数。利用这些功能强大的API,用户可以在不同层次上编写多媒体 应用程序。媒体播放器可以让用户播放Windows下常用的音频和视频文件,以达到娱乐和 欣赏的目的。本章我们通过编写一个媒体播放器的程序,向读者介绍一些最常用的多媒体 服务。
5.1
程序效果说明
媒体播放器可以播放Windows下常用的音频和视频文件,如MIDI、Wave、AVI等。 程序的界面如图5.1所示。用户可通过文件对话框打开想要播放的文件,并可实现暂停、 关闭或重播的功能,对打开的Wave文件还可以进行录音,并保存新文件。播放CD时,可 用程序打开或关闭光驱的门,如图5.2所示。
图 5.1
媒体播放器的界面
在播放音频文件时,程序用动画的方式显示柱状音响效果,并显示播放的时间和正在 播放文件的全路径和文件名,如图5.3所示。同时,在媒体文件播放的过程中,用户可随 时调节音量。当播放视频文件时,程序自动弹出另外一个窗口进行显示,如图5.4所示。
趣味程序导学 Visual C++
104
图 5.2
图 5.3
音响效果、时间和文件名的显示
图 5.4
5.2
程序的控制菜单
视频文件的播放
创建初始界面程序
首先新建一个工程,命名为Multimedia,然后选择Dialog based,设置对话框的Caption 属性为“媒体播放器”,切换到Styles标签,选中Minimize box,如图5.5所示,这样,在 对话框的右上角就会出现最小化按钮。
第5章
媒体播放器——多媒体程序设计
图 5.5
105
添加对话框最小化按钮
不选中对话框的“确定”和“取消”两个按钮的Visible属性,如图5.6所示。这样, 在程序运行的时候,这两个按钮就不会出现了。
图 5.6
5.2.1
不选中按钮的 Visible 属性
在按钮上显示位图
为对话框添加5个按钮(Button),在Styles标签中选中Bitmap属性,如图5.7所示。将 它们的ID分别设为ID_PLAY,ID_STOP,ID_PAUSE,ID_OPEN和ID_EXIT,以控制媒体 文件的播放。将5幅准备好的位图加入资源,其ID分别设为IDB_PLAY,IDB_STOP, IDB_PAUSE,IDB_OPEN和IDB_EXIT,分别和5个按钮相对应。然后,在 ClassWizard里 为每个按钮分别添加一个 CButton型的变量,分别命名为 m_playButton、m_stopButton、 m_pauseButton、m_openButton和m_exitButton,以对按钮进行操作。
图 5.7
设置按钮的 Bitmap 属性
在这里,我们使用CButton类的SetBitmap方法来为按钮设置位图,其原型为: HBITMAP SetBitmap(HBITMAP hBitmap);
参数hBitmap是位图的句柄,该函数返回此前在按钮上设置的位图的句柄。
趣味程序导学 Visual C++
106
M
注意:本函数用于为按钮设置一个新的位图。位图将会被自动地放到按钮的 表面,默认时居中放置。如果位图太大,多余部分会被剪去。可选择的对齐 方式有: ・ ・ ・ ・ ・ ・
BS_TOP BS_LEFT BS_RIGHT BS_CENTER BS_BOTTOM BS_VCENTER
用 过 CBitmapButton 的 读 者 可 以 发 现 , CBitmapButton 对 象 可 以 有 4 个 位 图 , 而 SetBitmap只为每个按钮设置一个位图。 我们在对话框初始化的时候为按钮设置位图,在 CMultimediaDlg 的OnInitDialog响应 函数中添加代码如下: BOOL CMultimediaDlg::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 tomatically // when the application's main window is not a dialog SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon // TODO: Add extra initialization here
第5章
媒体播放器——多媒体程序设计
107
m_hBmp = ::LoadBitmap(HINSTANCE(GetModuleHandle(0)), MAKEINTRESOURCE(IDB_PLAY)); m_playButton.SetBitmap(m_hBmp); m_hBmp = ::LoadBitmap(HINSTANCE(GetModuleHandle(0)), MAKEINTRESOURCE(IDB_STOP)); m_stopButton.SetBitmap(m_hBmp); m_hBmp = ::LoadBitmap(HINSTANCE(GetModuleHandle(0)), MAKEINTRESOURCE(IDB_OPEN)); m_openButton.SetBitmap(m_hBmp); m_hBmp= ::LoadBitmap(HINSTANCE(GetModuleHandle(0)), MAKEINTRESOURCE(IDB_PAUSE)); m_pauseButton.SetBitmap(m_hBmp); m_hBmp = ::LoadBitmap(HINSTANCE(GetModuleHandle(0)), MAKEINTRESOURCE(IDB_EXIT)); m_exitButton.SetBitmap(m_hBmp); return TRUE; // return TRUE unless you set the focus to a control }
5.2.2
菜单项位图的显示
为工程添加菜单资源,并设置菜单项,如图5.8和图5.9所示。这里包括了程序可实现 的全部功能。添加两个CMenu型的指针变量,代码如下: CMenu* pSubMenu; CMenu* pMainMenu;
利用这两个变量来实现对菜单的动态操作。
图 5.8
程序菜单项
图 5.9
程序菜单项
添加3个位图资源,其ID分别设置为IDB_MOPEN、IDB_MSAVE和IDB_EXIT,在 OnInitDialog中添加代码如下:
趣味程序导学 Visual C++
108
pMainMenu = GetMenu(); pSubMenu = pMainMenu->GetSubMenu(0); m_bmp[0].LoadBitmap(IDB_MOPEN); pSubMenu->SetMenuItemBitmaps(IDC_OPEN,MF_BYCOMMAND,&m_bmp[0],&m_bmp[0]); m_bmp[1].LoadBitmap(IDB_MSAVE); pSubMenu->SetMenuItemBitmaps(ID_MENUITEM_SAVE,MF_BYCOMMAND,&m_bmp[1], &m_bmp[1]); m_bmp[2].LoadBitmap(IDB_MEXIT); pSubMenu->SetMenuItemBitmaps(IDC_EXIT,MF_BYCOMMAND,&m_bmp[2],&m_bmp[2]);
这里,我们用SetMenuItemBitmaps方法来为菜单项设置位图,函数原型为: BOOL SetMenuItemBitmaps(UINT nPosition, UINT nFlags, const CBitmap* pBmp Unchecked,const CBitmap* pBmpChecked);
参数nPosition指定将要修改的菜单项。nFlags 指定如何解释nPosition的 值 。pBmp Unchecked指定位图用于不选中的菜单项。pBmpChecked指定位图用于将被选中的菜单 项。如果成功,则返回非0值,否则为0。
F
说明:将指定的位图与菜单项相关联。无论菜单是否被选中,Windows都将 显示紧跟菜单项的适当的位图。若pBmpUnchecked或pBmpChecked为NULL,那 么Windows将不显示相应属性菜单项的任何内容。若这两个参数都为NULL, 那么Windows在菜单项被选中时将使用默认的选中标记,在菜单项不被选中 时,就将删除选中标记。当菜单被撤消后,这些位图将不会被撤消,必须利 用应用程序删除它们。Windows GetMenuCheckMarkDimensions函数将获取菜 单项使用默认选中标记的维数。应用程序使用这些值来决定该函数提供的位 图的适当大小,获取该大小值,创建自己的位图,然后进行设置。
5.2.3
对话框背景图的添加
下面我们为对话框添加背景图,以增加界面的美观性。首先为工程添加一幅位图资 源,以作为对话框的背景。然后加入一个Static 控件,将其Type属性设为Bitmap,并为 Image属性选择位图资源,如图5.10所示。
图 5.10
设置 Static 控件的 Type 属性
参数设置之后对话框的外观如图5.11所示。
第5章
媒体播放器——多媒体程序设计
图 5.11
5.3
109
用 Static 控件显示背景图
媒体播放类的创建
在上一节中,我们已经创建了程序的初始界面。本节我们将创建自己的媒体播放类, 用来实现对MIDI,Wave,CD、AVI等Windows标准媒体文件的播放和控制。 设 计 多 媒 体 程 序 , 关 键 是 对 多 种 媒 体 设 备 的 控 制 和 使 用 , 在 Windows 95/98 和 Windows NT/2000系统中,对多媒体设备进行控制主要有三种方法:第一种方法是使用微 软公司窗口系统中对多媒体支持的MCI,即媒体控制接口,MCI是多媒体设备和多媒体应 用软件之间的通信桥梁。在VB和VC中,MCI都得到了很好的支持。第二种方法,通过调 用Windows的API(应用编程接口)多媒体相关函数实现媒体控制。第三种方法是使用 OLE(Object Linking & Embedding,对象链接与嵌入技术),它为不同软件之间共享数 据和资源提供了有力的手段。在本节里,我们将采用Windows MCI来实现对多媒体设备的 控制。 5.3.1
高级音频函数
在讲述MCI编程之前,我们首先介绍一下Windows提供的高级音频函数。Windows提 供了三个特殊的播放声音的高级音频函数:MessageBeep,PlaySound和sndPlaySound。这 三个函数可以满足播放波形声音文件的一般需要,但它们播放的Wave文件(波形声音文 件)的大小不能超过100KB,如果要播放较大的文件,则应该使用MCI服务。 MessageBeep主要用来播放系统报警声音。系统报警声音是由用户在控制面板中的声 音(Sounds)程序中定义的,或者在WIN.INI的[sounds]段中指定。该函数的声明为: BOOL MessageBeep(UINT uType);
参数uType说明了报警声音的类型,如表5.1所示。若成功则函数返回TRUE。
趣味程序导学 Visual C++
110
表5.1
报警声音的类型
类型
描述
0xFFFFFFFF(-1)
从机器的扬声器中发出蜂鸣声
MB_ICONASTERISK
播放由SystemAsterisk定义的声音
MB_ICONEXCLAMATION
播放由SystemExclamation定义的声音
MB_ICONHAND
播放由SystemHand定义的声音
MB_ICONQUESTION
播放由SystemQuestion定义的声音
MB_OK
播放由SystemDefault定义的声音
在开始播放后,MessageBeep函数立即返回。如果该函数不能播放指定的报警声音, 它就播放SystemDefault定义的系统默认声音,如果连系统默认声音也播放不了,那么它就 会在计算机的扬声器上发出嘟嘟声。在默认时上表的MB_系列声音均未定义。 MessageBeep 只 能 用 来 播 放 少 数 定 义 的 声 音 , 如 果 程 序 需 要 播 放 数 字 音 频 文 件 (*.WAV文件)或音频资源,就需要使用PlaySound或sndPlaySound函数。 PlaySound函数的原型为: BOOL WINAPI PlaySound ( LPCSTR pszSound, HMODULE hmod, DWORD
fdwSound
);
参数pszSound 指定了要播放声音的字符串,该参数可以是 WAVE文件的名字,或是 WAV资源的名字,或是内存中声音数据的指针,或是在系统注册表WIN.INI中定义的系统 事件声音。如果该参数为NULL则停止正在播放的声音。参数hmod是应用程序的实例句 柄,当播放WAV资源时要用到该参数,否则它必须为NULL。参数fdwSound是各种标志 的组合,见表5.2。若成功则函数返回TRUE,否则返回FALSE。 表5.2
播放标志
标志
含义
SND_APPLICATION
用应用程序指定的关联来播放声音
SND_ALIAS
pszSound参数指定了注册表或WIN.INI中的系统事件的别名
SND_ALIAS_ID
pszSound参数指定了预定义的声音标识符
SND_FILENAME
pszSound参数指定了WAVE文件名
SND_LOOP
重复播放声音,必须与SND_ASYNC标志一块使用
SND_MEMORY
播放载入到内存中的声音,此时pszSound是指向声音数据的指针
SND_NODEFAULT
不播放默认声音,若无此标志,则PlaySound在没找到声音时会播放默认声 音
SND_NOSTOP
PlaySound不停止原来的声音播出并立即返回FALSE
第5章
媒体播放器——多媒体程序设计
111 续表
标志
含义
SND_NOWAIT
如果驱动程序正忙,则函数就不播放声音并立即返回
SND_RESOURCE
pszSound参数是WAVE资源的标识符,这时要用到hmod参数
SND_SYNC
同步播放声音,在播放完后PlaySound函数才返回
SND_PURGE
停止所有与调用任务有关的声音。若参数pszSound为NULL,就停止所有的 声音,否则,停止pszSound指定的声音
在C:\WINDOWS\MEDIA目录下有一个名为The Microsoft Sound.wav的声音文件,在 Windows 95/98启动时会播放这个声音文件。下面我们用三种方法来调用PlaySound函数播 放Windows 95/98的启动声音。 第一种方法是直接播放声音文件,相应的代码为: PlaySound("c:\\Windows\\media\\The Microsoft Sound.wav", NULL, SND_FILENAME | SND_ASYNC);
注意参数中的路径使用两个连续的反斜杠转义代表一个反斜杠。 第二种方法是把声音文件加入到资源中,然后从资源中播放声音。Visual C++支持 Wave型资源,用户在资源视图中单击鼠标右键并选择Import命令,然后在文件选择对话框 中选择The Microsoft Sound.wav文件,则该文件就会被加入到Wave资源中。假定声音资源 的ID为IDR_STARTWIN,则下面的调用同样会输出启动声音: PlaySound((LPCTSTR)IDR_STARTWIN, AfxGetInstanceHandle(), SND_RESOURCE | SND_ASYNC);
第三种方法是用PlaySound播放系统声音,Windows启动的声音是由SystemStart定义的 系统声音,因此可以用下面的方法播放启动声音: PlaySound("SystemStart",NULL,SND_ALIAS|SND_ASYNC);
函数sndPlaySound的功能与PlaySound类似,但少了一个参数。函数的声明为: BOOL sndPlaySound(LPCSTR lpszSound, UINT fuSound);
除了不能指定资源名字外,参数lpszSound与PlaySound是一样的。参数fuSound是如何 播放声音的标志,可以是SND_ASYNC,SND_LOOP,SND_MEMORY,SND_NODEFAULT,SND_NOSTOP和SND_SYNC的组合,这些标志的含义与PlaySound中的一样。 可以看出,sndPlaySound不能直接播放声音资源。要用该函数播放WAVE文件,可按 下面的方式调用: sndPlaySound("MYSOUND.WAV",SND_ASYNC);
5.3.2 Windows MCI与多媒体软件开发 Windows MCI(Media Control Interface)是控制多媒体设备的高层命令接口,提供了
趣味程序导学 Visual C++
112
与设备无关的控制多媒体设备的方法。MCI可控制的多媒体设备包括标准的多媒体设备, 如CD音频(CD Audio)、数字视频、动画、Wave格式数字声音和MIDI音序器,以及影碟 机等可选设备。 MCI包含在Windows系统的MMSYSTEM.DLL动态链接库中,用以协调多媒体事件和 MCI设备驱动程序之间的通信。一些MCI设备驱动程序,影碟机设备驱动程序,可以直接 控 制 目 标 设 备 ; 另 外 一 些 MCI 设 备 驱 动 程 序 , 如 Wave 和 MIDI 设 备 驱 动 程 序 , 通 过 MMSYSTEM中的函数间接控制目标设备;还有一些MCI 设备驱动程序则提供了与其他 Windows动态链接库的高层接口。 MCI提供两种不同但相互联系的接口方式。第一种方式利用消息和数据结构来给多媒 体设备发送命令并接收设备传来的信息,这种方法用函数mciSendCommand来给设备发送 命令。其原型为: MCIERROR mciSendCommand ( MCIDEVICEID UINT
IDDevice, uMsg,
DWORD
fdwCommand,
DWORD );
dwParam
参数IDDevice表示用来接收命令的设备的标识符,注意在打开设备时不用该参数; uMsg是要发送的命令;fdwCommand是命令消息的标志,而参数dwParam则是一个指向包 含 命 令 消 息 的 参 数 的 结 构 。 如 该 参 数 返 回 非0 值 , 则 表 示 设 备 驱 动 出 错 ,这时可用 mciGetErrorString函数来取得错误信息,其原型为: BOOL mciGetErrorString ( DWORD
fdwError,
LPTSTR
lpszErrorText,
UINT );
cchErrorText
参 数 fdwError 是 由 mciSendCommand 或 mciSendString 返 回 的 错 误 代 码 ; 参 数 lpszErrorText是一个指针,它指向用来存储错误描述信息的缓冲区;参数cchErrorText表示 缓冲区的长度,以字符为单位。 MCI接口的第二种方式是使用ASCII字符串来发送驱动设备的命令,这种方式采用函 数mciSendString把命令字符发送给设备。其原型为: MCIERROR mciSendString ( LPCTSTR lpszCommand, LPTSTR UINT
lpszReturnString, cchReturn,
第5章 HANDLE
媒体播放器——多媒体程序设计
113
hwndCallback
);
参数lpszCommand是要发送的MCI命令字符串;参数lpszReturnString是一个指向接收 返回信息的缓冲区的指针;cchReturn表示缓冲区的长度,以字符为单位;hwndCallback是 回调窗口的句柄,一般为NULL。 这 种 字 符 串 命 令 很 直 观 方 便 , 近 似 自 然 语 言 , 如 “ play cdaudio ” , “ stop waveaudio”等。返回的信息字符串由lpszReturnString带回,如该函数返回非0值,同样可 用mciGetErrorString获取错误信息。 MCI 还包括一套多媒体应用编程接口MAPI (Multimedia Application Programming Interface)及一系列的多媒体函数、消息和数据结构。这些函数包括下列几类: ・ ・ ・ ・ ・ ・ ・ ・
高层声音服务函数 低层Wave形式声音服务函数 低层MIDI服务函数 辅助声音服务函数 特殊格式文件I/O服务函数 游戏杆服务函数 定时器服务函数 查错服务函数
媒体控制接口允许控制两类设备:第一类为简单设备,是指那些不需要文件的设备, 如CD音频播放器、视盘演播器等;第二类为复合设备,是指那些需要文件的设备,如数 字视频及波形音频设备等。 表5.3列出了MCI的设备类型。 表5.3
MCI设备类型
设备类型
说明
Animation
动画播放设备
Cdaudio
CD音响播放设备
digitalvideo
Windows用视频
Other
未定义的MCI设备
Overlay
窗口中的模拟设备
Sequencer
乐器数字接口(MIDI)音序器
Videodisk
视频演播设备
waveaudio
数字波形音频设备
还有一些其他设备由于不常用,所以在此不作介绍。
M
注意:设备类型和设备名是不同的概念。设备类型是指响应一组共用命令的 一类MCI设备,而设备名则是某一个MCI设备的名字。系统需要用不同的设备
趣味程序导学 Visual C++
114
名来区分属于同一设备类型的不同设备。 设备是在Windows注册表的SYSTEM.INI中[MCI]段描述里找到,通常在安装新的多媒 体设备时,安装程序都会更新SYSTEM.INI,并添加新的驱动程序。典型的SYSTEM.INI 如下: [MCI] WaveAudio = mciwave.drv Sequencer = mciseq.drv CDAudio = mcicda.drv
符号“=”的左边表示设备名,右边表示控制此多媒体设备所需的驱动程序。每一种 设备类型都有其惟一的识别名称。如果系统中安装了多个同一类别的多媒体控制设备,例 如有2个光盘驱动器时,则在再次安装同样的驱动程序后,SYSTEM.INI会增加一个设备类 型,新的设备名将与原有的名称相同,只不过多了一个数字。例如,我们有2个光盘驱动 器,都安装在同一台PC上,则这两台CD-ROM的设备名将会是CDAudio1及CDAudio2。 命令字符串具有使用简单的特点,但是它的执行效率不如命令消息。所以,在我们的 媒体播放类里,主要使用mciSendCommand函数。下面将逐步讲述各种Windows媒体文件 的播放和控制方法。
5.4 MIDI文件的播放和控制 虽然Microsoft支持MIDI文件,然而Visual C++或MFC并没有创建任何组件来实现这种 支持,Microsoft API提供了三种不同的方法来实现MIDI的播放: ・ MCI:这是最基本的方法,我们将使用这种方法。 ・ 流缓冲区:这种格式允许应用程序为MIDI数据分配缓冲区。在需要精确控制MIDI 播放的时候,流缓冲区将很有用处。 ・ 低级MIDI设备:需要完全控制MIDI数据的应用程序可以使用这种方法。 MCI 可 以 通 过 mciSendCommand 和 mciSendString 来 完 成 , 在 此 我 们 仅 使 用 mciSendCommand函数。 5.4.1
MIDI简介
MIDI是音乐与计算机结合的产物。MIDI是Musical Instrument Digital Interface(乐器 的数字化接口)的缩写。整个MIDI系统包括合成器,电脑音乐软件,音源,电脑,MIDI 连线,调音台,数码录音机等外围设备。而我们平常所说的MIDI通常只是指一种电脑音 乐的文件格式。 MIDI文件本身并不是音乐,而是发音命令,系统必须根据这些发音命令合成出乐 曲,合成的方法有两种:早期使用的FM合成法以及波表合成法。现在FM合成法基本已被
第5章
媒体播放器——多媒体程序设计
115
淘汰,最常见的是波表合成法。所谓波表合成法就是系统依据MIDI文件的发音命令从音 色库或软波表中提取乐器样本声音然后合成音乐。 对于我们普通欣赏者来说,MIDI制作的好坏我们是管不了的,关键是怎么把MIDI播 放好。影响播放效果的主要因素有:声卡的波表合成能力、音色库大小及质量、软波表配 置以及声卡、音箱的综合性能等等。早期的低档声卡没有波表合成芯片,完全依靠软波表 及CPU来进行音乐合成,播放效果很差;早期的高档声卡自身带有波表合成芯片及存储声 音样本(音色库)的ROM或RAM,不依赖软波表及CPU来进行音乐合成,不过由于声卡 上的ROM或RAM都比较昂贵,因此现在大部分PCI声卡都是自身带波表合成芯片,而把音 色库存储于硬盘上,使用时调至内存,不依赖CPU进行合成。 5.4.2
MIDI文件格式
分析MIDI文件是剖析MIDI技术的一个好方法,MIDI中的各种技术都蕴涵在MIDI文件 内,所以分析一下MIDI的文件格式是很有帮助的。 MIDI文件大体分为两个区块:一个是文件头区块,另一个是音轨区块,下面看一下 这两个区块具体是如何描述的。 1. 文件头区块 MThd (1)文件头标识(占4个字节) 文件头标识是用来标识这个区块的,它的固定值为“MThd”4D 54 68 64 (2)区块大小(占4个字节) 区块大小是指整个区块所占的字节数,它不包含区块标识本身这4个字节,因为文件 头的格式是定型的,所以这个值也是固定的(为6),二进制表示为00 00 01 10 (3)文件类型(占2个字节) MIDI文件有三种类型,每种类型的格式大 同小异,一般来说第1种类型是较为常见 的: ・ 类型0 00 00 由一个文件头区块紧跟着一个音轨区块构成。 ・ 类型1 00 01 由一个文件头区块紧跟着一系列音轨区块构成。 ・ 类型11 00 11 由多个独立的音轨区块构成,这些音轨区块不需同时播放(很少支 持这种格式)。 (4)音轨区块的数目(占2字节) (5)时间基准(占2字节) 这是文件头区块中最难理解的一个概念,它是划分时间单位的一种标准。 2. 音轨区块 MTrk 音轨区块是MIDI文件的主体部分,它详细描述了歌曲中的一切细节。 (1)文件头标识(占4字节) 与文件头标识的含义一样,固定值为“MThd”4D 54 68 64
趣味程序导学 Visual C++
116
(2)区块大小(占4字节) 没有固定值,看你的文件具体有多大,同样它不包含本身所占用的4个字节及区块标 识。 (3)区块数据 这是MIDI文件中描述声音行为的数据,比较复杂,所以在此不作详细介绍。 5.4.3
MIDI文件的播放
首先在stdAfx.h中加入下面两行语句: #include <mmsystem.h> #pragma comment(lib, "winmm.lib")
这样就可以实现与Windows多媒体库的链接。 为工程添加一个新的类CMedia,如图5.12所示。
图 5.12
添加 CMedia 类
在CMedia 类的头文件Media.h中添加一个枚举类型的变量,以标识所播放的文件类 型,相应代码如下: enum PLAYTYPE { MIDI, WAVE, CD, AVI, NONE };
为CMedia类添加PLAYTYPE型的成员变量如下:
第5章
媒体播放器——多媒体程序设计
117
PLAYTYPE type;
下面我们首先给出CMedia类头文件的完整代码,然后再逐步解释其中的成员变量和 成员函数的意义,并给出成员函数的具体实现。代码如下: class CMedia : public CObject { public: DWORD Seek(int nMiniute,int nSecond); DWORD CloseCDRom(); //开光驱门 DWORD OpenCDRom(); //关光驱门
//文件跳转
DWORD SaveRecord(LPCSTR pFilename); DWORD BeginRecord(); //开始录音 DWORD Pause(); //暂停
//存储录音
DWORD CloseDevice(); //关闭设备 DWORD Stop(); //停止 DWORD Play(CWnd* pWnd,LPCSTR pFileName); //播放 DWORD OpenDevice(LPCSTR pFileName,LPCSTR pFileExt); //打开设备 void DisplayErrorMsg(DWORD dwError); //显示出错信息 bool closed; bool paused; bool stopped; PLAYTYPE type; DWORD dwResult; MCI_SEEK_PARMS MCI_GENERIC_PARMS
mciSeekParms; mciStopParms;
MCI_SAVE_PARMS
mciSaveParms;
MCI_RECORD_PARMS MCI_GENERIC_PARMS
mciRecordParms; mciGenericParms;
MCI_STATUS_PARMS
mciStatusParms;
MCI_OPEN_PARMS MCI_PLAY_PARMS
mciOpenParms; mciPlayParms;
CMedia(); virtual ~CMedia(); protected: MCIDEVICEID m_nDeviceID; };
MCIDEVICE型的变量m_nDeviceID用来保存打开的设备标志,实际类型是UINT。 播放媒体文件首先要打开相应的设备,函数OpenDevice(LPCSTR pFileName,LPCSTR pFileExt)可实现这一功能,它的两个参数pFileName和pFileExt分别是要播放的文件的完整 路径名和它的扩展名,根据文件的扩展名确定它的类型,以打开不同的播放设备,并保存 打开的设备的标志号。相应代码如下:
趣味程序导学 Visual C++
118
DWORD CMedia::OpenDevice(LPCSTR pFileName,LPCSTR pFileExt) { if(!closed) { CloseDevice(); closed = true; m_nDeviceID = 1; } if(paused) paused = false; if (m_nDeviceID) { if(strcmp(pFileExt,"WAV") == 0 || strcmp(pFileExt,"wav") == 0) { mciOpenParms.lpstrDeviceType = "waveaudio"; type = WAVE; } else if(strcmp(pFileExt,"MID") == 0 || strcmp(pFileExt,"mid") == 0) { mciOpenParms.lpstrDeviceType = "sequencer"; type = MIDI; } else if(strcmp(pFileExt,"CD") == 0 || strcmp(pFileExt,"cd") == 0) { mciOpenParms.lpstrDeviceType = "cdaudio"; type = CD; } else if(strcmp(pFileExt,"AVI") == 0 || strcmp(pFileExt,"avi") == 0) { mciOpenParms.lpstrDeviceType = "avivideo"; type = AVI; } //open the sound device mciOpenParms.wDeviceID = 0; mciOpenParms.lpstrElementName = pFileName; dwResult = mciSendCommand(NULL, MCI_OPEN, MCI_WAIT|MCI_OPEN_TYPE| MCI_OPEN_ELEMENT,(DWORD)(LPMCI_OPEN_PARMS)&mciOpenParms); //save device identifier,will use eith other MCI commands m_nDeviceID = mciOpenParms.wDeviceID; //display error message if failed if(dwResult) DisplayErrorMsg(dwResult);
第5章
媒体播放器——多媒体程序设计
119
else { stopped = closed =false; } } //return result of MCI operation return dwResult; }
程序首先检测目前有没有未关闭的设备,若有的话首先将其关闭。然后给 MCI_OPEN_PARMS型的成员变量mciOpenParms的相应域赋值,MCI_OPEN_PARMS类型 的结构定义为: typedef struct { DWORD
dwCallback;
MCIDEVICEID
wDeviceID; LPCSTR lpstrDeviceType;
LPCSTR
lpstrElementName;
LPCSTR lpstrAlias; } MCI_OPEN_PARMS;
其中,参数wDeviceID用来保存打开的设备的ID号,参数lpstrDeviceType是要打开的 设备的类型,参数lpstrElementName则是要播放的文件的完整路径。这里我们要播放MIDI 文 件 , 所 以 将 参 数 lpstrDeviceType 的 值 赋 为 “ sequencer ” 。 然 后 用 mciSendCommand(NULL , MCI_OPEN , MCI_WAIT|MCI_OPEN_TYPE|MCI_OPEN_ELEMENT , (DWORD)(LPMCI_OPEN_PARMS)&mciOpenParms ) 语 句 打 开 设 备 。 这 里 我 们 将 mciSendCommand函数的uMsg参数设置为MCI_OPEN,表示要打开一个设备;将dwFlags 的值设为MCI_WAIT|MCI_OPEN_TYPE|MCI_OPEN_ELEMENT ,MCI_WAIT表示我们要 同步播放文件,这样会使应用程序在mciSendCommand函数返回之前等待文件播放完毕。 MCI_OPEN_TYPE表示打开指定类型的设备,MCI_OPEN_ELEMENT 表示使用了设备元 素。OpenDevice将MCI_OPEN命令传递给mciSendCommand函数,如果调用成功,就用数 据结构MCI_OPEN_PARMS 的wDeviceID成员返回波形设备的标识符,该标识符保存在 nDevice中,供以后使用。 在上面的程序中,我们使用成员函数DisplayErrorMsg来显示出错信息,相应代码如 下: void CMedia::DisplayErrorMsg(DWORD dwError) { //check if there was an error if(dwError) { //character string that contains error message
趣味程序导学 Visual C++
120
char szErrorMsg[MAXERRORLENGTH]; //retrieve string associated error message if(!mciGetErrorString(dwError,szErrorMsg,sizeof(szErrorMsg))) strcpy(szErrorMsg,"Unknown Error"); //display error string in message box AfxMessageBox(szErrorMsg); } }
其中,我们使用mciGetErrorString方法来取得错误的信息。 在打开新的设备之前,要首先关闭旧的设备。我们使用成员函数CloseDevice来实现这 一功能,相应代码如下: DWORD CMedia::CloseDevice() { //close if currently open if(m_nDeviceID && !stopped) { //close the MCI device dwResult=mciSendCommand(m_nDeviceID,MCI_CLOSE,MCI_WAIT,NULL); //display error message if failed if(dwResult) DisplayErrorMsg(dwResult); //set identifier to close state else m_nDeviceID=0; } //return result of MCI operation closed = true; return dwResult; }
这里,我们将mciSendCommand函数的uMsg参数设为MCI_CLOSE,这样就可关闭当 前正在使用的设备。 设备打开之后,我们就可以播放文件了。我们为成员函数Play加入如下代码: DWORD CMedia::Play(CWnd* pWnd,LPCSTR pFileName) { //instruct device to play file if(paused && type == WAVE) { dwResult = mciSendCommand(m_nDeviceID,MCI_RESUME,MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) &mciGenericParms); paused = false;
第5章
媒体播放器——多媒体程序设计
121
} else if(type != NONE) dwResult = mciSendCommand(m_nDeviceID,MCI_PLAY, MCI_FROM,(DWORD)(LPMCI_PLAY_PARMS)&mciPlayParms); //display error and close element if failed if(dwResult) { DisplayErrorMsg(dwResult); Stop(); stopped = true; } else stopped = false; //return result of MCI operation return dwResult; }
程序首先检测当前是否处于暂停状态,如果是的话,则从上一次播放到的位置开始播 放,否则从头开始播放。同时设置相应的状态标志。这里将mciSendCommand的uMsg参数 设为MCI_PLAY,并将一个MCI_PLAY_PARMS结构传给函数。该结构的原型为: typedef struct { DWORD dwCallback; DWORD dwFrom; DWORD dwTo; } MCI_PLAY_PARMS;
参数dwCallback是回调窗口的句柄,如果函数设置了MCI_NOTIFY标志,则将其设置 为窗口句柄,否则为NULL;参数dwFrom是文件开始播放的位置,若从头开始播放则将其 设置为0;参数dwTo则是文件播放到的位置。 如 果 要 将 程 序 从 暂 停 状 态 恢 复 为 播 放 状 态 , 则 使 用 MCI_RESUME 命 令 , 即 将 mciSendCommand函数的uMsg参数设置为MCI_RESUME,并传递MCI_GENERIC_PARMS 结构。MCI_GENERIC_PARMS结构的原型为: typedef struct { DWORD dwCallback; } MCI_GENERIC_PARMS;
参数dwCallback为回调窗口的句柄。 如果程序出错或者要停止播放当前文件的话,调用Stop函数,相应代码如下: DWORD CMedia::Stop()
趣味程序导学 Visual C++
122 {
//close if element is currently open if(m_nDeviceID) { dwResult = mciSendCommand(m_nDeviceID,MCI_STOP, MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) &mciStopParms); //display error message if failed if(dwResult) DisplayErrorMsg(dwResult); } stopped = true; return dwResult; }
这里,我们使用MCI_STOP命令,同时传递MCI_GENERIC_PARMS结构。 最后我们给出播放MIDI文件时暂停功能的实现方法,为CMedia类的Pause函数添加代 码如下: DWORD CMedia::Pause() { if(m_nDeviceID) { dwResult = mciSendCommand (m_nDeviceID, MCI_PAUSE, MCI_WAIT,(DWORD)(LPMCI_GENERIC_PARMS) &mciGenericParms); if(dwResult) { DisplayErrorMsg(dwResult); return dwResult; } } paused = true; return dwResult; }
这里使用了MCI_PAUSE命令,可暂停当前文件的播放。 5.4.4 MIDI 文件的控制 在上一小节里,我们介绍了MIDI文件播放的相关功能,下面我们将介绍如何对它来 进行控制,包括跳转、信息查询和时间格式与播放速度的设置等。 在文件播放的过程中,有时我们需要将其跳转到其他位置进行播放。实现这一功能可 使用MCI_SEEK命令,为Seek函数添加代码如下: DWORD CMedia::Seek(int nMiniute, int nSecond)
第5章
媒体播放器——多媒体程序设计
123
{ if(m_nDeviceID) { mciSeekParms.dwTo = (nMiniute * 60 + nSecond) * 1000; dwResult = mciSendCommand (m_nDeviceID, MCI_SEEK, MCI_TO|MCI_WAIT,(DWORD) (LPMCI_SEEK_PARMS)&mciSeekParms); if(dwResult) DisplayErrorMsg(dwResult); } return dwResult; }
跳转的目标时间以毫秒(ms)为单位,将值赋给MCI_SEEK_PARMS 的dwTo域,该 结构的原型为: typedef struct { DWORD dwCallback; DWORD dwTo; } MCI_SEEK_PARMS
参数dwCallback是回调窗口的句柄,参数dwTo是要跳转到的位置。 若要跳转到文件头,函数mciSendCommand可写为: mciSendCommand (m_wDeviceID, MCI_SEEK, MCI_SEEK_TO_START, NULL);
相应的,若要跳转到文件尾,则可写为: mciSendCommand (m_wDeviceID, MCI_SEEK,MCI_SEEK_TO_END, NULL);
若 要 查 看 当 前 文 件 的 状 态 和 信 息 , 我 们 可 使 用 MCI_STATUS 命 令 , 并 利 用 MCI_STATUS_PARMS结构来进行功能设置,相应代码可写为: MCI_STATUS_PARMS mciStatusParms; mciStatusParms.dwItem = MCI_SEQ_STATUS_DIVTYPE; mciSendCommand (m_nDeviceID, MCI_STATUS,MCI_WAIT| MCI_STATUS_ITEM,(DWORD)( LPMCI_STATUS_PARMS) &mciStatusParms);
函数函数mciSendCommand的原型为: MCIERROR mciSendCommand ( MCIDEVICEID wDeviceID, MCI_STATUS, DWORD dwFlags, (DWORD) (LPMCI_STATUS_PARMS) lpStatus );
趣味程序导学 Visual C++
124
参数wDeviceID是用来接收命令的MCI设备的标识符;参数dwFlags是命令消息的标识 符;参数lpStatus则为指向MCI_STATUS_PARMS结构的指针。 函数返回的信息存放于mciStatusParms的dwReturn域中,MCI_STATUS 命令的几个主 要标识符及其相应的意义如表5.4所示。 表5.4 MCI_STATUS命令的主要标识符 MCI_STATUS标识符
意义
MCI_STATUS_LENGTH
获得文件长度
MCI_STATUS_MODE
获得文件播放的当前状态
MCI_STATUS_POSITION
获得文件播放的当前位置
MCI_STATUS_TIME_FORMAT
获得当前的时间格式
MCI_SEQ_STATUS_DIVTYPE
判断文件是PPQN类型还是SMPTE类型
MCI_SEQ_STATUS_TEMPO
获得当前播放速度,若为PQRN 类 型 , 此 值 为 节 拍 /分 , 若 为 SMPTE类型,此值为帧/秒
F ・ ・ ・ ・ ・ ・ ・
说明:MCI_STATUS_MODE标识符返回的文件播放的当前状态可以是下列值之 一: MCI_MODE_NOT_READY MCI_MODE_PAUSE MCI_MODE_PLAY MCI_MODE_STOP MCI_MODE_OPEN MCI_MODE_RECORD MCI_MODE_SEEK
最后我们简单介绍文件时间格式和播放速度的设置。文件的时间格式可使用 MCI_SET命令,用下面的代码来设置: MCI_SET_PARMS mciSetParms; mciSetParms.dwTimeFormat = MCI_FORMAT_MILLISECONDS; mciSendCommand (m_nDeviceID,MCI_SET, MCI_SET_TIME_FORMAT, (DWORD) (LPMCI_SET_PARMS)&mciSetParms);
这里,我们将文件的时间格式设置为ms。MCI_SET_PARMS结构的原型为: typedef struct { DWORD dwCallback; DWORD dwTimeFormat; DWORD dwAudio; } MCI_SET_PARMS;
第5章
媒体播放器——多媒体程序设计
125
参数dwCallback是回调窗口的句柄;参数dwTimeFormat是设备的时间格式;dwAudio 是音频的输出频道。 文件的播放速度可用MCI_SEQ_SET_TEMPO命令来设置,若为PQRN类型,此值为节 拍/分,若为SMPTE类型,此值为帧/秒。
5.5 Wave文件的播放和控制 本节我们将介绍Windows的另外一种常用音频文件格式——Wave文件的播放和控制 方法。 5.5.1
Wave文件格式简介
在介绍Wave文件格式之前,我们先简单了解一下RIFF的有关知识。在Windows环境 下,大部分的多媒体文件都按照某种结构来存放信息,这种结构称为“资源互换文件格 式”(Resources Interchange File Format),简称RIFF。例如声音的WAV文件、视频的 AV1文件等等均是由此结构衍生出来的。RIFF可以看做是一种树状结构,其基本构成单位 为chunk,犹如树状结构中的节点,每个chunk由“识别码”、“数据大小”及“数据”所 组成。 识别码由4个ASCII码所构成,数据大小则标示出紧跟其后数据的长度(单位为字 节),而数据大小本身也为4字节,所以事实上一个chunk的长度为数据大小加8。一般而 言 , chunk 本 身 并 不 允 许 内 部 再 包 含 chunk , 但 有 两 种 例 外 , 分 别 为 以 “ RIFF ” 及 “L1ST”为识别码的chunk。而针对此两种chunk,RIFF 又从原先的“数据”中切出4字 节。此4字节称为“格式识别码”,然而RIFF又规定文件中仅能有一个以“RIFF”为识别 码的chunk。 只要符合此结构的文件,我们均称之为RIFF。此结构提供了一种系统化的分类。如果 和MS-DOS文件系统作比较,“RIFF”chunk就好比是一台硬盘的根目录,其格式识别码 便是此硬盘的逻辑代码(C:或D:),而“L1ST”chunk即为其下的子目录,其他的 chunk则为一般的文件。至于在RIFF文件的处理方面,微软提供了相关的函数。Windows 下的各种多媒体文件格式就如同在磁带机下规定仅能放怎样的目录,而在该目录下仅能放 何种数据。 Wave文件作为多媒体中使用的声波文件格式之一,它是以RIFF格式为标准的。每个 Wave文件的头4个字节便是“RIFF”。Wave文件由文件头和数据体两部分组成。其中文 件头又分为RIFF/Wave文件标识段和声音数据格式说明段两部分。Wave文件各部分内容 及格式见表5.5。 表5.5 Wave文件格式说明 文件头偏移地址
字节数
数据类型
内容
00H
4
char
RIFF标志
趣味程序导学 Visual C++
126
续表 文件头偏移地址
字节数
数据类型
内容
04H
4
long
文件长度
08H
4
char
Wave标志
0CH
4
char
fmt标志
10H
4
14H
2
int
格式类别(10H为PCM 形式的声音数据)
16H
2
int
通道数,单声道为1,双声道为2
18H
2
int
采样率(每秒样本数),表示每个通道的播放速度
1CH
4
long
波形音频数据传送速率,其值为通道数×每秒数据位数×
过渡字节(不定)
每样本的数据位数/ 8。播放软件利用此值可以估计缓 冲区的大小 20H
2
数据块的调整数(按字节算的),其值为通道数×每样
int
本的数据位值/ 8。播放软件需要一次处理多个该值大 小的字节数据,以便将其值用于缓冲区的调整 22H
每样本的数据位数,表示每个声道中各个样本的数据位
2
数。如果有多个声道,对每个声道而言,样本大小都一 样 24H
4
char
数据标记符“data”
28H
4
long
语音数据的长度
常见的声音文件主要有两种,分别对应于单声道(11.025KHz采样率、8位采样值) 和双声道(44.1KHz采样率、16位采样值)。采样率是指:声音信号在“模-数”转换过程 中单位时间内采样的次数。采样值是指每一次采样周期内声音模拟信号的积分值。 对于单声道声音文件,采样数据为八位的短整数(short int 00H-FFH);而对于双声 道立体声声音文件,每次采样数据为一个16位的整数(int),高八位和低八位分别代表左 右两个声道。 Wave文件数据块包含以脉冲编码调制(PCM)格式表示的样本。Wave文件是由样本 组织而成的。在单声道Wave文件中,声道0代表左声道,声道1代表右声道。在多声道 Wave文件中,样本是交替出现的。 PCM数据的存储方式如表5.6所示。 表5.6
PCM数据的存储方式
样本1
样本2
8位单声道
0声道
0声道
8位立体声
0声道(左)
1声道(右)
0声道(左)
1声道(右)
16位单声道
0声道低字节
0声道高字节
0声道低字节
0声道高字节
16位立体声
0声道(左)低字节
0声道(左)高字节
1声道(右)低字节
1声道(右)高字节
第5章
媒体播放器——多媒体程序设计
127
Wave文件的每个样本值包含在一个整数i中,i的长度为容纳指定样本长度所需的最小 字节数。首先存储低有效字节,表示样本幅度的位放在i的高有效位上,剩下的位置为0, 这样8位和16位的PCM波形样本的数据格式如表5.7所示。 表5.7
PCM波形样本的数据格式
样本大小
数据格式
最大值
最小值
8位PCM
无符号整型数
225
0
16位PCM
整型数
32767
-32767
5.5.2
Wave文件的播放和录音
在 多 媒 体 软 件 的 设 计 中 经 常 要 处 理 声 音 文 件 , 用 Windows 提 供 的 API 函 数 sndPlaySound可以实现小型Wave文件的播放。用sndPlaySound播放音频文件只需要一行代 码。比如实现异步播放的方法为: sndPlaySound("c:\windows\ding.wav",SND_ASYNC);
由此可以看到,sndPlaySound的使用是很简单的。但是用sndPlaySound播放音频文件 有一个限制,即整个音频文件必须全部调入可用的物理内存。因此应用sndPlaySound播放 的音频文件相对较小,最大约100K。要播放大一些的音频文件(在多媒体设计中是经常 要遇到的情况)需要使用MCI的功能。 使 用 MCI 播 放 Wave 文 件 与 播 放 MIDI 文 件 类 似 , 只 需 在 打 开 设 备 时 将 MCI_OPEN_PARMS结构的lpstrDeviceType设为"waveaudio"即可。相应代码如下: if(strcmp(pFileExt, "WAV") == 0 || strcmp(pFileExt, "wav") == 0) { mciOpenParms.lpstrDeviceType = "waveaudio"; type = WAVE; }
利用MCI 我们可以录制 Wave文件中的一段,这时需要使用MCI_RECORD命令,为 BeginRecord函数添加代码如下: DWORD CMedia::BeginRecord() { if(m_nDeviceID) { dwResult = mciSendCommand (m_nDeviceID, MCI_RECORD, NULL, (DWORD)(LPMCI_RECORD_PARMS)&mciRecordParms); if(dwResult) DisplayErrorMsg(dwResult); } return dwResult; }
趣味程序导学 Visual C++
128
这里我们为函数mciSendCommand传送MCI_RECORD_PARMS结构,其原型为: typedef struct { DWORD dwCallback; DWORD dwFrom; DWORD dwTo; } MCI_RECORD_PARMS;
参数dwCallback是回调窗口的句柄,参数dwFrom是录音的起始位置,dwTo是结束位 置。 利用MCI_SAVE命令可保存录音段,相应代码如下: DWORD CMedia::SaveRecord(LPCSTR pFilename) { if(m_nDeviceID) { mciSaveParms.lpfilename = pFilename; dwResult = mciSendCommand (m_nDeviceID, MCI_SAVE, MCI_SAVE_FILE | MCI_WAIT,(DWORD)(LPMCI_SAVE_PARMS) &mciSaveParms); if(dwResult) DisplayErrorMsg(dwResult); } return dwResult; }
程 序 为 mciSendCommand 函 数 传 递 MCI_SAVE_PARMS 结 构 , 并 将 录 音 段 存 入 lpfilename所代表的文件中。MCI_SAVE_PARMS结构的原型为: typedef struct { DWORD
dwCallback;
LPCSTR lpfilename; } MCI_SAVE_PARMS
参数dwCallback是回调窗口的句柄,lpfilename是用来存储的目标文件。
5.6
CD的播放和控制
CD的独特优势在于,它由作曲家设计,并由音乐厂家生产。不同的计算机播放MIDI 文件时,声音效果也不一样,但是 CD的声音效果总是相同的。我们依然采用 MCI 播放 CD,大部分的播放控制与前面相同,这里只列出不同的部分。
第5章
媒体播放器——多媒体程序设计
129
打开设备时,将MCI_OPEN_PARMS结构的lpstrDeviceType设为"cdaudio "。相应代码 如下: else if(strcmp(pFileExt,"CD") == 0 || strcmp(pFileExt, "cd") == 0) { mciOpenParms.lpstrDeviceType = "cdaudio"; type = CD; }
M
注意:指 定 播 放 起 点 必 须 经 过MCI_MAKE_TMSF(Track,Minute,Second, Frame)转化。
利用MCI_SET_DOOR_OPEN命令可以打开光驱门,相应代码如下: DWORD CMedia::OpenCDRom() { if(m_nDeviceID) { if(!closed) CloseDevice(); dwResult = mciSendCommand (m_nDeviceID, MCI_SET, MCI_SET_DOOR_OPEN, NULL); if(dwResult) DisplayErrorMsg(dwResult); } return dwResult; }
相应的,利用MCI_SET_DOOR_CLOSED命令可以关闭光驱门,代码如下: DWORD CMedia::CloseCDRom() { if(m_nDeviceID) { dwResult = mciSendCommand (m_nDeviceID, MCI_SET, MCI_SET_DOOR_CLOSED, NULL); if(dwResult) DisplayErrorMsg(dwResult); } return dwResult; }
利用MCI_STATUS命令同样可以读取CD中的信息,常用的标识符见表5.8。
趣味程序导学 Visual C++
130
表5.8
CD文件常用的MCI_STATUS标识符
标识符
意义
MCI_STATUS_CURRENT_TRACK
获取当前曲目
MCI_STATUS_LENGTH
获取CD或指定曲目长度
MCI_STATUS_MODE
获取驱动器的当前状态
MCI_STATUS_NUMBER_OF_TRACKS
获取CD曲目的数目
MCI_STATUS_POSITION
获取当前格式下的位置
MCI_STATUS_READY
检查设备是否就绪
MCI_STATUS_TIME_FORMAT
获取当前时间格式
MCI_STATUS_MEDIA_PRESENT
检查以确认CD是否在驱动器内
MCI_CDA_STATUS_TYPE_TRACK
检查已确认某曲目是否为音频曲目
使用时只要将MCI_STATUS替换为相应的标识符即可。
M
注意:使用MCI_STATUS_LENGTH参数查询CD及曲目长度,返回值通过调用 MCI_MSF_MINUTE(),MCI_MSF_SECOND()转换为分、秒。 MCI_STATUS_POSITION 参 数 返 回 值 调 用 MCI_TMSF_TRACK ( ) , MCI_TMSF_MINUTE(),MCI_TMSF_SECOND(),MCI_TMSF_FRAME才能得到当 前位置的道、分、秒、帧。
播 放 CD 文 件 时 也 可 以 实 现 跳 转 , 但 跳 转 的 目 标 必 须 经 过 MCI_MAKE_TMSF (Track,Minute,Second,Frame)转化,最好为上述三种格式分别建立一个类,或做成 动态链接库。在Project->Setting->Link->Object/library modules中加入winmm.lib。
5.7 AVI文件的播放和控制 5.7.1
AVI数字视频的格式
AVI(Audio Video Interleave)是一种音频视频交叉记录的数字视频文件格式。1992 年初Microsoft公司推出了AVI技术及其应用软件VFW(Video for Windows)。在AVI文件 中,运动图像和伴音数据是以交叉方式存储,并独立于硬件设备。按交叉方式组织音频和 图像数据可使得读取图像数据流时能更有效地从存储媒介得到连续的信息。构成一个AVI 文件的主要参数包括图像参数、伴音参数和压缩参数等: (1)图像参数 视频窗口大小(Video size):根据不同的应用要求,AVI的视频窗口大小或分辨率可 按4:3的比例或随意调整:大到全屏640×480,小到160×120甚至更低。窗口越大,视频文 件的数据量越大。
第5章
媒体播放器——多媒体程序设计
131
帧率(Frames per second):帧率也可以调整,而且与数据量成正比。不同的帧率会 产生不同的画面连续效果。 (2)伴音参数 在AVI文件中,图像和伴音是分别存储的,因此可以把一段视频中的图像与另一段视 频中的伴音组合在一起。AVI 文件与WAV文件密切相关,因为WAV文件是AVI文件中伴 音信号的来源。伴音的基本参数也即WAV文件格式的参数,除此以外,AVI文件还包括 与音频有关的其他参数: ・ 图像与伴音的交织参数(Interlace Audio Every X Frames):AVI格式中每X帧交叉 存储的音频信号,也即伴音和图像交替的频率X是可调参数,X的最小值是一帧, 即每个视频帧与音频数据交叉组织,这是CD-ROM上使用的默认值。交叉参数越 小,回放AVI文件时读到内存中的数据流越少,回放越容易连续。因此,如果AVI 文件的存储平台的数据传输率较大,则交叉参数可设置得高一些。当AVI文件存 储在硬盘上时,也即从硬盘上读AVI文件进行播放时,可以使用大一些的交织频 率,如几帧,甚至1帧。 ・ 同步控制(Synchronization):在AVI文件中,图像和伴音同步得很好。但在MPC 中回放AVI文件时则有可能出现图像和伴音不同步的现象。 (3)压缩参数 在采集原始模拟视频时可以用不压缩的方式,这样可以获得最优秀的图像质量。编辑 后应根据应用环境选择合适的压缩参数。 5.7.2
AVI数字视频的特点
AVI及其播放器VFW是PC机上最常用的视频数据格式,它具有如下的一些显著特 点: (1)提供无硬件视频回放功能:AVI格式和VFW软件虽然是为当前的MPC设计的, 但它也可以不断提高以适应MPC的发展。根据AVI格式的参数,其视窗的大小和帧率可以 根据播放环境的硬件能力和处理速度进行调整。在低档MPC机上或在网络上播放时, VFW的窗口可以很小,色彩数和帧率可以很低;而在Pentium级系统上,对于64K色、 320×240的压缩视频数据可实现每秒25帧的回放速率。这样,VFW就可以适用于不同的硬 件平台,使用户可以在普通的MPC上进行数字视频信息的编辑和重放,而不需要昂贵的专 门硬件设备。 (2)实现同步控制和实时播放:通过同步控制参数,AVI可以通过自调整来适应重 放环境,如果MPC的处理能力不够高,而AVI文件的数据率又较大,在Windows环境下播 放该AVI文件时,播放器可以通过丢掉某些帧,调整AVI的实际播放数据率来达到视频、 音频同步的效果。 (3)可以高效地播放存储在硬盘和光盘上的 AVI文件:由于AVI数据的交叉存储, VFW播放AVI数据时只需占用有限的内存空间,因为播放程序可以一边读取硬盘或光盘上 的视频数据一边播放,而无需预先把容量很大的视频数据加载到内存中。在播放AVI视频
趣味程序导学 Visual C++
132
数据时,只需在指定的时间内访问少量的视频图像和部分音频数据。这种方式不仅可以提 高系统的工作效率,同时也可以实现迅速地加载和快速地启动播放程序,减少播放AVI视 频数据时用户的等待时间。 (4)提供了开放的AVI数字视频文件结构:AVI文件结构不仅解决了音频和视频的同 步问题,而且具有通用和开放的特点。它可以在Windows环境下工作,而且还具有扩展环 境的功能。用户可以开发自己的AVI视频文件,在Windows环境下可随时调用。 5.7.3
AVI文件的播放
AVI文件的播放和控制方法与音频文件类似,只要在打开设备时将mciSendCommand 函数的uMsg参数设为avividio即可。相应代码如下: else if(strcmp(pFileExt,"AVI") == 0 || strcmp(pFileExt,"avi") == 0) { mciOpenParms.lpstrDeviceType = "avivideo"; type = AVI; }
5.8
其他媒体文件简介
在上面几节里,我们介绍了Windows的几种常用的媒体文件的播放和控制方法。下面 再介绍应用十分广泛,原理与VFW类似的另一种文件格式— — Quick Time,它的文件后缀 为.MOV,有时我们也称它为MOV文件。众所周知,Apple公司的MAC机具有强大的图像 处理功能,从一开始就被定位成多媒体计算机,尽管MAC机的性能的确不错,但大家更 看好PC市场,事实上,越来越多的多媒体应用正从MAC机转移到P C机上。Quick Time本 来是MAC系统软件7.0版的扩展,Apple希望把它作为一个交互式多媒体平台的标准,以便 使用户在PC机上的Windows环境也能运行Quick Time的多媒体功能。 当然,利用Windows的媒体播放器也能播放MOV文件,前提是必须先安装Quick Time For Windows。这也是利用C++ Builder播放MOV文件的基础。 Quick Time 为用户提供了处理位图图像、声音、动画及其他多媒体信息的统一接口 和标准文件格式,其核心是对MOV文件格式的支持。MOV文件是用来管理不同形态的动 态数据的。所谓动态数据即动态画面数据化后以文件形式表示的数据,包括图像和声音两 部分。在文件格式上根据信息类别分为三个区:Track 通道、Movie画面区和Media 区。 Movie区由不同的轨迹组成。轨迹里有表示画面演示起始与持续时间的数据,还有时 标(Time Scale)以及它在显示屏幕上相应的坐标。一个轨迹也可以看成一个程序,在指 定的时间内用数据完成一个特定的演示储备或功能。每个Movie含有一个或者多个轨迹, 轨迹里还有一系列指针,指向媒体文件区,媒体文件描述了数据主体实际存放在哪个媒体 中,即存放着数据主体的物理位置信息。这样可以使原始数据与Movie文件分开存放,具 有极大的灵活性。Quick Time使这些轨迹同步,以确保所有数据在合适的时刻送到屏幕和
第5章
媒体播放器——多媒体程序设计
133
语言合成芯片。由此可见,MOV文件格式具有以下特点: (1)有表示时间过程的参数,即画面的开始时间、延续时间及时标。 (2)文件信息分3个区存放,分层次表现。 (3)通过对轨迹编程,可对特定轨迹在特定的时刻、特定位置以特定速度进行演播 显示,以收到特定的效果。 Apple公司在1991年开发出的Quick Time 1.0版本,也可置入普通PC机,它支持160× 120×15帧/秒的画面显示,这时Windows的应用程序通过Quick Time 的API也可随意使用 Quick Time的各项功能。1992年下半年推出的Quick Time 1.5版,支持320×240×15帧秒/ 秒的画面显示能力。1994年上半年推出的Quick Time 2.0版,则达到了320×240×30帧/秒 的画面显示能力。 虽然MOV文件的格式比较复杂,但在VC++中播放一个MOV文件是十分简单的。 最后简单介绍一下基于MPEG 1的DAT 文件。国际标准化组织ISO建立了一个制定有 关运动图像编码压缩标准的组织— — MPEG(Moving Picture Experts Group)。MPEG委员会 的工作始于1988年,1990年制定出标准草案。 在设计动态图像的编码算法时,主要矛盾是,一方面仅仅靠帧内编码方法无法在保证 良好的画面质量前提下达到很高的压缩比,另一方面单一静止的帧内编码方法又能最好地 满足随机存取的要求。为了同时满足高压缩比和随机存取这两方面的要求,MPEG推荐的 标准化算法,必须使用帧间与帧内编码技术。 MPEG标准采用的技术就是预测与内插技 术。采用运动补偿技术对提高编码压缩比很有好处。尤其对于运动部分只占整个画面较小 的视频会议和可视电话,可以达到很高的压缩比。 上面简要介绍MPEG的压缩原理,只要我们安装XING之类的软解压软件,就可以在 程序中很方便地使用Windows所提供的MCI 控制它的播放了,而对于VC++,我们要做的 仅仅是改变文件名。
5.9
媒体播放类的使用
在上一节里,我们已利用Windows的MCI命令函数创建了自己的媒体播放类,在这一 节里,我们为对话框加入相应的代码,以实现媒体文件的播放。 首先为CMultimediaDlg类添加一个CMedia型的变量m_media,代码如下: CMedia m_media;
为5个按钮加入相应的OnClick事件,并添加代码如下: void CMultimediaDlg::OnOpen() { //按钮事件,打开设备 // TODO: Add your control notification handler code here
趣味程序导学 Visual C++
134
CFileDialog dlg(TRUE, "midi", "*.mid",OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, "Midi Files(*.mid)|*.mid|Wave Files(*.wav)|*.wav|AVI Files(*.AVI)|*.AVI|All Files(*.*)|*.*||"); if(dlg.DoModal()==IDOK) { pFileName = dlg.GetPathName(); pFileExt = dlg.GetFileExt(); m_media.OpenDevice((LPCTSTR)pFileName,(LPCSTR)pFileExt); fileOpen = true; m_wndStatusBar.SetText(pFileName,0,0); } } void CMultimediaDlg::OnPlay() { //按钮事件,播放媒体文件 // TODO: Add your control notification handler code here if(fileOpen) { m_media.Play(this,pFileName); SetTimer(1,250,NULL); } } void CMultimediaDlg::OnPause() { //按钮事件,暂停文件的播放 // TODO: Add your control notification handler code here if(fileOpen) { m_media.Pause(); KillTimer(1); } } void CMultimediaDlg::OnStop() { //按钮事件,停止文件的播放 // TODO: Add your control notification handler code here if(fileOpen) { m_media.Stop(); KillTimer(1);
第5章
媒体播放器——多媒体程序设计
135
m_HistogramCtrl.InvalidateCtrl(); count = 0; } } void CMultimediaDlg::OnExit() { //按钮事件,关闭设备并退出程序 // TODO: Add your control notification handler code here m_media.CloseDevice(); KillTimer(1); OnOK(); }
播放文件时,首先单击“打开”按钮,在弹出的文件对话框中选择想要播放的文件, 如图5.13所示。
图 5.13
打开媒体文件
设备打开后,单击“播放”按钮即可播放相应的媒体文件。在播放过程中,单击其他 相应的按钮,可实现暂停、停止和关闭程序等功能。 接下来,我们为菜单项也添加相应的功能。对“打开”、“播放”、“暂停”、“停 止”和“退出”5项,只要将它们的ID号设为与对应的功能按钮相同即可,如图5.14所示 为将“打开”菜单项的ID号设为IDC_OPEN,即与“打开”按钮相同。
趣味程序导学 Visual C++
136
图 5.14
设置菜单项的 ID 号与按钮相同
对其他的菜单项,添加代码如下: void CMultimediaDlg::OnMenuitemRecord() { //菜单事件,开始录音 // TODO: Add your command handler code here if(m_media.type == WAVE) { m_media.BeginRecord(); } } void CMultimediaDlg::OnMenuitemSave() { //菜单事件,保存录音段 // TODO: Add your command handler code here if(m_media.type == WAVE) { CFileDialog dlg(TRUE, "wave", "*.wav",OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,"Wave Files(*.wav)|*.wav|"); if(dlg.DoModal() == IDOK) { m_media.SaveRecord(dlg.GetFileName()); OnStop(); } } } void CMultimediaDlg::OnMenuitemReplay() { //菜单函数,重新播放文件 // TODO: Add your command handler code here if(fileOpen) {
第5章
媒体播放器——多媒体程序设计
137
m_media.paused = false; m_media.Play(this,pFileName); SetTimer(1,250,NULL); } } void CMultimediaDlg::OnMenuitemOpencd() { //菜单函数,开光驱门 // TODO: Add your command handler code here m_media.OpenCDRom(); } void CMultimediaDlg::OnMenuitemClosecd() { //菜单函数,关光驱门 // TODO: Add your command handler code here m_media.CloseCDRom(); }
5.10 5.10.1
音响效果显示和音量控制
音响效果的显示
下面我们自己制作一个控件来动态显示媒体文件的柱状音响效果,它的初始界面如图 5.15所示。
图 5.15
音响效果显示控件初始界面
在给出控件的源代码之前,我们先简单了解一下自定义控件编程的一些概念。 有一个吸引人的界面是软件成功的关键,Visual C++集成开发环境为我们提供了许多 标准控件,如按钮控件,编辑控件,组合框控件等,但是,要设计一个美观的界面这些控 件是远远不够的,它们无法满足所有的应用程序需要。自定义控件类允许用可重用组件来 扩展应用程序的能力。自定义控件类可以被链接成执行文件。 在关于类的构造方面MFC有自身的规则集。通过研究MFC的源代码,可很好的掌握 这套技术,用来把现存结构嵌入类及从现存类派生新类。我们要制作的CHistogramCtrl控 件类从窗口类CWnd派生。基类声明如下: class CHistogramCtrl : public CWnd
趣味程序导学 Visual C++
138 { }
重载CWnd类的Create,并添加代码如下: BOOL
CHistogramCtrl::Create(DWORD
dwStyle,
const
RECT&
rect,
CWnd*
pParentWnd , UINT nID, CCreateContext* pContext) { // TODO: Add your specialized code here and/or call the base class static CString className=AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW); return
CWnd::CreateEx(WS_EX_CLIENTEDGE | WS_EX_STATICEDGE, className, NULL, dwStyle, rect.left , rect.top , rect.right-rect.left, rect.bottom-rect.top ,pParentWnd->GetSafeHwnd(), (HMENU) nID);
}
为CHistogramCtrl类添加一个CDC类型的变量m_MemDC,并重载OnPaint函数如下: void CHistogramCtrl::OnPaint() { CPaintDC dc(this); // device context for painting // TODO: Add your message handler code here // Do not call CWnd::OnPaint() for painting messages // draw scale CRect rcClient; GetClientRect(rcClient); // draw scale if (m_MemDC.GetSafeHdc() != NULL) { dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &m_MemDC, 0, 0, SRCCOPY); } }
添 加 3 个 UINT 型 的 变 量 m_nLower 、 m_nUpper 和 m_nPos 及 一 个 CBitmap 型 的 变 量 m_Bitmap,相应代码如下: UINT
m_nLower;
// lower bounds
UINT
m_nUpper;
// upper bounds
UINT
m_nPos;
// current position within bounds
第5章
媒体播放器——多媒体程序设计
139
CBitmap m_Bitmap;
在类的构造函数中对这几个变量进行初始化,相应代码如下: CHistogramCtrl::CHistogramCtrl() { m_nPos = 0; m_nLower= 0; m_nUpper= 100; }
最后,我们给出用来绘制柱状音响效果的几个函数,具体算法由于与本章内容无关, 在此不做详述,只给出相应代码: void CHistogramCtrl::SetRange(UINT nLower, UINT nUpper) { ASSERT(nLower >= 0 && nLower < 0xffff); ASSERT(nUpper > nLower && nUpper < 0xffff); m_nLower = nLower; m_nUpper = nUpper; InvalidateCtrl(); } void CHistogramCtrl::InvalidateCtrl() { // Small optimization that just invalidates the client area // (The borders don't usually need updating) CClientDC dc(this); CRect rcClient; GetClientRect(rcClient); if (m_MemDC.GetSafeHdc() == NULL) { m_MemDC.CreateCompatibleDC(&dc); m_Bitmap.CreateCompatibleBitmap(&dc,rcClient.Width(), rcClient.Height()); m_MemDC.SelectObject(m_Bitmap); // draw scale m_MemDC.SetBkColor(RGB(0,0,0)); CBrush bkBrush(HS_HORIZONTAL,RGB(0,128,0)); m_MemDC.FillRect(rcClient,&bkBrush); } InvalidateRect(rcClient);
趣味程序导学 Visual C++
140 }
UINT CHistogramCtrl::SetPos(UINT nPos) { if (nPos > 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;i<2;i++) { m_set.MoveNext(); } str.Format("%ld",m_set.m_bus_number); SetDlgItemText(IDC_STATIC,str);
如此循环两次,即滚动两个记录,应该是得到记录“365;双清路”,输出应该是该记 录的车次365。运行该程序,输出如图6.30所示:
图 6.30
滚动两条记录后
判断记录集是否可以修改或添加 可以采用如下方式判断能否对记录集进行修改或者添加: if (!m_set.CanUpdate() || !m_set.CanAppend()) return TRUE;
趣味程序导学 Visual C++
172
添加一个记录 可以采用如下的方式加入一条记录,我们假设在原有的数据库中加入“395;王府 井”,代码如下: try { m_set.AddNew(); m_set.m_bus_number=395; m_set.m_bus_station="王府井"; m_set.Update(); } catch (CDaoException *e) { AfxMessageBox(e->m_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;j
标记为黑体部分是本段代码的关键。组合框控制变量的成员函数GetCurSel()返回的是 当前选项的索引。当我们以AddString方式加入车次数据时,第一项对应的索引为0。 int i=m_num.GetCurSel(); m_set.m_strFilter="[bus_station]"; if(m_set.IsOpen()){ m_set.Close(); } m_set.Open();
以上这段代码初看没有必要。但是结合到后面的站名模糊查询方式,我们就不难发 现,若在站名查询之后仍然能够查询选定车次的记录,必须注释掉在模糊查询时设定的 SQL WHERE语句。所以这里将[bus_station]设定为任意值。然后再打开记录集,保证了程 序的稳定性。运行本程序,从组合框的下拉列表中选择355车次,单击查询按钮,如图 6.48所示:
趣味程序导学 Visual C++
184
图 6.48
输出选定 355 次的信息
注意,运行程序后,也许你会发现选择的车次和输出信息并不匹配。这主要是因为组 合框的风格设置中对于数据载入时选择了排序,只需不选此项即可,如图6.49:
图 6.49
6.6.2
不要求加入字符串时排序
在编辑框内输入需要查询的车站并显示路线信息
设定编辑框 正确输出选定车次的记录后,我们开始处理车站查询时的消息响应。首先,选择控制 板 编辑框,单击右键进行属性设置,ID定义为IDC_EDITSTA,并在扩展风格中对边缘 形状进行设定,这里选择仍然是Client+Static ,则该编辑框显示为 。 打开ClassWizard,选择Member Variables,在原来添加CBusSet类时选择的Add Class 项下有一个 Add Variable 选项,单击该按钮,系统弹出设定对话框。我们输入变量名 m_edit,同时在类别(Category)项中选择Value,表示是值变量,系统自动设定此变量类 型为CString,如图6.50所示:
第5章
北京市公交查询系统——数据库编程基础
图 6.50
185
设定值变量
单击OK按钮,以与前面同样的方法找到m_edit声明: CString
m_edit;
响应编辑框中输入的数据 同样,我们加入按钮,用于在输入车站后确认查询对象。单击右键选择属性,在一般 属性中设定ID为IDC_STA,Caption设定为查询。 打开MFC ClassWizard(见图6.46),在Message Maps标签中设定类名为CbusDlg,在 Object IDs列中找到IDC_STA,并在Messages栏中同样选定BN_CLICKED,作为单击该按 钮的消息映射。单击 Add Function按钮,在弹出的对话框内设定该消息映射对应的函数 名,默认为OnSta。单击OK按钮,ClassWizard将在类CBusDlg 中生成OnNum函数的主体部 分。代码如下: void CBusDlg::OnSta() { // TODO: Add your control notification handler code here }
在其中,我们加入代码: UpdateData(TRUE); m_set.m_strFilter="[bus_station] like '%"+m_edit+"%'"; if(!m_set.Open()||m_set.IsEOF()) { AfxMessageBox("没有找到匹配数据,请重新输入"); } CString str; int i=IDC_BUSNUM1,j=IDC_BUSSTA1;
趣味程序导学 Visual C++
186 while(!m_set.IsEOF()) {
str.Format("%ld",m_set.m_bus_number); SetDlgItemText(i,str); str.Format("%s",m_set.m_bus_station); SetDlgItemText(j,str); i++; j++; m_set.MoveNext(); } m_set.Close(); }
函数UpdateData将开始设定的值变量m_edit与当前输入值连接,而SQL WHERE语句 m_set.m_strFilter="[bus_station] like '%"+m_edit+"%'";
则将输入站名连入模糊查询方式,若输入站名不在此数据库中的各记录路线中,则无法找 到匹配数据,需要重新输入,代码: if(!m_set.Open()||m_set.IsEOF()) { AfxMessageBox("没有找到匹配数据,请重新输入"); }
完成了上述功能。其中AfxMessageBox()函数完成弹出对话框功能(如图6.51所示),并可 以在输出字符串后面设定不同参数来显示不同类型的对话框。
图 6.51
显示错误信息
代码: int i=IDC_BUSNUM1,j=IDC_BUSSTA1; i++; j++;
是建立在资源显示区IDC_BUSNUM1-IDC_BUSNUM6和IDC_BUSSTA1-IDC_BUSSTA6中 宏定义的值递增的基础上。从工作区的FileView中的Source file中打开bus.rc,可以看到头 部有如下代码: #define IDC_BUSNUM1 #define IDC_BUSNUM2
1007 1008
第5章 #define #define #define #define #define #define #define #define #define #define
北京市公交查询系统——数据库编程基础
IDC_BUSNUM3 IDC_BUSNUM4 IDC_BUSNUM5 IDC_BUSNUM6 IDC_BUSSTA1 IDC_BUSSTA2 IDC_BUSSTA3 IDC_BUSSTA4 IDC_BUSSTA5 IDC_BUSSTA6
187
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
5.6.1节采用的快速复制保证了这一点。运行程序,输入站名“清华西门”,查询系统 将显示出bus表中bus_station字段里含有清华西门的记录。 6.6.3
完善界面
至此,这个简单的查询系统核心代码部分已经完成。剩下的部分则是对系统界面的完 善工作了。首先,利用静态文本框设定查询系统的标题,属性中Caption项设置为“北京 市公交查询系统”,排列风格选择为居中,并在扩展风格的边缘设置中采用 Client+Static ,同时选择Modal frame,最终显示如下:
然后,我们调整按钮属性,Caption分别设置为“车次查询”和“车站查询”,扩展 风格的设置同上。显示为:
同时,选择图标为 的组框,将左边查询区与右边显示区分割开来,并删去默认的 “取消”按钮,将“确定”按钮的Caption设为退出,如图6.52所示:
图 6.52
显示界面
运行该程序,在编辑框中输入“清华大学”,显示如图6.53所示:
趣味程序导学 Visual C++
188
图 6.53
6.6.4
运行结果
其他
由于这个简单的公交查询系统只是为了介绍VC和数据库的基础编程,无论是在事件 处理还是界面交互性上都有很大的欠缺。以下大致列出几个方面,读者可以根据自己的兴 趣对程序加以改进: (1)数据库结构设计问题:可以采用另外的组织方式,关键是方便接口实现和界面 显示。 (2)路线显示模式问题:可以将路线中各车站列表显示,便于检阅。 (3)初始化时载入车次信息的排序问题:可以便于选择车次。 (4)显示区根据匹配信息的多少动态调整问题:可以减少静态设置的冗余情况。 (5)数据源的自动设定和动态选择数据库问题:可以赋予程序更大的自主性。 以上仅是其中的几个方面,读者完全可以在本章学习的基础上充分发挥自己的想象力 和创造力,编写一个优秀的查询系统。
6.7
主要部分源代码
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);
第5章
北京市公交查询系统——数据库编程基础
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); // 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;j
void CBusDlg::OnSta() { // TODO: Add your control notification handler code here UpdateData(TRUE); m_set.m_strFilter="[bus_station] like '%"+m_bustation+"%'"; if(!m_set.Open()||m_set.IsEOF()) { AfxMessageBox("没有找到匹配数据,请重新输入"); } CString str; int i=IDC_BUSNUM1,j=IDC_BUSSTA1; while(!m_set.IsEOF()) { str.Format("%ld",m_set.m_bus_number); SetDlgItemText(i,str); str.Format("%s",m_set.m_bus_station); SetDlgItemText(j,str); i++; j++; m_set.MoveNext(); } m_set.Close(); }
第5章
北京市公交查询系统——数据库编程基础
6.8 数据库接口
191
本章知识点回顾
1. 设置用户DSN 2. 设置应用程序,确保头文件StdAfx.h中包含代码 #ifndef _AFX_NO_DB_SUPPORT #include // MFC ODBC database classes #endif // _AFX_NO_DB_SUPPORT #ifndef _AFX_NO_DAO_SUPPORT #include // MFC DAO database classes #endif // _AFX_NO_DAO_SUPPORT 3. 派生CRecordSet类 4. 实例化CDatabase和派生类 5. 利用对象的Open()函数打开数据库和表形成记录集
CDatabase类实 例对象的Open 函数
CDatabase类的对象调用函数Open打开含有ODBC驱动程序的数据源 if(!m_database.Open(NULL,//数据源说明,若为NULL,则默认表示为后面 的设定 FALSE, //通常为FALSE FALSE, //定义为TRUE时表示只读 "ODBC;DSN=bus"))//连接由DSN表示的数据源
打开ODBC 记录集
m_set.Open();
返回第一条记 录
m_set.MoveFirst();
滚动记录 m_set.MoveNext(); 增加一条记录
try { m_set.AddNew(); m_set.m_bus_number=395; m_set.m_bus_station="王府井"; m_set.Update(); } catch (CDaoException *e) { AfxMessageBox(e->m_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 <= 8) m_pPal = new CBmpPalette(this); return m_pPal ? TRUE : FALSE; } void CDIBitmap ∷ ClearPalette() { if(m_pPal) delete m_pPal; m_pPal = 0; } void CDIBitmap ∷ DrawDIB(CDC* pDC, int x, int y) { DrawDIB(pDC, x, y, GetWidth(), GetHeight()); }
209
趣味程序导学 Visual C++
210 // 显示位图
void CDIBitmap ∷ DrawDIB(CDC* pDC, int x, int y, int width, int height) { ASSERT(pDC); HDC hdc = pDC->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 << infoHeader.biBitCount); else nReturn = infoHeader.biClrUsed; return nReturn;
第7章
俄罗斯方块游戏──Visual C++应用深入
213
} DWORD CDIBitmap ∷ GetBitsPerPixel() const { ASSERT(m_pInfo); return m_pInfo->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<<14))) { // the key was not down before StartFall();//快速下降 } break; case VK_ADD: case VK_SUBTRACT: case TCHAR('+'): case TCHAR('-'): if(m_pMusic) { CVolumeCtrl dlg(m_pMusic); dlg.DoModal(); m_dwVolume = dlg.GetVolume(); } break; } } Paint(); //刷新显示 CWnd∷OnChar(nChar, nRepCnt, nFlags); }
同理,构造OnKeyUp()函数: void CGameBoard∷OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags) { if(nChar == VK_SPACE || nChar == VK_NUMPAD0) { if((nFlags & (1<<14))) {
第7章
俄罗斯方块游戏──Visual C++应用深入
225
// the key was down before StopFall(); } } else CWnd∷OnKeyUp(nChar, nRepCnt, nFlags); }
7.4.5
计时器
Windows计时器是非常有用的编程元素,使用它有时可以替代复杂的多线程编程。例 如,如果你需要读通信缓冲区,那么可以建立一个计时器,每隔100毫秒,便对积累的字 符进行检索。我们可以使用计时器来控制动画,因为计时器不依赖CPU的时钟速度。 计时器的使用很简单,只需要使用间隔参数调用CWnd成员函数SetTimer,然后,在 ClassWizard的帮助下,为产生的WM_TIMER消息提供一个消息处理函数。一旦用以毫秒 指定的周期启动了计时器,就将向窗口连续不断的发送WM_TIMER消息,直到你调用了 CWnd∷KillTimer或计时器窗口被删除。因为Windows并不是实时操作系统,所以,如果 你指定的周期小于100毫秒的话,计时器事件之间的周期可能不精确。 与其他任何的Windwos消息相比,计时器消息可以被程序中的其他处理函数所阻塞。 幸运的是,计时器消息并不会堆积起来。如果用于某计时器的消息已经存在,Windows并 不将所有的计时器消息放在一个队列之中。 void CGameBoard∷ResetTimer(BOOL bSpeed) { if(m_uTimer) KillTimer(m_uTimer); srand(time(0)); UINT uElaps = (m_usLevel < 20 && ! bSpeed) ? 500-(m_usLevel*25) : 50; m_uTimer = SetTimer(2, uElaps, 0); }
在每次消息响应中所做的处理: void CGameBoard∷OnTimer(UINT nIDEvent) { if(m_pNextPiece == 0) { m_pNextPiece = SelectPiece(); NotifyParent(FALSE); } if(m_pCurPiece == 0) { m_pCurPiece = m_pNextPiece; m_pNextPiece = 0; do {
趣味程序导学 Visual C++
226
m_clrCurPiece = RGB(rand()%255, rand()%255, rand()%255); } while(m_clrCurPiece == m_clrBackground); m_nCurLine = 1; if(m_usLevel >= 4) // 随机的起始点 m_nCurCol = rand() % (Width()-m_pCurPiece->GetColumns()) ; else // 固定起始点 m_nCurCol = (Width() - m_pCurPiece->GetColumns()) / 2 ; if(! CanPlace(m_nCurLine, m_nCurCol)) { OnGameOver(); return ; } OnNewPiece(); } else { if(! CanPlace(m_nCurLine+1, m_nCurCol)) { //判断是否能继续放置 FixPiece() ; delete m_pCurPiece ; m_pCurPiece = 0 ; CheckBoard() ; } else m_nCurLine++ ; } Paint() ; }
7.5
显示成绩和排名
游戏结束之后,需要保存玩家的成绩。用列表视图控件(CListCtrl)可以实现此功 能。 列表视图控件可用4种不同方式显示其内容。 图标视图 每一项以全尺寸图标(32×32像素)出现,下面有一个标签。用户可在列 表视图窗口拖动某项到任意位置。 小图标视图 每一项以小图标(16×16像素)出现,右边有一个标签,用户可在列表 视图窗口拖动某项到任意位置。 列表视图 每一项以小图标出现,下面有一个标签。各项按列排列,不能在列表视图 窗口中随意拖动所选项。
第7章
俄罗斯方块游戏──Visual C++应用深入
227
报表视图 每一项在本行上出现,右边有附加信息。最左边的列包含小图标和标签, 下一列包含应用程序指定的子项。 列表视图控件中的每项都含有一个图标、一个标签、一个当前状态和应用定义值(称 为“项数据”)。一个或更多子项还可与每项联系。一个“子项”是一个字符串,在报表 视图中可显示在项图标和标签右边的列里。列表视图控件中的每一项都必须与子项数目相 同。类CListCtrl提供一些函数来插入、删除、查找和更改这些项。默认时,列表视图控件 负责存储一个项的图标和文本属性。然而,除了这些项类型外,类CListCtrl支持“回调 项”。一个“回调项”是一个列表视图项,由每个应用而不是控件存储文本或图标。回调 掩码用于指定哪一项的属性(文本和/或图标)由应用程序提供。如果应用程序使用回调 项,它必须按需要提供文本和/或图标属性。 表7.2 构造函数
ClistCtrl
CListCtrl类的成员
构造一个CListCtrl对象
Create 创建列表控件并将其附加给CListCtrl对象 属性
获取列表视图控件的背景色
GetBkColor
SetBkColor 设置列表视图控件的背景色 GetImageList 获取用于绘制列表视图项的图像列表的句柄 SetImageList 指定一个图像(列表)为(列表视图)的控件 获取列表视图控件中的项数
GetItemCount GetItem
获取列表视图项的属性
GetCallbackMask
获取列表视图控件的回调掩码
SetCallbackMask 设置列表视图控件的回调掩码 查找指定特性和指定项关系的列表视图项
GetNextItem
GetFirstSeletedItemPosition
在列表视图控件中获取第一个选择的列表视图项的位置
GetNextSeletedItem 获取下一个选项的列表视图 GetItemRect
获取项的有界矩形
SetItemPosition
在列表视图控件中把某项移动到指定位置
GetItemPosition
获取列表视图项的位置
GetStringWidth 指定显示所有字符串的最小列宽 GetEditControl
获取用于编辑某项的编辑控件句柄
GetColumn 获取控件的列属性 SetColumn 设置列表视图列的属性 GetColumnWidth 获取报表视图或列表视图中的列宽度 SetColumnWidth 改变报表视图或列表视图中的列宽度 GetCheck
获取与某项相关的状态图像的当前显示状态
SetCheck 设置与某项相关的状态图像的当前显示状态 GetViewRect
获取列表视图控件中所有项的有界矩形
趣味程序导学 Visual C++
228
续表 获取列表视图控件的文本颜色
GetTextColor
SetTextColor 设置列表视图控件的文本颜色 获取列表视图控件的文本背景色
GetTextBkColor
SetTextBkColor 设置列表视图控件的文本背景色 获取最顶级项的索引
GetTopIndex
GetCountPerPage
计算可正好垂直放入列表视图控件中的项的数目
GetOrigin 获取列表视图控件的最初视图 SetItemState 改变列表视图控件的项的状态 GetItemState
获取列表视图控件的项的状态
GetItemText
获取列表视图项或子项的文本
SetItemText 设置列表视图项或子项的文本 准备一个列表视图控件以添加大量的项
SetItemCount GetItemData
获取当前项值
SetItemData
设置当前项值
GetSelectedCount 获取列表视图控件中选项的数量 SetColumnOrderArray
设置列表视图控件的列序(左或右)
GetColumnOrderArray
获取列表视图控件的列序(左或右)
SetIconSpacing 设置列表视图控件中的图标间距 GetHeaderCtr
获取列表视图控件的标题控件
GetHotCursor
获取在热调试对列表视图控件有效时使用的游标
SetHotCursor
设置在热调试对列表视图控件有效时使用的游标
GetSubItemRect
获取列表视图控件中某项的有界矩形
GetHotItem
获取当前在游标下的列表视图项
SetHotItem
设置列表视图控件的当前热项
GetSelectionMark 获取列表视图控件的选择标记 SetSelectionMark 设置列表视图控件的选择标记 GetExtendedStyle 获取列表视图控件的当前扩展风格 SetExtendedStyle 设置列表视图控件的当前扩展风格 SubItemHitTest GetWorkAreas
指定哪个列表视图项在指定位置 获取列表视图控件的当前工作区
GetNumberOfWorkAreas
获取列表视图控件的当前工作区数量
SetItemCountEx 设置虚列表视图控件的项的数量 SetWorkAreas 设置列表视图控件中图标可以显示的区域 ApproximateViewRect
指定显示列表视图控件项所需的宽度和高度
GetBkImage 获取列表视图控件的当前背景图像 SetBkImage 设置列表视图控件的当前背景图像
第7章
俄罗斯方块游戏──Visual C++应用深入
229 续表
GetHoverTime
获取列表视图控件的当前逗留时间
SetHoverTime 设置列表视图控件的当前逗留时间 操作
InsertItem
在列表视图控件中插入新项
DeleteItem 从控件中删除一项 DeleteAllItems 从控件中删除所有项 构造函数
FindItem 查找具有指定的字符的列表视图项 SortItems HitTest
应用比较函数排序列表视图项 指定哪个列表视图在指定的位置上
EnsureVisible 保证项是可见的 Scroll 滚动列表视图控件的内容 RedrawItems Update
强迫列表视图控件刷新一些项
强迫控件刷新一个指定的项
Arrange 调整一栏里的项 EditLabel 文本编辑处开始项 InsertColumn 插入列表视图控件中的新列 DeleteColumn 从列表视图控件中删除一列 CreateDragImage 为指定的项构造一个拖动图像列表
往列表视图中添加新的项: void CScoreDlg ∷ AddHiScore(UINT uScore, UINT uLevel) { int index = 0; const_iterator itend(m_ScoreArray.end()); iterator it(m_ScoreArray.begin()); for(; it != itend; ++it, ++index) if(uScore > 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] <= '9') pBmp = &m_bmpDigit[str[i]-'0']; else pBmp = &m_bmpDigit[BLANK]; } else { // 右对齐 const int nSpaceLeft = (m_nNumOfDigits - len < 0) ? 0 : (m_nNumOfDigits - len); if(i >= nSpaceLeft && str[i-nSpaceLeft] >= '0' && str[inSpaceLeft] <= '9') pBmp = &m_bmpDigit[str[i-nSpaceLeft]-'0']; else pBmp = &m_bmpDigit[BLANK]; }
第7章
俄罗斯方块游戏──Visual C++应用深入
241
ASSERT(pBmp != 0); CBitmap* pBmpOld = dcMem.SelectObject(pBmp); dc.StretchBlt(nWidth*i, 0, nWidth, nHeight, &dcMem, 0, 0, m_BM.bmWidth, m_BM.bmHeight, SRCCOPY); dcMem.SelectObject(pBmpOld); } dcMem.DeleteDC(); }
在绘制的过程中,首先创建一个用于绘制的CDC对象,用pBmp保留原有DC的位图信 息。并根据所需显示的内容,将不同的位图图片选入CDC中。用StretchBlt 函数来显示图 片,显示结束后,将旧的位图句柄选入,并及时删除CDC对象,因为系统可使用的DC数 目是有限的。 在 这 里 , 程 序 使 用 MFC提 供 的 数 据 传 输 机 制 来 传 递 数 据 。 DDX 是 Dialog Data Exchange(对话框数据交换)的缩写,编写DDX全局函数用以数据交换。 void DDX_DigiDisplay(CDataExchange* pDX, int nIDC, UINT & uNumber) { HWND hWndCtrl = pDX->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]<0) //创建不成功 return FALSE; sinLocal.sin_family=AF_INET; //SOCKADDR_IN要求必须如此 sinLocal.sin_port=htons(m_wPort); //把m_Port转换成TCP/IP格式 gethostname(szHostName,60); //得到本地机器的主机名
第8章
属于你的 OICQ——Visual C++ 网络编程
269
pHost=gethostbyname(szHostName); //由主机名得到指向HOSTENT的指针 if(pHost==NULL) //失败 return FALSE; memcpy((LPSTR)&(sinLocal.sin_addr), //把得到的主机信息存入Socket说明 (LPSTR)pHost->h_addr, pHost->h_length); iStatus=bind(m_sockUse[0],(struct sockaddr FAR*)&sinLocal, sizeof(sinLocal)); //把m_sockUse[0]绑定在本地主机上,为以后的listen,connect作准备 if(iStatus<0) //失败 { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } if(0>(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]<0) return FALSE; sinLocal.sin_family=AF_INET; sinLocal.sin_port=htons(m_wPort); gethostname(szHostName,60); pHost=gethostbyname(szHostName);
第8章
属于你的 OICQ——Visual C++ 网络编程
271
if(pHost==NULL) return FALSE; memcpy((LPSTR)&(sinLocal.sin_addr), (LPSTR)pHost->h_addr, pHost->h_length); iStatus=bind(m_sockUse[0],(struct sockaddr FAR*)&sinLocal, sizeof(sinLocal)); if(iStatus<0) { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } if(0>(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]<0) //失败 { SendMessage(strListens.hWnd, //送回一个失败的消息 strListens.uMessage, 0, WSAMAKESELECTREPLY(FD_ACCEPT,1)); return 0;
第8章
属于你的 OICQ——Visual C++ 网络编程
} sinLocal.sin_family=AF_INET; sinLocal.sin_port=htons(strListens.wPort); gethostname(szHostName,60); pHost=gethostbyname(szHostName); if(pHost==NULL) return FALSE; memcpy((LPSTR)&(sinLocal.sin_addr), (LPSTR)pHost->h_addr, pHost->h_length); iStatus=bind(strListens.psockUse[0],(structsockaddr FAR*)&sinLocal, sizeof(sinLocal)); if(iStatus<0) { closesocket(strListens.psockUse[0]); //返回失败消息 strListens.psockUse[0]=-1; SendMessage(strListens.hWnd, strListens.uMessage, 0, WSAMAKESELECTREPLY(FD_ACCEPT,1)); return 0; } if(0>(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)
第8章
属于你的 OICQ——Visual C++ 网络编程
return FALSE; iStatus=connect(m_sockUse[0],(struct sockaddr*)&sinServer,sizeof(sinServer)); //建立一个指向Server的连接,并将这个连接绑定到m_sockUse[0]上 if(iStatus<0) { closesocket(m_sockUse[0]); m_sockUse[0]=-1; return FALSE; } else return TRUE; }
M
注意:这里我们用的是winsock库的connect()函数,这个函数在建立连接的 同时也绑定了相应的Socket,这以后我们就可以用send()和recv()函数进行 数据传输。我们也定义了两个函数Read()和Write(),并提供了基于recv() 和send()的传输,但由于我们的类中已经有了相应的Socket结构,所以提供 给编程人员的是一个比recv()和send()要简单的编程接口,相应代码如下:
int CTCPSocket::Read(LPSTR pRead,DWORD dwRead) // { DWORD dwR=0; DWORD dwLeft=dwRead; SOCKET sockRead=GetCommSocket();
// 判断到底是等待方还是连接方,并得到 // 相应的Socket描述符
while(dwLeft) { dwR=recv(sockRead,pRead+dwRead-dwLeft,dwLeft,0); //接收数据 if(dwR==-1) { char szError[100]; sprintf(szError,"%X Read Error",WSAGetLastError()); return -1; } dwLeft-=dwR; } return (dwRead-dwLeft); } int CTCPSocket::Write(LPCSTR pWrite,DWORD dwWrite) { DWORD dwW=0;
275
趣味程序导学 Visual C++
276 DWORD dwLeft=dwWrite;
SOCKET sockWrite=GetCommSocket(); while(dwLeft) { dwW=send(sockWrite,pWrite+dwWrite-dwLeft,dwLeft,0); //发送数据 if(dwW==-1) return -1; dwLeft-=dwW; } return (dwWrite-dwLeft); }
补充说明 (1)我们用GetCommSocket()来判断是等待方还是发送方: SOCKET CTCPSocket::GetCommSocket(void) { return (IsListenSide())?m_sockUse[1]:m_sockUse[0]; }
(2)用Established()读消息事件或者Socket关闭时发送由m_uRead所定义的消息: BOOL CTCPSocket::Established(HWND hWndOwner,UINT uRead) { ASSERT(hWndOwner); m_uRead=uRead; int iStatus=WSAAsyncSelect(GetCommSocket(),hWndOwner, m_uRead,FD_READ|FD_CLOSE); return !(iStatus>0); }
(3)CloseListenSocket()用来断开用于等待的m_sockUse[0]描述符: void CTCPSocket::CloseListenSocket(void) { if(!IsListenSide()) return; 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是对程序员的基本要求!