445
目录 i 本人并不是一个软件工作者,也没有从事过软件开发方面的工作,充其量只能算是一 C++语言的爱好者。 最早接触 C++语言是在西北大学物理系读书的时候,那时学习 C++完全是出于一种时 尚,因为教 FORTRAN 的老师说:“C 语言是现在比较流行的语言”!既然 C 语言流行,那 么比 C 加了一点的 C++就应该更流行!从而在没有学完 FORTRAN 的时候,就捧起了 C++ 狂啃不止。 然而在接触了 C++之后不久,就被这种优美的语言所深深的吸引,也许正是因为在 C++里面这种对事物的抽象描述更符合一个学物理人的思维过程,从那时起我便和 C++下了不解之缘。从那个时代的 Turbo C++ Borland C++再到 Visual C++最后锁定在了 C++ Builder,这么多年来 C++一直伴随着我的生活。 或许是因为受到崇拜爱因斯坦而学了物理的影响,生活中总喜欢使用运动规律来解释 一切,也总是喜欢尝试着用 C++来描述物质的运动,却总是感觉 C++对事物的描述不够完 整,直到使用了 C++ Builder VCL 技术之后,才发觉 VCL 对客观世界的描述更为合理、 更符合自然规律。于是萌生了写这本书的想法,并为书取了名字《面向状态的程序设计与 C++ Builder面向状态并不是一个公认的叫法,大多数作品中还将其称之为面向对象,然而作者认 为在 C++ Builder 中的“面向对象”已经和经典的 C++中的面向对象大不相同,从而将其 命名为“面向状态”。在经典的 C++语言中对象的模型只是对事物的一个静态的描述,而 没有反映出事物的动态特征;在 C++ Builder 中,对象不仅抽象出了静态特征,而且从更 高的层次对事物的动态特征进行了抽象,反映的是比较完整的客观规律;程序设计的核心 很大程度上从对象的静态关系转移到状态的约束关系上。因此,面向状态实际上是从面向 对象发展而来的一种新的程序设计思想。 面向状态只是作者的一个叫法,本书中的内容也是非常不成熟的,但作者希望能通过 本书给读者一个抛砖引玉的作用。也希望通过本书达到共同学习、进步的目的。 本书中没有采用类似数学教学中常用的直线型结构,而是采用了类似物理教学中的螺 旋式结构,即大多数概念、思想在本书的多个章节中都有讲解和描述,但各个章节的侧重 点有所不同,属于螺旋式上升。这样的目的是读者阅读时能够瞻前而顾后,对于整个书的 内容建立广泛的联系,有助于阅读和理解。 本书内容分为两篇,第一篇从理论的角度来讲述面向状态的思想及 C++ Builder 的语 言特点,总共包含四章。第一章程序设计的发展,简单介绍了程序设计的发展过程,并且 引入了面向状态的思想。第二章面向对象的程序设计,回顾了 C++语言的思想及语法特点, 熟悉的读者可以越过。第三章面向状态的程序设计思想,是本书的核心内容,第三章结合 客观规律从理论的角度讲述了面向状态的思想,并针对 C++ Builder VCL 类库对这 种思想进行了总结和归纳。第四章 C++ Builder 语言的扩充,着重就 C++ Builder 中为了支 持面向状态而对标准 C++语言进行了扩充的部分作了介绍,并对 VCL 类库的基本结构和

read.pudn.comread.pudn.com/downloads43/ebook/146163/面向状态的程序设计.pdf · 目录 i 前 言 本人并不是一个软件工作者,也没有从事过软件开发方面的工作,充其量只能算是一

  • Upload
    others

  • View
    20

  • Download
    0

Embed Size (px)

Citation preview

目录 i

前 言

本人并不是一个软件工作者,也没有从事过软件开发方面的工作,充其量只能算是一

个 C++语言的爱好者。 最早接触 C++语言是在西北大学物理系读书的时候,那时学习 C++完全是出于一种时

尚,因为教 FORTRAN 的老师说:“C 语言是现在比较流行的语言”!既然 C 语言流行,那

么比 C 加了一点的 C++就应该更流行!从而在没有学完 FORTRAN 的时候,就捧起了 C++狂啃不止。

然而在接触了 C++之后不久,就被这种优美的语言所深深的吸引,也许正是因为在

C++里面这种对事物的抽象描述更符合一个学物理人的思维过程,从那时起我便和 C++结下了不解之缘。从那个时代的 Turbo C++到 Borland C++再到 Visual C++最后锁定在了 C++ Builder,这么多年来 C++一直伴随着我的生活。

或许是因为受到崇拜爱因斯坦而学了物理的影响,生活中总喜欢使用运动规律来解释

一切,也总是喜欢尝试着用 C++来描述物质的运动,却总是感觉 C++对事物的描述不够完

整,直到使用了 C++ Builder 的 VCL 技术之后,才发觉 VCL 对客观世界的描述更为合理、

更符合自然规律。于是萌生了写这本书的想法,并为书取了名字《面向状态的程序设计与

C++ Builder》 面向状态并不是一个公认的叫法,大多数作品中还将其称之为面向对象,然而作者认

为在 C++ Builder 中的“面向对象”已经和经典的 C++中的面向对象大不相同,从而将其

命名为“面向状态”。在经典的 C++语言中对象的模型只是对事物的一个静态的描述,而

没有反映出事物的动态特征;在 C++ Builder 中,对象不仅抽象出了静态特征,而且从更

高的层次对事物的动态特征进行了抽象,反映的是比较完整的客观规律;程序设计的核心

很大程度上从对象的静态关系转移到状态的约束关系上。因此,面向状态实际上是从面向

对象发展而来的一种新的程序设计思想。 面向状态只是作者的一个叫法,本书中的内容也是非常不成熟的,但作者希望能通过

本书给读者一个抛砖引玉的作用。也希望通过本书达到共同学习、进步的目的。 本书中没有采用类似数学教学中常用的直线型结构,而是采用了类似物理教学中的螺

旋式结构,即大多数概念、思想在本书的多个章节中都有讲解和描述,但各个章节的侧重

点有所不同,属于螺旋式上升。这样的目的是读者阅读时能够瞻前而顾后,对于整个书的

内容建立广泛的联系,有助于阅读和理解。 本书内容分为两篇,第一篇从理论的角度来讲述面向状态的思想及 C++ Builder 的语

言特点,总共包含四章。第一章程序设计的发展,简单介绍了程序设计的发展过程,并且

引入了面向状态的思想。第二章面向对象的程序设计,回顾了 C++语言的思想及语法特点,

熟悉的读者可以越过。第三章面向状态的程序设计思想,是本书的核心内容,第三章结合

客观规律从理论的角度讲述了面向状态的思想,并针对 C++ Builder 的 VCL 类库对这

种思想进行了总结和归纳。第四章 C++ Builder 语言的扩充,着重就 C++ Builder 中为了支

持面向状态而对标准 C++语言进行了扩充的部分作了介绍,并对 VCL 类库的基本结构和

目录 ii

部分重要的属性、方法及事件作了简单的介绍。第二篇为应用篇,结合实际例程,讲述如

何使用面向状态的技术,共分三章。第五章使用 C++ Builder,主要针对 C++ Builder 扩充

的关键字,就面向状态的使用作了简单介绍。第六章控件设计示例,介绍了四个不同方面

的控件设计的例子,通过这些例子来实践掌握面向状态的技术。第七章应用程序中的面向

状态,本书一个很重要的目的就是让读者能够掌握将 VCL 的组件设计技术应用到应用程

序的设计中,在第七章中通过两个完整的示范程序来介绍如何在应用程序中使用面向状态

的设计技术。 本书中给出的代码均经过调试,其中关于 C++语言部分的使用 Borland C++ 5.02 和

Visual C++ 6.0 编译,关于 C++ Builder 的代码大部分在 C++ Builder 5 中调试通过,第六、

七章中的例子代码均在 C++ Builder6.0 中调试通过,由于 6.0 和之前的版本在资源存储上

格式稍有改动,因此无法兼容以前版本。 在此我要感谢在 C 语言、C++语言以及 C++ Builder 领域工作的各位先辈,他们为我

能够写完这本书而提供了非常有价值的参考资料。同时在这里还要感谢 www.Delphibbs.net上的“Ai”、“Aleyn”、“Gmto”、“不不”等网友,他们的讨论使我对写这本书更具信心。

在撰写这本书的近两年时间里,我的妻子和家人给了我极大的关怀和支持,我在此一并表

示感谢。最后我向本书的所有读者表示衷心的感谢,感谢大家的支持和爱戴。 再次声明:作者并不是软件工作者,也没有系统的学习过软件开发的知识,纯属个人

爱好的原因而写本书,因此书中错误之处还希望读者能够谅解并提出批评指正。

作 者 二○○二年岁末

目录 iii

目 录

前 言 ..................................................................................................................... I

目 录 ................................................................................................................... III

第一篇 理解面向状态 ...............................................................................................1

第一章 程序设计的发展 ........................................................................................1 1 机器语言及汇编语言 ........................................................................................1 2 高级语言..........................................................................................................2 3 结构化程序设计 ...............................................................................................3 4 面向对象的程序设计 ........................................................................................4 5 面向状态的程序设计 ........................................................................................6 6 小结............................................................................................................... 11

第二章 面向对象的程序设计 ...............................................................................13 1 对象的基本概念.................................................................................................13

1.1 对象的现实含义 ......................................................................................13 1.2 对象的划分和联系...................................................................................14 1.3 类和类的实例..........................................................................................15

2 C++语言的扩充 ..................................................................................................15 2.1 类 Class...................................................................................................15 2.2 ::作用域运算符 ........................................................................................22 2.3 new、delete .............................................................................................22 2.4 &引用 .....................................................................................................23 2.5 inline 关键字............................................................................................25 2.6 const .......................................................................................................25

3 C++中对象的封装性 ...........................................................................................26 3.1 对象的封装 .............................................................................................27 3.2 友员的使用 .............................................................................................31

4 C++的继承性与类的派生 ....................................................................................34 4.1 类的派生.................................................................................................34 4.2 多继承 ....................................................................................................43

5 C++中的多态性和重载........................................................................................56 5.1 函数重载.................................................................................................56 5.2 运算符重载 .............................................................................................63 5.3 虚函数 ....................................................................................................68

6 面向对象的设计过程..........................................................................................80

目录 iv

6.1 几种软件设计方法简介............................................................................81 6.2 软件的生命周期 ......................................................................................84 6.3 面向对象开发的特征 ...............................................................................85

7 小结 ..................................................................................................................87

第三章 面向状态的程序设计思想 ........................................................................88 1 从 Hello World 开始............................................................................................88

1.1 问题与分析 .............................................................................................88 1.2 Hello World 的实现 .................................................................................89

2 什么是面向状态.................................................................................................92 2.1 状态的概念 .............................................................................................92 2.2 状态的特征 .............................................................................................95 2.3 面向状态的概念 ......................................................................................97 2.4 面向状态的优点 ......................................................................................98

3 面向状态的基本思想........................................................................................ 116 3.1 基于事件驱动的程序设计 ...................................................................... 116 3.2 属性与状态 ........................................................................................... 125 3.3 事件的概念 ........................................................................................... 129 3.4 面向状态的特征 .................................................................................... 135 3.5 如何面向状态........................................................................................ 143

4 属性之间的关系............................................................................................... 144 4.1 同一对象中属性之间的关系................................................................... 144 4.2 对象之间的关系 .................................................................................... 150 4.3 不同对象中的属性之间的关系 ............................................................... 153

5 小结 ................................................................................................................ 154

第四章 C++ BUILDER 语言的扩充 ...................................................................... 157 1 关键字扩充...................................................................................................... 157

1.1 Ansi C++关键字的扩充 .......................................................................... 158 1.2 类声明列表关键字__declspec ................................................................. 159 1.3 C++ Builder 扩充变量类型 ..................................................................... 162 1.4 函数调用规则与返回 ............................................................................. 162 1.5 函数和数据的输入输出.......................................................................... 165 1.6 __try、__except、__finally 关键字.......................................................... 166 1.7 VCL 类库关键字扩充 ............................................................................ 167 1.8 其他 C++ Builder 扩充 ........................................................................... 179

2 VCL 类库层次结构 ........................................................................................... 181 2.1 区分 VCL 类库、组件和控件 ................................................................. 181 2.2 TObject 基类.......................................................................................... 182 2.3 TPersistent 分支..................................................................................... 190 2.4 TComponent 分支................................................................................... 198

目录 v

2.5 TControl 分支 ........................................................................................ 217 3 C++ Builder 的资源 ........................................................................................... 223

3.1 Windows 资源........................................................................................ 224 3.2 C++ Builder 资源 ................................................................................... 230

4 C++ Builder 的异常处理 .................................................................................... 239 4.1 C++异常处理......................................................................................... 239 4.2 Win32 下的结构异常.............................................................................. 243 4.3 VCL 异常处理 ....................................................................................... 250

5 小结 ................................................................................................................ 253

第二篇 应用 C++ BUILDER 的面向状态............................................................... 255

第五章 使用 C++ BUILDER ................................................................................ 255 1 C++ Builder 中的命名约定................................................................................. 255

1.1 类的命名............................................................................................... 256 1.2 属性的命名 ........................................................................................... 256 1.3 事件的命名 ........................................................................................... 257 1.4 方法的命名 ........................................................................................... 258

2 创建属性 ......................................................................................................... 258 2.1 属性的必要性........................................................................................ 258 2.2 属性的类型 ........................................................................................... 259 2.3 发布继承的属性 .................................................................................... 260 2.4 定义属性............................................................................................... 260 2.5 创建数组属性........................................................................................ 264 2.6 存储和载入属性 .................................................................................... 265

3 创建事件 ......................................................................................................... 270 3.1 什么是事件 ........................................................................................... 270 3.2 实现标准事件........................................................................................ 272 3.3 定义你自己的事件................................................................................. 274

4 创建方法 ......................................................................................................... 277 4.1 避免相关性 ........................................................................................... 277 4.2 保护方法............................................................................................... 278 4.3 虚拟方法............................................................................................... 278 4.4 声明方法............................................................................................... 278

5 在组件中使用图形 ........................................................................................... 279 5.1 图形概述............................................................................................... 279 5.2 使用画布............................................................................................... 281 5.3 使用图片............................................................................................... 281 5.4 幕后位图............................................................................................... 283 5.5 响应改变............................................................................................... 284

目录 vi

6 处理消息 ......................................................................................................... 285 6.1 理解消息处理系统................................................................................. 285 6.2 修改消息处理........................................................................................ 289 6.3 创建新的消息处理器 ............................................................................. 292

7 使组件在设计时可用........................................................................................ 293 7.1 注册组件............................................................................................... 294 7.2 添加组件面板位图................................................................................. 297 7.3 为你的组件提供帮助 ............................................................................. 297 7.4 添加属性编辑器 .................................................................................... 299 7.5 添加组件编辑器 .................................................................................... 303 7.6 将组件编译成软件包 ............................................................................. 307 7.7 解决定制组件问题................................................................................. 307

8 小节 ................................................................................................................ 307

第六章 控件设计示例........................................................................................ 309 1 组合控件 TPickEdit .......................................................................................... 309

1.1 控件的分析 ........................................................................................... 310 1.2 源代码 .................................................................................................. 311 1.3 控件的详解 ........................................................................................... 318

2 图形控件 TDataPick.......................................................................................... 321 2.1 控件的分析 ........................................................................................... 321 2.2 源代码及注解........................................................................................ 322 2.3 控件的详解 ........................................................................................... 336

3 创建属性编辑器和控件的缺省动作................................................................... 338 3.1 控件简介............................................................................................... 338 3.2 源代码 .................................................................................................. 338 3.3 控件的详解 ........................................................................................... 349

4 创建数据控件 TDBJpegImage ........................................................................... 352 4.1 控件的分析 ........................................................................................... 352 4.2 源代码 .................................................................................................. 353 4.3 控件详解............................................................................................... 358

5 小结 ................................................................................................................ 363 第七章 应用程序中的面向状态.......................................................................... 365

1 面向状态在应用程序和组件中的区别 ............................................................... 365 1.1 应用程序中的属性................................................................................. 365 1.2 应用程序中的事件................................................................................. 367

2 示范程序 Sells .................................................................................................. 368 2.1 程序功能的介绍 .................................................................................... 368 2.2 状态的分析 ........................................................................................... 373 2.3 实现代码及介绍 .................................................................................... 374

目录 vii

2.4 总结...................................................................................................... 404 3 示范程序 Drawing ............................................................................................ 405

3.1 MDI 主窗体对象 TForm2 类................................................................... 406 3.2 MDIChild 对象 Main 单元 ...................................................................... 412 3.3 TDraw 对象........................................................................................... 416 3.4 个体编辑............................................................................................... 422 3.5 整体编辑............................................................................................... 430 3.6 文件存取功能........................................................................................ 433 3.7 使用剪贴板 ........................................................................................... 437 3.8 总结...................................................................................................... 440

4 小节 ................................................................................................................ 441

第一章 程序设计的发展 第 1 页

第一篇 理解面向状态

第一章 程序设计的发展

从世界上第一台计算机问世到现在,已经有 50 多年的历史了,计算机程序设计也经

历了从简单到复杂,从功能单一的程序设计到大型软件开发的过程。在这期间,计算机的

体积是越来越小,而软件的“体积”却是越来越大,软件实现的功能也越来越强大。计算

机应用的核心逐渐由硬件向软件过渡已经成为一个不争的事实。 本章将从计算机语言和程序设计的发展讲起,简单介绍在这半个多世纪计算机语言演

变的过程,帮助读者更好的理解本书将要讲述的面向状态的编程方法。

1 机器语言及汇编语言

在最初期,程序设计用的使机器语言,即将程序中机器执行的“0”“1”代码按照相

应的功能排成一个序列,而且那时候没有现在的硬盘、显示器、键盘等设备,这些“0”和“1”组成的代码序列需要手工编制在纸带等存储设备中,需要运行时,将纸带放入纸

带读写器中,计算机依次读入信息,运算完成后在将计算结果打在纸带上。 可以想象,这样的编程过程是非常庞大、非常繁琐,调试和修改也都非常的复杂,当

然计算机的功能和用途也受到很大的限制。而且,机器语言最大的问题是其指令是针对机

器而言的,对于不同的计算机,即使执行同一个操作,其指令代码也是不相同的,或者说,

不同的计算机有不同的指令系统。例如有的计算机的指令长度为 8 位,有的机器则为 16位或 32 位,显然,第一个机器上的程序指令代码拿到第二个计算机上是不能够执行,因

此机器语言是依赖于具体计算机硬件,而不是各类计算机通用,它是面向机器的。 之后,随着技术的发展,人们发明了键盘和显示器等设备,同时也寻求更先进的编程

语言。由于机器语言指令繁琐难记,人们想到可以用一些特定的符号来代替某些操作。例

如用 ADD代表“加”运算,SUB代表“减”运算,LD代表“传送”AX代表微处理器的寄

存器等等,这就是后来的汇编语言。 例如在 IBM兼容 PC机中: MOV AX,A MOV BX,B ADD AX,BX 表示,将 A和读入 AX寄存器,将 B读入 BX寄存器,然后相加,而把这一段代码翻

第一章 程序设计的发展 第 2 页

译成机器指令,则需要 8 个字节,也就是 64 位“0”“1”代码,显然,汇编语言比机器

指令容易记也容易学的多,其中 ADD、SUB、LD、AX 等被称作助记符,因此汇编语言也

称作“符号语言”。 但是计算机并不能直接识别和执行符号语言的指令,必须先将这些符号转换成为二进

制代码机器才能执行,一般来说一条汇编指令对应的翻译成为一条机器指令,而且这种翻

译工作通常是由计算机来完成的,我们把这个翻译程序称作:“汇编程序”。 一般来说,汇编语言随着不同计算机而有所不同,每一类型的计算机分别有自己的汇

编语言,汇编语言也是面向机器的。用汇编语言编写程序,也需要了解计算机内部的结构,

对使用者的要求较高,因此也不易推广普及。但是汇编语言程序和机器语言一样,执行速

度快、占用存储器空间小。随着计算机技术的发展,除了单板机、操作系统的内核、底层

的驱动程序和某些病毒程序之外,一般的应用程序几乎都不再使用汇编语言。

2 高级语言

由于机器语言和汇编语言是面向机器,不同的机器有不同的汇编语言,可移植性差、

又难学难懂,可维护性差,而且每一条机器代码都对应一行汇编代码,通常原代码比汇编

后的可执行代码体积还要大。因此人们希望能找到一种工具语言,更接近自然语言方式,

易学易懂,又容易在其它计算机上使用,这就是后来的高级语言。 高级语言又称作面向过程的语言,面向过程的意思是:程序员不必考虑机器内部构造

和特点,只需要针对所要解决问题而编写出相应的过程,不同的计算机会用不同的编译程

序将其翻译成为可以运行的机器代码,这个翻译程序就叫做“编译器”。 由于编译器是一种中间工具,同一个源程序在不同的编译器下面,可能会被编译成不

同的机器代码,但是其执行结果是相同的,这正是高级语言和汇编语言所不同的一个特点,

例如同样一个标准 C++的程序源代码在 Visual C++和 Borland C++ 或 C++ Builder下编译的结果可能有所不同,实现效率也有所不同,但是他们的运行状况和执行结果却是

完全一样的。因此,我们有理由也有必要选择优秀的或者适合于自己使用的编译器。 目前使用比较多的高级语言有:BASIC、FORTRON、PASCAL、C、C++、ADA、COBOL、

LISP等等。 在这些语言中 CALBOL 是适合一种管理领域应用的高级语言;FORTRON 是出现最早

的一种高级语言,适合于数值计算;PASCAL是出现最早的一种结构化语言,常用于教学

中;LISP和 ADA基本上是纯粹的面向对象的编程语言,属于人工智能型语言;C则是上

一世纪 80 年代到 90 年代初最流行的编程语言,在系统软件和应用软件领域都有着广泛

的应用;C++则是在 C语言的基础上进行扩充并增加了面向对象的机制而形成的一种面向

对象的程序设计语言,同时 C++语言完全兼容 C语言,很多用 C语言编写的程序稍加改动

或不作该动就可以利用 C++编译器进行编译,因此 C++语言一出现就赢得了广大程序设计

者的喜爱,本书就是介绍如何运用 C++编译器的一个家族 C++ Builder 中提供的新的编

程方法,来编写更实用、更优秀的程序。

第一章 程序设计的发展 第 3 页

3 结构化程序设计

高级语言的出现克服了汇编语言的许多缺点,当同时也出现了新的问题,由于技术的

发展,计算机所能胜任的工作也越来越多,软件的规模也越来越大,流程越来越复杂,在

这些复杂的流程中,很难有按顺序一条一条代码往下执行的情况,会出现很多的条件和非

条件“跳转”。如下图是一个判断给定年份是否为闰年的一个流程:

在这一个短短的流程中就出现了 3、4 次跳转,在一个大型程序中,这种跳转是非常

多的,若程序也按照这样的流程去设计,将会混乱不堪难以阅读和理解,最初的 FORTRON语言中就包含着 GOTO语句,是从汇编语言的 Jump语句演化而来的,这个语句允许程序

员无条件地、丝毫不受限制地从程序代码中跳转来跳转去,这样编出来的程序就难以阅读、

难以修改,从而使软件的开发受到了很大的制约。 这是一个方法论的问题,于是众多的软件工作者便试图从实现方式的改变来解决这种

年份 =?Year

入口

结果

结束

N

Y

Y

N

N Y打印Year是闰年

打印Year不是闰年

打印Year不是闰年

打印Year是闰年

Year是否被100整除

Year是否被4整除

Year是否被400整除

图 1.1 判断 Year是否为闰年的流程

第一章 程序设计的发展 第 4 页

混乱的局面。 到上一世纪 60 年代,便出现了结构化程序设计。结构化程序设计的设计思想是:自

顶向下、逐步细化、模块化设计、结构化编码。其程序结构是按功能划分为若干个基本模

块,这些模块形成一个树状结构;各模块之间的关系尽可能简单,在功能上相对独立;每

一模块内部均由几种基本结构组成;其模块化实现的具体方法是使用子程序,因此,在各

种高级语言里基本都支持子程序。 1966年,Bobra 和 Jacopini提出了三种基本结构,并且现在已经证明任何复杂的

算法都可以由这三种基本结构组合而成,这三种基本结构是:顺序结构、选择结构、循环

结构,这三种基本结构的共同点是:只有一个入口、只有一个出口、结构内每一部分都有

机会被执行到即没有落空代码、机构内不存在“死循环”。 由于模块化的设计和结构化的编码,可以将一个较为复杂的程序系统分解为许多个容

易控制和容易处理的子任务,而每一个子任务都是可以独立编程的子程序模块,而每个模

块内又都是由若干个基本结构组成,这样程序的代码就非常清晰,界面明确,使用起来也

相对简单容易。 这在上一世纪 70 年代到 80 年代各个领域开发软件所都采用的设计方法,但从本质

上来讲还是面向过程的程序设计,其数据和过程是相互独立,并没有内在联系,程序员在

编程时必须考虑其代码所要处理的数据,以及数据格式,有时候可能还会出现数据和处理

程序不匹配的情况,而且设计出来的程序可重用性也差,这是结构化程序设计在所难免的,

正是这些问题促使了软件开发技术的进一步发展。

4 面向对象的程序设计

和计算机发展初期相比较,今天的计算机软件已经有了翻天覆地的变化,尤其是图形

界面出现以后,计算机软件更是由最初的抽象化变得越来越形象化,程序也逐渐由纯粹的

“计算机思维方式”向“人类的思维方式”转变,在软件系统工程中,著名的 Jackson方法就是要求程序中实现的算法应该尽量和人的思维方式一致,这样,所规定的程序才具

有更好的适用性和可维护性。如: “打印 10级杨辉三角”这一个简单问题,你可以有两种办法实现: 一.直接打印出来,实现方法如下 begin

print 1 print 1 1 print 1 2 1 print 1 3 3 1 ⋯⋯ ⋯⋯ ⋯

end

第一章 程序设计的发展 第 5 页

二.按照杨辉三角的构成规律,让计算机运算: begin

arrey Data[10][10] //定义 10×10数组 Data[1] = 1 do I = 2 until I > 10

do j = 1 until j > I If j = 1 Data[I][j] = 1 Else If j = I Data[I][j] = 1 Else Data[I][j] = Data[I-1][j-1] + Data[I-1][ j ] End If print Data[I][j]

end do print ↙ //换行

end do end 在这个例子中,第二种方法比第一种方法更加容易维护,比如:将要求改为打印 20

级杨辉三角,第二种方法只需将数组下标和循环次数改为 20 就可以,而第一种方法最需

要重新输入一些代码。而实际的程序中,所用到算法比这个例子复杂得多,即使在设计过

程中,也可能多次不断的对代码进行修改,因此,就要求计算机语言尽可能和人的意识及

思维方式一致。 面向对象的程序设计思想就是从这里产生的,其主要思想是:将计算机程序设计问题

抽象化,建立符合人类思维习惯和认识过程的模型。这种模型在程序中称为对象,程序设

计是以对象为核心的。 人类总是将相关性较强的几个物体联系在一起,而抽象的表达为一个新的物体或一个

新的类别,这种抽象的认识过程,就可以认为是一种对象的生成。如:由车轮、车厢组成

并可以移动的物体,我们称之为车;装有四个车轮、方向盘和汽油发动机的车叫做汽车等

等,都是对象的生成过程。可见面向对象是一个不断抽象和延续的过程。 在计算机程序设计过程中,程序员使用面对的可能有很多的数据以及对这些数据进行

处理的程序过程,这些过程和数据之间的关系错综复杂,程序员要处理这些复杂的关系往

往会消耗很大的精力,面向对象的设计思想就是将关系比较密切的数据和处理过程有效地

组织在一起,形成一个相对独立的个体——对象,在对象内部采用数据抽象和信息隐藏的

技术,程序员所做的是生成对象和处理对象与对象之间的联系。 面向对象的最大优点是提高了软件的生产率和解决了软件复杂性控制的问题。目前,

比较常用的开发软件都是面向对象型,比如:C++的各种编译器,Turbo Pascal、Object

第一章 程序设计的发展 第 6 页

Pascal、Visual C++、Visual Basic、Java 的各种开发工具等等,都属于面向对

象的开发工具。 面向对象的程序设计方法本身和结构化程序设计并不矛盾,前者也是在后者的基础上

发展而形成了,在一个对象的内部也应该是由许多基本结构组成的。面向对象的程序虽然

降低了代码的执行效率,但是却大大提高软件的开发速度,提高了软件的可维护性,使代

码共享更加容易。许多编译器和开发工具包都习惯了许多可以给我们利用的基本类库,如:

Borland C++的 OWL(Object Windows Library)、Microsoft的 MFC(Microsoft Foundation Class)以及 AutoCAD 2000 的 Object ARX 类库等。应用这些类库我

们就可以很方便地开发出功能非常强大的应用软件。目前,面向对象的程序设计已经成为

软件开发的主流。

5 面向状态的程序设计

首先声明的是:面向状态的程序设计并不是一个公认的叫法,很多书籍里都将其归结

为面向对象的程序设计,而且试图以面向对象的思想解释整个过程,似乎不尽完善。作者

根据多年的 C++使用经验,将其称为面向状态以区别于面向对象,本书将试图从理论的角

度对这种新的编程思想加以分析、描述,并结合一些实例分析,希望能对 C++ Builder的初学者更好的使用该软件有所帮助;对喜欢钻研 C++ Builder的读者起到抛砖引玉的

作用。 面向状态的技术最早是出现在可视化编程工具中,如:Visual Basic、Visual

FoxPro、Delphi、C++ Builder、Java Beans 等,基本上都使用了面向状态的技术,

ActiveX、Com等也都是使用了面向状态的技术,应该说是流派比较多。本书主要介绍的

是在 Delphi和 C++ Builder 中广泛使用的 VCL(Visual Component Library)可视化组件技术。

如同当初面向对象的产生过程一样,软件日趋庞大也使程序员的开发过程越来越繁

杂,软件维护难度越来越大。尽管面向对象技术对事物的描述已经非常接近人的思维和认

识过程,但面向对象只是反映了一个静态的过程。计算机应用软件日益人性化的今天,程

序员所面临的最复杂的、最令人头疼的就是人和计算机之间的相互交流,或者叫人机界面

的设计。计算机软件是一个运行着不断变化的过程,每时每刻都要和操作者进行信息的交

流,而软件内部又要对操作者的输入信息进行正确的处理,同样的一个处理过程,可能会

在不同的运行状态被触发或者被调用。要保证这种处理的正确性,采用面向对象的设计思

想,显然是不够客观、不够科学的。对于一个软件,人们并不是运行后就置之不理,而是

不断的判断程序在做什么?程序已经做了什么?我让程序做什么?我将要让程序做什

么⋯⋯ 仍然以车为例: 对于一个汽车,人们显然不是关心它本身是什么样的,而是关心汽车是停着的,还是

开动着的,车门关着还是开着,车速是多少⋯⋯ 用面向对象的技术对汽车的描述可以表示如下:

第一章 程序设计的发展 第 7 页

Class Car Begin Member: Wheel //车轮 Steering wheel //方向盘 Engine //引擎 Door //车门 ⋯⋯ ⋯⋯

Method: StartCar //点火 Run //开车 Accelerate //加速 Decelerate //减速 CloseDoor //关门 OpenDoor //开门 ⋯⋯ ⋯⋯

End 我们假设现在要求将车速变为 60Km/h,可能会出现的初始状态是:

一.车速现在为 90Km/h; 二.车速现在为 30Km/h; 三.车停止,发动机熄火; 四.车停止,发动机熄火,车门未关;

可能还有更多的情况,我们不多举例。那么,针对这四种不同的情况,我们所要执行

的操作也是不一样的。 第一种情况是减速;实现过程为: Procedure Fn1 Begin Decelerate //减速 End 第二种情况是加速; Procedure Fn2 Begin Accelerate //加速

第一章 程序设计的发展 第 8 页

End 第三种情况首先是点火,然后是加速至 60Km/h; Procedure Fn3 Begin StartCar //点火 Run //启动 Accelerate //加速 End 而第四种情况是先关车门,然后点火,最后加速至 60Km/h; Procedure Fn4 Begin CloseDoor //关门 StartCar //点火 Run //启动 Accelerate //加速 End 在这四种情况里,第一、第二种都是改变车速,一个为正方向的,一个为反方向,但

都属于同一种操作;第三种情况,多了一个执行过程——点火,也同样包含一个改变车速

的操作;第四种情况,多了关车门、点火两个过程,也包含了改变车速的操作。它们的共

同点是都包含车速改变的过程,因为这个操作是车速状态改变的核心操作过程。进一步分

析归纳可以发现这四种情况也包含了一个共同之处,它们必须有两个共同的约束条件:

一.车门必须是关着的 二.发动机必须处于点火状态

因为第一、第二种情况隐含的满足这两个条件,而第三种情况隐含的满足第二种条件,

第四种中情况两个条件都不满足。根据这一特点,我们可以设想建立以下几个机制: n 将汽车的运行抽象出三个独立的运行状态:车速状态、车门状态、发动机状态。 n 每个状态都和一个操作过程和其相对应。 n 每个状态对应的操作过程都要负责其核心操作。 n 每个状态对应的操作过程必须负责其约束条件的检测和设置。 这样以来问题将会大大简化,程序员不必去考虑在不同的条件下是如何经过不同的过

程而达到目的的,只需要关心对象中各个状态之间的约束关系,以及每个状态改变时的核

心操作的实现过程就行。

第一章 程序设计的发展 第 9 页

简单讲,这就是面向状态的编程思想,它可以化解数据与数据之间的相关性,将一些

复杂的逻辑关系转化成为较为简单的逻辑关系,使程序的设计、修改和维护都相对简单得

多。用面向状态的思想描述上面的例子可以表述如下: Class Car Begin Member: Wheel //车轮 Steeringwheel //方向盘 Engine //引擎 Door //车门 ⋯⋯ ⋯⋯

Method: StartCar //点火 ShutDown //熄火 Run //开车 Accelerate //加速 Decelerate //减速 CloseDoor //关门 OpenDoor //开门 ⋯⋯ ⋯⋯ SetSpeed //对应车速状态 SetDoor //对应车门状态 SetEngine //对应发动机状态 ⋯⋯ ⋯⋯

State: Speed //车速状态 IsDoorOpen //车门状态 IsTurnOn //发动机状态 ⋯⋯ ⋯⋯

End Procedure SetSpeed(X) Begin IsDoorOpen = False //约束条件,实际/执行 SetDoor(False) IsTurnOn = True //约束条件,实际执行 SetEngine(True) If Speed > X Decelerate Else If Speed = 0

第一章 程序设计的发展 第 10 页

Run Accelerate Else Accelerate

End If End Procedure SetDoor(Bool) Begin If IsDoorOpen != Bool If ⋯⋯ //关车门的约束条件,这里没有 ⋯⋯ End If If IsDoorOpen = True CloseDoor Else OpenDoor End If End If End Procedure SetEngine(Bool) Begin If IsTurnOn != Bool If IsTurnOn = True ShutDown Else StartCar End If End If End 要求将车速变为 60Km/h,我们仅仅需要做的是 Speed = 60 //执行 SetSpeed(60) 读者可以尝试使用结构化程序设计方法和面向对象的设计方法将上面这一条执行语

句的整个流程画出来,跟上面这一段伪代码的描述比较一下,似乎这个很简单的逻辑用其

他设计方法来描述都会变得复杂起来。其根本原因是,编写上面的这段伪代码,你根本不

需要思考,因为这就是“现实”,这就是你的思维。是你的思维,当然不用费力的思考。 尽管使用面向状态的方法在建立对象的时候似乎比面向对象的方法要冗长得多,但在

第一章 程序设计的发展 第 11 页

使用的时候却非常方便,简洁得多、直观得多了。而且,这个对象已经包含了目前的所有

可能出现的不同使用环境和条件制约关系。从逻辑上也更加符合人类的思维,更加简单明

了,修改和维护都相对简单得多。

6 小结

本章简单的介绍了计算机程序设计的发展过程,并且引入了面向状态的概念,通过一

个形象的实例读为者建立一个面向相状态的感性认识。希望能对部分读者阅读后面的内容

有所帮助, 跟任何事物一样,软件设计的发展也是循序渐进的,新的思想、新的方法总是在以前

的基础上建立的,都是对以前的方法改进、提高和扩充。面向对象的语言基本上都需要结

构化程序设计的支持,同样的面向状态的开发工具本身就应该是面向对象的,迄今为止,

Bobra 和 Jacopini 提出的三种基本结构仍然是程序设计中亘古不变的真理,而且任何

语言编写的程序最终都是要机器代码来实现的。你完全可以使用 C++ Builder而不使用

面向状态的技术,当然没有人会去怪你,你的上司也不会扣你的薪水。但既然有这样的技

术,我们就应该充分发挥它的长处,给我们的工作带来便利。

Beibei
Note
看完这一章了
Beibei
Note
Completed set by Beibei
Beibei
Note
Marked set by Beibei

第二章 面向对象的程序设计 第 13 页

第二章 面向对象的程序设计

上一章我们简单介绍了计算机程序设计的发展过程,让读者对程序设计有一个整体上

的了解。结构化程序设计相对简单、易懂,一般的读者都熟练掌握。面向对象程序设计包

含了比较深的思想过程,又是面向状态程序设计的基础,本书的读者应当对其有比较深入

的了解。本章将系统的介绍一下面向对象的程序设计思想和 C++语言的一些特点以及常用

的程序设计思想,但限于篇幅有限,而且本书的宗旨不是介绍面向对象,因此这里面向对

象不进行特别细致的描述,需要的读者可根据自身情况,参阅相关书籍和资料。 对于 C++的初学者,建议认真阅读本章,而对于已经熟练掌握 C++语言的读者可以跳

过本章,从第三章开始阅读。

1 对象的基本概念

面向对象的设计思想是以对象为核心的,因此要更好的理解面向对象,就必须从对象

的基本概念和基本特征了解起。

1.1 对象的现实含义

在面向对象的程序设计中,必须首先弄清楚什么是对象,在英文里面对象是 Object,可以翻译为目标、物体,因此对象可以理解为一个物体、一件事情、甚至是一些非常抽象

的事件。如:一张桌子、一辆汽车、一个车队等等,都可以理解为对象,结合到程序中则

可以是一个按钮、一个菜单、一个图形等等。 对象可以是简单的,也可以是相对复杂的事物,比如车队是由若干辆汽车组成的,而

汽车本身又是一个对象,这两个对象之间有着密切的联系,却又有各自的特征,而且汽车

进一步划分可以由车轮、车厢等组成,车轮、车厢又是更小更细的对象。再比如:就汽车

这个对象而言,可以象上一章那样,只关心几个主要的参数和一些主要的操作,也可以非

常细致描述到一个螺丝钉、车体的材料、车轮的大小、车身的颜色、轮胎的花纹、开车、

修车、转弯、刹车等等。这是根据我们实际的需要来定的。 我们可以这样来理解对象: n 对象是经过抽象生成的,是将一些联系比较密切的事物整合在一起而形成的。 n 和其相关的一些操作也作为对象的一部分,和对象保持着一定的关系。 n 对象是经过抽象生成的,可以完整的、相对独立的描述该事物。 n 根据描述的细致程度不同,对象允许是相互嵌套的。 比如汽车是车,火车也是车,它们都属于“车”这个对象,但各自的内涵取不同,它

们的共性就是“车”这个对象的内涵。也就是车包含着“汽车”、“火车”等不同的种类,

这些不同的种类都具备“车”的基本特征。 对象也可以认为是由不同的更小的对象联合而成的,由多个联系比较密切的对象联合

在一起也可以组成一个新的对象。

第二章 面向对象的程序设计 第 14 页

1.2 对象的划分和联系

和结构化程序设计相比,面向对象是一种全新的概念,因而不要试图将一个结构化程

序一刀未剪或者稍加修改的“升级”为面向对象的程序,这是徒劳也是不可能的。面向对

象的程序需要对数据进行重新分析和抽象,这就出现了如何对对象进行划分,以及实现对

象与对象之间联系等问题。 从前面的内容我们也可以看出,对象的划分也并不是唯一的,也没有统一的标准,然

而对象的划分的好坏却直接影响着一个程序的质量。一般来说,确定的对象应该是可独立

的,即: n 对象可以独立的描述一个事物或一个操作过程; n 对象应该尽量的符合事物的客观规律,在结构上、联系上相对紧密和固定的一些

元素可以作为一个对象; n 确定对象时应该全面的对问题进行分析,一个对象应该有其明确的目的以及所能

表达的问题; n 对象应该尽量需求系统中事物的共性,将所有的共性确定为一个新的对象更符合

思维的抽象过程。 比如:在一个矢量会图的程序中,将画布上的每一个“圆形”、每一根“直线段”都

作为一个对象,那么线段这个对象有其“起点”、“终点”、“颜色”、“宽度”等特征,而圆

则有“圆心”、“半径”、“颜色”等特征,将整个画布上的“对象”按照各自的特征显示在

画布上,就可以构成我们的整个图形。 如果将画布上所有的“线段”作为一个对象,将画布上的所有的“圆形”也作为一个

对象,那么划分这个对象将失去所有的意义,这张画布上的“所有线段”和另外一张画布

上的“所有线段”可能具有的共同特点只有一个字“乱”。这样的划分方法也许还不如用

结构化的程序设计。 对象在程序中是相对独立的,但并不是孤立的,也是不可能孤立的,但作为我们抽象

出来的对象,其内部成员必然有和外界极少发生关系的,甚至不会发生关系的;也有一些

操作是和外界不发生关系。这些特征我们完全可以通过一种方式将其隐含起来,这就是面

向对象中的数据和过程的隐藏。通过这种技术可以使程序之间的关系明朗、简化,进一步

使程序的设计和维护都大大简化。 仍然以车为例: 开车时我们并不需要知道变速箱中的齿轮是如何组织在一起的,也不需要亲自将这些

齿轮摆放在正确的位置,而只需要将档位放置在适当的位置就可以了,调整齿轮是车子自

己的事情,和操作者不会发生直接的关系,这就是被隐藏的一个过程,齿轮也是被隐藏起

来的成员。 而相反的,对象中有一些成员或者操作是必须和外界发生关系的,比如车子的档位和

调档这个操作。对象也正是通过这些公共的“接口”与外界联系在一起的。 了解对象的划分和联系对于程序设计是至关重要的,也是面向对象中最基本的概念。

只有正确的划分了对象并建立对象之间的关系,才可能更好的利用面向对象的技术来建立

高质量的程序,提高设计和开发的速度。接下来我们将要介绍如何在 C++语言中实现面向

Beibei
Highlight

第二章 面向对象的程序设计 第 15 页

对象的程序设计技术。

1.3 类和类的实例

面向对象是一种思想,在不同的计算机语言里,对其的描述和实现过程时有所不同的,

在 C++语言里面向对象是以“类(Class)”为基础来实现的。 类是对数据和操作即为对象的抽象描述,具体的对象便是类的实例,在现实中我们有

着抽象的概念“车子”,那么张三的车子、李四的车子,都是车子的一个实体,是一个具

体的对象。在程序里面,这种关系对应的是“类”和“类的实例”。 具有“起点”、“终点”、“颜色”、“宽度”等特征的简单图形我们可以定义为“线段类”,

那么画布中每一条线段都将是“线段类”的一个实例。 如何理解类和实例呢?就如同 C语言中 int是整数类型,float是浮点类型,而 int

i,j float x,y;其中 i 和 j是整型变量,x,y是浮点变量,那么 i,j 和 x,y分别是

int和 float 类型的实例。因此上讲:类是一种数据结构,是一个抽象的概念,而实例

是一个现实数据,是实实在在存在的。这是两个不同的概念,也是初学者经常容易混淆的

概念。另一个应该区分的是语言和开发环境,初学者往往也容易将其混在一起,开发环境

是一种高度集成的编译器,而语言则是实现一定算法时所采用的描述方法。 在 C++语言里面,将类中描述类特征的数据称作类的“成员”或者叫“数据成员”,

将类的特定的操作过程称作“成员函数”。成员描述的是类的构成特征,而成员函数是描

述类的行为特征。

2 C++语言的扩充

C++语言是由 Bjarne Stroustrup 开发的,最初被称为“带类的 C”,是在 C语言

的基础上扩充而形成的,后来人们在 C后面加上 C++语言中的自增运算符++,而将其命名

为 C++语言。通常 C++语言设计的程序源文件的扩展名为.CPP(Plus),以区别 C 语言

的源文件,这一点是很重要的,因为大多数 C++编译器在处理 C源文件和 C++源文件时的

方法和过程时有所不同的,这一点在我们后面稍作说明。 为了更好的理解 C++语言,这里有必要介绍一下 C++对 C语言的一些重要的扩充,要

获得更详细的内容,读者可以参考有关 C++语言的专门书籍。

2.1 类 Class

上面提到了类是 C++中实现面向对象的关键,而类 Class也正是 C++语言中最重要

的扩充。我们看下面一段 C++代码: struct Point

{

int X;

int Y;

};

第二章 面向对象的程序设计 第 16 页

//----------------------------------------------------

class Line

{

private: //下面的成员为 Private属性

int Color; //成员,记录颜色

public: //以下成员为 Public属性

Point StartPoint; //成员,起点

Point EndPoint; //成员,终点

void Show( void ); //成员函数,显示图形

void Hide( void ); //成员函数,隐藏图形

void ChangeColor( int NewColor ); //成员函数,改变颜色

};

//----------------------------------------------------

void Line::Show()

{

⋯⋯ //显示图形;

};

//----------------------------------------------------

void Line::Hide()

{

⋯⋯ //隐藏图形;

};

//----------------------------------------------------

void Line::ChangeColor( int NewColor)

{

Hide(); //先隐藏图形;

Color = NewColor; //设置新的颜色值

⋯⋯ //改变颜色;

Show(); //设置颜色后重新显示图形

};

这正是 1.2中对于“线段类”描述的代码,在这里面 struct 是 C语言中本来就有

的关键字,“结构”的标识符,而其他的 class、private、public以及例子中未出现

的 friend、protected、virtual、this等都是 C++扩充的关键字,下面就构成类的

及个关键字作以简单介绍:

2.1.1 class 关键字

class是由 C语言中 struct发展而来,但它 struct比增加了以下几点:

第二章 面向对象的程序设计 第 17 页

n 增加了对成员引用范围即作用域的限定。 n 增加了成员函数 n 增加了可继承性 n 增加了对自身的引用指针 除此之外,类还具备自动初始化等多种特性,读者可参阅相关书籍。 class关键字用于定义新类,其格式如下: class 类名称 { private: [私有成员列表] [私有成员函数列表] protected: [保护成员列表] [保护成员函数列表] public: [公有成员列表] [公有成员函数列表] }; []中的内容表示非必要成员。 在类的声明中 private、protected、public 为成员访问的说明符,跟随在

private 符号后面的成员为私有类型,protected 后面的成员为受保护类型,public之后的为公有类型。这几个关键字用于类的定义时,后面总要跟一个冒号(:)其范围为

自冒号以后至下一个说明符之前,并且可以在类中多处出现。 在类的成员函数中,通常有一个或者多个与类的名称相同的函数,称之为“构造函数”

(constructor),构造函数总是没有返回值的,即为 void类型而且只用于对象的初始

化过程,通常不允许用户直接调用,而是在建立一个类的新的实例时被系统自动的调用。

如果没有显式的指明构造函数,系统会自己生成一个默认的构造函数,并且系统在生成一

个新的对象时,会自动的将所有的成员初始化为“0”,因此,很有必要对一些成员在对象

生成时进行一些特殊的初始化而制定构造函数。 除此之外,类中还包含另外一个特殊的函数——“析构函数”(destructor),同构

造函数相同,析构函数和类名也相同,但函数名前面多了一个“~”符号;析构函数也没

有返回值,但每个类中只能包含一个析构函数。析构函数用于对象被销毁时,来释放对象

中的某些数据,并不是所有的类都必须包含显式的析构函数,如果类中没有用户自行分配

的内存空间,则可以使用系统默认的“析构函数”来释放对象占用的资源。 利用 class 关键字,我们可以构建自己的对象,读者可以在本书的光盘中找到 2.1

中 Line类的完整例子,这里不再多讲。 在这个例子中,类 Line使用了 4个不同的构造函数,分别有着不同的作用,在 C++

第二章 面向对象的程序设计 第 18 页

中允许多个函数具有相同名称,这在后面函数重载的内容中详细讲解。另外这个例子中使

用了 new 和 delete 关键字,这也是 C++语言中增加的关键字,用于创建对象和销毁对

象。

2.1.2 private、protected、public 关键字

private、protected 和 public 这三个关键字在类的声明中为成员访问控制说明

符,在类声明中,这几个关键字后面都要跟一个“:”表示作用域的开始,直到下一个访

问说明符出现,表示该说明符的作用域结束。 在类的声明中,被声明在 private 域中的成员及成员函数由具有私有性质,不能被

类以外的对象或函数访问,只能对该类的成员所访问。这样一来,可以使一些不常与外界

发生关系的、或者比较重要的数据,被有效地保护起来,不会因为不当的操作而被修改。 定义在 protected域中的成员具有被保护的属性,这类成员可以被派生类访问,也

可以被友员访问,但并不是所有的对象或函数可以访问该类的成员或成员函数,

protected 类型为成员提供了一种相对较弱的限制,但并没有完全限制,这对派生类需

要访问父类的成员时非常有用的。 public向外界提供完全开放的公共接口,任何对象或函数都可以访问一个类中被定

义为 public 类型成员或成员函数。通常会把一些和外界有联系的或联系比较密切成员或

成员函数声明为public类型,类的私有成员就是通过这些接口间接的和外界进行通讯的。 private、protected 和 public除了在类的声明中说明成员的访问类型之外,还

是类的继承类型的说明符。 继承性和类的派生是 C++的类中增加特性,派生指的是在已有类的基础上,对该进行

更深层次抽象和描述,从而形成一个新的,比原来的类具有更广泛内涵的类,这个类就叫

做派生类,原来的类叫做父类或者基类。 类的继承和派生可以大大地提高程序代码的可复用性,加快程序开发过程,也是符合

人类的认识和抽象过程的。 比如对“汽车”这个类加上“用途”就可以生成一个新的类,这个类是通过“汽车”

而派生的,将具有其“汽车”的所有共性,同时具有自己的个性应,父类的共性便是继承。

如: “小轿车”和“载重汽车”都是“汽车”的派生类,它们继承了“汽车”的所有特征,

却也有各自不同的特征。 C++语言中的继承性正是这种抽象关系在计算机语言中的反映,我们将在后面对继承

性作更多的讲述。这里先举一个派生类的例子: 在我们前面定义的类 Line中,包含了起点、终点、颜色等信息,但并没有包含线段

的线型和宽度,我们设想从 Line 派生一个包含线型和宽度的新类 NewLine,在 C++中可以这样实现:

class NewLine : public Line //声明 NewLine类,从 Line类共有继承

{

private:

第二章 面向对象的程序设计 第 19 页

public:

int LineStyle; //增加 LineStyle成员

int Linewidth; //增加 Linewidth成员

void Show(); //定义新的 Show()成员函数

void Hide(); //定义新的 Hide()成员函数

void NewLine( Point StartP, Point EndP, int Style, int Width );

//NewLine的构造函数

void ~NewLine( void ); //NewLine的析构函数

};

//----------------------------------------------------

void NewLine::NewLine(Point StartP, Point EndP, int Style, int Width)

: Line( StartP, EndP ) //增加了父类构造函数的调用

{

LineStyle = Style;

Linewidth = Width;

Show();

}

//----------------------------------------------------

void NewLine::Show()

⋯⋯

void NewLine::Hide()

⋯⋯

void NewLine::~NewLine()

⋯⋯

在这个新类中我们增加了两个共有成员 LineStyle 和 Linewidth,并且有了新的

构造函数和析构函数,同时,由于新的类中 Show()和 Hide()的处理方法和 Line中的

有所不同,因此在 NewLine中也重新定义了 Show()和 Hide(),但是问题也随之而来,

类 Line中的 Show()和 Hide()同样被 NewLine继承,这使得在 NewLine 中出现了两

个版本的 Show()和 Hide(),一个是自己的,一个是从父类继承而来的,函数名称相同

在 C++语言中是允许的,但在调用这两个函数时到底应该是那一个版本被调用却成了一个

问题,这涉及到了多态性及虚拟函数等概念,我们在后面作以介绍。 在 NewLine 的声明中,与 Line的声明不同,class NewLine后面多了一些内容

“:public Line”,这表示 NewLine 类是由 Line 共有派生而来。同样 private、protected也都可以作为继承类型的说明符,但它们继承来的成员却会产生不同的影响。

被标记为:public 型式继承的父类,其成员可以访问类型会被原封不动地继承到派

生类中;标记为:protected型式继承的父类,其 public域的成员被继承为 protected

第二章 面向对象的程序设计 第 20 页

类型的成员,protected 和 private 域的成员仍然保持原来的性质不变;而标记

为:private型式继承的父类,其所有的成员均被按照 private类型继承到派生类中。

2.1.3 friend 关键字及友员

friend关键字用于声明友员函数和友员类,友员函数和友员类都是宿主类的作用于

以外定义的,但可以访问该类的私有成员和受保护的成员。友员关系是单向的、非对称的、

而且不可传递,但友员关系是可以继承的。 比如有两个类 A、B,若声明 A为 B的友员类,则 A类中的成员函数可以访问 B类中

的私有数据和受保护数据,但反过来 B类不能访问 A类中的私有数据和受保护数据。若类

B又是另外一个类 C的友员类,则 A和 C之间不存在友员关系。友员的关系是在宿主类中

定义的,是“给予”的而不是“索取”的。 友员关系的声明形式如下: class A //定义类 A

{ ⋯⋯

}

//----------------------------------------------------

class B

{ ⋯⋯

friend class A; //说明类 A为 B的友员类

friend void FuncA(); //说明 FuncA为 B的友员函数

}

友员的声明不会受到 private、protected和 public 这三个关键字的限制,可以

出现在类定义的任何位置。 友员为 C++的私有数据提供了访问的接口,但同时也破坏了信息隐藏的特性,破坏了

C++的封装性。因此,并不主张频繁的使用友员关系,在 C++ Builder 里面,尽管仍旧

支持 friend 关键字,但几乎不再使用友员关系。在对类的定义合理的情况下,也完全可

以避免频繁的使用友员关系。

2.1.4 virtual 关键字

C++语言里面允许在同一个程序或者原文件中有相同的函数名称,在一个类里面也可

以有不同参数列表但函数名称相同的成员函数,这就是所谓的多态性,但这也带来了一个

新的问题,就是如何判断调用适当版本的函数。如我们上面例子中的 Line 和 NewLine中都有 Show()和 Hide(),如果用 new关键字生成一个 NewLine的实例,

Line1 = new NewLine( StartP, EndP, Style, Width );

第二章 面向对象的程序设计 第 21 页

那么下面这条语句的调用是会出现问题的, Line1->Show();

为了解决这一矛盾,在 C++里我们引入了虚函数的概念,虚函数的定义只需要在函数

的声明前面加上一个 virtual 关键字,虚函数是一个全新的概念,关于它的其它特点,

我们在多态性中详细讲述。

2.1.5 this 关键字

在 C++语言里,一个类可以由多个不同名称的实例,在程序运行时,这些实例的数据

是独立存放的,但和其相关的成员函数在内存中只有一份拷贝,所有相同类的实例都是共

享其成员函数的代码。 this 指针是类中一个特殊的指针,它始终指向对象自身的,每一个成员函数中,虽

然没有明确指定引用那一个实例的成员,但程序也会引用正确的对象的成员,这是因为,

在类中 this始终作为一个隐含的参数出现的,当然程序员也可以显式的使用 this指针。 比如: class A

{

int X;

int Y;

int SumXY(void )

{

return X + Y;

}

};

//----------------------------------------------------

A* A1;

A* A2;

A1 = new A();

A2 = new A();

int SumA;

SumA = A1->SumXY(); //求对象 A1中 X、Y的和

SumA = A2->SumXY(); //求对象 A2中 X、Y的和

在成员函数 SumXY()中有对成员的引用(X+Y),它和(this->X + this->Y)是等

价的,this指针被隐含的包括在对成员的引用里面,在 A1->SumXY()调用时,this将

指向 A1,而在 A2->SumXY()调用时,this指向对象 A2。因此,A1->SumXY()得到的

是 A1->X与 A1->Y的和,而 A2->SumXY()得到的是 A2->X与 A2->Y的和。 这只是 this 指针的一个用途,也是非常重要的一个用途,一般不需要显式的使用

第二章 面向对象的程序设计 第 22 页

this 指针,在某些情况下,程序员必须明确的使用 this 指针来完成一些操作,比如在

C++ Builder 的 VCL组件里面,要求有一个父组件(Parent),如果程序运行时动态生

成一个运行时可视的组件,像 TButton、TImage 等,就必须指明父组件(Parent),否

则该组件不会显示在屏幕上。通常我们用以下代码解决: Form1::CreatButton()

{

Tbutton * Button = new Tbutton( NULL ); //创建一个新的 Button

Button->Parent = this; //指明 Button的 Parent为 Form1

}; //新建的 Button将显示在 Form1上

2.2 ::作用域运算符

在 C++语言中,作用域是一个很重要的概念,因为在一个程序中允许出现相同的函数

名称,作用域对变量和函数都是有效的。C++中作用域运算符为两个紧连着的冒号(::),运算符前面为作用域的范围或作用于名称,后面为变量、成员、成员函数等。

如我们在 Line 类中定义了成员函数 Show()的原型,在后面的实现代码中,必须

注明 Line::Show(),如果省略了::和前面的 Line,编译器将认为该函数是公共域中

的 Show()函数,因而编译时会报错。 在成员函数中也可以通过::运算符对公共域中的变量进行引用,只要将::运算符前

面的参数为空就行。

2.3 new、delete

new和 delete为 C++语言中另外两个重要的关键字。面向对象编程时,很多情况对

象是运行时创建的,因此系统必须为其分配适当的内存空间,在 C语言里面,动态分配内

存用 malloc和 free函数,但将其用在 C++里面便有一些笨拙,不够灵活。 首先是 malloc分配内存时必须指定所需内存的空间大小;而且在早些版本的 C语言

中分配的内存返回为 void类型的指针,创建类时还要进行类型转换;最令人头疼的是,

在 C++中一个类的生成,往往需要进行一些初始化工作,用 malloc只能为其分配内存,

而无法自动的初始化对象。因此在 C++中引入了 new和 delete关键字。 new关键字在使用时也非常方便,我们前面用 new创建 Line的实例: Line1 = new Line( ⋯⋯ ); 使用起来非常直观,也非常方便,更重要的是用 new关键字创建 Line1时,系统会

自动调用 Line的构造函数,并根据所带的参数选择正确的版本,完成对对象的初始化。 除了对类可以初始化之外,使用 new关键字还可以对其它类型的变量进行初始化,比

如一个整型变量: int* IP = new int(0); 系统会创建一个指向整型变量的指针,并将其内容初始化为“0”。 将 new关键字用于数组的创建也非常方便,比如需要创建一个 10乘 10的浮点类型

第二章 面向对象的程序设计 第 23 页

数组,使用以下代码就可以: float* fPrt = new float[10][10]; 使用 new 关键字创建的内存空间,必须使用 delete 关键字予以释放,相对应的,

使用 delete关键字释放内存时,也不需要指明内存空间的大小,即使数组也是一样。而

且对于销毁一个对象,delete关键字的使用同样会让系统自动的调用该类的析构函数,

以释放对象的其他资源。 上面的几个用 new关键字分配的内存可以使用以下语句释放: delete Line1; //自动调用析构函数

delete IP; //销毁该数据

delete [] fPrt; //释放该数组所有元素占用的内存

new 关键字分配的内存必须用 delete 关键字释放,一个好的程序员应该经常检查

是否有闲置的内存开销,并及时释放,以获得更大的系统资源。但并不是只有 new分配的

内存才可以使用 delete释放,在经典的 C++里面是这样的,但在 C++ Builder 中并不

是这样,比如:Form往往是由系统自动创建的,或者使用 CreatForm 也可以创建,new当然也可以创建,但都可以使用 delete销毁,一些设计时创建的可视化组件也是可以用

delete销毁的。

2.4 &引用

在 C语言中函数调用的参数都属于按值传递,也叫做形式参数,即函数调用时,会将

所有参数拷贝一份并交给被调用的函数使用,这样被调用的函数可以随意的改变参数的值

而不担心会无意中修改了原来参数的值。但是,任何函数的返回值只有一个,因此有些时

候希望多个被传递的参数在经过处理后,能将处理后的值返回给调用者,就只能用指针来

完成,而且 C语言的这种调用方式在将一个类或者结构等体积较大的变量作为参数时,系

统将不得不花费额外的时间来复制参数。在 C++语言里,引入了其它一些高级语言中的引

用调用的概念,实现了类似实参数调用方法,从而大大地提高了程序的灵活性和实用性。 引用的说明符为&,比如定义一个引用的变量如下: int i; //定义一个整型变量 i

int &y = i; //定义一个整型引用 y(i的引用)

定义一个引用参数的函数如下:

bool Exchange(int &X, int &Y) //返回值为整型,参数为整型引用 X,Y

{ //交换 X,Y值

int Temp;

Temp = X;

X = Y;

Y = Temp;

第二章 面向对象的程序设计 第 24 页

return true;

};

需要调用该函数可以是如下的代码:

int i, j;

bool Ret;

Ret = Exchage(i, j);

同样的功能如果采用 C语言可能需要如下的代码:

bool Exchange(int *X, int *Y) //返回值为整型,参数为整型指针 X,Y

{ //交换 X,Y值

int Temp;

Temp = *X;

*X = *Y;

*Y = Temp; return true;

};

//----------------------------------------------------

int i, j;

bool Ret;

Ret = Exchage( &i, &j ); //这里的&是取地址运算符

实际上这两段的代码在编译后生成的可执行代码是完全相同的,但在书写和表达上,

显然采用引用&要方便的多。 需要注意的是不要将 C语言中取址运算符“&”和 C++中的引用“&”相混淆,尽管这

两者有许多相似之处。 取址运算符只会出现在“=”的右边或者是在函数调用时传递参数,如: int *p = &i; 的含义是将变量 i的地址传给整型指针变量“p”。 Ret = Exchage( &i, &j ); 的含义是将 i和 j的地址作为参数传递给函数 Exchage。 而引用说明符“&”只能出现在函数声明时或者变量声明时,如: int i; int &y = i; 的含义是定义一个整型引用 y,并指定 y为 i的引用,绝对不可能是将 i的值传递给

y的地址,单纯的 int &y是不合法的。这里 y只是 i的一个别名,在内存中也并不会为

y另外的开辟空间。引用更多的是用在函数的声明中: bool Exchange(int &X, int &Y);

第二章 面向对象的程序设计 第 25 页

表明 Exchage 的两个参数均为整型引用,在这里似乎按照取址运算符理解也是正确

的:&X和&Y作为 X和 Y的地址被当做形式参数传递给 Exchange,函数体内直接操作 X、Y,&为取址运算符是*指针运算符的逆运算,但实际并不是这样,如果按照这样的理解,

那么函数调用时应该是: int i, j; bool Ret; Ret = Exchage( &i, &j ); 而不应该是: Ret = Exchage( i, j ); 在面向对象的程序设计中,很多时候都需要使用引用作为函数的参数。同指针一样,

引用也可以作为函数的返回值传递给调用者,但必须注意的是,如果返回的是该函数局部

变量的引用,则该变量必须在函数体内用 static声明为静态变量,否则在函数返回时,

该变量实际上已经被销毁,而该变量的引用也将不可预知,处理不当将会给程序带来灾难

性后果。需要作更多的了解,请读者翻阅相关资料。

2.5 inline 关键字

inline 也是 C++扩充的一个关键字,它主要用来声明所谓的“内联函数”。在 C 语

言中,如果有一个体积非常小用的函数又在多处使用,为了减小调用的时间开销,通常使

用宏定义函数,这样编译器会按照宏定义进行文本替换而将这一段代码插入到相应的位

置。但是采用宏定义函数仅仅是进行简单的文本替换,如果有这样的代码: #define SQUARE( X ) X * X

SQUARE (3 + 2);

在编译时会被展开为: 3 + 2 * 3 + 2 这显然与最初的目的是不相同的,而在 C++中可以使用内联函数: inline double Square( double X );

采用内联函数,也可以象宏那样将短小的函数代码直接联编嵌入在调用它的位置,但

内联函数本质上还是一种合法的 C++函数,因此编译器会对内联函数进行正常的语法检查

和类型检查,这是宏所不具备的。

2.6 const 在 C++中另外一个非常有用的关键字是 const。用这个关键字可以声明“常量变量”,

这是一个矛盾的叫法,实际上更确切的叫法应该是“只读变量”,意思说被 const修饰的

第二章 面向对象的程序设计 第 26 页

变量除了不能被改变之外,和其他变量是一样的。const关键字也可以在函数声明时,指

定常量参数。 使用 const关键字可以定义常量变量来替代在 C语言中常用的宏定义常量,常量变

量可以和其他变量一样的被程序所识别和应用,也可以进行取地址等操作。此外使用

const关键字还可以定义常量指针,如: int i; int * const ip = &i; 或者: const int *ip = &i; 注意:这两者定义方法的含义是不同的,int * const的含义是一个整型的常量指

针,即 ip是不可以改变的,但它所指向的变量是可以被改变的。 *ip = 8; 是合法的,但是 ip 的值不能够被改变,而 const int *的含义是定义一个指向整

型常量变量的指针,尽管变量 i在定义时并没有声明为常量变量,但是被 ip引用后,将

不能通过*ip来改变它的取值,但是 ip的取值是可以被改变的: *ip = 8; 是不合法的,而 ip = &j; 是合法的。 从这也可以看出,const关键字修饰紧跟其后内容:在 int * const ip中 const

修饰 ip,因此 ip是一个不能改变的变量;在 const int *ip 中 const修饰 int,因

此 ip可以被改变,但是它是一个指向“const int”类型的指针,所以*ip 的内容不能

被改变! 编译器可以识别不同类型的指针和变量,从而使用 const 可以有效地保护某些数据

在传递给其他函数时不会被非法更改。 C++语言除了这些以外还增加了许多有用的扩充,包括语法的、书写格式上的等等,

而且不同的 C++编译器也可能会有一些生产厂商对其的扩充。这里仅仅就一些 Ansi C++主要的扩充内容作以简单介绍,读者可参阅相关书籍做更深一步了解。

3 C++中对象的封装性

封装实际上就是信息隐藏,在面向对象的系统中封装是构成对象的处理过程,一个封

装的对象也常被称之为一个抽象的数据类型,对象没有封装性,实际上就不能构成真正的

面向对象的程序设计。 前面我们已经提到过,在 C++语言中封装是通过类 class 来实现的,并且举了一个

简单的例子来说明 class关键字的一些基本用法。

第二章 面向对象的程序设计 第 27 页

3.1 对象的封装

面向对象系统的封装性主要是通过对象的封装性来体现的。对象的封装性是指将对象

的属性(数据内容)和作用于这些属性上的操作封装在一起,对象的使用者只能通过提供它

的接口(类声明)使用这个对象。对象的内部是被保护起来的,它的使用是通过调用公有成

员函数完成的。下面我们看一个没有封装概念的例子: #include <graphics.h>

#include <string.h>

#include <conio.h>

#include <stdlib.h>

//----------------------------------------------------

struct square

{ //定义一个正方形结构

int x, y; //定义中心坐标

int length; //定义正方形边长

};

//----------------------------------------------------

struct circles //定义一个圆形结构

{

int x,y; //定义圆心坐标

int length; //定义圆直径

};

//----------------------------------------------------

int sarea(int length) //计算正方形面积

{ return length*length;

}

//----------------------------------------------------

float carea(int length) //计算圆面积

{ return 3.1416*length*length/4;

}

//----------------------------------------------------

void sshow(int x,int y,int length)

{ //以 z,y为中心,以 length为边长在屏幕上画一个正方形

rectangle(x-length/2,y-length/2,x+length/2,y+length/2);

}

//----------------------------------------------------

第二章 面向对象的程序设计 第 28 页

void cshow(int x,int y,int length)

{ //以 x,y为圆心坐标,以 length为直径在屏幕上画一个圆

circle(x,y,length/2);

}

//----------------------------------------------------

main()

{

char *str, *s1;

square s; //定义一个正方形结构变量 s

circles c; //定义一个圆形结构变量 c

s.x = 200; //为正方形的各分量赋值

s.y = 200;

s.length = 100;

c.x = 200; //为圆形的各分量赋值

c.y = 200;

c.length = 70; int gdriver = DETECT,gmode;

initgraph(&gdriver,&gmode,"..\\bgi"); //置图形工作模式

setcolor( 4 );

sshow(s.x,s.y,s.length); //在屏幕上画正方形

setcolor(14); cshow(c.x,c.y,c.length); //在屏幕上画图

itoa(sarea(s.length),str,10); //求正方形面积,并将其转换成字符串 str

s1 = "square area:";

strcat(s1,str); //将两个字符串合并

outtextxy(20,400,s1); //显示字符,即正方形的面积

gcvt(carea(c.length),10,str); //求圆面积,并将浮点型结果转换成字符串 str

s1 = "circle area:";

strcat(s1,str);

outtextxy(20,440,s1); //显示圆面积

getch(); closegraph(); //关闭图形模式

return 1;

}

在上面的例子中有两个结构——正方形结构和圆形结构,对两个结构分别进行的操作

——显示操作和求面积操作的定义都是全局的,任何函数过程都可以对它们进行访问。它

们没有被封装隐藏起来。这样就容易造成错误,例如在主函数中我们定义了正方形 s和圆

形 c,由于它们的分量都是 x,y,1ength,就不免要出现错误。再如在调用正方形显示

第二章 面向对象的程序设计 第 29 页

函数时,写成如下形式: sshow(s.x,s.y,c.1ength);

在这里 s.1ength用成了 c.1ength,表示用圆形变量的直径作为正方形边长画一个

正方形,这是一个严重的错误,并且是没有任何异议的做法。但是程序在运行过程中非但

不拒绝,其结果看上去也没有什么不正常。 上述这些都说明了在程序中对数据和操作设置封装保护机制的必要性。 在有了封装机制以后,我们把数据和对数据的操作封装在一个类中,除去一些与外界

的接口外,类中的内容是被隐藏保护起来的,外界要使用类是通过接口来完成的。 上面的例子我们将它改为类来做: class square

{ //定义一个正方形类

private:

int x, y;

int length;

public:

square(int vx,int vy,int vlength);

int area();

void show();

};

//----------------------------------------------------

class circles //定义一个圆类

{

private:

int x, y;

int length;

public:

circles(int vx,int vy,int vlength);

float area();

void show();

};

//----------------------------------------------------

square::square(int vx,int vy,int vlength)

{ //正方形类的构造函数,给类中数据成员初始化

x = vx;

y = vy;

length = vlength;

第二章 面向对象的程序设计 第 30 页

} //----------------------------------------------------

circles::circles(int vx,int vy,int vlength)

{ //圆类的构造函数,给数据成员赋初值

x = vx;

y = vy;

length = vlength;

}

//----------------------------------------------------

int square::area()

{ //求正方形面积,不需传递参数

return length*length;

}

//----------------------------------------------------

float circles::area()

{ //求圆面积

return 3.1416*length*length/4;

}

void circles::show() //在屏幕上画圆

{

circle(x,y,length/2);

}

void square::show()

{

rectangle( x - length/2,y - length/2,x + length/2,y + length/2 );

} //在屏幕上画一个正方形

main()

{

char *str1,*s1,*str2,*s2;

square s(200,200,100); //创建一个正方形对象,并初始化

circles c(200,200,70); //创建一个圆形对象,并初始化

int gdriver = DETECT,gmode;

initgraph(&gdriver,&gmode,"..\\bgi");

setcolor(4);

s.show(); //画正方形

setcolor(14);

c.show(); //画圆

setcolor(5);

第二章 面向对象的程序设计 第 31 页

gcvt(s.area(),10,str1); //求正方形面积,并将其转换成字符串 strl

s1 = "square area:";

gcvt(c.area(),10,str2); //求圆面积,并将其转换成字符串 str

s2 = "circle area:";

strcat(s1,str1); //将两个字符串合并

strcat(s2,str2); outtextxy(20,400,s1); //显示字符,即正方形的面积

outtextxy(20,440,s2); //显示圆面积

getch();

closegraph(); //关闭图形模式

return 1;

} 在以上程序中,将正方形的中心坐标、边长以及求面积和画正方形操作封装在一个正

方形对象中,将圆的圆心坐标、圆的直径以及求圆面积和画圆操作封装在一个圆对象中。

关于对象的数据内容,我们创建对象时传递给构造函数参数使其对对象进行初始化,而在

后面的求面积及显示图形操作中均不需传递参数,函数从对象的数据成员中取值,外界不

能直接访问对象的私有内容,而只有通过调用公有成员函数来间接访问,因此对象是封装

是比较完善的,这样做可以避免了很多犯错误的机会。

3.2 友员的使用

前面已经提到过 friend关键字以及友员的概念。之所以引入友员是由于:一个对象

的私有数据和方法只能在类定义的范围内使用,也就是说只能通过它的成员函数来访问。

对象的这种数据封装和数据隐藏使对象和外界以一堵不透明的墙隔开。因此要求设计者确

保每个类提供足够的方法对所有遇到的情况进行处理,这会给软件设计者增加了负担。 而数据隐藏给两个类共享同一函数或数据带来了额外的开销,这是因为每次访问这些

共享内容都需要通过函数调用来完成。如果这种访问不经常发生,这类开销还能接受;如

果经常发生,就会变得难以容忍。 因此,就须寻求一种途径使得类以外的对象或函数能够访问类中的私有成员,在 C++

中就用友元作为能够实现这个要求的辅助手段。C++中的友元为封装隐藏这堵不透明的墙

开了一个小孔,外界可以通过这个小孔窥视内部的秘密。 然而使用友元会使数据封装性受到削弱,导致程序可维护性变差。实际上友员已经违

背了最初提出面向对象目的,但是在最初的 C++语言中,友员却为程序设计工作者带来极

大的方便,随着技术的不断发展,这种特殊的关系在实际中用的机会日益见少。

3.2.1 友元的说明和定义

只要将外界的某个对象说明为某一个类的友元,那么这个外界对象就可以访问这个类

对象中的私有成员。

第二章 面向对象的程序设计 第 32 页

声明为友元的外界对象既可以是另一个类的成员函数,也可以是不属于任何类的一般

的函数,还可以是整个的一个类(这样,此类中的所有成员函数都成为友元函数)。 友元声明包含在其私有成员可被作为友元的外界对象访问的类定义中,也就是将

friend关键字放在函数名或类名的前面。此声明可以放在公有部分,也可以放在私有部

分,其结果是相同的。例如: class point

{

private:

int x,y;

public:

point(int, int);

friend void print();

friend OtherClass::OtherFun();

}

在这个例子中,void print()并不是 point 类的成员函数,而是它的友元,它可

以访问point类对象的私有成员x,y。另一个友员是类OtherClass的成员函数OtherFun,

与 print类似,只是在函数前面加上作用域的范围。

3.2.2 友员的使用示例

下面的例子使用了一般函数 AddXY和类 TFriend 作为类 TFirstClass的友员,由此

可以看出,使用友员可以访问一个对象的私有成员。代码如下:

#include "conio.h"

#include "iostream.h"

class TFirstClass;

int __fastcall AddXY(TFirstClass &Cls);

class TFriend

{

public:

int __fastcall Multip(TFirstClass &Cls);

};

class TFirstClass

{

private:

int x;

第二章 面向对象的程序设计 第 33 页

int y;

public:

__fastcall TFirstClass(int Ox, int Oy);

friend int __fastcall AddXY(TFirstClass &Cls);

friend class TFriend;

}; int main(int argc, char* argv[])

{

TFirstClass Demo(5,6);

TFriend Frnd;

cout<<"TFirstClass Object Demo, X = 5, Y = 6\n";

cout<<"This is Use Friend Function AddXY \n";

cout<<"The Sum of Demo.x &Demo.y Is: "<<AddXY(Demo);

cout<<"\n\n";

getch();

cout<<"This Is Use TFirend Class's Member Fuction Multip\n";

cout<<"The Meyilp of Demo.x & Demo.y Is: "<<Frnd.Multip(Demo);

getch();

return 0;

}

__fastcall TFirstClass::TFirstClass(int Ox, int Oy)

{

x = Ox;

y = Oy;

}

int __fastcall AddXY(TFirstClass &Cls)

{

return Cls.x + Cls.y;

}

int __fastcall TFriend::Multip(TFirstClass &Cls)

{ return Cls.x * Cls.y;

}

程序的运行结果为: TFirstClass Object Demo, X = 5, Y = 6 This is Use Friend Function AddXY The Sum of Demo.x &Demo.y Is: 11 按任意键后显示:

第二章 面向对象的程序设计 第 34 页

This Is Use TFirend Class's Member Fuction Multip The Meyilp of Demo.x & Demo.y Is: 30 在按任意键程序结束,注意在这个例子中,只能通过友员来访问 TFirstClass 的私

有成员 x、y因此在 TFriend的成员函数中只能以 TFirstClass作为参数,而不能直

接将 TFirstClass::x和 TFirstClass::y作为参数。

4 C++的继承性与类的派生

类确实提供了良好的模块分解技术,通过信息隐藏,将它们的界面与实现清楚地分开。

但是,仅仅这一些是不够的,人们更希望能在类的基础上取得可重用性和可扩充性的目标。 要设计可重用性模块,任何方法都必须面对重复和差别,为了避免一再地重写同样的

代码,引入不一致的错误,就必须进行抽象,抽象出一般特性后,还需在此基础上扩充其

特殊的功能,使之能表达具体的事物。这就是面向对象中的派生与继承,继承和派生是一

对相辅相成的关系, 继承是 C++面向对象程序设计的重要特性之一,它是指建立一个新的派生类,从一个

或多个先前定义的基类中继承函数和数据,而且可以重新定义或加进新的数据和行为,从

而建立了类的层次或等级。

4.1 类的派生

在了解派生类的概念之前,我们先看一下人们对现实中的事物抽象的过程。现实世界

中许多实体之间并不是孤立的,它们具有共同特征,也有细微的差别,人们可以使用层次

结构来描述它们之间的相似点和不同点。例如鱼的分类,如图 2.1所示。

这是树状的分类结构,这棵树反映了鱼类的派生关系。最高层是最普遍的最一般的,

越往下反映的事物越具体,内涵越丰富,并且下层都包含了上层的特征。这中关系也是符

合人类认识事物的过程,层次越高的分类,内涵越少,包括的事物越多,层次越低的分类

越具体,内涵越多,个性更明显。这就是一种基类和派生类的关系。 派生和继承总是联系在一起的,例如确定某一条鱼是带鱼以后,没有必要指出它是生

成在海水中的,因为带鱼本身就是从海水鱼类中派生出来的,它继承了这一特性;同样也

鱼类

淡水鱼 海水鱼

草鱼 鲫鱼 鲤鱼 带鱼 桂鱼 比目鱼

图 2.1 鱼分类图

第二章 面向对象的程序设计 第 35 页

不必指出它会游,因为凡是鱼,都会游。带鱼是从海水鱼类中派生而来,而海水鱼又是从

鱼类派生而来,因此带鱼也可以继承鱼类的一般特性。 从上面的例子可以看出派生和继承是对象类间的一种相关关系,具有以下性质: n 表达了类间的共同特性; n 体现不同类间的区别; n 存在这类间的层次结构。

4.1.1 为什么使用继承

引入继承为代码重用提供有效手段。继承的优点主要有以下两个方面: 一方面使用继承可以重用先前项目的代码,如果原来的代码不能完全满足要求,还可

以做少量的修改,满足不断变化的具体应用要求,从而提高程序设计的灵活性,避免不必

要的重复设计。一般情况在选择了一种编译器之后,编译器会将许多常用的、提供特定功

能的类提供用户,这就是通常称作 FrameWorks框架。 另一方面若某个项目使用了几个非常相似或稍有不同的类,就可以通过派生类的继承

性达到函数和数据继承的目的。 下面举例说明使用继承优点,有一个字符串类,可以如下定义: class String

{

int Length;

char *Data;

public:

String();

~String();

int GetLength();

char *GetContents();

int SetContents(int DataLength,char *TheData);

int SetContents(char *TheData); //假如现在要定义的是一个可编辑的字

//符串类,在一般情况下,我们将这样定义:

class EditString

{

int Length;

int Position;

char *Data;

public:

EditString();

~EditString();

int GetLength();

int GetPosition();

第二章 面向对象的程序设计 第 36 页

void SetPosition(int ThePosition);

char *GetContents();

int SetContents(int DataLength,char *TheData);

int SetContents(char *TheData);

int Insert(char *TheText,int Pos);

int Replace(char *Sour,char *Dest);

int Delete(int Pos,int Count);

};

从上面两个类的定义中看出,这两个类中的数据成员和函数成员有许多相同的地方,

有一些 EditString类中的函数实际上只需将 String类中的稍加修改即可;还有一些

成员函数在两个类中完全相同。这是一个简单的例子,实际中的情况比这个例子要复杂的

多,因此重复的代码也要严重的多,在引入了继承以后,代码的利用将会大大提高。后面

的内容将讲解如何实现类的继承。

4.1.2 派生类的定义

前面已经提到继承和派生关系是相互依存的,只有派生而来的类才能够继承被派生类

的特性。C++提供了从现有类派生出一个新类的能力,所派生的新类具有下列特性: n 新的类可在基类所提供的基础上包含新的成员; n 在新的类中可隐藏基类的任何函数成员; n 可为新的类重新定义成员函数。 类的派生格式如下: class x

{ //...

};

class y : x

{

//...

};

其中类 y是从类 x派生而来的,我们将 x称为 y的基类,也可以称作超类或父类;y是 x的派生类,也称子类。上面 String和 EditString 类如果使用派生,可定义如下:

class String

{

//String类不变

};

第二章 面向对象的程序设计 第 37 页

class EditString : String //EditString从 String派生而来

{

int Position;

public:

EditString();

~EditString(); int GetPosition();

void SetPosition(int ThePosition);

int Insert(char *TheText,int Pos);

int Replace(char *Sour,char *Dest);

int Delete(int Pos,int Count);

};

从这里可以看出,EditString 的声明比原来简单了许多,实现的代码相应也会简化,

但是具备着和原来相同的功能。再如有一个汽车类,被定义如下: class Vehicle

{

int Wheels; //表示车轮数

float Weight; //表示汽车重量

public:

void Init(int TheWheels,float TheWeight); //给数据成员初始化

int GetWheels(); //取车轮数

float GetWeight(); //取车重

};

现要定义一个小车类 Car,其中很多成员可以是继承 Vehicle类的,此外还具有它

自身所特有的性质,故将 Car类定义成 Vehicle类的派生类,如下: c1ass Car : Vehicle

{ int Person; //允许载客数

public:

void Init(int TheWheels,float TheWeight,int ThePerson = 4);

//初始化函数

int GetPerson(); //返回载客数

};

如果再有一个卡车类 Truck,同样可以定义成 Vehicle的派生类,因为卡车和小汽

第二章 面向对象的程序设计 第 38 页

车都具有车的共同属性,但是卡车和小汽车各自又都有其特点。Truck 类要从 Vehicle类派生,只需要根据卡车的特点增加新的成员和成员函数等,比如加一个载重量,因为一

般卡车是用来装运货物的。如下: class Truck : Vehicle

{ float CarryWeight;

public:

void Init(int TheWheels,float TheWeight,float TheCarry);

float GetCarryWeight();

};

4.1.3 派生类对基类成员的访问权

前面讲过,类中的成员由两部分组成,即私有成员和公有成员。私有成员是收到类保

护的成员,不能被外界访问,但派生类和基类又有着密切的关系,这就出现了派生类对基

类成员访问权限的问题。 派生类并不是对基类中的所有成员都可以无条件地进行访问的,这个问题上在 C++语

言中将类的派生类按照“外界”来处理,也就是说派生类不能够访问基类的私有成员。但

是派生类可以访问基类的受保护成员,可以完全访问基类的公有成员。从这里也可以看出

protected 似乎是为了继承和派生而引入的,处于 protected 定义域的成员可以被其

派生类访问,但不能被外界所访问,是界于 private和 public之间的一种访问限制。 对类的派生同样可以分作几种不同的派生类型,C++中提供了将 public、private

和 protected关键字用于派生关系用法,因此派生关系也有三种: n 私有派生 n 保护派生 n 公有派生 需要指定派生类型时,只需要在基类的前面使用 public、private或者 protected

关键字就可以,如: class Base

{

};

class Driv : public Base

{

};

就表示 Driv从 Base公有排生而来。这几种不同的派生关系,对基类的成员访问权

限也有所不同。 1.私有派生 前面所定义的派生类都是私有派生。由私有派生得到的派生类,对它的基类的公有成

第二章 面向对象的程序设计 第 39 页

员只能是私有继承。也就是说基类的所有公有成员都只能成为私有派生类的私有成员,这

些私有成员只能被派生类的成员函数访问,而派生类的使用者无权访问。 2.保护派生 和私有类似,基类的私有成员被继承到派生类中只能成为保护成员,基类的私有成员

派生类不能访问,基类的保护成员被继承到派生类中访问属性不变,仍然为保护成员,由

这个派生类派生的类可以访问。 3.公有派生 在公有派生中,基类的所有成员的访问属性被不变的继承下来,私有成员仍然为私有

成员,且在派生类中不能访问,保护成员同样被继承为保护成员,派生类可以访问,派生

类的派生类也可以访问,但是外界不能访问,公有成员仍为共有成员,可以被派生类和外

界同时访问。 实际上这几种派生关系始终遵循这一个规则,就是访问控制权限就低不就高,下图给

出了几个不同派生类关系的类,我们来分析一下它们的访问权限。

图 2.2 中总共有四个类:Base、Driv1、Driv2、Driv3。虚线表示它们的派生关

public成员 private成员

class base

public成员 private成员

class Driv1:private base

public成员 private成员

public成员 private成员

class Driv3:public Driv2外界通过Driv3对各个类成员的访问权限

class Driv2:protected Driv1

图 2.2 几种不同派生关系对基类的访问权限

第二章 面向对象的程序设计 第 40 页

系,曲线表示访问方向,实心箭头表示可以访问,空心箭头表示不能访问。我们来逐个分

析: Driv1从 Base私有派生而来,因此 Driv1可以访问 Base的公有成员,但是不能

访问 Base的私有成员,Base的公有成员被继承到 Driv1中将成为私有成员,不能被外

界和 Driv1的派生类访问。 Driv2从Driv1保护派生而来,Driv2可以访问Driv1的公有成员,不能访问Driv1

的私有成员,同时也不可以访问 Driv1继承 Base的公有成员。Driv1的公有成员被继

承到 Driv2中成为保护成员,不能被外界访问。但是可以被 Driv2的成员函数或者派生

类所访问。 Driv3从 Driv2公有派生而来,因此 Driv3可以访问 Driv2的公有成员和保护继

承 Driv1 的公有成员,不能访问 Driv2 的私有成员和继承 Driv1 的私有成员即 Base的所有成员。

当外界通过 Driv3访问这几个类时,只能访问到 Driv3的公有成员和 Driv3公有

继承 Driv2的公有成员,Driv1的公有成员被继承到 Driv2中成为保护成员,因此也不

能百外界访问。Base的所有成员都不可能通过 Driv3让外界访问到,显然外界对 Base的成员在其派生类中比在自身中的访问权限更低,这就是因为访问权限就低不就高造成

的。

4.1.4 派生类的构造函数和析构函数

面向对象的继承机制可以从一个类中派生出另一个类,而派生出来的类又可以拿去派

生新的类,这就构成了类层次,建立了类等级。人们可以通过创建某个派生类的对象来使

用这个类等级,包括隐含地使用基类的数据和函数。基类往往有构造函数和析构函数,当

创建一个派生类对象时,怎样调用基类的构造函数对基类数据初始化,在结束派生类对象

时,又如何调用基类的析构函数来对基类对余的数据成员进行善后处理,这涉及到派生类

对基类的构造函数和析构函数的使用规则,不同的语言在类的构造和析构规则上不太一

样,这里就 C++中的构造函数和析构函数作个简单介绍。 1.派生类构造函数及析构函数的构造规则 构造函数 构造函数是在构建一个对象的时候,为对象初始化成员的,一个类可以包含多个不同

版本的构造函数。我们知道派生类继承了基类的成员,也包括了构造函数,但是每一个派

生类都必须有自身的构造函数,用于初始化自身的成员,C++允许声明一个类的时候不指

定构造函数,编译器会自动生成缺省的构造函数。 既然派生类继承了基类的成员,就必须在构建派生类的时候,同时对基类的成员进行

初始化。在下面两种情况下,必须定义派生类的构造函数: n 派生类本身需要构造函数; n 在定义派生类对象时,其相应的基类对象需调用带有参数的构造函数。 构造函数的格式: 派生类构造函数(参数列表):基类构造函数(参数列表),对象成员 1的构造函数(参

第二章 面向对象的程序设计 第 41 页

数列表),⋯⋯,对象成员 n的构造函数(参数列表) 例如: class Base //定义一个基类

{

//...

public:

Base(int First,int Second);//基类构造函数

//...

}

//----------------------------------------------------

class Driv : Base

{

//...

public:

Driv(int First, int Second,int Other);

//派生类构造函数,此处不用缀上基类的构造函数

};

//----------------------------------------------------

Driv::Driv(int First,int Second,int Other) : Base(First,Second)

{ //定义派生类构造函数时缀上基类的构造函数

//...

}

若基类使用缺省构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略

去“:基类构造函数(参数类列表)”;此时若派生类不需要特殊的初始化,则可不定义构

造函数。构造函数在派生类被构建时执行的顺序是先祖先(基类),再客人(对象成员),后自己(派生类本身)。这和 Object Pascal 是不一样的,而 C++ Builder 的 VCL 是

使用 Object Pascal 编写的,因而 C++ Builder 中 VCL类的构造顺序和 Ansi C++有所不同,这是读者需要注意的。

析构函数 在派生类中是否要定义析构函数与它所属的基类无关。若派生类在被销毁之前有数据

需做善后工作(如释放在构造函数中分配的内存等),就需定义析构函数。基类的析构函数

不会因为派生类没有析构函数而得不到执行,它们各自是独立的,也因此析构函数总是不

带参数的,而且一个类的析构函数只有一个。 若基类、成员类、派生类均有析构函数,则执行时的顺序刚好与构造函数的相反,即

先自己(派生类)、再客人(对象成员)、后祖先(基类)。 2.示例 下面举几个带有构造函数和析构函数的例子,来说明派生类中构造函数与析构函数的

使用。

第二章 面向对象的程序设计 第 42 页

有四个类。Data 为基类,它含有一个需传递一个参数的构造函数,用它来为其私有

成员 x赋值,并显示一句话;类 a中含有一个 Data类的成员对象;类 b为类 a的派生类,

并在其中也含有一个 Data类的成员对象;类 c为类 b的派生类。构造这些类,并在它们

的构造函数中均加上输出本类名的语句,这样可验证这些构造函数的执行顺序。程序如下: #include <iostream.h> class Data //定义 data类

{

int x ;

public:

Data(int x) {data::x = x;} //基类构造函数

cout<<"class Data \n";

};

//----------------------------------------------------

class a //定义 a类

{ Data d1; //dl为 data类对象;作为 a类的成员对象

public:

a(int x) : d1(x) //定义构造函数,缀上对象成员的构造函数

{

cout<<"class a \n";

}

};

//----------------------------------------------------

class b : public a //定义 b类,为 a类的派生类

{ Data d2; //d2为 data类对象,作为 b类的成员对象

public:

b(int x) : a(x), d2(x) //定义构造函数,缀上基类和成员对象的构造函数

{

cout<<"class b\n";

}

}

//----------------------------------------------------

class c : public //定义 c类,为 b类的派生类

{ public:

c(int x):b(x) //定义构造函数,缀上基类 b的构造函数

{

第二章 面向对象的程序设计 第 43 页

cout<<"class c\n";

}

};

//----------------------------------------------------

main()

{ c object(5); //定义 c类的一个对象 object,为构造函数传递参数为 5

return 1;

}

运行结果为:

class Data

c1ass a

class Data

class b

c1ass c

从得到的结果看出,构造函数的调用严格地按照先祖先(基类),再客人(成员对象),后自己(派生类本身)的顺序。

4.2 多继承

前面介绍的是,一个派生类是由一个基类派生的,但在解决实际问题时,有时还会遇

到更复杂的情况,某个类也许具有多个类所具有的特点,这就是下面要讨论的多继承。

4.2.1 多继承的概念

当一个类 B从类 A派生出来,类 B继承了类 A的部分或全部属性。即使给定的一个

类本身是派生类,也还可以从它派生出所需要的任意多的类,这就构成了类层次。例如,

有下列几个类定义: class A {//⋯};

class B : A {//⋯};

class C : A {//⋯};

class D : A {//⋯};

class E : B {//⋯};

class F : B {//⋯};

class G : D {//⋯};

class H : F {//⋯}; 定义的这八个类组成了如图 2.3的类层次。

A

F

H

GE

DCB

图 2.3 单继承的类层次图

第二章 面向对象的程序设计 第 44 页

图 2.3中的各个派生类都只有一个基类,因此组成了一个单继承的类层次。 C++中允许派生类具有多个基类,这就是多继承。所谓多继承就是某一个派生类具有

多个基类的继承。例如,用户界面所提供的窗口、滚动条、尺寸框以及多种类型的按钮,

所有这些假定都是通过类来支持的,若把这些类型中的两个或多个合并产生一个新类,如

把窗口和滚动条合并起来产生一个可翻滚的窗口,这个可翻滚的窗口就是由多继承得来

的。 读者需要注意的是,在 Object Pascal 中不允许多重继承,因而 C++ Builder

中 VCL类库也是不可以多继承的,实际上多继承在实际的应用中也不是必须,可以通过适

当的变通建立另一种组合关系,比如上面说的“可翻滚的窗口”,如果不使用多继承,就

可以用一个容纳了一个滚动条的窗口来描述,这是将两个类组合在了一起,而形成的,属

于包含关系,而不是单个一个对象。

4.2.2 多继承的定义

在 C++中,要定义具有两个以上基类的派生类,与前面的派生类定义相同,只需将要

继承的多个基类使用逗号分隔即可。例如: class c : a, b

{

//⋯⋯

};

c是由 a和 b作为基类多重继承得来的派生类。在定义派生类时还可定义它的访问控

制。例如,上述定义说明,c 类对于其基类 a,b 均为私有派生。若将上述定义改为如下

几种形式,c类对于其基类将具有不同的派生类型: class c : public a,b //c类是对 a类的公有派生,对 b类的私有派生

{

//⋯⋯

};

class c : a, public b //c类是对 a类的私有派生,对 b类的公有派生

{ //⋯⋯

};

class c : public a, public b //c类对其基类 a,b均为公有派生

{

//⋯⋯

};

在多重继承中,公有派生和私有派生对于基类成员在派生类中的可访问性与单继承的

第二章 面向对象的程序设计 第 45 页

规则相同。

4.2.3 多继承的构造函数与析构函数

具有多个基类的派生类其构造函数和析构函数与单继承的情况稍有不同,和单继承一

样,派生类的构造函数和析构函数都和它们基类的构造函数和析构函数发生关系,主要是

在基类的构造和析构的顺序上。 1.构造函数的定义 多继承的构造函数的定义与单继承的相似,只是几个基类的构造函数之间用“,”分隔。

例如我们前面讲过的窗口类和滚动条类: class Window //定义窗口类

{

//⋯⋯

public:

Window(int Top,int Left,int Bottom,int Right);

~Window();

};

//----------------------------------------------------

class Scrollbar //定义滚动条类

{ //⋯⋯

public:

Scrol1bar(int Top,int Left,int Bottom,int Right);

~Scrollbar();

}; //----------------------------------------------------

class ScrollbarWind : Window,Scrollbar //定义派生类

{

//⋯⋯

public:

ScrollbafWind(int Top,int Left,int Bottom,int Right);

~ScrollbarWind();

};

//----------------------------------------------------

ScrollbarWind::ScrollbarWind(int Top, int Left, int Bottom, int Right) :

Window(Top,Left, Bottom, Right),

Scrollbar(Top, Right-20, Bottom, Right ) //缀上基类的构造函数

{

//⋯⋯

第二章 面向对象的程序设计 第 46 页

}

通过上面的定义,得到了一个带有滚动条的窗口,这个窗口类是由一般窗口类 Window和滚动条类 Scrollbar共同派生得来的。

2.构造函数和析构函数的执行顺序 多继承的构造函数的调用顺序与单继承的相同,也是遵从先祖先(基类),再客人(成

员对象),后自己(派生类)的原则。在多个基类之间则严格按照派生定义时从左到右顺序

来排列先后。而析构函数的调用顺序则刚好与构造函数的相反。下面通过一个例子,来显

示多继承中构造函数和析构函数的调用顺序: #include <iostream.h>

#include <conio.h>

//----------------------------------------------------

class x

{

//⋯⋯

public:

x() {cout<<"the constructor of class x! \n";}

~x() {cout<<"the destructor of class x! \n";}

//⋯⋯

}; //----------------------------------------------------

class y

{

//⋯⋯

public:

y() {cout<<"the constructor of class y! \n";}

~y() {cout<<"the destructor of class y! \n";}

//⋯⋯

};

//----------------------------------------------------

class z : public x,public y

{

//⋯⋯

public:

z() {cout<<"the constructor of class z! \n";}

~z() {cout<<"the destructor of class z! \n";}

//⋯⋯

};

第二章 面向对象的程序设计 第 47 页

//----------------------------------------------------

main()

{

z obj;

return 1;

getch();

}

运行结果为: the constructor of class x! //x类构造函数的执行结果 the constructor of class y! //y类构造函数的执行结果 the constructor of class z! //z类构造函数的执行结果 the destructor of class z! //z类析构函数的执行结果 the destructor of class y! //y类析构函数的执行结果 the destructor oj class x! //x类析构函数的执行结果 在定义派生类 z时,基类的顺序为 x 在前,y在后,之间用“,”分隔,因此构造函

数的执行顺序应为先 x,再 y,后派生类 z;析构函数的执行顺序正好相反。执行所得到

的结果的顺序与此原则是相符的。 在这个例子中,所有的类不需要对成员进行初始化,因此没有显示的声明派生类构造

函数和基类构造函数的关系,下面用一个例子来演示多继承的应用。 例子:在一个圆内显示正文,实际上是完成显示一个带字符串的圆,可以有多种方法

来实现这个功能,这里我们使用从圆类和正文类多重继承得到一个新的类 Mcircle,而

圆和正文本身又都是从 point类派生出的类。这个问题的类层次如图 2.4所示。

下面就是这一程序的具体实现: #include <graphics.h>

#include <string.h>

#include <conio.h>

Point Point点类 点类

Circle Gmessage圆类 文字类

Mcircle 带文字的圆形

图 2.4 显示文字的圆的继承关系

第二章 面向对象的程序设计 第 48 页

//----------------------------------------------------

class point //定义点类

{

protected:

int x,y; //x,y为 point类的保护段成员

public:

point(int initx,int inity);

//...

};

//----------------------------------------------------

class circles : point //定义圆类,它是 point类的私有派生

{

int radius;

public:

circles(int x,int y,int radius);

void show();

//...

};

//----------------------------------------------------

class gmessage : point //定义正文类

{ char * msg;

int font;

int field;

public:

gmessage(int mx, int my, int msgfont, int fleldsize, char * text);

void show();

//...

};

//----------------------------------------------------

class mcircle : circles,gmessage //定义带字符串的圆类

{

public:

mcircle(int mrx,int mry,int mrradius,int font,char * msg);

void show();

//...

};

//----------------------------------------------------

point::point(int initx, int inity)

第二章 面向对象的程序设计 第 49 页

{

x = initx;

y = inity;

}

//----------------------------------------------------

circles::circles(int x,int y,int radius) : point(x,y)

{

circles::radius = radius;

}

//----------------------------------------------------

void circles::show()

{

circle(x,y,radius);

}

//----------------------------------------------------

gmessage::gmessage(int mx,int my,int msgfont, int fieldsize,char * text)

: point(mx,my)

{

font = msgfont;

field = fieldsize;

msg = text;

}

//----------------------------------------------------

void gmessage::show()

{

int size = field/(8 * _fstrlen(msg));

settextjustify(CENTER_TEXT,CENTER_TEXT); //指定文本输出的对齐方式

settextstyle(font,HORIZ_DIR,size); //设置文本输出属性

outtextxy(x,y,msg); //输出字符串

}

//----------------------------------------------------

mcircle::mcircle(int mrx, int mry, int mrradius, int font, char * msg)

: circles(mrx, mry, mrradius),

gmessage(mrx, mry, font,2 * mrradius,msg)

{

}; //----------------------------------------------------

void mcircle::show()

{

第二章 面向对象的程序设计 第 50 页

图 2.5 多继承对象的显示结果

circles::show();

gmessage::show();

};

//----------------------------------------------------

main()

{ int gdriver = DETECT, gmode;

initgraph(&gdriver,&gmode,"..\\bgi");

setcolor(12);

mcircle la(250,250,225,GOTHIC_FONT,"universerr");

la.show();

getch();

closegraph();

return 1 ;

}

这段程序执行后的结果如图 2.5所示,这是 C++中的一个经典例程,它是在 Dos的

图形模式下运行的,在 Windows中也可以运行,但是不能在 Windows 2000 和 NT中运

行,本书的光盘中含有所有代码和可执行文件。

4.2.4 虚基类

虚拟基类也是 C++中的一个特点,是和多继承相对应的语言需求,在实际编程中,也

会经常用到虚基类。什么是虚拟基类、为什么要使用虚基类、它会给编程带来什么益处,

以及编程中如何使用虚基类,我们下面就这个问题简单作一个介绍。

第二章 面向对象的程序设计 第 51 页

1.为什么要使用虚基类 为什么要使用虚基类,这个问题可以通过下面的例子来解释说明:

#include <iostream.h>

//------------------------

class x

{

protected:

int a;

public:

x() {a = 10;}

};

//----------------------------------------------------

class x1 : public x

{

public:

x1()

{

cout<<a<<"\n";

}

}; //----------------------------------------------------

class x2 : public x

{

public:

x2()

{

cout<<a<<"\n";

}

};

//----------------------------------------------------

class y : x1, x2

{

public:

y()

{ cout<<a<<"\n"; //错,x1::a或 x2::a

}

};

第二章 面向对象的程序设计 第 52 页

//----------------------------------------------------

main()

{

y obj;

return 1;

}

这是一个有问题的程序,问题出在派生类 y的构造函数的定义上,它试图输出一个它

有权访问的变量,表面上看是合理的,但此处的问题不是可访问性的问题,而是二义性的

问题,即函数中要输出的 a变量值,是由 x1派生路径上来的,还是 x2派生路径上来的,

在这里缺乏明确的说明。 虽然 x1和 x2是由共同基类 x派生而来,但它们所对应的是基类的不同副本,在实

际定义对象时,就是对应不同的对象。y类是 x1和 x2的派生类,因此 x类是 y类的间

接基类,它有两个副本与 y类相对应,一个是 x1派生路径上的副本,另一个是 x2派生

路径上的副本。当 y类要访问这个间接基类 x时,必须指定要访问的是哪个派生路径上的

x副本。将上面的例子用一个类层次来表达如图 2.6所示。但在很多的应用场合,并不希

望出现这种情况,而是希望这两个基类的副本合二为一,使 y类只对应一个间接基类 x,使上面的类层次变为图 2.7所示。

要实现这种类层次,就需要引出虚基类。 2.虚基类的概念

当在多条继承路径上有一个公共的基类,在这些路径中的某几条路径汇合处,这个公

共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个

公共基类说明为虚基类。 定义方法是,在定义派生类时,在需定义为虚基类的基类描述前加上 virtual 关键

字。如上例中,可将 x1和 x2定义成如下形式: class x1 : virtual public x

{

X X

X1 X2

Y

X

X1 X2

Y

图 2.6 非虚基类的继承结果 图 2.7 虚基类的继承结果

第二章 面向对象的程序设计 第 53 页

//⋯⋯

};

//----------------------------------------------------

class x2 : virtual public x

{

//⋯⋯

};

这样定义过以后,派生类 y中的构造函数就不再有问题了。 在现实中,虚基类还有其它的使用价值,例如:当定义图形用户界面时,肯有这样三

个类: class port{⋯}; 图形显示区 class region {⋯}; 屏幕上的任意区域 class menu{⋯}; 菜单—选项的集合 若从其中派生出两个新类:window类和 tools类: class window : public port, public region

{

//⋯⋯

};

//----------------------------------------------------

class tools : public port,public menu

{

//⋯⋯

};

这两个派生类还可以再派生出新类: class appwind : public window, public tools

{

//...

}

派生 appwind 的目的是为了得到一个带工具条的窗口,但根据上述定义得到的是如

图 2.8所示的结果: 工具条和窗口分别拥有自己的显示区,这并不是所希望的结果。所希望得到的是这两

个对象为一个整体,只包含一个显示区,即工具条是包含在窗口之内的。要做到这一点需

第二章 面向对象的程序设计 第 54 页

要将显示区类 port定义为虚基类,上述 tools和 window的定义变为如下形式:

class window : virtual public port, public region

{

//⋯⋯

};

//----------------------------------------------------

class tools : virtual public port, public menu

{

//⋯⋯

};

此时窗口即变为如图 2.9所示的形式:

虚拟基类是一个相对的概念,是否为虚拟基类是在定义派生类的时候决定的,而不是

在声明基类的时候决定的,因此同一个基类,在定义它的派生类时,可以作为某些派生类

的虚基类的,又可以作为另一些派生类的非虚基类,这种情况是允许的。例如: class b

Tool Window

T1

T2

T3

T4

图 2.8 非虚基类的运行结果

Window

T1

T2

T3

T4

图 2.9 使用虚基类的运行结果

第二章 面向对象的程序设计 第 55 页

{ //⋯⋯

};

//----------------------------------------------------

class x : virtual public b //b为 x派生类的虚基类

{ //⋯⋯

};

//----------------------------------------------------

class y : virtual public b //b为 y派生类的虚基类

{ //⋯⋯

};

//----------------------------------------------------

class z : public b //b为 z派生类的非虚基类

{ //⋯⋯

};

//----------------------------------------------------

class aa : public x, public y, public z

{ //⋯⋯

};

派生类 aa的定义,是在三条继承路径的汇合处。aa与它的间接基类 b之间对应的关

系是,它既是 x、y继承路径上的一个实例,也是 z继承路径上的一个实例,可以给出此

例的类层次图,如图 2.10。

3.虚基类的初始化

b

x y

aa

b

z

图 2.10 类的层次结构

第二章 面向对象的程序设计 第 56 页

虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用次序

不同。 派生类构造函数调用的次序,原则有三: n 虚基类的构造函数在非虚基类之前调用; n 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用; n 若虚基类由非虚基类派生而来,则仍然先调用基类构造函数,再调用派生类的构

造函数。

5 C++中的多态性和重载

多态性是面向对象系统中的又一支柱性概念。在 C++语言中,两个函数的名称完全相

同是允许的,而在 C语言中这是不允许的,即使在不同的程序模块中,只要这两个函数有

着某种联系,都是不允许名称相同的。比如我们前面提到的一个类可以有若干个构造函数,

而构造函数是和类名相同的,但是在一个类中或者相同的作用域中,这种名称相同的函数,

必须有不同的参数,这点表明,C++语言中编译器是根据函数所带的参数来区分不同的函

数,而不像 C语言中必须依靠函数名称来区分不同的函数,这就是 C++语言多态性的一种

表现,这是依靠所谓的重载来实现的。 另一方面,在一个类中,允许成员函数具备和其父类“完全相同”的函数,即不仅是

函数名称相同,而且参数也完全相同,连返回值也完全相同,这也是一种重载,是派生类

对继承父类的成员函数进行重载。而这种重载需要引入“虚函数”的概念。 重载和虚函数是多态性在面向对象系统中两种不同表现形式,既有区别又有联系。重

载函数通常在编译时就已经确定该调用那一个正确版本的函数,被称作“早期联编”而虚

拟函数是一种特殊的重载函数类型,它在系统运行时才确定调用那一个版本的函数,被称

作“滞后联编”。一般来说,“早期联编”提供了执行速度快的优点,而“滞后联编”提供

了灵活和高度问题抽象的优点。而这两种编译方式都支持多态性的一般概念。 下面就重载和虚函数分别作以讲解。

5.1 函数重载

重载概念体现了面向对象程序设计的多态性,而且把更大的灵活性和扩展性添加到该

语言中。通过为函数和运算符创建附加定义而使它们的名字可以重载,也就是说相同名字

的函数或运算符在不同的场合可以表现出不同的行为,实际上是运行的不同函数版本。 两个以上的函数,取同一名字,只要使用不同类型的参数或参数个数不同,编译器便

知道在什么情况下该调用哪个函数,这就叫函数重载。为什么要引入函数重载,可以通过

一个例子来说明。有四个函数: iplus() 整数相加; dplus() 实数相加;

第二章 面向对象的程序设计 第 57 页

fplus() 浮点数相加; cplus() 字符串相加。 若用 C语言来处理,必须给四个函数规定四个不同的函数名。程序员使用这些函数完

成两个变量之间的加法时,必须调用合适的函数,也就是说,程序员所必须记住的是四件

事,而不是一件事。 在面向对象程序设计中,由于它支持重载,所以用 C++处理时,虽然只给这四个函数

起一个共同的名字 plus,但它们的参数类型是不同的。当程序员调用此函数时,只需在

其中带入实参,编译器就根据实参类型来确定到底调用哪个重载函数。因此,程序员只需

记住 plus是将两个参量相加的函数这一件事即可,剩下的则都是系统的事情。上述例子

我们可以用下面的程序行实现: #include <string.h>

#include <iostream.h>

int plus(int x,int y) //定义两个整数相加

{ return x + y;

}

//----------------------------------------------------

float plus(float x,float y) //定义两个浮点数相加

{ return x + y;

}

//----------------------------------------------------

double plus(double x,double y) //定义两个双精度数相加

{ return x + y;

};

//----------------------------------------------------

char * plus(char *x,char *y)

{ //定义两个字符串相叠加

return strcat(x,y);

}

//----------------------------------------------------

void main()

{ int i = 12,j = 345;

float x1 = 1.2,y1 = 4.5;

double x2 = 24.5,y2 = 635.4,

第二章 面向对象的程序设计 第 58 页

char * strl= "C++ " ,*str2="Builder";

cout<<plus(i,j)<< "\n";

cout<<plus(xl,y1)<< "\n";

cout<<plus(x2,y2)<< "\n ";

cout<<plus(strl,str2)<< "\n";

}

在 main()中的 plus函数的四次调用,实际上是调用的四个不同的重载版本,由系

统根据传递的不同参数类型来决定调用哪个重载版本。例如 plus(i,j),因为 i,j为

整型变量,所以当执行到此处时系统将调用两个整数相加的重载版本 int plus(int,int);plus(strl,str2),因为 strl、str2为字符串变量,执行到此处时系统将调

用两个字符串相叠加的重载版本 char *plus(char *,char *)。从上面可以看出,

利用重载概念,程序员在调用函数时,书写非常方便。

5.1.1 构造函数重载

在前面介绍类的构造函数时,曾经讲过在定义构造函数时可定义多个,只是它们的参

数的个数或类型取的不同,实际上这就是构造函数的重载,因为那时还没有介绍重载概念,

所以没有使用“重载”这个术语。对构造函数进行重载定义,可使系统有几个不同的途径

对对象进行初始化。下面举一个计时器的例子。 定义一个计时器类,对计时器对象初始化就是给其数据成员赋初值。在创建对象时,

构造函数传递的参数可以用一个整数表示初始的秒数;也可以用数字串表示;还可以传递

两个参数,一个表示秒数,一个表示分钟数。这个构造函数既可以先将它们转换成总秒数

后再赋值;还可以不带参数,使初始值为 0。有这么多的情况需要构造函数处理,这只有

使用构造函数重载才能解决,此程序如下所示: #include <stdlib.h>

class Timer //定义计时器类

{

int Seconds;

public:

Timer(); //声明四个重载构造函数

Timer(char * t);

Timer(int t);

Timer(int,int);

int GetTimer(); //此函数返回 seconds的值

//...

};

//----------------------------------------------------

Timer::Timer() //定义无参数构造函数,给 seconds清 0

第二章 面向对象的程序设计 第 59 页

{

Seconds = 0;

}

//----------------------------------------------------

Timer::Timer(int t) //定义含一个整型参数的构造函数

{

Seconds = t;

}

//----------------------------------------------------

Timer::Timer(char * t) //定义一个含一个数字串的构造函数

{

Seconds = atoi(t);

}

//----------------------------------------------------

Timer::Timer(int min,int sec) //定义一个含两个整型参数的构造函数

{

Seconds = min*60 + sec;

}

//----------------------------------------------------

int Timer::GetTimer() //取私有变量 seconds的值

{ return Seconds;

}

//----------------------------------------------------

main()

{ Timer tl,t2("123"),t3(23),t4(2,34);

cout<<"object tl: "<<t1.GetTimer() <<"\n";

cout<<"object t2: "<<t2.GetTimer() <<"\n";

cout<<"object t3: "<<t3.GetTimer() <<"\n";

cout<<"object t4: "<<t4.GetTimer() <<"\n"; return 1;

}

在 main()函数中定义了四个对象。t1对象没有传递参数,所以在创建它时调用的是

无参数的构造函数 Timer::Timer();t2 对象在被定义时传递了一个数字串参数,所以

在创建 t2 对象时调用的是构造函数 Timer::Timer(char *);t3 对象在被定义时传

递了一个整型参数,在创建 t3对象时调用的是构造函数 Timer::Timer(int);t4对

象在被定义时传递了两个整型参数,在创建 t4 对象时调用的是构造函数

第二章 面向对象的程序设计 第 60 页

Timer::Timer(int,int)。

5.1.2 类成员函数重载

在类中除了构造函数可以重载外,一般的成员函数也可以重载,其重载原则与构造函

数相同,重载函数之间靠所包含的参数的类型与数量不同来进行区分。 下面给出一个日期类的例子,其中包含 year,month,day 为它的数据成员。在构

造函数中既包含直接传递三个整数给这三个成员赋值的版本,又包含传递一个日期字符串

的版本。在取得日期的函数中也采用了重载,它既可以返回三个量,又可以返回一个字符

串。实现程序如下: #include <stdlib.h>

#include <iostream.h>

#include <string.h>

//----------------------------------------------------

class Date //定义日期类

{

int Year;

int Month;

int Day;

public:

Date() {Year = 0; Month = 0; Day = o;};

Date(int,int,int);

Date(char *);

void GetDate(char *);

void GetDate(int *,int *,int *);

};

//----------------------------------------------------

Date::Date(int yy,int mm,int dd) //传递整型参数的构造函数

{

Year = yy;

Month = mm;

Day = dd;

}

//----------------------------------------------------

Date::Date(char * str) //传递字符串的构造函数

{

char * ss = new char[5]; //定义一个字符串,并为其分配存储

ss[0] = str[0]; //取字符串前两个字符,为月份

ss[1] = str[1];

第二章 面向对象的程序设计 第 61 页

ss[2] = '\n'; Month = atoi(ss); //将字符串转换成整数

ss[0] = str[3]; //取字符串中的第 4,5个字符,为日期

ss[1] = str[4];

Day = atoi(ss);

ss[0] = 'l';

ss[1] = '9';

ss[2] = str[6]; //取字符串中的第 7,8个字符,为年份

ss[3] = str[7];

ss[4] = '\n';

Year = atoi(ss); delete ss; //释放分配的存储

}

//----------------------------------------------------

void Date::GetDate(char * str)

{ char * ss = new char[5];

itoa(Month,ss,10); //将 month转换成字符串给 ss,10为进制

if(Month<10)

{

ss[1] = ss[0]; //若月份小于 10,在第一位加上 0

ss[0] = '0';

ss[2] = '\0';

}

strcpy(str,ss); //字符串赋值

itoa(Day,ss,10); //将 day转换成字符串给 ss

strcat(str,'/'); //在日期字符串中的月份后加入分隔符

if(day < 10)

{

ss[1] = ss[0]; //日小于 10,第一位为 0

ss[0] = '0';

ss[2] = '\n';

}

strcat(str,ss); //将 ss加到 str后面

itoa(Year,ss,lo); //将 year转换成字符串给 ss

ss[0] = '/';

ss[1] = ss[2];

ss[2] = ss[3];

ss[3] = '\0';

第二章 面向对象的程序设计 第 62 页

strcat(str,ss); //将表示年份的字符串加到日期表达式后面

delete ss; //将分配的存储释放

}

//----------------------------------------------------

void Date::GetDate(int *yy, int *mm, int *dd)

{ //将年、月、日值分别赋给三个整数

*yy = Year;

*mm = Month ;

*dd = Day;

}

//----------------------------------------------------

main()

{

int *yy = new int,*mm = new int,*dd = new int;

char * date = new char[9];

Date dal,da2(2002,12,27),da3("03/01/96");

da2.GetDate(datt);

da3.GetDate(yy,mm,dd);

cout<<date<<"\n";

cout<<*yy<<" "<<*mm<<" "<<*dd<<"\n";

delete yy,mm,dd,date;

return 1;

}

在 main()函数中,首先定义了三个对象,da1 对象被创建时调用无参量构造函数,

da2对象在被创建时调用了整型参量的构造函数 Date(int,int,int),da3 对象被创

建时调用了字符串参量的构造函数 Date(char *)。da2.GetDate(date)函数的调用,

因为 Date 为字符串变量,所以此时调用的是 GetDate(char *)重载版本;

da3.GetDate(yy,mm,dd)函数的调用,因为 yy,mm,dd 均为整型指针变量,所以

此时调用的是 GetDate(int *,int *,int *)重载版本。

5.1.3 类以外的一般函数重载

在 C++中,除了类中的各成员函数允许重载以外,类以外的一般函数也允许重载,规

则都一样。例如,写个求最大值函数 max的重载版本,用它来比较两个数(可以是整数、

实数、字符串)的大小,并返回其中较大的一个。程序行如下: #include <iostream.h>

#include <string.h>

int max(int x,int y) //比较两个整数大小

第二章 面向对象的程序设计 第 63 页

{ return x > y ? x : y;

}

//----------------------------------------------------

double max(double x,double y) //比较两个实数大小

{ return x > y? x : y;

}

//----------------------------------------------------

char * max(char *x,char*y) //比较两个字符串大小

{ return strcmp(x,y) > 0? x: y;

}

//----------------------------------------------------

main()

{

cout<<max(9,4)<<"\n";

cout<<max(4.5,7.4)<"\n";

cout<<max("windows","MS-Dos")<<"\n";

return 1;

}

在 main()函数中,max(9,4)中的 9和 4均为整数,因此它调用的是 max函数的整

数版本 int max(int,int);max(4.5,7.4)中的 4.5和 7.4为实数,因此它调用的

是 max 函数的实数版本 double max(double,double);最后一个函数调用

max("windows","MS-Dos"),其中的参数为字符串,因此它调用的是 max 函数的字符

串版本 char *max(char *,char *)。

5.2 运算符重载

在 C++中,除了函数可以重载之外,运算符也是可以重载,运算符的重载不仅增强了

C++语言的可扩充性也提高了使用的灵活性。最著名的例子是 C++中提供的复数运算,复

数是有实部和虚部两个部分组成的,通常在数学运算库中,复数的加、减、乘、除等运算

都不能使用 C语言中的“+、-、*、/”四个运算符,而在 C++中,通过对运算符的重载,

可以使复数运算向整数、浮点数一样使用“+、-、*、/”来运算。 在 C++中,说明一个类就是说明一个新类型。因此,类对象和变量一样,可以作为参

数传递,也可以作为返回类型。编译器为基本数据类型定义了许多运算符,例如"+"运算

符: int x, y; y = x + y;

第二章 面向对象的程序设计 第 64 页

这是表达两个整数相加的方法,非常简单,但是对于一个类这样的表达式是无效的,

例如对类“x”要实现特定的“加法”运算,需要使用类似下面的代码: class x

{

public:

int y;

//...

};

//----------------------------------------------------

main()

{

x a,b; //定义了两个对象

a.y = a.y + b.y; //将两个对象的数据内容相加

//...

return 1;

}

这样表达不如前面的简洁,也不直观;而且还存在一个问题,若类 x中的 y为私有成

员,上述表达式 a.y = a.y + b.y就是错误的。 因此,人们为了表达上的方便,希望已预定义的运算符(如+,-,*,/等),也可以

在特定类的对象上以新的含义进行解释。如上面的表达式则希望变为 a = a + b,这就

需要用重载运算符来解决。 在 C++中,大多数系统预定义的运算符都能被重载。例如: + - *(乘法) / % &

~ ! = < > += -= *=

/= %= ^= &= |= << >> >>=

<<= == != <= >= && || ++

-- [] () new delete

也有一些运算符是不能被重载的,如: . :: *(指针) ?: #

重载运算符时,不能改变它们的优先级,不能改变这些运算符所需操作数的数目。实

际上被重载的运算符也是通过调用特定的函数来完成其运算的,但是使用了重载的运算符

不仅在表达方式上简单、直观了很多,也减少了出错的机会。

5.2.1 用成员函数重载运算符

在 C++中,用成员函数重载运算符就是将运算符重载定义成一个类的成员函数的形

式,通常将重载运算符的成员函数称为运算符函数。

第二章 面向对象的程序设计 第 65 页

(1) 在类定义体中声明运算符函数 声明重载运算符时,写成如下形式: type operator@(参数表) 其中,type 为返回类型;operator 是运算符重载时不可缺少的关键字;@为所要

重载的运算符符号;参数表中罗列的是该运算符所需要的操作数。 用成员函数重载运算符的函数参数表中,若运算符是一元的,则参数表为空,此时当

前对象作为运算符的单操作数;若运算符是二元的,则参数表中有一个操作数,此时当前

对象作为此运算符的左操作数,参数表中的操作数作为此运算符的右操作数。 现在举一个例子说明: class x

{

//...

int operator + (x a);

};

在类定义中声明一个重载运算符"+",返回类型为 int,它具有两个操作数,一个是

当前对象,另一个是对象 a。 (2) 定义运算符函数 运算符函数的定义与一般的成员函数相似,对类成员的访问与一般成员函数相同,格

式如下: type x::operator@(参数表)

//定义的操作

其中 x是可以重载此运算符的类名,其它符号的含义与上面相同。 上面在 x类中重载"+"的例子也可定义为下面的形式: int x::operator + (x a)

{

return a.x + x;

}

(3) 重载运算符的使用 运算符重裁定义后是为了方便使用,定义后的重载运算符使用起来就像原运算符一样

方便。只是要注意一点,即它的操作数一定要是定义它的特定类的对象,如上面重载“+”运算符的例子可用一个 main()来使用它:

main()

{

x a1,b1;

y a2,b2;

a1 = a1 + b1; //正确

第二章 面向对象的程序设计 第 66 页

a2 = a2 + b2; //错误

//...

}

从例子中看出,al = a1 + b1是正确的,因为 a1,b1是 x类的对象,在 x类中

已重载了“+”运算符,因此可直接使用。而 a2 = a2 + b2是错误的,这是因为 a2,b2是 y类的对象,而不是 x类的对象,在 y中没有定义“+”重载,因此不能使用。在 x类中对“+”运算符有了重载定义,因此对此类对象进行相加时就像做一般的加法一样简

单。

5.2.2 用友元重载运算符

在 C++中,还可以把重载运算符定义成某个类的友元函数的形式,而且这种方法用得

更多。在定义格式上与用成员函数定义相似,但稍有差别。 用友元重载运算符的函数也称运算符函数。它与用成员函数重载运算符的函数之不

同,在于后者本身是类中的成员函数;而它是类的友元函数,是独立于类外的一般函数。 (1) 在类定义体中声明重载运算符 在类定义体中,要声明用友元重载的运算符时与一般的友元函数有些相似,可采用如

下形式: friend type operator@(参数表); 与用成员函数定义的方法相比较,只是在前面多了一个关键字friend,其它项目含

义相同。 在为用友元函数重载运算符的参数表填写操作数时,要注意友元函数是不属于任何类

的,它没有 this指针,这与成员函数完全不同。若重载的是一元运算符,则在参数表中

有一个操作数用来充当这唯一的操作数;若重载的是二元运算符,则在参数表中有两个操

作数。也就是说在用友元定义重载运算符时,所有的操作数均需要用参数来传递。 看下面一个声明的例子:

class point

{

int x, y;

public:

//...

}

friend point operator + (point p1,point p2);

因为是用友元重载的运算符"+",而"+"又是一个二元运算符,所以需要传递两个参

数,这里传递的是 p1和 p2。 (2) 定义重载运算符

第二章 面向对象的程序设计 第 67 页

定义用友元重载的运算符与定义一般的友元函数相似,格式如下: type operator @(参数表) 例如上面 point类中用友元重载的"+"运算符的定义为: point operator + (point p1,point p2)

{

point p;

p.x = p1.x + p2.x;

p.y = p1.y + p2.y;

return p;

}

(3) 重载运算符的使用 用成员函数定义的重载运算符和用友元定义的重载运算符,使用起来没有什么差别,

因此在这里不再重复。 下面代码将示范分别使用成员函数和友员函数来为类 TOPClass 重载“+”和“*”

运算符,TOPClass包含了两个成员:x、y,“+”运算将两个操作数的 x、y分别相加作

为结果的 x和 y,“*”运算将两个操作数的 x相乘作为结果的 x,y相乘作为结果的 y,代码如下:

class TOPClass;

TOPClass operator* (TOPClass Cls1,TOPClass Cls2);

class TOPClass

{ public:

int x;

int y;

TOPClass operator + (TOPClass TheClass);

friend TOPClass operator*(TOPClass Cls1,TOPClass Cls2);

};

TOPClass operator *(TOPClass Cls1,TOPClass Cls2)

{

TOPClass Result;

Result.x = Cls1.x * Cls2.x;

Result.y = Cls1.y * Cls2.y;

return Result;

第二章 面向对象的程序设计 第 68 页

}

TOPClass TOPClass:: operator + (TOPClass TheClass)

{

TOPClass Result;

Result.x = x + TheClass.x;

Result.y = y + TheClass.y;

return Result;

}

5.3 虚函数

虚函数是重载的另一种表现形式,它是一种动态的重载方式,它提供了一种更为灵活

的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行

时才决定如何动作,即所谓的“动态连接”。当一般的类型对应于不同的类型变种时,这

个能力显得尤其重要。下面先介绍对象指针,然后介绍虚函数的引入及其使用。

5.3.1 对象指针

在 C++中,对象除可以直接引用外,还可以通过对象指针来引用。在前面虽然没有正

式介绍对象指针,但在许多例子中已经采用了对象指针。 1. 一般对象的指针 指向一般对象的指针与指向一般变量的指针,其定义和使用语法都是相同的。例如:

#include <iostream.h>

c1ass class1

{

//...

public:

void show()

{cout<<"this is the class of class1!\n";}

};

class class2

{

//...

public:

void show()

{ cout<<"this is the class of class2!\n";}

};

main()

第二章 面向对象的程序设计 第 69 页

{ class1 cl, *ptrl; //定义 classl类对象 c1和 classl类指针 ptrl

class2 c2, *Ptr2; //定义 class2类对象 c2和 class2类指针 ptr2

ptrl = &c1; //将 ptrl指针指向 cl对象

ptr2 = &c2; //将 ptr2指针指向 c2对象

ptr1->show();

ptr2->show();

return 1;

}

运行结果为: this is the class of classl! this is the class of class2! 2. 引入派生类后的对象指针 前面介绍的一般对象的指针,它们各自独立,之间没有什么联系,相互不能混用。引

入派生类后,由于派生类是由基类派生出来的,派生类和基类之间是息息相关的,因此指

向派生类和基类的指针也是相关的。 在引入了派生的概念后,任何被说明为指向基类对象的指针都可以指向它的公有派生

类。下面看一个例子: #include <iostream.h>

#include <string.h>

class String

{

char *Name;

int Length;

public:

String(char *Str)

{

Length = strlen(Str);

Name = new char[Length + 1];

strcpy(Name,Str);

}

void show()

{cout<<Name<<"\n";}

}; class de_string : public String

{

int age;

第二章 面向对象的程序设计 第 70 页

public:

de_string(char *str,int age) : String(str)

{

de_string::age = age;

}

void show()

{

string::show();

cout<<"the age is:"<<age<<"\n";

}

};

main()

{

string s1(“Smith”), * ptrl; //定义 string类对象 s1及 string类指针 ptrl

//de_string s2(“Jean”,20), *ptr2;

//定义 de_string类对象 s2及 de_string类指针 ptr2

ptrl = &s1; //将 ptrl指向 s1对象

ptrl->show(); //调用 string类的成员函数

ptrl->&s2; //将 ptrl指向 string类的派生类 de_string的对象 s2

ptrl->show(); //调用 s2对象所属的基类的成员函数 show()

ptr2 = &s2; //将指针 ptr2指向 de_string类对象 s2

ptr2->show(); //调用 de_string类的成员函数 show()

return 1;

}

运行结果为: Smith Jean Jcan the age is:20 从例子中看出,虽然 ptrl指针已经指向了 s2对象(ptr1 = &s2),但是它所调用

的函数(ptrl->show())仍然是其基类对象的成员函数,这是使用时要注意的问题。在

使用引入派生类之后的对象指针时要注意下面几个问题: n 可以用一个声明让指向基类对象的指针指向它的公有派生的对象。若试图指向它

的私有派生的对象则是被禁止的。

n 不能将一个声明为指向派生类对象的指针指向其基类的一个对象。

n 声明为指向基类对象的指针,当其指向派生类对象时,只能利用它来直接访问派

生类中从基类继承来的成员,不能直接访问公有派生类中特定的成员。

这三点是使用对象指针所需要注意的,如果希望通过指向派生类的基类指针可以派生

第二章 面向对象的程序设计 第 71 页

类的特定成员,可以将其显式的类型转换为派生类指针来实现。例如: class Point

{

protected:

int x,y;

public:

Point(int x,int y)

{Point::x = x;point::y = y;}

void Show()

{putpixel(x,y,getcolor());}

void Show1() { cout<<"x = "<<x<<" y = "<<y<<"\n";}

};

class Circles : public Point

{

int Radius;

public:

Circles(int x,int y,int radius) : Point(x,y)

{circles::Radius = radius;}

void Show()

{Circle(x,y,radius);} void Show2()

{

Show1()

cout<<"Radius = "<<Radius<<"\n";

}

};

main()

{

Point obj1(100,100),*ptr; //定义对象 objl及指向 point类的指针 ptr

Circles obj2(200,200,100); //定义 obj2对象

ptr = &obj1; //将指针 ptr指向 objl对象

int gdrivef = DETECT,gmode;

initgraph(&gdriver,&gmode,’ ’);

setcolor(13);

ptr->Show(); //调用 objl对象所属类 point中的成员函数在屏幕上画一个点

ptr = &obj2; //将指针 ptr指向其派生类对象 obj2

ptr->Show(); //调用 obj2对象从基类继承来的成员函数,在屏幕上画一个点

((Circles *)ptr)->Show(); //调用 obj2对象的特定成员 show(),画一个圆

第二章 面向对象的程序设计 第 72 页

getch();

closegraph();

ptr = &obj1;

ptr->Show1();

ptr = &obj2;

((Circles *)Ptr)->Show2(); //调用 obj2对象的成员函数 show2()

getch();

return 1;

}

灵活的指针应用是C语言的一大特色,因而在 C++中对象的指针也是非常重要的概念,

而且具有非常广泛的用途,尤其是在 C++ Builer 中,所有的 VCL类库必须是动态分配

的内存,因此对 VCL对象的使用必须使用对象指针。

5.3.2 虚函数的必要性

为什么要引入虚函数,我们先来看一个例子: #include <iostream.h>

class base

{

public:

void who()

{cout<<"this isthe class of base! \n";}

};

class derive1 : public base

{ public:

void who()

{cout<<"\this is the class of derivel! \n";}

};

class derive2 : public base

{

public:

void who()

{cout<<"this is the class of derive2! \n";}

};

main()

{

base obj1,*p;

derive1 obj2;

第二章 面向对象的程序设计 第 73 页

derive2 obj3; p = &obj1; //指针 p指向 base类对象 objl

p->who();

p = *obj2; //指针 p指向 derivel类对象 obj2

p->who();

p = &obj3; //指针 p指向 derive2类对象 obj3

p->who();

obj2.who();

obj3.who();

return 1;

}

此例在 main()函数中定义了二个基类对象 obj1,和两个派生类对象 obj2,obj3,又定义了一个指向基类对象的指针 p。此程序的意图是用 p指针分别指向不同的对象,以

便执行不同对象所对应的类的成员函数。当 p指向 objl对象时,则 p->who()调用 base类的成员函数 who();当 p指向 obj2对象时,我们希望 p->who()调用 derivel 类的

成员函数 who();当 p指向 obj3对象时,希望 p->who()调用 derive2 类的成员函数

who()。 此程序执行后实际得到的结果是: this is the class of base! ① this is the class of base! ② this is the class of base! ③ this is the class of derivel! ④ this is the clasl of derive2! ⑤ 在结果中,①,④,⑤,与所预想的相符,而②和③却不是希望得到的。这说明,不

管指针 p当前指向哪个对象(是基类对象还是派生类对象),p->who()调用的都是基类中

定义的 who()函数版本。也就是说,通过指针引起的普通成员函数调用,仅仅与指针的类

型有关,而与此刻正指向什么对象无关。在这种情况下,必须显式的调用派生类中的函数

成员,例如: obj2.who()或 obj3.who()或者是采用对指针的强制类型转换的方法,如: ((derie1 *)p)->who(),或((derive2 *)p)->who()

本来使用对象指针是为了表达一种动态的性质,即当指针指向不同对象时执行不同的

操作,现在看来并没有起到这种作用。要实现这种功能,就需要引入虚函数的概念,上面

的功能只需将基类的 who()函数声明为虚函数即可。

第二章 面向对象的程序设计 第 74 页

5.3.3 虚函数的定义及使用

1.虚函数的定义 虚函数是引入了派生概念以后,用来表现基类和派生类的成员函数之间的一种关系

的。虚函数定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中

冠以关键字 virtual,从而提供了一种接口界面。在基类中的某个成员函数被声明为虚

函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其

函数原型,包括返回类型、函数名、参数个数、参数类型顺序,都必须与基类中的原型完

全相同。如下面的例子: #include <iostream.h>

class base

{

//...

public:

virtual void who() //定义虚函数

{cout<<"base\n";}

};

class first : public base

{ //...

public:

void who() //重新定义虚函数

{cout<<"the first derivation \n";}

}; class second : public base

{

//...

public:

void who() //重新定义虚函数

{ cout<<" the second derivation\n";}

};

main()

{

base obj1,*ptr;

first obj2;

second obj3;

ptr = &obj1;

ptr->who(); //调用 base类的 who()版本

第二章 面向对象的程序设计 第 75 页

ptr = &obj2; ptr->who(); //调用 first类的 who()版本

ptr = &obj3;

ptr->who(); //调用 second类的 who()版本

return 1 ;

} 运行结果为: base the first derivation the second derivation 分析一下上面这个程序: ①在基类中对 void who()进行了虚函数声明 virtual,在其派生类中就可以重新

定义它。 ②在派生类 first和 second 中分别重新定义 void who()函数,此虚函数在派生

类中重新定义时不再需要 virtual声明,此声明只在其基类出现一次。在 void who()函数被重新定义时,其函数的原型与基类中的函数原型必须完全相同。

③在 main()函数中,定义了一个指向基类类型的指针,它也被允许指向其派生类。

在执行过程中,不断改变它所指向的对象,p->who()就能调用不同的版本。虽然都是

p->who()语句,但是当 p指向不同的对象,所对应的执行动作就不同。可见用虚函数充

分体现了多态性,并且,因为 p指针指向哪个对象是在执行过程中确定的,所以体现的又

是一种动态的多态性。 2 虚函数与重载函数的关系 在一个派生类中重新定义基类的虚函数是函数重载的另一种特殊形式,但它不同于一

般的函数重载。一般的函数重载,函数的返回类型及所带的参量可以不同,仅是函数名相

同即可,而虚拟函数要求函数名称和参数以及返回值必须严格的一致,否则将会出错或者

失去虚特性。 n 假如仅仅返回类型不同,其余均相同,系统会当作出错处理,因为仅仅返回类型

不同的函数本质上是含糊的,不是虚拟函数也不是重载函数。 n 函数原型不同,仅函数名相同。此时,系统会将它认为是一般的函数重载,将丢

失虚特性。 下面看一个例子: #include <iostream.h>

class base

{

public:

virtual void f1()

{cout<<"function of base ! \n";}

virtual void f2()

第二章 面向对象的程序设计 第 76 页

{cout<<"f2 function of base ! \n";} virtual void f3()

{cout<<" f3 function of base! \n";}

void f4()

{cout<<" f4 function of base! \n";}

}; class derive : public base

{

void f1()

{cout<<" f1 function of derive! \n";}

void f2(int x)

{cout<<"f2 function of derive! \n";}

int f3(); //错误,只有返回类型不同,应去掉

void f4()

{cout<< "f4 function of derive! \n";}

};

main()

{

base obj1,* ptr;

derive obj2;

ptr = &obj1;

ptr->f1();

ptr->f2();

ptr->f3();

ptr = &obj2;

ptr->f1();

ptr->f2();

ptr->f4();

return 1;

}

此例在基类中定义了三个虚函数 f1(),f2(),f3(),这三个函数在派生类中被重

新定义时,f1()符合规则,它仍为虚函数;f2()增加了一个整型参量,变为 f2(int x),因此它丢失了虚特性,变为一般的重载函数;f3()的返回类型由 void变为 int,系统指

出冲突的出错信息。基类中的 f4()函数和派生类中的 f4()函数都为一般的重载函数。 在 main()函数中,定义了一个基类指针 ptr,在执行过程中当指向基类对象时,用

它调用的函数均为基类的成员函数;当 ptr指向派生类对象 obj2时,ptr->f1()执行

的是派生类中的成员函数,这是因为 f1()为虚函数;ptr->f2()执行的是基类的成员函

数,因为 f2()函数丢失了虚特性,按照一般的重载函数来处理;ptr->f4()执行的是基

第二章 面向对象的程序设计 第 77 页

类的成员函数,因为 f4()为重载函数。 3.多继承中的虚函数 在多继承中由于派生类是由多个基类派生而来的,因而,虚函数的使用就不像单继承

那样简单。看下面的例子: #include <iostream.h> class a

{

public:

virtual void f() //定义 f()为虚函数

{cout<<"class a \n";}

};

class b

{

public:

void f() //此 f()函数为一般函数

{cout<<"class b \n";}

};

class aa : public a,public b

{

public:

void f()

{cout<<"class aa \n";}

};

main()

{

a obj1,*ptr1;

b obj3,*ptr2;

aa obj3;

ptr1 = &obj1; //将指针 ptrl指向 a类对象

ptr1->f(); //调用 a类的 f门

ptr2 = &ohj2; //将指针 ptr2指向 b类对象

ptr2->f(); //调用 b类的 f()

ptr1 = &obj3; //将指针 ptrl指向 a类的派生类 aa类的对象

ptr1->f(); //调用 aa类的 f(),此时的 f()为虚函数

ptr2 = &obj3; //将指针 ptr2指向 b类的派生类 aa类对象

ptr2->f(); //调用 b类中的 f(),因为此处 f()为非虚函数,而 ptr2又为 b类指针

return 1;

}

第二章 面向对象的程序设计 第 78 页

从上面的例子看出,派生类 aa中的函数 f()在不同的场合,呈现不同的性质。若相

对于 a派生路径,由于在 a中的 f()函数前有关键字 virtual,所以它是一个虚函数;

若相对于 b派生路径,在 b中的 f()函数为一般函数,所以此时它只能是一个重载函数。 当 a类指针指向 aa类对象 obj3时,函数 f()就呈现出虚特性;当 b类指针指向 aa

类对象 obj3时,函数只呈现一般的重载特性。 运行结果如下: c1ass a class b class aa class b 若一个派生类,它的多个基类中有公共的基类,在公共基类中定义一个虚函数,则多

级派生以后仍可以重新定义虚函数,也就是说虚特性是可以传递的。看下面的例子: #include <iostream.h>

c1ass a

{

public:

virtual void f()

{cout<<"class a \n";}

}; class a1 : public a

{

public:

void f()

{cout<<"class al\n";}

};

class a2 : public a

{

public:

void f()

{cout<<"class a2 \n";)

};

class aa : public al,public a2

{

public:

void f()

{cout<<"class aa \n";}

};

第二章 面向对象的程序设计 第 79 页

main()

{

a1 *ptr1; //定义 al类指针 ptrl

a2 *ptr2; //定义 a2类指针 ptr2

aa obj; //定义 aa类对象 obj

ptr1 = &obj; //a1类指针 ptrl指向 aa类对象 obj

ptr1->f(); //调用 aa类的 f()

ptr2 = &obj; //a2类指针 ptr2指向 aa类对象 obj

ptr2->f(); //调用 aa类的 f()

return 1;

}

运行结果为: class aa c1ass aa 从例子中看出,虚特性是可以传递的。a类作为 al和 a2类的直接基类,它的成员函

数 f()被声明为虚函数,则 al和 a2类中的 f()都具有虚特性,即均为虚函数;而 aa类

为 a1和 a2类的派生类,因此它的成员函数 5()也为虚函数。若 a类中 f()函数声明时

的 virtual省掉,则运行结果将变为: class a1 class a2 4.基类构造函数调用虚函数 当一个基类的构造函数调用一个虚函数时,会出现意想不到的结果。看下面的例子: #include <iostream.h>

class base

{

public:

base()

{

cout<<"the constfuctor of base\n"; clone();}

virtllal void clone()

{cout<<"the clone function of base\n";}

};

class derive : public base

{

public:

derive()

第二章 面向对象的程序设计 第 80 页

{cout<<"the constructor of derive\n";} void clone()

{cout<<"the clone function ofderive\n";}

};

main()

{

derive obj;

base *ptr = &obj;

ptr->clone();

return 1;

}

运行结果为: the constructor of base the clone function of base the constructof of derive the clone function of derive 输出结果的第二行是出乎意料的,下面来分析一下此程序的运行过程。 首先创建一个派生类对象 obj,在创建时先调用基类构造函数。基类构造函数首先输

出第一行结果,然后调用 clone()函数,因为此函数为虚函数,并且当前定义对象为

derive 类对象。照理此时应执行 clone()在 derive 类中的版本,但此时,派生类对

象 obj正在创建中,只做了部分的初始化,当编译器遇到要调用虚函数 clone()时,它

无法与派生类中的 clone()版本联编(因为对象还不完整),编译器只能调用基类的

clone()版本来代替它,因此出现第二行的意外结果。之后再执行派生类的构造函数,输

出第三行结果。执行到 ptr->clone()时,派生类对象早已圆满创建,因此就可调用

clone()函数的派生类版本,得到第四行的输出结果。 在构造函数中调用虚拟函数时,一定要注意对象的构造顺序,否则可能会得到意想不

到的结果,在 C++ Builder 中 VCL 的构造顺序和 Ansi C++有所不同,调用虚拟函数

的结果可能也会有所不同,因此在 C++ Builder中,除非是必须的,一般不要在构造函

数中调用虚拟函数。

6 面向对象的设计过程

前面将 C++语言中的一些特点作了一个简单的介绍,这和面向对象的大部分思想是一

致的,通过这些内容的学习,可以对面向对象的程序设计有一个大致的了解。接下来就几

种比较经典的程序设计方法作一个简单的介绍。

第二章 面向对象的程序设计 第 81 页

6.1 几种软件设计方法简介

60 年代中期开始爆发了众所周知的软件危机。为了克服这一危机,在 1968、1969年连续召开的两次著名的 NATO会议上提出了软件工程这一术语,并在以后不断发展、完

善。与此同时,软件研究人员也在不断探索新的软件开发方法。至今已形成多种软件开发

方法。 在这些软件设计方法中,有的是近些年才出现的,而有些是已经被人们运用了数十年

的方法。这些设计方法本身并不是相互对立,也不是简单的补充关系,而是每种设计方法

都包含了各自的核心思想,在一个软件的开发过程,可能是多重设计方法共同存在,相互

补充的。以下对这些设计方法作一简单介绍。

6.1.1 Parnas 方法

最早的软件开发方法是由D﹒Parnas在 1972年提出的。由于当时软件在可维护性

和可靠性方面存在着严重问题,因此 Parnas 提出的方法是针对这两个问题的。首先,

Parnas 提出了信息隐蔽原则:在概要设计时列出将来可能发生变化的因素,并在模块划

分时将这些因素放到个别模块的内部。这样,在将来由于这些因素变化而需修改软件时,

只需修改这些个别的模块,其它模块不受影响。信息隐蔽技术不仅提高了软件的可维护性,

而且也避免了错误的蔓延,改善了软件的可靠性。现在信息隐蔽原则已成为软件工程学中

的一条重要原则。 Parnas提出的第二条原则是在软件设计时应对可能发生的种种意外故障采取措施。

软件是很脆弱的,很可能因为一个微小的错误而引发严重的事故,所以必须加强防范。如

在分配使用设备前,应该取设备状态字,检查设备是否正常。此外,模块之间也要加强检

查,防止错误蔓延。 Parnas对软件开发提出了深刻的见解。遗憾的是,他没有给出明确的工作流程。所

以这一方法不能独立使用,只能作为其它方法的补充。但这中方法实际上始终存在于我们

的软件开发过程中。不光是驱动程序、操作系统,就是在一般的应用程序中,开发也通常

遵循这一规则。

6.1.2 SASD 方法

1978年,E﹒Yourdon 和 L﹒L﹒Constantine 提出了结构化方法,即 SASD方法,

也可称为面向功能的软件开发方法或面向数据流的软件开发方法。1979年 TomDeMarco对此方法作了进一步的完善。

Yourdon方法是 80年代使用最广泛的软件开发方法。它首先用结构化分析(SA)对

软件进行需求分析,然后用结构化设计( SD)方法进行总体设计,最后是结构化编程( SP)。这一方法不仅开发步骤明确,SA、SD、SP相辅相成,一气呵成,而且给出了两类典型的

软件结构(变换型和事务型),便于参照,使软件开发的成功率大大提高,从而深受软件

开发人员的青睐。

第二章 面向对象的程序设计 第 82 页

6.1.3 面向数据结构的软件开发方法

(1) Jackson方法 1975年,M﹒A﹒Jackson 提出了一类至今仍广泛使用的软件开发方法。这一方法从

目标系统的输入、输出数据结构入手,导出程序框架结构,再补充其它细节,就可得到完

整的程序结构图。这一方法对输入、输出数据结构明确的中小型系统特别有效,如商业应

用中的文件表格处理。该方法也可与其它方法结合,用于模块的详细设计。 Jackson方法有时也称为面向数据结构的软件设计方法。 (2) Warnier方法 1974年,J﹒D﹒Warnier 提出的软件开发方法与 Jackson 方法类似。差别有三点:

一是它们使用的图形工具不同,分别使用 Warnier 图和 Jackson 图;另一个差别是使

用的伪码不同;最主要的差别是在构造程序框架时,Warnier方法仅考虑输入数据结构,

而 Jackson方法不仅考虑输入数据结构,而且还考虑输出数据结构。

6.1.4 问题分析法

PAM问题分析法。PAM(Problem Analysis Method)是 80年代末由日立公司提

出的一种软件开发方法。 PAM方法希望能兼顾 Yourdon 方法、Jackson 方法和自底向上的软件开发方法的优

点,而避免它们的缺陷。它的基本思想是:考虑到输入、输出数据结构,指导系统的分解,

在系统分析指导下逐步综合。这一方法的具体步骤是:从输入、输出数据结构导出基本处

理框;分析这些处理框之间的先后关系;按先后关系逐步综合处理框,直到画出整个系统

的 PAD图。从上述步骤中可以看出,这一方法本质上是综合的自底向上的方法,但在逐步

综合之前已进行了有目的的分解,这个目的就是充分考虑系统的输入、输出数据结构。 PAM方法的另一个优点是使用 PAD图。这是一种二维树形结构图,是到目前为止最好

的详细设计表示方法之一,远远优于 NS图和 PDL语言。 这一方法在日本较为流行,软件开发的成功率也很高。由于在输入、输出数据结构与

整个系统之间同样存在着鸿沟,这一方法仍只适用于中小型问题。

6.1.5 面向对象的软件开发方法

随着 OOP(面向对象编程)向 OOD(面向对象设计)和 OOA(面向对象分析)的发展,

最终形成面向对象的软件开发方法 OMT(Object Modelling Technique)。这是一种

自底向上和自顶向下相结合的方法,而且它以对象建模为基础,从而不仅考虑了输入、输

出数据结构,实际上也包含了所有对象的数据结构。所以 OMT彻底实现了 PAM没有完全

实现的目标。不仅如此,OO 技术在需求分析、可维护性和可靠性这三个软件开发的关键

环节和质量指标上有了实质性的突破,彻底地解决了在这些方面存在的严重问题,从而宣

告了软件危机末日的来临。 面向对象技术是软件技术的一次革命,在软件开发史上具有里程碑的意义。面向对象

的基本思想是以对象为核心,通过对事物本身特性的抽象,产生可维护性高、可复用性强、

容易扩展的程序代码,也是本章主要所述的内容,后面将对面向对象的软件开发技术进行

Beibei
Highlight
Beibei
Highlight

第二章 面向对象的程序设计 第 83 页

更详细一些的讲述。

6.1.6 可视化开发方法

可视化开发是 90 年代软件界最大的两个热点之一。随着图形用户界面的兴起,用户

界面在软件系统中所占的比例也越来越大,有的甚至高达 60~70 %。产生这一问题的原

因是图形界面元素的生成很不方便。为此 Windows 提供了应用程序设计接口 API(Application Programming Interface),它包含了 600 多个函数,极大地方便

了图形用户界面的开发。但是在这批函数中,大量的函数参数和使用数量更多的有关常量,

使基于 Windows API的开发变得相当困难。 为此许多公司都推出了可视化编程工具,如:Mecrosoft 的 Visual C++、Visual

Basic,Borland 公司的 Delphi、C++ Builder,IBM公司的 Power Builder 以及

JAVA Beans 等。大多数的可视化开发环境都将 API 的各部分用对象类进行封装,并为

这些定义了许多成员函数。利用子类对父类的继承性,以及实例对类的函数的引用,应用

程序的开发可以省却大量类的定义,省却大量成员函数的定义或只需作少量修改以定义子

类。 可视化开发就是在可视开发工具提供的图形用户界面上,通过操作界面元素,诸如菜

单、按钮、对话框、编辑框、单选框、复选框、列表框和滚动条等,由可视开发工具自动

生成应用软件。 这类应用软件的工作方式是事件驱动(Events Driver Program)。对每一事件,

由系统产生相应的消息,再传递给相应的消息响应函数。这些消息响应函数是由可视开发

工具在生成软件时自动装入的。 可视开发工具应提供两大类服务。一类是生成图形用户界面及相关的消息响应函数。

通常的方法是先生成基本窗口,并在它的外面以图标形式列出所有其它的界面元素,让开

发人员挑选后放入窗口指定位置。在逐一安排界面元素的同时,还可以用鼠标拖动,以使

窗口的布局更趋合理。 另一类服务是为各种具体的子应用的各个常规执行步骤提供规范窗口,它包括对话

框、菜单、列表框、组合框、按钮和编辑框等,以供用户挑选。开发工具还应为所有的选

择(事件)提供消息响应函数。 由于要生成与各种应用相关的消息响应函数,因此,可视化开发只能用于相当成熟的

应用领域,如目前流行的可视化开发工具基本上都是多用于关系数据库的开发。对一般的

应用,目前的可视化开发工具只能提供用户界面的可视化开发。至于消息响应函数(或称

脚本),则仍需用通常的高级语言(3GL)编写。只有在数据库领域才提供 4GL,使消息响

应函数的开发大大简化。 从原理上讲,与图形有关的所有应用都可采用可视化开发方式。而且目前不光是在

Windows 系列操作系统出现了大量的可视化开发环境,在其他的操作系统中也逐渐出现

了可视化的开发环境,如:Linux中有Borland公司基于Object Pascal语言的Kylix,通常被人们成为 Linux中的 Delphi。

实际上可视化编程也是面向对象的一种设计语言,但比面向对象似乎更高级了一些,

第二章 面向对象的程序设计 第 84 页

本书将要讲的面向状态正是在可视化开发方法中产生的,但它并不局限于可视化的开发过

程,而且也并不是所有的可视化开发环境都支持面向状态的设计方法。

6.2 软件的生命周期

软件的生命周期指的是软件从开发一直到不再使用时的这一段存在时间,一般来讲在

结构化程序设计以及之前的软件设计中,软件的生命周期是呈现瀑布状,阶梯形式,通常

可以分作 5个阶段如图 2.13所示:

分析阶段——定义并描述系统的外部特征。 设计阶段——将整个系统划分成为一系列简单的子系统,并定义每个子系统的功能和

接口。 编码阶段——用某种程序设计语言将每个子系统写成程序代码,并将各个子系统集成

为完整的系统。 测试阶段——对系统进行测试,证明系统达到了预期的目标。 维护阶段——对系统进行修改和扩充,以适应新的应用需求。 而在面向对象的设计方法中则有所不同,软件的生命周期通常被划分为 4个阶段,如

图 2.14所示:

分析

设计

编码

测试

维护

图 2.13 软件开发的生命周期和关系

分析

设计

演化

维护

图2.14 面向对象的软件生命周期

第二章 面向对象的程序设计 第 85 页

分析阶段——在面向对象设计中,把分析看作是从问题域中选出词汇建立类和对象的

模型世界。 设计阶段——设计是对问题域的行为进行关键抽象再分解的过程。设计的结果反馈到

分析上进行纠正,当关键抽象足够简单不需要再进行分解时即可停止设计。 演化阶段——演化实际上是由编码、测试和集成组合在一起的阶段。 维护阶段——维护是在系统交付使用后的变更活动。 实际上,面向对象的这四个阶段并没有明确的界限,从时间上讲也是交叉进行的,因

此严格的划分这四个阶段是没有任何实际意义的。一般来说,进行面向对象的程序设计需

要经过以下几个步骤: (1) 发现对象 前面我们讲过,对象是经过抽象的,具有唯一的名称,可以独力一个事物的实体,因

此发现对象就是一个抽象的过程,将相似的或者相同的事物抽象成类,这些类将具有共同

特征和行为,类的不同实例就是对象。在现有的开发工具中,大多数提供了许多基本类库,

抽象是应该多从已有的类出发,通过派生和继承等组合而形成新的可用的类。 (2) 发现对象之间的关系 在抽象出类和对象之后,就是确定对象和对象之间的关系。将这些对象的特征和行为

有机的组合起来就形成了我们程序结构。因此发现对象之间的关系是一个集成或者叫整合

的过程,这个过程在一些具有简单功能的对象之间建立一些特定的关系,从而将其整合成

为一个具有复杂功能的实体,这就是程序。 (3) 编码 编码就是采用一种计算机语言来将上面这两个过程的结构描述成计算机可以识别的

信息,如采用 C++语言或 Delphi等编写程序代码。

6.3 面向对象开发的特征

上面所说的三个步骤,在实现的过程中可能会复杂的多,而且也可以由不同的方法来

实现。比如分析就有“综合法”和“分析法”两种截然不同方法。下面就面向对象技术(OMT)中一些常用的方法和特征作一介绍。

(1) 自底向上的归纳 OMT的第一步是从问题的陈述入手,构造系统模型。从真实系统导出类的体系,即对

象模型包括类的属性,与子类、父类的继承关系,以及类之间的关联。类是具有相似属性

和行为的一组具体实例(客观对象)的抽象,父类是若干子类的归纳。因此这是一种自底

向上的归纳过程。在自底向上的归纳过程中,为使子类能更合理地继承父类的属性和行为,

可能需要自顶向下的修改,从而使整个类体系更加合理。由于这种类体系的构造是从具体

到抽象,再从抽象到具体,符合人类的思维规律,因此能更快、更方便地完成任务。这与

自顶向下的 Yourdon 方法构成鲜明的对照。在 Yourdon 方法中构造系统模型是最困难

的一步,因为自顶向下的“顶”是一个空中楼阁,缺乏坚实的基础,而且功能分解有相当

大的任意性,因此需要开发人员有丰富的软件开发经验。而在 OMT中这一工作可由一般开

发人员较快地完成。在对象模型建立后,很容易在这一基础上再导出动态模型和功能模型。

这三个模型一起构成要求解的系统模型。

第二章 面向对象的程序设计 第 86 页

(2) 自顶向下的分解 系统模型建立后的工作就是分解。与 Yourdon方法按功能分解不同,在 OMT 中通常

按服务(Service)来分解。服务是具有共同目标的相关功能的集合,如 I/O 处理、图

形处理等。这一步的分解通常很明确,而这些子系统的进一步分解因有较具体的系统模型

为依据,也相对容易。所以 OMT也具有自顶向下方法的优点,即能有效地控制模块的复杂

性,同时避免了 Yourdon方法中功能分解的困难和不确定性。 (3) OMT的基础是对象模型 每个对象类由数据结构(属性)和操作(行为)组成,有关的所有数据结构(包括输

入、输出数据结构)都成了软件开发的依据。因此 Jackson方法和 PAM中输入、输出数

据结构与整个系统之间的鸿沟在 OMT 中不再存在。OMT 不仅具有 Jackson 方法和 PAM的优点,而且可以应用于大型系统。更重要的是,在 Jackson方法和 PAM方法中,当它

们的出发点——输入、输出数据结构(即系统的边界)发生变化时,整个软件必须推倒重来。

但在 OMT中系统边界的改变只是增加或减少一些对象而已,整个系统改动极小。 (4) 需求分析彻底 需求分析不彻底是软件失败的主要原因之一。即使在目前,这一危险依然存在。传统

的软件开发方法不允许在开发过程中用户的需求发生变化,从而导致种种问题。正是由于

这一原因,人们提出了原型化方法,推出探索原型、实验原型和进化原型,积极鼓励用户

改进需求。在每次改进需求后又形成新的进化原型供用户试用,直到用户基本满意,大大

提高了软件的成功率。但是它要求软件开发人员能迅速生成这些原型,这就要求有自动生

成代码的工具的支持。 OMT彻底解决了这一问题。因为需求分析过程已与系统模型的形成过程一致,开发人

员与用户的讨论是从用户熟悉的具体实例(实体)开始的。开发人员必须搞清现实系统才

能导出系统模型,这就使用户与开发人员之间有了共同的语言,避免了传统需求分析中可

能产生的种种问题。 (5) 可维护性大大改善 在 OMT之前的软件开发方法都是基于功能分解的。尽管软件工程学在可维护方面作出

了极大的努力,使软件的可维护性有较大的改进。但从本质上讲,基于功能分解的软件是

不易维护的。因为功能一旦有变化都会使开发的软件系统产生较大的变化,甚至推倒重来。

更严重的是,在这种软件系统中,修改是困难的。由于种种原因,即使是微小的修改也可

能引入新的错误。所以传统开发方法很可能会引起软件成本增长失控、软件质量得不到保

证等一系列严重问题。正是 OMT才使软件的可维护性有了质的改善。 OMT的基础是目标系统的对象模型,而不是功能的分解。功能是对象的使用,它依赖

于应用的细节,并在开过程中不断变化。由于对象是客观存在的,因此当需求变化时对象

的性质要比对象的使用更为稳定,从而使建立在对象结构上的软件系统也更为稳定。 更重要的是 OMT彻底解决了软件的可维护性。在 OO语言中,子类不仅可以继承父类

的属性和行为,而且也可以重载父类的某个行为(虚函数)。利用这一特点,我们可以方

便地进行功能修改:引入某类的一个子类,对要修改的一些行为(即虚函数或虚方法)进

行重载,也就是对它们重新定义。由于不再在原来的程序模块中引入修改,所以彻底解决

了软件的可修改性,从而也彻底解决了软件的可维护性。OO 技术还提高了软件的可靠性

第二章 面向对象的程序设计 第 87 页

和健壮性。

7 小结

本章简单的介绍了面向对象的程序设计方法,其目的是使读者具备面向对象的基本知

识,因为面向对象是面向状态的基础。但这不是本书的主要内容,已经熟悉掌握 C++语言

的读者可以跳过本章,从第三章阅读,需要作更深入了解的读者,可以参考其他专门介绍

面向对象的书籍。本章也介绍了一些常用的程序设计方法,可以帮助读者更深入的理解和

使用面向状态技术,需要详细了解的读者应该查阅相关资料。 从下一章开始将进入本书的主题,介绍面向状态的思想和设计方法,其中很多内容都

是作者的一些经验的总结,希望能对读者有所帮助。

Beibei
Note
这一章看得不时很仔细! 大部分是一目十行!
Beibei
Note
Completed set by Beibei
Beibei
Re: Note
Beibei
Note
Accepted set by Beibei
Beibei
Re: Note
Beibei
Note
Completed set by Beibei
Beibei
Note
Rejected set by Beibei

第三章 面向状态的程序设计思想 第 88 页

第三章 面向状态的程序设计思想

从本章开始将介绍面向状态的程序设计思想和一些常用的方法,思想是关键,方法是

途经,因此建议读者需要仔细阅读本章。从本章开始,将会根据不同的情况据一些简单的

例子,来讲解面向状态的思想和方法,有些甚至于是比较弱智的例子,而采用常规方法或

者面向对象的方法可能会更简单一些,但请读者一定要注意,这里的例子仅仅是为了体现

如何实现面向状态,所以在例子上力求简单、明了,正如我国著名的象棋棋谱《局中密》

一样,看似平淡无奇,近似弱智,但是按照棋谱一步一步深入下去,若能逐一旁通,便会

发现其中奥妙无穷,受益匪浅。本书也希望通过一些简单的例子能给读者了解面向状态提

供最简捷的途径,通过练习,举一反三而最终达到熟练掌握的目的。

1 从 Hello World开始

很多计算机书籍在介绍一种开发工具时,总会从 HelloWorld 开始,本章也将沿袭

这一习惯,从一个稍微复杂一点的 HelloWorld 开始,给读者建立一个面向状态的感性

认识,然后在一步一步的深入问题,最后从理性的角度认识并掌握面向状态的技术。 本书面对的读者是对 C++ Builder有一定基础的,因此在这个例子中,涉及到 C++

Builder 的一些语法、规则等不多介绍,而重点放在与常规不同的实现方法上,不熟悉

的读者应该先阅读有关 C++ Builder的书籍。此外,这个例子非常简单,但却贯穿这本

章内容的重要思想,因此建议读者不要因为是一个弱智的例子而轻易越过。 面向状态并不是一个新的理论,而是在应用的早已出现的概念,尽管很多书籍都没有

这样称呼,但这是一个存在的事实。如果你使用过 Visual Basic 或者 Delphi 或者

C++ Builder,那么可以肯定你已经使用过面向状态的技术,前面第一章我们也说过,

面向状态正是从这些可视化编程工具中产生的,只不过那时候你可能只是被动的使用,而

没有真正的领会其中的奥妙和思想,更谈不上主动的使用面向状态的技术。接下来的内容

将让你全面的了解面向状态的技术,并教会你如何主动使用它。

1.1 问题与分析

本例要实现的功能如下: (1) 在屏幕上显示一个窗体,

窗体中包含一个 Button和

一个 Label,Label 显示

“HelloWorld!” (2) 当鼠标点击 Button时,

窗体底色由银灰色变为白

色,并且 Label的字符串变 图 3.1 HelloWorld 的运行界面

第三章 面向状态的程序设计思想 第 89 页

为“This is New State HelloWorld”,当再次点击 Button 时,窗体及

字符还原。 如图 3.1和图 3.2所示: 对该例进行分析,不难发现,当鼠标点

击 Button时,Form中有两项内容需要改

变,一是 Form 的底色,另一个是 Label显示的字符串,而且这两项内容始终相关联

在一起,当 Form 底色为银灰色时 Label为“HelloWorld!”,当 Form的底色为白

色时,Label 为“This is New State HelloWorld”,我们可以认为这是 Form的两个不同状态,而且在鼠标点击了 Button以后,程序必须知道到前 Form处于什么样

的状态,然后决定什么样的操作。按照常规的做法,我们可能需要定义一个 bool变量:

IsNewState来记录当前 Form的状态,在 OnButton1Click事件中写入要执行的代码,

尽管我们可以利用 Form的颜色来判断 Form当前处于什么样的状态,但这种做法显然不

够规范,而且是一种不负责任的做法,在复杂的程序中是很危险的。 今天我们将采用与上面做法稍微不同的实现方法,这就是面向状态的方法,从表面上

看,代码变动不大,而且似乎更加冗长,但实际上这是一种根本的变革,我们先看采用新

的方法是如何实现的。

1.2 Hello World 的实现

本例子中只包含一个 Form,Form中可视化构件只有一个 Button和一个 Label,其它成员是手工创建的,因此我们并不给出 Unit1.dfm 文件,只给出 Unit1.h 和

Unit1.cpp,源文件如下: Uint1.h文件 #ifndef Unit1H

#define Unit1H

//------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

//------------------------------------------------------------------

class TForm1 : public TForm

{ __published:

TButton *Button1;

TLabel *Label1;

图 3.2 HelloWorld 的新状态

第三章 面向状态的程序设计思想 第 90 页

void __fastcall Button1Click(TObject *Sender);

private:

bool FIsNewState; //定义一个 Bool变量,记录 State

protected:

void __fastcall FSetIsNewState (bool Value);

//为 FisNewState创建设置函数

public:

__property bool IsNewState = {read = FIsNewState,

write = FSetIsNewState};

//定义一个属性(Property)供程序使用

__fastcall TForm1(TComponent* Owner);

};

extern PACKAGE TForm1 *Form1;

//------------------------------------------------------------------

#endif

Unit1.cpp文件

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h" //------------------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

//------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{ //一个空的构造函数

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsNewState (bool Value)

{

if( Value == FIsNewState )

return;

if( Value )

{

Label1->Caption = "This is New State Hello World";

Color = clWhite;

第三章 面向状态的程序设计思想 第 91 页

} else

{

Label1->Caption = "Hello World!";

Color = clBtnFace;

}

FIsNewState = Value;

}

//------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)

{ IsNewState = !IsNewState; //令属性 IsNewState与原来相反

} 在这一段代码中,有几个明显的特点: (1) Button1Click中没有实际操作的代码,而仅有一条赋值语句。 (2) bool变量 FIsNewState为私有成员,不可以被其它对象直接访问。 (3) 多含了一个 IsNewState属性(Property)。 (4) 多了一个成员函数 FSetIsNewState。 Property是 C++ Builder 中特有的一个关键字,在英文中的含义为属性、道具、

财产、所有物、性质、特性等,在这段代码中理解为道具等为合适一些,但大多数的书籍

中均将其翻译为属性,为了保持一致,我们在本书中也称之为属性。 关于__property 关键字以及其用法我们在后面章节中详细讲解,这里我只要知道

IsNewState 是一个属性,它并不是实实在在的变量,也就是说程序并不会给它分配内存

空间,使用 IsNewState 属性的目的是:程序员可以将其当成一般变量一样给其赋值。

而这个赋值过程会通过 FSetIsNewState 函数最终反映在 FIsNewState 变量中。读者

可以验证 Button1Click中的代码: IsNewState = !IsNewState; 与

FsetIsNewState(!FisNewSate);

在编译后是完全相同的,从使用的角度讲,第一种方法显然直观得多,但这并不是最

根本的变革,最根本的变革是 Button1Click 摆脱了与其相关性较差的代码,而将这些

任务交给了属性 IsNewState,由其相关的配套函数 FSetIsNewState 在背后进行,属

性 IsNewState 不仅记录了当前 Form 所处的状态,还将这个状态的表现行为与之紧密

相连。因此程序员可以分别的去考虑在什么样的状态下程序有什么样的表现行为,以及在

什么时候要改变程序的状态。这就是面向状态的方法。 实际上,这个例子中除了用户定义的 IsNewState 属性以外,Color 和 Caption

也分别是 Form和 Label的属性,因此在 FSetIsNewState 函数中,也基本都是赋值语

第三章 面向状态的程序设计思想 第 92 页

句,而真正的执行中,Color的改变除了改变保存 Color真实变量的值以外,还需要对

Form进行重绘,Caption 的赋值也需要对画布的文字进行重新输出。可以想象,如果没

有这种机制,要实现这样一个简单的过程并不是那么简单。而且代码之间的逻辑也会变得

相对复杂化。

2 什么是面向状态

上一节我们从一个简单的例子接触了面向状态的程序设计,这一节将讲述面向状态的

一些概念和思想,在了解面向状态之前,首先从状态来了解起。因为程序设计实际上是将

人类日常活动计算机化,让计算机代替人类作一些事务,所以,程序设计和自然物质世界

是分不开的,和人类的思维过程是分不开的,这一节我们先从状态的概念了解起。

2.1 状态的概念

状态在面向状态的程序中有着特殊的地位,如同面向对象一样,状态也是对自然界的

一种抽象,或者是一种描述方法,而状态也和对象有着非常紧密的联系,下面我们将从自

然界对状态的定义开始学习。

2.1.1 什么是状态

状态在自然界是一个非常广泛的概念,指的是物体所处的状况和形态,可以是动态的,

也可以是静态的,比如物理上可以是指物体的运动状态,这是一个动态的概念,也可以是

指物体的通电状态、发光状态等,这些都是静态的。更广义的理解比如:心理状态、身体

状态、竞技状态等都是自然界对状态的定义。 从另一方面来看,状态又是一种抽象的概念,只有和人类的思维结合在一起才有实际

的意义,比如物体的运动状态,并不属于客观物质世界实实在在存在的物质体系,它不像

一棵树、一根草那样,是一个客观存在的物质,而是经过人类抽象而形成的一个概念,而

它的存在性又是客观的,并不会因为人类没有意识到运动的状态时,物体就不会运动。 从这两方面观察,我们可以这样理解状态: 状态是描述事物动态特征的变量,从上面的例子我们可以看出,无论这状态是动态的

还是静态的,它所描述的都是物质世界经过某种运动或者作用后的结果,比如运动状态是

描述物体受到外界力量以后的表现行为;心理状态可能会受到自身或者外界的某种影响而

改变。总之状态所描述的都是事物的动态特征。 将这一点引申到程序设计中,不难发现在程序设计中,状态的动态特征更加明显。在

程序的运行过程中仔细分析不难发现,每个过程的运行结果都可以是多个状态的组合,比

如一个绘图程序,程序的状态可能是某个按钮有效或者无效,屏幕显示某个对象被选中或

未选中,如果用鼠标点击某个图形对象,将其选中,这时屏幕显示图形被选中,同时“剪

切”、“删除”、“拷贝”等按钮将变成有效,当未选中任何图形时,这些按钮则是无效的。

记录或者表示一个按钮是否有效的变量便是程序中的一个状态,而这些状态最终反映的是

程序的动态特征。

第三章 面向状态的程序设计思想 第 93 页

如果说对象和类是认识客观世界一个静态的抽象,那么状态则描述的是客观世界的一

个动态过程。 状态是事物本身的特征,正如前面所提到的,不管你是否认识到事物处于某种状态,

也不管对它的状态是否有定义,它的状态都是客观存在的,因为物质由于运动而改变状态

也是可观存在的,因此说状态是事物本身的特征。在程序设计中一样,程序的运行状态也

是客观存在的,但是对于状态的抽象却可以是多种多样的。在自然界人们首先认识的是事

物,其次才是事物的状态:只有对“车”先有了定义,才可能认识到“车的运动状态”。

在程序设计中,只有在采用了面向对象技术之后,状态才有了更加精确的含义,状态应该

是某个对象的状态,没有对象的抽象就没有状态更精确的抽象。在一个程序设计过程中,

可以有多种不同的对象的抽象方式,因此也具有多种状态的抽象方式。 状态是程序设计中必不可少变量,不管程序设计经过了怎么样的发展,状态变量始终

是程序中不可却少的变量,即使在机器代码时代,程序中也会有一些环境变量来记录当前

程序运行的一些情况,也可以将这些环境变量认为是状态——属于整个程序的状态,只是

其含义和复杂程度比面向对象以后的状态要弱得多。 到了面向对象产生以后,状态的必要性越来越明显,因为程序本身是一个运动的过程,

需要时刻记录当前的运行状况,尤其是在图形界面出现以后,程序的状态就显得更加明显。

现在众多的软件开发工具中,尤其是图形化的开发界面,状态更是一个不可缺少的变量。

2.1.2 状态和对象的关系

前面已经对状态的概念有了一定的了解,对于程序设计而言,必须明确状态在程序中

的作用和关系。 在第二章介绍面向对象的时侯,已经讲过,程序的设计过程实际上就是一个抽象过程,

将所要完成的任务通过适当的模型进行描述,然后再用计算机语言表达出来,在后完成所

需要的功能。而这个模型的建立过程很大一部分内容就是对象的抽象过程。 根据前面我们对状态的理解,可以看出在程序设计中对象和状态有着以下关系: 状态属于对象的特征:正如前面所说的,面向对象是对客观事物的一种静态描述,对

象的成员是类的固有属性,包括成员变量和成员函数;而状态恰恰描述的是对象动态的特

征,是在程序运行中所处的不同状况。因此状态也是对象的固有特征,比如一个对象包含

着一个整型的成员,当这个成员被赋值为“0”或者“1”时,这个对象将处于两种不同的

状态。 有对象必然有状态: 状态是一种事物运动的一种抽象表达,就像上一个例子,有了对象。有了对象的成员,

就已经涉及到了这个对象的状态,这是客观存在的。在面向对象中,我们可能将精力主要

放在成员和成员函数的关系上,并没有太多的关注这个对象的状态,或者说并没有将对象

的状态放在第一位,而在面向状态的方法中,可能会将更多的精力用来分析对象的状态以

及处理对象和对象之间的状态的相互关系。 面向对象从面向对象发展而成的:面向状态并不是开始就有的,而是在面向对象技术

产生之后才发展起来的,当一个程序的对象比较多的时候,各个对象之间的联系会变得非

第三章 面向状态的程序设计思想 第 94 页

常复杂,任何一个操作,或者操作的结果都会影响到其他的对象,仅仅采用面向对象的分

析方法,要将一个程序中所有的逻辑关系表达清楚会有一定的难度。面向状态就是在这种

情况下产生的,从后面的例子中我们会发现,如果采用了面向状态的分析方法,可以将程

序中的逻辑关系简单化,使一些复杂逻辑关系转化为相对简单逻辑,从而使程序更加清晰、

明了,也更容易维护。 状态具有一般成员的特性又区别于一般成员:尽管在程序中存在对象,就有对象的状

态,但是并不是所有的成员都需要用对象的状态来描述。在这里我们可以先将状态理解为

也是对象的成员,但它又有区别于一般成员的特点,在后面的内容里将要介绍 C++ Builer中类的“属性”,通过对“属性”的了解和应用,会进一步理解状态和对象的关系。

2.1.3 状态的分类

作为对象的一个成员,状态可以是任意的变量类型,包括编程语言本身固有的数据类

型和用户扩展的数据类型,甚至是其他的对象,但从生存周期和表现形态上大致可以分作

以下几种类型: 持续状态和非持续状态: 所谓持续状态,指的是状态的生存周期比较长而已,比如汽车在匀速行使的过程中,

其运动速度就是一个持续状态,而此时的位置就是一个非持续的状态,是在不断的在变化,

同样的还有一些状态为瞬间状态,也属于非持续状态,比如汽车的点火过程,关车门的过

程等。 在程序中也有类似现实中的持续状态和非持续状态,比如一个对象的颜色,可能是持

续不变的,这时候颜色便是一个持续的状态,也可能是在连续变化的,这时候颜色便是一

个非持续状态。再比如按钮是否被按下这个状态也有不同的表现,一种情况是按钮本身自

己可以弹起来,当鼠标键按下时,按钮也按下去,当鼠标弹起时,按钮也弹起来,这种按

钮的状态便是一种非持续状态;另一种按钮是自己不能弹起来,鼠标点击后按钮被按下去,

只有再次点击该按钮时或者其他按钮被按下时才能弹起来,这种按钮的状态就是一种持续

状态。 实际上持续状态和非持续状态是相互关联的,位置的匀速移动实际上表明物体有一个

恒定的速度。也就是说一种持续状态可能是另一种非持续状态的运动结果,而一个瞬间状

态的作用可能使一个持续状态发生改变。这在数学上表示为运动函数和其导数的关系。 连续状态和离散状态: 所谓连续状态和离散状态是从状态的表现特征上分析的结果,很多状态,象速度、位

置等都是一种可以连续取值的量,属于连续状态;而发动机状态、车门状态等只有两种或

者几种有限的状态取值,属于离散状态。 由于计算机的特点,在程序中不管是连续的状态还是离散的状态,要对其进行处理或

者建立约束关系,都要转化成离散状态。在前面举的汽车的例子中,就很明显,汽车的速

度是一个连续的状态,而和车门状态、发动机状态建立约束关系时,只能用车速为不为“零”

或者车速“大于”多少和“小于”多少等来描述,这实际上就是将连续状态转换为离散状

态。

第三章 面向状态的程序设计思想 第 95 页

这一特点实际上也表明了计算机程序设计的另一个原则:建立约束和反约束关系的条

件只能是持续状态。比如车门的状态对车速是一个约束条件,而车速则对车门的状态是一

个反约束条件,我们建立它们之间的约束和反约束关系时,只能表示为车速大于零时,车

门必须是关的,车门为打开状态时,车速必须为零。在这个过程里将车速表示为离散状态

的同时也是将车速的状态转化为持续状态来表示,因为不管车速怎么样变化,车速是否为

零的状态都将是一个持续的状态。 对于状态的分类这只是一个大致的分法,是从状态的表现特征和生存周期来区分的,

而且这些不同类型的状态也可以相互转化,在程序不同的运行时刻也可能属于不同的类

型。从另一方面讲对于状态的抽象是根据实际需要进行的,跟程序设计者的经验和习惯都

是相关的。习惯上在程序设计时,总是将一些持续的状态称之为“属性”,而将另一些非

持续的状态称之为“事件”,“事件”实际上也是一种特殊的“属性”,但不能作为约束条

件,而是引发一些动作,或引起某些状态的改变。这也正好表明了“属性”和“事件”之

间的数学关系。

2.2 状态的特征

前面我们从自然界对状态的定义开始分析,已经逐渐了解在程序中的状态的基本含

义,但在接受面向状态的概念之前,我们还需要对状态的特征进行更深入的了解,因为程

序设计中要处理好各种状态之间的关系,就需要找出更适合计算机的描述方法。 尽管程序设计是对自然界事物的运动规律采用计算机语言描述而形成的,但程序必然

有不同于自然界事物运动的特征,有其自己独特的规律,计算机程序设计中的状态也和自

然界中的状态有着不同的地方。下面所讲的状态的特征,就是指在程序设计中所谓状态的

一些主要特征,它并不违背自然界对状态的定义,但又不等同于自然界对状态的定义。

2.2.1 状态是抽象的概念

对于状态的抽象这一特征,我们已经不止一次的提到,在程序设计中状态的出现也是

经过抽象而形成的。 在本章一开始我们举的例子 Hello World 中 IsNewState就是一个状态,它记载

着当前程序所运行的一种“状态”,这个状态所涉及的只有 Form 的颜色和标题字符串的

内容,这是一个简单的例子,但是从这个例子中可以看出:Form 的颜色和标题的内容本

身并没有什么联系,但是在程序运行过程中,它们是被联系在一起的,是同步的呈现着不

同的表现形式,这种表现形式经过抽象便是我们所看到的 IsNewState。显然,这个抽象

过程使得程序看起来更加“复杂”,但整个程序的结构会更加清晰,而且,IsNewState本身也封装了这个状态的具体实现过程,甚至包括这个状态所要成立的条件。

2.2.2 状态描述的是动态特征

前面已经说过,状态本身描述的就是一个动态的特征,这一点在程序设计中也是这样

的,前面也举过一些例子说明程序中的状态是描述程序的动态特征,从 Hello World 的

例子中我们也可以看出,IsNewState 和程序的运行是相关的,IsNewState 本身是一

第三章 面向状态的程序设计思想 第 96 页

个变量(严格的讲,这并不是一个变量,在 C++ Builder中冠以__property关键字,

我们通常称之为属性,在程序中可以当变量一样使用,但程序并不为其分配内存,在下一

章将有详细的将解),它并不是程序运行的“数据”,而是记录着程序运行的状况。如果程

序运行的自始至终 Form 都将是一个颜色,标题也不会变化,那么 IsNewState 就没有

任何实际意义。IsNewState 不同于一般变量的另一个特点是,它还封装着状态的实现过

程,这一点我们称作面向状态的封装性,在本章的后面和下一章将作以详细介绍。

2.2.3 状态之间存在约束关系

状态是描述程序的动态特征,状态之间的约束关系实际上反映的是程序运行过程的相

互制约关系,及在什么样的条件下可以进行什么样的操作,或者要进行某种操作需要具备

哪些条件。在 Hello World例子中 IsNewState 实际上没有任何约束条件,但它却是

另外状态 Form1->Color 和 Lable1->Caption这两个状态的约束条件。前面我们也说

过,Color和 Caption 实际上也是一种“属性”,在 Form和 Lable 的头文件中均被冠

以__property 关键字。Color 和 Caption这两个属性状态要受到 IsNewState的制

约。 状态之间的约束关系也存在着好多种类型,在 Hello World中的 IsNewState对

Color和 Caption实际上是一种不完全的约束关系,因为 Color和 Caption 除了受到

IsNewSate 的制约之外,本身还有一定的自由度,也就是说程序还可以在其他的条件下

单独的改变 Caption或者 Color,这也使得 IsNewState 成为一个不完整的约束条件,

因为 IsNewState不能完整的反映出程序所处的“状态”。 在一些更为复杂的关系中,状态之间不仅有约束关系,还存在反约束关系。比如在一

个程序中可能需要操作员登录,之后才能根据其权限进行其他操作,象数据录入、查询等,

这时登录权限就是进行数据录入和查询的约束条件,同时查询和数据录入也会是登录的一

种反约束条件,要退出登录状态必须先退出录入或者查询状态。 从这里也可以看出利用这种约束关系来分析程序的运行,会使其逻辑更加简单一些,

这也是我们为什么要进行面向状态程序设计的原因之一。

2.2.4 状态之间存在层次关系

状态之间的关系本身可能是非常复杂的,对一个应用软件而言或者一个程序模块,其

中的状态可能也是成千上百的,而这些状态之间的关系也是有密有疏。划分状态之间的层

次是为了更好的分析状态之间的关系。 前面所讲到的登录和数据录入、查询便是状态层次关系的一个很好的例证,登录是数

据录入和查询的必要条件,那么从程序整体上讲,登录是更高一层次的状态,而数据录入

和查询则是低一层次的状态。 建立层次的目的是为了简化分析过程和相互关系,因此在状态的层次结构上应该符合

以下原则: n 状态的层次是以其所辖范围或者生存周期进行划分的,这实际上已经包含了一种

制约的关系。从这一角度讲高层次的状态对低层次的状态应该具有正向的约束

第三章 面向状态的程序设计思想 第 97 页

力。 n 只允许相邻层次之间的状态或者同一层次之间的状态存在直接的相互作用关系。

这一要求实际上是为了使状态之间不会存在环形约束或者复杂的约束关系。 n 允许低层次状态对高层次状态具有反约束作用。 在这几个原则下建立状态的层次结构,或者是在程序建立各种约束关系时遵循层次之

间的这几个原则,则可以使程序各个状态之间的关系相对简化,使程序的复杂逻辑变为相

对简单的逻辑关系,也可以避免程序中出现的一些逻辑漏洞。

2.3 面向状态的概念

前面已经讲述了状态的定义、概念和特征,以及状态在程序中的一些表现特点,接下

来的内容将要介绍本书的核心问题——面向状态的程序设计。 所谓面向状态就是指在程序设计中,状态作为设计者和程序之间的第一界面,然后围

绕着状态的各种关系和逻辑结构,最终形成一个满足要求的应用程序。正如在面向对象的

设计中,程序员始终是将对象和类放在了第一位,一切都是围绕着对象来完成的。但是,

面向状态是由面向对象发展而来的,在面向状态的设计方法中,必不可少的需要以对象作

为基础。所以,面向状态的设计方法需要具备以下两个基本条件。

2.3.1 以状态为核心

在本书的第二章,我们介绍过几种不同的程序设计思想,其中有一些是把过程放在了

第一位的,有的是将数据放在了第一位,但在过去的这些经典思想中,基本还停留在一个

较低层次。直到面向对象的技术才将数据和过程有机的融合在了一起,通过数据和方法的

封装、对象的继承等技术手段,实现了现实空间和计算机语言的进一步统一,使计算机更

加真实的描述了客观事物,但这些并没有能够完全的对事物的运动特征进行详细的描述,

因为在面向对象的技术中始终是以对象为核心的,而构建对象也是程序开发的很大一部分

内容,尽管很多的 SDK中包含了非常强大的类库可供使用,但是对这些类库的使用仍然是

以对象为核心的,前面我们也讲过,面向对象的技术很难将程序运行的状态进行动态的描

述,这就出现了本书需要介绍的面向状态的技术。 针对程序运行的动态特征我们称之为状态,在面向状态的技术中就是以状态为程序开

发的核心,程序设计者的主要精力将是放在对与程序状态之间的关系的处理上。而面向状

态的技术是从面向对象的技术发展而来的,它本身具有面向对象的特征,但和面向对象又

有很大的区别。在面向对象技术中,对象的接口是一些公开的成员和公开的成员函数,而

在面向状态的技术中,一个对象的对外接口可能主要是由状态来构成的,这些状态在 C++ Builder中通常称作属性(property)或事件(Events),实际上事件也是一种特殊的

属性,它描述了对象状态的另一种特征,在后面的章节中我们详细讲解。总之在面向状态

的技术中,程序设计是以状态为核心而进行设计的。 在 C++ Builder中对象的对外接口还有一种就是方法(Method),熟悉数据库的读

者都知道 Table->Active = true 和 Table->Open()执行后的效果是等价的,但这

是两种不同的接口,这两种接口所不同的是如果采用方法(Method)作为接口,其执行

第三章 面向状态的程序设计思想 第 98 页

结果可以是带返回值的,程序员可以通过代码判断执行结果是否正确,而属性(Property)的设置,是没有返回值的。但是在 C++ Builder中并不提倡使用方法的返回值作为判断

执行结果的依据,而是通过“抛出例外”来处理程序遇到的错误,因此 C++ Builder中

Table->Open()方法实际上是一个没有返回值的函数,上面的 Table->Active = true和 Table->Open()是两个完全等价的操作。

2.3.2 以对象为基本构成单位

前面也已经说过,面向状态是从面向对象的技术发展扩充而形成的,其本质上还是基

于对象的程序开发,但不同的是更注重于程序运行时各个状态之间的相互关系,并且采用

了一定的新技术,使得对象之间的关系更加清晰,逻辑关系变得更加简单,使程序更符合

客观事物的本身规律,从而最终提高程序的性能和开发速度。 状态并不是孤立的存在着,是对一个对象的特征抽象而生成的,因此状态必须是依附

于对象而存在的,离开了状态的主体谈状态是毫无意义的。在 C++语言中对象和类相互依

存的,但从更广义的角度理解,一个运行的程序也是一个对象,是 Windows 中的一个对

象,这个对象由很多的元素组成,每一个元素也就是一个更小单位的对象。比如一个程序

可能由好几个 Form组成,每一个 Form本身就是一个对象,而这个 Form中可能又包含

了很多个组件⋯⋯ 总之在面向状态的程序设计中,对象是一个程序的基本构成单位,这一点和面向对象

是一致的。 实际上从广义的角度理解对象,可以认为对象就是一种数据类型或者数据结构的实

例,尽管在 C++语言中所有的类都被冠以 class关键字声明,但是没有冠以 class关键

字的数据类型,甚至是 C 语言的基本数据类型比如 int、double 等都可以理解为一种

“类”,只是这些“类”属于最最基本的“类”,而且没有任何独立于外界的操作,不像 class定义的类具有成员函数,也不具备继承、封装、多态等特性。举一个现实中更加形象的例

子,仍然以汽车为例: 组成汽车的对象有车身、车轮、发动机等,而车身、车轮、发动机又都是由更低层次

的对象构成的,如发动机是由气缸、活塞、曲轴、气门等构成⋯⋯直到最后无法再细划分

的对象,象铆钉、螺丝、活塞环、气缸盖等这些基本的零件就好像 C语言中基本的数据类

型一样。在面向对象之前的程序设计如同交给你一堆基本的零件:铆钉、螺丝、活塞环、

气缸盖⋯⋯然后统一组装成一辆汽车;而在面向对象的程序设计中好比将这些基本零件在

各个车间组装成本成品:车轮、发动机、车身等大的构件之后,再集中起来总装成一辆完

整的汽车;那么面向状态的程序设计如同给车加装了 ABS防抱死、自动换档、故障报警的

功能。这个比喻并不是十分切切,但可以帮助读者理解对象在面向状态中的地位。

2.4 面向状态的优点

面向状态的技术并不是最近才出现的,而是在可视化编程出现以后就已经发展起来

的。最早的面向状态应该算是 Microsoft的 Visal Basic,但 VB和 VF一样属于解释

性编译器,并不是真正编译器,直到 Borland推出了 Delphi之后,面向状态才真正的

第三章 面向状态的程序设计思想 第 99 页

深入到了编程语言中,C++ Builder 是 Borland 在 Delphi 之后推出的基于 C++语言

的可视化编译器,其性能已经得到大家的认可,我们就 C++ Builer 来看一下使用面向

状态都有那些优点。

2.4.1 消除相关性

所谓相关性就是指相互之间的关联,其主体可以是两个对象、对象和外界或者对象内

部的属性或状态等。换句话说:消除相关性就是使对象更加独立,尽量减少外界对自身的

限制。 在本章一开始的例子:HelloWorld 中,属性 IsNewState的创建就是一个很好的

消除相关性的例子,Button1的 OnClick 事件仅仅是 Button1 响应鼠标而触发的一个

事件,其处理函数 Button1Click和 Label1->Caption以及 Form->Color 并无直接

关系,程序员想要做的事当鼠标点击 Button1 时改变 Form 的状态,如果没有定义

IsNewState 属性,显然 Label1->Caption 和 Form->Color都要和 Button1Click联系在一起,这就增加了程序之间的相关性。相反的,定义 IsNewState 恰恰是将必须

关联在一起的 Label1->Caption 和 Form->Color 抽象成一个独立的状态,而且

IsNewState 属性还封装了其状态改变的条件和实现方法,这样不仅消除了相关性还使程

序的结构更加清晰、明了。 再比如 TCanvas类就是一个很好的例子,TCanvas 对象的 Handle 属性实际上是一

个 DC(device context)句柄,在 Windows 程序中 Device Context 是必须谨慎

使用的一个资源,因为 Windows 中的 DC 资源是有限的,每个程序使用以后必须在适当

的时候将其释放,不仅如此,API函数中对于 Device Context 的使用还有诸多的限制,

因此程序设计时必须考虑的面面俱到,否则很容易出现错误。而使用了 TCanvas 对象以

后,你完全不必担心 Device Context资源是否会被耗尽,对于 Brush、Pen 等绘图的

对象也不必担心出现冲突,这些在 C++ Builder 中早已被封装在 TCanvas的内部,你

只要专心去设计如何在 Canvas上绘制图形就行了。 另外一个典型的例子是 TWinControl 的 Handle属性,这是一个窗口的句柄,熟悉

早期 Windows程序设计的读者都知道,一个窗口在调用 API函数 CreateWindow 之前,

其句柄并不存在,而错误往往就出现在这里,程序要保证每一次访问窗口句柄的时侯该句

柄都是有效的,将会是非常繁琐的事情。但是使用 TWinControl 及其派生类就不会出现

这样的问题,C++ Builder 中属性的特点可以保证任何时候调用 Handle 都会得到一个

有效的句柄,因为属性 Handle封装了其实现的条件和实现过程,若该窗口没有被创建,

则对象会自动调用 CreateWindow来创建窗口,并设置 Handle为相应的句柄。 可能很多读者都注意到在 C++ Builder中使用属性时,只需要关心发生直接关系的

内容就行了,而不必要关心一个属性改变状态时所需要具备的条件和到底是怎样实现状态

变化的。这就是消除相关性带来的直接的便利。假如没有 C++ Builder中属性的这种特

点,在 HelloWorld的例子中,Button11Click函数不仅仅是对 Color 和 Caption进行设置,至少还应该向窗体发送一个刷新的消息,或者有一个刷新函数的调用。

第三章 面向状态的程序设计思想 第 100 页

2.4.2 简化逻辑关系

消除相关性的同时,就等于已经化简了逻辑关系。从 HelloWorld 这个例子中也可

以看出,IsNewState 是一个可要可不要的属性,但是要和不要的结果却相差很大,相比

较下,使用属性 IsNewState 无疑是增加了一个中间环节,但这个环节的加入不仅是消

除了相关性而将 IsNewState独立的抽象出来,更重要的是抽象出 IsNewState 后,该

属性和其周围的元素的逻辑关系将变得非常简单,在这个例子中,简化逻辑关系的效果也

许并不明显,我们来看一个逻辑稍微复杂一点的例子。 比如一个音频播放器,可能会有六个功能按钮:暂停、停止、快进、播放、快退、录

音。这几个按钮的功能和相互作用关系分别如下: 暂停按钮:是属于一个相对自由的按钮,无论播放器处于播放、录音、快进、快退还

是未工作状态,该按钮均可以被按下,按下以后保持状态,而且暂停按钮只能通过再次按

下时自行弹起来。 停止按钮:该按钮和暂停按钮互不影响,并且只有后面四个按钮中的一个被按下时才

有效,否则按钮为无效。停止按钮被按下以后自行弹起,状态不保持。 快进按钮:允许在未工作状态和播放状态被按下,因此除了录音状态始终有效,暂停

按钮对其不起作用。快进、播放、快退、录音四个按钮之间为相互排斥的按钮,即不能被

同时按下,但允许同时弹起来。 播放按钮:除了录音状态下,该按钮均是有效的,但不同的是如果暂停键被按下的化,

播放按钮并不动作,而是处于预动作状态,诸如打开音频设备,准备音频文件等工作都已

进行完毕。 快退按钮:同快进按钮一样,值时操作的内容相反,暂停键也对快退不起作用。 录音按钮:该按钮和播放按钮类似,当系统处于播放状态时不能进行录音即录音按钮

为无效状态,同样暂停键按下时,录音工作并不真正开始,而是准备好所有的录音条件。 这是一个非常典型的状态相互约束的问题,诸如此类的比如 Edit菜单中的 Delete、

Paste、Copy、Cut 等都和是否选中内容以及剪切板的内容有关。而处理这类问题往往

会出现顾此失彼的现象,如果一个程序中存在大量的按钮、菜单,它们之间又有着各种各

样的约束关系,处理起来就会显得非常繁琐,这是由于各种对象之间的逻辑关系复杂化而

造成的,我们试着采用分析状态的方法来对同样一个问题进行分析。 针对这个例子,程序的状态之间的影响可以分作两种类型,一种是按钮的状态和运行

状态之间的关系,也就是说在什么情况下,那个按钮是有效(Enable)的,在什么情况

下又是无效的;另一种是在不同的运行状态下对按钮动作所产生的影响。按钮的状态和程

序运行状态之间的关系可以用下表 3.1和 3.2来表示: 按钮有效性(Enabled属性)和程序运行状态之间的关系

运行状态 按钮

播放 快进 快退 录音 停止 暂停 暂停 1 1 1 1 1 - 停止 1 1 1 1 - 1

第三章 面向状态的程序设计思想 第 101 页

快进 1 - 1 0 1 1 播放 - 1 1 0 1 1

快退 1 1 - 0 1 1 录音 0 1 1 - 1 1 表中“0”表示无效,“1”表示有效。

表 3.1按钮有效性和各按钮状态的关系 按钮动作影响的状态和程序运行之间的关系

运行状态 按钮

播放 快进 快退 录音 停止 暂停 暂停 1 0 0 1 0 - 停止 1 1 1 1 - - 快进 1 - 1 0 1 1 播放 - 1 1 0 1 0

快退 1 1 - 0 1 1 录音 0 1 1 - 1 0 表中“0”表示未产生动作,或者动作执行不彻底,“1”表示动作彻底执行。

表 3.2按钮动作和各按钮之间的关系 比较两个表格,不难发现只有暂停的行和列不相符合,其他的行和列的值均完全一致,

这正说明了暂停和运行状态的关系更加复杂。另一方面这两个表格也体现出了状态之间的

另一种关系,就是约束的层次关系,显然按钮的状态对按钮的动作所影响的状态有约束力,

从层次上讲按钮有效性的状态比按钮的动作影响的状态高了一个层次,因为只有一个按钮

有效时才可能产生动作,在上面两个表中表现为只有第一个表中为“1”的单元格,第二

个表格中才可能为“1”。 除了这两个表描述的相互关系之外,按钮还有一个状态就是是否被“按下”,有些按

钮被按下的状态是保持的,除了“停止”按钮以外,其他的按钮都是这样,但是它们弹起

来的条件却是不一样的,暂停按钮是自己弹起来的,另外四个可以相互弹起来,也可以被

停止按钮弹起来,这几种关系可以通过设置 SpeedButton 的 Down属性来实现,而且也

比较简单,这里不列出来了。 根据上面的分析,我们可以设想定义六个属性来完成这些功能:WorkState、

IsPause、IsPlay、IsPrev、IsNext、IsRec。WorkState 用来记录工作状态,其

他几个属性用来记录按钮操作后的影响的状态,为 WorkState约束的状态。 整 个 程 序 包 括 三 个 .CPP 文 件 , 分 别 为 : Player.cpp、 Unit1.cpp、

PropertyFun.cpp。 其中 Player.cpp 为程序的主文件, Unit1.cpp 为 Form1 的单元文件,

PropertyFun.cpp 为一个辅助 cpp 文件,将建立属性的一些受保护的函数放在这个单

元,这样作的目的是将属性的建立和属性的使用分开放置,有利于设计,当然也完全可以

第三章 面向状态的程序设计思想 第 102 页

将它们放在一个文件中。 程序的代码如下: 资源文件“Unit1.dfm” object Form1: TForm1

Left = 284

Top = 133

Width = 297

Height = 77

Caption = 'Player'

Color = clBtnFace

Font.Charset = DEFAULT_CHARSET

Font.Color = clWindowText

Font.Height = -13

Font.Name = 'MS Sans Serif'

Font.Style = [ ]

OldCreateOrder = False

OnCreate = FormCreate

PixelsPerInch = 96

TextHeight = 16

object ToolBar1: TToolBar

Left = 0

Top = 0

Width = 289

Height = 29

Caption = 'ToolBar1'

TabOrder = 0

object Pause: TSpeedButton

Left = 0

Top = 2

Width = 48

Height = 22

AllowAllUp = True

GroupIndex = 1

Caption = '暂停'

OnClick = PauseClick

end

object Stop: TSpeedButton

Left = 48

第三章 面向状态的程序设计思想 第 103 页

Top = 2

Width = 48

Height = 22

GroupIndex = 2

Caption = '停止'

OnClick = StopClick

end

object Next: TSpeedButton

Left = 96

Top = 2

Width = 48

Height = 22

GroupIndex = 2

Caption = '快进'

OnClick = NextClick

end

object Play: TSpeedButton

Left = 144

Top = 2

Width = 48

Height = 22

GroupIndex = 2

Caption = '播放'

OnClick = PlayClick

end

object Prev: TSpeedButton

Left = 192

Top = 2

Width = 48

Height = 22

GroupIndex = 2 Caption = '快退'

OnClick = PrevClick

end

object Record: TSpeedButton

Left = 240

Top = 2

Width = 48

Height = 22

第三章 面向状态的程序设计思想 第 104 页

GroupIndex = 2 Caption = '录音'

OnClick = RecordClick

end

end

object StatusBar1: TStatusBar

Left = 0

Top = 30

Width = 289

Height = 20

Panels = <

item

Text = '工作状态:'

Width = 68

end

item

Width = 50

end>

SimplePanel = False

end

end 这个文件所定义的 Form如下图 3.3所示:

预定义头文件“Unit1.h”

#ifndef Unit1H

#define Unit1H

//------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <Buttons.hpp>

图 3.3Player界面

第三章 面向状态的程序设计思想 第 105 页

#include <ComCtrls.hpp>

#include <ToolWin.hpp>

//------------------------------------------------------------------

enum RecState{_STOP = 1,_PLAY,_PREV,_NEXT,_REC};

//定义列举类型的数据供属性 WorkState使用,列举从“1”开始是为了避开程序

//初始化对属性的影响/,因为程序在给变量初始化时赋值为“0”。

//------------------------------------------------------------------

class TForm1 : public TForm

{

__published: // IDE-managed Components

TToolBar *ToolBar1;

TSpeedButton *Pause;

TSpeedButton *Stop;

TSpeedButton *Next;

TSpeedButton *Play;

TSpeedButton *Prev;

TSpeedButton *Record;

TStatusBar *StatusBar1;

void __fastcall StopClick(TObject *Sender);

void __fastcall FormCreate(TObject *Sender);

void __fastcall NextClick(TObject *Sender);

void __fastcall PlayClick(TObject *Sender);

void __fastcall PrevClick(TObject *Sender);

void __fastcall RecordClick(TObject *Sender);

void __fastcall PauseClick(TObject *Sender);

private: // User declarations

RecState FState;

bool FIsPause;

bool FIsPlay;

bool FIsPrev;

bool FIsNext;

bool FIsRec;

//声明各属性的存储变量;

protected: // Protected域是手工后增加的

void __fastcall FSetState(RecState Value);

void __fastcall FSetPause(bool Value);

void __fastcall FSetIsPlay(bool Value);

void __fastcall FSetIsPrev(bool Value);

void __fastcall FSetIsNext(bool Value);

第三章 面向状态的程序设计思想 第 106 页

void __fastcall FSetIsRec(bool Value);

//声明各属性的设置函数;

public: // User declarations

__property RecState WorkState = {read = FState,write = FSetState};

__property bool IsPause = {read = FIsPause,write = FSetPause};

__property bool IsPlay = {read = FIsPlay,write = FSetIsPlay};

__property bool IsPrev = {read = FIsPrev,write = FSetIsPrev};

__property bool IsNext = {read = FIsNext,write = FSetIsNext};

__property bool IsRec = {read = FIsRec,write = FSetIsRec};

//声明属性;

__fastcall TForm1(TComponent* Owner);

};

//------------------------------------------------------------------

extern PACKAGE TForm1 *Form1;

//------------------------------------------------------------------

#endif

附加 CPP单元“PropertyFun.cpp”

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h"

void __fastcall TForm1::FSetState(RecState Value)

{ //WorkState的设置函数

if(Value == FState)

return; //这是一个安全措施,使程序引用属性时不需要考虑目前该属性

//处于什么状态;

switch ( Value )

{

case _STOP: //表示设置到未工作状态

//Pause->Enabled = true; Pause的状态和运行状态无关

Stop->Enabled = false;

Next->Enabled = true;

Play->Enabled = true;

Prev->Enabled = true;

Record->Enabled = true;

//设置按钮的有效性

Next->Down = false;

第三章 面向状态的程序设计思想 第 107 页

Play->Down = false;

Prev->Down = false;

Record->Down = false;

//设置按钮是否被按下

IsPlay = false;

IsRec = false;

IsPrev = false;

IsNext = false;

//设置底层次的属性,使其与工作状态 WorkState相吻合;

StatusBar1->Panels->Items[1]->Text = ""; //用于测试的状态栏

break;

case _PLAY: //表示设置到播放状态

//Pause->Enabled = true;

Stop->Enabled = true;

Next->Enabled = true;

Play->Enabled = true;

Prev->Enabled = true;

Record->Enabled = false;

IsRec = false;

IsPrev = false;

IsNext = false;

IsPlay = true;

break;

case _PREV: //表示设置到快退状态

//Pause->Enabled = true;

Stop->Enabled = true;

Next->Enabled = true;

Play->Enabled = true;

Prev->Enabled = true;

Record->Enabled = true;

IsRec = false;

IsNext = false;

IsPlay = false;

IsPrev = true;

break;

case _NEXT: //表示设置到快进状态

//Pause->Enabled = true;

Stop->Enabled = true;

Next->Enabled = true;

第三章 面向状态的程序设计思想 第 108 页

Play->Enabled = true;

Prev->Enabled = true;

Record->Enabled = true;

IsRec = false;

IsPrev = false;

IsPlay = false;

IsNext= true;

break;

case _REC: //表示设置到录音状态

//Pause->Enabled = true;

Stop->Enabled = true;

Next->Enabled = false;

Play->Enabled = false;

Prev->Enabled = false;

Record->Enabled = true;

IsPlay = false;

IsPrev = false;

IsNext = false;

IsRec = true;

break;

} FState = Value; //设置存储变量的内容

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetPause(bool Value)

{ //设置运行到暂停的状态

if(FIsPause == Value)

return;

switch (WorkState) //暂停和当前的工作状态有关

{ //快进、快退对暂停操作无影响

case _PLAY: //当处于播放状态

if(Value)

{

//此处添加暂停播放的代码;

StatusBar1->Panels->Items[1]->Text = "播放暂停";

} else

{

//此处添加暂停恢复播放的代码;

第三章 面向状态的程序设计思想 第 109 页

StatusBar1->Panels->Items[1]->Text = "正在播放";

}

break;

case _REC: //当处于录音状态

if(Value)

{ //此处添加暂停录音的代码;

StatusBar1->Panels->Items[1]->Text = "录音暂停";

}

else

{ //此处添加暂停恢复录音的代码;

StatusBar1->Panels->Items[1]->Text = "正在录音";

}

break;

}

FIsPause = Value;

Pause->Down = FIsPause; //该行代码可以省略

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsPlay(bool Value)

{ //设置程序为播放状态,播放状态受到暂停属性的约束

if(FIsPlay == Value)

return;

if(IsPause)

{ //添加播放并被暂停代码;

StatusBar1->Panels->Items[1]->Text = "暂停播放";

}

else

{ //添加播放代码;

StatusBar1->Panels->Items[1]->Text = "正在播放";

}

FIsPlay = Value;

Play->Down = FIsPlay; //该行代码可以省略

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsPrev(bool Value)

第三章 面向状态的程序设计思想 第 110 页

{ //设置快退状态

if(FIsPrev == Value)

return;

//添加快退代码;

FIsPrev = Value;

Prev->Down = FIsPrev; //该行代码可以省略

StatusBar1->Panels->Items[1]->Text = "快速退回";

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsNext(bool Value)

{ //快进的设置函数

if(FIsNext == Value)

return;

//添加快进代码;

FIsNext = Value;

Next->Down = FIsNext; //该行代码可以省略

StatusBar1->Panels->Items[1]->Text = "快速前进";

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsRec(bool Value)

{ if(FIsRec == Value)

return;

if(IsPause)

{

//添加录音并被暂停代码;

StatusBar1->Panels->Items[1]->Text = "暂停录音";

}

else

{

//添加播放代码;

StatusBar1->Panels->Items[1]->Text = "正在录音";

}

FIsRec = Value;

Record->Down = FIsRec; //该行代码可以省略

} 在这个文件中,多处对按钮的 Down属性的设置是可以省略的,但笔者建议加上更好

一些,该例中去掉这些代码对程序没有任何影响,因为引用这些属性的只有按钮的动作,

第三章 面向状态的程序设计思想 第 111 页

而按钮的动作已经可以正确的设置按钮的 Down属性(关于 SpeedButton 的属性和用法

详见联机帮助内容),但是作为一个程序应该尽量的保证状态对应的完整性。IsPause和

Pause->Down 是严格对应的,但是若有其他动作,比如由一个菜单项引发动作来设置

IsPause为 True,那么 Pause->Down = FIsPause就显得非常重要。 单元文件“Unit1.cpp” //------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h"

//------------------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

//------------------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

}

//------------------------------------------------------------------

void __fastcall TForm1::StopClick(TObject *Sender)

{

WorkState = _STOP;

}

//------------------------------------------------------------------

void __fastcall TForm1::FormCreate(TObject *Sender)

{

WorkState = _STOP;

}

//------------------------------------------------------------------

void __fastcall TForm1::NextClick(TObject *Sender)

{

WorkState = _NEXT;

}

//------------------------------------------------------------------

void __fastcall TForm1::PlayClick(TObject *Sender)

{

WorkState = _PLAY;

}

第三章 面向状态的程序设计思想 第 112 页

//------------------------------------------------------------------

void __fastcall TForm1::PrevClick(TObject *Sender)

{

WorkState = _PREV;

}

//------------------------------------------------------------------

void __fastcall TForm1::RecordClick(TObject *Sender)

{

WorkState = _REC;

}

//------------------------------------------------------------------

void __fastcall TForm1::PauseClick(TObject *Sender)

{

IsPause = !IsPause;

}

从这里也可以看出,这个单元只是 Form的各种事件对属性的简单引用。 这是一个完整的例子,只是缺少了其中部分关于具体操作代码,而这部分代码和本书

要将书的内容关系不大,有兴趣的读者可以自行完善。从这个例子中可以看出采用了针对

程序运行状态的分析之后,使程序内部的逻辑关系非常清晰,将一些复杂的逻辑关系用若

干简单逻辑进行组合而表达了,而这种组合关系正是经过抽象后的状态。

2.4.3 提高程序的可维护性

对于程序的维护一直是软件开发的一个重要内容,任何程序设计出来都不能保证没有

丝毫的缺限,而且维护本身还包含解决程序分析时遗留的缺限。由于面向状态的技术本身

是对事物更加真实的抽象模型,更加符合客观事物的规律,而且最大限度的消除了程序之

间的相关性,并且力图使用最简单的逻辑关系完成复杂的功能,因此在程序的维护上将更

加容易、可行。 仍然就上面 Player的例子而言,假如暂停按钮不光是对播放和录音其作用,而且还

对快进和快退起作用,那么只需要将 IsPause、IsPrev 和 IsNext 三个属性的设置函

数稍加修改就可以,其他代码完全不用动,因为也只有这三个状态和原先有差异。修改后

的函数如下: void __fastcall TForm1::FSetPause(bool Value)

{ if(FIsPause == Value)

return;

switch (WorkState)

{

第三章 面向状态的程序设计思想 第 113 页

case _PLAY:

if(Value)

{

//此处添加暂停播放的代码;

StatusBar1->Panels->Items[1]->Text = "播放暂停";

} else

{

//此处添加暂停恢复播放的代码;

StatusBar1->Panels->Items[1]->Text = "正在播放";

} break;

case _REC:

if(Value)

{

//此处添加暂停录音的代码;

StatusBar1->Panels->Items[1]->Text = "录音暂停";

}

else

{

//此处添加暂停恢复录音的代码;

StatusBar1->Panels->Items[1]->Text = "正在录音";

}

break;

case _PREV: //此处至 Switch结尾是增加的内容

if(Value)

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快退暂停";

}

else

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快速回退";

}

break;

case _NEXT:

if(Value)

{

第三章 面向状态的程序设计思想 第 114 页

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快进暂停";

}

else

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快速前进";

}

break;

}

FIsPause = Value;

Pause->Down = FIsPause;

}

//------------------------------------------------------------------

void __fastcall TForm1::FSetIsPrev(bool Value)

{ if(FIsPrev == Value)

return; //以下 if是修改的内容

if(IsPause)

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "回退暂停";

}

else

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快速回退";

}

FIsPrev = Value;

Prev->Down = FIsPrev;

} //------------------------------------------------------------------

void __fastcall TForm1::FSetIsNext(bool Value)

{

if(FIsNext == Value)

return; //以下 if是修改的内容

if(IsPause)

{

//⋯⋯

第三章 面向状态的程序设计思想 第 115 页

StatusBar1->Panels->Items[1]->Text = "快进暂停";

}

else

{

//⋯⋯

StatusBar1->Panels->Items[1]->Text = "快速前进";

}

FIsNext = Value;

Next->Down = FIsNext;

}

读者可以验证,在不考虑具体执行代码的情况下(以省略号代替的部分),将前面的

Player例子修改成上面这三个函数绝对不会超过 5分钟,而且不会出现错误。可维护性

高正是面向状态的一个显著特征。

2.4.4 提高程序的可扩展性

和可维护性一样,程序的可扩展性也是衡量一个软件优略的重要指标,有其对商业软

件而言往往是不断地提高、不断地完善功能。结构不合理的软件,很可能在扩充功能时不

得不将以前的代码推翻而重新编写,但这种情况在采用了面向状态的分析方法之后,出现

的几率将要小得多,除非扩展的内容与原来大相径庭。 前面 Player的例子是一个用于声音文件的播放程序,假如现在要扩展为播放多种格

式的文件,比如视频文件、Midi 文件或其他的流式多媒体文件。那么如何对程序进行修

改,我们从下面两点考虑: n 几个按钮对不同的多媒体文件表现形式是一样的,最多是某些类型不存在“录制”

功能,比如对于 Flash动画文件,但也可以将 Record 定义为新建一个 Flash对象并调用相应的编辑软件或者其他什么的,在这里我们不作过多考虑,认为所

有按钮不同类型的文件的表现行为都一样。 n 播放不同类型的文件只是具体的核心操作有所不同。 根据这两个条件我们看如何来修改程序。由于篇幅的关系,这里并不给出完整的代码,

仅就 Play功能播放 Avi文件给出一个修改思路: n 要增加一个关于文件类型的属性 MediaType; n 定义一个新的列举数据类型,或使用宏定义,提供给 MediaType不同的状态; 如 enum MDType{_WAV = 1,_AVI , /*⋯其他类型*/}; n 定义一个关于播放处理的函数句柄类型:TMEvents; n 增加一个关于播放的“事件”; 如:__property TMEvents OnMDPlay={read = ⋯}; n 修改 FSetIsPlay函数,并使用触发 OnMDPlay事件来完成具体的播放操作; n 在 MedisType 的设置函数 FSetMediaType 中写入根据不同文件类型改变

第三章 面向状态的程序设计思想 第 116 页

OnMDPlay句柄的代码; 在这个修改思路里面我们使用了面向状态的另一个非常重要的概念——事件,严格的

讲,事件也是一种属性(Property)只是这种属性其类型必须是函数的句柄,而且通常

是无返回值的函数句柄。需要对事件(Events)详细了解,请参阅后面有关章节。 采用了这种结构要对程序进行扩充是非常容易的事情——需要增加一种文件类型的

支持,只需要在 MediaType的定义中增加该类型的标识码,然后编写一个 TMEvents类

型的处理函数,最后在 FSetMediaType函数中增加挂入该类型处理函数句柄的就行了。

采用类似的技术编写现在比较流行的“插件”也是非常容易的事。 总之面向状态的技术给我们提供了很多解决问题的新方法和新思路,只要使用得当,

就会获得事半功倍的效果。

3 面向状态的基本思想

前面对面向状态的基本概念和优点作了一些介绍,读者应该有一个初步的了解,那么

到底什么是面向状态的设计思想呢?这需要结合编译器及编程语言本身的特点来进一步

了解。

C++ Builder 中的可视化组件(VCL)是基于属性、方法、事件(PEM)的模式,

PEM 模式定义了数据成员(Property),对数据成员的操作函数(Methods)以及类的

操作数之间的作用(Events)。VCL 为分层结构的类库,是用 Object Pascal 编写并

将之用于 C++ Builder 的 IDE中,通过对 VCL可视化类库的使用,可以使你非常方便

运用 C++ Builder的组件面板和对象查看器建立你的应用程序,而不需要手工编写很多

的代码。这正是 VCL的最初目的,但 VCL 的作用远不止于此,PEM模式不仅可以应用到

可视化组件之中,也可以使用到任意的代码单元中,只要这个单元运用了 VCL类库,我们

前面所举的两个例子就是这样的。 所谓面向状态的思想实际上一直贯穿于 VCL类库中,我们将通过了解 VCL的运行机

制来了解面向状态程序设计思想。

3.1 基于事件驱动的程序设计

VCL 组件是建立在 EDP 技术基础上的,EDP(Event driven programming)技

术是指程序基于事件驱动的,在这种方式中,程序员并不需要知道用户下一个操作会是什

么,而所有的过程都是由事件触发的,比如在 Windows 中消息队列及消息处理机制,便

是一种由事件驱动的程序处理机制。但是在 VCL中对这种事件构造要比 Windows 复杂的

多,也更有效地多,通常 VCL组件的事件都是通过抽象而形成的,能够独立并完整的描述

一个状态的改变情况,如PageControl的Onchanged事件,可以由用户鼠标点击Pages

标签而触发,也可以是由于用户按 Ctrl + Tab键而触发。

在进一步了解 VCL的事件至前有必要对不同程序设计的运行机制做更深入的了解,只

第三章 面向状态的程序设计思想 第 117 页

有深刻的理解了 VCL的处理机制才可能更加得心应手的使用之。

3.1.1 单任务操作系统的处理机制

单任务操作系统是 PC 中出现最早的操作系统,典型的代表示 MS-DOS,当时的程序

设计都有一个显著的特点,就是程序主要使用顺序的过程驱动方式。因为当时的程序基本

上是基于字符界面的,而且大多数应用都会是独占方式运行,即每一时刻时能运行一个程

序,即使在 Linux 或者 Unix 等多任务操作系统中,也是由系统为每一个程序创建一个

“虚拟机”,程序的切换只是在不同的“虚拟机”之间切换。 正因为程序运行是独占的,机器的所有资源都是该程序独自使用,因此大多是程序都

是按照顺序过程驱动的,比如一个程序包含了四个功能模块 A、B、C、D,其中 C模块为

判断并和 B模块组成循环,那么程序的执行顺序是可以预测的,流程如图 3.4所示: 程序的执行顺序总共有两个:要么沿 A→B→C→D执

行要么沿 A→B→C→B(循环过程)→D执行。 尽管在后也出现了许多优秀的图形界面,但整体来说

程序的处理机制仍然和按照顺序结构执行的,界面越是复

杂,处理起来难度也就越大。最令人头疼的是这种结构在

现实中是很难找到的,也就是说它并不完全符合客观规

律,因此程序设计的难度也相对较大。

3.1.2 多任务操作系统的处理机制

正是由于这个原因,在多任务操作系统尤其是图形界

面的操作系统中,普遍的引入的消息处理机制。图形界面

允许用户同时运行多个应用程序,而计算机的资源包括鼠

标、键盘等都是多各程序共同使用的,因此操作系统有必

要统一管理、协调各个程序占用的资源。 在 XWindows 中通常是操作系统向应用程序发送 Signal(信号),而在 Windows

中则是通过 Message(消息)处理机制来完成的,由于 Signal 和 Message 的产生是随

机的,用户的鼠标可能点击这个按钮也可能点击那个按钮或者菜单,是有一定的不可预知

性的,程序也是根据收到的不同消息或信号来调用相应的模块。这使程序功能得到大大的

增强,也更符合客观规律。 很可能出现的程序流程如图 3.5: 在 Windows 中,对于消息的处理过

程是这样的: n 系统由一个消息管理器,负责接

收所有的消息,并将消息分类,

分发给相应的应用程序或者线

程。 n 每 个 应 用 程 序 都 包 含 一 个

B

C

入口

结束

D

A

图3.4Dos 程序的执行流程

入口

CA

D

结束

消息处理函数

B

图 3.5 Windows 系统的处理流程

第三章 面向状态的程序设计思想 第 118 页

TranslateMessage 和 DispatchMessage 函数,TranslateMessage 负责

将消息中的 virtual-key 翻译成字符消息,DispatchMessage 负责分派消

息,并将其放入消息队列。 n 消息队列使系统分配给应用程序或者线程的资源,队列中消息并不立即被执行,

而是由系统根据当前的资源情况,在适当的时候调用相应的处理函数。也就是说

应用程序将处理权是交给操作系统的,而不是自己决定什么时候处理,什么时候

不处理的。 n 系统对应用程序功能的调用是通过所谓的回调(CallBack)函数来完成的,每

个应用程序或者线程至少包含一个以上的回调函数,一个典型的应用程序的回调

函数可能如下形式:

LRESULT FAR PASCAL _export WndProc(HWND hWnd, UINT message,

WPARAM wParam, LPARAM lParam)

{

switch (message)

{

case WM_QUIT:

//⋯⋯

case WM_DESTROY: // message: window being destroyed

PostQuitMessage(0);

break;

case /*其它消息*/

//处理过程

break;

... ...

default: // 调用 Windows缺省的处理函数;

return (DefWindowProc(hWnd, message, wParam, lParam));

}

return 0; }

这正是图 3.5中消息处理函数的那个圆的内容,但这个函数实际上是 Windows 系统

来调用的,而不是程序自己来调用。对于 Windows 的消息处理机制可以用下面的图 3.6来表示:

第三章 面向状态的程序设计思想 第 119 页

这是更贴近操作系统的消息处理机制,无论是采用什么语言或者技术来编写 Windwos程序,都必须遵循这个规则,但现在编程的时侯,我们很少再去编写程序的回调函数,而

是通过 Windows FrameWorks(框架)来完成的,使程序员可以从更高一层次驾驭消息。

3.1.3 其他 Windows FrameWorks 处理机制

FrameWorks 实际上就是一组 C++类库的组合,包含了基本界面元素和相应的消息处

理机制。在 VCL框架出现以前,Windows FrameWorks 分为两大阵容,一是 Borland公司推出的 OWL(Object Windows Library)和微软公司的 MFC(Microsoft Foundation Classes)。在 OWL 和 MFC 的最底层仍然使用类似 Switch⋯Case 结构

来处理系统的消息,但在写代码时可以采用被框架封装以后的更为简单的处理机制。 在 MFC 中消息处理最基本的也是使用最多的是:BEGIN_MESSAGE_MAP(消息映射

表)宏。对该宏的定义如下: #define BEGIN_MESSAGE_MAP(theClass, baseClass) \

const AFX_MSGMAP* theClass::GetMessageMap( ) const \

{ return &theClass::messageMap; } \

AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = \

{ &baseClass::messageMap, &theClass::_messageEntries[0] }; \

AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = \

{ \

应用程序1 应用程序2

TranslateMessage( )

DispatchMessage( )消息队列

回调函数

Windows消息管理器

Msg1

Msg2

Msg3⋯

Msg1

MsgA

Msg2

⋯⋯

消息队列

MsgA

MsgB

MsgC

⋯⋯

CallBackFun1CallBackFun2⋯

TranslateMessage( )

DispatchMessage( )

回调函数CallBackFun1CallBackFun2⋯

输入、输出

图3.6 Windows的消息处理机制

第三章 面向状态的程序设计思想 第 120 页

消息映射表的结尾使用 END_MESSAGE_MAP宏,定义如下: #define END_MESSAGE_MAP( ) \

{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \

}; \

由这两个宏的定义可以看出,实际上在每个 MFC类中都包含一个_messageEntries

数组,用以存放消息处理函数的代码入口,当消息表接到一个消息定义时,首先检查父类

的消息映射表是否包含对该消息处理函数的定义,如果有则给该消息分配与派生类中同名

的处理函数,如果没有则根据消息的定义宏取分配派生类中相应的处理函数。 一个程序的 BEGIN_MESSAGE_MAP定义可能如下: BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)

//{{AFX_MSG_MAP(CMainFrame)

ON_WM_CREATE( )

//}}AFX_MSG_MAP

END_MESSAGE_MAP( )

这一段代码告诉编译器在 CMainFrame类中,WM_CREATE系统事件将被注册使用,

并且在 Cpp单元文件中将有一个 OnCreate(LPCREATESTRUCT lpCreateStruct)函数在 CMainFrame的对象在接收到 WM_CREATE事件后被调用。

WM_CREATE是一个系统事件发出的消息,即该消息是程序运行时是系统自己产生的,

在 CMDIFrameWnd类中已经包含了对该消息的处理方法 OnCreate函数,CMainFrame是 CMDIFrameWnd的派生类,因此也继承了相应的消息映射表,缺省条件下 WM_CREATE消息将会调用父类的 OnCreat 函数,上面的宏将会使父类的 OnCreat 函数被派生类的

OnCreat函数所代替。因此在 CMainFrame 类的 OnCreat函数中至少要有一次对父类

OnCreat函数的调用。 Windows 中更多的消息是属于用户事件所发出的消息,即消息是由用户的操作而引

发的,象键盘、鼠标等操作引发的消息等都属于用户事件。用户事件往往在父类中没有消

息映射,对于消息映射表中消息的定义必须给出完整的参数,如下面的代码就是为

CWinApp 的派生类 CBatchApp 定义一个响应 ON_COMMAND 事件中 About 菜单

ID_APP_ABOUT消息的处理函数 OnAppAbout。 BEGIN_MESSAGE_MAP(CBatchApp, CWinApp)

//{{AFX_MSG_MAP(CBatchApp)

ON_COMMAND(ID_APP_ABOUT, OnAppAbout) //}}AFX_MSG_MAP

END_MESSAGE_MAP( )

第三章 面向状态的程序设计思想 第 121 页

在 Borland C++中的 OWL框架中,通常使用事件响应表(Response Table),总

共有 4个响应表的宏定义,分别为: DEFINE_RESPONSE_TABLE

DEFINE_RESPONSE_TABLE1

DEFINE_RESPONSE_TABLE2

DEFINE_RESPONSE_TABLE3

这几个宏的区别是所带的参数不同,后面的数字代表该类由几个父类派生而来的,它

们功能基本是相同的。DEFINE_RESPONSE_TABLE3的定义如下: #define DEFINE_RESPONSE_TABLE3(cls, base1, base2, base3)\

bool cls::Find(TEventInfo& eventInfo, TEqualOperator equal)\

{eventInfo.Object = (GENERIC*)this;\

return SearchEntries((TGenericTableEntry __RTFAR*)__entries,

eventInfo, equal) ||\

base1::Find(eventInfo, equal) ||\

base2::Find(eventInfo, equal) ||\

base3::Find(eventInfo, equal);}\

DEFINE_RESPONSE_TABLE_ENTRIES(cls)

结尾的 END_RESPONSE_TABLE宏定义如下: #define END_RESPONSE_TABLE\

{0, 0, 0, 0}}

其中 DEFINE_RESPONSE_TABLE_ENTRIES也是一个宏,定义如下: #define DEFINE_RESPONSE_TABLE_ENTRIES(cls)\

TResponseTableEntry< cls > __RTFAR cls::__entries[ ] = {

比较一下不难发现,MFC和 OWL采用的是非常接近的处理方法,尽管内部处理机制仍

有很大区别,而且性能和使用方法也不一样,但是这两种 FrameWorks 都属于同一类型

的。由于篇幅的关系,我们在这里不对 OWL 和 MFC 的消息处理作更多的介绍,需要进一

步了解的读者参阅相关书籍。

3.1.4 VCL 类库的处理机制

Borland 的 VCL 的消息机制相对于前两种 FrameWorks 有了很大的提高,在 VCL

第三章 面向状态的程序设计思想 第 122 页

中大多数情况下用户不必和 Windows消息直接打交道,而是和事件(Events)打交道。 Events不能简单的理解为 Windows 消息的映射,它不仅可以是 Windows 消息触发

的,而且还可以是抽象的事件,比如控件的 OnChange、OnClick 事件都不是消息的直

接映射,实际上抽象的事件在应用中更具有使用价值。关于 VCL中的事件我们在后面详细

介绍,这里仅就 VCL的消息处理机制和 Windows API 及 OWL/MFC 作一比较,API的

消息出来机制如图 3.7所示:

图中实线箭头表示直接对应关系,从上图以及回调函数的例子中可以看出,在

Windows API处理消息的时侯,其控制权是由回调函数来掌握的,用户定义的处理函数

都受到它的指派,而且在 CallBackWinProc 中本身就可以处理某些 Windows 消息,很

难作到针对对象进行消息分派。 在 MFC和 OWL中不再直接使用回调函数,消息映射或者响应表将 Windows 消息和相

应的处理函数对应起来,这种对应关系是根据类来作的,可以区分消息的来源和使用者。

但消息映射表是设计时建立的,运行时很难动态的修改消息的相应函数。在 MFC 和 OWL中也有部分“消息”属于经过处理后的抽象事件,但由于本身这种消息派发机制的限制,

功能比 VCL中要弱得多,没有从根本上得到提升,没有本质上的飞跃,如下图 3.8所示。

下图 3.9是关于 VCL 类库中消息处理的示意,图中的虚线箭头表示可以重新定向的

函数和事件的对应关系,双点划线表示消息是经过深度加工而触发事件。

消息

消息

消息

消息

CallBackWinProc 用户定义的函数

Fun1( )Fun2( )

消息

消息

⋯⋯

Fun3( )

⋯⋯

⋯⋯

Fun4( )

消息和响应函数并不是一一对应的,WinProc可以在一个消息中顺序调用多个处理函数。

消息处理函数也可以调用其他处理函数

不是所有的消息都有处理函数

图 3.7 使用 Windows API 的消息处理机制

消息

消息

消息

消息

CallBackWinProc 用户定义的函数

Fun1( )

Fun2( )

Fun3( )

消息

消息

⋯⋯

Fun4( )

⋯⋯

⋯⋯

消息的响应函数是根据需要在设计时定义的(MESSAGEMAP)(RESPONSE TABLE)

不是所有的消息都有处理函数,通常每个消息只能有一个处理函数。

消息处理函数也可以调用其他处理函数。

图 3.8 OWL 和 MFC 的消息处理机制

第三章 面向状态的程序设计思想 第 123 页

事件在 VCL中是预先定义好的处理函数的句柄,对于需要处理的事件,程序只需要将

句柄指向相应的处理函数就可以了。VCL 中的事件大多数都是经过处理的,过滤掉了

Windows 消息中的多余信息,并提供所必须的某些信息,最终提供给处理函数有针对性

地、特定的数据包,比如 OnClick事件通常是由鼠标左键弹起而触发,但在 TWinControl类派生的控件中,也可以是由控件获得焦点后按空格键或回车键而触发,同时 OnClick事件不需要任何关于鼠标和键盘的信息,而更希望得到一个事件发起者的信息,因此

OnClick事件通常只需要一个参数 TObject* Sender 来告知处理函数该事件是属于谁

发起的。 在 VCL类库中保留了对 OWL和 MFC的支持,因此在程序中也可以使用消息映射表和

事件响应表。此外 C++ Builder 的 VCL 类库本身中间层的消息处理也使用了

BEGIN_MESSAGE_MAP宏,宏定义的名称和使用方法同 MFC的宏完全相同,但它们的内

部处理机制是完全不同的,在编写包含 MFC类库的程序时,应该避免使用 VCL类库的消

息映射表,取而代之的是直接重载 Dispatch函数(VCL 的 BEGIN_MESSAGE_MAP 宏实

际上是重载 Dispatch函数)或者重定向 TControl派生类的 WindowProc属性。 观察 VCL关于 BEGIN_MESSAGE_MAP宏的定义: #define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message)\

{ \

switch (((PMessage)Message)->Msg) \

{

消息

消息

消息

消息

CallBackWinProc 用户定义的事件

OnMsg1

OnMsg2

OnMsg3

消息

消息

⋯⋯

OnMsg4

⋯⋯

⋯⋯

在VCL中为每个控件都定义了若干事件,可以分作“系统触发的事件”和“抽象事件”,

系统触发的事件

OnMsgA

OnMsgB

OnMsgC

OnMsgD

⋯⋯

抽象事件

用户定义的函数

Fun1( )

Fun2( )Fun3( )

Fun4( )

⋯⋯

Fun5( )

系统触发的事件是由系统的消息直接触发,抽象事件可以是由系统消息经过处理后触发,也可以由其他处理过程触发。

经过处理后

触发抽象事

函数触发抽象事件

事件的处理函数

事件的处理函数可以被重定向

VCL事件是经过筛选、重新组织并抽象而生成的只想特定函数类型的指针,因此可以被重定向,设计时可以为其分配函数句柄,运行时也可以随时更改。

系统触发的事件只能和相应的系统消息单一的对映,抽象事件则可以由不同的条件触发

消息直接触发事件

图 3.9 VCL 类库的消息处理机制

第三章 面向状态的程序设计思想 第 124 页

宏 VCL_MESSAGE_HANDLER的定义: #define VCL_MESSAGE_HANDLER(msg,type,meth) \

case msg: \

meth(*((type *)Message)); \ break;

结束宏 END_MESSAGE_MAP的定义: #define END_MESSAGE_MAP(base) default: \

base::Dispatch(Message); \

break; \

} \

} 就不难理解 VCL 最底层的消息处理机制了——消息映射表预编译后的代码正是

Windows回调函数的那种形式: BEGIN_MESSAGE_MAP

VCL_MESSAGE_HANDLER(WM_SIZE, TWMSize, WMSize);

VCL_MESSAGE_HANDLER(WM_SETFOCUS, TWMSetFocus, WMSetFocus);

END_MESSAGE_MAP(TCustomControl);

预编译后将会被翻译为: virtual void __fastcall Dispatch(void *Message)

{

switch ( ( ( PMessage )Message )->Msg )

{

case WM_SIZE:

WMSize(*((TWMSize*)Message));

break;

case WM_SETFOCUS:

WMSetFocus (*((TWMSetFocus *)Message));

break;

default:

TCustomControl::Dispatch(Message);

break;

第三章 面向状态的程序设计思想 第 125 页

}

} 基于 VCL类库的程序,各个功能模块基本上都是通过事件驱动并调用适当的函数而完

成的,事件并不等同于 Windows 中的消息。在面向状态的程序设计中,事件也是一个非

常重要的概念,要对事件更深入的了解,需要结合 VCL中另一个重要的概念——属性,以

及属性和状态、事件和状态之间的关系,我们在后面的内容对 VCL的事件做更详细的介绍。 关于对 Windows消息的响应,VCL支持使用 API 的方式,大多数的 VCL事件是经过

过滤、加工而成的,很大一部分 Windows 消息没有事件和之对应,但这些事件有时是必

须进行处理的,这时可以使用消息映射表或者使用 WindowProc 属性重定向某个对象的

消息处理函数。另外 C++ Builder中 TApplication 提供了更加贴近底层的处理接口,

OnMeesage 事件,这个事件是整个程序中所有消息的必经之路,通过它可以截获该程序

的所有消息,并可以决定是否允许后面的代码继续处理某个消息。这也已看出 VCL 在

Windows消息方面有着非常强大的处理能力。

3.2 属性与状态

“属性(property)”是 VCL 中最重要的概念,Proeprty 的英文含义为:财产、

所有物、所有权、性质、特性、道具,通常我们都将其翻译为属性,但从 VCL中 Property所实现的功能上看,称之为“道具”似乎更为合理,而从 Property和状态的关系上看,

称作“属性”却更恰当一些,Property正确的含义应该是“道具”和“属性”的结合体。

为了和习惯上的叫法统一,本书内 VCL的 Property均称之为“属性”。关于面向对象的

内容,很多书籍中均将对象的成员等称为“对象的属性”,本书第二章中没有采用这样的

称呼就是为了避免和 VCL面向状态中的“属性”相混淆。 属性最初目的是为了使 IDE在对象查看器(Object Inspector)中能够对对象的

某些状态在设计时进行设置,为此 Borland在 C++ Builder中对 C++语言进行了扩展,

并引入了属性的一些相关处理机制,包括引入事件,在此基础上便产生了面向状态分析和

设计方法。我们来看一下属性的一些特点。

3.2.1 属性封装了数据

属性本身并不是一种变量,更不是对象的成员,尽管在使用上属性和一般的成员非常

相似,程序在运行的时侯,并不会为属性分配存储单元,对属性的操作通过 C++ Builder的处理机制最终实际上是对封装在内部的其他数据进行操作的,从这一点意义上讲

Property更像是一个道具,而不是一个真是的变量。 细心读者可能已经发现在 C++ Builder 中所有 VCL 类库的__publish:定义域和

public:定义域中定义的数据基本上都是成员函数或者属性或者事件,成员一般只出现在

private:或者 protected:定义域中,可以说在 VCL中所有的数据成员都是受保护的,

其对外接口只有属性、方法和事件。 我们来看一下一个典型的属性的定义,这是 HelloWorld中的部分代码:

第三章 面向状态的程序设计思想 第 126 页

class TForm1 : public TForm

{

__published:

//⋯⋯

private:

bool FIsNewState;

protected:

void __fastcall FSetIsNewState (bool Value);

public: // User declarations

__property bool IsNewState = {read = FIsNewState,

write = FSetIsNewState}; //⋯⋯

}; __property 关键字后面的内容是定义了一个 bool 类型的属性 IsNewState,括

号中的内容是为该属性指定了该属性的读取和存储的内容,读取内容 read = FisNewState 表示在读取该属性值的时侯,所读取的内容是私有成员 FIsNewState 的

内容,存储内容 write = FSetIsNewState 表示在为该属性赋值时,实际调用了受保

护的设置函数 FSetIsNewState。关于属性的定义是比较多种多样的,__property 关

键字的详细使用方法在后面 C++ Builder的语言扩充中介绍,读者在这里只要知道:属

性本身不是一种实实在在的变量,对它的操作只是形式上的。正如前面提到的: IsNewState = !IsNewState; 与

FSetIsNewState(!FIsNewSate);

在编译后的可执行代码是完全一样的,但这种形式上的改变却带来了革命性的变化—

用全新的处理方法和思维方式解决问题。 属性和变量的使用方法基本是相同的,程序设计时完全可以将它看作是一般的变量,

只有一个小小的例外,就是不能将属性以引用的方式传递给其它函数,因为属性不是变量,

没有系统为其分配的内存单元,因此引用是一个无效的操作。 另一个值的注意的问题是属性的数据类型,从表现形式上看,属性可以为任意类型的

数据,包括 C++语言本身的基本数据类型和用户自定义的数据类型,但对于数组类型的属

性处理是比较特殊的,因为 C++语言中不允许将数组作为一个函数的返回值或者是参数。

这样一来属性所封装的数据中不能包括数组类型,这一点希望读者能有所注意。

3.2.2 属性封装了实现过程

属性不是变量,却封装着特定的数据,这是一个特点,除此之外还有另一个特点是属

性封装了特定的处理过程,或者叫实现过程。如何来理解这一特点呢,我们回想一下

第三章 面向状态的程序设计思想 第 127 页

FSetIsNewState函数: void __fastcall TForm1::FSetIsNewState (bool Value)

{

if( Value == FIsNewState )

return;

if( Value )

{

Label1->Caption = "This is New State Hello World";

Color = clWhite;

} else

{

Label1->Caption = "Hello World!";

Color = clBtnFace;

}

FIsNewState = Value;

}

当出现 IsNewState = !IsNewState;语句时编译器会自动调用 FSetIsNewState( !FIsNewSate ) ;函 数 , FSetIsNewState 不 仅 是 对

IsNewState 封装的数据 FIsNewState 进行设置,而且还将 IsNewState 的表现性状

一起更新,从这一方面讲将 Property称之为属性也是合理的。 这只是一个简单的处理过程的封装,复杂的可能还会有对属性约束条件的检查和设

置,举个很简单的例子: 在 Table的属性中包含一个 DataBase属性和 Active 属性,DataBase 属性可以

是一个 TDataBase组件,TDataBase组件中也包含一个 Active属性。它们的约束关

系为: TdataBase->Active 为 TTable->Active的必要条件。 因此在 Table->Active = true的执行过程中应该包含类似 Table->DataBase->Active

= true的处理过程。这样做的目的就是前面我们说到的消除相关性。 属性不仅在定义的存储( write)部分可以封装执行过程,也可以在读取部分( read)

封装特定的处理过程,象前面提到的 TWindControl的 Handle属性,其定义原型为: __property HWND Handle = {read=GetHandle, nodefault};

这里读取部分为一个函数句柄GetHandle,当程序读取 Handle属性时,GetHandle

会检查当前存储的数据 FHandle是否有效,若有效则返回 FHandle,无效则创建窗口并

设置和返回 FHandle为新创建的窗口句柄。

第三章 面向状态的程序设计思想 第 128 页

3.2.3 属性是状态的直接体现

从这里已经可以看出,属性是对象状态的直接反映。 属性和状态的关系就像类和对象的关系一样,属性是状态抽象的表现形式,而状态则

是属性的具体表现行为。 采用属性来标记对象的状态比采用变量来标记显然要先进得多,状态的标记必须是公

开的,声明在 public:定义域,能够被其他对象或者函数访问,而属性的存储数据是声

明于 private:定义域,外界只能通过属性或者成员函数来进行访问,这就保证了属性的

取值和状态是严格一致的,从而不必担心状态的标记已被修改而状态并没有刷新。因此说

属性是状态的直接体现。

3.2.4 属性的分类

属性与成员变量比较,还有一个特殊的地方,就是属性可以根据实际需要定义成不同

的类型,这些属性没有本质上的区别,但在使用上可能会稍有不同。大致的属性可以分作

读写型属性、只读型属性和抽象属性。 读写型属性 读写型属性使用起来和一般变量一样可以读取属性的取值,也可以为属性赋值,定义

属性时,一般读取(read)和存储(write)部分都有内容,通常读取部分直接和存储

的数据关联,而存储部分是由一个设置函数来完成的。这类属性在现实中如同仪表盘上带

指示灯的按钮一样,本身可以操作,同时也显示了状态信息。 在 VCL 组件中大部分属性都是这种类型,比如 Form 的 Color、Font、Height、

Top等属性都属于读写型属性。 只读型属性 只读属性在定义属性的时候,存储部分没有内容,TControl的 Handle 属性就是这

样的类型,通常读取部分是由读取函数来完成的,也可以是直接和存储的数据关联。这类

的属性在使用时只能当 Const 变量一样读取属性的取值,而不能为其赋值。现实中仪表

盘上的仪表、指示灯等就属于这种情况,不能对其直接操作,只能用来显示一些状态信息。 组件中只读属性并不是特别多,但却是不可缺少的,它能提供为对象提供非常重要的

状态信息,比如 Form中的 Active、ActiveMDIChild、Canvas、ClientHandle、ClientRect、Floating、Monitor等属性都属于这种类型。

虚拟属性 所谓虚拟属性,指的是属性表述了一个抽象状态,这种情况在 VCL组件里面非常的少,

虚拟属性并没有存储数据和其对应,而表现的可能是多个状态按照某种逻辑组合抽象而成

的状态,这种情况在现实中是存在的,比如仪表中的电功率表,它不是由一个自然的物理

量来显示的,而是由电流和电压共同确定的。 虚拟属性可以是只读的,也可以是读写型的,但是读写型的虚拟属性其设置函数的处

理要比一般的属性的设置函数复杂得多,通常会有附加条件,比如现实中对功率改变的附

加条件可以是定电阻改变功率、定电压改变功率和定电流改变功率。 这种属性在实际编程时用的很少,也应该尽量避免使用,或者通过增加中间属性的方

第三章 面向状态的程序设计思想 第 129 页

法转换成更简单的逻关系。

3.2.5 属性的继承性

在 C++ Builder中,属性还有一个重要的特点,就是具有 C++语言中类的继承性和

多态性。__publish:是 C++ Builder 中增加的一个声明的域,声明在__publish:域中的属性和 public:定义域中的属性或者成员有着相同的外部访问权限,而且是被发布

的,可以在 IDE 的 Object Inspector 中访问,但继承特性稍微不同。关于属性的继

承性和__publish:关键字的使用方法在后面的章节作详细介绍。 属性的多态性体现在不同的类的同一属性可能具有不同的表现形态,比如 Active在

很多组件中都有,但是 Active的意义和实现方法却相差的很远。

3.3 事件的概念

C++ Builder 中另一个重要的概念就是事件,前面对事件也有所接触,接下来将重

点就事件作一些概念性的介绍,事件的定义以及使用方法也在后面针对 C++ Builder语

言语法扩充的内容中详细介绍。 在 C++ Builder中,事件通常都是以On前缀开始的,比如 OnClick、OnMouseDown

等等都是事件,从字面上理解似乎是当⋯的时侯,这是 C++ Builder中的一个缺省约定,

就象属性的设置函数以 FSet或者 Set开头后面跟属性的名称一样,编程序的时候也可以

完全不按照这种约定来定义事件,但这种可以顾名思义的命名习惯显然更有利于程序的开

发和设计。下面我们看一下 C++ Builder中事件有什么样的特征。

3.3.1 事件是一种特殊的属性

在 C++ Builder中事件实际上就是一种属性,但属于一种非常特殊的属性,和一般

的属性有较大的区别,从其特征和用途上来看,事件是一个更适合的称谓。我们来看一个

典型事件 OnClick的定义: __property Classes::TNotifyEvent OnClick = {read=FOnClick, write=FOnClick,

stored=IsOnClickStored};

从形式上看这和定义一个属性的形式非常类似,但这种形式上的一致并不代表着本质

上的一致。对于一个组件在对象查看器中属性通常被显示在 Propertys标签页中,而事

件则被显示在 Events标签页中,双击对象查看器中的事件,IDE会自动创建一个和事件

匹配的处理函数,并将光标移动到相应的位置,这仅仅是在表现形式上的不同。对于一个

事件要求其类型必须是函数的指针类型,上面的 TNotifyEvent 就是一个函数指针,其

定义如下: typedef void __fastcall (__closure *TNotifyEvent)( TObject* Sender);

第三章 面向状态的程序设计思想 第 130 页

这里又涉及到 C++ Builder 扩充的另一个关键字__closure,这是对事件继承性

的另一个要求,关于__closure关键字的详细介绍参见后面章节。 通常情况下事件是可读写的,其读写部分和存储部分可以直接和存储数据关联(上面

的 OnClick就是这样),也可以是由读取函数和设置函数来完成。因此事件在程序运行的

时候是允许改变取值的,这正是 C++ Builder中对消息处理和其它 FrameWorks最大

的区别,这也使得事件了对操作系统的依赖和约束,可以更加独立的描述一件事情或者一

个过程,使用也更加灵活。

3.3.2 事件是抽象的瞬间状态

前面我们将状态分为持续状态、非持续状态、离散状态和连续状态,在计算机中连续

状态通常也需要转化为非连续状态而使用数字量来描述,而持续状态通常是用属性来描述

的,非持续状态正是用我们这里要讲的事件来描述的。 在 C++ Builder 中的事件中有 OnClick、OnChange、OnActive 等事件,都属

于“当⋯发生的时侯”型的事件,还有一些是“当⋯发生之前”(Befoer⋯)或“当⋯发

生之后”(After⋯)等形式的事件,但仔细观察,这些所有的事件都描述的是一种瞬间

状态,是一种非持续的状态。 但在 C++ Builder中几乎所有的事件都是经过一定的抽象而形成的,它是对象的一

种内在的因素,对象本身就是抽象的,因而事件也是需要抽象而形成,尽管 VCL中好多的

事件都是 Windows消息的直接映射,但是反映到 VCL组件中时也是具有抽象性的,所有

的事件都是针对某个对象而言的,并不是整个程序。 读者最需要区分的就是事件和 Windows 的消息,这属于两个完全不同的概念,有着

非常大的本质区别,由于事件是 VCL 消息处理机制的一部分,很多 C++ Builder 用户

都没有正确区分两者的关系,但实际上 VCL的事件从其产生的那一刻起,就比 Windows消息有了质的飞跃。正确认识事件对学习和使用 VCL类库有着非常大的影响。

3.3.3 事件的分类

事件是 C++ Builder中同属性一样重要的语言支持,跟属性一样,事件是针对对象

而进行表述的,当然可以有系统消息触发,也可以后其它运行状态而触发,抛开对象,仅

从触发的条件来看,事件大致可以分作以系统事件、用户事件和抽象事件等三种类别。 系统事件 系统事件指的是这个事件是由系统消息直接触发的,是对系统消息的一个直接映射,

在图 3.9中可以看出,系统事件是和系统消息单一对映的,即一个系统消息只允许触发一

个系统事件,但系统事件可以由其他条件触发。 在 C++ Builder中这类事件是非常多的,比如 OnPaint、OnTimer、OnMaxim、

OnResize等,在进行程序设计的时候,定义这类的事件也比较简单,处理过程相对也简

单。 但系统事件并不等同于简单的消息处理,通常对一个系统消息也要进行加工处理,象

OnPaint、OnTimer 这些事件完全不需要消息中的任何参数,因此在这些事件的处理函

第三章 面向状态的程序设计思想 第 131 页

数中不需要定义任何与系统消息有关的参数。 用户事件 用户事件是通过用户的操作所触发的,但并不是 Windows 消息的直接映射,通常对

一些消息进行一些简单加工而形成的。用户事件有一定的抽象性,但是和 Windows 消息

的关系也比较紧密,一般来讲用户事件更能描述一个过程或操作的结果,用户事件不一定

对应一条消息,也可能由多个消息而触发,或者是不同的消息都能够触发,典型的例子是

OnClick事件,而且在不同的组件中 OnClick触发的条件也是不同的。比如: OnClick事件在 Button中可以是用户点击鼠标左键,也可以是 Button获得焦点

后按空格键或者回车键。 在 Form 中如果包含缺省按钮(Default Property)和取消按钮( Cancel

Property),则按动回车和 ESC 键分别可以触发缺省按钮和取消按钮的 OnClick 事件。 而在 Grid、List、ComboBox 等组件中用户选择不同的 Item 都会触发 OnClick

事件。 显然这种针对对象的事件比起针对 Windows消息的处理方式要有效和方便得多,VCL

中的用户事件也非常多,象 OnKeyPress、OnDblClick、OnMouseDown 等等,这些事

件和系统事件一样,往往经过将消息中的参数进行加工,象 OnClick 事件不需要消息的

参数,OnKeyPress 事件将键盘扫描码转换成 Virtual Key 代码,并包含了功能键

(Shift,、Alt和 Ctrl)的信息,使 OnKeyPress 事件甚至可以响应 Mouse的点击消

息。 用户事件是 VCL中使用比较多的事件,也是对 VCL类库来说必须要掌握的基本事件。 抽象事件 抽象事件应该说也是一种用户事件,是由用户的操作所触发的,但是抽象事件比起前

面所说的用户事件要更具有抽象性,和系统消息的相关性更小,一般来说,抽象事件不是

直接由系统消息触发的,但是可能是很多系统消息对一个对象状态产生的相同的影响,有

时候甚至抽象事件并不是系统消息触发的,而是在其他状态改变时触发的。 在 C++ Builder 的 VCL 中这类事件非常多,比如组件的 OnChange 事件,Form

的 OnActivate 、 OnClose、 OnCloseQuery、 OnCreate 、 OnDeactivate、

OnDestroy、OnHelp、OnShow等事件都属于这一种类型。 抽象事件在 VCL中的作用是无法替代的,它使事件完全属于对象的范畴,而和系统消

息没有多大的关系,它反映的是对象的特征,而不是反映了用户的操作。程序员完全不必

知道 OnClose 事件是怎么产生的,也许是系统的任务管理器发出的、也许是用户点击关

闭菜单或者是其它操作发出的,但这些操作共同的特征是关闭该 Form,这对于程序员来

说,也仅需要关心在接收到关闭指令时需要做什么善后事情。 诸如此类的例子非常的多,抽象事件正是 VCL事件中最大的突破和革新,熟练掌握和

巧妙利用,可以对改善程序结构和性能有非常大的帮助。

3.3.4 事件的继承

同 VCL中的属性一样,事件也是具有继承性的,而且事件也具有多态性,事件的多态

Beibei
Note
在此休息一下!

第三章 面向状态的程序设计思想 第 132 页

性表现在不同的类中,事件具体的响应条件可能有很大的差别。 对于事件的继承区别于一般成员的继承,也区别于一般属性的继承,而且在不同的定

义域__publish:、public:等之间的继承与标准的 C++语言有很大的区别。这主要是由

于事件是一种函数句柄,在标准的 C++语言里,可以将一个父类的指针指向其一个派生类

的实例,如: TCostumerForm * Form = new TForm ( ParentHandle);

// TForm由 TCostumerForm派生而来;

将是合法的,但是将一个父类中的成员函数的指针变量指向派生类的成员函数这是不

允许的,如: class base

{

public:

void func(int x);

};

class derived: public base

{

public:

void new_func(int i);

};

void (base::*bptr)(int); //定义一个指向父类成员函数的指针

bptr = &derived::new_func; //这是非法的;

父类的事件在继承到派生类的时侯,将会出现这种情况,但在 VCL类库中对事件这样

操作是允许的,这需要依赖于前面提到过的关键字:__closure。此外,事件的继承还

有一些其它需要注意的地方,我们将在后面内容详细讲解,这里读者只需要了解事件也具

有继承性。

3.3.5 属性和事件的关系

从程序的执行上,可以这样认为,事件就是提供了一个处理函数的接口,这个接口会

在适当的时候自动的调用,当然调用之前必须检验从处理句柄的有效性。例如事件

OnClose的触发: begin

if Assigned(FOnClose) then FOnClose(Self, Action);

end;

第三章 面向状态的程序设计思想 第 133 页

注:这是 TForm 中 OnClose 触发的一段源码,是由 Object Pascal 编写的

FOnClose是 OnClose事件的内部存储数据,翻译成 C++ 代码如下: {

if( FonClose )

FonClose(this,Action);

}

那么从这本书要讲述的面向状态的设计思想来分析,事件处于什么样的一个地位呢?

根据前面对状态、属性以及事件的介绍,再结合 C++ Builder中的事件的实例,不难理

解:事件实际上就是体现了持续状态的变化。所有的事件都是由状态改变触发的。这就是

为什么在 C++ Builder中事件都是以 On前缀开头的,因为这样更容易理解为:“当⋯的

时侯”。 n 对于系统事件可以理解为系统状态改变的时候触发的,如: OnTimer事件就是系统时间改变的过程中当达到我们设定的步进值时触发。 OnKeyDown可以理解为键盘状态改变的时候触发的。 n 用户事件和系统事件类似,但是更贴近于对象的状态,如: Button的 OnClick事件反映的是 Button状态处于被按下并弹起来的时候触发的,

而在 ListView中又可以反映鼠标点击或类似点击效果的被选中时触发。 n 而抽象状态则反映的是更加抽象的状态的改变状况,象 Form的 OnActivate、

OnClose、OnCloseQuery等,读者稍加分析就不难理解。 从数学和物理的角度来分析,对一个事物或者某时刻运动状态的描述,至少应该知道

该点的函数值(或运动状态)和该点的一阶导数(或叫做运动趋势),而在 C++ Builder中属性和事件恰恰反映了这两者的关系,在程序的运行中,大多数状态都是持续的离散状

态,即使连续数据在计算机中也只能用离散的数字量来表示。这样一来对计算机程序的描

述只要使用属性和事件就已经相当完善了——研究阶梯函数只需要知道函数值和奇点就

可以了。数学描述和状态分析的对比示意见图 3.10:

这个图的表示并不是非常恰当,但可以反映出事件合状态之间的关系,对于

Before⋯、After⋯事件可以认为是上面第二个图中 St曲线的两个拐角处。 在数学中,函数的导数也是一个函数,而在 C++ Builder中属性是状态的直接体现,

dy0dx0F(x0)

y = F(x)

x

y

x0

事件OnStChange

状态St

程序运行

数学中函数的分析 程序中状态和事件的关系

状态1

状态2

图 3.10 数学分析和面向状态的关系

第三章 面向状态的程序设计思想 第 134 页

事件是状态变化的反映,而事件也恰恰是一种特殊的属性,这两者之间的吻合并不是偶然

现象,而是因为面向状态的方法正是从实际应用中总结出来的。 另外,在 C++ Builder中完全可以为事件句柄的改变而定义一个触发事件的接口,

就是当一个事件的数理函数被换掉时触发另一个事件,比如: class EventsSteps

{

previte:

TMyEvent FEventsStep1; //一阶事件存储句柄;

TMyEvent FEventsStep2; //二阶事件存储句柄;

protected:

void __fastcall FSetEVT1(TMyEvent Value);

//一阶事件的设置函数

public:

__property TMyEvent OnEventsStep1 = {read = FEventsStep1,write

= FSetEVT1}; __property TMyEvent OnEventsStep2 = {read = FEventsStep2,

write = FEventsStep2};

}

EventsSteps::FSetEVT1(TMyEvent Value)

{

if( Value == FEventsStep1 )

return;

if(Value ...) //二阶事件的触发条件;

if(OnEventsStep2 )

OnEventsStep2(...);

FEventsStep1 = Value;

}

这种情况在 C++ Builder中是完全允许的,甚至你可以在程序中定义无穷多级事件,

这就像函数的二阶导数和高阶导数一样,但在实际程序设计中很少出现这样的需求,也是

没有必要的,因为计算机中的状态最终都需要表示为离散的状态,相当于数学中的阶梯函

数,因此取其一阶导数就已经足够了。 这里可以看出,C++ Builder 中这种对象的模型是对自然界事物基本规律的真实反

映,不仅具有静态数据的抽象,还具备了运行动态的描述,是目前最符合实际规律的抽象

模型,因而采用 C++ Builder作为开发工具的时侯,可以获得更高的开发效率,最大限

度的提高软件的性能。面向状态的分析方法正是这种抽象模型的分析方法。

第三章 面向状态的程序设计思想 第 135 页

3.4 面向状态的特征

关于面向状态的两个重要的概念属性和事件,我们已经有了一定的了解,但要实现面

向对象,还需要在程序设计过程中遵循一定的规律,或者是构成规则,这种规则并不是唯

一的,但必须存在。仅有属性和事件是不足以组成面向状态的设计技术,接下来将针对 C++ Builder语言中 VCL 类库,来讲述面向状态的一些基本特征。通过对这些基本特征的了

解,读者可以更好的理解面向状态的设计思想。

3.4.1 抽象性

从本书的一开始,我们就强调抽象性,程序设计本身就是一个将现实抽象到计算机的

工作,面向对象的时侯就是这样,在面向状态中更是这样。 在对事物的描述中,类的出现和划分就是一个非常抽象的过程,通过类和对象使得程

序设计中的结构、逻辑更加符合客观自然规律,更加符合人的思维习惯和认识过程,而状

态是对事物静态特征之外的动态特征更加抽象的描述。这比面向对象中只描述静态特征要

先进了许多,因此抽象性也将是面向状态中一个突出的特征。 从前面的一些例子中我们可以看出,不管是属性还是事件,最终都是和对象紧密的结

合在一起,成为对象本质特征的一部分。一个简单的属性例,如对数据的直接映射: __property Data = {read = FData,Write = FData};

这个属性在实际中唯一的作用是可以在 IDE环境中设置变量的值,除此之外没有任何

实际意义。 正确的划分一个对象的状态并合理的创建属性,可以提高程序的质量,相反的,不合

理的状态划分和属性表达,则可能使程序的结构更加复杂,更加混乱,达不到应由的效果,

而会适得其反。将抽象性列入面向状态的特征之首,正是为了提醒读者,面向状态是来源

于现实,不是空中楼阁,正确的使用是应该尊重客观规律,而不能是盲目设计。

3.4.2 封装性

在面向对象的设计方法中,封装性就是其特点之一,面向状态中仍然遵守着面向对象

的原有规则,即封装性、继承性和多态性。这里所说的封装性不是面向对象的封装性,而

指的是面向状态比面向对象扩充的部分所具有的特征,也就是对象的状态和其表现特征的

封装。 这一点主要体现在属性上,因为属性是状态的直接体现,通过前面的例子,相信读者

已经不难理解属性的封装性。从表面上看,采用了 C++ Builder中属性的封装,使程序

中改变了一种书写方式,正像前面举 HelloWorld的例子: IsNewState = !IsNewState; 与

FSetIsNewState(!FIsNewSate);

第三章 面向状态的程序设计思想 第 136 页

是完全等价的,编译后的机器代码是一模一样的,但实际上这种表面形式的不同代表

了一种本质的区别。属性 IsNewState 完全可以独立的描述一种状态,而变量

FIsNewSate 就不可以,必须依赖 FSetIsNewState 函数,这种依赖关系会破坏状态的

本来含义。 在 C++ Builder中属性的设置函数和读取函数通常是被声明在__protected:定义

域,而属性才是最终对外公开的接口,这是利用了面向对象的封装性对一个对象的行为进

行保护,在标准的 C++中,这种受保护的函数不能随便被外界访问,而在 C++ Builder中则是通过属性的封装性,使得外界可以间接访问受保护对象,从而实现了状态和表现行

为的严格一致。 属性的封装性不仅封装了状态的实现过程也就是表现行为,而且还封装了状态的制约

条件,这样可以使对象和属性更加独立,没有任何的附加条件。对于外界而言,对一个对

象的操作,仅需要知道我要求作什么操作,而不必关心操作之前对象是否已经具备了操作

的条件,这些都是由对象自己去完成的。

3.4.3 约束性

约束性来源于状态之间的约束,前面也已经讲过,状态是描述的事物的动态特征,因

此状态之间的制约关系是不可避免的。这种状态之间的约束关系在面向状态的程序设计中

必须按照一定的方法和规则来体现出来,这就是面向状态的约束性。 约束性是面向状态中必须遵循的的一个规律,否则对于状态的抽象将完全失去意义。

一般来说,属性的设置函数应该包括两个部分的内容,一是对于属性描述的状态的核心操

作,另一个是对于该状态的约束条件的检查或设置。约束性以及状态之间的约束关系是靠

属性的封装性来保证的,下面看几个 VCL中典型的属性之间的约束关系: TDataSet的 Active属性,由于 Active属性受到数据库 Active 等其他条件的约

束,因此在这个设置函数中,必须对其约束条件进行判断,或者需要对约束条件进行设置。

我们在这里不研究 TDataSet的 Active设置函数是如何实现的,因为关于 Active的

定义和内涵涉及到 DataSet 的很多方面,关系也非常复杂,仅就其表现特征进行分析。

当一个 DataSet 的数据库(DataBase属性)被指定为一个 TDataBase组件时,那么

TDataSet的 Active属性将受到 TDataBase 的 Active属性的约束,用户需要打开一

个 DataSet的时侯,并不需要关心当前的 DataBase是否已经打开,只要执行: TDataSet->Active = true; 就可以,TDataBase->Active 的检查和打开均由 TDataSet->Active 的设置函

数来完成,相反的如果用户关闭一个数据库: TDataBase ->Active = false; 这时候必须先关闭 DataSet,那么 TDataSet->Active = false;则是由

TDataBase ->Active的设置函数进行检查并完成关闭工作。 在这个例子中可以看出,不同的属性所表示的状态,基本都是各司其职,属性的设置

函数所要关心的是我在做某件事之前需要具备什么样的条件,而这个条件是否具备是别人

第三章 面向状态的程序设计思想 第 137 页

的事情。这就是状态之间的约束关系。在这个例子中约束关系是双向的,或者说是约束和

反约束关系,因为 TDataBase 比 TDataSet 要高一个层次,先有 DataBase然后才能

有 DataSet,而且一个 TDataBase可能要约束多个 TDataSet。 对于 TDataSet而言可能还有其他的状态,比如编辑状态(IsEdit),是受到 Active

约束的,只有 Table 打开的情况下才可以处于编辑状态,这几个状态之间的约束关系可

以用下面的图 3.11来表示:

图中圆矩形表示为一个 bool类型的属性,它有两种状态 true和 false,箭头表示

约束关系,实心箭头表示正逻辑约束,即起点为真是终点为真的必要条件,空心箭头表示

反逻辑关系,即起点为假是终点为假的必要条件。建立这样一条约束关系链,并根据我们

前面所说的约束性的处理原则,不管 DataSet1 还是 DataSet2 处于编辑状态,当

DataBase需要关闭的时候,完全不需要考虑,只需要保证 DataSet1 和 DataSet2的

Active都为 false,也就是首先关闭 DataSet,在 DataSet1和 DataSet2 关闭过程

中,约束性会自动确保这两个 DateSet不处于编辑状态,然后再关闭。 这个例子中的约束关系是具有互动性的,是允许约束双方相互改变对方的状态,从其

执行的权限上没有高低之分,属于一种平等的关系。在另外一些的约束关系上约束双方不

一定的平等的,例如: 在大多数可视化的图形组件中都包含一个“排列”属性Align,其数据类型是 TAlign,

一个枚举类型的数据,包含 alNone、alTop、alBottom、alLeft、alRight、alClient等取值,从 TControl派生的组件都缺省的包含该属性,但在某些组件中,这个属性是无

效的。对于一个包含 Align属性的组件,其很多属性都将受其约束,比如 Top、Left、Height、Width,甚至是停靠(Dock)属性等都要受其约束,但是并不允许改变 Align属性。用图表示它们之间的约束关系如图 3.12所示:

图中六边形表示Align属性有六个独立的状态取值,既alNone、alTop、alBottom、alLeft、alRight、alClient等,由于 alNone 实际上对 Top、left、Width、Height等都没有约束影响,故未画出关系图,圆形表示该属性具有连续的状态取值,比如整型变

量、浮点型变量、双精度等。其中双箭头表示连线的起点为终点的必要条件,并且终点对

TDataBase

Active

Active

IsEdit

Active

IsEdit

TDataSet TDataSet

DataBase

DataSet1 DataSet2

状态层次

状态层次

状态层次

图3.11 状态的约束关系及层次关系

第三章 面向状态的程序设计思想 第 138 页

起点存在正向约束关系,不允许起点改变终点的状态。

在上图中,Align 对 Top、Left、Width、Height 的约束为绝对约束的,因为在

TControl的属性中,Align不仅比 Top、Left、Width、Height层次更高,而且执

行级别也要高,程序允许 Align的状态改变时,相应的改变 Top、Left、Width、Height的取值,但是不允许它们改变 Align的状态。上图还显示出 Align 是 Top、Left、Width、Height 的充分条件,当 Align = alTop 时,Top = 0、Left = 0 和 Width = Parent.ClientWidth 是不允许改变的,但并不是必要条件,Align != alTop 时,

Top也可以为 0; Align为 aBottom、alLeft、alRight、alClient时对 Top、Left、Width、

Height的约束关系分别如图 3.13、3.14、3.15:

图中显示 Align = alBottom是 Top = Parent.ClientHeight - Height、

Top Left Height Width

alNone

alTop

alBottom alLeft

alRight

alClient

Top = 0

Left = 0

Width = Parent.ClientWidth

Align

图:3.12 alTop 的约束关系

Top Left Height Width

alNone

alTop

alBottom alLeft

alRight

alClient

Top = Parent.ClientHeight- Height

Left = 0

Width = Parent.ClientWidth

Align

图:3.13 alBottom的约束关系

第三章 面向状态的程序设计思想 第 139 页

Left = 0、Width = Parent.ClientWidth 的充分条件,反向为必要条件,Align不允许被 Top、Left、Width、Height改变,反之可以。

图中的 Height和 Align = alBottom无关,即无直接约束关系。

Align = alLeft 对 Top、Left、Width、Height 的约束关系。其中 Width 和

alLeft无关。Top、Left、Height受其约束。 与 Align = alLeft相似,Left的取值为 Parent.ClientWidth – Width。

Top Left Height Width

alNone

alTop

alBottom alLeft

alRight

alClient

Top = 0

Left = 0 Height = Parent.ClientHeight

Align

图:3.14 alLeft的约束关系

Top Left Height Width

alNone

alTop

alBottom alLeft

alRight

alClient

Top = 0

Left =Parent.ClientWidth - Width

Align

Height = Parent.ClientHeight

图:3.15 alRight 的约束关系

第三章 面向状态的程序设计思想 第 140 页

当 Align = alClient 时,Top、Left、Width、Height 等都受其约束,并满足

Top = 0 、 Left = 0 、 Width = Parent.ClientWidth 、 Height = Parent.ClientHeight。

在上面的几个图中,均没有考虑 Parent的 Client 区域中包含其它可视组件,或者

TSplitter组件的情况下。实际中的约束关系要比上图复杂一些,包含了 Parent的其

它组件以及这些组件创建的先后顺序等因素。 关于约束关系还有很多种,而且不适合进行完全的归纳,最重要的是理解这种约束性

的存在,并且能够处理各种约束关系。即使同一种约束关系在编写代码的时侯也可以采用

不同的方法来处理,只要其逻辑关系没有错,程序就没有漏洞。 实际上 VCL中处理 TControl的 Align属性和 Top、Left、Width、Height的

约束关系时主要采用了两个函数的调用: SetBounds 和 RequestAlign,关于这两个函数的详细内容读者可以参考 C++

Builder根目录下的\Source\Vcl\Controls.pas文件,该单元包含了 Align属性

的原始定义和处理代码。 后面第四节的内容是专门针对 C++ Builder 中 VCL 类库的属性之间存在的制约关

系而进行的总结,实际上就是对象的状态之间的约束关系。当然,VCL 中的制约关系并不

代表了实际中全部的约束关系类型,第四节的总结也并不一定正确,但通过这些内容,希

望读者能够很好的理解状态之间的约束关系的及其处理方法。

3.4.4 完整性

完整性是实现约束性的一个基本要求。 完整性简单来说是指:任何一个程序过程,其约束关系,消息的传递必须是一个闭环

的,不允许出现有头无尾的现象。这并不是指在代码的编写上必须完全的前后对应,而是

Top Left Height Width

alNone

alTop

alBottomalLeft

alRight

alClient

Top = 0

Left = 0

Width = Parent.ClientWidth

Align

Height =Parent.ClientHeight

图:3.3.6 alClient的约束关系Top Left Height Width

alNone

alTop

alBottomalLeft

alRight

alClient

Top = 0

Left = 0

Width = Parent.ClientWidth

Align

Height =Parent.ClientHeight

图:3.16 alClient的约束关系

第三章 面向状态的程序设计思想 第 141 页

指在程序设计的时候,必须完整的考虑这种约束关系。 从一方面理解,完整性体现在状态的约束关系存在时,其反约束关系必然也存在,在

上面的例子中,DataBase的 Active 属性和 Table的 Active属性就是这种关系,当

Table设置为 Active 时要求其约束条件 DataBase的 Active属性必须成立,反之,

当 DataBase转变状态为非激活状态时,要求 Table的 Active 属性也必须为 false,这就是约束和反约束的关系。在另一个例子中,Top、Left、Width、Height 并不能改

变 Align的状态,代码中会采用不同的处理方式,读者可以参考 Borland 的源代码自行

分析,这正是在确保完整性前提下,而采用了更加合理的处理方式,使得最终程序的运行

更加符合实际的逻辑关系,这是完整性的另一种表现方式。 从另一方面理解,完整性体现在消息传递的闭合性。也就是说程序在发出一条执行指

令的时候,必须是可掌握这个执行过程的有效性,最终是否达到了指令执行的要求,必须

将消息反送回去。在 C++ Builder中默认的情况总是认为执行是有效地,这个消息也并

不需要明显的返回,当程序没有成功的执行,或者某些操作失败的时候,才将消息返回,

这就像现实中,我们经常说:“你去干什么事吧,有问题了给我打个电话,没问题就不用

打了”。在 C++ Builder 中这种类似的处理机制就是“抛出例外”,或者叫“异常处理”,

是依靠 try⋯catch等语句组合来完成的。 这一点也是 C++ Builder中一个显著的特点,在 Windows 的 SDK中,大部分 API

函数的返回值,包含着该函数执行情况的信息,程序必须一步一步的逐条判断执行是否成

功,然后决定下一步的执行过程,比如在 Windows 系统中为程序分配内存,可能使用的

代码如下: LPVOID lp; //void类型指针

HGLOBAL hp; //全局对象句柄

hp = GlobalAlloc(GMEM_MOVEABLE,1024);

if(hp)

lp = GlobalLock(hp); else

{

//显示内存分配失败;

return;

} if(lp)

//⋯⋯

else

{ //显示内存锁定失败

GlobalFree(hp); return;

}

第三章 面向状态的程序设计思想 第 142 页

这样以来代码会变得冗长而繁琐,而且对于程序而言,并不一定是每个执行过程都需

要清楚知道其执行结果,比如上面的内存分配,很可能程序只关心最后是否分配成功,而

并不关心是由于什么原因而导致的内存分配失败,显然上面的代码显得过于细致。若采用

C++ Builder中的 VCL进行内存的分配,可能会采用如下的代码: try

{

TStream* lp = new TStream();

lp->Size = 1024;

}

catch(...)

{

//分配失败的处理代码

}

其中 catch(...)中的“...”可以是很多中类型,这取决于程序的需要,“...”

代表了所有的异常都会被捕获,即无论是什么原因产生的“例外”,都将由 catch语句中

的代码进行处理,当然也可以指定例外。这种处理方法显然比采用返函数回值判断的方式

更加先进,更加有效,和面向状态的思想最为相符,程序在进行一项操作之前只关心需要

什么样的条件,这通过给相应的属性赋值即可,至于该条件是如何实现的,并不是本次操

作的需要关心的,要关心的只是该条件是否达到要求,如果没有则按照例外情况来处理。 完整性是体现在设计过程中的,对于设计过程的完整考虑,则允许在实现的代码中出

现不完整的情况,这是一个辩证的关系,如何理解这个关系,我们举个例子说明: 在我们前面 Player的例子中,定义了一个 enum类型: enum RecState{_STOP = 1,_PLAY,_PREV,_NEXT,_REC};

在这里 enum并不是从 0开始的,而是从 1开始,0是空缺的,而在 RecState类型

的属性中并没有对 0 值的处理。程序在任何状况下启动时,初始状态均不会是

_PLAY,_PREV,_NEXT,_REC 其中之一,确切的讲也不会是_STOP,因为_STOP 是在前

几种状态下按停止键后的状态,而程序在初始化时总是将成员缺省的初始化为“0”,因此

在 TForm1的构造函数中我们不需要显式的为 FWorkState 赋值为“0”,这是和实际情

况相符合的。相反的假如 RecState采用了以下的定义: enum RecState{ _PLAY,_PREV,_NEXT,_REC,_STOP,_NONE };

其中_NONE代表了空闲状态,那么从完整性的角度考虑,在 TForm1的构造函数中必

须为 WorkState进行初始化:

第三章 面向状态的程序设计思想 第 143 页

FWorkState = _NONE;

否则程序将会出现非常大的漏洞。从这里可以看出,面向状态中完整性要求会使代码

的逻辑性错误尽可能的少,而且可以设计阶段将状态之间逻辑关系进行约简,最终在保证

无逻辑错误的情况下尽量的简化代码,提高程序的执行效率。

3.5 如何面向状态

前面已经介绍了面向状态的基本概念以及面向状态的基本特征,通过对这些基本概念

和特征的学习,相信读者对面向状态已经有了一个初步的了解,那么在程序设计中如何来

实现面向状态呢?除了遵循面向对象的设计方法之外,面向状态在分析和设计阶段和面向

对象有所不同。 分析阶段 在面向状态的设计方法中,分析阶段和面向对象有所不同,主要体现在:面向状态的

分析方法中,不仅要考虑对象或者类的成员的静态依存关系,还必须分析对象的运动过程

中的运行状态之间的动态约束关系,这是比面向对象更加高级、更加抽象的一种数据模型。 在面向状态的分析过程中,通常是先有静态成员的抽象,然后在根据静态成员的运动

特点抽象出描述动态特征的成员,这就是属性。也有一种方式是先根据对象或者根据类要

实现的功能,抽象出必要的运行状态,然后根据状态建立属性,最后根据属性的需求在完

善整个对象或者类的静态数据成员。这是两种不同的分析方法,但是最终达到的目的是相

同的,一个对象或者类的静态特征和动态特征是一致的、统一的,分析过程中,可以根据

不同的需要相互调整。 设计阶段 面向状态和面向对象最大的区别就是:采用状态之间的约束关系来处理对象之间复杂

的逻辑关系。因此,面向状态的设计过程必须花费很大的精力来处理属性之间的相互制约

关系,当属性之间的相互制约关系确定时,实际上整个程序的结构就已经非常清晰。 在面向对象的设计中,编码是在演化阶段进行的,但在面向状态的设计中,这两个阶

段的界限竟不是非常的清晰了,面向状态通常可以将代码分作两大类型,一类是描述各种

属性之间相互约束关系的代码,可以将其称为框架代码,框架代码描述的一些关系应该是

相对固定的,可以在设计阶段进行编码,比如在上面的 Player的例子中,我们给出的代

码实际上就是一个框架。另一类是核心操作代码,在一个程序中,核心操作不必要在关心

操作会对其他属性产生什么样的影响,不必要再去关心约束条件,在一个完整的系统中,

约束条件都是由框架代码处理的,比如 Player例子中没有给出来的播放、录音、快进、

快退等操作的核心执行代码,但却能够表达整个程序的逻辑过程。 在面向对象的设计中,设计阶段将会设计出对象和类的原型,包括数据结构和操作方

法,而在面向状态的设计中,设计阶段不仅设计出对象和类的原型,还要将各个有关连的

对象之间的固定关系,也就是它们之间的动态特征也设计出来。而到了演化阶段,面向状

态的设计相对就要简单、容易得多。 总而言之,面向状态是在面向对象的基础上发展而来的,其设计基础仍然遵循面向对

象的特征,但面向状态也有自己的特征,设计过程不一定按照一个固定的模式或者方法进

第三章 面向状态的程序设计思想 第 144 页

行,最终的目的是为了提高程序的性能和方便设计。面向状态是一个新生的设计思想,在

许多设计模式和方法上还有待于进一步完善,这里所讲的也仅仅是给出一个参考。读者可

以在实际中完善或提出更加适合设计过程。

4 属性之间的关系

在 VCL中状态之间的约束关系,可以通过属性的关系中体现出来,而且状态的约束关

系最终必须通过属性来体现,前面我们讲述了面向状态的约束性,这一节将针对 VCL类库

中属性的关系进行一些简单的归纳和分类,帮助读者理解状态之间的约束关系。

4.1 同一对象中属性之间的关系

对象是一个相对独立的个体,能够完整的描述某一件事物,状态也必须是对象的状态,

因此在属性的关系中,同一对象中属性的关系也是最为突出和重要的。 在 C++ Builder 中对象的属性也可以是另一个对象,比如大多数 VCL 类库中都包

含 TFont对象,从 TControl派生的类都有一个 Font属性,其类型为 TFont,TFont类中又会有其他的属性,它们的关系可以用图 3.17表示:

在这种关系里面,TFont->Name 和 TControl->Caption 是属于不同的类,但是

从 TControl的层次上分析,Font 本身就是 TControl的一个属性,因此 Font的属性

TFont->Name 自然而然也应该是 TControl 的一个属性。而进一步详细划分从

TControl的属性这一层次分析时,它们仍然隶属不同的类的实例——对象。从这里看出

严格区分 TFont->Name和 TControl->Caption是否属于同一个对象并没有多达的实

际意义。我们作如下的约定: 在 C++ Builder 中已经定义的类,象 TControl 这样的类,其属性的属性和自身

的属性,我们都认为是属于同一个对象的属性。 C++ Builder 中不能从个原始定义一次性形成的关系,比如 TToolBar 中的

Button,是需要设计时手工设置或者添加的,将 TToolBar->Button 的属性和

TToolBar的属性这类关系,认为是不同对象之间的属性。 根据这样的一个约定,我们来看一下同一个对象中属性之间的关系。

4.1.1 并列关系

这是对象的属性中存在最多的一种相互关系,并列关系指的是属性之间没有特定的制

约关系,它们的存在不会相互影响。

图 3.17 TControl 和 TFont的关系

+...()

+Caption : AnsiString+Font : TFont+...

TControl

+...()

+Height : int-Name : AnsiString-...

TFont<<uses>>

第三章 面向状态的程序设计思想 第 145 页

比如在 TControl中,属性 Font、Height、Color、Name、Cption等,之间的

关系均为并列关系,甚至包括 Font 的属性 Name、Color、Height 等都和它们是并列

的,相互不会影响的,从制约关系上讲它们都是并列的,但从层次上讲 TControl的 Font、Height等属于同一个层次,而 Font的 Name和 TControl的 Height属于不同的层次。

4.1.2 排斥关系

处于排斥关系的属性往往是反映的一个状态的两个或多个不同侧面,排斥关系的属性

在同一时刻只能有一个是有效地,而其他的需要忽略而且自动清空。 比如 TDataBase的 DriverName 和 AliasName,这两个属性从不同的侧面最终描

述的都是数据库驱动的信息,当指定了 DriverName时,AliasName 会自动清空,并依

照 Params属性定义的相关参数数值数据库驱动,当指定了 AliasName时,DriverName会自动清空,TDataBase 会根据系统中的别名对数据库的驱动进行配置,并且忽略了

Params属性关于 DSN、ServerName 等相关的信息。关于 TDataBase 中 DriverName以及 AliasName的详细说明请参阅 C++ Builder的联机帮助。

另一个排斥关系的例子是 TTable中的 IndexName和 IndexFieldNames,这两个

属性描述的都是 TTable的关于索引的内容,但同时只能是一个有效。当 IndexName 指

定时,IndexFieldNames 将会自动清空,反之 IndexFieldNames 被指定时,

IndexName会自动清空。 我 们 看 一 下 在 C++ Builder 中 如 何 实 现 TTable 的 IndexName 和

IndexFieldNames 的相互排斥关系,VCL 中关于 TTable的 Object Pascal的部分

源代码如下: function TTable.GetIndexFieldNames: string;

begin

if FFieldsIndex then Result := FIndexName else Result := '';

end;

function TTable.GetIndexName: string;

begin

if FFieldsIndex then Result := '' else Result := FIndexName;

end;

//IndexName和 IndexFieldNames的读取函数是呈现相反的特型,属性的表现特征为相互排斥的;

procedure TTable.SetIndexName(const Value: string);

begin

SetIndex(Value, False);

end;

第三章 面向状态的程序设计思想 第 146 页

procedure TTable.SetIndexFieldNames(const Value: string);

begin

SetIndex(Value, Value <> '');

end;

///IndexName和 IndexFieldNames的设置函数殊道同归,最终调用 SetIndex实现

procedure TTable.SetIndex(const Value: string; FieldsIndex: Boolean);

var

IndexName, IndexTag: string;

begin

//⋯⋯

end;

不管 VCL中是如何实现这种关系,但从最终的表现形式上,也就是最终为用户提供的

对外接口来看,它们的排斥关系是显而易见的。TTable 中这两个属性实际上是 TTable关于索引的两个不同的表现形式。

4.1.3 相互制约

相互制约关系是指属性之间可以相互影响,从层次上讲,处于相互制约的属性可以在

同一层次,也可以处于不同的层次。相互制约关系正好体现的是约束与反约束的关系,在

VCL 中 , 象 TForm 的 Color 和 ParentColor 以 及 控 件 的 ShowHint 和

ParentShowHint 都属于这种类型,在程序设计时自定义的属性,大多数会处于这种关

系。 以 TPanel 的 ShowHint 和 ParentShowHint 为 例 , ShowHint 和

ParentShowHint是从 TControl继承而来的 bool类型的属性。当 ParentShowHint设置为 true的时侯,ShowHint 必须和父窗口的 ShowHint一致,ParentShowHint设置为 false 时,ShowHint 不会发生改变,而当 ShowHint 改变并且与父窗口的

ShowHint不一致时,则 ParentShowHint 则必须为 false,我们来看一下它们的源代

码: procedure TControl.SetParentShowHint(Value: Boolean);

begin

if FParentShowHint <> Value then

begin

FParentShowHint := Value;

if FParent <> nil then Perform(CM_PARENTSHOWHINTCHANGED, 0, 0);

end;

end;

function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;

第三章 面向状态的程序设计思想 第 147 页

var

Message: TMessage;

begin

Message.Msg := Msg;

Message.WParam := WParam;

Message.LParam := LParam;

Message.Result := 0;

if Self <> nil then WindowProc(Message);

Result := Message.Result;

end;

procedure TControl.CMParentShowHintChanged(var Message: TMessage);

begin

if FParentShowHint then

begin

SetShowHint(FParent.FShowHint); FParentShowHint := True;

end;

end;

在这里,对 ParentShowHint的设置使用了 TControl的另一个方法 Perform,这个函数允许绕过 Windows 消息队列使控件可以直接响应指定的消息内容,而最终是调

用 WindowProc 对消息进行处理,“CM_PARENTSHOWHINTCHANGED”是 VCL中用户定

义的消息,当一个窗口 ShowHint改变时,会向其子窗口广播该消息。采用这种方式的目

的使得 ParentShowHint 为 true时,ShowHint和 Parent->ShowHint 也建立约束

关系——Parent 的 ShowHint 改变时,自身的 ShowHint 也会随之改变。在控件的

WindowProc 函数中,对“CM_PARENTSHOWHINTCHANGED”消息的处理通过调用

CMParentShowHintChanged 函数来完成的,最终也是调用了 ShowHint 的设置函数

SetShowHint。 在这个例子中 ShowHint和 ParentShowHint 这是一种不完整的制约关系,即:不

是所有的条件下都相互约束,在实际中可能存在一种关系,就是两个属性的不同状态下都

是相互约束的,称作完整约束,但完整约束意味着这两个属性很可能是等价的,因此实际

中这种关系并不多见,往往是为了方便某些应用而将某一个属性在不同的环境重复定义。

4.1.4 单向制约

除了相互制约以外,两个属性之间可能会处于一种单向制约的关系,即:一方对另一

方存在制约,反之则不存在制约。比如 TSpeedButton类的 GroupIndex 和 Down属性,

当 GroupIndex为“0”时,Down属性不允许为“true”,而当 GroupIndex 为任意正

第三章 面向状态的程序设计思想 第 148 页

整数时,Down 属性则允许为“true”,同时 AllowUp 属性将制约着该组所引的所有

SpeedButton的 Down属性是否可以同时弹起来。在这里 GroupIndex 对 Down属性是

一种单向约束,Down 属性对 GroupIndex 属性不具备约束力。TSpeedButton 的

GroupIndex 属性还肩负着另一种职责,这就是在同一个窗体中(或者容器类组件中),

处于同一个 GroupIndex的 SpeedButton的 Down 属性将会是相互排它的,任何时刻

只能有一个 SpeedButton的 Down属性为“true”。我们来看一下 TSpeedButton的

部分源代码: procedure TSpeedButton.SetGroupIndex(Value: Integer);

begin

if FGroupIndex <> Value then

begin

FGroupIndex := Value;

UpdateExclusive; //排他性更新调用 UpdateExclusive

end;

end; //在此没有对 Down属性的操作,即 Down对 GroupIndex没有反向约束

procedure TSpeedButton.SetDown(Value: Boolean);

begin //Down属性的设置函数

if FGroupIndex = 0 then Value := False; //GroupIndex为“0”时

//Value始终应该保持为 false

if Value <> FDown then //当 Value和 Down属性不一致时,进行以下操作

begin

if FDown and (not FAllowAllUp) then Exit;

//若该 SpeedButton已经被按下,并且该索引组不允许全部弹起时返回,反之继续;

FDown := Value; if Value then //设置后的 Down属性为“true”时

begin

if FState = bsUp then Invalidate; //原来状态为 Up,则重新绘制;

FState := bsExclusive //记录新的状态;State是另外一个记录组件的属性,

//请参阅在线帮助

end

else //若原来状态不为 Up,则设置并重新绘制;

begin

FState := bsUp;

Repaint; end;

if Value then UpdateExclusive; //排他性更新,和其他按钮的排它关系

end;

第三章 面向状态的程序设计思想 第 149 页

end;

procedure TSpeedButton.UpdateExclusive;

var

Msg: TMessage;

begin //非“0”索引组,并有父窗体时,传递一个用户自定义消息给父窗体进行广播;

if (FGroupIndex <> 0) and (Parent <> nil) then

begin

Msg.Msg := CM_BUTTONPRESSED;

Msg.WParam := FGroupIndex;

Msg.LParam := Longint(Self);

Msg.Result := 0;

Parent.Broadcast(Msg); //消息广播,用于其他子控件的更新。

end;

end;

这里使用了 TWinControl 的一个方法 Broadcast,它可以不经过 Windows 的消

息队列而发送相同的消息给每一个由 Parent属性关联起来的子控件,关于 Parent属性

以及建立的通过其建立的对象之间的关系,参见后面内容。

4.1.5 间接制约

间接制约关系是指两个或者多个属性之间不是直接的建立制约关系,而是通过其他属

性而建立的制约关系。 这种关系在 C++ Builder的 VCL 中也是非常多的,而且处理起来也相对复杂一些,

并没有一个特定的方式。比如上面例子中的 TSpeedButton 的 Down 属性,当

GroupIndex 设置为正整数时,同一个窗体中索引相同的 SpeedButton 的 Down属性之

间就会建立相互排它的制约关系,在上面的代码中我们也看到是通过窗体的消息广播机制

来完成的。 相类似的例子比如:TImage 的 AutoSize、Height、Width 和 TPicture 的

Height、Width等,TImage 的 AutoSize、Height、Width属性是通过继承 TControl而获得的。TPicture 为 TImage 的一个成员,当 AutoSize 为“true”时,TImage的 Height和 Width属性将和 TPicture的 Height、Width相同,而且不允许改动,

只有 TPicture 的 Height、Width 改动时,才可以跟随变化。它们的关系可以表示如

图 3.18: 实际上 TImage和 TPicture关系远比图中复杂得多,单就 AutoSize和大小属性

而言可以简单的表示成图中关系,而且 TPicture的 Height 和 Width属性实际上也是

受到它的其他属性成员的约束。关于 TImage的 AutoSize、Height、Width 属性等属

性的处理方法涉及到了 TControl的相关属性,在 TControl中它们是其派生类的共同

第三章 面向状态的程序设计思想 第 150 页

属性,因此在处理方式上需要充分考虑它们的共性,并且必须考虑派生类如何继承,如何

重载相关成员函数而获得最佳的处理途径,因此比较复杂,这里不列出来分析,读者有兴

趣可以参考 controls.pas和 extctrls.pas两个源代码文件。

间接制约关系可以是单向制约也可以是双向制约,这与直接的制约关系是比较类似

的,这里不再赘述,读者可以参考上面内容理解。 在 VCL中间接制约关系非常重要,也是容易出现错误的地方,编写控件的时侯,这是

非常需要注意的,但是在编写应用程序的时候,这种关系可能要少一些,即使有也通常是

不同对象之间属性的间接制约。 按照我们上面的约定:VCL中已经定义的类,其属性的属性和自身的属性,属于同一

个对象的属性;而不能从 VCL的原始定义一次性形成的关系,需要设计时手工设置或者添

加的,属于不同对象之间的属性。TImage和 TPicture 之间的间接制约是同一个对象的

属性之间的间接制约,而 SpeedButton 的 Down属性之间的制约是属于不同对象的属性

之间的间接制约。对于不同对象的属性之间的关系,必须结合对象之间的关系进行分析,

下面的内容将针对 VCL中对象之间的关系进行一些简单归纳。

4.2 对象之间的关系

属性是对象的一个重要元素,属性的类型可以是任意的数据类型,也包括 VCL中的类,

从根本上讲对象不可能是一个个孤立的,对象之间必然存在着诸多的联系。根据关联的方

式不同,对象之间的关系也可以划分为不同的类型,不同类型的关系在信息传递以及属性

的制约上也有所不同。大体上分,对象之间的关系可以分为并列关系、包含关系和逻辑包

含关系。逻辑包含本身也是包含关系的一种,但是在 C++ Builder中这种逻辑包含的关

系有着其特殊的意义和作用,VCL中也采用了几种不同的方式来建立逻辑包含关系,因此

这里将逻辑包含分开来讲解。

TImage

AutoSize

TPicture

Image

状态层次

状态层次

Width Height

Width Height

图:3.18 TImage和TPicture的制约关系

第三章 面向状态的程序设计思想 第 151 页

4.2.1 并列关系

对象的并列关系是指对象完全处于一种等同的位置关系,没有层次的高低之分,也没

有其他的逻辑层次关系。 在程序中,可能会有多各 Form对象,那么这些 Form 之间就是一种并列关系,不存

在谁凌驾于谁之上。在同一个 Form中也会有很多的对象属于并列关系,比如 MainMenu和 PopupMenu。

另外在 VCL类库中,也会存在这样的并列关系,比如 TPictrue组件包含了 Bitmap、Icon、Metafile 等三个不同的属性,这三个属性都是图形类的元素,而且都是由

TGraphic派生而来,它们在 TPictrue组件中属于并列关系。 并列关系并不代表着没有相互影响,而且通常并列关系的同一种对象很可能通过其他

属性或者上一层对象而建立了某种制约关系,比如 TSpeedButton 就可以通过

GroupIndex而建立相互制约的关系。

4.2.2 包含关系

包含关系实际上就是一种隶属关系,即一个对象作为另一个对象的成员出现,前面提

到的 TControl和 TControl->Font 就是这样的一种关系,像这样的关系在 VCL类库

中非常多,TPictrue组件中的 Bitmap、Icon、Metafile之间相互为并列关系,但它

们和 TPictrue之间的关系就为包含与被包含关系。 包含关系有两种不同的表现形式,一种物理包含,即一个对象直接作为另一个对象的

成员出现;另一种形式是引用包含,就是一个对象以指针成员出现,或者说一个对象的引

用作为另一个对象的成员出现。 在 C++ Builder的 VCL中,大多数包含关系都是引用包含,比如 TControl ->Font

的定义形式为: __property TFont * Font; TPictrue组件中的 Bitmap、Icon、Metafile属性和 TPictrue也是引用包含

关系,它们的定义形式为: __property TBitmap* Bitmap;

__property TIcon* Icon;

__property TMetafile* Metafile;

这种包含关系是 VCL固有的,正如前面所说的,我们将这些属性和其属性的属性看作

是一个对象的属性,但是从对象的内部来分析,这种包含关系仍然是存在的。 在 C++ Builder 中也有一些包含关系是属于物理包含,但相对要少得多,像

AnsiString 类型的属性,几乎在 VCL中都是直接出现在对象中,比如 Caption 属性、

Text属性等都是 AnsiString 类型的属性。还有一些像枚举类型的属性、TColor 属性

等都是直接出现在对象中的,是属于一种物理包含。 严格的讲,引用包含实际上是通过指针或者引用而建立的一种逻辑上的包含,应该属

第三章 面向状态的程序设计思想 第 152 页

于下面所说的逻辑包含关系,但是在上面的例子中,这种包含关系是在 VCL内部建立的,

属于 VCL固有的,通常并不允许改变这种关系,因此我们并不将其归属于逻辑包含。 从设计者的角度来考虑,一个 VCL的对象一旦建立,它所包含的对象也会相应的建立,

或者在需要的时侯会自动建立,设计者完全不需要考虑在代码中或设计时创建这种包含关

系,而对于设计者需要在设计时建立的或者在程序中需要通过代码实现的包含关系,我们

成作为伪包含或者叫做逻辑包含关系。

4.2.3 逻辑包含关系

逻辑包含关系是指通过某种联系使得多各对象在形式上,存在隶属关系,使一个对象

在形式上类似于另一个对象的成员而出现。逻辑包含也可以称作伪包含关系,这是因为逻

辑包含不能够反映出真实的对象真实的层次关系,到更像是为了方便使用而定义的别名,

或者是实现特定功能而建立的约束关系。从另一方面来讲,逻辑包含关系在为组件建立约

束关系的同时,也为满足完整性提供了可能。 逻辑包含是具有客观现实意义的,就像现实中某个人具有双重或多重身份一样,在不

同的条件下,处于逻辑包含的对象会有不同的表现形式和含义。通常逻辑包含关系是程序

设计者在设计时建立的,或者是在程序运行的过程中通过代码实现的。但并不是所有在设

计时或者通过代码建立的都属于逻辑包含,比如 TImage的 Picture 是一个 TPicture类型的属性,通常需要设计时指定内容或者执行时载入图片等,但这是一种 VCL固有的包

含关系,当需要的时侯,TImage会自行创建相应对象并且正确设置。 最常见的逻辑包含关系是通过对象指针来建立的,比如所有 TControl派生的类都包

含一个 PopupMenu属性,PopupMenu允许为空,也可以为其指定任意一个 TPopupMenu对象,甚至是其它 Form 中的 PopupMenu,而且允许程序运行过程中任意的改动,这种

关系一旦建立,就会实现其特定的功能。 像这一类的包含关系非常的多,TForm 中的 Menu,TTable 中的 DataBase,

TListView中的 SmallImages、StateImage 等,都属于这种方式建立的包含关系。

VCL中对象指针的属性通常在 Object Inspector 中可以使用下拉框选取,当没有相匹

配的对象时,下拉框的选项为空,使用指针建立的包含对象时必须对其有效性进行判断。 在 C++ Builder的 VCL中,有两个非常重要的逻辑包含关系,一个是由 TComponent

继承而来的 Components 属性和 Owner 属性,另一个是从 TWinControl 继承来的

Controls 属性和由 TControl 继承而来的 Parent 属性。这两组包含关系建立的方式

是非常相似的,Components 和 Controls都是数组类型,Components数组的元素必

须是从 TComponent继承而来的对象,Controls数组的元素必须是从 TControl 继承

而来的对象,它们分别和包含的控件中的 Owner 属性和 Parent 属性相对应。当一个

TComponent 或其派生类的对象指定了 Owner 以后,它的 Owner组件的 Components属性会自动增加一个元素,并且该元素指向这个 TComponent 或其派生类的对象。

Controls和 Parent和这相似,所不同的是 Owner和 Components是一种“拥有关系”,

是决定对象的生存关系的属性,Owner属性一般在一个组件的构造函数中指定,而且不允

许改变,是一个只读属性,当一个组件被销毁时,它所拥有的所有组件(Components数

第三章 面向状态的程序设计思想 第 153 页

组列表中的组件),将会被一并销毁。而 Parent 是一个可读写的属性,是用于可视化组

件显示关系的属性,能被指定为 Parent的组件必须是容器类的对象,从 TWinControl派生而来,当一个控件指定了 Parent 属性以后,它的 Parent对象的 Controls属性

中会自动增加一个元素,并且该元素指向这个控件,当 Parent对象移动时,它的子控件

位置会跟随移动,Parent 隐藏时,子控件也会跟随隐藏,一个可视化控件没有指定

Parent时,是不能在屏幕上显示的。 与这种关系类似的状况在 VCL还有很多,比如 TTable的 Fields 属性和由 TField

派生的各种数据库字段组件,TPageControl 的 Pages属性和 TTabSheet 对象,也都

是这一种类型的逻辑包含关系。 在 C++ Builder 的 VCL 中,有两个从 TPersistent 派生的 VCL 分支,一个是

TCollectionItem,另一个是 TCollection,它们之间可以建立一种一对多的逻辑包

含关系,最典型的例子是TStatusBar的 Panels属性和TStatusPanels对象,Panels为 TStatusPanels 类型的指针,是由 TCollection 派生而来,TStatusPanels 是

由 TCollectionItem 派生而来。细心的读者可能都会发现,通过 TCollectionItem和 TCollection 的派生类们建立的包含关系,其被包含的对象都是在一个特殊的属性编

辑器中生成的,而且在单元文件中没有其声明的原型,它的创建是程序根据资源文件自动

进行的,因此程序中不能对其进行直接引用,只能通过包含它的对象而间接引用,比如

TStatusBar的 Panels只能采用: StatusBar1->Panels->Items[0] ->Text = "Test";

的形式进行访问,而不像 TPageControl中的 Pages 属性和 TTabSheet对象,可

以采用下面形式访问: PageControl1->Pages[0]->Caption = "Test";

或者:

TabSheet1->Caption = "Test";

的形式进行访问。 类似这种关系的像 DBGrid的 Columns属性和 TDBGridColumns对象,TCoolBar

的 Bands属性和 TCoolBand 对象,TDataSet的 FieldDefs 属性和 TFieldDef 对象

等等。

4.3 不同对象中的属性之间的关系

按照我们前面的约定:不能从 VCL的原始定义一次性形成的关系,需要设计时手工设

置或者添加的,属于不同对象之间的属性。实际上不同对象之间的属性关系才是程序设计

时侯最为复杂的处理关系,前面所说的同一个对象之间的属性的关系是 VCL本身已经建立

好的,我们只要理解了并使用正确就行了,但在程序设计时候创建的属性,或者建立不同

对象之间的关系,都属于不同对象之间属性的关系。

第三章 面向状态的程序设计思想 第 154 页

和同一个和对象之间属性的关系类似,不同对象之间属性的关系也可以分为:排斥关

系、并列关系、相互制约、单向制约和间接制约等不同的类型,我们简单作一介绍,读者

可以参考同一个对象属性间的关系进行理解。

4.3.1 排斥关系

这种情况应该是比较少见的,排斥关系在这里也是意味着两个属性是描述着同一事物

的不同侧面,因此在同一时刻只能有一个是有效地被使用,尽管很少出现,但这种情况也

可能出现在应用程序设计或者控件设计的过程中。

4.3.2 并列关系

并列关系指的是属性之间相互没有特别明显的制约关系,而是相互并列存在的,这是

程序设计中最常见的一个相互关系。但是并列关系并不是说属性之间不会产生影响,而是

这个影响是由程序的其他代码来完成的,它们之间的影响关系并不是固定的,没有典型的

代表性。

4.3.3 相互制约

具有相互制约关系的属性,之间的关系是相互影响的,这种关系在程序设计中也是经

常出现的,而且在应用程序设计中可能出现的机会更多一些。相互制约关系可以表现为同

一层次之间的状态相互约束,也可以表现为不同层次之间的状态存在的约束和反约束关

系。

4.3.4 单向制约

单向制约关系,表现为一个属性对另一个属性具有制约关系,反过来被制约的属性并

没有对该属性的反向制约关系,在同一个对象中,属性具有这样的关系,在不同的对象中,

属性也可以具有这样的关系。

4.3.5 间接制约

同前面讲过的同一对象之间属性的关系一样,间接关系指的是属性之间没有直接的建

立制约关系,而是通过其他属性间接的建立了相互制约的关系。这种关系和前面讲的同一

对象之间的间接制约关系是完全一样的,不同的是按照是否属于 VCL本身已经包含的属性

关系来区分的。前面说的间接制约从内部分析也是不同对象之间的属性,只是这种关系在

VCL类库中已经建立,我们将其归属于同一对象的属性,而这里说的间接制约关系是需要

在进行程序设计时或者进行控件设计时,由程序员建立的。

5 小结

本章简单的讲述了面向状态的基本思想,并结合实际进行了一些代码的分析,通过将

接和代码的分析,帮助读者理解状态的概念、属性的概念以及在 C++ Builder 中关于消息

第三章 面向状态的程序设计思想 第 155 页

的处理机制等。

本章第四节针对 VCL 类库,对面向状态中的属性之间的关系进行了一些简单的归纳和

总结,其目的是帮助读者深入的了解这种新的程序设计思想,其中部分内容的划分或者归

纳并不一定正确,这只是作者个人的理解。而且这种归纳后的关系也并不能代表了所有属

性之间的关系,但这种划分可以帮助读者加深理解。在实际的程序设计中,不需要仔细的

分析每一个属性和其它属性之间的到底是那种关系,实际中属性的关系可能也比给出的这

些关系复杂得多。

尽管本章对面向状态的特征、思想、以及第四节 VCL 类库的中属性的常见关系进行了

一些讲解,但是建议读者在阅读时,尽可能多的思考并理解,而在实际程序设计的时侯,

不要刻意分析属于那种关系、是否遵循了完整性等,这样会束缚住手脚,正像金庸先生笔

下的张无忌,学完太极拳的时候,也全部忘光了,但实际上是掌握了精髓,而不浮于表面。

Beibei
Note
看完了,洗头洗锅!

第四章 C++ Builder 语言的扩充 第 157 页

第四章 C++ Builder 语言的扩充

上一章讲述了面向状态的基本思想和方法,也针对 C++ Builder语言给出了一些简

单的例子,但是作为一种新的编程思想。必须有特定编译器的支持,面向状态也一样,尽

管在很多开发工具中都已经支持这种方法,但各个编译器差别还是很大的。本书是针对

C++ Builder语言而编写的,因此有必要对 C++ Builder语言有一定的了解,至少应

该对支持面向状态的语言扩充有一个较为深入的了解。 本书并不是 C++ Builder的参考书或使用指南,只是针对 C++ Builder 来讲解面

向状态的设计思想,因此本章对于 C++ Builder中关于面向状态的语言扩充会进行详细

的讲解,而其他内容不作详细说明。需要对 C++ Builder进行深入了解的读者请参考相

关书籍,已经非常熟悉 C++ Builder的读者可以越过本章。 本章第三节针对 VCL的基本类库的结构进行了一个简单的讲解,这将有助于读者更深

入的了解 VCL的处理机制,也会更加深入的理解这种新的技术。

1 关键字扩充

在 C++ Builder中,有许多关键字是对 Ansi C++中关键字的扩展,其功能和作用

和标准的 C++语言类似,也有一些关键字是 Ansi C++中所没有的,而且只在 C++ Builder中有效的,确切的讲是在 VCL类库中才有效的。在 C++ Builder 5中,大概有以下 36个扩充的关键字:

__asm _asm asm __automated __cdecl _cdecl

cdecl __classid __closure __declspec __except __export

_export __fastcall _fastcall __finally __import _import

__inline __int16 __int32 __int64 __int8 __msfastcall

__msreturn __pascal _pascal pascal __property __published

__stdcall _stdcall __thread __try unsigned __int64 __rtti

其中 asm、__rtti 等关键字在 Ansi C++中有相同的型式,作用也相同,但在 Ansi

C++和 C++ Builder 中对最终代码的影响会稍微有所不同;inline、try等在 Ansi C++中有类似的型式,作用也相似;而__published、__property、__msfastcall 等关

键字是 Ansi C++中所没有的,而且只在 C++ Builder 中有效,像__published、__property、__closure 等部分关键字是和本书所讲内容关系比较大的关键字,下面

的内容将对它们作一些详细的介绍,其它关键字只作一些简单的描述,读者可以查阅相关

书籍或者查看 C++ Builder的联机帮助。

第四章 C++ Builder 语言的扩充 第 158 页

1.1 Ansi C++关键字的扩充

在 C++ Builder中,有一些扩充的关键字是在 Ansi C++中本来就有相对应的关键

字,它们的功能和作用基本相同,形状也相似,比如 asm、inline 等关键字,本书中只

作简单描述,不作详细说明。

1.1.1 __asm、_asm、asm 关键字

__asm、_asm、asm 关键字是用来标记汇编代码的关键字,在 Ansi C++中也有相

同的型式,用法和功能也相同,例如: __asm MOV AX 12;

__asm{

ADD EAX EBX;

MOV Mem EAX;

}

通常 C++ Builder在编译的过程中,首先是将 C++代码编译成汇编代码,然后再启

动汇编代码编译器将汇编代码编译成目标代码,这个过程是在内部执行的,而且也不会生

成汇编代码的文件。但若在程序中使用__asm关键字插入汇编代码,C++ Builder 在编

译过程中会显式的分作两个过程来完成——首先启动 C++编译器将代码编译成汇编代码,

并且将生成的汇编代码保存为扩展名是.asm 的同名汇编文件,然后启动汇编语言编译器

将.asm 文件编译成目标代码。生成中间文件有助于程序员分析插入的汇编代码,也可以

帮助程序员分析函数调用规则即调用过程,本书中多处例子编写时作者就是采用插入空汇

编代码来验证调用过程的。

1.1.2 __rtti 关键字

__rtti 关键字是运行时类型判断 RTTI(Runtime type identification)选

项的声明关键字,RTTI在 C++ Builder 中是缺省有效的,可以在 Project 的 Option中使之无效,也可以通过命令行的-RT-选项使之无效,在 RTTI无效时,可以通过代码前

缀__rtti关键字使某个类具有 RTTI特性。 RTTI是类的多态性的一个选项,主要用于运行时使用 typeid关键字获取对象的表

示或类型。当 RTTI选项有效时,一个类的指针或者引用的类型总是实际指向的对象的类

型,而不一定是声明时的类型,而 RTTI选项无效时,一个类的指针或者引用其类型可以

是其声明时的类型,而不是实际指向的对象的类型。比如: class A

{

⋯⋯

第四章 C++ Builder 语言的扩充 第 159 页

}; class B : public A

{

⋯⋯

};

A* pA;

B* pB;

B ClassB;

pA = &ClassB;

pB = &ClassB;

RTTI有效时: typeid(pA) 返回为类型 B,typeid(pB) 返回为类型 B; RTTI无效时: typeid(pA) 返回为类型 A,typeid(pB) 返回为类型 B;

1.1.3 __inline 关键字

__inline关键字在 C++ Builder 中扩充的关键字,在 Ansi C++中有相同作用的

关键字 inline,两者的作用基本上是一样的,使用与定义内联函数,所不同的是,inline关键字只能在 C++的函数中使用,而使用__inline则可以定义 C语言或者 C++语言的内

联函数。

1.2 类声明列表关键字__declspec

__declspec 关键字是 C++ Builder中提高修饰符灵活性的一个关键字,Ansi C++中没有相对应的关键字,通常__declspec 需要和其他修饰性关键字同时使用,其语法如

下: __declspec(<decl-modifier>);

其中 decl-modifier可以为以下几种: dllexport dllimport naked noreturn nothrow

novtable property selectany thread uuid

dllexport是作为动态链接库输出函数的表示关键字,这为 C++ Builder语言和

Microsoft C++以及标准的 Windows 程序提供了良好的兼容性,在 Windows 程序设计

中,动态链接库的输出函数或数据需要在模块控制文件中定义,Microsoft 的 C++提供

了__export关键字,C++ Builder 中也包含了这个关键字,使用__export 将使不必

使用模块控制文件来定义输出函数,在 C++ Builder中也可以通过更加灵活方便的修饰

符__declspec(dllexport)来定义输出函数。

第四章 C++ Builder 语言的扩充 第 160 页

dllimport 修饰符和 dllexport 类似,是用与动态链接库的输入函数和数据的关

键字,__declspec(dllimport)和__import关键字作用一样,但使用上更加方便。 naked 关键字用于定义函数,__declspec(naked)表明被修饰的函数不按照常规

的方式建立和使用堆栈,而且调用函数以及函数返回时不保护任何寄存器的内容,这一点

使所修饰的函数特性类似汇编语言,在后面的内容中,将要讲述 C++ Builder中几种函

数的调用规则,而 __declspec(naked)修饰正是这几种调用规则之外的类型——实际上

是一种没有规则的规则,需要程序员清楚的知道并实现函数调用时参数的传递以及返回值

的传递过程,并且要程序员要负责编写代码实现调用前和调用后对寄存器的保护和恢复。

因而__declspec(naked)关键字通常都和内嵌的汇编语言结合在一起使用,而且多用于

设备驱动等底层的开发。 需要注意的是 __declspec(naked)修饰符不能和任意一种函数修饰符如:

__fastcall、__stdcall等合用,只能单独使用。 noreturn关键字用来通知编译器,该函数调用是不返回的。通常编译器在会变过程

中会分析代码,对某些不能运行到的代码会发出警告或报错,如果这种情况是由于调用的

函数没有返回而造成的,则可以通过声明__declspec(noreturn)函数来避免编译器报

错。 nothrow 关键字用于修饰函数,告诉编译器该函数不会产生例外,使编译器不为其

生成同步的例外跟踪处理代码,从而有效的降低代码长度,提高运行效率。下面三个函数

的效果是一样的: #define WINAPI __declspec(nothrow) __stdcall

void WINAPI foo1();

void __declspec(nothrow) __stdcall foo2();

void __stdcall foo3() throw();

其中 foo3是标准 C++的声明语法,foo2是 C++ Builder 的扩充声明语法,foo1是采用宏定义的 C++ Builder声明。当处理多个相同类型的函数时,采用宏定义要高效

得多。 novtable关键字用于类的定义声明,__declspec(novtable)只能用于纯粹的接

口类,这些类永远不会自身实例化,例如某些纯虚基类就是这样的,当定义成

__declspec(novtable)类的时侯,编译器不为其产生虚拟函数分配表(vtable),编

译器连接时会去处所有关于该类的虚拟函数连接,从而有效的降低代码长度。 property关键字用于定义“虚拟数据成员”,和后面要介绍__property关键字类

似,但功能似乎要比__property 弱得多,而且实际中好像也没有多大意义,因为

Borland也好像只在帮助文件中出现过这样的用法。其语法如下: __declspec(property(get=get_func_name)) declarator

__declspec(property(put=put_func_name)) declarator

__declspec(property(get=get_func_name,put=put_func_name)) declarator

第四章 C++ Builder 语言的扩充 第 161 页

同__property 关键字一样,__declspec(property())也只能用于类的成员定义中,

比如可以定义一个虚拟数组成员 X: __declspec(property(get=GetX, put=PutX)) int x[];

i = p->x[a][b]

实际上就是: i=p->GetX(a, b),

p->x[a][b] = i

实际上就是: p->PutX(a, b, i);

selectany 用于头文件中对变量声明且初始化的情况,通常可执行文件或者动态链

接库对全局变量只能进行一次初始化,当一个变量在头文件中初始化并被不同的源 cpp文

件引用时就会产生错误,selectany 就是告诉编译器初始化时“任选其一即可”例如在

头文件中: __declspec(selectany) int x1 = 1; 是正确的,x1被声明并正确初始化。 const __declspec(selectany) int x2 = 2; 在 C++语言中这是不正确的,因为 C++中 const 缺省的代表着静态变量,不具备

extern 特性,因此不能正确初始化,但是在 C 语言中,这是正确的,因为 const 在 C语言中缺省的不是静态变量。

extern const __declspec(selectany) int x3=3; 是正确的,const被重载具有 extern特性。 extern const int x4; const __declspec(selectany) int x4=4; 正确,和上面的情况相同。 extern __declspec(selectany) int x5; 是不正确的,因为 x5没有初始化,__declspec(selectany)只能用于变量的初始

化过程。 thread用于“进呈局部变量”的定义,__declspec(thread)和__thread 关键字

的作用一样,关于“进呈局部变量”参见__thread关键字的介绍。 uuid用于将一个完整的 COM对象标记为一个类或者结构类型,主要用于分布式编程,

uuid的参数可以是注册对象的 GUID字符串,也可以是带有{}分界符的字符串,例如: struct __declspec(uuid("00000000-0000-0000-c000-000000000046")) IUnknown;

struct __declspec(uuid("{00020400-0000-0000-c000-000000000046}"))

IDispatch;

可以使用__uuidof 关键字来获取和__declspec(uuid(..))修饰符一样的功能,但

第四章 C++ Builder 语言的扩充 第 162 页

在 C++ Builder中__declspec关键字可以带来很多方便。 通常,C++语言中的修饰符必须处于被修饰对象的前面,并且相邻的位置,使用

__declspec关键字可以消除这种修饰符位置的强约束性,比如输出一个函数: 使用__export关键字只能采用以下形式: void __export f(void); 而不能是: __export void f(void); 型式,但是采用__declspec 关键字,则下面几

种声明语句都是正确的: void __declspec(dllexport) f(void); __declspec(dllexport)void f(void); class __declspec(dllexport) ClassName { };

1.3 C++ Builder 扩充变量类型

在 C++ Builder中允许程序员指定整型变量的长度,以二进制位数来表示,但仅限

于以下几种类型 __int8 八位整型。 __int16 十六位整型。 __int32 三十二位整型。 __int64 六十四位整型。 unsigned __int64 六十四位无符号整型。 而且,在使用扩展整型变量时,必须对数值加上适当的后缀,__int8 、__int16、

__int32、__int64、unsigned __int64 的后缀分别为 i8、i16、i32、i64、ui64,例如:

__int8 c = 127i8; __int32 i = 123456789i32; __int64 big = 12345654321i64;

1.4 函数调用规则与返回

所谓调用规则是指函数采用什么样的方式来传递参数和返回值的,在高级语言中有两

大类比较常用的调用规则,分别是 C调用规则和 Pascal调用规则,许多编译器为了提高

程序性能,在这两种规则的基础上进行了一些扩充。 C++ Builder 中支持五种不同的调用规则,它们分别是:C 调用规则、Pascal 调

用规则、Win32标准调用规则、寄存器调用规则和 Microsoft 的寄存器调用规则。除了

这五种调用规则之外。C++ Builder 还允许使用“自定义”类型的调用规则,就是前面

所讲到的__declspec(naked)关键字修饰符。 下面就这五种调用规则逐一介绍。

第四章 C++ Builder 语言的扩充 第 163 页

1.4.1 __cdecl、_cdecl、cdecl 关键字

这是在 C++ Builder中特有的关键字,__cdecl、_cdecl、cdecl 可以用来修饰

变量或者函数,其含义是指定的变量或者函数使用 C语言的调用规则和命名规则。在 C++ Builder中 C调用规则是缺省的设置,可以通过 Project的 Option菜单在 Advanced Compiler标签中来改变缺省设置。

对于 C命名规则的变量或函数,具有大小些敏感、编译后变量名称前下划线前导符,

这与 Pascal语言是不一样的。对于函数,C调用规则要求使用堆栈传递参数,参数传递

的顺序为自右向左依次压入堆栈,由调用者自行清除堆栈,C调用规则允许向函数传递不

定参数,但这种调用方式系统需要更多的开销来完成参数的匹配,除非特殊用途一般尽量

不要使用,这种调用方式最典型的例子是控制台程序中的 main函数,程序运行时可以不

带参数,也可以带多个参数。 在一个独立的程序中,采用那种调用规则或者命名规则影响并不十分大,但是在多种

语言混合编程,或者编写带有数据输出或函数输出的可执行模块(可执行文件和动态链接

库)时,命名规则和调用规则显得尤为重要,使用不当则可能导致程序无法正常运行,甚

至使系统崩溃。 __cdecl、_cdecl、cdecl的使用语法如下: cdecl <data/function definition>;

_cdecl <data/function definition>;

__cdecl <data/function definition>;

例如: int cdecl I; //编译后标识符为“_I” void __cdecl Fun(); //编译后函数名为“_Fun”

1.4.2 __fastcall、_fastcall 关键字

__fastcall 和_fastcall 也是 C++ Builder 中特有的关键字,只能用于修饰函

数,其作用是指定函数使用“寄存器”调用规则,使用语法如下: return-type _fastcall function-name(parm-list)

return-type __fastcall function-name(parm-list)

例如: void __fastcall GetName(int Index);

使用“寄存器”调用规则的函数,其参数传递顺序为自左向右依次传递,并且将前三

个参数尽可能的使用 EAX、EBX、ECX 这三个寄存器传递,对于前三个参数中浮点类型、

第四章 C++ Builder 语言的扩充 第 164 页

结构类型等超过四个字节的变量,和第四个及以后的参数则采用堆栈来传递,因此采用“寄

存器”调用规则的函数只能传递固定数量的参数。 在 C++ Builder中,所有属于 VCL 的成员函数,必须是__fastcall类型,编译

器将“寄存器”调用规则和 C 调用规则、Pascal 调用规则以及 Win32 的标准等其它调

用规则是同等对待的,因此__fastcall 关键字不能和__cdecl、 __pascal、__stdcall 等关键字联合使用,也不能和__export 关键字同时使用(实际上使用

__fastcall 规则的函数是允许输出的,只是这种调用规则编写的 dll 或可执行模块只

能被 C++ Builder或者 Delphi开发的程序载入并调用输出的函数)。 被指定为“寄存器”调用的函数会在编译时被冠以前导符@,这将使得 C及 C++的函

数命名过于混乱,但是这种调用规则有很高的执行效率,因此在 C++ Builder中几乎所

有 VCL的成员函数都使用这种调用规则。 但是__fastcall 和_fastcall 是 C++ Builder之前的 C++编译器所没有的调用

规则,为了保证兼容性,可以使程序连接以前版本的编译器生成的库文件,C++ Builder提供了-VC命令行选项,缺省状态下,该选项是关闭的,当需要时,可以通过命令行编译

并选择-VC选项。

1.4.3 __msfastcall、__msreturn 关键字

__msfastcall和__msreturn 也是 C++ Builder 中特有的关键字,使用这两个

关键字可以为程序提供和 Microsoft兼容的“寄存器调用”规则以及和 Microsoft兼

容的函数返回的规则。 被__msfastcall修饰的函数,其参数传递顺序为前两个参数如果长度小于 4字节,

则分别使用 ECX 和 EDX 寄存器传递,其余的参数和前两个不能使用寄存器传递的参数则

按照从右向左的顺序通过堆栈来传递,堆栈由被调用函数负责清除。 __msreturn 是使用 Microsoft兼容的“寄存器调用”返回规则,在这种返回规则

中,返回值的长度在 4~8个字节时,将采用 EAX/EDX寄存器返回函数值。

1.4.4 __pascal、_pascal、pascal 关键字

__pascal、_pascal、pascal关键字表示一个变量或者函数遵循 Pascal 的调用

和命名规则。 在 Pascal的命名规则中,对大小写并不敏感,而且不象 C调用规则,会在变量和函

数明成前面加前导符。Pascal调用规则中,参数的传递是按照从左到右的顺序依次压入

堆栈,并且由被调用函数自行清除堆栈。 Pascal调用规则破坏了 C语言的本身的调用体系,同时也带来了更多的灵活性,被

表示为__pascal调用规则的函数,编译后的目标文件中,名称会被全部转换为大写字母,

在 Pascal语言中,过程和函数的书写是对大小些不敏感的,但是在 C++ Builder中采

用 Pascal调用规则的函数,在 C++源文件中仍然需要遵循 C语言中对大小些必须严格一

致的要求,Pascal调用规则的大小写不敏感性只能体现在动态链接库、或者输出、输入

函数以及数据的时侯。也正是这样的原因,对一些采用 Object Pascal 编写的第三方控

第四章 C++ Builder 语言的扩充 第 165 页

件或者书写不是很规范的 Pascal程序代码,使用 C++ Builder编译的时候,会出现找

不到函数原型的错误,实际上可能只是程序中函数名称的大小些不一致造成的。

1.4.5 __stdcall、_stdcall 关键字

在 Windows 程序设计中,有一种 Win32的标准调用规则,基本上所有的 Windows API函数都采用了这种 Win32的标准调用规则,在讲__declspec(nothrow)关键字时的例

子,其中宏定义就是 C++ Builder 中 winuser.h 宏定义。Win32 标准调用在 C++ Builder中以__stdcall、_stdcall关键字来标识。

在__stdcall、_stdcall 调用规则中,函数的参数是按照从右向左的顺序依次压

入堆栈,所有的参数均采用堆栈传递,而且是被调用函数负责清除堆栈。在参数的传递顺

序上,Win32 标准调用规则和 C 调用规则一样,从右向左,但和 C 调用规则不同的是,

Win32的标准调用要求调用者必须严格按照被调用函数的参数数量及类型进行参数传递。 关于 C++ Builder中函数调用规则以及命名规则的详细内容,读者参考相关书籍,

这里不做过多讲解,下表给出了 C++ Builder 中几种调用规则的一个简单比较,具有汇

编语言基础的读者可以参考本书光盘中相关代码自行分析区别。

关键字 调用规则 参数传递方向 返回 参数寄存器 堆栈的清除

__cdecl C调用规则 从右向左 EAX 无 调用者

__fastcall 寄存器 从左向右 EAX EAX、EBX、ECX 被调用者

__stdcall Win32标准 从右向左 EAX 无 被调用者

__pascal Pascal 从左向右 EAX 无 被调用者

__msfastcall Ms寄存器 从右向左 EAX/EDX ECX、EDX 被调用者

表 4.1 C++ Builder中几种调用规则的比较

1.5 函数和数据的输入输出

在 Windows 程序中,任何一个可执行模块的函数和数据都可以输入和输出,但需要

在模块控制文件(.def 文件)中定义。C++ Builder 提供了_export、__export、__import、_import 等关键字,可以使程序员不必使用.def文件来定义数据和函数的

输入输出。它们的作用分别和__declspec( dllexport )、__declspec( dllimport )相当。

被定义成__export 的函数,编译器连接时会将列入这个可执行模块的输出列表

(export table),这将增加了代码的长度并降低了执行效率,但同时为编程提供了方

便,如果不采用 export 关键字,动态链接库会按照函数的顺序进行输出,这样在采用

Loadlibrary 动态加载可执行模块时,就不能使用函数名获取函数的入口,而必须采用

函数的输出序号,关于函数输出和输入的详细内容请参考 Windows SDK。

第四章 C++ Builder 语言的扩充 第 166 页

1.6 __try、__except、__finally 关键字

例外处理机制是 C++语言引入的一种程序的保护措施,可以有效的防止意外的执行错

误,比如内存溢出,“0”除数等,从而保护系统不会因为程序出错而崩溃。try、throw等关键字就是为例外执行处理而引入的,在C++ Builder中Borland公司提供了比Ansi C++更为强大的处理机制,下面就 C++ Builder 中例外处理的关键字作以简单介绍,关

于 C++异常处理的详细内容我们在后面另行介绍。 在 Ansi C++中关于异常处理的关键字在 C 语言中是不受支持的,因而在 C 与 C++

语言的混合编程中,对异常的处理是比较困难的,而 C++ Builder提供了几个新的关键

字,来支持 C 语言的异常处理,这就是__try、__except、__finally,__try 关键

字在 C++语言中和 try的作用是相同的,其区别是__try关键字支持 C语言中的应用,

__except、__finally是扩充的关键字,可以用于 C和 C++的异常处理。 异常处理,通常是由一个 try关键字和其后的 catch关键字组成,try 后面的语句

或语句组合是可能出现异常的代码,catch 关键字用于捕捉异常,__except 是用于 C程序产生的异常处理,而__finally关键字后面的语句是不管 try 语句中的执行状况如

何,都需要执行的动作。下面的例子说明了如何使用这几个关键字: #include <stdio.h>

#include <string.h>

#include <windows.h>

class Exception

{ //定义一个异常类

public:

Exception(char* s = "Unknown"){what = strdup(s); }

Exception(const Exception& e ){what = strdup(e.what);}

~Exception(){free(what); }

char* msg()const{return what; }

private:

char* what;

};

int main()

{

float e, f, g;

try

{ try

{

f = 1.0;

第四章 C++ Builder 语言的扩充 第 167 页

g = 0.0; try

{

puts("Another exception:");

e = f / g; //制造一个异常;

} __except(EXCEPTION_EXECUTE_HANDLER)

{ //处理 C类型的异常

puts("Caught a C-based exception.");

throw(Exception("Hardware error: Divide by 0"));

} //抛出例外,被 catch捕获;

}

catch(const Exception& e)

{

printf("Caught C++ Exception[ %s ]\n", e.msg());

}

}

__finally

{

puts("C++ allows __finally too!");

} return e;

}

上面的程序运行后的结构如下: Another exception: Caught a C-based exception. Caught C++ exception[Hardware error: Divide by 0] C++ allows __finally too! 在这个例子中先定义了一个异常的类 Exception,然后在 main 函数中使用了两个

try语句组合,其中一个是基于 C语言的异常处理,另一个是基于 C++的异常处理,结果

是这两个异常处理过程均被触发。 前面也提到过 VCL类库不建议使用太多的条件判断来完成一个操作,而更推荐使用异

常处理机制来确保程序的正确执行和出错时的条件复原。异常处理机制也是面向状态的程

序设计中必不可少的,后面将专门就 C++ Builder的异常处理进行讲解。

1.7 VCL 类库关键字扩充

VCL是 C++ Builder 基本类库,其中不少内容是和以前的 OWL 以及 MFC大不相同

的,面向状态也正是建立在 VCL 基础上的,因此也有一些语法以及关键字是 Ansi C++

第四章 C++ Builder 语言的扩充 第 168 页

不具备的,这正是面向状态最需要的关键字,下面就 VCL中使用的特殊关键字作以描述。

1.7.1 __classid 关键字

__classid 关键字实际上是一个操作符,编译器使用这个关键字以生成指向特定的

classname列表的指针。 也就是说这个操作符被用来从一个类获得元类,其语法是: __classid(classname) 比如在每一个 C++ Builder的程序中,WinMain函数中都可能有这样一条语句: Application->CreateForm(__classid(TForm1), &Form1);

用来自动创建一个窗体 TForm1的实例 Form1,CreateForm的原型为: void __fastcall CreateForm(System::TMetaClass* InstanceClass,

void *Reference);

CreateForm 函数用于运行时动态创建一个窗体,元类 TMetaClass 是 Object

Pascal中类引用在 C++ Builder中的体现,因此__classid关键字实际上是为支持

Object Pascal语言而引入的。通常这个关键字程序员不必直接使用,编译器往往会自

动在必要的时侯生成相应代码。 比如组件开发时,__classid用于注册属性编辑器,组件,和类,并可用于 TObject

的 InheritsFrom方法。下列代码说明创建从 TWinControl派生的一个新组件时使用

__classid: namespace MyWndctrl

{

void __fastcall PACKAGE Register()

{

TComponentClass classes[1] = {__classid(MyWndCtrl)};

RegisterComponents("Additional", classes, 0);

}

}

在这个例子中,RegisterComponents 要使用一个 TComponentClass 类型的数

组作为参数,来决定要将那些组件安装在相应的组件面板,TComponentClass 是指向

TComponent及其派生类的元类指针,它的定义如下: typedef TMetaClass* TComponentClass;

第四章 C++ Builder 语言的扩充 第 169 页

这和上一个例子中的作用实际上是一样的。

1.7.2 __closure 关键字

__closure 关键字被用来声明一个特殊类型的指针指向成员函数。不同于一般的

C++ 成员函数指针,__closure是包含了对象指针的。 在 Ansi C++中,可把一个派生类的实例分配给基类指针;但是,不能把一个派生类

的成员函数分配给基类成员函数指针。 下列代码说明这种情况: class base

{

public:

void func(int x);

};

class derived: public base

{

public:

void new_func(int i);

};

void (base::*bptr)(int);

bptr = &derived::new_func; // 这是非法的

然而,__closure 语言扩展允许在 C++ Builder 中使用这种用法。__closure

将指向成员函数的指针与指向类实例的指针联系起来。当调用相联的成员函数时,指向类

实例的指针被作为 this指针使用。__closure 指针的声明与普通函数指针的声明一样,

只是在被定义的标识符前增加__closure关键字就可以。例如: struct MyObject

{

double MemFunc(int);

};

double func1(MyObject *obj)

{

//定义一个 closure属性的函数指针,返回 Double,参数为整型。

double ( __closure *myClosure )(int);

//初始化指针

myClosure = obj -> MemFunc;

//用这个闭合指针调用函数

return myClosure(1);

第四章 C++ Builder 语言的扩充 第 170 页

}

__closure 在 C++Builder 中总是与事件一起使用。前面我们也讲过了,事件是

VCL中消息处理的基础,也是描述状态的一个必不可少的属性,我们来看对一个事件的初

始化过程。通常设计时指定的事件处理句柄都被 C++ Builder保存在资源文件中(C++ Builder的资源文件为.dfm),程序启动时自动的初始化,然而事件也可以在运行时初始

化,甚至是在运行时动态的改变,前面的例子中我们已经有这样的应用,这是这种灵活的

处理方式,才使面向状态更加完整。我们看一下代码: Form1的头文件: #include <Buttons.hpp> //---------------------------------------------------------

class TForm1 : public TForm

{

__published:

private:

public:

void __fastcall MyOnMessageHandle(tagMSG &Msg, bool

&Handled);

__fastcall TForm1(TComponent* Owner);

}; //----------------------------------------------------------

extern PACKAGE TForm1 *Form1;

//-----------------------------------------------------------

#endif

Form1的单元文件:

#include <vcl.h>

#pragma hdrstop

#include "Unit1.h"

//-----------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

//-----------------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

Application->OnMessage = MyOnMessageHandle;

第四章 C++ Builder 语言的扩充 第 171 页

} //-----------------------------------------------------------

void __fastcall TForm1::MyOnMessageHandle(tagMSG &Msg, bool &Handled)

{

//... 消息处理代码;

}

在这个例子中 Application->OnMessage的原型如下: typedef void __fastcall (__closure *TMessageEvent)(tagMSG &Msg,

bool &Handled);

__property TMessageEvent OnMessage = {read=FOnMessage,

write=FOnMessage};

TMessageEvent 是 一 个 __closure 的 函 数 指 针 类 型 , OnMessage 是

Application 对象的成员,在 Form1 中定义了一个与之相符合的消息处理函数:

MyOnMessageHandle,Form1在创建的时侯对 Application->OnMessage 进行初始

化。从这里可看出,实际上 Form1 和 Application 是两个不相干的对象,但是

Application 的函数指针成员却指向了 Form1 的成员函数,这在 Ansi C++中是绝对

不允许的,但是在 C++ Builder中这样却是正确的。由此可见__closure指针的出现

为对象Application提供了更加有好和灵活的对外接口,也是更加符和客观规律的接口,

通过事件这一特殊属性的接口,才使面向状态成为可能。

1.7.3 __property 关键字

在上一章中,我们已经接触过__property 关键字,它主要应用与创建和声明属性,

前面也讲过属性只能在类中被声明。 对于属性数组(当<prop dimlist>被使用时),数组的索引可以是任何类型。 其语法: <property declaration> ::= __property<type> <id> [ <prop dim list> ] = "{" <prop attrib list> "}"

其中<prop dim list>是可选的,只有声明一个数组属性的时侯才需要,成为索引

参数,可以为任何类型的变量,它的格式如下: <prop dim list> ::= "[" <type> [ <id> ] "]" [ <prop dim list> ]

索引列表可以由一个或者多个索引参数组成。

第四章 C++ Builder 语言的扩充 第 172 页

属性声明中<prop attrib list>为特征列表,其格式如下:

<prop attrib list> ::= <prop attrib> [ , <prop attrib list> ]

其中<prop attrib>有以下六中特征: read = <data/function id>

write = <data/function id>

stored = <data/function id/bool constant>

default = <constant>

nodefault

index = <const int expression>

这六个特征各有用途,其中 read 和 write可以使用在 C++ Builder中任何的类

中,其他几个只能使用在 VCL风格的类中,而且通常都是在编写组件时使用的。 read 部分指定了该属性的读取操作,可以是一个和属性类型相同数据变量,也可以

是一个函数句柄,称作读取函数,该函数必须有着和该属性类型相同的返回值,属性为数

组时,读取函数至少包含一个索引参数,其参数的类型和数量与<prop dim list>中的

类型和数量一一对应。 write部分指定了属性的存取操作,可以是一个和属性类型相同的数据变量,也可以

是一个函数句柄,称作存储函数或者写函数,该函数一般无返回值,但是必须带有一个和

属性类型一致的参数,如果属性是数组的时侯,存储函数的参数中应该包含一个和属性类

型一致的参数,并且是第一个参数,后面的参变数应该和<prop dim list>中的类型和

数量一一对应。 stored用于 VCL组件中,表明这个属性在设计时所设置的取值需要被保存在资源文

件中,可以为一个 bool类型的变量也可以为一个具有 bool类型返回值的函数,关于 VCL组件的存储机制以及 C++ Builder对资源的使用在后面的章节介绍,这里仅仅对关键字

的使用作一了解。 default 用于指定一个缺省的取值,应该是和属性类型相同的一个常量。该缺省值

是供编译器使用的,当设计程序时,在 Object Inspector 中为组件属性设置的取值和

缺省值相等的时侯,编译器将不管 stored部分为真还是为假,都不会将属性取值存储在

资源文件中。default部分并不能为属性进行初始化,也就是说,定义了 default 部分

的属性,必须在构造函数中对该属性进行正确初始化。 nodefault表示属性没有缺省值,不能和 default 部分同时出现。指定了一个属性

没有缺省值,编译器会将为该属性设置的任何取值都存储在资源文件中。 index部分定义了一个索引值,必须为一个整型的常量。索引值为多个属性共用相同

的存储函数和读取函数提供了可能,当一个属性定义了索引的时侯,其读取函数和存储函

数必须将索引作为第一个参数,后面的参数和不带有索引项的属性相同。 通常一个属性的特征列表中,至少应该包含一个 read部分,这样该属性可以被其他

对象当做变量一样访问,只包含 read部分的属性是只读的,不能写入,可读可写的属性

第四章 C++ Builder 语言的扩充 第 173 页

应该至少包含 read 和 write 两个部分。下面这一段代码显示了如何使用__property关键字。

class PropertyDemo : public TObject

{

private:

int Fi0;

int Fi1;

int Fi2;

int Fi3;

int Fi4;

int Fi5;

protected:

void __fastcall SetFi2(int Value){ /*...*/ };

int __fastcall GetFi3(){ return Fi3; };

void __fastcall SetFi3(int Value){ /*...*/ };

int __fastcall GetFi4(){ return Fi4; };

void __fastcall SetFi4(int Value){ /*...*/ };

int __fastcall GetFi5(){ return Fi5; };

void __fastcall SetFi5(int Value){ /*...*/ };

public:

__property int PropI0 = {read=Fi};

__property int PropI1 = {read=Fi,write=Fi,default=1};

__property int PropI2 = {read=Fi,write=SetFi};

__property int PropI3 = {read=GetFi3,write=SetFi3};

__property int PropI4 = {read=GetFi4,write=SetFi4,

default=4};

__property int PropI5 = {read=GetFi5,write=SetFi5,

default=5,stored=true};

__fastcall PropertyDemo();

};

__fastcall PropertyDemo:: PropertyDemo ()

: TObject ()

{

/*其他初始化内容*/

Fi1 = 1; //对属性 PropI1的 default取值初始化

Fi4 = 4; //对属性 PropI4的 default取值初始化

Fi5 = 5; //对属性 PropI5的 default取值初始化

第四章 C++ Builder 语言的扩充 第 174 页

}

在 VCL风格的类中(从 TObject 派生而来)上面这几个属性的声明都是正确的,其

中: PropI0是一个只读属性,其 read部分直接和一个数据变量 Fi0对应; PropI1是一个可读可写的属性,其 read和 write部分都直接和数据变量 Fi1对

应,该属性的缺省取值为 1,在构造函数中必须对属性进行初始化 Fi1 = 1; PropI2是一个可读可写的属性,其 read部分直接和数据变量对应,而 write部分

和一个存储函数 SetFi2对应,该函数带有一个参数,其类型 PropI2相同。 PropI3是一个可读可写的属性,其 read和 write部分分别和两个函数对应,read

部分的 GetFi3是一个返回值和 PropI3相同的无参数函数,SetFi3同 PropI2。 PropI4是一个可读可写的属性,其 read和 write部分分别和两个函数对应,read

部分的 GetFi4是一个返回值和 PropI4相同的无参数函数,SetFi4 同 PropI2。该属

性的缺省取值为 4,在构造函数中必须对属性进行初始化 Fi4 = 4; PropI5是一个可读可写的属性,前 read、write和 default部分和 PropI4类

似,在 stored部分定义为 true,代码上和 PropI4 基本一样,但是如果设计时使用该

组件,其生成的可执行文件的资源是不一样的。 下面的代码显示了如何使用<prop dim list>参数列表来构建一个 AnsiString

的数组属性。这个属性使用时表现形式和一个数组比较接近。 class TDemo : public TComponent

{

private:

AnsiString __fastcall GetNumberSize(int Index);

public:

__property AnsiString NumberSize[int Index] =

{read=GetNumberSize};

//...

};

AnsiString __fastcall TDemo::GetNumberSize(int Index)

{

AnsiString Result;

switch (Index)

{

case 0:

Result = "Zero";

break;

case 100:

第四章 C++ Builder 语言的扩充 第 175 页

Result = "Medium"; break;

case 1000:

Result = "Large";

break;

default: Result = "Unknown size";

}

return Result;

}

在这个例子中TDemo只有一个属性NumberSize,类型为一个AnsiString的数组,

并且是一个只读属性。数组属性的 read部分只能是读取函数,而不能和一个数组直接对

应。 下面的代码显示了如何使用 Index部分,使多各属性使用同一个读取和存储函数: class TSampleCalendar : public TCustomGrid

{

private:

TDateTime FDate;

//⋯⋯其它成员,用于显示日历

int __fastcall GetDateElement(int Index); //索引作为参数

Void __fastcall SetDateElement(int Index, int Value);

//索引作为第一个参数

public:

__property int Day = {read=GetDateElement,

write=SetDateElement, index=3, nodefault}; __property int Month = {read=GetDateElement,

write=SetDateElement, index=2, nodefault};

__property int Year = {read=GetDateElement,

write=SetDateElement, index=1, nodefault};

//⋯⋯其它成员函数,用于显示日历

};

由于日期的每一个元素(年、月、日)都是一个整数类型,而且每一个元素存储时都

要进行编码变换,这种变换具有一定的共性,因此采用带有索引的属性可以避免重复的运

算代码,读取函数 GetDateElement的代码如下: int __fastcall TSampleCalendar::GetDateElement(int Index)

{

第四章 C++ Builder 语言的扩充 第 176 页

unsigned short AYear, AMonth, ADay;

int result;

FDate.DecodeDate(&AYear, &AMonth, &Aday); //

switch (Index)

{

case 1:

result = AYear;

break;

case 2:

result = AMonth;

break;

case 3:

result = ADay;

break;

default:

result = -1;

}

return result;

}

存储函数 SetDateElement的代码如下: void __fastcall TSampleCalendar::SetDateElement(int Index,

int Value)

{

unsigned short AYear, AMonth, ADay;

if (Value > 0) //有效的元素必须是大于“0”的

{

FDate.DecodeDate(&AYear, &AMonth, &ADay); //获取原始元素

switch (Index)

{ case 1:

AYear = Value;

break;

case 2:

AMonth = Value; break;

case 3:

ADay = Value;

第四章 C++ Builder 语言的扩充 第 177 页

break;

default:

return; //无效的索引项,则不需要设置新的日期

}

}

FDate = TDateTime(AYear, AMonth, ADay); //对新的元素编码

Refresh(); //刷新图形界面

}

这个例子是一个日历控件 TSampleCalendar 的部分代码,控件中使用了年、月、

日三个元素将日期类型的变量分解开来表示,并且采用了有效的索引来共享日期编码和转

换过程。 从前面的几个例子,读者就__property 关键字的使用应该由一个大致的了解。在上

面的两个例子中都使用了 Index 作为参数,但是两者有根本的不同,第一个例子中的

Index相当于“数组”属性的数组下表,可以为任何类型的变量;而第二个例子中的 Index是属性声明部分的 index定义,使用 index部分可以使多个属性使用相同的读写函数,

该变量只能为整型,这一点读者一定要区分清楚。关于属性的更进一步内容,我们放在有

关组件设计内容里面和实际例子结合起来作更深入的了解。通常可以通过属性的一些特征

将它们与数据成员区分开来,一般来说属性具有以下特征: n 可以通过一个标识符(属性)连接到特定的数据或者读、写方法,程序中并不存

在为属性分配的存储空间; n 可以为属性设置缺省值(仅在组件设计中有效); n 可以存储在一个窗体文件中(仅在组件设计中有效); n 可以扩展一个在基类中被定义了的属性。

1.7.4 __published 关键字

__published 是 C++ Builder 中特有的一个关键字,而且__published关键字

只能用于 VCL类库,即从 TObject派生的类中,用于声明变量或者函数为“发布”类型,

同__public关键字类似。声明在__published 域中的成员,其可见性规则与公共的成

员相同,所不同的是__published 域中的成员会生成运行时类型信息(RTTI),RTTI是 Object Pascal风格的,它将允许应用程序能够动态地查询数据成员,成员函数和未

知类型的属性。 在组件中使用__published 可以使组件被安装后,设计时__published 域中的属

性可以在 Object Inspector 中被查看和更改,TCustomForm 是一个例外,因为它和

它的派生类并不是一个常规的控件,通常是作为别的控件的容器而出现,只有其派生类在

程序设计时,__published 域中声明的类变量、成员函数是在 Object Inspector 可

见的,而设计组件的时侯,组件的属性等都不可以在 Object Inspector 查看。另外,

TCustomForm 及其派生类的类成员变量在__published 域中只能为 VCL可视组件、必

第四章 C++ Builder 语言的扩充 第 178 页

须是指针对象、而且在相应的资源文件中必须有对应的对象,也就是必须在 IDE的 Form中创建,__published 域中的指针对象不需要进行初始化,程序运行时会自动调用相应

的资源进行初始化,这个域中的成员函数也具有可视性,可以在 Object Inspector 分

配给相应类型的事件,Object Inspector 会对所有的属性、成员函数进行过滤,只显

示类型匹配的属性或函数。 在__published 域不允许有构造函数或析构函数。属性、Object Pascal 内在的

或 VCL派生的数据成员、成员函数及闭合都允许在__published 域中。__published域中定义的属性不能是数组属性,而且类型必须是顺序类型,real 类型、字符串类型、

小集合类型、对象类型或方法指针类型。

1.7.5 __declspec 关键字扩展

在 C++ Builder中,__declspec关键字扩展的一些参数为 VCL提供语言支持。

这些参数全部是为了兼容 Object Pacal 而引入的,下面将分别简单作以介绍,这些参

数在 sysmac.h 中均有对应的宏定义。通常程序员不会用到这些关键字和宏定义,C++ Builder在编译 Object Pascal 单元时会自动生成,当确实需要增加它们时,应该尽

量使用宏。 1.__declspec(delphiclass ) delphiclass 参数用于从 TObject 派生的类的声明。这些类在创建时将遵循下列

VCL的兼容性: n VCL兼容的 RTTI。 n VCL兼容的构造函数/析构函数行为。 n VCL兼容的异常处理。 一个 VCL兼容的类有下列限制: n 不允许虚基类。 n 不允许多重继承。 n 必须使用全局的 new操作符或与之相当的动态地分配。 n 必须有一个析构函数。 n 拷贝构造函数和赋值操作符对于 VCL派生类不会是编译器生成的。 如果编译器需要知道类是从 TObject派生的,从 Object Pascal 转换的类声明将

需要这个修饰符。 2.__declspec(delphireturn) delphireturn参数只在 C++ Builder 中被 VCL内部使用。它用于 C++ Builder

创建的类的声明,这些类用于支持那些没有原本的 C++类型的 Object Pascal 内嵌的

数据类型和语言结构。包括 Currency、AnsiString、Variant、TDateTime 及 Set。delphireturn参数将 C++类标记为在作为函数调用的参数和返回值方面是 VCL兼容处

理的。当 Object Pascal和 C++需要按值传递一个结构给函数时,需要使用这个修饰符。 3.__declspec(dynamic) dynamic 参数被用于动态函数的声明。动态函数仅存储在定义它们的对象的

Beibei
Note
先暂时看到这里!
Beibei
Note
Unmarked set by Beibei

第四章 C++ Builder 语言的扩充 第 179 页

vtables中,而不是在派生 vtables中,除此之外都与虚拟函数类似。如果调用一个动

态函数,而函数没在对象中定义,其祖先的 vtables 将被寻找直到函数被发现。动态函

数只对从 TObject派生的类有效。 4.__declspec(hidesbase) 当移植 Object Pascal的虚拟和重载的函数到 C++ Builder 时,hidesbase 参

数维护 Object Pascal程序语义。在 Object Pascal中,基类的虚拟函数能以相同

名字的函数在派生类出现,但是它是一个新的函数,与原来的一个没有显式关系。 编译器使用在 sysmac.h定义的 HIDESBASE 宏,指定这些类型的函数声明是完全独

立的。例如:在一段 Object Pascal 的代码中,如果基类 T1声明一个虚拟函数 func,它没有参数,同时它的派生类 T2以相同的名字声明了一个函数,当使用 DCC32 – jphn命令编译后,在 HPP文件中将生成如下的定义:

virtual void T1::func(void); HIDESBASE void T2::func(void); 若没有 HIDESBASE声明,C++程序语义指出虚拟函数 T1::func()被 T2::func()

重载。 5.__declspec(package) package参数指示定义类的代码可被编译到包中。当在 IDE创建包时,这个修饰符

由编译器自动生成。关于包的更多信息,参见后面章节。 6.__declspec(pascalimplementation) pascalimplementation 参数指出定义类的代码以 Object Pascal 实现。这个

修饰符在一个带有.hpp扩展名的 Object Pascal可移植头文件中出现。

1.8 其他 C++ Builder 扩充

1.8.1 __automated 关键字

这个关键字和 public、private 及 protected关键字是类似的,对于 automated成员和 public成员相比较而言,它们的可视性规则是相同的,所不同的是声明的定义域

不同,这个关键字就是指定为 automated定义域,例如: class myclass : TAutoObject

{

//这一部分用于声明成员函数和属性

// and properties that need OLE automation information.

__automated :

// __dispid关键字指示了联合的 OLE自动化对象,派发 id 1000给成员函数 func。

void __fastcall func(void) __dispid(1000);

};

第四章 C++ Builder 语言的扩充 第 180 页

声明在 automated定义域的成员才可能具有建立 OLE自动化对象的能力,和普通的

成员函数和属性相比较,__automated关键字后的成员和属性又具有以下的要求: n 对于成员函数,其返回值(若有)和参数的类型必须是 Currency、OleVariant、

DelphiInterface、double、int、float、short、String、TDateTime、Variant和 unsigned short等类型之一,否则编译器会报错。

n 成员函数必须采用__fastcall调用规则。 n 成员函数可以为虚拟函数 n 在成员函数的参数列表后面可以加上__dispid(constant int expression)

关键字。 n 属性的声明部分只能包含__dispid、read和 write部分,而不能象一般 VCL

类包含 index、stored、default和 nodefault等部分。 n 属性的存取部分列表必须是成员函数,而不能是成员标识符。 n 属性的存取函数必须是__fastcall调用规则的。 n 属性的重载是不允许的。

1.8.2 __thread 关键字

和 C++的其他方法一样,TThread 的 Execute 方法及其调用的任何函数,都有其自

己的局部变量。Execute 和其调用的函数一样也能访问程序的全局变量。这为线程之间

的通讯提供了非常方便的途径。但是,在某些情况下希望各个线程在使用全局变量时互不

影响,但在线程中有时需要使用全局变量,而不需要额外的参数传递,这时就可以使用

__thread关键字来定义一个线程局部(thread-local)变量,例如: int __thread x;

就是声明一个整型变量,对应用程序中的各个线程来说它是私有的,当 Execute 执

行的时候,会从全局变量 x获取一份拷贝,这个拷贝在其内部是全局变量,但又不会影响

到原来的变量以及其他线程获取新的拷贝。__thread关键字只能用于全局变量或静态变

量。指针和函数等变量不能作为“线程局部”变量。“在写入时复制”类型的变量,如

AnsiStrings 也不能作为“线程局部”变量。需要运行时初始化或运行时完成的程序元

素不能被声明为__thread类型。下面的声明就是非法的,原因是需要运行时初始化。 int f();

int __thread x = f(); //非法

但是

int __thread x = 50; //合法

是合法的,因为它的初始化不是在运行时完成的。另外,类的实例也不能作为“线程

局部”因为类需要运行时调用构造函数进行初始化,例如:

第四章 C++ Builder 语言的扩充 第 181 页

class X

{

X( );

~X( );

};

X __thread myclass; //非法

需要注意的是,线程在获取“线程局部”变量的拷贝时,只和最初声明的初始值有关,

也就是说,线程中的和非线程中的函数和例程之间,对__thread关键字修饰的全局变量

也是相互独立的,不管 TThread创建的时,“线程局部”变量的取值如何,线程所获取拷

贝的值总是声明该变量时初始化的取值,因为主进程实际上也是一个特殊的线程,它也是

从“线程局部”变量那里获取了一份拷贝而已。

2 VCL 类库层次结构

C++ Builder使用了 VCL作为编译器的 FrameWorks,因此对 C++ Builder使

用的娴熟程度很大程度的决定于对 VCL类库的了解。可以这样说,一个应用程序所能用到

基本上 Borland也已经想到了,并且已经渗透在 VCL类库中,我曾碰到的情况是很多网

友提出了一个特殊要求、或者特殊功能的应用,非常困惑,不知道使用 C++ Builder该

如何解决,结果是使用 VCL本身的属性、事件和方法,不需要进行任何扩充,就可以非常

容易的实现,而困惑的原因正是对 VCL本身的了解不够透彻。 本书并不准备对 VCL的使用进行详细的讲解,而且内容也非常的多,关于 VCL类库

的更深入的了解需要读者参考有关专门的书籍或者 C++ Builder的联机帮助。本节内容

将主要针对 VCL类库的层次结构作一简单介绍,并将主要的分类的一些重要的属性、事件

和方法作一介绍,这些内容是对于使用面向状态设计和后面分析例程所必须的。而且通过

对 VCL结构的了解,可以使读者建立一个关于 VCL整体框架的认识。

2.1 区分 VCL 类库、组件和控件

在进行深入的了解以前必须先了解几个概念,这就是 VCL类库、组件和控件,在前面

几章中,这几个概念都出现过,但并没有明确其含义,很多书籍中对此也没有做明确的解

释,有时候也很模糊。在本书后面章节中所出现的这几个名词,都具有以下特定的含义: VCL类库,VCL类库指的是从 TObject派生的类,所有的 VCL组件、控件都是 VCL

类库的一部分,但并不是所有的 VCL 类库都是组件或者控件,在 C++ Builder 中

FrameWorks 除了 VCL 类库以外还有一些类库不是从 TObject 派生而来,这些多用于

C++语言对 VCL 类库的支持而新增加的基本类库如 AnsiString 等,或者是为应用

ActiveX等组件而添加的基本类库,它们都属于 C++ Builder 的 FrameWorks 的一部

分,但并不是本书的内容,本书也不作过多描述。 组件,严格的讲是 VCL组件,本书中所提到的组件均指由 TComponent派生而来的

第四章 C++ Builder 语言的扩充 第 182 页

类库,TComponent 定义组件必须具有的一些基本特征,大致来讲组件应该具有以下特征: n 能够被注册在组件面板上,并且可以通过 Form设计器进行设置,也就是设计时

可用。 n 可以作为其他组件的拥有者(Owner),或者可以拥有别的组件。 n 增强从流构建或者归档,这一点说明组件是可以在 dfm文件被记录存储的。而非

组件的类库则可能不具备这种能力。 n 可以在 New Objects对话框中使用 ActiveX向导转换成为 ActiveX控件或

者其他的 Com控件。 控件,本书中所说的控件也是指 VCL控件,VCL控件是从 TControl类派生而来的

类库,所谓控件是一种特殊的组件,因为 TControl是从 TComponent派生而来。控件

具有组件的特征,也有自己的特点,和一般组件最大的区别是控件均应该是“可视的”,

即程序运行时控件是操作员可以看到的,并且可以响应用户的操作,即通过适当的操作完

成某些功能。控件一般是可以接收 Windows消息并能够进行处理的。 需要注意的是并不是所有的控件在程序中都是“可见的”,实际上从 TControl派生

来的类库中,大多数的并不是可见的,也并不出现在组件面板中,它们大多是某一类最终

可视控件的共性的集合体,而且往往是虚拟基类。比如 TControl是所有控件的基类,它

定义了所有控件的共同特征,但实际中永远不会直接使用 TControl。

2.2 TObject 基类

TObject是所有 VCL 类库共同的、最原始的基类,TObject 并没有任何数据成员,

只有一些成员函数,这些成员函数定义了 VCL类库的基本行为规范,因此在 TObject 类

中的成员函数大多数都是虚拟函数,而且很多都是纯虚拟函数,这是这个原因,使得

TObject的子孙将沿袭了 VCL风格的特征,而区别于一般的类。 在 TObject 中并没有成员,因此其构造函数也将是空的,这是一个特例,包括

TObject的派生类 Tpersistent 也是一个具有空构造函数的类。下面将就 TObject的

成员函数作以简单介绍,详细了解请参阅联机帮助。 ClassInfo、ClassName、ClassNameIs、ClassParent、ClassType: 这几个函数都是提供类信息的函数,这是体现 VCL风格的重要函数,其中前四个均有

两个版本,一个是静态函数,另一个是为了使用方便而扩展的函数,如 ClassInfo的原

型如下: typedef TMetaClass* TClass;

static void * __fastcall ClassInfo(TClass cls);

void * __fastcall ClassInfo(){return ClassInfo(ClassType()); }

ClassType用于动态的判断对象的类型,与__classid 相当,但是__classid 操

作符需要使用对象的名称作为参数,只能静态的进行类型判断。 Dispatch、DefaultHandler: 提供对象进行消息处理的虚拟函数,Dispatch会自动分发消息到相应的处理函数,

第四章 C++ Builder 语言的扩充 第 183 页

当指定的消息没有处理句柄时将会调用 DefaultHandler 进行处理,需要注意的是这并

不是 Windows 的消息处理,而是一种类似 Widnwos 消息的处理机制,通过重载这两个

函数,可以使类库响应用户定义的特定消息。Dispatch、DefaultHandler 的参数均

为 void*类型,在 TWinControl 中正是重载了这两个函数,是其成为 Windows 消息的

处理函数。 FieldAddress: 这是一个供对象内部使用的函数,用来提供定义在__publis 定义域中字段的地址,

通常是在组件的流式构造和归档中使用,用户不需显式调用。 Free: 用于销毁对象,一般不需要直接调用,在 C++ Builder中应当使用 delete 关键字

进行销毁,但是在 VCL类库中允许直接使用 Free进行销毁。 InheritsFrom: 用于判断两个对象是否属于派生和继承关系。 InitInstance: 用于初始化一个新分配的对象实例,将所有成员置“零”,然后初始化该对象的虚拟

函数列表指针。程序中不需要直接调用该函数,当重载 NewInstance 函数时,必须在最

后一行代码中调用 InitInstance,此外 InitInstance是一个非虚拟函数,不能够重

载,对成员的初始化需要在构造函数中进行。 MethodAddress 获取一个发布(published)的方法的地址,程序也不需要直接调用该函数,而只

有一个对象从 Stream构造时才会使用到这个方法,是由系统自行完成的。通常是在 VCL类库的事件被初始化是使用该函数,将资源文件中的方法名称转换为相应的入口地址。

MethodName 用于获取一个指定入口地址的函数的名称,这和 MethodAddress 是一个相反的过

程,用于 VCL类库流式输出的时侯。关于 VCL的流式输入输出见后面 TStream 类库的介

绍。 SafeCallException 用于处理 OLE的异常情况。对于 COM接口的异常处理需要重载该函数。 TObject是所有 VCL类库的基类,简言之,TObject具有以下 VCL的共同特征和

行为: n 对于 VCL对象创建和销毁的共有特征。 n 对 VCL对象的类型信息的支持,以及发布属性的运行时类型的支持。 n 对消息处理的支持 大多数 VCL类库的基本行为都是从 TObject类的方法继承而来,而且 TObject的

大多数的方法都是供 VCL类库内部使用的,用户一般不会直接调用。但在一些复杂的组件

或者控件中,它们的表现行为要比 TObject中的复杂的多,这通常是通过重载 TObject的方法实现的。作为一个 C++ Builder 的程序员,有必要对 VCL 作深入的了解,尤其

是希望使用 C++ Builder开发组件的程序员。

第四章 C++ Builder 语言的扩充 第 184 页

2.2.1 TObject 的派生类

下面就 TObject 的直接派生类作一简单介绍。在这些直接派生类中,很多类都是可

以直接使用的,它们都是一些简单类,由一个共同特征——都属于“暂时”类,指的是,

这些类都没有用来在对象被销毁之前保存状态的方法,它们不是固有的。 从 TObject直接派生的类大约有以下 50个: TPersistent TBasicActionLink TInterfacedObject TStream

Exception TWebRequest

TWebResponse TBasicActionLink TBits TFiler

TList TThreadList TConversion TOrderedList

TDragObject TMouse TDataSetDesigner TFields

TLookupList TPaintControl TBookmarkList TBDECallback

TDBError TParamList TSessionList TDSTableProducerEditor

TPropertyCategory TPropertyCategoryList TPropertyEditor TPropertyFilter

TMonitor TSharedImage TChangeLink TCustomIniFile

TMask TPrinter

TXMLData TEnumPropDesc OutlineError TUpdateTree

Tregistry TCustomWinSocket Tthread TDataBlockInterpreter

TSynchroObject TLanguages TWebComponentList TLayoutAttributes

TWebContentOptions TMultiReadExclusiveWriteSynchronizer

在这些派生类中,有一些是封装了数据结构的,比如:TBits、TList、TStack、

TQueue等;有一些是封装了硬件设备的操作,比如 TPrinter、TMonitor、TMouse;还有一些是针对某种特殊应用程序的。在这些派生类中有三个分支是 VCL中比较重要的,

它们分别是 TStream、Exception 和 TPersistent。关于其他派生类的详细说明请参

考联机帮助。

第四章 C++ Builder 语言的扩充 第 185 页

2.2.2 TStream

TStream是这一分支里其它类型的典型代表。TStream 是所有流对象的基类,使用

流可以与磁盘文件、动态内存等等各种存储介质进行读写操作。TStream 也是定义了流

的基本特征,通常并不能直接使用,而是针对不同的流源而使用不同的 TStream派生类: TFileStream 用于文件操作的流。 TStringStream 用于字符串流的操作,通常是位于内存中的字符串。 TMemoryStream 存储器流,通常用于内存缓冲。 TBlobStream 用于数据库的大二进制块字段的读写。 TWinSocketStream 用于读写 Socket的流。 TOleStream 用于 COM接口读写的流。 不同用途的流,在结构和用法上稍有不同,但差别并不特别大,这里仅就流的共同属

性和方法作以简单介绍。 属性: Position 这是一个可读可写的属性,用于记录当前的存取位置,以字节为单位。 Size 也是一个可读可写的属性,用于确定流的大小,对于 TStream,设置 Size是无效的,

其大小是由内部的处理过程完成的,某些派生类的 Size属性被重载,如 TMemoryStream是允许设置 Size来改变流的大小。

方法: 在 TStream的方法中,定义了流操作的基本行为,大致如下: ~TStream、TStream 构造、析构函数,其中构造函数是一个纯虚函数,必须在派生类中实现。 CopyFrom 原型为:int __fastcall CopyFrom(TStream* Source, int Count); 从指定的的源流中拷贝指定数量的字节内容。该函数会覆盖之前的数据,如果指定的

数量为 0,则将 Source的 Position置零,并完整拷贝整个源流的内容,如果 Count不为零,则从 Source 的当前 Position 开始拷贝。函数返回值为实际拷贝的字节数。

使用该函数可以在不同类型的流之间传递数据。 Read 原型为:virtual int __fastcall Read(void *Buffer, int Count) = 0; 是一个纯虚函数,提供给派生类重载,函数从流中读取指定数量字节的内容到缓冲区。

每个派生类必须对该函数进行实例化的重载,因为这是一切流读取操作的基本行为,

ReadBuffer,ReadComponent都是通过调用 Read来实现最终的读取。 ReadBuffer 原型为:void __fastcall ReadBuffer(void *Buffer, int Count);

第四章 C++ Builder 语言的扩充 第 186 页

和 Read 类似,通常用于已知的,相对固定的 Count 的读取,如结构、类等,该函

数也用于内部调用实现 Stream的拷贝,与 Read不同的是这个函数具有 VCL 的异常处理

能力,当不能获取指定的 Count字节时,会抛出一个异常。 Seek 原型为:virtual int __fastcall Seek(int Offset, Word Origin) = 0; 是一个纯虚函数,作用是将 Position移动指定的字节数 Offset,Origin 表明指

定偏移的其实位置,可以为: soFromBeginning从流的起始位置,此时 Offset必须>= 0。 soFromCurrent从当前位置移动后的 Position为 Position + Offset。 soFromEnd从流的结尾,此时 Offset必须<= 0。 SetSize 原型为:virtual void __fastcall SetSize(int NewSize); 提供一个虚拟函数,用于改变流的 Size,该函数也是 Size 属性的存储函数,在

TStream中为空函数,其派生类根据实际情况重载。 Write 原型为: virtual int __fastcall Write(const void *Buffer, int Count) = 0; 一个和 Read相对应的纯虚函数,作用正好相反。 WriteBuffer 原型为:void __fastcall WriteBuffer(const void *Buffer, int Count); 和 ReadBuffer的作用相反。 TStream的派生类在属性和方法上和 TStream 稍有差别,读者要详细了解可参考联

机帮助文件。

2.2.3 Exception

Exception 提供了 VCL 中异常处理的异常基类,这也是在 VCL 中仅有的不是以 T开头命名的类库,从 Exception派生的异常类有几十种,它们覆盖了数学、文件 IO、非

法类型、数据库、内存等等 VCL 类库所涉及到的异常范围。这些派生类也均以 E 开头命

名。尽管这些派生类有者不同的用途和特征,但实际上它们和 Exception 的型式是完全

相同的,方法也是完全相同的(除了构造函数和析构函数),因此我们这里仅仅对

Exception进行一些讲解,关于 C++ Builder 异常处理读者可以参考后面的异常处理

章节。 属性: HelpContext 一个整型的可读可写属性,用以标记上下文相关的帮助内容的 ID,该帮助是和相应

的异常类型关联在一起。 Message AnsiString 类型的可读可写属性,用于 Exception 对话框中显示异常类型的文本

第四章 C++ Builder 语言的扩充 第 187 页

信息,该属性可以是在构造函数中作为参数传递而获取,也可以当做动态的参数而创建,

或者按照动态或静态的格式化参数从流中读取。 方法: 除了继承 TObject的方法以外,Exception 只有构造函数和析构函数,析构函数为

一个虚拟函数,而构造函数有多个不同的型式,分别如下: __fastcall Exception(const AnsiString Msg);

__fastcall Exception(const AnsiString Msg, const System::TVarRec * Args,

const int Args_Size);

__fastcall Exception(int Ident);

__fastcall Exception(int Ident, const TVarRec * Args, const int Args_Size);

__fastcall Exception(const AnsiString Msg, int AHelpContext);

__fastcall Exception(const AnsiString Msg, const System::TVarRec * Args,

const int Args_Size, int AHelpContext);

__fastcall Exception(int Ident, int AHelpContext);

__fastcall Exception(int Ident, const System::TVarRec * Args, const int

Args_Size, int AHelpContext);

这些构造函数有不同的构造方式,供不同的派生类使用,Exception 的构造不能使

用 new关键字,否则不会产生例外,取而代之的是 throw关键字,如: throw Exception("My error message");

将构造一个 Exception 异常类,抛给当前的执行代码被 catch 等关键字中的代码捕

获并处理,其 Message信息为"My error message"。

2.2.4 TPersistent

TPersistent 为 TObject 派生类中最为重要的一个类,所有的 VCL 可视化类库都

从它派生而来,TPersistent同 TObject一样,没有任何成员,但 TPersistent扩

展了 TObject的固有特征,是其具备以下特点: n 定义了针对非发布的数据存储到流或者从流载入的过程。 n 提供了为属性赋值的方法。 n 提供了从另一个类中拷贝赋值的方法。 TPersistent 最大的特点就是提供了流的存储和载入机制,从 TPersistent 派生

的类中都继承了两个重要的方法 SaveToStream 和 LoadFromStream。这些方法向对象

提供固有特性。例如:当窗体设计器需要创建 DFM文件(存储窗体及其组件信息的文件)

时,它循环检查窗体的组件数组,并对其中所有的组件都调用 SaveToStream 方法。每

个组件都“知道”如何将被改变的属性写入流中(如文本文件)。反过来,若窗体设计器

需要从 DFM文件中装入组件属性时,它循环检查窗体的组件数组,并对其中所有的组件都

第四章 C++ Builder 语言的扩充 第 188 页

调用 LoadFromStream 方法。因此,任何 TPersistent 的派生类都有保存状态信息和

需要时还原它们的能力。 TPersistent扩充的方法有: Assign 原型为:virtual void __fastcall Assign(TPersistent* Source); 提供从相似的类中拷贝对象的方法,前面说过在 VCL类库中没有类似 Ansi C++中的

拷贝构造函数,Assign可以看作是弥补这一缺陷而引入的。这是一个虚拟函数,不同的

派生类需要根据实际情况重载该函数。 使用 Assign可以从其他对象中复制属性或者其他的特征,通常使用如下: Destination->Assign(Source);

这一个语句将告诉 Destination对象从 Source复制内容 Destination中。 多数从TPersistent派生的类都需要重载Assign函数来处理从相似的对象中拷贝

内容,当一个类重载 Assign函数时,遇到 Source中有自身不能处理的成员或者属性,

需要调用父类的 Assign函数进行处理。如果 Destination 中没有重载 Assign 函数,

该方法会通过 TPersistent 的 Assign函数来调用 Source 类的 AssignTo函数进行

处理,Source类中如果没有定义 AssignTo函数,则 TPersistent 的 Assign 函数会

抛出一个异常 EconvertError。 通常情况下 Destination = Source和 Destination->Assign(Source)是完

全不同的,前者是一种引用的拷贝,两个类完全相同,而且是指向同一个对象,而后者是

对内容的拷贝,执行的结果是 Destination和 Source 是两个对象,而且这种操作允许

对象的类型不同,这取决于程序员这设计 Assign方法时的心情。比如 TBitmap就可以

通过 Assign拷贝 TJPEGImage,这就是由 TJPEGImage组件的设计师决定的。 在另一种情况下 Destination = Source和 Destination->Assign(Source)

是等价的,这只出现在一个类的属性也是 VCL 对象,并且这个属性的存储函数使用了

Assign方法来设置属性的存储值。这种要求是必须对,因为在一个组件中属性为 VCL 对

象时,实际上这两个对象构成了一种逻辑包含关系,属性只是表达了这种关系,而不是需

要根本上作一份拷贝。 AssignTo 原型为:virtual void __fastcall AssignTo(TPersistent* Dest); 提供向指定的 Dest目标对象复制内容的方法,正如在 Assign方法中提到的,使用

AssignTo方法可以扩展某些已有类的 Assign方法。 当创建一个新的 VCL类时,需要重载 Assign 函数,对每一个已经存在的 VCL类库

都要逐一考虑,若希望能够从其中复制数据的类,就要在 Assign方法中加入相应的代码,

但是对于逆向过程,希望从已有的类中拷贝新建类的数据时,由于已有类的 Assign 方法

没有定义对新建类的 Assign处理代码,TPersistent 会自动调用新建类的 AssignTo方法。因此,在一个新建的 VCL类中,需要针对每一个需要相互能够复制数据的已有类的

重载 AssignTo方法。

第四章 C++ Builder 语言的扩充 第 189 页

TPersistent 提供了 Assign和 AssignTo 方法的相互协作关系,这使得一个新建

的 VCL类不需要依赖于已有类库的方法和特征而实现之间数据的相互复制。由此也可以看

出 VCL类库中的这种机制比 C++中的拷贝构造函数功能也要强大得多。 比如两个不同的类,A是 VCL中固有的类,B是用户新建的类: B->Assign(A);

若 B中重载了 Assign 方法来处理复制 A类的数据,则调用 B的 Assign方法并正

确返回,相反的: A->Assign(B);

由 于 A 中 预 先 没 有 定 义 处 理 复 制 B 的 Assign 代 码 , 会 最 终 调 用

TPersistent::Assign方法来处理,TPersistent::Assign则自动调用: B->AssignTo(A);

来处理这个过程,如果 AssignTo没有定义复制到 A的代码,TPersistent 将抛出

异常。 DefineProperties 原型为:virtual void __fastcall DefineProperties(TFiler* Filer); 提供了一个用于存储和载入未发布数据的接口。 在 TPersistent 的派生类中可以通过重载这个函数来未发布的数据到流中,缺省的

情况下,存储一个对象时,只将所发布的属性进行存储,而读取时,也是将发布的属性从

流中读出,并赋给相应的属性,希望能够对未发布的数据进行存取就需要重载

DefineProperties方法,但并不是必须要重载。 重载该方法需要注意: n 需要调用父类的 DefineProperties方法。 n 调用 filer对象的 DefineProperty方法。 n 调用 filer的 DefineBinaryProperty方法。 GetNamePath 原型为:DYNAMIC AnsiString __fastcall GetNamePath( ); 用于返回 Object Inspector中对象的名称的信息,这是一个内部使用的函数,用

户不需直接调用,对于 components,该函数返回其名称;对于 TCollectionItem对

象,该函数返回其宿主组件、所书属性以及 collection中的 Index索引值。 GetOwner 原型为:DYNAMIC TPersistent* __fastcall GetOwner(void); 用于返回一个对象的所有者。 GetOwner 被 GetNamePath 使用,以获取 persistent 对象的拥有者,

第四章 C++ Builder 语言的扩充 第 190 页

TPersistent本身的 GetOwner返回为 NULL。 对于 TCollection对象,GetOwner 方法返回的是 collection的拥有者;对于

collectionitems,该方法返回的是 collectionitems 嵌入的 collection;对于

组件,该方法返回的是 Owner属性。 关于 TCollection以及 TCollectionItems,详见后面内容。

2.3 TPersistent 分支

在 TPersistent分支中包含的类有以下 28个: TGraphic TComponent TCollectionItem TCollection

TStrings TClipboard TIconOptions TListItem

TListItems TMonthCalColors TParaAttributes TTextAttributes

TTreeNode TTreeNodes TSizeConstraints TDataLink

TColumnTitle TControlScrollBar

TGraphicsObject TCanvas TPicture THTMLTagAttributes

TCaptionAttributes TGridAttributes TGridRowAttributes TBaseArray

TOleServer TAbstractSocket

在这些派生类中,比较重要的类有: n TGraphicsObject,图形对象的基类,封装了 Windows 图形对象:TBrush、

TFont、TPen。 n TGraphic,图标、位图、图元文件等能存储和显示可视图像的对象的基类,封

装了 TBitmap、TIcon和 TMetaFile。 n TCollection 是 TCollectionItem容器的基类。 n TCollectionItem,包含特殊预定义项的索引集。 n TComponent,所有组件的共同基类。 下面就这几个类进行简单的介绍,了解其它的派生类请读者参考联机帮助。

2.3.1 TComponent

TComponent 是所有 VCL 组件的共同基类,组件就是在设计时可在窗体中操纵的对

象。虽然 VCL名为可视组件库,但其实包含的大部分类库为非可视化的组件。VCL组件都

是固有的对象,继承了 TPersistent的特征,其功能主要有: n 可出现在组件面板上并可在窗体设计器中修改。 n 可拥有和管理其他的组件。

第四章 C++ Builder 语言的扩充 第 191 页

n 增强的流及文件管理能力。 n 可通过 New Objects 对话框中 ActiveX 页的向导程序转换成 ActiveX 控件

或其他 COM对象。 TComponent 扮演了一个所有组件都可插入的标准“总线”的角色。TComponent

通过 Name和 Owner属性及一些方法规定组件在设计时的行为。所有派生于 TComponent的类都有 Name和 Owner属性。Owner属性值为组件的拥有者,组件的拥有者负责该决

定该组件的销毁时刻。下面对 TComponent 的属性、方法作个简单介绍,TComponent中没有事件。

TComponent 中的属性、方法都比较多,从其派生的组件也都有着非常多的属性和方

法,由于篇幅的原因,这里不可能一一介绍,而且有一些内部使用的方法和属性等,也不

是经常使用的,这里仅对比较重要的和常用的属性、方法进行简单介绍,后面将要介绍的

类库同样只对重要和常用的属性、方法和事件作个简单介绍。 属性: ComponentCount 一个整型只读属性,表明该组件所拥有的组件的数量,TComponent 的特征之一就是

可以拥有和管理其他组件,这是依赖于 Owner属性的,若组件 A的 Owner属性为 B,则

B就是 A的拥有者,当 B销毁时,B负责将 A也销毁。B的 ComponentCount属性记载

了所有 Owner为 B的组件数量。 ComponentIndex 一个整型的可读可写的属性,用于记录组件在其拥有者中组件数组的索引位置,通常

联合 Components同时使用,用于遍历一个组件拥有的所有组件。ComponentIndex 的

最大取值为其拥有者 ComponentCount属性减 1。 Components 一个 TComponent数组属性,只读。记录了一个组件所拥有的所有组件。 ComponentState 一个 TComponentState 类型的只读属性,用于记录当前组件的状态,以表明允许

或禁止某些操作或行为,这是由组件内部负责更新和设置的,对于新设计的一个组件,如

果 有 需 要 禁 止 的 行 为 就 需 要 针 对 ComponentState 属 性 添 加 相 应 代 码 。

TComponentState的状态有以下取值: csDesigning csDestroying csFixups csFreeNotification csInline csLoading csReading csUpdating csWriting csDesignInstance 详细信息参阅联机帮助。 ComponentStyle 管理一个组件行为的属性,TComponentStyle类型,只读。 TComponentStyle 可供选择的取值有 csInheritable, csCheckPropAvail。

该属性用于决定一个组件是否能够被继承,或者需要检测属性的可读性。 Name AnsiString 类型的可读可写属性,用以记录一个组件的名称。也可以通过设置该属

第四章 C++ Builder 语言的扩充 第 192 页

性来改变一个组件的名称。需要注意的是,一般情况,设计时在 C++ Builder 的 IDE中创建的组件都会在相应 Form 的__publish 定义域,而且其声明的指针标识符和组件

的名称相同,但这是两个不同的概念,一个是变量名称,只是一个标识符,而另一个是字

符型的成员,记录这一个组件的名称信息。Name 属性在运行时是可变的,而指针变量会

在编译时被替换成相应的存储器地址。 Owner 一个 TComponent类型的只读属性,用来记录一个组件的拥有者。 Owner和拥有者的 Components 属性共同构成了一种逻辑包含关系,一般情况,设

计时创建的组件,其 Owner就是容纳这个组件的 Form窗体、或者 DataModule 等组件,

而 Form或 DataModule的 Components属性中就一一记录着每一个所拥有的组件。 Owner在设计时和运行时都不允许改变,一个组件在创建时就必须指定其拥有者,一

旦建立这种关系,直到这个组件被销毁都是不能改变的。 在程序中,设计时创建的组件是必须依赖于 Owner 而存在,当一个组件被销毁时,

它的 Components 属性中所记录的所有组件也将一并销毁。一个程序中,当某个 Form被销毁时。这个 Form 所容纳的组件随着一起销毁,程序中所有的 Form 其 Owner 均为

Application 对象,一个程序结束时,Application 将被销毁,所包含的所有 Form也将被销毁。

程序中允许动态创建 VCL类库的对象其 Owner为空,但是程序中必须有程序员自行

负责对该组件进行销毁,否则一旦创建直到程序运行结束时,这个组件所占用的内存才会

被释放。 Tag 一个整型的可读可写属性,这个属性并没有特定的含义但很有用处的属性,为程序开

发者保留了一个 4字节的成员,用来完成一些特定功能,比如利用该属性存储一个同类型

的指针,就可以构成一个单向链表等等。 方法: ChangeName 原型为:void __fastcall ChangeName (const AnsiString NewName); 改变私有的内部 Name属性的存储值,NewName 为要改变的新名称,在程序中不能直

接使用该方法,而是使用 Name属性进行改变,该方法不能重载。 DefineProperties 原型为:virtual void __fastcall DefineProperties(TFiler* Filer); 这是从 TPersistent 派生而来的方法,作用相同,在 TComponent中,该函数用

来构造了两个虚拟属性,或叫做伪属性:Top 和 Left,所有的组件在设计时是允许可见

的,设计时创建的组件会在 dfm文件中保存它们的位置 top和 left,然而这两个属性在

运行时是不需要的,因为只有控件在运行时才是真正可见的。 DestroyComponents 原型为:void __fastcall DestroyComponents(void); 用于销毁所有拥有的组件,该方法不需直接调用。 Destroying

第四章 C++ Builder 语言的扩充 第 193 页

原型为:void __fastcall Destroying(void); Destroying 设置 ComponentState 属性到 csDestroying 标记,并且将所拥有

的组件的 ComponentState属性也一并设置,一般不需直接调用。 ExecuteAction 原型为: DYNAMIC bool __fastcall ExecuteAction(TBasicAction* Action); 用来调用一个 Action动作,并将该组件作为这个动作的目标。这个调用过程遵循以

下原则:若 Action的 OnExecute 事件没有指定处理函数,将会调用 Application 的

ExecuteAction 方 法 , Application 的 ExecuteAction 方 法 首 先 触 发

Application 对象的 OnActionExecute 事件,若该事件没有处理函数,则触发

OnExecute 事件,若 OnExecute 同样没有处理函数时,将会调用当前活动控件的

ExecuteAction方法。 FindComponent 原型为: TComponent* __fastcall FindComponent(const AnsiString AName); 用来查找指定名称的组件是否包含在 Components 内。由于 Object Pascal的缘

故,这个函数中并不区分 AName参数的大小写。 TComponent 原型为:__fastcall virtual TComponent(TComponent* AOwner); TComponent 的构造函数,所有的组件在设计时创建的,都不要在程序中使用代码进

行实例化,这个工作在程序运行时会自动完成。运行时创建的组件必须使用 new关键字来

实例化并指定 AOwner参数作为组件的拥有者,允许 AOwner为空。

2.3.2 TGraphic

TGraphic是一个抽象基类,不能被实例化,但它定义了图形对象的许多共同特征。

TGraphic的派生类对很多方法进行了重载来适应不同格式的图像。从 TGraphic 派生的

图像类支持3种格式bitmap、icon和 metafile等,另一个与之相关的类是 TPicture,如同一个容器一样,使用 TPicture可以在不知道格式的情况接受 TGraphic支持的格

式。 属性: Empty 布尔类型的只读属性,记录着图像是否为空。 Height 整型可读可写的属性,以像素为单位,表示着图像的高度。 Modified 布尔类型可读可写的属性,记录了图像文件是否被编辑或改变,在 TGraphic的派生

类中,只有 TBitmap 中这个属性是有效的,其他的派生类不管是否被改变,Modified都不能为 true。

Palette

第四章 C++ Builder 语言的扩充 第 194 页

HPALETTE类型的可读可写属性,定义了图像的调色板,若图形格式不需要调色板,

该属性可以为空。 PaletteModified 布尔类型的可读可写的属性,记录着调色板是否被改变。 Transparent 布尔类型的可读可写的属性,决定图像是否为透明的,在 metafile和 icon格式中,

这个属性无效,因为这些格式始终都要求是透明的,但象 bitmap格式的图像,是否透明

要取决于该属性。 Width 整型可读可写的属性,以像素为单位,表示者图像的宽度。 方法: LoadFromClipboardFormat 原型为:virtual void __fastcall LoadFromClipboardFormat(Word AFormat, int AData, HPALETTE APalette) = 0; 一个纯虚函数,用于从有效的剪贴板中载入图像,派生类需要重载。 LoadFromFile 原型为: virtual void __fastcall LoadFromFile(const AnsiString FileName); 虚拟函数,用于从指定的文件中载入图像。 LoadFromStream 原型为: virtual void __fastcall LoadFromStream(Classes::Tstream *Stream) = 0; 纯虚拟函数,提供从流中载入图像的方法。 SaveToClipboardFormat 原型为:virtual void __fastcall SaveToClipboardFormat(Word &AFormat, int &AData,HPALETTE &APalette) = 0; 纯虚拟函数,和 LoadFromClipboardFormat 作用相反,用于复制图像到剪贴板板。 SaveToFile 原型为: virtual void __fastcall SaveToFile(const AnsiString Filename); 虚拟函数,和 LoadFromFile的作用相反。 SaveToStream 原型为:virtual void __fastcall SaveToStream(Classes::TStream* Stream) = 0; 纯虚拟函数,作用和 LoadFromStream相反。 事件: OnChange TNotifyEvent类型的事件,当图像被改变的时侯触发。

第四章 C++ Builder 语言的扩充 第 195 页

OnProgress 触发于图像被处理的过程,原型如下: enum TProgressStage {psStarting, psRunning, psEnding};

typedef void __fastcall (__closure *TProgressEvent)(System::TObject*

Sender,TProgressStage Stage, Byte PercentDone, bool

RedrawNow, constWindows::TRect &R, const AnsiString Msg);

__property TProgressEvent OnProgress = {read=FOnProgress,

write=FOnProgress};

使用 OnProgress 事件可以在图像被处理的过程中及时的向用户回馈到到前的处理

状况,如使用 ProgressBar来显示进度等情况。

2.3.3 TCollection

在 VCL类库中,TCollection 和 TCollectionItem是两个比较重要的类,它们

并不是组件,也不能容纳在 Form 中,但是设计时仍然是可用的,有专门

TCollectionItem的编辑器可以对其进行编辑,在 Object Inspector中也是可见的,

而且设置的属性也会被相应的存储在 dfm文件中,但和组件不同的是它们在 Form的头文

件中并不存在相应的声明,因此相应的在代码中的引用也不能象组件一样直接使用名称,

比如在 TStatusBar中: 有一个 TStatusPanels 类型的属性 Panels,其 Items 属性容纳了众多的

TStatusPanel对象,程序中使用 StatusPanel时只能采用如下代码: StatusPanel->Panels->Items[1]->Text = "Test"; 而不能使用象:

StatusPanel1->Text = "Test";

等类似的代码,因为在 Form的头文件的声明中不会包含 TStatusPanel类型的变

量。

在上面的代码中,TStatusPanels 是 TCollection 的派生类,TStatusPanel是 TCollectionItem的派生类,TCollection 是 TCollectionItem的一个容器类,

通过这两个类,在 VCL组件之间建立了另一种逻辑包含关系,而且是使用的比较多的一种

关系,要深入的理解这中包含关系,可以仔细分析 StatusPanel->Panels的使用以及后面

关于 C++ Builder资源的相关内容,这里先就 TCollection 的重要属性和方法作个简

单介绍。 属性: Count 整型只读属性,返回 Collection 中项目的数量。所有的 CollectionItem 项目

都记录在 TCollection的 Items属性中,以数组型式存放。 ItemClass 一个 TMetaClass类型的只读属性,用以记录 Items中项目的类型。 Items

第四章 C++ Builder 语言的扩充 第 196 页

可读可写的 TCollectionItem 指针类型属性,存储 TCollection 中容纳的

CollectionItem,可以以数组的方式访问每一个 Items项目。 方法: Add 原型为:TCollectionItem* __fastcall Add(void); 该方法用于创建一个新的 TCollectionItem 并且将其添加到 Items数组中,方法

的返回值即新建的 Item。该方法会自动更新 Count、Items等属性。 BeginUpdate、EndUpdate 原型为:void __fastcall BeginUpdate(void); void __fastcall EndUpdate(void); 这两个方法用于冻结和解冻屏幕的刷新。通常情况下 TCollection 和

TCollectionItem 组合的对象非常容易和屏幕的显示内容关联在一起,而且每一个更新

动作都会触发对屏幕的刷新,但这在某些情况下是不希望的,比如批量更新一个 Items时,屏幕会频繁闪烁,而且执行效率也会随之降低,这时候,可以使用 BeginUpdate 将

屏幕刷新挂起,等批量更新结束后再使用 EndUpdate恢复屏幕刷新。 Clear 原型为:void __fastcall Clear(void); 用以清除 Items中的所有 CollectionItem。 Delete 原型为:void __fastcall Delete(int Index); 用于删除指定索引号的 Items元素。 Insert 原型为:TCollectionItem* __fastcall Insert(int Index); 该方法用于创建一个新的 TCollectionItem 并且将其插入到 Items 数组中,与

Add 方法不同的是,该方法的 Index 参数指定了一个索引号,新建的 Item 将插入到相

应的位置,Add则是添加在数组的末尾。 TCollection对象中没有事件。

2.3.4 TCollectionItem

属性: Collection 一个 TCollection 类型的只读属性,指定该 TCollectionItem 的容器

TCollection。这个属性在构件 TcollectionItem 时就被指定,之后不允许改变。

TCollectionItem 的 Collection 属性和 TCollection 的 Items属性,相互构成了

一种特定的逻辑包含关系。 DisplayName 一个 AnsiString 类型的可读可写属性,这个属性类似于组件的 Name 属性,它是

一个 Item出现在 Collection editor中的名称,Collection editor 专门用于编

辑 TCollection 的 Items 属性。该属性不能象 Name 一样在代码中引用,只能用于

第四章 C++ Builder 语言的扩充 第 197 页

Collection 编辑器,因为 Item在程序的头文件中并没有声明的原型,程序在运行时根

据 dfm文件中的相关信息进行创建并初始化。 Index 整型可读可写属性,用于标记一个 TCollectionItem 在其容器的 Items属性中的

位置,该属性允许改变,这意味着运行时可以改变一个 Item 在 Items 数组中的位置,

Index属性的改变不会影响到其他 Item的相对位置关系,比如: A、B、C、D、E五个 Item顺序排列在 Items数组中,它们的 Index依次为 0、1、

2、3、4,当 E的 Index改为 0时,E在 Items中的位置将被移动到第一个,A、B、C、D将会整体向后移动,A、B、C、D、E的 Index依次为:1、2、3、4、0。

ID 整型只读属性,是 TCollectionItem 永久的唯一的序列号,通常 ID 会和 Index

一致,但是当一个 TCollectionItem 被进行了删除、移动操作之后,它们将会不再一

致,Index是 TCollectionItem在 Items数组中的索引值,就是 Item在 Items数

组中的位置,ID是序列号,是一个永久的标识,比如:当一个 Item被删除后,它的 ID将不会重新被别的 Item使用,Index则可以。

方法: Changed 原型为:void __fastcall Changed(bool AllItems); 用于更新 collection 的其他项目来反映出更新的结果,该方法建立了处于并列关

系的 TCollectionItem 对象之间的一个联系的途径,这个方法是自动调用的,但在某

些派生类中可能有必要直接调用来强性完成 Items的更新任务。 GetOwner 原型为:virtual TPersistent __fastcall GetOwner(void); 和组件的 GetOwner类似,但不同的是,TCollectionItem 对象本身并没有 Owner

属性,它多返回的 Owner 指的是 TCollectionItem 对象的容器,也可以使用

Collection 获取 Owner容器,这里的 Owner并不象组件的 Owner,需要负责对其拥

有的组件的销毁工作。 关于 TCollection 和 TCollectionItem 这种集合及容器关系的类很多,它们都

是从这两个类派生而来,有着不同的用途和功能,差别也比较大,这里不可能一一介绍,

读者需要详细了解请参考联机帮助。这也是 C++ Builder 中一种非常重要的逻辑包含关

系,读者需要结合实际使用仔细理解。

2.3.5 TGraphicsObject

TGraphicsObject是一个抽象基类,它封装了 Widnows的图形对象,包括 TBrush、TFont和 TPen。这几个类都是 Windows 的基本会图工具。一方面,图形对象是提供给

应用程序使用的封装后的对象,另一方面,图形对象还将负责爱 Windows 中共享图形资

源的任务,下面就 TGraphicsObject的属性、方法和事件作个简单介绍。 属性: OwnerCriticalSection

第四章 C++ Builder 语言的扩充 第 198 页

这个属性的定义如下: typedef _RTL_CRITICAL_SECTION TRTLCriticalSection;

__property Windows::PRTLCriticalSection OwnerCriticalSection = {read

=FOwnerLock, write=FOwnerLock};

该属性用于获得图形对象的线程锁定状态,在一个多线程的程序,Canvas对象都有

可能被每一个线程访问,为了防止多个线程同时对一个 Canvas进行操作而引起意外的竞

争,C++ Builder中可以对 Canvas进行锁定,OwnerCriticalSection 属性就是用

于这个目的的。 在一个 Graphic Object 对象对 Canvas 进行操作的时候,不能改变这个属性,可

以使用 Lock方法和 UnLock方法对一个 Canvas对象进行锁定和解除锁定。 方法: Changed 原型为:DYNAMIC void __fastcall Changed(void); 当一个 Graophic Object 的属性被改变时,用于执行 OnChanged 事件。所有派

生类的属性发生改变时都会调用这个方法。 Lock 原型为:void __fastcall Lock(void); Lock 方法用于阻止同时执行的其他线程对相关的 Canvas 对象进行操作,在使用

UnLock方法对这个 Canvas释放之前,其他线程均不能进行操作。 Lock 方法是对多线程协作的一种保护,而且允许嵌套调用,在最后一层的嵌套的

Unlock被调用之前,Canvas不会真正的被释放。 这个方法和 OwnerCriticalSection 属性相关,当 OwnerCriticalSection 属

性没有被设置的时候,Lock方法不会真正的锁定 Canvas。TCanvas 对象会自动的设置

OwnerCriticalSection属性。 Unlock 原型为:void __fastcall Unlock(void); 和 Lock函数对应,用于解除 Lock锁定的 Canvas。 事件: OnChange 一个 TNotifyEvent类型的事件,原型如下: __property Classes::TNotifyEvent OnChange = {read=FOnChange, write=FOnChange}; 当一个 Graphic Object 发生改变的时候触发该事件,Graphic Object 的实行

将反映出这些变化。

2.4 TComponent 分支

TComponent 分支是 VCL 中一个重要的分支,它是所有组件的共同基类。直接由

TComponent派生的组件并不需要可视化的接口。在这一分支中包含的类主要有: n TMainMenu为窗体提供菜单栏及相应的下拉菜单的类。

第四章 C++ Builder 语言的扩充 第 199 页

n TTimer包含 Windows API中的计时器函数的类。 n TOpenDialog 、 TSaveDialog 、 TFontDialog 、 TFindDialog 、

TColorDialog等,是通用 Windows对话框类。 n TActionList,包含组件或控件,如菜单项和按钮的动作列表的类。 n TScreen,负责管理应用程序创建的窗体和数据模块,当前的活动窗体及其上的

活动控件、屏幕尺寸和分辨率,应用程序使用的光标和字体等内容的类。 但也有更多的类不是直接使用的最终类,TComponent的派生类有以下 37个。 TControl TbasicAction TCustomActionList TADOCommand

TCustomConnection TdataSet

TCustomApplicationEvents TWebApplication TBasicAction TCustomContentProducer

TCustomImageList TAppletApplication TDataModule TField

TDataSource TBatchMove TDataSetUpdateObject TSession

TDdeClientConv TDdeClientItem TDdeServerConv TDdeServerItem

TCommonDialog TTimer TApplication TScreen

TCustomObjectBroker TMenu TMenuItem TWebButton

TWebControlGroup TWebForm TWebDataDisplay TCustomDataStore

TDecisionSource TServiceApplication

TXMLBroker

这些派生类里面,比较重要的有: TControl:所有控件的基类,定义了控件所具备的基本特征。 TDataSet、所有数据库表集的基类,包括 BDE、ADO等数据库的组建。 TApplication应用程序的类,大多数应用程序应该包含一个 TApplication 对象

或者与之相当的对象,比如 TServiceApplication、TWebApplication等, TField字段类,与 TDataset对象配合使用,用于对数据库表的操作。 在 TComponent 的派生类中,还有很多类都是比较有用,但是限于篇幅,这里不能

详细说明,仅就上面提到的类进行简单说明。

2.4.1 TApplication

TApplication类是标准的 Windows 应用程序类,这个类是非常常用的,却往往被

人了解的甚少,原因是 TApplication并不出现在组建面板中,而且 Application的

第四章 C++ Builder 语言的扩充 第 200 页

属性也不会出现在 Object Inspector 内,由此以来 TApplication的许多功能并没

有被真正发挥出来。 在一个 Windows程序中,TApplication 封装系统对应用程序所要求的一些基本操

作,如:运行、持续和关闭程序所要进行的基本操作。在 Web 应用程序中,相应的类为

TWebApplication,服务程序为 TServiceApplication等等。 TApplication类应该具有以下基本功能: n Windows的消息处理能力。 n 提供了上下文相关的在线帮助。 n 菜单和加速键的处理。 n 异常处理能力。 n 包含了操作系统定义的基本构成,如 MainWindow、WindowClass等等。 在使用 C++ Builder创建一个应用程序的时候,IDE 会自动根据需要创建的程序类

型来声明相应的 Application 类实例。在 TApplication组件中包含了一些程序基本

的事件如:OnActionExecute、OnActionUpdate 等,由于 TApplication 并不出现

在组件面板,属性和事件也不出现在 Object Inspector,这些事件也往往被忽略,若

希望能够在 IDE中使用这些事件可以通过创建一个 TApplicationEvents组件对象,

来将 TApplication的事件映射到 TApplicationEvents中。 接下来就 TApplication的属性、方法和事件作以简单介绍,由于 TApplication

也是一个比较“庞大”的类库,这里对一些含义比较明确,通过名称就能够理解其含义的

或者使用的较少的属性、方法不再赘述,仅对一些重要的属性、方法和事件作个介绍。 属性: Active 指定了程序是否处于激活状态。是一个 bool类型的只读属性。 当程序的,某个 Form或者程序获得焦点时,程序便处于激活状态,Active 为 true,

否则为 false,当程序结束之前,也会将 Active设置为 false。 Handle HWND 类型的可读写属性。这个属性提供了程序主窗口句柄访问的接口,在使用

Windows API 函数创建一个窗口或者 MessageBox 的时候,需要指定一个 Windows Handle作为窗口的父窗口,使用 Application 的 Handle属性可以提供给 API 一个宿

主的窗口。在后面的例子 Sells中,就使用了 Handle 属性作为动态连接库的一个参数。 MainForm TForm类型的只读属性。指定了应用程序的主窗口,一个应用程序只能有一个窗口为

主窗口,主窗口会代表着整个应用程序的一些特征,比如最小化、最大化等。在 C++ Builder中,主窗口总是 Application 创建的第一个 Form,当这个 Form被关闭时,

整个程序也就结束了。MainForm在运行时是不能改变的,设计时可以通过 Option 选项

来设置调整那一个 Form为主窗口。 ShowMainForm bool 类型的可读写属性。指定应用程序在启动时是否显示主窗口,缺省的情况下,

该属性始终为 true,程序在启动的时候总是显示主窗口,如果一个程序希望在启动的时

第四章 C++ Builder 语言的扩充 第 201 页

候不显示主窗口,则可以通过在 WinMain 函数中 Application->Run以前设置该属性

为 false,并且将主窗口的 Visible属性设置为 false即可。 Title AnsiString 类型的可读写属性。用于标识应用程序的标题,在 Windows 的任务栏,

程序的图标上显示的就是 Title 的字符串,缺省的状态下,一个用于程序或者动态连接

库的 Title就是文件名称,可以在 Project | Options 对话框中修改这个缺省值,也

可以在程序运行时该这个属性,当一个可执行程序的 Title 为空的时候,可执行执行程

序不会出现在 Ctrl+Alt+Del按键组合的对话框中。 方法: BringToFront 原型为:void __fastcall BringToFront(void); 设置最后一个激活的窗口为桌面的最顶端窗口,不会被其他窗口所覆盖。 CreateForm 原型为:void __fastcall CreateForm(System::TMetaClass * InstanceClass, void *Reference); 用于创建一个新的窗口,程序员实际上不需要直接使用这个方法,在 C++ Builder

中,CreateForm 方法是 IDE自动增加到 WinMain 函数中的,但代码中也允许这样使用,

同样在程序中动态创建一个窗体,使用 new关键字更符合习惯一些。 Initialize 原型为:void __fastcall Initialize(void); 提供了一个用于初始化象 OLE等子系统的机会,在每一个基于 VCL类库的出现中,

Initalize都是第一个被调用的方法,这个方法会自动调用 InitProc过程,实际上缺

省的,在用于程序中这个方法什么也没有作,因为缺省的 InitProc指针是空的,可以通

过在头文件中增加关于 InitProc的定义或者在程序中定义自己的初始化过程,并将其值

赋给 InitProc指针。 一个程序中只能有一个 InitProc,当多个头文件都包含 InitProc 定义时,只有最

后一个出现的定义是有效的,如果一个程序中确定没有 InitProc,则可以将

Initialize安全的从应用程序中删除。 MessageBox 原型为:int __fastcall MessageBox(const char * Text, const char *Caption, int Flags); 这是一个提供显示用户消息对话框的方法,基本和 API中的 MessageBox一样,不

同的是在这个方法中不许要指定对话框的父窗口或进程实例,其他的和 API 中的

MessageBox一样。 Run 原型为:void __fastcall Run(void); 该方法通常在 IDE生成的代码中,实际上在 WinMain函数中,这个方法是最后调用

的,直到程序结束的时候,Run方法才会返回,该方法主要是建立 Application 对象的

消息处理机制。通常需要对程序作的一些特殊处理需要在 Run方法以前操作或者调用。

第四章 C++ Builder 语言的扩充 第 202 页

Terminate 原型为:void __fastcall Terminate(void); 用于结束用于程序。该方法调用了 Windows API函数 PostQuitMessage来向程

序发送关闭指令,并不意味着程序会立即结束,而是按照正常的顺序和关闭过程来关闭程

序,比如在数据没有存储的时候,可以通过代码来阻止 Terminate方法的进一步完成关

闭工作。 事件: OnActivate TNotifyEvent 类型的事件,当用于程序被激活时触发。OnActivate 一般发生在

程序初始化后并正常运行时或者用于程序的焦点从别的用于程序转移到该用于程序的任

意一个窗体的时候。填写该事件的处理代码可以在程序激活时进行一些特殊处理。 OnDeactivate TNotifyEvent类型的事件同 OnActive相反,当一个用于程序失去焦点的时候被

触发。 OnException 原型如下: typedef void __fastcall (__closure *TExceptionEvent)(System::TObject* Sender, Sysutils::Exception* E); __property TExceptionEvent OnException = {read=FOnException, write=FOnException}; 当一个没有被定义了处理过程的异常发生时触发。这是一个比较有用的事件,因为在

C++ Builder 中,对于用户没有处理的异常会有缺省的处理行为,比如在 try 语句以外

发生的异常,C++ Builder 会自动的向用户报告一个错误和相应的错误类型,通常这个

错误的信息是英文输出的,若希望程序不要出现英文界面就可以使用这个事件对没有在代

码中处理的异常进行简单信息输出等处理。 OnMessage 原型如下: typedef void __fastcall (__closure *TMessageEvent)(tagMSG &Msg, bool &Handled); __property TMessageEvent OnMessage = {read=FOnMessage, write=FOnMessage}; 这个事件在用于程序接收到 Windows消息时触发,这是 C++ Builder消息处理的

一大特色,通过这个事件可以使程序员获得 Windows 消息的最高控制权,这是一个非常

有用的事件,但是使用中也必须谨慎,因为每一个 Windows 消息都会触发该事件,对于

每秒种出现上千次的消息,代码如果处理不当很可能造成正个程序的性能下降。此外应该

注意的是该事件只对 Windows 消息队列中的消息发生作用,而对于不在消息队列中的消

息,比如使用 SendMessage函数发送的消息则不起作用。 OnShortCut

第四章 C++ Builder 语言的扩充 第 203 页

这是和 OnMessage事件比较类似的一个事件,原型如下: typedef void __fastcall (__closure *TShortCutEvent)(Messages::TWMKey &Msg, bool&Handled); __property TShortCutEvent OnShortCut = {read=FOnShortCut, write=FOnShortCut}; 和 OnMessage 不同的是这只是对 WM_KEYDOWN 消息的一个处理,严格的说是对键

盘击键消息的处理,因为 WM_KEYDOWN消息还包括了鼠标的消息。 使用这个事件可以过滤调一些不需要的消息,而只对键盘消息进行处理,比如定义额

外的热键等。这个事件也是具有最高控制权的,它发生在所有控件的 OnKeyDown 和

OnKeyPress 之前。在本书所附的例子 Sells中使用了 OnMessage对消息进行全局处

理,实际上使用 OnShortCut 会获得更好的效果和更高的程序性能,但出于例子的目的

而采用了OnMessage事件进行处理,有兴趣的读者可以自行修改代码,使用OnShortCut事件。

2.4.2 TField

TField 也是一个比较重要的基类,它是所有数据库字段类型的基类,在 C++ Builder 开发的数据库程序中,TField 扮演着极为重要的角色,通常情况 TField 和

TDataSet中的 Fields属性构成了一种逻辑包含关系,这种逻辑关系和 TCollection与 TCollectionItem 之间的关系比较类似,但也有所不同,因为 TField是一个组件,

因此设计时定义的TField对象在头文件中是有声明原型的,可以在程序代码中直接引用,

而不需要通过宿主对象来间接访问。TField对象并不出现在 Form或其它类似的容器控

件中,而使用了和 CollectionItem 编辑器相似的容器进行可视化编辑,表现形式上和

TCollectionItem非常类似,但这是完全不同两种类型。 在 C++ Builder中所有的数据库访问都是通过 TField 来完成的,尽管有些时候并

不需要明显的在设计时创建 TField对象(运行时 C++ Builder会根据需要自动创建),

但在大多数情况下,设计时创建 TField对象会对应用程序的设计带来很大的方便,本书

的例子Sells中就使用了从TField派生而来的对象,读者可参考阅读。下面先就 TField的一些重要的属性、方法和设计作以简单介绍。

属性: Calculated bool类型的可读写属性,用于指定该字段是否是由 OnCalcFields 事件中的代码计

算而获得。缺省情况下这个属性是 false,当一个字段确定是由其它字段或者当前环境中

的某些变量计算而得到的话,可以在 OnCalcFields 事件中添加相应的计算代码,并且

将 Calculated设置为 true,该属性在运行时不能改变,只能在设计时确定。 这也是一个比较有用的属性,比如一个表中包含了产品的数量和单价,那么合计价格

就可以通过计算字段而获得,从程序的逻辑上讲,这也是比较合适的处理方法。 ConstraintErrorMessage AnsiString 类型的可读写属性,用于指定一个用户的出错信息,这个信息是当该字

第四章 C++ Builder 语言的扩充 第 204 页

段的取值违反了该字段的约束条件时报告给用户的,比如一个正整形字段,为其赋的值小

于 0时,就是这样的错误。对于字段的约束条件在 CustomConstraint属性中定义,或

者是由数据库所决定的约束条件。 CustomConstraint AnsiString 类型的可读写属性,指定一个额外的字段约束条件,这个属性是按照

ANSI SQL的语句格式来书写的,比如要求一个整形字段的取值范围为 1~100。则可以将

该属性设置为: x > 0 and x < 100

通常这个属性被指定时,ConstraintErrorMessage 属性通常也需要针对该约束

条件设置相应的输出信息。 DefaultExpression AnsiString 类型的可读写属性,也是以 ANSI SQL 的格式指定一个用于用户没有

设置该字段的时候,添加缺省值的语句,比如某个时间字段当用户没有为新建的记录没有

赋值时,指定缺省值为 12点,可以将该属性设置为: '12:00:00'

即可 DisplayLabel AnsiString 类型的可读写属性,用于指定显示在 DBGrid控件中字段的标题,缺省

的情况下,DisplayLabel 和数据库中字段的名称相同,但大多数情况下,应用程序的

DBGrid中字段的标题和数据库中的字段的名称并不一样,数据库更习惯于使用英文名称,

而 DBGrid中则更适合使用中文标题,这可以通过设置 DisplayLabel了实现。 希望更改 DBGrid的标题也可以通过 DBGrid的 Columns 属性来设置 Title,但这

种方式将不允许在运行的过程中更改字段列表或更换 DBGrid绑定的数据集,即使通过代

码来修改,也要烦琐的多。使用 Columns 属性的另一个不可取之处是,数据集的字段显

示特性没有和相应的字段结合起来,而是决定于和其相关性较弱的 DBGrid。 DisplayName AnsiString 类型的只读属性,指定用于应用程序中用于显示目的的字段名称,其含

义是,当应用程序需要输出某些信息,比如产生异常的时候,格式化的字符串中包含的字

段信息。缺省情况下,DisplayName 和数据库的字段名称相同,当 DiaplayLabel 被

指定时,该属性和 DisplayLabel相同。 DisplayText AnsiString 类型的只读属性,这个属性实际上就是经过格式化处理的字段的取值,

应用程序中,对一个字段值的引用可以使用 AsBoolean、AsCurrency、AsDateTime、AsFloat、AsInteger、AsString、AsVariant 等属性,但是在界面显示,或者 Web程序中,可能使用 Text属性更加方便一些。

这个属性的另外一个用处是提供了应用程序隐藏真实数据的途径,当 TField对象的

OnGetText 事件被定义的时候,若处理函数的 DisplayText 参数为 true 时,

DisplayText将是经过 OnGetText事件处理的 Text参数,关于 OnGetText属性参

见后面内容。

第四章 C++ Builder 语言的扩充 第 205 页

Index 一个整形的可读写属性。指定了 TField 在 DataSet的 Fields数组中的位置索引。

DataSet的 Fields属性和 TField对象构成了一种于 TCollection对象类似但并不

相同的逻辑包含关系,Fields属性参见 TDataset类,包括了数据集中的所有字段以及

应用程序增加的字段,Index属性可以是程序员象 TCollection与 TCollectionItem对象那样访问一个数据集的某个字段,比如:

Table1->Fields->Fields[1]

就是访问 Table1的第二个字段(索引号符合 C语言对于数组的习惯,从 0开始),

在 TDataset 对象中还提供了另一种访问途径:使用 FieldByName 方法。另外,由于

TField本身就是一个组件,因此在程序的头文件中有其声明原型,因此在程序中也可以

通过使用 TField的原型进行访问,这也是最直接、效率最高的访问方式。 通常只在设计时更改 TFiled对象的 Index属性。 IsIndexField 一个 bool类型的只读属性。用于指定该指定是否为索引字段,这是在创建数据库时

就已经决定的,在数据集中定义的索引字段其相应的 TField对象的 IsIndexField即

为 true,反之为 false。在一个 lookup table 中,其 LookupKeyFields 必须是一

个索引字段。 KeyFields 一个 AnsiString 类型的可读写属性,用于指定 LookUp 字段(字段类型为

fkLookup并且字段的 Lookup属性为 true时)需要进行匹配查找的字段,当包含多个

查找匹配字段的时候,各个字段之间使用分号隔离。 Text 和 DisplayText 类似的属性,不同的是可读可写,用于字段的编辑状态,可以更改,

Text 属性同样可以使用 OnGetText 事件重新构建,当 OnGetText 事件句柄的

DisplayText 参数为 false时,该事件处理函数的 Text参数将代表着 TFiled 对象的

Text属性。 ValidChars 原型为: typedef Set<char, 0, 255> TFieldChars; __property TFieldChars ValidChars = {read=FValidChars, write=FValidChars}; 用于指定 Text属性的合法输入字符集,比如整形字段就可以在该属性中指定字符集

为’0’~’9’,这样就可以过滤掉非法的字符输入,这一个工作通常在定义数据集字段的时

候系统就已经自己完成,可以通过该属性更改法定的字符集。 Value Variant 类型的可读写属性,提供了在运行时直接访问一个字段数据的途径,比如

可以在一个字段为不可视状态下的存储和读取。 Visible

第四章 C++ Builder 语言的扩充 第 206 页

bool类型的可读写属性,用于指定该字段是否显示于 DBGrid控件,这个属性对于

DBEdit、DBLabel 等控件不起作用,当一个 DBGrid 使用了 Columns 属性来定义显示

列的时候,该属性也不起作用。 方法: FocusControl 原型为:void __fastcall FocusControl(void); 强制于该字段相关联的 Data控件获取焦点,由于数据字段在失去焦点的时候,字段

的数据会随之改变,使用该方法可以确保在没有获得正确的数据之前,字段的数据不会改

变,可以在 OnChange事件中加入相应的判断代码。 IsBlob 原型为:virtual bool __fastcall IsBlob() { IsBlob(__classid(TField))}; 用于判断该字段是否为 Blob字段,若是 Blob字段则对该字段数据的存取应该使用

TBlobStream对象,而不能使用 GetData及 SetData方法存取。 IsValidChar 原型为:virtual bool __fastcall IsValidChar(char InputChar); 用于判断输入的字符是否包含在该字段的有效字符集内,这通常是一个 Data控件需

要调用的方法,在新创建一个 TField派生类的时候,可能需要重载该函数,有效字符集

在 ValidChars 属性中定义。对于某些非字符输入的字段,比如大二进制数据,该方法

始终返回为 true。 SetFieldType 原型为:virtual void __fastcall SetFieldType(TFieldType Value); 提供了一个改变字段数据类型的接口,通常数据集中固有的字段,不需要改变字段的

数据类型,只有程序运行时创建的字段,或者是用于新建一个数据集时创建的字段才需要

重新设置字段数据类型。 事件: OnChange 当向一个字段数据缓冲区写入数据时触发的事件,原型为: typedef void __fastcall (__closure *TFieldNotifyEvent)(TField* Sender); __property TFieldNotifyEvent OnChange = {read=FOnChange, write=FOnChange}; 当向一个数据缓冲区成功的写入数据的时候,将会按照以下的顺序触发事件: 1. OnValidate事件率先被触发,来使数据生效。 2. 若 OnValidate事件的处理函数判断数据有效,将写入数据缓冲区。 3. 若写入过程没有产生异常,则写入后触发 OnChange事件。 OnGetText 当 DisplayText或者 Text属性被访问的时候触发,原型为: typedef void __fastcall (__closure *TFieldGetTextEvent)(TField* Sender,

AnsiString &Text, bool DisplayText);

__property TFieldGetTextEvent OnGetText = {read=FOnGetText,

第四章 C++ Builder 语言的扩充 第 207 页

write=FOnGetText};

访问 DisplayText 属性时,DisplayText 参数为 true,访问 Text 属性时,

DisplayText 参数为 false,若该事件没有指定处理函数句柄,则 DisplayText 和

Text都等于 AsString属性。 使用这个事件可以定制数据的显示格式或者隐藏真实数据,例如可以填写该事件的处

理代码,将 Currency类型的数据显示为符合中国习惯的人民币大写金额。 OnSetText 当为 Text属性赋了一个新值的时候触发,原型为: typedef void __fastcall (__closure *TFieldSetTextEvent)(TField* Sender,

const AnsiString Text);

__property TFieldSetTextEvent OnSetText = {read=FOnSetText, write

=FOnSetText};

这是和 OnGetText事件相反的一个过程,当该事件没有指定处理函数句柄时,编辑

后的 Text将直接赋给 AsString属性,可以在 OnGetText 和 OnSetText 事件中定义

两个操作相反的过程,对需要保存的数据进行加密,可以参考本书所附的例子。 OnValidate 在数据被写入缓冲区之前触发,原型为: typedef void __fastcall (__closure *TFieldNotifyEvent)(TField* Sender);

__property TFieldNotifyEvent OnValidate = {read=FOnValidate, write

=FOnValidate};

该事件通常用于整体判断输入的数据是否有效,和 TField的 EditMask 属性不同,

EditMask允许对用户输入的字符逐个进行判断。若在该事件中拒绝数据被写入缓冲区,

则之后的 OnChange事件将不会被触发,同时该事件会抛出一个异常。

2.4.3 TDataSet

TDataSet是所有数据集组件的公共基类,它使用行和列反映了一个数据库中数据集

的数据。TDataSet 封装了数据库的引擎,以及数据集所需要的属性、方法和事件。在

TDataSet中,很多属性、事件和方法都是纯虚拟的,因此 TDataSet是不允许被实例化

的,通常使用的都是从 TDataSet派生而来的数据集组件,比如 TTable、TQuery等。 在单层结构的应用程序中,使用的是文件数据库,两层结构的应用程序使用的都是

SQL数据库,在 C++ Builder中封装后的数据库组件可以消除这两种数据库之间的差别,

使程序开发者不需要面对两种不同类型的数据库而编写不同的代码。这都是依赖于

TDataSet的不同派生类来实现的。 在 TDataSet的方法中有一些尽管不是纯虚拟函数,但是功能是不完整的,需要在其

派生类中进一步来完善,如果开发者需要构建自己的数据集组件,则需要从 TDataSet的

某个派生类中派生新的组件,因此也有必要对 TDataSet的基本特征有个大致的了解。 属性: CanModify 一个 bool类型的只读属性,用于表明 Dataset 是否允许写入数据,TDataSet的

第四章 C++ Builder 语言的扩充 第 208 页

实例均是可写入的,而其派生类中 TQuery,TStoredProc和 TTable等都对这个属性

进行了重载,会根据实际的情况来判定是否具有写入权限。 DataSource 一个 TDataSource类型的只读属性,在 TDataSource类中有一个 TDataSet类

型的属性,每一个 DataSource 必须为该属性赋予相应的值后才能被其它的数据库控件

所使用。这是两个相互对应的属性,当为一个 TDataSource 对象指定一个 DataSet 时,

这个 TDataSet的 DataSource属性就指向该 TDataSource对象。 FieldCount 一个整型的只读属性,记录着 DataSet 组件中相关的字段数目。这个属性记录的字

段指的是在 Fields属性列出的字段的数量,对于 TDataSet对象,Fields 属性中的字

段有两种创建方式,一种是在设计时就已经根据需要创建的字段,这些字段的类型一个相

关信息会被保存在 dmf文件中,当程序运行的时候,Fields 属性是根据资源中的信息创

建 TField 对象,因此设计好的程序,其 DataCount 是不会发生改变的;当一个

TDataSet对象的 Fields属性是动态创建字段的话,所有的字段信息不会在设计时保存

到 dmf文件,而是在 DataSet 被打开的时候,根据数据库的结构来创建相应的字段,因

此,一个 TDataSet对象打开不同的数据库表集的时候,其 Fields 和 FieldCount 都

可能是不一样的。 Fields 一个 TFields类型的只读属性,用于罗列 DataSet 中的所有非合计字段,TFields

组件包含了一个 TField数组属性,用于存储所有列表的字段,TDataSet 通过 Fields属性和 TField 对象建立一种逻辑包含关系,由于 TField 和 TDataSet 都是从

TComponent 派生的,因此具有流式存取能力,而且在设计时创建的 TField对象会在头

文件中生成相应的声明原型,因此,程序代码中可以直接通过与名称(Name 属性)相同

的变量来访问 TField 对象,这一点和 TCollection 是完全不同的,尽管他们在 IDE中的表现形式是完全一样的。

TFields属性中包含的 TField 对象可以是在设计时创建,也可以是在运行时创建,

设计时创建的 TField对象,在运行时 Fields中包含的 TField 对象将严格按照设计时

的结构和类型生成相应的对象,运行时是不可改变的,但同时允许在设计时设置 TField对象中的许多属性,诸如:Title、Color、Font、甚至是字段名称等等;而运行时创

建则是 TDataSet 对象根据打开的数据集的结构自动的生成相应的字段 TField 对象,

前面我们也说过了,任何数据库的最终操作都是通过 TField来完成的,不管是设计时创

建还是运行时创建,TField对象都是绝对存在的,但是运行时创建的 TField对象,其

大部分属性象:DisplayName 等都是缺省的和数据集中的字段名称相同,不利于定制个

性化界面或国际化应用程序,它的优点是可以通过一个 TDataSet对象打开不同结构的数

据集。 FieldValues 一个 Variant 类型的可读写属性,这实际上是为程序员提供了一个访问很多记录的

途径,FieldValues 是一个数组属性,它的索引为 AnsiString 类型的变量,用于指定

字段的名称,其值等于该字段当前记录的值。由于 FieldValues 使用了 Variant 类型,

第四章 C++ Builder 语言的扩充 第 209 页

因此在存取过程中程序员不必去仔细区分字段的数据类型,会由系统自动进行区分,但同

时也带来的效率的降低等弊端,可以采用其它的访问方法来提高访问速度,比如使用

Fields 属性的字段列表直接对某个字段访问,并且指明数据的格式 AsString、AsCurrency、AsDate等。

方法: CreateBlobStream 原型为:virtual Classes::TStream* __fastcall CreateBlobStream(TField*

Field, TBlobStreamMode Mode);

提供了访问 Blob数据的一种接口,对于大二进制数据的访问在 C++ Builder 中是

通过 TBlobStream对象来实现的,创建 BlobStream 具有两种方式,一种是使用 new关键字,并在构造函数中指明相应的 Blob 字段,另一种是使用 Blob 字段的

CreateBlobStream方法。 GetBlobFieldData 原型为:virtual int __fastcall GetBlobFieldData(int FieldNo,

TBlobByteData &Buffer); 这个方法从 Blob字段中读取数据到一个动态数组的 Buffer中,其返回值为实际读

入字节数,FieldNo指定了读取那个字段的内容。 Locate 原型为:virtual bool __fastcall Locate(const AnsiString KeyFields,

const System::Variant &KeyValues,

TLocateOptions Options);

提供一个虚拟的记录定位的方法,按照提供的 KyFields字段中值为 KyValues的

记录,其中 Option为定位的匹配选项,在 C++ Builder中有精确匹配和部分匹配等选

项。该方法找到对应的记录时返回为 true,反之则返回 false,返回为 true时,查找

到的记录被激活为当前记录,Locate更适合基于文件的数据库,而两层以上的数据库则

更适合使用 SQL语句进行定位或者查找匹配。 Lookup 原型为: virtual System::Variant __fastcall Lookup(const AnsiString KeyFields, const

Variant &KeyValues, const AnsiString ResultFields);

提供一个虚拟函数用于查找给定字段的特定记录的值,与 Locate相似,但返回的不

是 bool类型,而是指定字段的取值,KeyFields指定用于匹配的字段,KeyValues 为

该字段匹配的取值,ResultFields为需要返回其值的字段。 事件: 在 TDataSet的事件中,大部分是以 After和 Before 开头的,这些事件有着非常

明确的含义,根据事件的名称就可以了解其作用,读者可以参考帮助,这里不多讲。只对

On开头的事件作个简单介绍。 OnCalcFields 在应用程序需要重新计算字段的值时触发。原型如下:

第四章 C++ Builder 语言的扩充 第 210 页

__property TDataSetNotifyEvent OnCalcFields = {read=FOnCalcFields,

write=FOnCalcFields};

当 TDataSet的 AutoCalcFields属性为 true时,该事件在以下情况下触发: n TDataSet被打开时; n 一个 TDataSet对象被设置为 dsEdit状态时; n 数据库相关的可视化控件的焦点转移时或者 DBGrid的 Column改变时; n 一条记录从数据库中重新获取数据时; 当一个程序允许用户改变数据的时候,该事件可能会被频繁的触发,这时可能会导致

程序的反映速度大为降低,可以将 AutoCalcFields 设置为 false来降低触发的频率。

当 DataSet的 AutoCalcFields 为 false时,一条记录内部的变化不会触发该事件。 OnDeleteError 当应用程序试图删除数据库的记录,并出现异常的时候触发。原型如下: typedef void __fastcall (__closure *TDataSetErrorEvent)(TDataSet* DataSet,

EDatabaseError* E, TDataAction &Action);

__property TDataSetErrorEvent OnDeleteError = {read=FOnDeleteError,

write=FOnDeleteError};

在该事件的处理句柄中,Action 为处理的动作,当该事件第一次被触发的时候,

Action 总是设置为 daFail,如果该事件的处理函数能够正确的解决故障,在函数的退

出前,将 Action设置为 daRetry,这时 delete 操作会重新尝试操作。如果不能正确

处理故障,当 Action为 daFail 时,程序不会发出错误的提示信息,如果希望程序出现

提提示信息,可以通过将 Action设置为 daAbort来实现。 OnEditError 当一个 DataSet编辑出错时被触发,原型为: typedef void __fastcall (__closure *TDataSetErrorEvent)(TDataSet* DataSet,

EDatabaseError* E, TDataAction &Action);

__property TDataSetErrorEvent OnEditError = {read=FOnEditError,

write=FOnEditError};

在这个事件的处理函数中也有一个 Action参数,其用法和作用和 OnDeleteError事件中的 Action相同。

OnPostError 当 Post操作失败时触发,原型为: typedef void __fastcall (__closure *TDataSetErrorEvent)(TDataSet* DataSet,

EDatabaseError* E, TDataAction &Action);

__property TDataSetErrorEvent OnPostError = {read=FOnPostError,

write=FOnPostError};

这个事件和 OnDeleteError 以及 OnEditError 类似,都是在出错时触发的,而

且在处理函数中包含了一个 Action,它关系到该事件以后的执行动作,和 Delete方法

中的类似。

第四章 C++ Builder 语言的扩充 第 211 页

2.4.4 TControl

TControl 是所有控件的公共基类,所有的可视化组件都是从 TControl 派生而来

的,前面我们也已经提到过,控件是具有用户交互操作能力的组件,同时可以响应 Windows的消息,因此在 TControl中需要提供控件的基本行为,包括 Windows 的消息处理能力。

属性: MouseCapture 一个 bool类型的可读写属性,用于表明控件是否捕获鼠标事件,当一个控件捕获鼠

标的时候,之后发生的鼠标事件都会属于这个控件所有,直到该控件释放了鼠标。用户可

以通过这个属性来限制某些操作。 Parent 一个 TWinControl 类型的可读写属性,用来表明一个控件的父窗体。在 Windows

程序中,通常每一个可视的窗体都会有一个父窗体作为容器,父窗体允许为空,为空时这

个窗体是属于 Windows桌面的,比如 Form就是属于桌面。但通常的控件,包括图形控

件和窗体控件都是属于某一个 Form或者其它容器类控件(必须是从 TWinControl 派生

而来)如:TPanel等,大多数控件没有父窗体的话将不能够被正确的显示。 控件通过 Parent属性和其父窗体建立了一种用于显示特性的逻辑包含关系,这种关

系个接近于直观的表现,当一个控件的父窗体移动时,该控件也会跟随移动位置,父窗体

的显示比例改变时,该控件的比例也会随之改变,甚至当控件的 ParentFont、ParentColor等为 true时,该控件的 Font、Color等属性也会随着父窗体改变。但

需要注意的是,这只是在显示特性上的逻辑包含,并不一定是“真正”的包含关系,在 VCL控件的逻辑关系中有三个比较类似但是不同的逻辑包含关系,一个是通过 Parent建立的

显示特性的逻辑包含,一个是通过 Owner 建立的生存周期的逻辑包含关系,另一个是在

C++原文件中声明的位置包含关系,从语言的角度来看,只有原文件中定义的这种关系是

实实在在的包含关系。通常在设计时创建的控件,头文件中声明的位置关系和通过 Owner建立的逻辑关系是一致的,但在运行是创建的控件可以将其 Owner指定为 NULL,正如前

面所说的 Owner为空的控件其生存周期是由 Application所掌握的。 Parent 属性和 Controls 属性相对应,当一个控件指定了父窗体时,其父窗体的

Controls属性中会增加一个元素。 Visible 一个 bool类型的可读写属性,决定这个控件是否被显示在屏幕上,缺省的情况下控

件都是被显示的,其显示的位置是以 Parent 为参考的相对坐标,当父窗体的 Visible为 false的时候,所有 Child控件都不会被显示。

WindowProc 一个 TWndMethod类型的可读写属性,但实际上该属性从各方面讲更象是一个事件,

它为控件的消息处理提供了一个接口,TWndMethod的原型如下: typedef void __fastcall (__closure *TWndMethod)(Messages::Tmessage

&Message);

尽管 VCL组件封装了大多数的 Windows消息,而且进行了归类和加工处理,但是在

第四章 C++ Builder 语言的扩充 第 212 页

某些情况下,开发程序时仍然需要对一些底层的 Windows 消息进行处理,使用

WindowProc 属性可以非常方便是实现这个目的,但是 WindowProc 属性本身并不为空,

它包含了一个控件 VCL 的基本消息处理能力,因此在设置该属性前需要将原来的

WindowProc 属性记录下来,并且在新的 WindowProc 属性所指向的 Windows 消息处

理函数中调用原来的 WindowProc句柄,其位置可以根据需要放在最前面或者是最后面,

WindowProc属性对应的消息处理方法为 WndProc。 对于组件的设计时,可以采用重载 WndProc 函数来实现新增加的消息处理过程,前

面讲述 VCL的消息处理机制时,也曾讲过,组件的消息处理可以通过消息映射宏定义来完

成,而在 VCL类库中,则更推荐使用重载 WinProc 方法来实现,在重载的 WinProc方

法中必须记着调用父类的 WinProc 方法,可以通过这种方式来实现对某一个特定的

Windows 消息是同时响应派生类和父类的 WinProc 处理或者只是响应派生类的

WinProc处理过程,而在消息映射的机制里,我们也可以看出,被映射的 Windows 消息

只能响应派生类的消息处理过程,除非在映射的消息处理函数中调用父类的 Dispatch方

法。 WindowText 一个字符串的可读写的属性,在一个控件中包含了三个比较类似的字符型属性,一个

是 Caption、一个是 Text,另一个就是 WindowText,它们各有用途,Caption通常

更多的用于固定的标题等,Text 则更多的用于变化的输入或者输出等字符串等,

WindopwText则可以被重载为其它的用途。 在 Edit控件中,WindowText 为输入的内容,在 ComboBox中为组合 Edit框中的

内容,在 Button中为控件的 Name,而在其它的控件中基本上是 Window的 Title,这

时 Windows系统中对窗体对象的要求。 方法: BringToFront 原型为:void __fastcall BringToFront(void);

其作用是将控件从底层提升到最顶层,这有以下几种情况,一是对 Form窗体,该方

法是有效的,BringToFront可以改变窗体在屏幕上的叠放次序,但是对于 Form的情况

可能会复杂一些,如果屏幕上存在着其它的应用程序窗体;另一种情况是对于都是从

TWinControl 派生而来的控件,它们都是窗体控件,缺省的情况是控件按照创建的顺序

自底向顶排列,即最后创建的窗体显示在最前面,可以通过 BringToFront 方法将排在

底层的控件提升到最顶层,这个方法会改变控件在 Parent数组中的位置;再一种情况是

对于都是从 TGraphicControl 中派生而来的控件,它们不属于窗体控件,但是和前一

种情况相同,排列次序是根据创建的先后而定的,BringToFront 可以改变它们的叠放

次序;最后一种情况是分别由 TWinControl和 TGraphicControl派生控件,它们属

于不同的控件分支,BringToFront 不起作用,因为所有的窗体控件都会显示在图形控

件的上面。 Changed 原型为:void __fastcall Changed(void);

向控件发送一个 CM_CHANGED消息。在属性发生变化以后调用 Changed 方法可以影

第四章 C++ Builder 语言的扩充 第 213 页

响到 Parent对象,这样可以使其容器对象产生一些必要的调整动作。 ChangeScale 原型为:DYNAMIC void __fastcall ChangeScale(int M, int D);

用于调整控件的显示比例,M为分子,D为比例的分母,比如 ChangeScale(3,4)将会

调整控件的大小为原来的 75%,这个方法在某些情况下是非常有用的,经常在网上听到网

友为了界面在不同的桌面分辨率下显示不正常,使用比例放缩可以有效的解决这个问题。 ChangeScale 不仅改变了控件的长宽比例,同时也会改变定点的位置坐标,当一个

窗体控件的比例改变时,其 Child 控件也会随之一并改变,关于这方面的内容这里不多

讲,读者可以参考本书所附的例程。 Click 原型为:DYNAMIC void __fastcall Click(void); 触发 OnClick 事件,Click最典型的在收到 WM_LBUTTONUP时系统自动调用,同

时 Click方法会调用 OnClick事件所关联的处理函数,通过对 Click方法的重载,可

以实现一个控件响应 WM_LBUTTONUP的额外处理过程,有一些控件 Click方法不光是在

收到 WM_LBUTTONUP 消息时触发,而且会在收到某些于 WM_LBUTTONUP 同等作用的消

息时触发,比如热键、加速键或者回车等。 ClientToScreen 原型为: Windows::TPoint __fastcall ClientToScreen(const Windows::TPoint &Point);

是将控件中的相对位置坐标转换成屏幕的绝对坐标。 ConstrainedResize 原型为: virtual void __fastcall ConstrainedResize(int &MinWidth, int &MinHeight,

int &MaxWidth, int &MaxHeight);

触发一个 OnConstrainedResize 事件,和 Click方法类似,可以被派生类重载。 DblClick 原型为:DYNAMIC void __fastcall DblClick(void);

触发 OnDblClick事件,和 Click类似,可以被派生类重载。WM_LBUTTONDBLCLK消息会调用给方法,在 TControl的 DblClick 方法中调用 OnDblClick 事件的处理句

柄。 DefaultHandler 原型为:virtual void __fastcall DefaultHandler(void *Message);

是所有控件的缺省消息处理函数,这和 Windows SDK 编程的缺省处理过程比较类似

的,可以通过重载这个函数来实现对一个控件的 Windows 消息做出额外处理,这和

WndProc重载有这曲艺同工的作用。 MouseDown、MouseMove、MouseUp 原型分别为: DYNAMIC void __fastcall MouseDown(TMouseButton Button, Classes::

第四章 C++ Builder 语言的扩充 第 214 页

TShiftState Shift, int X, int Y);

DYNAMIC void __fastcall MouseMove(Classes::TShiftState Shift, int X,

int Y);

DYNAMIC void __fastcall MouseUp(TMouseButton Button, Classes::

TShiftState Shift, int X, int Y);

触发 OnMouseDown、OnMouseMove、OnMouseUp事件,和 Click类似,可以被

派生类重载。 Notification 原型为: virtual void __fastcall Notification(Classes::TComponent* AComponent,

Classes::TOperation Operation);

这是为对象之间的通讯提供一种途径,Notification 可以将自身的创建、销毁等

消息通报给 Owner,派生类可以通过重载这个方法来实现其它的一些通讯。当两个对象通

过 Parent、Owner 等属性建立逻辑包含关系的时候,创建、销毁等动作通常都要相互交

流,甚至在 DataSet和 DataSource 等关系也是需要这种信息的交流,否则对象之间的

关系或者是相互的约束关系是极不完整的,Notification 为这种关系提供了一种途径。 Perform 原型为:int __fastcall Perform(Cardinal Msg, int WParam, int LParam);

例行 Windows消息的方法,在 VCL中提供了许多消息通讯的方法和属性,当然也可

以使用 Windows API来向系统的消息队列发送某个消息,但这种情况比较复杂,需要经

过 Windows的处理,Perform 提供了一种途径,使程序需要向某个对象发送某个消息不

经过 Windows的消息队列,而直接使用 Perform 方法来响应。在 VCL中定义了许多用

户消息,这些消息是 Windows 中本身不包含的,因此通过 Perform 来响应消息要高效

的多。 Perform方法通常用在 VCL 内部的消息传递,比如通过 Parent、Owner 等属性建

立的逻辑关系,象 ToolBar和其中的 Button之间就是使用了 Perform传递一些消息

的。 Refresh、Repaint 原型为: void __fastcall Refresh(void);

virtual void __fastcall Repaint(void);

这两个方法的作用是等价实际上是等价的,将强迫控件重绘显示区域。当一个控件的

ControlStyle属性包含 csOpaque 选项时,Repaint 直接绘制显示区域,反之则是通

过 Invalidate方法来重绘区域。 SendToBack 原型为:void __fastcall SendToBack(void); 将控件的叠放顺序改变到最底层,是和 BingToFront 相反的过程。当一个控件获得

输入焦点的时候,SendToBack方法会使其失去焦点。和 BingToFront 方法一样只能对

都是窗体控件或者都是图形控件起作用。

第四章 C++ Builder 语言的扩充 第 215 页

Show 原型为:void __fastcall Show(void);

改变控件的可视属性,其作用和 Visible = true;语句是相同的,在该方法中包含

了对 Parent对象 Visible属性的检查工作。 VisibleChanging 原型为:DYNAMIC void __fastcall VisibleChanging(void); 提供了一个显示属性改变的响应接口,当一个控件的 Visible 属性需要改变时会自

动调用该方法。这个发生在 Visible 改变之前,因此重载该方法,并且在该方法中增加

条件的判断和异常行为的处理代码,可以阻止某些非法的显示属性的改变。 WndProc 原型为:virtual void __fastcall WndProc(Messages::TMessage &Message);

这实际上就是 WindowProc 属性所指向的处理句柄,它提供了一个控件的消息处理

函数,同时这个方法也是虚拟函数,可以被派生类重载,在 TControl的 WndProc 方法

中,提供了基本的鼠标操作的响应,可以根据控件的 ControlStyle、DragMode 等属

性来正确处理鼠标消息。 使用重载 WndProc和重定向 WindowProc 属性基本上可以认为是一回事,但也稍有

不同,如果不是用户自定义的类,则不能够对 WndProc 进行重载,因而只能使用重定向

WindowProc属性。 前面也提到,使用 VCL 所提供的这些扩展的消息处理机制,不仅可以获得最底层的

Windows 消息的处理能力,还可以非常灵活的组织这些消息,可以使派生类具有父类表

现特征的同时具有自己的个性。 事件: 在 TControl的事件中,OnDragDrop、OnDragOver、OnEndDock、OnEndDrag、

OnStartDock、OnStartDrag等都是为控件的拖放而抽象出来的事件,这些是 Windows本身不具备的,而且是比较一致的,在其派生类中关于拖放的属性、非法等基本都和

TControl中的相同,对这几个事件我们不多作讲解。 OnCanResize 当试图改变一个控件的大小时触发,该事件在 OnConstrainedResize 事件之前触

发,可以在该事件的处理函数中增加适当的代码来阻止随意改变控件的大小。 OnClick 这是一个抽象的事件,在不同的控件中触发的方式可能会有所不同,但有一个共同的

触发条件就是单击鼠标的左键可以触发该事件,通常可以触发该事件的条件有: n 在 Grid中 OutLine中选择一个单元或者项目时; n 在 ComboBox中按键盘的方向键时; n 当一个控件获得焦点时按下空格键或者回车键; n 在一个 Form 中定义了缺省确认按钮是按下了回车键会触发缺省按钮的

OnClick事件,当一个 Form中定义了缺省取消按钮时,按下 Esc 键会触发缺

省取消键的 OnClick事件。 n 用户按下了某个控件的热键或者加速键等,会触发相应控件的 OnClick事件。

第四章 C++ Builder 语言的扩充 第 216 页

n 某些含有 Checked属性的控件,Checked属性改变时; n MenuItem的 Click方法被调用时; 在一个 Form 中点击空白区域或者已经处于无效状态的控件,都会触发该 Form 的

OnClick事件。 关于 OnClick 事件在不同的控件中都可以不一样,用户如果设计控件时,通常需要

针对设计的控件来重载这个事件,以满足不同的应用需求。 OnConstrainedResize 当一个控件试图改变其大小的时候触发,在 OnCanResize 事件之后,OnResize 事

件之前。每个控件可以定义其约束范围,并保存在 Constraints 属性中,如果需要重新

定义约束范围则可以在该事件中设置 Constraints属性中相关参数。 OnContextPopup 当用户鼠标的右键点击控件时触发,或者是通过其它的条件来弹出 PopupMenu时触

发(比如 Win95 键盘的 Popup 键),这个事件在控件没有指定 PopupMenu 时或者其

AutoPoup属性设置为 false的时候特别有用,可以让程序来集中管理加速菜单。 值 得 注 意 的 是 任 何 控 件 的 OnContextPopup 总 是 会 触 发 其 Parent 的

OnContextPopup 事件,而且 Parent 的总是在前触发,因此如果没有在 Parent 中指

定 PopupMenu,那么 Parent的 OnContextPopup事件可能会多次被触发。 直接使用这个事件的机会也许并不多,但是这个过程我们经常会遇到,比如在一个

Form中定义了 PopupMenu,其多个 Child控件并没有指定弹出菜单,那么在 Child 控

件上点击右键都会弹出 Parent所指定的 PopupMenu。 OnDblClick 响应控件的鼠标双击事件,这个事件不象 OnClick 那么丰富,通常只有 Windows

的双击消息才会触发,当然在用户自行设计的控件中也可以将其定义的更为丰富。 OnMouseDown、OnMouseMove、OnMouseUp 这几个事件都是鼠标事件,它们的原型分别如下: typedef void __fastcall (__closure *TMouseEvent)(System::TObject* Sender,

TMouseButton Button, Classes::TShiftState Shift, int X, int Y);

typedef void __fastcall (__closure *TMouseMoveEvent)(System::TObject

* Sender, Classes::TShiftState Shift, int X, int Y);

__property TMouseEvent OnMouseDown = {read=FOnMouseDown,

write=FOnMouseDown};

__property TMouseMoveEvent OnMouseMove = {read=FOnMouseMove,

write=FOnMouseMove};

__property TMouseEvent OnMouseUp = {read=FOnMouseUp,

write=FOnMouseUp};

分别是在鼠标键被按下、释放和鼠标移动的时候触发,其中 X,Y分别是鼠标的位置坐

标,为 Sender 的 Client 区域的相对坐标,Shift 参数记录着鼠标事件发生的时候键

盘的功能键的状态。

第四章 C++ Builder 语言的扩充 第 217 页

OnResize 在控件重新调整大小后触发,这个事件和前面提到的 OnConstrainedResize、

OnCanResize共同负责关于控件大小方面的消息处理。

2.5 TControl 分支

在 VCL类库中,所有的控件都是从 TControl中派生而来,在 TControl 的分支中,

包含了两大类的控件,一类是从 TWinControl 派生而来,属于一种窗体控件;另一类是

从 TGraphicControl 派生而来,属于图形控件。这两种控件的最大区别是窗体控件具

有 Windows 窗体的所有特征:具有窗口句柄,可以接收键盘输入,能够获取焦点,也可

以作为其它控件的父窗体及可以为其它控件的容器,而图形控件则没有窗口句柄,不能接

收焦点,不可以作为其它控件的容器。但是图形控件的消息处理过程要比窗体控件简单的

多,因此其响应速度和执行效率要相对高一些。在设计控件的时候,决定是从

TWinControl 派生还是从 TGraphicControl 派生完全取决于控件的需求,只有了解了

它们的不同之处才能很好的使用它们。

2.5.1 TGraphicControl

TGraphicControl 是一个相对比较简单的控件,它是由 TControl 派生而来,除

了增加了一个 TCanvas属性和一个 Paint 方法之外,其它的内容和 TControl完全一

样。 属性: Canvas 原型如下: __property Graphics::TCanvas* Canvas = {read=FCanvas};

这是一个只读属性,Canvas是由控件自身内部创建的,使用者不能自行改变,事实

上 Canvas画布是和控件相对应的固有属性,一旦控件创建,它的画布就是固定的。 在 C++ Builder中,TCanvas 封装了几乎所有的 Winodws DC资源和大多数绘图

函数,如果用户不使用 OpenGL 或者 DirectX 等绘图函数时,TCanvas 可以完成

Windows图形显示的所有功能。 对于图形控件,由于本身没有窗口句柄,而 TCanvas 需要一个窗口句柄来创建

Device Context,图形控件都是从其父窗口那里获取 DC资源的。 方法: Paint 原型为:virtual void __fastcall Paint(void);

提供了一个绘图的接口,所有的派生类都需要重载该方法来实现自身的图形显示。该

方法在从 Parent接收到 WM_PAINT消息时自动被调用,通常派生类需要重载该方法。

2.5.2 TWinControl

TWinControl 具有标准 Windows 的特征,在 VCL中还为其定义了许多用于界面显

第四章 C++ Builder 语言的扩充 第 218 页

示的属性、方法和事件,例如 Bevel 相关的属性可以使窗口的显示具有三维效果,Doc相关的属性使控件可以自由的拖放停靠等。对这些特征我们不多讲解,读者需要翻阅相关

资料或者察看联机帮助,我们仅对 TWinControl 中和消息传递,以及可能影响到对象之

间相互约束关系的特征作个简单介绍。 属性: Brush 一个 TBrush类型的只读属性,用来记录控件的背景显示特征,如:颜色、风格等,

由于 TWinControl 没有 Canvas属性,因此需要用 Brush来记录这些信息,通常不一

定直接使用该属性,而是可以通过 Color 等属性间接的改变之,在某些 TWinControl的派生类中,也定义了 Canvas属性,对这些控件可以直接使用 Canvas 进行界面显示的

操作。 ControlCount、Controls 这两个属性的原型为: __property int ControlCount = {read=GetControlCount, nodefault};

__property TControl* Controls[int Index] = {read=GetControl};

都为只读属性,TWinControl 可以作为其它控件的容器,就是可以作为别的控件的

Parent属性,Child控件的 Parent属性和 TWinControl 控件 Controls属性共同

建立了一种逻辑包含关系,这在 TControl的 Parent属性介绍中已经讲过。 ControlCount 记录这 Controls 属性中所包含的 Child对象的数目,当一个控件

被创建的时候,并将 Parent设置后,该控件会使用 Notification 方法向 Parent 通

报,Parent控件则向 Controls属性中添加一个控件列表,同时 ControlCount 会自

动更新。 DefWndProc 原型如下: __property void * DefWndProc = {read=FDefWndProc, write=FDefWndProc};

该属性指定了窗体控件的缺省消息处理函数。在 Windows 程序中,每个窗口都会有

一个缺省的消息处理函数,用于标准事件的处理,DefWndProc在 TWinControl 使用 API函数 CreateWindow创建窗体的时候,会调用 DefWndProc来处理缺省的Windows消息,

对于一些 WndProc方法中没有处理的消息,可以通过 DefWndProc来处理, Handle 一个 HWND类型的只读属性,原型如下: __property HWND Handle = {read=GetHandle, nodefault};

记录着窗体控件的窗口句柄。这时和图形控件最大的一个区别,而且在 Windows 程

序中,每个窗口都需要一个句柄来在操作系统中定位,大多数的 API函数也都需要有一个

窗口句柄作为参数来表明操作对象,VCL尽管封装了很多 API的功能,但在某些特殊操作

可能使用 API 更加方便一些,Handle 属性为使用 API 函数提供了可能;另外,对于一

般的窗体控件,需要有一个父窗体,包括一个程序的主窗口也是需要指定父窗体(通常为

空),如果将一个应用程序或者 ActiveX控件甚至是其它可执行模块窗体的 Parent 指定

为另一个窗口控件的句柄,则可以将可执行模块的窗体嵌入到这个窗体控件中。

第四章 C++ Builder 语言的扩充 第 219 页

ParentWindow 一个和 Parent非常类似的属性,原型如下: __property HWND ParentWindow = {read=FParentWindow,

write=SetParentWindow, nodefault};

也是记录着窗体控件的父窗口句柄,和 Parent不同的是 ParentWindow通常是用

于非 VCL类型窗口作为控件的父窗体时,ParentWindow对于 VCL 窗口作为父窗体也是

有效的,这两个属性可以同时使用,但是当 Parent 属性非空时,ParentWindow 会被

忽略。 如果编制多语言混合程序,需要注意的是,在一些共享的可执行模块中使用 VCL控件

时,不能使用 Parent 来确定控件的父窗体,除非已经明确这个窗口不会使用非 VCL 窗

体作为父窗体,取而代之的是使用 ParentWindow来传递相应的信息。 Showing 一个 bool类型的只读属性。 Showing 本身是一个供内部使用的属性,用来判断控件是否被显示,通常是用来判

断是否为控件分配用于缓冲的内存,比如显示图形资源等,Showing 和一个控件的

Visible以及父窗体的 Visible 相关,甚至是父窗体的父窗体等等。在编制控件时代码

使用 Showing属性可以提高控件的性能。 WindowHandle 原型如下: __property HWND WindowHandle = {read=FHandle, write=FHandle,

nodefault}; 这和 Handle 属性非常相似,并且指向的是同一个存储内容,但不同的是

WindowHandle 是可读可写的属性,因此这个属性只是内部使用的受保护的属性,在编

写控件时有可能会用到,另一个与 Handle属性不同之处在于,Handle 对外提供了一个

完整的接口,当 Handle不存在时,读取 Handle属性会使控件自动的创建窗口句柄,而

WindowHandle则没有这个过程。 方法: 在 TWinControl 的方法中,有很多是窗口控件的共同特征,这些方法通常已经和相

应的 Windows消息或者经过处理加工的消息相关联,并且允许派生类重载,象 KeyDown、KeyPress、KeyUp 以及和 Mouse 操作相关的方法等,还有一些方法是触发某些事件的

内部方法,派生类可以重载,象 DoEnter等 Do开头的方法,这和 TControl中的 Click等方法比较类似,鉴于篇幅有限,而且 TWinControl 中新增加的方法又特别多,因此对

这些方法我们这里不再介绍,而且这些方法也是容易掌握的。 AlignControls 原型为: virtual void __fastcall AlignControls(TControl* AControl,

Windows::TRect &Rect);

重新排列窗体控件中容纳的控件。Child控件的排列方式按照各自的 Align属性的

设置,Rect为需要重新排列的区域,Acontrol为指定的 Parent容器,可以为空。

第四章 C++ Builder 语言的扩充 第 220 页

Broadcast 原型为:void __fastcall Broadcast(void *Message); 向容器中所有的 Child控件发送一个消息。这个方法在 TWinControl 中非常重要,

通常一些重要的信息都是通过 Broadcast 来以广播的方式向 Child 控件发送的。在一

个容器控件中,往往会容纳了很多具有一定约束关系的 Child控件,比如 ToolBar 中的

SpeedButton或者 ToolButton 等,它们之间是通过 Parent 来建立相互之间的关系,

这些关系在表现形式上最终是以属性来实现的,但内部的消息传递是通过一系列的方法来

实现的。 Broadcast方法很少用在程序设计时,但在编写控件时却可能会经常使用到。 ContainsControl 原型为:bool __fastcall ContainsControl(TControl* Control);

用来判断一个控件是否在容器控件之内,这和 Controls 属性中列表的内容有所不

同,Controls 列表中之包含了自身的 Child 控件,如果 Child 控件也是一个

TWinControl控件,并且也包含了其它的 Child控件,那么使用 Controls属性来判

断就和 ContainsControl 方法判断的结构不一致,ContainsControl 不管是自身的

Child控件,还是 Child的 Child控件,都可以正确的区分是否包含在窗体之内,并且

没有层次深度的限制。 ControlAtPos 原型为: TControl* __fastcall ControlAtPos(const Windows::TPoint &Pos,

bool AllowDisabled, bool AllowWinControls);

用来返回指定位置的控件,这个方法只检查自身的 Child 控件,两个参数分别决定

是否检查无效的控件和是否过滤 TWinControl的派生类。 CreateParams 原型为:virtual void __fastcall CreateParams(TCreateParams &Params); 用于创建窗口时所需要的参数。每个 Windows 窗口在使用 API 函数

CreateWindowEx或者CreateWindow是都需要指定一个 Parameter结构来存储窗口

的风格、设置等信息,在 VCL中 TWinControl 的 Parameter 是设计控件时指定的,或

者由其它的一些属性来决定的,该方法可以用来额外的为窗口指定相关参数,本书例子

PickEdit中就通过了重载 CreatParams方法为 PickEdit限定了字符的显示区域。 CreateParentedControl 原型为: static TWinControl* __fastcall CreateParentedControl(TMetaClass* vmt,

HWND ParentWindow); 用于将 VCL的 TWinControl 控件插入一个非 VCL风格的窗口中,这个方法并不是

创建一个父窗体,而是重新创建一个 vmt指定的对象,并将新创建的窗口的父窗口指定为

ParentWindow参数指定的窗体。 GetDeviceContext 原型为:virtual HDC __fastcall GetDeviceContext(HWND &WindowHandle);

第四章 C++ Builder 语言的扩充 第 221 页

提供了一个获取控件Device Context的接口,尽管在VCL中可以使用WindowsAPI函数 GetDC获取 DC资源,但建议使用该方法会更好一些,因为在 VCL封装的过程已经

考虑了获取资源失败的情况,而使用 GetDC则需要判断函数是否执行成功。 另外这个方法是虚拟的,他允许设计控件的时候重载,这样可以更加灵活、方便的使

用 VCL控件,而不必担心在使用 GetDC的时候是否传递了正确的参数(在某些控件中,

画布并不是自身的 Context DC,而是其中一个 Child控件的 DC,设计控件时可以通过

重载 GetDeviceContext方法得到纠正)。 GetParentHandle 原型为:HWND __fastcall GetParentHandle(void);

返回父窗体的窗口句柄,完全使用 VCL 控件的程序可以使用 Parent 属性来获取父

窗体的句柄,但是父窗体不是 VCl风格的对象时,Parent属性是空的,这样会得不到正

确的结果,使用 GetParentHandle可以使程序员不必关系父窗体是否为 VCL对象。 IsControlMouseMsg 原型为:bool __fastcall IsControlMouseMsg(Messages::TWMMouse &Message);

在 Windows 系统中,所有的系统消息是发送给回调函数的,更多的是发送给窗口的

回调函数,TWinControl 的派生类具有窗口的所有特征,因此可以接收 Windows 的消

息,而从 TGraphicControl 派生来的的控件,并不具备这种能力,可以认为图形控件

只是 VCL 对象在 Parent 的画布上绘制的一个形状,并不是窗口,所有关于图形控件的

鼠标消息都是发送给了 Parent,Parent 则需要判断并正确的将鼠标消息发送给每一个

Child图形控件,这就不难理解 TWinControl 为什么有一个 ControlAtPos方法,而且

这个方法具有过滤无效控件和窗体控件的能力。 NotifyControls 原型为:void __fastcall NotifyControls(Word Msg);

用于向所有 Child 控件发送一个消息,和 Broadcast 类似,但是要简单一些,对

于一些参数为空的消息,使用NotifyControls要比Broadcast方便的多,BroadCast需要使用一个完整的 Windows消息结构作为参数,而 NotifyControls只需要指明消

息类型,因此 NotifyControls不适合用于带参数的消息。 PaintControls 原型为:void __fastcall PaintControls(HDC DC, TControl* First);

在指定的 Debice Context上绘制 Child控件,DC指定需要绘制的画布,First指定从 Controls 列表中的那一个 Child 控件开始画起,当 First 为 NULL,所有的

Child控件都会被绘制。 这个方法对于控件组合复杂又可能经常刷新的情况非常有用,可以为 Parent控件创

建一个内存的缓冲区,然后重载相关的绘图函数(如 PaintWindow),在绘图函数中先将

Child控件绘制到缓冲区然后在拷贝到 Parent控件的画布上,可以有效的缓解显示闪烁

的问题,读者可以参考本书的例子 DataPick控件。 PaintWindow 原型为:virtual void __fastcall PaintWindow(HDC DC);

用 来 绘 制 控 件 , 类 似 TGraphicControl 的 Paint 方 法 , 该 方 法 向

第四章 C++ Builder 语言的扩充 第 222 页

DefaultHandler 传递一个 WM_PAINT 消息,并将消息的 WPara设置为参数 DC,其它

参数和 Result设置为 0。 事件: OnEnter 当控件获得了输入焦点的时候触发,原型如下: __property Classes::TNotifyEvent OnEnter = {read=FOnEnter, write

=FOnEnter};

在两个 Form之间切换的时候,不会触发该事件,在两个应用程序之间切换时也不会

触发该事件。 当一个容器控件中的 Child 控件获得焦点的时候,它的父窗体也会触发该事件,而

且是在 Child控件的 OnEnter之前触发。 OnExit 当控件失去焦点的时候触发,和 OnEnter是相反的,原型如下: __property Classes::TNotifyEvent OnExit = {read=FOnExit, write=FOnExit};

这个事件在 Form之间切换的时候不会触发,在两个应用程序之间切换的时候也不会

触发,和 OnEnter相反,Child控件总是先一步于 Parent控件失去焦点。 另一个需要注意的是 Form中的 ActiveControl的更新是在 OnExit事件触发之前

进行的。 OnKeyDown、OnKeyPress、OnKeyUp 当一个控件获得输入焦点的时候,所有的键盘消息会被分发到该控件,这三个事件是

响应键盘消息的事件,原型分别如下: typedef void __fastcall (__closure *TKeyEvent)(System::TObject* Sender,

Word &Key, Classes::TShiftState Shift);

typedef void __fastcall (__closure *TKeyPressEvent)(System::TObject

* Sender, char &Key);

__property TKeyEvent OnKeyDown = {read=FOnKeyDown, write=FOnKeyDown};

__property TKeyPressEvent OnKeyPress = {read=FOnKeyPress,

write=FOnKeyPress};

__property TKeyEvent OnKeyUp = {read=FOnKeyUp, write=FOnKeyUp};

OnKeyDown事件键盘按下任意键时触发,相当于 WM_KEDOWN消息触发,OnKeyUP则相当于 WM_KEYUP消息触发。这两个事件的触发是在 IME输入法接收到消息之前产生

的,是和键盘消息对应的。输入法不会对其产生任何影响。 OnKeyPress 是在键盘按下一个字符键的时候触发,这个事件不能区分组合键、功能

键等。和上面两个事件不同,OnKeyPress 是在 IME 输入法之后收到消息的,因此当输

入法打开的时候,OnKeyPress 不会收到键盘的击键内容,取而代之的是输入法的消息。 OnMouseWheel、OnMouseWheelDown、OnMouseWheelUp 这三个事件在鼠标的滚轮被滚动的时候触发,原型分别如下: __property TMouseWheelEvent OnMouseWheel = {read=FOnMouseWheel,

write=FOnMouseWheel};

第四章 C++ Builder 语言的扩充 第 223 页

__property TMouseWheelUpDownEvent OnMouseWheelDown = {read=

FOnMouseWheelDown,write=FOnMouseWheelDown};

__property TMouseWheelUpDownEvent OnMouseWheelUp = {read=FOnMouseWheelUp,

write=FOnMouseWheelUp};

和上面的三个事件相似,OnMouseWheel在滚轮滚动时触发,OnMouseWheelDown在滚

轮向下滚动的时候触发,OnMouseWheelUp在滚轮向上滚动的时候触发。 本节所讲的这些类库都属于基类,不会在实际中直接应用,有些甚至是抽象基类,不

能被实例化,但这些基类定义了其分支的共同特性,安装在组件面板中的组件都是从这些

类中派生而来的。 对于可以直接使用的或者能够被实例化的类,我们这里不再介绍了,它们都具有其祖

先的形状和使用特征,只有一些细微的差别,比如 TStream 类不能被实例化,但在组件

和程序中往往都是以引用 TStream 的方式是来操作和访问其子孙,这也是面向对象的一

个特点,TStream 所提供的属性和方法对大多数的应用都是能够满足的,只有极少数情

况,需要引用派生类本身对象的类型才能完成,比如直接访问 TMemoryStream 的内存区

就只能通过对 TMemoryStream类操作来完成。 VCL的结构我们只作一个简单介绍,需要进一步了解的读者请参考相关书籍。

3 C++ Builder的资源

在 Windows 程序设计中,资源是用户界面不可缺少的,所谓资源就是一种数据,供

程序运行时使用的数据。资源通常存在于可执行文件的内部,是程序的一部分,当程序需

要向用户界面输出,程序会调用相应的 API函数将提取资源并显示在屏幕或者输出在相应

的设备上。用户可以为应用程序设计任何类型的资源,但除了标准的 Windows资源以外,

其他类型的资源都可能需要使用用户自行设计的资源处理代码。 在使用 Windows SDK设计程序时,资源和代码是相对独立的,使用和设计都很不方

便,因此在 C++语言中,大多数的 FrameWorks 都将对象和资源进行了不同程度的封装,

包括 MFC、OWL 以及 C++ Builder中 VCL。在这些 FrameWorks 中 VCL 是将资源最彻

底的封装的一种类库,也是使用最为方便的一种,也是最不遵循 SDK标准的资源类型,实

际上用户使用 C++ Builder设计的程序,大部分资源是不能使用 Windows SDK 进行初

始化的,但这些工作都是由 VCL的代码自行完成的,这是对资源的更高层次的应用,同时

也带来了相应的副作用,一般情况 C++ Builder 编制的可执行程序体积都比使用其他

FrameWorks 编制的相同功能的程序大得多。这并不是一个主要的问题,因为目前的硬件

发展速度远远大于软件的发展速度,特别注重程序运行性能的时代已经成为过去,大多数

应用程序都将更多的注重于程序的稳定性、可靠性、易维护性以及可扩展性,C++ Builder的 VCL正是为了适应这种需求而产生的。

尽管对于大多数程序而言,已经不需要单独的使用资源编辑器进行资源设计,因为

C++ Builder 中会自动根据界面的设计而生成其专用的资源,但如果你是一个组件开发

第四章 C++ Builder 语言的扩充 第 224 页

者、如果你希望编制功能更加强大的程序或者你希望更多的了解 C++ Builder,那么还

是有必要了解 Windows 的历史,下面将对 Windows 的资源进行简单描述,需要更深的

了解请参阅相关书籍!

3.1 Windows 资源

这里所说的 Windows资源,是针对 C语言的 SDK而言的,是没有涉及到面向对象的

设计方法,MFC、OWL中资源的使用已经有所不同,但不是本书要讲的内容,假如你想深

入学习并使用 C++ Builder,也就没有必要对 MFC和 OWL进行深入的了解。 首先我们来看一下 Windows 的资源类型,确切的将是 Win32 的资源类型,在

Windows 95和 WindwosNT 4.0 之前的操作系统,使用的是 16位代码,资源稍有区别,

而且 16 位的操作系统已经步入历史的红尘,有兴趣的读者可以查阅早期的 Windows 参

考书籍。

3.1.1 Windows 资源的类型

在 Win32 SDK中,提供了以下类型的 Windows资源类型: Accelerator:加速键,俗称热键,通常在程序中使用组合键,如经常使用的 Ctrl+C

为拷贝、Ctrl+V为粘贴等都属于加速键,使用加速键的目的是将一些常用的操作和相对

简单的按键组合对应起来,这里所指的加速键都是应用程序内部的,与 Windows 加速键

有所区别,也就是只有一个应用程序处于激活状态时,这些加速键才在应用程序中起作用,

而 Windows热键需要使用 Windows API向系统注册,无论什么情况下只要系统热键注

册成功,都是有效的。加速键通常可以使用资源编辑器或者文本编辑器来编辑。 Bitmap:位图,这是 Windows中应用比较多的一种资源,实际上就是 bmp图片,大

小没有限制,可以是 4色、16色、256色以及 16位、真彩等,但在程序中为了减少资源

占用,通常使用 256色以下的图像,对于 16位色、真彩等高质量的图像可以采用工具软

件进行抖动处理转换成 256色位图。位图资源可以使用资源编辑器、图像工具、文本编辑

器等来编辑。 Cursor:光标,是一种特殊的位图,大小限制为 32 x 32像素,用于屏幕上显示鼠

标的位置,可以使用图像编辑器或者资源编辑器来编辑光标资源。 Dialog:对话框,是一种用于建立特定对话框窗体的资源通常需要使用资源编辑器

进行创建和编辑。 DialogEx:扩展对话框,同 Dialog 资源,但是属于 Windows 95 及以后版本的

Widnows资源,支持 Win95扩展界面,使用对话框编辑器创建和编辑。 Icon:图标,是一种较小的位图文件通常也使用简单颜色,大小限制为 16 x 16、

32 x 32、48 x 48、64 x 64像素,使用图像编辑器创建和编辑图标。 Menu:菜单资源,Windows中菜单资源也是应用最为广泛的资源,基本上是不可缺

少的资源类型,可以使用文本编辑器或者资源编辑器创建和编辑一个菜单资源。 MenuEx:扩展菜单资源,同 Menu资源,也是用于 Win32的资源类型,使用资源编

辑器或者文本编辑器创建和编辑。

第四章 C++ Builder 语言的扩充 第 225 页

RCDATA:RCDATA 为 Windows程序提供了一种扩展资源类型,RCDATA 可以是任何

类型的资源,比如声音、动画等都可以作为 RCDATA 类型的资源被加入可执行程序或者动

态链接库等。通常需要使用文本编辑器或资源编辑器编制资源脚本(RC文件),用以包含

最终的资源。 Stringtable:字符串表,在一个程序中,通常会有一些特定的字符串用于显示错误

信息、警告、提示等,这些信息的显示模式是类似的,但是内容随不同的要求在变,这时

候可以使用 StringTable 资源,将常用的字符串保存在可执行文件内部。字符串资源使

用文本编辑器或者资源编辑器创建和编辑。 VersionInfo:版本信息,版本信息对于 Windows 程序而言,不像 Linux 中要求

的那样严谨,但是良好的习惯可以为程序开发以及维护带来方便,有其在共享代码的程序

中,版本信息可以提供给调用模块和被调用模块之间一个良好的确认关系,可以让程序有

能力处理不同版本之间的代码差异。 其中RCDATA是Windows不提供处理代码的,Bitmap通常也需要用户干预,Dialog

是一个交互式的界面元素,Windows 负责将其按照用户预先设计的形状显示于桌面,但

其各个元素的操作需要和程序的代码对应,StringTable,需要代码根据实际情况自行

提取。其他的资源类型基本都是由 Windows 完成处理的,程序只要告诉系统,使用什么

资源就行了。 除了以上的类型以外,实际上 Windows 支持用户自定义的资源类型,尽管 RCDATA

可以支持任意类型的资源,但是 RCDATA类型并没有一个明确的含义,根据其名称并不能

判断是属于什么样的资源,比如程序需要加载一个 Jpg文件作为资源,可以使用以下脚本

(存在与.RC文件) BackGround RCDATA ".\BackGround.jpg" 在这个脚本中定义了一个名称为 BackGround 的资源,程序可以通过其名称对其进

行访问,类型为 RCDATA(用户的未知类型),该资源的实体为当前目录的

“BackGround.jpg”文件,这是正确的,但是不明确。如果使用以下脚本,则会更加

明确: BackGround JPEG ".\BackGround.jpg" 在这个脚本中,与前一个不同的是资源的类型为 JPEG类型,这是 Windows没有提

供的类型,但在程序中是允许的,类型为 JPEG,可以明确的指定该资源的数据类型是 Jpeg压缩图像。这对于程序设计的规范化而言是非常有利的。

3.1.2 Windows 资源的初始化

前面将 Windows 的资源类型进行了简单的说明,加下来将就 Windows 资源的初始

化,即 Windows 如何使用这些资源进行简单的介绍。我们先看一个 Windows 标准的资

第四章 C++ Builder 语言的扩充 第 226 页

源文件。 下面是一个典型的资源文件,这是使用 Borland C++的资源编辑器创建的资源脚本

文件。名称为 sample.RC,其中包含了三个资源,Menu、Dialog和 BitMap,如下:

#define IDB_BITMAP1 1

#define IDM_MENU1 1

#define IDD_DIALOG2 2

#define IDC_EDIT1 102

//以下是 Dialog的内容

IDD_DIALOG2 DIALOG 0, 0, 240, 150

STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE

| WS_CAPTION | WS_SYSMENU

CAPTION ""

FONT 8, "MS Sans Serif"

{

CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD

| WS_VISIBLE | WS_TABSTOP, 184, 48, 50, 15

CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD

| WS_VISIBLE | WS_TABSTOP, 184, 80, 50, 14

CONTROL "Edit1", IDC_EDIT1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER

| WS_TABSTOP, 8, 4, 60, 16

CONTROL "TabControl1", IDC_TABCONTROL1, "SysTabControl32", WS_CHILD

| WS_VISIBLE, 8, 24, 168, 116

}

//以下是 Menu的内容

IDM_MENU1 MENU

{

POPUP "&File"

{

MENUITEM "&New", 101

// ⋯⋯ ⋯⋯ ⋯⋯

MENUITEM SEPARATOR

MENUITEM "E&xit", 108

}

POPUP "&Help"

{

MENUITEM "&Help Topics", 901

MENUITEM "&What's This?", 902

MENUITEM SEPARATOR

第四章 C++ Builder 语言的扩充 第 227 页

MENUITEM "&About", 903

}

}

//以下是 BitMap的内容

IDB_BITMAP1 BITMAP

{

'42 4D 76 06 00 00 00 00 00 00 36 04 00 00 28 00'

'00 00 18 00 00 00 18 00 00 00 01 00 08 00 00 00'

// ⋯⋯ ⋯⋯ ⋯⋯ 位图数据

'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'

'FF FF FF FF FF FF'

}

资源脚本文件(RC 文件)同其他源文件一样,可以在其中定义一些标识符,因为

Windows需要对每一个资源分配一个唯一的标识号,用以区分各个资源,上面例子中的: #define IDB_BITMAP1 1

#define IDM_MENU1 1

#define IDD_DIALOG2 2

#define IDC_EDIT1 102

就是用来定义资源标识的,尽管 IDB_BITMAP1 和 IDM_MENU1 都是 1,但它们属于

不同的资源类型,因此上在系统中也是唯一标识的。这实际上就是为资源定义了一个名称,

也可以不这样去做,像前面的例子: BackGround JPEG ".\BackGround.jpg"

就没有使用定义的标识,而直接使用了资源的名称 BackGround,这对于程序的执行

没有任何影响,唯一的区别是,编译后的资源名称有所不同,使用定义标识符,编译后的

资源名称将会用较为简短的文字取代有固定含义的长字符串,而直接使用名称的资源编译

后其名称将会原封不动的被保存在可执行文件中。在早期的 Windows 程序中对一个可执

行文件的大小还比较关注,所以通常采用预先定义标识符,而在 C++ Builder中,一般

不提倡预定义的名称,因为资源往往是和对象联系在一起的,一个具有特定含义的名称不

仅方便程序设计,有时候还必须的,比如注册一个自行设计的组件,并且在组件面板中要

为其指定一个相应的图标,这时候就需要一个位图资源,而且这个资源的名称必须和组件

的名称一致,编译后才可能被 IDE正确的加载到组件面板。 对于资源的定义一般是: 资源名称 + 资源类型 + 资源的数据

Beibei
Highlight
Beibei
Comment on Text
Beibei
Note
Cancelled set by Beibei

第四章 C++ Builder 语言的扩充 第 228 页

图 4.2 对话框资源的显示结果

关于资源的名称和类型前面已经说过,资源的数据根据资源类型的不同也有所差异,

通常可以使用 {} 来表示数据块的起始和结束。在 Sample.RC中: IDD_DIALOG2 DIALOG 0, 0, 240, 150

定义了一个对话框资源,名称为 IDD_DIALOG2实际编译后的名称为 2,后面为这个对

话框的基本数据,左上角为(0,0),右下角为(240,150); STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE

| WS_CAPTION | WS_SYSMENU

CAPTION ""

FONT 8, "MS Sans Serif"

这一部分定义了对话框的风格;从这里可以看出,这个对话框中的基本数据其实就是

使用 Windows API函数 CreatWindow 创建一个窗体时的基本数据,当然创建窗体的基

本数据并不止这些,没有设置的数据均会按照缺省值进行创建。 到这里读者也许会明白资源和程序的关系,资源就是提供给程序使用的数据。一个简

单窗体,可以使用对话框资源来创建,也完全可以在程序中使用代码进行创建,但使用资

源显然要灵活的多,修改、增加也相对容易直观的多。 Dialog后面{}中的数据为扩展的数据块,它们不属于窗体本身的数据,而是在基本

窗体中增加的控件,这个例子中包含了两个按钮(Button)、一个编辑框(Edit)和一

个 TabControl1控件,对话框创建

后如图 4.2所示: 当程序中需要

创建这个对话框

时,会调用相应的

系统函数创建这个

窗体,并将{}中包

含的控件使用相应

的API函数创建于

这个窗体中,同时

系统需要作的还有

将这个窗体或者控

件的回调函数向系

统注册,以确保该

窗体的 Windows消息会被正确的处理函数接收到。 在 Sample.RC中 IDM_MENU1 MENU 表示定义一个 Menu资源,名称为 IDM_MENU1,

{}的数据表示菜单项的内容,和 Dialog类似,当程序需要创建或者使用该资源的时侯,

会自动的调用相应的 API函数去创建菜单,并将菜单发生的消息发送给相应的处理函数,

与 Dialog不同的是每一个菜单项都会有一个标识符:

第四章 C++ Builder 语言的扩充 第 229 页

MENUITEM "&New", 101

中的 101就是一个唯一的标识,用于在回调函数中区分属于那个菜单项的消息。关于

Windows 消息的详细内容参阅相关书籍,这里不多赘述。这个例子中的菜单显示结果如

图 4.3: 在 Sample.RC 中 另 一 个 资 源 为

BitMap类型: IDB_BITMAP1 BITMAP

表示定义了一个位图资源,其名称为:

IDB_BITMAP1,实际编译后的名称为:1,Bitmap的所有数据都在其后{}中,包括位

图的大小、颜色深度以及像素的各种信息

等。在这里纯文本方式来观察 Sample.RC很不直观,也可以使用另一种方法,将资源

数据指向到另外的文件中,比如: IDB_BITMAP1 BITMAP ”Bmp1.bmp” 这种文件指向可以是其他的 RC脚本文

件,其他的二进制资源文件(Res文件)或者和资源相对应类型的文件,上面的 IDB_BITMAP1

就对应了一个 Bitmap文件 Bmp1.bmp。 对于位图资源的使用往往不是由系统自行完成的,可能会和用户的代码有关,这里不

作例子,读者可以参考 SDK或者后面关于 C++ Builder中使用资源的例子。 简单的总结一下,Windows对资源的初始化可以使用下图 4.4说明: 对于大部分资源,如同图中显示的,是由 Windows API 来完成资源的载入、创建、

显示等等,应用程序仅仅是告诉系统下来该使用那个资源,甚至有些过程是在 Windows API和 User Interface 之间就完成的,而应用程序并不参与,比如在 Windows 95 以后弹出菜单会跟随鼠标移动,而应用程序并不知道在什么时候某个菜单项会处于什么状

态,这些都是 Windows 自行完成的(实际上是 Windows 的缺省消息处理函数进行处理

的),应用程序只需要接收菜单所发出的消息并处理就行了。 对于不是 Windows标准的资源,使用时并没有相应的 API 进行处理,这时候,通常

需要应用程序自行处理,而需要 Windows API 所作的仅仅是从程序中提取资源,并交给

应用程序。C++ Builder中使用的资源大多数属于这种情况。

图4.3 菜单资源的显示效果

第四章 C++ Builder 语言的扩充 第 230 页

3.2 C++ Builder 资源

在 C++ Builder设计的 Windows 应用程序中,资源仍然是不可缺少的部分,也正

如前面所说的,VCL是不同于其它以前的 OWL、FMC 等 FrameWorks的,在资源的使用

上也有很大的不同,同时 C++ Builder也是完全兼容 MFC和 OWL 的,也完全支持 C语

言 SDK中对资源定义的规范。 这一节我们简单的讲述一下 C++ Builder中资源的一些特点,有助于读者更好的理

解 VCL的运行机制,也能更好的使用 C++ Builder。

3.2.1 解读 dfm 文件

首先我们来看一个 C++ Builder中的资源文件,这个文件通常不需要程序员手工创

建,而是在 C++ Builder的 IDE中自动生成的,这就是 dfm文件。 下面的文件为一个典型的 Form 的资源(dfm 文件),在这个 Form 中,包含了一个

ToolBar、三个 ToolButton、一个 StatusBar、一个 ImageList 和一个 MainMenu,dfm文件的字符形式如下:

object Form1: TForm1

Left = 351

Top = 131

Width = 396

Height = 260

Caption = 'Form1'

Color = clBtnFace

Font.Charset = DEFAULT_CHARSET //属性为对象 Font,直接包含的

图 4.4 Windows 中对资源的使用

Code Section

Resource Section

Windows APIFunction

UserInterFaceFunc1

Func2

Func3

Resource1

Resource2

Resource3

Resource API

⋯ ⋯ ⋯显示资源

获取用户输入

输出资源

应用程序 系统 用户界面

第四章 C++ Builder 语言的扩充 第 231 页

Font.Color = clWindowText

Font.Height = -11

Font.Name = 'MS Sans Serif'

Font.Style = []

Menu = MainMenu1

OldCreateOrder = False

PixelsPerInch = 96

TextHeight = 13

object ToolBar1: TToolBar //包含对象,使用 Parent建立逻辑包含,

//并且头文件中有对象声明

Left = 0

Top = 0

Width = 388

Height = 29

Caption = 'ToolBar1'

EdgeBorders = [ebTop, ebBottom]

Flat = True

Images = ImageList1

TabOrder = 0

object ToolButton1: TToolButton //逻辑包含,Parent为 ToolBar

Left = 0

Top = 0

Caption = 'ToolButton1'

ImageIndex = 0

end

object ToolButton2: TToolButton

Left = 23

Top = 0

Caption = 'ToolButton2'

ImageIndex = 1

end

object ToolButton3: TToolButton

Left = 46

Top = 0

Caption = 'ToolButton3'

ImageIndex = 2 end

end

object StatusBar1: TStatusBar

第四章 C++ Builder 语言的扩充 第 232 页

Left = 0

Top = 195

Width = 388

Height = 19

Panels = <>

SimplePanel = False end

object ImageList1: TImageList

Left = 240

Top = 32

Bitmap = {

494C010103000400040010001000FFFFFFFFFF00FFFFFFFFFFFFFFFF424D3600

000000000000000000000000FFFFFF00FFFF882380000000F83FC00755550000

//⋯⋯⋯⋯

//位图文件

//⋯⋯⋯⋯

3C190001E6F200009C0B0001E6F200008C438003FEFE0000C467C6C700000000

E00FFC7F55550000F83FFFFF00000000}

end

object MainMenu1: TMainMenu

Left = 208 //Left和 Top属性只在设计时使用

Top = 32

object File1: TMenuItem //MainMenu的主菜单项

Caption = 'File'

object New1: TMenuItem //File菜单项的子菜单项,其 Parent为 File菜单项

Caption = 'New' end

object N2: TMenuItem

Caption = '-'

end

object Open1: TMenuItem

Caption = 'Open'

end

object Close1: TMenuItem

Caption = 'Save'

end

object SaveAs1: TMenuItem

Caption = 'Save As'

end

第四章 C++ Builder 语言的扩充 第 233 页

object Close2: TMenuItem

Caption = 'Close'

end

object N1: TMenuItem

Caption = '-'

end

object Exit1: TMenuItem

Caption = 'Exit'

end

end

object Edit1: TMenuItem

Caption = 'Edit'

object Cut1: TMenuItem

Caption = 'Cut'

end

object Delete1: TMenuItem

Caption = 'Delete'

end

object Copy1: TMenuItem

Caption = 'Copy'

end

object Pastl1: TMenuItem

Caption = 'Paste'

end

end

end

end

从这个文件可以看到 dfm和 Windows资源脚本文件非常相似,但也有很大的区别,

看起来 dfm文件倒更象一个 ini 文件的格式,在 C++ Builder中正是依靠这种格式来

记录一个 Form的所有信息。 面前也说过,C++ Builder 中,资源都是以对象为单位的,一个资源实际上就是一

个对象的信息,上面的 dfm文件在 C++ Builder的 IDE中的显示形状如图 4.5: 运行后的形状和图 4.5中的 Form是一致的,除了 TmainMenu和 TimageList是

组件不在 Form中显示以外,其它的控件和设计时是一样的。我们观察一下 dfm文件,发

现对一个 Form来说基本结构是这样的: object ObjectName : ObjectClass ObjectProperty = PropertyValue

第四章 C++ Builder 语言的扩充 第 234 页

//⋯⋯⋯⋯ object ObjectName:ObjectClass //子对象,允许多重嵌套 //⋯⋯⋯⋯ end end

在上面的例子中,顶层的 ObjectName 为 Form1,ObjectClass 为 TForm1,每

一个对象总是以 object 开始,以 end 结束,中间的内容是这个对象的属性值,包括事

件和其它的对象,嵌套在一个对象内部的对象称作子对象,其 Parent 属性就是上一层对

象,比如在上面的例子的 Form1 中,包含了 ToolBar1、StatusBar1、ImageList1和 MainMenu1 等四个子对象,而 TollBar1 中包含了三个子对象,分别是

ToolButton1、TollBuitton2 和 TollButton3,ToolBar 就作为三个 ToolButton的容器控件 Parent在程序中出现,三个 ToolButton 都是 ToolBar 的 Child 控件。

MainMenu的情况和 ToolBar相似。 在 dfm文件中,对象属性的值和在 IDE中设置的值是一致的,实际上 C++ Builder

正是通过 dfm文件将设计时的属性记录下来的,在设计一个控件的时候,可以定义某个属

性是否需要存储,将属性的 stored部分设置为 false 的时候,属性在 IDE中的设置不

会存储在 dfm文件中,只有定义成需要存储的属性,并且在 IDE中设置的值和其缺省值

不相同的时候才会被存储在 dfm 文件中,这在前面讲述__property 关键字的时候已经

讲过,不过事件在 dfm文件中总是被存储下来的,而且事件这个特殊的属性没有缺省值,

也不能定义是否存储,而且通常必须是可读可写的属性。

3.2.2 C++ Builder 资源的初始化

Dfm 文件在 C++ Builder 中编译以后会生成一个 res 的二进制的资源文件,而每

图 4.5 C++ Builder 中的资源显示效果

第四章 C++ Builder 语言的扩充 第 235 页

图 4.6 C++ Builder 中的资源的使用

Code Section

Resource Section

Windows APIFunction

UserInterFaceObj1 Constructor

Obj2 Constructor

Obj3 Constructor

Object1

Object2

Object3

Resource API

⋯ ⋯ ⋯

显示资源

获取用户输入

输出资源

VCL Package

VCL Function

⋯ ⋯ ⋯

一个 Form(最顶层的对象,也可能是 DataMoudel 等对象)会被当成一个资源来处理,

其类型是 RCDATA 用户自定义类型,这种类型的资源 Windows API 是没有对应的 API函数来处理的,只能由应用程序来进行处理。

在所有的 VCL 类库中,从 TPersistent 类开始,包括它的派生类中都已经包含了

对流式数据的存储和读取,在 TComponent 类之后,增强了从流(TStream)存储和读

取组件的能力,而在 dfm文件中的对象大多数都是从 TComponent派生的组件,也有一

些是从 TPersistent 派生的类,资源文件中所有的组件在单元的头文件中都有声明的原

型,而从 TPersistent 派生的类一般在头文件中没有相应的声明原型。一个使用 C++ Builder 编制的应用程序和其它语言设计的程序一样,都包含代码部分和资源部分,但

它们的初始化过程有所不同,采用 C++ Builder设计的程序其资源的初始化过程可以用

下图 4.6来表示:

图中显示,应用程序在获取资源的时候,首先需要调用 VCL 资源的处理函数,诸如

LoadFromStream 等方法,将经过编译而生成的二进制资源解释为每个对象所需要的数

据,然后每个对象调用相应的构造函数,来生成对象,并最终由 Windows API 函数完成

窗体的创建、显示图形等资源的输出和显示。 在 VCL中有一些控件并不是 Windows的 API所提供的,对于这一些对象在创建的时

候,很可能是由 VCL直接向用户输出而不是通过一定的 Resource API 来完成的。除此

之外,在 C++ Builder中,同样可以使用 Windows的标准资源,用户可以象 SDK 编程

那样来使用资源。

3.2.3 在 C++ Builder 中使用资源文件

C++ Builder 可以支持所有的 Windows 标准资源类型,但通常是没有必要这么作

的,因为 VCL所提供的 IDE已经非常的强大,在某些时候,也许会用到一些特殊的资源,

比如声音、压缩图像等等,这些都不是 Windows 的标准资源,而且 VCL 也没有对这些资

源也没有进行相应的封装,这时就需要使用资源文件来完成这些功能。 在下面的这个例子中,使用了 Windows 的标准资源类型 Menu、Icon、Cursor、

Bitmap和一个用户类型的资源 JPG类型,其中 Menu、Icon、Cursor和 Bitmap 仅仅

第四章 C++ Builder 语言的扩充 第 236 页

是为了演示一下如何在 C++ Builder 中使用标准资源,而这个 JPG 示范才是很可能是

在实际中用到的。 在这个例子中有一个主程序和一个动态连接库,主程序中包含了一个资源文件,里面

有一个 Windows的 Menu、Icon 和 Cursor资源,动态连接库中包含了一个 JPG的资

源,程序运行后会载入 Icon、Menu和 Cursor,当用户点击 Button1是程序载入 Bitmap资源并显示在主窗体中,当用户点击 Button2 时,程序会自动加载动态连接库,并将其

中的资源载入主窗体中的 Image控件。 程序的部分代码如下: TForm1 *Form1;

const crMyCursor = 5; //载入光标资源使用

//--------------------------------------------------

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{ /* 空构造函数 */ }

//--------------------------------------------------

void __fastcall TForm1::FormCreate(TObject *Sender)

{

HMENU hMenu; //定义一个 Menu句柄

Screen->Cursors[crMyCursor] = LoadCursor(HInstance, "IDC_CURSOR1");

if(Screen->Cursors[crMyCursor])

Cursor = crMyCursor; //通过 Screen对象将光标载入并供程序使用

Icon->ReleaseHandle(); //Form加载 Icon前首先要释放,VCL的需要

Icon->Handle = LoadIcon(HInstance,"IDI_ICON1"); //载入图标 IDI_ICON1

hMenu = LoadMenu(HInstance,"IDM_MENU1"); //载入 Menu,IDM_MENU1

if( hMenu )

if(!::SetMenu( Handle,hMenu )) //将 Form的菜单设置为 hMenu

Application->MessageBox("error","Error",0);

}

//------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)

{ //点击 Button1的事件处理函数

Graphics::TBitmap *Bitmap1 = new Graphics::TBitmap();

try

{

Bitmap1->LoadFromResourceName((unsigned int)HInstance, "BITMAP1");

Canvas->Draw(12,12,Bitmap1); //将位图显示在窗体中

} //使用 LoadFromResourceName方法载入位图,比 API有更好的适用性

catch (...)

第四章 C++ Builder 语言的扩充 第 237 页

{

Application->MessageBox("error","Error",0);

}

delete Bitmap1;

}

//--------------------------------------------------

void __fastcall TForm1::Button2Click(TObject *Sender)

{ //点击 Button2的事件处理函数

TMemoryStream *Stream = new TMemoryStream();

TJPEGImage * Jpg = new TJPEGImage();

void *Buffer;

int Length;

HMODULE Module;

HRSRC ReSource;

HGLOBAL hBuffer;

//以上为临时变量

Module = LoadLibrary("Res.dll"); //加载动态连接库 Res.dll

ReSource = FindResourceEx(Module,"JPG","DllRes",

MAKELANGID(LANG_NEUTRAL,SUBLANG_NEUTRAL));

//查找资源 DllRes,类型为 JPG,JPG为用户自定义的类型

Length = SizeofResource(Module,ReSource); //确定资源的长度

hBuffer = LoadResource(Module,ReSource); //载入资源数据

Stream->WriteBuffer(hBuffer,Length); //将资源数据导入 Stream对象

Stream->Position = 0;

Jpg->LoadFromStream(Stream); //将数据导入 JPG图形对象

Image1->AutoSize = true;

Image1->Picture->Assign(Jpg); //显示 Jpeg图形

Image1->AutoSize = false;

FreeLibrary(Module); //释放动态连接库

delete Stream;

delete Jpg;

}

应用程序可以通过动态连接库来使用特定的资源,这样的好处是可以最大限度的节省

应用程序占的系统资源,比如程序的启动画面可能会包含一个图形,而这个图形在程序启

动以后就不再需要,这时就可以通过动态连接库的资源来加载。 使用动态连接库来加载资源的另一个用处在于隐藏了实际资源的数据,且方便程序更

换不同的资源而不需要重新编译整个应用程序,只需要将相应的模块重新编译就可以了。

这个例子可以按照一下步骤来实现:

第四章 C++ Builder 语言的扩充 第 238 页

一.创建一个新的 Project,在 Form中加入一个 Image控件,两个 Button 控件。 二.在 Project的 Form单元文件中加入 Jpeg单元的包含信息。 三.创建一个资源脚本文件 res.rc并将其加入到 Priject中。 四.创建一个任意的 dll工程,并命名为 Res.dll。 五.创建一个资源脚本文件 dllres.rc,并加入到 Res.dll工程中。 六.将上面的几个事件处理函数加入单元文件。 这个例子中有两个资源脚本文件,可以按照下面的例子编写: Res.rc: IDC_CURSOR1 CURSOR

{

'00 00 02 00 01 00 20 20 00 00 00 00 00 00 30 01'

//光标资源的数据部分,也可以指定一个特定的光标文件

'FF FF FF FF FF FF'

}

BITMAP1 BITMAP

{

'42 4D 76 08 00 00 00 00 00 00 76 00 00 00 28 00'

//位图资源的数据部分,也可以指定一个特定的位图文件

'99 99 99 99 99 99'

}

IDI_ICON1 ICON

{

'00 00 01 00 01 00 20 20 10 00 00 00 00 00 E8 02' //图标资源的数据部分,也可以指定一个特定的图标文件

'FC A1 FF F9 FE BC FF F9 FE 3E FF F9 FF FF'

}

IDM_MENU1 MENU

{

POPUP "&MenuTest"

{

MENUITEM "&Item1", 101

MENUITEM "&Item2", 102

MENUITEM "&Item3", 103

}

}

第四章 C++ Builder 语言的扩充 第 239 页

这个资源脚本可以使用 Resource Workshop,或者 Borland C++等工具编写。另

一个资源文件 dllres.rc如下: DllRes JPG "Test.jpg" 其中 Test.jpg可以是任意的 Jpeg类型文件。关于这个例子可以在本书所附的例程

中找到完整的代码。

4 C++ Builder的异常处理

在上一章讲面向状态的思想时,我们已经提到过,C++ Builder使用了异常处理机

制来保证一段代码可以可靠、完整的执行,而不必要一步一步小心翼翼的,使程序的设计

更加符合人类的习惯。异常处理并不是 C++ Builder所特有的,在标准的 C++语言中本

身就支持异常处理,而且在 Win32 中,对于异常处理又加入了更多的内容,这一节将简

单的就 C++ Builder中的异常处理作以介绍。 C++ Builder 支持 C++异常处理、基于 C的结构异常处理以及 VCL异常处理。本节

中所用的关于 C++异常处理及结构异常处理的例子通过命令行使用 bcc32.exe可成功地

编译并运行,而不是使用 IDE。在调用标准 C++例程及对象时可用 C++异常处理。VCL 异

常处理可在 IDE内进行。实际上,C++ Builder 同样支持 C++异常处理和基于 C的结构

异常处理。但使用 C++ Builder 和 VCL 可开发包含内嵌异常处理的例程的应用程序,

这些例程可以在出现错误时自动发送异常。

4.1 C++异常处理

异常是指需要特殊处理的例外情况,包括运行时发生的错误,如除数为零,存储空间

不足等。异常处理提供了一种标准的方法以处理错误,发现可预知及不可预知的问题,及

允许开发者识别、查出和修改错漏之处(bugs)。到了 C++ Builder 的 VCL 中异常处理

并不只是提供了调试、测试等基本功能,而且成为程序正常运行不可缺少的部分。

4.1.1 异常处理的 ANSI 规定

C++ Builder异常处理支持 ANSI/ISO提议的 C++工作标准。发送异常可允许你收

集发送点的信息,这将有助于诊断异常发生的原因。可使用异常处理程序来确定程序终止

之前的操作。只有同步异常(错误都由程序内部引起)可被处理。外部事件(例如按下

Ctrl+C组合键)不被认为是一个异常。C++语言规定所有的异常都应在一个 try-block中被发送。这个块之后紧接着是一个或多个 catch 块,用于识别和处理 try-block 中

发生的错误。

第四章 C++ Builder 语言的扩充 第 240 页

4.1.2 异常处理语法

异常处理要求使用三个关键字:try、catch及 throw。程序通过可能产生特殊状况

的 trying 语句以准备捕捉异常。当 C++程序发送一个异常时,可将控制转移或 throw到程序另外的被称为异常处理程序的、用于处理该类异常的部分中去。这种处理程序被称

为 catch异常。程序通过执行 throw语句来发送异常。throw 语句通常在一个函数内发

生,如: throw "overflow"; 在这个例子中,语句发送一个描述异常类型的对象,这是一个算术运算溢出。从而程

序的另一部分可捕捉并处理这个异常对象。要使用异常处理,需将代码封闭在一个

try/catch结构中,try/catch结构的语法如下: try-block: try compound-statement handler-list

handler-list:

handler handler-list //handler-list是可选的

handler:

catch (exception-declaration) compound-statement

exception declaration:

type-specifier-list declarator

type-specifier-list abstract-declarator

type-specifier-list

...

throw-expression:

throw assignment-expression //assignment-expression是可选的

注意 C程序中不支持 try、catch及 throw关键字。一个由 try确定的 try-block后必须紧接着一个由 catch 确定的处理程序。try-block 是一段用于确定程序执行时的

控制流的语句。若一个异常在 try-block 中被发送,程序控制被转移到适当的异常处理

程序。处理程序是一段被设计来用于处理异常的代码。C++语言要求至少有一个处理程序

紧接着 try-block。程序应包括一个可处理程序中可能发生的各个异常的处理程序。

4.1.3 声明异常

虽然 C++允许异常可为任意类型,但异常对象还是很有用的。异常对象可当作其他对

象一样看待。异常对象将异常发送点的信息携带到异常对象被捕捉的地方。应用程序的用

户在程序运行时遇到不正常的情况时会很需要这些信息。由 C++语言预定义的异常,在前

面结束异常类可以找到,或在联机帮助中的“Library Reference”中找到更为详细的

第四章 C++ Builder 语言的扩充 第 241 页

解释。

4.1.4 发送异常

可能发生异常的代码块需以 try为前缀同时封闭在花括号内。这表明程序准备检查异

常。若异常发生,则程序流中断,然后会发生: n 程序搜索匹配的处理程序。 n 若找到处理程序,则栈在该点被打开。 n 程序控制转到处理程序。 n 若没有找到处理程序,可调用 set_terminate()以提供终止处理程序,否则,

程序会调用 terminate函数。若无异常被发送,程序正常执行。 当异常发生时,throw语句初始化一个 T类型(匹配 arg参数的类型)的临时对象

在 throw(Targ)中使用。对象的其他拷贝可在编译器需要时生成。因此,对于包含子

对象的类来说,定义一个拷贝结构很有用(对于按位拷贝无须定义拷贝结构)。 下列例子说明了几种发送异常的方法。这些例子通过命令行使用 bcc32.exe可成功

地编译并运行,而不需使用 IDE,例子中在没有处理程序存在是调用 terminate

/*程序执行结果:

*** Nobody threw an exception ***

*/

#include <except.h>

#include <process.h>

#include <stdio.h>

bool pass;

class Out{};

void my_terminate(){ puts("*** Nobody threw an exception ***"); exit(1); }

void festival(bool firsttime){ if(firsttime) throw Out(); }

void test()

{

try { festival(false); }

catch(Out& e){ pass = false; }

throw; //这里不会继续抛出异常

}

int main()

{

set_terminate(my_terminate); try { test(); }

catch(Out& e){ pass = true; }

return pass ? (puts("got out of test"),0) : (puts("still have test"),1);

}

第四章 C++ Builder 语言的扩充 第 242 页

4.1.5 异常规范

C++提供一种称为议程规范(exception specification)的特性可将一个函数

可能发送的异常在声明中列出。 exception specification 使用时类似于函数声明的后缀,句法如下: exception-specification:

throw (type-id-list) //type-id-list为可选的

type-id-list:

type-id

type-id-list, type-id

这样的函数后缀不是函数类型的一部分。因此,指向函数的指针不会受 exception specification 的影响。像这样的指针只检查函数返回值和参数类型。因此,下列声明

是合法的: void f2(void) throw(); //没有异常被抛出

void f3(void) throw (BETA); // 只能抛出 BETA异常类

void (* fptr)(); // 返回为 void的函数指针

fptr = f2;

fptr = f3;

当重载虚拟函数时需要小心。因为 exception specification 不是函数类型的一

部分,所以可能会破坏程序设计。 例子:在下例中,定义的派生类 BETA::vfunc 不发送任何异常,这偏离了原始的函

数声明。

class ALPHA

{

public:

struct ALPHA_ERR {};

virtual void vfunc(void) throw (ALPHA_ERR) {} // Exception specification

};

class BETA : public ALPHA

{

void vfunc(void) throw() {} // Exception specification被改变

};

第四章 C++ Builder 语言的扩充 第 243 页

下例是带有异常规范的函数。

void f1(); // 该函数可以抛出任意类型的异常

void f2() throw(); // 不能抛出任何类型的异常

void f3() throw( A, B* ); // 可以抛出一个 A或其派生类的异常,或者 B类或其派生

// 类的指针异常

这样的函数的定义和所有的声明必须有一个包含相同的 type-ids集的异常规范。若

函数发送一个规范中未列出的异常,程序会调用 unexpected。

4.1.6 异常处理的构造和析构

若不能成功地构造一个对象,类构造函数会发送异常。当按值发送对象且异常被引发

时,拷贝构造函数被调用。拷贝构造函数在发送点初始化一个临时对象。应用程序也可产

生另外的拷贝。如果构造函数发送了异常,对象的析构函数就不需要被调用。析构函数仅

为基类和那些在 try-block中构造成功的对象调用。为 try-block 中构造的 throw语

句后的对象调用析构函数的进程称为 stack unwinding 。若析构函数在这个进程中引

发一个异常且没有处理,则 terminate被调用。析构函数是缺省调用的,不过可通过使

用-xd-编译器选项关闭缺省设置。

4.2 Win32 下的结构异常

Win32 支持与 C++异常类似的基于 C 的结构异常处理。不过还是有一些关键性的差

别,当与 C++代码混用时需要小心地使用。在 C++ Builder 应用程序中使用结构异常处

理时需注意: n C结构异常可在 C++程序中使用。 n C++异常不能在 C 程序中使用,因为 C++异常要求处理程序由 catch 关键字指

定,而 catch关键字在 C程序中是不允许的。 n 一个由调用 RaiseException函数生成的异常应在一个 try/__except(C++)

块中或 __try/__except(C)块中处理 (也可使用 try/__finally 或

__try/__finally 块。当 Raise Exception 函数调用时所有 try/catch块中的处理程序都被忽略。

n 没有被应用程序处理的异常不会导致调用 terminate(),而是被传递给操作系

统(通常来说,操作系统最终处理的结果也是结束进程)。 n 异常处理程序不接受异常对象的拷贝,除非它们请求如此。 可在 C或 C++程序中使用下列异常函数: n GetExceptionCode 。 n GetExceptionInformation 。 n SetUnhandledExceptionFilter 。 n UnhandledExceptionFilter 。

第四章 C++ Builder 语言的扩充 第 244 页

C++ Builder 没有限制在__try/__except 过滤器或 try/__except 块之外使用

UnhandledExceptionFilter 函数。但是,当这个函数在__try/__except 过滤器或

try/__except块之外调用时,程序行为不确定。

4.2.1 结构异常的语法

在 C程序中,实现结构异常的 ANSI兼容关键字为__except、__finally和__try。注意__try 关键字仅能在 C 程序中使用。若想要编写可移植的代码,就不要在 C++程序

中使用结构异常处理。 try-except异常处理语法如下: try-block:

__try compound-statement (in a C module)

try compound-statement (in a C++ module)

handler:

__except (expression) compound-statement

try-finally termination syntax

try-finally终止语法如下:

try-block:

__try compound-statement (in a C module)

try compound-statement (in a C++ module)

termination:

__finally compound-statement

结构异常可使用扩展的 C++ 异常处理:

try

{

foo();

}

__except(__expr__)

{

//异常处理

}

4.2.2 异常过滤器

一 个 过 滤 器 表 达 式 可 调 用 过 滤 器 函 数 , 但 是 过 滤 器 函 数 不 能 调 用

GetExceptionInformation。可以把 GetExceptionInformation 的返回值作为一个参数传

递给过滤器函数。为传递 EXCEPTION_POINTERS 信息到一个异常处理程序,过滤器表达式

第四章 C++ Builder 语言的扩充 第 245 页

或过滤器函数必须从GetExceptionInformation拷贝指针或数据到处理器以后可访问它的

地方。在嵌套的 try-except 语句中,每个语句的过滤器表达式都会被计算,直到它定位

到 EXCEPTION_EXECUTE_HANDLER 或 EXCEPTION_CONTINUE_EXECUTION。过滤器表达式能调用

GetExceptionInformation 以 得 到 异 常 信 息 。 只 要 GetExceptionInformation 或

GetExceptionCode 直接在提供给__except 的表达式中被调用,你可使用这个函数决定如

何处理异常而非创建一个复杂的 C++表达式。几乎所有处理异常需要的信息都能从

GetExceptionInformation()的结果中提取。GetExceptionInformation()返回一指针到

EXCEPTION_POINTERS 结构:

struct EXCEPTION_POINTERS

{

EXCEPTION_RECORD *ExceptionRecord;

CONTEXT *Context;

};

EXCEPTION_RECORD包含与硬件无关的状态:

struct EXCEPTION_RECORD

{

DWORD ExceptionCode;

DWORD ExceptionFlags; struct EXCEPTION_RECORD *ExceptionRecord;

void *ExceptionAddress;

DWORD NumberParameters;

DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

};

GetExceptionInformation()返回一指针到 EXCEPTION_POINTERS结构: EXCEPTION_RECORD 包含与硬件无关的状态:通常,过滤器函数在 ExceptionRecord 中

搜 索 信 息 以 决 定 如 何 响 应 。 有 时 需 要 一 些 更 特 殊 的 信 息 ( 特 别 是 操 作 为

EXCEPTION_CONTINUE_EXECUTION:若没做任何事,引起异常的代码将再被执行)。对于这

种状况,EXCEPTION_POINTERS 结构中另外的字段提供异常发生时的处理器状态。如果这个

结构被修改并且过滤器返回 EXCEPTION_CONTINUE_EXCEPTION,它将被用来在执行继续前设

置线程的状态。例如:

static int xfilter(EXCEPTION_POINTERS *xp)

{

int rc;

EXCEPTION_RECORD *xr = xp->ExceptionRecord;

第四章 C++ Builder 语言的扩充 第 246 页

CONTEXT *xc = xp->Context; switch (xr->ExceptionCode) {

case EXCEPTION_BREAKPOINT:

//内嵌的断点,步进!

++xc->Eip;

rc = EXCEPTION_CONTINUE_EXECUTION; break;

case EXCEPTION_ACCESS_VIOLATION:

rc = EXCEPTION_EXECUTE_HANDLER;

break;

default:

//放弃

rc = EXCEPTION_CONTINUE_SEARCH;

break;

};

return rc;

}

...

EXCEPTION_POINTERS *xp;

try

{

func();

}

__except(xfilter(xp = GetExceptionInformation()))

{

abort();

}

4.2.3 在 C++中混用结构异常

在 C++程序中混用结构异常时需要了解几项内容。首先,尽管 C++ Builder 用 Win32结构异常实现 C++异常,C++异常对__except 块来说仍是透明的。一个 try块后可跟随

一个 except 块或至少一个 catch 块。若试图混用二者会产生一个编译错误。需要处理

两种类型异常的代码应该简单地在两个 try块内嵌套: try

{

EXCEPTION_POINTERS *xp;

try

{

第四章 C++ Builder 语言的扩充 第 247 页

func();

}

__except(xfilter(xp = GetExceptionInformation()))

{

//...

}

}

catch (...)

{

//...

}

函数 throw()规范不会影响程序关于 Win32 异常的行为。另外,未处理的异常最后

由操作系统处理(若调试器不首先处理它),不像 C++程序那样调用 terminate()。任

何使用-xd 编译器选项(缺省打开)编译的程序块将调用所有“自动”存储的对象的析构

函数。打开栈的操作从异常发生点到异常被捕捉点间发生。 C++程序中基于 C的异常示例

/*程序执行结果:

Another exception:

Caught a C-based exception.

Caught C++ exception[Hardware error: Divide by 0]

C++ allows __finally too! */

#include <stdio.h>

#include <string.h>

#include <windows.h> class Exception

{

public:

Exception(char* s = "Unknown"){ what = strdup(s); }

Exception(const Exception& e ){ what = strdup(e.what); }

~Exception() { delete[] what; }

char* msg() const { return what; }

private:

char* what;

}; int main()

{

float e, f, g;

第四章 C++ Builder 语言的扩充 第 248 页

try

{

try

{

f = 1.0;

g = 0.0; try

{

puts("Another exception:");

e = f / g;

} __except(EXCEPTION_EXECUTE_HANDLER)

{

puts("Caught a C-based exception.");

throw(Exception("Hardware error: Divide by 0"));

}

}

catch(const Exception& e)

{

printf("Caught C++ Exception: %s :\n", e.msg());

}

}

__finally

{

puts("C++ allows __finally too!");

} return e;

}

4.2.4 __finally 终止块

结构异常处理模型支持“终止块”,在被保护块正常退出后执行或经由异常。C++ Builder编译器在 C中以下列语法支持它:

__try

{

func();

}

__finally

{

第四章 C++ Builder 语言的扩充 第 249 页

//不管 func是否抛出异常,该段代码都会被执行

}

下例说明如何使用终止块:

/*程序执行结果:

An exception:

Caught an exception.

The __finally is executed too!

No exception:

No exception happened, but __finally still executes! */

#include <stdio.h>

#include <windows.h>

int main()

{ float e, f, g;

try

{

f = 1.0;

g = 0.0; try

{

puts("An exception:");

e = f / g;

} __except(EXCEPTION_EXECUTE_HANDLER)

{

puts("Caught an exception.");

}

} __finally

{

puts("The __finally is executed too!");

}

try

{

f = 1.0;

g = 2.0;

第四章 C++ Builder 语言的扩充 第 250 页

try

{

puts("No exception:");

e = f / g;

}

__except(EXCEPTION_EXECUTE_HANDLER)

{

puts("Caught an exception.");

}

}

__finally

{

puts("No exception happened, but __finally still executes!");

}

return e;

}

C++代码也能通过创建局部对象处理终止块,这些对象在范围退出时调用析构函数。

由于 C++ Builder 结构异常支持析构清理,这使得程序可以不用考虑异常的类型。注意

有一个需要担心的情况,当异常被引发并没有处理程序时。对于 C++异常,C++ Builder编译器为局部对象调用析构函数(不要求由语言定义),而未处理的 Win32异常,析构清

理则不会发生。

4.3 VCL 异常处理

在应用程序中使用 VCL组件时,需要理解 VCL 异常处理机制。因为异常被内置于许

多类中,并且当意外事件发生时自动地被发送。若不处理异常,VCL将以缺省方式处理。

通常,是显示描述发生错误类型的消息。当编程时遇见显示的一条指示被发送异常类型的

消息时,可在 VCL Reference中查一下异常类。提供的信息经常能够帮助你决定错误在

何处发生及其原因。另外,前面介绍了可能引起异常的语言差异,以及在对象构造期间发

送的异常。

4.3.1 C++和 VCL 异常处理之间的差别

下面是 C++和 VCL异常处理之间一些需注意的差别。 由构造函数发送的异常: n C++析构函数应为被完整构造的基类及成员调用。 n VCL基类析构函数甚至在对象或基类未被完整构造时也调用。 捕捉并发送异常: n C++异常可以通过引用、指针或值捕捉。VCL 异常是从 TObject 派生出的异常

第四章 C++ Builder 语言的扩充 第 251 页

类,仅可通过引用或指针捕捉。试图通过值捕捉 TObject异常会导致编译错误。

硬件或操作系统异常,例如 EAccessViolation,应通过引用捕捉。 n VCL异常通过引用捕捉。 n 不能使用 throw重新发送在 VCL代码内被捕捉的异常。

4.3.2 处理操作系统异常

C++ Builder 允许处理操作系统引发的异常。操作系统异常包括越权访问、整数运

算错、浮点运算错、栈溢出以及 Ctrl+C中断。这些在 C++ RTL中处理,并且在分发到

应用程序前被转换为 VCL异常类。可以编写如下的 C++代码: try

{

char *p = 0;

*p = 0;

}

//必须始终以引用的方式 catch

catch (const EAccessViolation &e)

{

printf("You can't do that!\n");

}

C++ Builder 使用的类与 Delphi 的相同,并仅存在于 C++ Builder VCL 应用

程序中。从 TObject派生并要求 VCL支持。下面是 C++ Builder 异常处理的一些特征: n 不负责释放异常对象。 n 操作系统异常应通过引用捕捉。 n 在操作系统异常被相关的 VCL catch 块捕捉,并且 catch 块结束后不能重新

发送操作系统异常。 n 在操作系统异常被相关的操作系统 catch块捕捉,并且 catch块结束后不能重

新发送操作系统异常。 最后两点可以粗略地说:一旦操作系统异常作为 C++异常被捕捉,就不能作为操作系

统异常或 VCL异常被重新发送,除非在 catch栈框架之内。

4.3.3 处理 VCL 异常

C++ Builder为处理由 VCL引发的软件异常或由 C++引发的 TObject 派生的异常

类而拓宽语义。在这种情况下,由于 VCL风格类仅能在堆中被分配,从而有下面两个规则。 n VCL风格异常类只能通过指针捕捉,若其为软件异常也可以通过引用(通常推荐

第四章 C++ Builder 语言的扩充 第 252 页

使用引用)捕捉。 n VCL风格异常应该用“by value”语法发送。 C++ Builder 包括一大组内置异常类用以自动处理诸如除数为零错误、文件 I/O 错

误、无效类型转换及许多其他的异常状况。正如前面章节说讲述的,所有的 VCL异常类都

从一个根对象 Exception派生而来。Exception 封装了所有异常类的基本属性和方法,

并且为应用程序处理异常提供一致的接口。可以用一个 Exception类型的参数把异常传

递给 catch块。可以使用下列语法捕捉 VCL异常: catch (const exception_class &exception_variable)

确定想要捕捉的异常类并提供一个变量以引用它。下面是如何发送 VCL 异常的例子: void __fastcall TForm1::ThrowException(TObject *Sender)

{

try

{

throw Exception("VCL component");

}

catch(const Exception &E)

{

ShowMessage(AnsiString(E.ClassName())+ E.Message);

}

}

在前例中 throw 语句创建异常类的一个实例并且调用其构造函数。所有由

Exception 派生而来的异常都有可被显示的消息,通过构造函数传递并通过消息属性检

索。下面给出了由 Exception派生的一些 VCL内置的异常类及其用途,关于 Exception类及其派生类的详细内容参见前面相关内容和 C++ Builder联机帮助。

异常类 类描述: EAbort 不显示错误消息对话框,终止事件序列 EAccessViolation 检查内存访问错误 EBitsError 阻止对布尔型数组无效访问 EComponentError 表示试图无效注册或重命名组件 EConvertError 指出字符串或对象转换错误 EDatabaseError 指出一个数据库访问错误 EDBEditError 捕捉与指定模式不兼容的数据 EDivByZero 捕捉整数零除错误

第四章 C++ Builder 语言的扩充 第 253 页

EExternalException 代表未被识别的异常代码 EInOutError 代表一个文件 I/O错误 EIntOverflow 指出其结果对分配的寄存器太大的整数计算 EInvalidCast 检查不合法的类型转换 EInvalidGraphic 指出欲使用未被识别的图形文件格式 EInvalidOperation 试图对组件非法操作时发生 EInvalidPointer 源于非法指针操作 EMenuError 菜单项包含的问题 EOleCtrlError 检测 ActiveX控件的问题 EOleError 指定 OLE自动化错误 EPrinterError 表明打印错误 EPropertyError 设置属性值失败时发生 ERangeError 指出整数值对其声明类型来说太大 ERegistryException 指出注册表错误 EStackOverflow 当栈进入最后的保护页时发生 EZeroDivide 捕捉浮点零除错误 从上面的表可看到,内置 VCL异常类为你处理了大部分异常并能简化代码。这样可方

便你创建自己的异常类处理唯一的状况。可声明一个新异常类为 Exception的派生类,

和 Exception有同样多的构造函数(或从 SYSUTILS.HPP拷贝一个已有的类构造函数)。

5 小结

本节主要讲述了 C++ Builder 中的一些关键字及语法的扩充;C++ Builder 的 VCL 基

本类库的结构即主要属性等;C++ Builder 中资源的使用以及 C++ Builder 中异常处理的

基本特征,通过本节内容,相信读者会对 C++ Builder 的一些运行机制有个更加深入的了

解,这些内容有些并不是使用 C++ Builder 所必须的,但是了解这些内容可以更好的、更

自由的驾驭 C++ Builder。

第五章 控件的设计 第 255 页

第二篇 应用 C++ Builder 的面向状态

第五章 使用 C++ Builder

前面几章讲述了面向状态的基本思想和 C++ Builder的语言特征,侧重于知识性,

从这一章开始,我们将要针对应用 C++ Builder来介绍如何使用面向状态。有一点还要

声明,本书并不是 C++ Builder的使用手册,因此本章并不对如何使用 C++ Builder进行详细的讲解,而主要是就面向状态的一些使用方法进行讲述,读者需要有一定的使用

基础和编程经验,否则阅读本章并无多大帮助。 面向状态的方法是源于组件技术的,本章要讲的也是在 VCL组件设计中如何使用 C++

Builder 的面向状态,在应用程序中使用面向状态技术和组件设计中大同小异,而且更

简单一些,能够设计组件的时候自然而然的能够在应用程序中使用面向状态的技术。

1 C++ Builder中的命名约定

任何一种编程工具,都会有自己的命名规则或者叫命名约定,C++ Builder也不例

外,实际上,关于变量的名称和程序本身并没有多大关系,也不会产生实质性影响,但是

采用一套好的命名规则对程序的设计会带来许多便利。 熟悉 Windows SDK 的读者都知道在 SDK 中采用了一种叫做“匈牙利命名规则”的

约定,在这种约定中可以非常容易的通过变量的名称来判断变量属于那一种数据类型,比

如 hWnd是一个 HWND类型的窗口句柄;lpText 是一个 LPCTSTR 类型,其含义是指向字

符串的 long point长指针,HWND和 LPCTSTR 是在 Windows.h 中预定义的数据类型,

其名称的含义也显而易见。 在 C++ Builder 中,Borland 公司提供了一种更加适合面向状态的命名规则,在

这种规则里面,也许你并不能根据其名称判断数据或者属性是什么数据类型,但从其名称

可以很容易的判断出其用途或者在对象中真正的含义。 C++ Builder 并不对如何命名方法或参数施加限制,但是遵守这一约定可以方便应

用程序开发人员使用方法。你也可以完全不遵守 Borland 公司的命名规则,而在开发程

序或者组件的时候使用一种自己熟悉的约定,但是从一致性来考虑,建议使用 Borland在 VCL中的命名规则是非常有必要的,为此首先要了解 C++ Builder的命名规则。

C++ Builder 中使用名词、动词、动名词甚至是介词的组合方式来对一个变量、属

性、方法等进行命名,使其具有一个非常明确的含义,比如 Active表示的是一个 bool类型的变量(属性),Enabled同样,而 Show则理解为是一个方法用于显示某些内容更

为合理,同样 OnShow则应该理解为显示时触发的一个事件,OnShowing是显示的过程

第五章 控件的设计 第 256 页

中的一个事件。 显然这种命名规则对于某些对象和面向状态的程序设计更为适合一些,下面我们就

C++ Builder中类、属性、事件和方法的命名规则作个简单介绍。

1.1 类的命名

在 VCL中,类的命名最为简单,所有的 VCL类库中除了 Exception和其派生类之

外,其类名都是以“T”开头的,象 TObject、TButton、TPicture 等,“T”后面跟

随的是能够明确表示类用途的词汇或词汇组合。除了类以外,其它的数据类型也大多数是

以 T开头的比如,TColor为 enum类型,而在 Graphics.h 中预定义了部分 Color 的

取值如:clBlack,代表黑色,clBlue 代表蓝色等,其中 cl为 Color的缩写,Blue是蓝色。也有一些数据类型不是以“T”开头的比如 AnsiString 等,这些大多数是从

Object Pascal转换过来的。 类实例的命名也非常简单,在 IDE 中 C++ Builder 会自动根据类名词为使用 IDE

创建的类实例命名,大多数类的实例都是根据创建的数量在类名称的后面加上序号并去掉

前面的“T”,比如 TButton的实例为 Button1、Button2 等,另外一些向 TMenuItem,则是根据 Caption 中的字符,去掉中间的空格再加上序号的,如果 Caption 中的字符

不是 Ascii字符,则 IDE会自己对 MemuItem进行命名。类实例的名称一般不需要自行

定义也是可行的,但如果希望类实例更具有明显的含义,可以在 IDE中修改类实例的名称,

其变量的名称也会跟随改变,比如输入用户姓名的 TEdit 实例,可以为其取名

UserNameEdit等。

1.2 属性的命名

在 C++ Builder中,属性的类型允许为任意类型,可以是 C++语言中的基本数据类

型,也可以是 VCL类或其它任意用户定义的数据类型,而对于属性,不仅仅是属性显示的

名称,可能还包括了内部存储数据和存取方法等名称,我们逐一了解。

1.2.1 属性名称

一般来说,属性的名称一个可以表达明确的含义,通过名称可以知道属性的功能或者

描述的状态,而且需要尽可能的通过其名称来判断属性的数据类型,属性的名称通常有以

下几种: 名词 由单个名词构成,这些名词往往是具有明确含义的词汇如:Count、Menu、Canvas、

Designer、Position、Icon、Monitor、Parent、Child 等,象 Count 就很容易

理解为数量,其类型应该是 int类型,Menu、Canvas、Icon等本身是类名的一部分,

这些属性的类型应该是这些类的类型,Parent、Child本身不是类名的一部分,基本数

据类型也不能直接何其对应,这些属性的类型一个是和对象相关的类型,很可能和自身相

同。 动词

第五章 控件的设计 第 257 页

直接使用单个动词的属性很少,因为单个动词看起来更象是一个方法,在 VCL中也确

实如此,动词往往是以被动形式出现在属性名称中,比如:Scaled,而被动形式也可以

理解为形容词,因此这种属性和形容词作属性名称类似,多是 bool类型的,Scaled表

示是否按照比例作某件事。 形容词 单个形容词作属性名称的是比较多的,象 Active、Floating、Visible、Enable

等,形容词所描述的状态往往是非此即彼的,因此这种属性的类型大多数是 bool类型。 词汇组合 词汇组合是属性名称中使用最多的形式,因为单个词汇往往不能表达复杂的含义,为

了更加明确属性的含义,使用词汇组合可以达到这个目的。 在词汇组合的属性中,属性的数据类型则可以根据被修饰的主要部分进行判断,比如

ActiveControl、ActiveMDIChild、ClientHandle、ClientRect、HelpFile,这几个属性前面词汇都是修饰用的,最后一个词才是主要部分,其类型可以通过最后一个

词来判断。 词汇组合的属性名称中有一些是数据类型本身名称的一部分如:FormState、

FormStyle等,这些实际上和单个名词的情况是一样的。 另外一些词汇组合的是由动词和其它词汇构成的,比如:HasChild、HasParent、

IsEmpty 等,这些名称看起来更象是方法,往往也是只读属性,这种情况既可以作为方

法处理,也可以作为属性来处理,但我们建议当做属性来处理。

1.2.2 内部存储变量名称

前面也讲过,大多数属性在其内部都有各自的存储数据,来保存真实的数据信息,这

些数据原则上都是私有数据,在 VCL 中内部数据的命名规则是属性名称前面加上“F”,这也是一个习惯问题,采用和 Borland 一致的命名约定,在设计程序和组件的时候可以

不必费心的考虑某个属性是 VCL固有的还是自己扩充的,这样以来在成员函数中引用内部

数据的时候只需要在属性名称前面加上“F”就行。

1.2.3 存取函数的命名

对于属性,另外一个命名约定就是存取函数的名称,在 VCL中所有的存取函数均是保

护成员,通常并不需要显式调用,在引用属性时被隐含调用。 属性的读取函数的命名原则为属性名称前面加上“ Get”,存储函数的名称为属性名称

前面加上“Set”,这正好也是一个判断原则,如果你设计的方法名称中出现以“Get”或

者“Set”开头的,就应该考虑是否将其改为属性。

1.3 事件的命名

事件本身就是一种属性,但又和一般属性有着不同之处,我们知道事件表示的是状态

的改变,是一种瞬间状态,可以理解为“当⋯⋯怎么样”的时候,或者“在⋯⋯之前”等,

因此事件往往是由介词“On”开头的,后面紧跟着相应的动作,通常是动词、形容词或词

第五章 控件的设计 第 258 页

汇组合,比如 OnCreate、OnShow、OnPaint 等,有一些事件是以“Before”或者“After”开头的,表示“在⋯⋯之前”或“在⋯⋯之后”。

事件的处理函数名称也非常简单,IDE通常是将对象名称和事件名称组合在一起构成

事件处理函数的名称,比如 Button1的 OnClick 事件,在 IDE中生成的处理函数的名

称为:Button1Click;

1.4 方法的命名

方法的命名更加直观,通常可以由能够表达操作内容的单个动词构成,也可以是动词

和其它词汇的组合。一般来说,方法是相对独立的一个操作或动作,其名称至少包含一个

动词,如果名称中没有动词,则需要考虑是否应该作为属性来处理。方法的名称也可以是

名称、形容词甚至是副词,这时,其名称表达的是该方法执行后的结果或者状态。 由单个动词构成的方法如 TForm类中的: Close、Hide、Print、Release等等。 由其他类型的单个词汇构成的方法有: Next、Cascade、Previous、Tile等。 词汇组合为名称的方法更多,如: ArrangeIcons 、 CloseQuery 、 SendCancelMode 、 DefocusControl 、

FocusControl、GetFormImage等等。 一般来讲,方法的命名应该遵循以下原则: 具有描述性,能够确切表达动作的含义。 名称应能够反映出返回值的性质。尽管对于作为程序员的你而言,返回某个对象水平

位置的函数命名为 X是很自然的,但用 GetHorizontalPosition 作为名字更易理解。 如果为 void返回类型,方法名称应该具有一个主动动词或者是动作执行后的结果。 尽管 C++ Builder对变量、对象、类、属性、事件和方法的名称并没有限制,但是

使用 C++ Builder的命名规则会有助于更快更好的开发程序,了解这个约定对阅读后面

的章节也有帮助。

2 创建属性

属性是面向状态中最重要的概念,属性体现着对象的状态,要使用面向状态的技术,

就必须深刻的理解属性,并能够熟练的创建和应用属性。而且在组件中,可见部分大多数

是属性。即使不使用面向状态的方法,也应该对属性有所了解。接下来的内容是在组件设

计中如何创建属性及使用属性,在应用程序中创建和使用属性和这类似。

2.1 属性的必要性

前面讲过,属性就如同变量一样,程序员可以象使用变量一样对他们进行读取和设置,

唯一与变量不同的地方是不能把属性作为参数按引用传递给方法。但是属性不仅提供了面

第五章 控件的设计 第 259 页

向状态的可能,而且比简单的数据成员更强有力的能力,这是因为: n 应用程序员可以在设计时设置属性的值。与方法只能在运行时有效不同,属性使

得应用程序员可以在应用程序运行之前定制组件。属性显示在对象观察器中,这

简化了应用程序开发人员的工作。构造对象,你可以让 C++ Builder从对象观

察器中读取值,而不是使用几个参数。当属性值被设置时,对象观察器将进行验

证。这一点是 C++ Builder能实现可视化程序设计的关键。 n 属性可以隐藏实现细节。例如,内部被加密的数据在显示为属性值时将被解密。

虽然,值只是简单的数字和字符,但组件却可能需要在数据库中查询或进行复杂

的计算才能获得。属性使你可以把复杂的事情弄得看上去只是简单的设置,而看

上去犹如设置数据成员的东西实际上却调用了精心编制的方法。 n 属性可以是虚拟的,因此,应用程序开发人员看上去差不多的属性实际上可能由

不同的组件用不同的方式实现的。比如 Active对不同的组件,实现的方法和效

果都是不同的。 n 属性封装了实现过程,体现了对象的状态,一个简单的例子是所有控件都有的

Top属性。设置 Top属性值并不仅仅只是改变了内存中的值,而且会导致控件重

定位与重画。 n 设置属性值引起的反应并不局限于单个组件,例如,设置一个快速按钮的 Down

属性为 true会使得与它同组的其他快速按钮的 Down属性被设为 false。

2.2 属性的类型

C++ Builder 中属性可以是任何类型的。不同类型的属性在对象观察器中的显示是

不同的。设计时对象观察器在创建属性时将对属性的赋值进行验证,下表显示了不同类型

的属性在对象察看器中显示的方式。 表 5.1对象观察器如何显示属性属性类型对象观察器的处理

简单型(Sample) 数字、字符和字符串属性被显示为数字、字符和字符串。应用程序开发人员能直接编辑它们的值

属性类型对象观察器的处理枚举型(Enumerated)

枚举型(包括 bool型)属性显示为可编辑的字符串。应用程序开发人员既可以通过双击值域,也可以通过下拉式列表框选择可能的值

集合型(Set) 集合型属性显示为集合(sets)。应用程序开发人员通过双击该属性象处理布尔值一样扩展、处理集合的每个元素

对象型(Object)

本身就是类的属性,自身就有自己属性的编辑器,由组件注册过程指定。如果作为属性的类有自己的发布部分,对象观察器允许应用程序开发人员扩展(通过双击)列表包含并编辑它们。对象属性必须从 TPersistent派生而来

数组型(Array) 数组属性必须有自己的属性编辑器。对象观察器对它们的编辑没有内置支持。当你注册你的组件时,你能指定一个属性编辑器

第五章 控件的设计 第 260 页

2.3 发布继承的属性

所有组件都继承它们祖先的属性。当从已有的类派生组件时,新组件继承了直系祖先

的所有属性。如果你从抽象类派生组件,它继承的属性可以是 public或 protected,但没有 published。为了使 protected或 public 属性在设计时可以在对象观察器中

可见,需要把该属性重新声明为 published。重新声明意味着在派生类的声明中加入继

承属性的声明。 例如,从 TWinControl 控件派生组件,继承它的 DockSite 保护属性。通过在新

组件中重新声明 DockSite,能把 protected改为 public或 published。 下面的代码显示如何重新声明 DockSite为 published,从而使它在设计时可见: class PACKAGE TMyComponent : public TWinControl

{

__published:

__property DockSite;

};

在重新声明属性时,只能指定属性名,而无需指定属性类型和其他在 2.4“定义属性”

中出现的信息,但你能为该属性声明新的缺省值并指明是否存储该属性。重新声明属性可

以降低属性的限制等级,但不能提高限制等级。因此,可以把 protected 改为 public,但不能把 public属性声明为 protected。

2.4 定义属性

下面的内容将阐述如何声明新属性以及解释一些标准组件遵循的约定,主要包含以下

几个方面:

2.4.1 属性声明

在组件类的声明中声明属性。声明属性的时候,有三件事情需要明确:属性名称、属

性类型、读写属性值的方法。如果没有写方法,那么该属性为只读属性。 在设计时,对象观察器可以编辑声明在__published 定义域中的属性。已发布的属

性值与组件一同保存于窗体文件中。public部分声明的属性可以在运行时访问,由程序

代码读取或设置。下面是 Count的属性声明:

class PACKAGE TMyComponent : public TComponent

{ private:

int FCount; //存储属性的数据成员

int __fastcall GetCount(); //属性的读取方法

void __fastcall SetCount( int ACount ); //属性的存储方法

第五章 控件的设计 第 261 页

public:

__property int Count = {read=GetCount, write=SetCount};

//声明属性

//...

};

2.4.2 内部数据存储

在前面的章节中我们已经介绍过,对于大多数属性都有一个用于存储数据的数据成

员,该成员必须和属性是同一类型,但如何存储属性的数据是没有限制的,也就是说这个

用于存储数据的成员可以是私有的、是受保护的、也可以是完全公开的。 一般的 C++ Builder的组件对于属性内部存储数据遵循下列约定: n 属性数据存储于数据成员中,这也是我们推荐的,大多数属性都将数据存储在数

据成员中,也可以使用读取方法引用其它组件的属性。 n 存储属性的私有数据成员只有组件自身才能访问。派生组件应使用继承的属性,

而不要直接访问属性内部的数据存储。这一点对于 C++ Builder来说是非常不

幸的,因为当父类的属性定义为只读属性的时候,其派生类没有办法对其内部数

据进行修改,而在 Object Pascal中派生类可以对父类的私有成员进行存取,

Borland公司在开发 VCL 的时候,并没有考虑到这两者之间的差异,从而给使

用 C++ Builder 的程序员在组件设计上(多存在于对原有 VCL 组件功能的扩

充时)带来了许多不如 Delphi灵活的地方。 n 数据成员标识由 F和紧接着的属性名组成,这就是我们前面说的命名规则。例如,

TControl中 Width属性的原始数据存于 FWidth数据成员中。 有个原则强调了上述约定:只有属性的方法可以访问属性的数据。如果某一方法或其

他属性想改变属性的数据,它应遵循上述原则,而不要直接访问存储的数据。这保证了继

承的属性可以改变而不会导致派生组件无效。实际上灵活性和语言的约束是相互联系和对

立的,强的语言约束可以保证程序设计更少的出错,但灵活性差一些,灵活性提高的时候,

却可能带来程序潜在错误的发生,在后面的例子中我们就使用了公开的成员作为属性的内

部存储数据,目的是为了在其它对象能够访问和设置内部数据。

2.4.3 直接访问

使数据可以被访问的最简单方法就是直接访问。也就是说,属性声明中指定赋值和读

取的 read和 write部分直接访问内部存储的数据,而不是通过某一访问方法做这些事。

直接访问对那些在对象观察器中可见的、改变值不会立即触发处理的属性非常有用,但并

不实用。通常,属性声明中的 read 部分会直接访问数据,但 write 部分则使用访问方

法。当组件值被改变时,这点使得组件可以更新状态。 下面的组件类型声明显示了一个属性在 read和 write部分中使用直接访问。 class PACKAGE TSampleComponent : public TComponent

第五章 控件的设计 第 262 页

{ private: //内部存储数据是私有的

bool FReadOnly; //声明私有的数据成员,用于存储属性

// ...

__published: //__publish使属性在设计时可见

__property bool ReadOnly = {read=FReadOnly, write=FReadOnly};

}; //声明属性

这个属性只是提供了能够在 Object Inspector 中设置和修改,以及将设计时指定

的数据存储在资源文件中的能力,对于资深的程序员来说毫无意义。

2.4.4 访问方法

可以在属性声明的 read 和 write 部分指定访问方法,而不是数据成员。访问方法

应是 protected,而且通常声明为 virtual,允许派生类重载。不要把访问方法声明为

public,把它们声明为 protected,确保应用程序开发人员不会无意中通过它们修改了

属性。 下面的例子就是上面提到的 Count属性的访问方法,在这里 Count同时具有 read

和 write方法: class PACKAGE TMyComponent : public TComponent

{

private:

int FCount;

int __fastcall GetCount();

void __fastcall SetCount( int ACount );

public:

__property int Count = {read=GetCount, write=SetCount};

//...

};

下面是设置方法:

int __fastcall GetCount()

{

return FCount; //直接返回存储的数据

}

void __fastcall SetCount( int ACount )

{

if( FCount == Value )

return; //如果设置的值和原来的值相同,则直接返回,因为后面对于状态更

第五章 控件的设计 第 263 页

//新的代码可能会有较大的系统开销,或是不必要的操作。

// ... 需要针对 Count属性的改变而需要更新的代码

FCount = Value; //更新内部存储数据

}

1. 读方法 读方法通常都是无参数的函数(带有索引的属性或者数组属性除外),它返回与属性

的类型相同的值。根据约定,函数的名字是 Get 后紧接属性名。例如,Count 属性的读

方法为 GetCount。使用读取方法时,往往需要在读取函数对内部存储数据操作,生成适

当类型的值。读方法一般没有参数,仅有的例外是数组和使用索引分类的属性。如果你没

有声明读取部分,属性就是只写的。只写属性非常少见。 2. 写方法 写方法是个只有一个参数的函数(带有索引的属性或者数组属性除外),它的参数类

型与属性类型相同。参数可以按值传递,也可以按引用传递,对参数名也没有限制。根据

约定,写方法的名字是 Set后紧跟属性名称。 例如,Count属性的写方法为 SetCount。传递的参数即是属性的新值。写方法在需

要时将适当的数据存储于属性内部存储区。写方法一般只有一个参数,仅有的例外是数组

和使用索引分类的属性,这两个属性以索引值作为第二个参数(使用索引分类创建一个读

写方法被几个属性共享)。 如果你没有声明写方法,属性就是只读的。为了在设计时能使用__published 属性,

你必须将它们定义为读/写类型的。通常在改变属性之前,写方法需要检测新的属性值与

原有值是否相同。

2.4.5 缺省属性值

当声明了属性之后,可以为它设置缺省值。C++ Builder使用缺省值确定是否把属

性存储在窗体文件中。如果没有为属性指定缺省值,C++ Builder总是存储该属性。为

了为属性声明缺省值,在属性名后加一个等号和一对大括号,大括号中包含 default 关

键字与缺省值。例如: __property bool IsTrue = {/*...*/,default=true};

注意声明缺省值并非就是将属性设置为该值,组件的构造函数应对属性进行初始化。

另外,由于对象总是把它们的数据成员设为 0,因此,无需严格要求构造函数非要把缺省

值为 0 整数属性设为 0、把字符串属性设为空、把缺省值为 false 的布尔型属性设为

false不可(参见前面对完整性的要求)。 可以为属性指定无缺省值,当重声明属性时,可以指定该属性没有缺省值,即使继承

的属性已指定了一个,也可以这么做。为了将属性指定为无缺省值,在属性名后加一个等

号和一对大括号,大括号中为 nodefault关键字。 例如: __property bool IsTrue = { /* */,nodefault };

第五章 控件的设计 第 264 页

在第一次声明属性时,你无需包含 nodefault,没有声明缺省值就意味着无缺省值。

下面是个组件的声明,该组件有个 bool型属性 IsTue,缺省值为 true,构造函数设置

了此值。 class PACKAGE TMyComponent : public TComponent

{

private:

bool FIsTrue;

public:

virtual __fastcall TMyComponent( TComponent* Owner );

__published:

__property bool IsTrue = {read=FIsTrue, write=FIsTrue, default=true};

};

__fastcall TMyComponent::TMyComponent( TComponent* Owner )

: TComponent( Owner )

{

FIsTrue = true; //初始化属性的缺省值,

//需要注意,初始化属性尽量不要对属性进行,而是对属性的内部存储数据进行操作,

//因为设置属性通常会引发更新动作,而在构造函数没有执行完以前,某些更新操作是

//不能进行的,也是没有必要的!!后面 2.6.2中例子有所不同。

}

指定缺省值的目的是为了在程序在存储和载入属性的时候判断是否需要将设计时的

属性值存储在资源中,读者可以通过后面 2.6的内容了解到,缺省值可以有效的压缩不必

要的重复过程和程序可执行文件的体积。

2.5 创建数组属性

某些属性可能需要向数组那样使用索引,这些属性在使用上和数组比较类似,我们称

之为数组属性,但并不是指内部存储数据为属性,或者将属性当做 C++中的数组一样使用,

数组属性只能按照单个数组元素的方式来使用。 例如,TMemo的 Lines属性就是一个由备注文本组成的、可以索引的列表,你可以

把它当作字符串数组。Lines属性为在大量数据(备注文本)中访问特定的元素(一个字

符串)提供了很自然的访问手段。除了下面的例外,数组属性同其他属性一样声明: n 声明包含至少一个规定类型的索引。索引通常都是整型。 n 如果指定了属性声明中的 read 和 write 部分,那么它们必须为方法,而不是

数据成员。任何数组属性的读和写方法都有一个额外的参数标识索引,该参数必

须与声明中指定的索引类型、次序相同。数组属性和数组有一些重要的不同之处。

第五章 控件的设计 第 265 页

不像数组,数组属性的索引并非一定是整型,例如,可以以字符串为索引。另外,

只能引用数组属性的单个元素,而不能引用属性的整个范围。 下面显示如何声明用整型作为索引返回字符串的属性。

class PACKAGE TDemoComponent : public TComponent

{ private:

System::AnsiString __fastcall GetNumberSize(int Index);

public:

__property System::AnsiString NumberSize[int Index] =

{read=GetNumberSize}; // ...

};

下面是. CPP文件中的 GetNumberSize方法: System::AnsiString __fastcall TDemoComponent::GetNumberSize(int Index)

{

System::AnsiString Result;

switch (Index)

{ case 0:

Result = "Zero";

break;

case 100:

Result = "Medium"; break;

case 1000:

Result = "Large";

break;

default: Result = "Unknown size";

}

return Result;

}

2.6 存储和载入属性

C++ Builder 将窗体和窗体的组件存储在窗体文件(.DFM,参见 C++ Builder中资源使用部分的内容)中。编译后窗体文件使用二进制数据描述窗体和它的组件,这就

第五章 控件的设计 第 266 页

是窗体的资源。如果 C++ Builder开发员将你的组件加入他的窗体,那么当窗体被存储

时,你的组件应有能力将自己的属性写入窗体文件中。同时,当 C++ Builder需要载入

窗体或组件作为程序的一段代码被执行时,组件也必须能从窗体文件中恢复自己的属性。

通常对于这些,你无需做任何工作,因为你的组件已经继承了这种能力。但有时,设计一

个组件可能想换种存储方式或者改变载入时的初始化方式,这组件设计中实在经常碰到

的,也是有必要的,因此,对于一个优秀的 C++ Builder程序员应理解它的底层处理机

制。包括以下几个方面:

2.6.1 使用存储-载入机制

窗体描述是由一系列窗体属性和类似的组件属性描述组成的,存储在窗体文件中。每

个组件,包括窗体自身,都需自己存储与载入自己的描述。缺省情况下,当组件存储自身

时,组件按次序存储 public属性和与缺省值不同的 published 属性。而载入时,组件

首先构造自己,并在构造函数中将所有属性设置为缺省值,然后读取已存储的非缺省属性

值。缺省机制可以满足大多数组件的需要,组件开发员通常无需做任何工作。但也有几个

途径可定制存储和载入机制以满足自身的需要。

2.6.2 指定缺省值

只有属性值与它的缺省值不同时,C++ Builder才会存储它们。如果不指定缺省值,

C++ Builder 就假定该属性没有缺省值,这意味着而不管是什么值,组件都要存储它们

的属性。要指定属性的缺省值参见前面内容。 如上所属,也可以在重新声明时指定缺省值。实际上,重新声明属性的一个目的就是

修改指定的缺省值。注意指定缺省属性值并不意味着在对象创建时会将该值赋给它。你必

须通过构造函数对属性进行必要的赋值。如果构造函数没有对某属性赋值,那么无论该属

性值曾被假定为什么值,在内存中都会被设置为 0。也就是说,数字值设为 0,布尔值设

为 false,而指针设为 NULL,诸如此类。如果你有什么不放心,那么处于完整性考虑最

好在构造函数中设置属性值。 下面的代码是个组件的声明,该组件在构造函数中将 Align 属性设置为缺省值。在

这个例子中,新组件是个标准的面板组件,用于窗体的状态条,因此它缺省的对齐方式是

对齐窗口的底部。 class PACKAGE TMyStatusBar : public TPanel

{

public:

virtual __fastcall TMyStatusBar(TComponent* AOwner);

__published:

__property Align = {default=alBottom};

};

第五章 控件的设计 第 267 页

.CPP文件中 TMyStatusBar组件的构造函数如下:

__fastcall TMyStatusBar::TMyStatusBar (TComponent* AOwner)

: TPanel(AOwner)

{

Align = alBottom;

}

在这个例子中,Align属性是通过派生而来的,在 TMyStatusBar的构造函数中这个

属性是可用的,当改变 Align属性是会引发相应的更新动作,但是引发的是父类 TPanel的更新动作,从前面章节关于 C++ Builder 类的构造过程中,我们可以知道,TPanel在 TMyStatusBar构造之前已经被创建,因此可以直接对 Align属性进行操作,而且无法

对 Align属性的内部数据 FAlign进行直接操作,这和 2.4.5的例子中的注解有所不同。 这里就出现了前面说过的问题,如果一个父类的属性是只读的,在其派生类中无法对

其缺省值进行修改,而且不能访问原始的数据成员,而在 Delphi中,这是允许的,读者

可以思考以下,对于这种情况应该如何处理。

2.6.3 决定存储内容

可以控制 C++ Builder 是否存储组件属性。缺省情况下,类声明中所有的

published 属性都会被存储。但可以指定不存储某一属性,或者指定一个程序在运行时

决定是否存储该属性。要控制属性的存储可以使用 stored关键字。 下面的组件声明了 3个新属性。一个总是被存储,一个不存储,而另一个是否存储由

返回 bool类型方法的值决定:

class PACKAGE TSampleComponent : public TComponent

{

protected:

bool __fastcall StoreIt();

public:

... __published:

__property int Important = {stored=true}; //总是存储

__property int Unimportant = {stored=false}; //总是不存储

__property int Sometimes = {stored=StoreIt}; //是否存储取决于 StoreIt

};

2.6.4 载入后的初始化

组件从被存储的描述中读取所有的属性值后,将调用 Loaded虚拟方法执行所需的初

第五章 控件的设计 第 268 页

始化。这个调用发生在显示窗体和它的组件之前,因此,你无需担心初始化会导致屏幕闪

烁。为了在载入后初始化组件,你要重载 Loaded方法。注意任何 Loaded方法做的第一

件事就是调用继承的 Loaded方法,这保证了继承的属性在你初始化你自己的组件之前能

正确地初始化,也保证了扩充增强了的属性不会被继承来的 Loaded方法覆盖而使你徒劳

无功。

2.6.5 存储和载入未发布的属性

缺省情况下,在组件中只有 published属性被存储和载入。但是,存储和载入未发

布的属性也是可以的。这意味着你可以拥有相对固定的、且不显示在对象观察器中的属性。

并且,你还能够存储和载入由于过于复杂,C++ Builder不知道如何存储和读取的属性。

例如,TString 对象不能依赖 C++ Builder 的自动化行为来存储和载入它描述的字符

串,因而必须使用下面的机制。通过加入代码告诉 C++ Builder如何存储和载入,你可

以存储和载入你的组件属性,有以下两种方式: 1.创建方法来存储和载入属性值为了存储和载入未发布的属性,首先必须创建两个方

法,一个用于存储属性值,另一个载入属性值。有两种方式可以选择: n 创建 TWriterProc 类型的方法存储属性值,创建 TReaderProc 类型的方法载

入属性值。这个选择允许你使用 C++ Builder内置的支持读写简单类型的能力。

如果你的属性值是由 C++ Builder知道如何读写的类型组成,那么选择这个方

式。 n 创建两个 TStreamProc 类型的方法,一个用于存储属性值,一个用于载入属性

值。TStreamProc的参数中有流,你可以用流方法读写属性值。 例如,考虑一个属性,它标识一个在运行时创建的组件。C++ Builder知道如何写

它的值,但却不能自动地做,因为该组件在窗体设计时并未被创建。由于流系统能够存储

和载入组件,因此你可以选择第一种方式。下面的方法可以存储和载入动态创建的组件,

该组件是 MyComproperty属性的值: void __fastcall TSampleComponent::LoadCompProperty(TReader *Reader)

{

if (Reader->ReadBoolean())

MyCompProperty = Reader->ReadComponent(NULL);

}

void __fastcall TSampleComponent::StoreCompProperty(TWriter *Writer)

{

if (MyCompProperty)

{

Writer->WriteBoolean(true);

Writer->WriteComponent(MyCompProperty);

}

第五章 控件的设计 第 269 页

else

Writer->WriteBoolean(false);

}

2. 重载 DefineProperties方法 如果曾创建过方法来存储和载入属性值,那么就会重载组件的 DefineProperties

方法。当 C++ Builder需要存储和载入组件时,就调用该方法。在 DefineProperties方 法 中 , 必 须 调 用 当 前 归 档 员 对 象 的 DefineProperties 方 法 或

DefineBinaryProperty 方法,将存储和载入属性值的过程作为参数传给它。如果使用

TReaderProc 和 TWriterProc 类型过程,则调用归档员对象的 DefineProperty 方

法;如果使用 TStreanProc 类型过程,则应调用 DefineBinaryProperty方法。无

论用什么方法定义属性,都要传递一个布尔型参数指示是否需要存储属性值。如果该属性

值是继承的或是有缺省值的,则无需存储它。 例如,当使用 TReaderProc 类型的 LoadCompProperty 方法和 TWriterProc 类

型的 StoreCompProperty方法时,重载 DefineProperties方法如下: void __fastcall TSampleComponent::DefineProperties(TFiler *Filer)

{

//必须确保父类已经定义了其属性,这个例子中假定是从 Tcomponent直接派生的

TComponent::DefineProperties(Filer);

bool WriteValue;

if (Filer->Ancestor) //检查继承来的值

{

if ((TSampleComponent *)Filer->Ancestor)->MyCompProperty == NULL)

WriteValue = (MyCompProperty != NULL);

else if ((MyCompProperty == NULL) || (((TSampleComponent *)Filer

->Ancestor)->MyCompProperty->Name !=

MyCompProperty->Name))

WriteValue = true;

else

WriteValue = false;

}

else // no inherited value, write property if not null

WriteValue = (MyCompProperty != NULL);

Filer->DefineProperty("MyCompProperty ", LoadCompProperty,

StoreCompProperty, WriteValue);

};

第五章 控件的设计 第 270 页

3 创建事件

事件是一种特殊的属性,反映的是状态的改变,也就是系统或者应用程序所发生的事

情(如用户动作或焦点改变)与响应代码之间的连接。响应代码被称为事件处理程序,差

不多总是由应用程序开发人员编写。通过事件,应用程序开发人员可以定制组件的行为,

而无需改变类自身。这也就是通常所说的代理(delegation)。处理大多数用户动作的

事件已内置在所有的标准组件中,但你也可以定义新事件。理解了下面的内容,你就可创

建新事件: n 什么是事件。 n 实现标准事件。 n 定义你自己的事件。 由于事件是用属性实现的,因此,在创建或改变组件的事件之前,应该熟悉属性相关

的内容。

3.1 什么是事件

关于什么是事件的问题,在前面讲述面向状态的思想时已经作过详细的介绍,但那是

着重于事件和状态的关系以及从事件的作用等角度来讲的。从程序代码的角度来讲,事件

是连接发生的事情与响应代码的机制。更为明确地说,对于某一个类实例,事件就是指向

方法的 closure。从应用程序开发人员的观点来看,事件只是一个与系统中发生的事情

相连的名字,如 OnClick,一些代码与这个名字联系在一起。例如,按钮 Button1 有个

Onclick 方法。缺省情况下,C++ Builder 在该按钮的窗体中生成一个事件处理程序

Button1Click,并将其分配给 OnClick。当该按钮的单击事件发生时,按钮就调用分

配给 OnClick的方法,即 Button1Click。要编写事件,应理解下面的内容:

3.1.1 事件是 closure

C++ Builder 使用 closure 实现事件。前面已经讲过,closure 是种指针类型,

该类型的指针指向特殊类实例中的特殊方法。作为组件开发员,你可以把 closure 当作

占位符处理:检测事件发生,然后调用用户指定的方法(如果有的话)。closure拥有一

个隐藏的、指向类实例的指针。当用户指定组件事件的处理程序时,不是指定了一个有特

殊名字的方法,而是指定了特殊类实例中的特殊方法。通常,特殊的类实例就是包含该组

件的窗体,但并非都是如此。 事件最典型的例子是所有的控件都继承了 Click方法用于处理单击事件: virtual void __fastcall Click(void);

如果用户的单击事件处理程序存在,Click的实现就会调用它。如果用户指定了控件

的 OnClick 事件处理程序,那么单击控件将导致该处理程序被调用。如果没有指定处理

程序,什么也不会发生。

第五章 控件的设计 第 271 页

关于 closure的详细内容参见前面关键字介绍部分的内容。

3.1.2 事件是属性

组件使用属性实现它们的事件。与其他属性不同,事件不使用方法实现它们的读写部

分。相反,事件属性使用与其同类型的私有数据成员采用直接存取的方式,这并不是说事

件不能采用读写方法来事件读写部分,而是大多数情况没有必要,因为事件反映的是属性

的改变,而事件的改变一般不许要进行再次出发处理过程或者实现更新等动作。 根据约定,数据成员的名字同属性一样,但有一个 F前缀。例如,OnClick的 closure

被存储在 TNotifyEvent 类型的 FOnClick 数据成员中,OnClick 事件属性的声明类

似:

class PACKAGE TControl : public TComponent

{

private:

TNotifyEvent FOnClick;

//...

protected:

__property TNotifyEvent OnClick = {read=FOnClick, write=FOnClick};

//...

};

关于 TNotifyEvent 和其他事件类型,参见联机帮助。同其它任何属性一样,在运

行时能设置或改变事件的值。事件是属性的主要用处在于,组件用户可以在对象观察器中

为事件指定处理程序,因此对于应用程序中添加事件(主要是区别在组件设计中使用面向

状态技术),则可以不必声明为属性,作为一般的成员即可。

3.1.3 事件类型是 closure 指针类型

由于事件是指向事件处理程序的指针,因此事件属性类型必须为 closure 类型。同

样,任何作为事件处理程序的代码必须是类中有适当类型的方法。为了与给定类型的事件

相容,事件处理程序方法的参数个数、类型、次序、传递方式均应一样。C++ Builder为标准事件定义了 closure 类型。当你创建自己的事件时,如果合适,你可以使用已有

的 closure,或者自己定义一个,例如定义一个自己的 Click事件的 closure: typedef void __fastcall (__closure *TMyNotifyEvent)(TObject *Sender,

int &MyExtendData);

在这个 closure中比标准的 Click 事件增加了一个 int类型的引用参数,比如可

以通过给数据来记录这个事件被触发的次数等。用户需要自定义 closure 的时候,需要

第五章 控件的设计 第 272 页

注意:至少应该包含一个参数就是 TObject *Sender,用来表明事件的触发者,从语言的

角度讲,这不是必须的,但是在程序中是非常有必要的,因为事件的处理代码可以根据

Sender来判断是由谁来触发该事件的,这对于运行时创建的控件是必不可少的。 事件的返回类型是 void,或者说事件只能返回 void 类型。尽管如此,但你可以通

过引用型的参数返回需要的值。当你这么做的时候,要保证在调用处理程序之前,传递的

参数有正确的值,从而免去用户代码设置该值的麻烦。OnKeyPress 事件处理程序为

TKeyPressEvent 类型,定义了两个参数,一个标识产生该事件的组件,另一个即是引

用参数,标识那个键被按下了: typedef void __fastcall (__closure *TKeyPressEvent)(TObject *Sender,

Char &Key);

通常,Key参数含有用户按下的字符,在某些环境中,组件的用户可能想修改该字符。

例如,在编辑控件中强制所有的字符为大写。在这种情况下,用户应如下定义按键事件处

理程序: void __fastcall TForm1::Edit1KeyPress(TObject *Sender, Char &Key)

{

Key = UpCase(Key);

}

还可以通过使用按引用传递的参数让用户重载缺省的处理。

3.1.4 事件处理程序是可选的

当事件被创建时,要记住开发人员可能使用你的组件,却没有连接处理程序。这意味

着你的组件会由于没有对应的事件处理程序而失效或出错。在 Windows 应用程序中,事

件总是不断地产生,仅仅在可视组件上移动鼠标就会导致 Windows 发送鼠标移动消息,

组件将该消息传至 OnMouseMove 事件。大多数情况下,开发人员不愿处理鼠标移动消息,

这会出现问题,就是触发了事件而程序并没有指定处理代码。因此,开发的组件不应对每

个事件都要求用户连接一个处理程序。此外,应用程序开发人员可以在事件中编写任何想

写的代码。由于精心编码,VCL 组件的事件处理程序极少出现错误。自然,你无法保证自

己开发的组件不出现逻辑错误,但至少可以保证数据结构在调用事件处理程序之前已被正

确初始化,从而避免了应用程序开发人员访问无效数据。

3.2 实现标准事件

C++ Builder 的组件均继承了 Windows 常用消息的事件,即标准事件。但是这些

事件均是内置于控件中,为 protected类型,因此,应用程序开发人员无法为它们指定

处理程序。当你创建控件时,你可以决定是否让你的事件对用户可见。当需要与标准组件

第五章 控件的设计 第 273 页

协作时,要考虑下面三个问题:

3.2.1 标识标准事件

标准事件有两类:为所有控件定义的事件和为标准窗口控件定义的事件。 1.为所有控件定义的事件 最基本的事件定义在 TControl类中,所有控件,无论是窗口、图形,还是定制的,

均继承这些事件。 下面是对所有控件均有效的事件: OnClick OnDragDrop OnEndDrag OnMouseMove OnDblClick OnDragOver OnMouseDown OnMouseUp 标准事件的调用是在 TControl中声明的 protected 虚拟方法,方法的名字是相应

的事件名。例如,OnClick 事件被 Click 方法调用,OnEndDrag 事件被 DoEndDrag方法调用。下面是 TCustomUpDown组件 Click方法 Object Pascal源代码:

procedure TCustomUpDown.Click(Button: TUDBtnType);

begin

if Assigned(FOnClick) then FOnClick(Self, Button);

end;

从这里可以看出,重载相应的调用方法可以扩展组件内部对事件的处理,在需要触发

事件的地方调用该方法就可以,当然在你开发组件的时候,也可以不使用重载标准事件的

调用方法来触发事件,而是在直接添加如上代码来实现,但这会带来一个缺点,就是控件

自身的处理代码将不能得到执行。 2. 为标准窗口控件定义的标准事件 除了拥有一般控件都有的事件之外,标准窗口控件(从 TWinControl 派生的控件)

还拥有下列事件: OnEnter OnKeyDown OnKeyPress OnKeyUp OnExit 窗口控件的事件同 TControl中的标准事件一样有相应方法。

3.2.2 使事件可见

TControl和 TWinControl 中标准事件均声明为 protected,同它们对应的方法

一样。如果你继承上述抽象类,并且想使事件在设计时或运行时可见,应将它们重新声明

为 public或 published。重新声明属性不需要它的实现,而是保持原有的实现,重新

声明只是改变它的保护等级。可以使用 TControl中定义的事件,它是不可见的,并将它

封装为 public或 published。 例如,创建一个组件,在设计时封装 OnClick,需要在组件类中加入下列声明:

第五章 控件的设计 第 274 页

class PACKAGE TMyControl : public TCustomControl

{

//...

__published:

__property OnClick; //使 OnClick事件在 Object Inspector中可见

};

3.2.3 改变标准的事件处理

如果你想改变组件响应某一类事件的方式,你可能会给事件加一些代码。作为应用程

序开发员,这正是他们所要做的。而你创建组件时,必须允许使用组件的应用程序员访问

事件。这就是与标准事件连接的方法被实现为 protected的缘由。通过重载方法,你可

以修改内部的事件处理。通过调用继承的方法,你可以得到标准的处理过程,包括为应用

程序开发员代码服务的事件。调用方法的次序是非常重要的。作为一般原则,首先调用继

承的方法,使应用程序开发员的事件处理程序在你定制之前被执行(在某些情况下,会禁

止执行定制)。然而,有时需要在调用继承的方法之前先调用你的代码。例如,继承的代

码可能依赖组件的状态,而你的代码修改该状态,此时,你应先运行你的代码,然后允许

用户的代码对此响应。假定你编写了一个组件,且你改变它响应鼠标单击的方式。你可以

重载 protected 方法 Click,而不要像应用程序开发人员那样为它指定事件处理程序

OnClick: void __fastcall TMyControl::Click()

{

TWinControl::Click(); //调用标准的 Click

//自定义代码

}

3.3 定义你自己的事件

定义一个全新的事件在组件设计中是并不多见的,但是在应用程序中使用面向状态的

方法是,出现的机会可能要大一些。然而,当组件的行为完全不同于其他组件时,你需要

为它定义事件。定义事件时,需要考虑下列内容: n 触发事件。 n 定义处理程序类型。 n 声明事件。 n 调用事件。 本书中一个应用程序的例子 Sells 其中报表和系统设置的 Form 窗体都是使用了动

态连接库来实现的,实际上载入的接口就是定义了自己的事件。

第五章 控件的设计 第 275 页

3.3.1 触发事件

应知道是什么触发了事件。对某些事件,结果是显然的。例如,当用户按下鼠标左键

时,Windows向应用程序发送 WM_LBUTTONDOWN 消息,触发鼠标按下事件。接收到该消

息后,组件调用 MouseDown方法,MouseDown则调用 OnMouseDown事件句柄,并将

按键作为事件的参数,进而执行用户连接的代码。但是某些事件并不是清楚地与外部事情

相连接。例如,滚动条的 OnChange事件可以有多种触发方式,包括击键、鼠标单击以及

其他组件的改动。定义事件时,你要保证所有事情会调用正确的事件。 两类事件你要为两类事情提供事件:用户交互和状态改变。用户交互事件几乎总是由

Windows传递的消息触发,消息指示组件响应用户动作。状态改变事件也可以与Windows消息(例如,焦点改变或允许焦点)相连接,但是也会由属性改变或其他代码触发。对你

定义的事件,你拥有完全的控制。小心定义事件让开发人员能够理解和使用它们。 读者请参阅前面章节相关内容理解事件的触发过程。

3.3.2 定义处理程序类型

一旦你确定了事件什么时候发生,你必须定义如何处理事件。这意味着确定事件处理

程序的类型。在大多数情况下,定制事件的处理程序要么是简单的通知,要么是专用事件

类型。你可以从处理程序中返回信息。 1.简单的通知 通知事件只是告诉你某个事件发生了,不含时间和地点信息。通知使用

TNotifyEvent 类型,该类型只有一个参数,即事件的发送者。所有通知的处理程序都

知道事件的种类以及发生事件的组件。例如,单击事件是通知。当你为单击事件编写处理

程序时,你所知道的就是那个组件发生了一次单击。通知是单向过程。没有提供反馈用以

禁止进一步处理的机制。 2.专用事件类型 在某些情况下,仅知道什么组件发生了什么是不够的。例如,按键事件需要知道用户

按下了哪个键。在这种情况下,你需要含有其他信息的处理程序类型。如果事件是为消息

定制的,那么所需的参数可以直接从消息参数中得到并经过加工提供给事件句柄。 3.从处理程序返回信息 由于所有的处理程序只返回空值( void),故获取信息的唯一途径就是传递引用参数。

从用户代码返回以后,你的组件可以通过返回的信息决定是否或如何处理事件。 例如,所有的键事件(OnKeyDown、OnKeyUp 和 OnKeyPress)传递引用键值的参

数 Key。事件处理程序可以修改 Key,这样作使得应用程序得到的按键与发生事件的按键

不同。如前面的例子可以强迫所有的字符大写。

3.3.3 声明事件

一旦你确定了处理程序的类型,就可以为事件声明 closure 和属性了。确保事件的

名字是有含义的、描述清楚的,这样用户才能理解事件是干什么用的。多个组件中类似的

属性尽量使用统一的名字。事件名以“On”开头 C++ Builder 的大多数事件以“On”

第五章 控件的设计 第 276 页

开头,这仅仅只是一个约定,而不是编译器强制的。对象观察器通过查看属性的类型确定

该属性是否是事件:所有的 closure 属性(声明于 publish 中的)被认定为事件,并

被显示在事件页面上,并允许程序员双击列表框来创建新的处理方法。开发人员习惯通过

观察名字的起始是否有“On”来找到事件。使用其他的名字很容易混淆。此外 After 和

Before也是常用的事件的起始单词。

3.3.4 调用事件

你要集中调用事件,也就是说,在你的组件中创建一个虚拟方法,调用应用程序的事

件处理程序(如果有的话),并提供缺省处理。将所有的事件调用集中在一起,当有人从

你的组件派生新组件时,通过重载某个方法定制处理,此时,他无需搜遍你的代码寻找你

调用事件的地方。 调用事件还需考虑如下两个方面: n 空处理程序是允许的。 n 用户可以重载缺省的处理。 1.空处理程序是允许的 不要编写会导致空处理程序出错的代码,不要使你的组件依赖于用户的事件处理代

码。空处理程序产生如同没有处理程序一样的结果,因此,调用应用程序事件处理程序形

状应该如下:

if (OnClick)

OnClick(this);

//控件的缺省处理

}

而且绝对不要这么做:

if(OnClick)

OnClick(this);

else

//缺省处理

2.用户可以重载缺省的处理 对于某些事件,开发人员想取代缺省处理,甚至取消所有响应。为此,你应传递一个

引用参数给处理程序,然后检查返回值。这里要遵循的一个约定是:空事件处理程序的影

响同根本没有处理程序一样。由于空处理程序并不改变参数值,因此缺省的处理在空处理

程序被调用后总会被执行。 例如,处理按键事件时,用户可以通过设置 Key为空字符来取消组件的缺省处理。逻

辑过程如下:

第五章 控件的设计 第 277 页

if(OnKeyPress)

OnKeyPress(this, &Key);

if(Key != NULL)

//执行标准的缺省处理

实际的代码与此有些不同,因为它要处理 Windows 消息,但逻辑是一样的。缺省情

况下,组件先调用用户指定的处理程序,然后执行标准的处理。如果用户的处理程序将 Key设为空字符,组件跳过缺省处理。

4 创建方法

组件方法与其他类方法之间并无不同之处,也就是说,它们都是组件类的成员函数。

虽然,对如何处理组件的方法本质上没有任何限制,但 C++ Builder 使用了一些应遵守

的标准。包括以下内容: n 避免相关性。 n 命名方法。 n 保护方法。 n 虚拟方法。 n 声明方法。 通常,组件不应有太多的方法,应尽量减少应用程序需要调用的方法。用方法实现的

特性常常可以用属性封装,属性提供适合 C++ Builder环境的接口,可以在设计时访问,

并且是实现面向状态设计的基础。

4.1 避免相关性

编写组件时,自始至终都要尽可能减少强加给应用程序开发人员的约束。无论他们想

做什么都应尽最大可能满足他们的要求。有时候,可能无法做到这一点,但也应尽可能接

近这个目标。 下面列出了应避免的各种相关性: n 用户必须调用的方法。 n 必须按一定次序调用的方法。 n 使组件进入可能会导致某个事件或方法无效的状态与模式的方法。 处理这种情形的最好办法就是确保不提供上述类型的方法。例如,如果调用某个方法

会导致组件进入某个状态,在该状态中另一个方法会失效,那么应该重新编写第二个方法

(即会出错的那个方法),这样,当组件在该状态中调用该方法时,该方法就首先修正状

态,然后执行主要代码。至少,当用户调用某个无效方法时应发送异常。换句话说,如果

代码的一部分依赖另一部分,那么你有责任保证当代码被不正确调用时不能产生问题。例

如,当用户不适应你的代码时,产生警告消息显然比导致系统错误更好。

第五章 控件的设计 第 278 页

4.2 保护方法

类的所有部分,包括数据成员、方法和属性,都有保护等级或者说“可见性”,具体

解释参见前面内容。为每个方法选择可见性是相当简单的。组件中大多数方法的可见性是

public 或 protected 的。很少需要 private 方法,除非它是专为本类型的组件定制、甚

至由其派生组件都不应访问的。 注意通常没不需要将方法声明为__published。这样做的结果是对于用户来说,与声

明方法为 public没有什么不同。

4.2.1 应为 public 的方法

任何应用程序开发人员需要调用的方法都必须声明为 public。需要记住的是,大多数

方法都是在事件处理程序中调用的,因此,方法要避免占有系统资源或导致 Windows 无

法响应用户。 类的构造函数和析构函数必须为 public。

4.2.2 应为 protected 的方法

任何为组件实现的方法都应声明为 protected,这样应用程序就无法在错误的时刻调

用它们。如果应用程序无需调用你的方法,而派生类却需要调用,那么将它们声明为

protected。例如,假设你的方法依赖于某个以前设置的数据,如果你将该方法声明为

public,那么应用程序就有可能在所需数据设置之前调用它。另一方面,将它声明为

protected,就可保证应用程序不会直接调用它。你可以另建一个 public方法以保证在调

用 protected方法之前,数据会被设置。用于实现属性的方法应声明为 virtual、protected

方法。这样,应用程序开发人员既可以重载该属性的实现,又可以修改它的功能或完全取

代它。这样,属性将有充分的多态性。将方法声明为 protected,可以确保开发人员不会

偶然地调用它们,或无意中修改属性。

4.3 虚拟方法

对于同一个方法,如果你想根据不同类型调用不同的代码,那么将你的方法声明为

virtual。如果你的组件是由应用程序开发人员直接使用,那么可以将所有的方法声明为

virtual。另一方面,如果组件是可以派生其他组件的抽象类,可以考虑将附加的方法声

明为 virtual。这样,派生的组件可以重载它继承的 virtual方法。

4.4 声明方法

声明组件的方法同声明任何类方法一样。要在组件中声明方法,要做以下的事情: n 在组件头文件的类声明中加入声明。 n 在单元的.CPP文件中编写实现代码。 下面是个组件的代码,它定义了两个新方法,一个 protected 方法和一个 public 虚

拟方法。.H文件中的接口定义为:

第五章 控件的设计 第 279 页

class PACKAGE TSampleComponent : public TControl

{

protected:

void __fastcall MakeBigger();

public:

virtual int __fastcall CalculateArea();

//...

};

下面是在 CPP文件中实现方法的代码

void __fastcall TSampleComponent::MakeBigger()

{

Height = Height + 5;

Width = Width + 5;

}

int __fastcall TSampleComponent::CalculateArea()

{

return Width * Height;

}

5 在组件中使用图形

Windows为设备无关图形的绘制提供了强大的图形设备接口(GDI)。但是,GDI给

程序员提出了一些额外的要求,如管理图形资源,正如前面所提到的那样,Windows 只

提供了有限的绘图资源,如果不能正确的管理和使用,将会带来灾难性的后果。C++ Builder提供了更高一级的资源管理能力,是程序员可以将精力集中在有效益的工作上,

而不是放在寻找丢失的句柄或者释放资源等问题上。 与任何 Windows API一起使用时,可以在 C++ Builder应用程序中调用 GDI函

数。但是你很快会发现使用 C++ Builder的图形函数封装可以更快更方便。

5.1 图形概述

C++ Builder 在多个层次上封装 Windows API。对作为组件开发员而言,最重要

的是组件在屏幕上显示图片的方式。直接调用 Windows GDI 时,需要一个设备场境

(device context)句柄,在设备场境中,你选择了各种绘图工具如笔、刷子和字体。

在绘图以后,你必须将设备场境恢复到设置之前的原始状态。为了使你无需在低层处理图

第五章 控件的设计 第 280 页

形,C++ Builder 提供了一个简单、完备的接口:组件的 Canvas 属性。画布保证拥有

一个有效的设备场境,而在你不用它时自动释放它。同样,画布拥有自己的用于描述当前

笔、刷子和字体的属性。画布替你管理所有这些资源,因此,你无需再关心创建、选择和

释放诸如画笔句柄这样的东西。你只需告诉画布你需要什么样的笔,画布自会处理其他事

情。让 C++ Builder管理图形资源的另一个好处是它能为以后使用资源进行缓冲,这样

可以加速重复性的操作。例如,如果你有个程序需要重复地创建、使用和配置某种笔,你

不得不在每次使用它时重复这些工作。由于 C++ Builder能缓冲图形资源,那么很可能

你重复使用的工具就在缓冲区中,因而你可以使用它们而无需重新创建。这方面的一个例

子是,某个程序有几十个打开的窗体,有几百个控件。每个控件都有一个或多个 TFont属性,这样就可能存在数百乃至数千个 TFont对象实例。由于 VCL的缓冲作用,大多数

应用程序只使用两三个字体句柄。 下面有两个例子显示 C++ Builder图形代码可以简单到什么程度。第一个例子使用

标准的 GDI函数在窗口中画一个蓝边的黄色椭圆,如果你使用其他的开发工具,你很可能

就是这么干的;第二个例子是使用 C++ Builder中的画布画同样的椭圆。 下面是在 Object Windows中实现的代码: void TMyWindow::Paint(TDC& PaintDC, bool erase, TRect& rect)

{

HPEN PenHandle, OldPenHandle;

HBRUSH BrushHandle, OldBrushHandle;

PenHandle = CreatePen(PS_SOLID, 1, RGB(0, 0, 255));

OldPenHandle = SelectObject(PaintDC, PenHandle);

BrushHandle = CreateSolidBrush(RGB(255, 255, 0));

OldBrushHandle = SelectObject(PaintDC, BrushHandle);

Ellipse(10, 20, 50, 50);

SelectObject(OldBrushHandle);

DeleteObject(BrushHandle);

SelectObject(OldPenHandle);

DeleteObject(PenHandle);

)

下面的 C++ Builder代码完成同样的事情: void __fastcall TForm1::FormPaint(TObject *Sender) {

Canvas->Pen->Color = clBlue;

Canvas->Brush->Color = clYellow;

Canvas->Ellipse(10, 20, 50, 50);

}

第五章 控件的设计 第 281 页

显然使用 C++ Builder中的 TCanvas画布组件来实现 Windows的绘图操作要比

使用 API高效、简洁的多。

5.2 使用画布

画布(TCanvas)在多个层次上封装 Windows 图形,包括高层用于画单个直线、形

状和文本的函数;中层用于封装画布绘图能力的属性;下层直接访问 Windows GDI。表

5.2总结了画布的能力。 表 5.2画布能力摘要 层次 操作 工具

画直线和图形 MoveTo、LineTo、Rectangle和 Ellipse等方法 显示和调节文本 TextOut、TextHeight和 TextRect等方法 高层 填充区域 FillRect和 FloodFill方法 定制文本和图形 Pen、Brush和 Font属性操作 像素 Pixels属性

中层 拷贝和融合图片 Draw、StrechDraw、BrushCopy 和 CopyRect 方

法;CopyMode属性

下层 直接调用 Windows GDI函数

Handle属性

TCanvas类及其属性和方法的信息参见联机帮助。

5.3 使用图片

在 C++ Builder中,你所做的大部分图形工作仅限于直接在组件和窗体的画布上绘

图。C++ Builder 还提供对单个图形图片,如位图、元文件和图标的处理,包括调色板

的自动管理。 下面是在 C++ Builder中使用图片需要关注的三个重要方面: n 使用图片、图形或画布。 n 载入和存储图形。 n 处理调色板。

5.3.1 使用图片、图形或画布

C++ Builder中有三个处理图形的类: 1. TCanvas 标识窗体、图形控件、打印机或位图的位图作图面。画布总是作为其

它对象的属性或者工具出现,从来不会是单独的类出现在程序中。 2. TGraphic 标识图形的图片,图片通常存于文件、资源,如位图、图标或元文件

中。C++ Builder定义了TBitmap、TIcon和TMetafile类,它们均从 TGraphic派生。用户也可以定义自己的图形类。通过为所有图形定义基本的标准接口,

TGraphic为简化应用程序使用各种图形提供了一个简单的机制。 3. TPicture 是个图形容器,这意味着它可以包含任何图形类。也就是说,

第五章 控件的设计 第 282 页

TPicture类型的项可以包含位图、图标、元文件或用户自定义的图形类型,应用

程序可以通过图片类使用同一种方式访问它们。 例如,图片控件有 TPicture类型的属性 Picture,从而可以显示各种图形。在 VCL

中,图片类总是包含有图形类,而图形类包含画布类(标准的含有画布的图形类是

TBitmap),通常,当处理图片时,仅仅使用了一部分通过 TPicture显示的图形类,如

果需要访问特写的图形类本身,可以参考图片的 Graphic属性。

5.3.2 载入和存储图形

C++ Builder中的所有图片和图形都能从文件中载入它们的图像,也能存储它们(也

可以存储在不同的文件中)。你可以在任何时候载入和存储图片。调用图片的

LoadFromFile 方法从文件载入图像。调用图片的 SaveToFile 将图像保存到文件。

LoadFromFile 和 SaveToFile 方法只有一个参数,即文件名。LoadFromFile 方法

使用文件扩展名确定创建和装入的图形对象的类型。SaveToFile 方法保存与图形对象相

适应的任何类型的文件。要装入图像控件的位图,将位图文件名作为参数传给图片的

LoadFromFile方法:

void __fastcall TForm1::FormCreate(TObject *Sender)

{

Image1->Picture->LoadFromFile("c:\\windows\\athena.bmp");

}

图片 TPicture能够识别 BMP 是位图文件的标准扩展名,于是,它创建 TBitmap 图

形,然后调用图形的 LoadFromFile方法。由于图形是个位图,于是它从文件装入位图。 在 C++ Builder 中能够被图片识别的文件类型只有 Icon、Bitmap、Jpeg、

Metafile 等几种类型,大多数的图形文件并不能直接在 VCL中使用,需要载入这些文件

可以通过派生图片类,并且重载其载入方法。 除了文件载入之外,VCL还提供了“流”载入方法,也可以将图形文件存入“流”中,

任何从 TStream 派生的类,入资源流、Socket 流、文件流或者是自定义的流都可以作

为图形文件的载体。

5.3.3 处理调色板

当 C++ Builder控件运行在基于调色板的设备上时,它自动支持调色板实现。也就

是说,如果你有一个需要用调色板的控件,可以使用从 TControl继承的两个方法去控制

Windows调节调色板。 控件的调色板支持下面两个方面: n 为控件指定调色板。 n 响应调色板改变。 大多数控件不需要调色板,但是那些有“丰富色彩(rich color )”的图形图像(如

第五章 控件的设计 第 283 页

图像控件)可能需要与 Windows 交互,然后屏幕设备驱动程序保证控件正确显示。

Windows称这个过程为实现(realizing)调色板。实现调色板是个确保前台窗口最大

限度地使用自己的调色板的过程,后台窗口尽可能多地使用自己的调色板,然后将其他的

颜色映射为“现实(real)”调色板中最接近的颜色。当窗口移到另一个窗口的前面时,

Windows不断地实现相应的调色板。 注意除了在位图中,C++ Builder 自身并未对创建或维护调色板提供专门的支持。

但是,如果你有一个调色板句柄,C++ Builder控件会替你管理它。 1.为控件指定调色板 为了给控件指定一个调色板,重载控件的 GetPalette方法返回一个调色板句柄。 要控件指定调色板需要完成下列事情: n 通知应用程序控件的调色板需要实现 n 指定将要实现的调色板 2.响应调色板改变 如果你的控件通过重载 GetPalette指定了一个调色板,C++ Builder 会自动响应

由 Windows 发送的调色板消息。处理调色板消息的方法是 PaletteChanged。PaletteChanged 的基本功能是确定是在前台还是在后台实现调色板。Windows 处理调

色板实现的准则是,最顶层窗口使用前台调色板,其他窗口使用后台调色板。C++ Builder对调色板实现做了进一步处理,即在同一个窗口中的控件按 Tab次序实现调色板。只有当

你的控件不是在 Tab次序的第一个而又想使用前台调色板时,你才需要重载该缺省行为。

5.4 幕后位图

当绘制复杂的图形图像时,Windows 编程中一个常用的技术是创建一个幕后位图,

在该位图上绘制图像,然后将其从幕后位图中拷贝到屏幕上。使用幕后图像减少了由于重

复直接在屏幕上作图而产生的闪烁。 C++ Builder的位图类表示资源和文件中的位图图像,也能以幕后位图的方式工作。 当绘制复杂的图像时,要避免直接在屏幕画布上绘制。你可以构造一个位图对象,在

它的画布上绘制,然后将其拷贝到屏幕的画布上去,而不要直接在窗体或控件的画布上绘

制。通常人们在图形控件的 Paint 方法中使用幕后位图。本书的例子中,DataPick 就

使用了幕后位图来实现图形的绘制,从而避免了绘制过程中图形频繁闪烁。 C++ Builder 提供了四种不同的方式将图像从一个画布拷贝到另一个。依据需要的

效果,可以调用不同的方法。 下面列出了这几种拷贝的方法和效果: 效果 方法 拷贝整个图形 Draw 拷贝图形且重置图形大小 StretchDraw 拷贝画布的一部分 CopyRect 在拷贝中使用光栅操作 BrushCopy 这几种方法在 API中有相应的函数和其对应,但使用起来要比它们复杂一些,而且根

据实际测试的结果,使用 VCL的图形函数在绘图速度上并不比直接使用 API慢,而在对

第五章 控件的设计 第 284 页

Jpeg文件的测试中,VCL的效果比 API还略胜一筹。

5.5 响应改变

所有图形对象,包括画布和它拥有的对象(笔、刷子和字体)均有内置的方法响应对

象的改变。通过使用这些方法,可以使你的组件响应改变重画图像。如果将对象作为设计

时组件接口的一部分发布的话,那么响应图形对象的改变就尤其重要。使组件设计时的显

示与在对象观察器中的属性设置一致的唯一途径就是响应对象的改变。为了在图形对象中

响应改变,要为类的 OnChange事件指定一个方法。 例如 shape 组件发布了标识自己使用的笔和刷子的属性。组件的构造函数为每个

OnChange 事件指定了一个方法,于是当笔或刷子发生改变时组件将重画自己的图像。

shape组件是用 Object Pascal编写的,而下面的代码是它的 C++版本,用了个新名

字 TMyShape。 下面是头文件中的类声明: class PACKAGE TMyShape : public TGraphicControl

{ private:

protected:

public:

virtual __fastcall TMyShape(TComponent* Owner);

__published:

TPen *FPen;

TBrush *FBrush;

void __fastcall StyleChanged(TObject *Sender);

};

下面是.CPP文件代码: __fastcall TMyShape::TMyShape(TComponent* Owner)

:TGraphicControl(Owner)

{

Width = 65;

Height = 65;

FPen = new TPen;

FPen->OnChange = StyleChanged;

FBrush = new TBrush;

FBrush->OnChange = StyleChanged;

}

第五章 控件的设计 第 285 页

void __fastcall TMyShape::StyleChanged(TObject *Sender)

{

Invalidate();

}

6 处理消息

在传统的 Windows 编程中,一个关键之处就是处理 Windows 发送给应用程序的消

息。C++ Builder为你处理大多数消息。但是,你还是可以处理 C++ Builder未处理

的消息或创建你自己的消息。 使用消息需要注意下面三个方面: n 理解消息处理系统。 n 修改消息处理。 n 创建新的消息处理器。

6.1 理解消息处理系统

所有的 C++ Builder类都有内置的消息处理机制,称为消息处理方法或消息处理器。

消息处理器的基本思路是,类接收某些消息和分派消息,依据接收的消息调用一系列指定

的方法。对某一消息如果没有指定的方法,就进行缺省处理。 下面的框图显示了 VCL的消息分派系统:

在前面介绍 VCL类库的时候,我们已经讲过关于消息处理的一些属性和方法等,也对

VCL 的消息处理机制作了本质上的分析,实际的 VCL 消息处理过程遵循着上面图中的顺

序,对于消息的响应,可以根据需要在上面的消息链中加入自己的消息处理代码,也可以

使用宏定义 MESSAGEMAP来响应 Windows 消息(实际上 MESSAGEMAP是对 Dispatch方法的重载),但有一点需要注意,无论采用什么样的方式,都必须保持上面的消息链不

能断,如重载了某个消息处理方法或者重新设置了某个消息处理的属性时,必须保证至少

是自己没有处理的消息仍然要传递给原有的消息处理过程。 下面的例子是使用了自定义的消息处理方法来对 Form1 的 WM_COMMND 消息进行处

理,采用的是重新设置 WindowProc 属性的方式来在消息链中增加自己的消息处理代码,

如下: .H文件中定义 class TForm1 : public TForm

{

__published:

MainWinProc WinProc Dispatch HandlerEvent

图 5.1 VCL 组件消息派发的顺序

第五章 控件的设计 第 286 页

void __fastcall FormCreate(TObject *Sender);

public:

TWndMethod OldWinProc;

void __fastcall MyWinProc(Messages::TMessage &Msg);

__fastcall TForm1(TComponent* Owner);

};

CPP文件中实现代码 void __fastcall TForm1::FormCreate(TObject *Sender)

{ OldWinProc = WindowProc; //保存原有的 WindowProc属性

WindowProc = MyWinProc; //设置新的 WindowProc属性

}

//-------------------------------------------------------

void __fastcall TForm1::MyWinProc(Messages::TMessage &Msg)

{

if(Msg.Msg == WM_KEYDOWN)

{ //以下代码只对 WM_KEYDOWN起作用

switch (Msg.WParam)

{

case VK_F1:

//... 执行自定义代码,并且不提交给原有处理过程

Application->MessageBox("OK This is My Proc","OK",0);

break;

case VK_F4: //... 其它的处理键

//...

break;

default :

OldWinProc(Msg); //缺省时,提交给原有处理过程

}

}

else

OldWinProc(Msg); //非 WM_KEYDOWN消息时,提交给原有处理过程

}

第五章 控件的设计 第 287 页

在这个例子中使用了 TWinControl 提供的 WindowProc 属性,而 WindowProc 属

性对应的处理方法实际上就是 TWinControl 的 WndProc方法,WndProc 是一个虚拟方

法,因此也可以通过重载 WndProc的方式来实现以上功能,其代码如下: .H文件中定义 class TForm1 : public TForm

{

public:

virtual void __fastcall WndProc(Messages::TMessage &Message);

__fastcall TForm1(TComponent* Owner);

};

CPP文件中实现代码 void __fastcall TForm1::WndProc(Messages::TMessage &Message)

{ if(Message.Msg == WM_KEYDOWN)

{ //以下代码只对 WM_KEYDOWN起作用

switch (Message.WParam)

{

case VK_F1:

//... 执行自定义代码,并且不提交给原有处理过程

Application->MessageBox("OK This is My WinProc ","OK",0);

break;

case VK_F4: //...其它的处理键

//...

break;

default :

TWinControl::WndProc(Message);

//缺省时,提交给原有处理过程

}

}

else

TWinControl::WndProc(Message);

//非 WM_KEYDOWN消息时,提交给原有处理过程

} 比较这两种方式,发现使用属性比起重载方法要稍微复杂一些,但是使用属性会带来

一些灵活性。而且,实际上 WindowProc是一个没有发布的“事件”,具有事件的所有特

第五章 控件的设计 第 288 页

征,因此可以在 Form中定义一个消息处理方法并将其赋给任何一个 Form中的窗体控件

的 WindowProc属性,而重载 WndProc 方法只能在用户代码定义的类中使用。并且属性

允许运行时更换处理句柄,而重载的方法一旦生成可执行代码就不能再更改。 关于消息处理的其它方面在后面的内容中进一步介绍。

6.1.1 Windows 消息中有什么

可以把 Windows 消息当作包含几个有用数据成员的数据结构。成员中最重要的是个

标识消息的整型值。Windows 定义了许多消息,MESSAGES.HPP 中声明了它们的标识符。

Windows 程序员早已习惯了使用那些标识消息的 Windows 定义,如 WM_COMMAND 或

WM_PAINT。传统的 Windows 程序有个窗口过程作为系统消息的回调函数。在窗口过程

中,通常有一个庞大的 switch语句,且对每个有待处理的消息有一个 case语句。附加

的信息作为两个参数 wParam和 lParam传递给该窗口过程,分别用于“字类型参数”和

“long 类型参数”。通常,每个参数都含有不止一个信息,因此需要调用 LOWORD 和

HIWORD宏取出相应的信息。 例如,调用 HIWORD(lParam)得到该参数的高字。早先,Windows 程序员不得不记

住或在 Windows API 中查找每个参数包含的信息。近来,Windows 实现了“消息解析

器”以简化处理 Windows 消息和消息参数的语法。通过使用“消息解析器”代替庞大的

switch 语句解析消息参数,你可以简化消息处理函数。如果你在标准的 Windows 程序

中包含了 WINDOWSX.H,就可以在你的代码中使用 HANDLE_MSG宏: void MyKeyDownHandler( HWND hwnd, UINT nVirtKey, BOOL fDown, int CRepeat,

UINT flags )

{ //... }

LRESULT MyWndProc( HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam )

{

switch( Message )

{

HANDLE_MSG( hwnd, WM_KEYDOWN, MyKeyDownHandler );

//...

}

HANDLE_MSG宏的原型如下:

#define HANDLE_MSG(hwnd, message, fn) \

case (message):return HANDLE_##message((hwnd),(wParam),(lParam),(fn))

通过使用这种消息解析,可以很清楚地看到消息被派发给了指定的处理器。你还可以

给处理函数的参数表命名有意义的名字。对于 WM_KEYDOWN 消息处理函数来说,将

第五章 控件的设计 第 289 页

wParam参数命名为 nVirtKey显然有助于理解。 要了解详细的 Windows消息结构请参阅 Windows SDK帮助文件。

6.1.2 派发消息

当应用程序创建窗口时,它在 Windows 内核中注册了一个窗口过程。窗口过程就是

为该窗口处理消息的例程。传统上,窗口过程包含一个庞大的 switch语句,该语句对每

个有待处理的消息均有一个入口。记住,此处的“窗口”意思是屏幕上的任何东西:每个

窗口、每个控件(不包括 VCL中的图形控件),诸如此类。每次创建新类型的窗口时都不

得不创建一个完整的窗口过程。 C++ Builder采用几种方式简化消息派发: n 每个组件都继承了完整的消息派发系统。 n 分派系统有缺省处理。只需为需要特殊处理的消息定义处理器。 n 可以依靠继承的方法完成大多数处理,而自己只需修改消息处理的一小部分。 C++ Builder 为应用程序中的每个组件注册了 MainWndProc 方法作为窗口过程。

MainWndProc包含异常处理块,它将 Windows传来的消息结构传给虚拟方法 WndProc,并调用应用程序类的 HandleException 方法处理所有的异常。MainWndProc 不是虚拟

方法,对任何特定的消息也不作特别处理。由于每个 VCL控件都可以重载 WndProc 以适

应自己特殊的需要,因此定制发生在 WndProc中。WndProc 方法检查任何对处理有影响

的条件,因此它可以“屏蔽”不想要的消息。例如,当被拖动时,组件忽略击键消息,因

此 TWinControl 的 WndProc 只有在没有被拖动的情形下才传递击键事件。最终,

WndProc 调用从 TObject 那儿继承来的虚拟方法 Dispatch 确定该调用哪个方法来处

理消息。Dispatch使用消息结构中的 Msg数据成员来确定如何派发一个特定的消息。如

果组件为该消息定义了处理器,那么 Dispatch就调用该处理器。如果组件没有为该消息

定义处理器,那么 Dispatch就调用 DefaultHandler。

6.2 修改消息处理

在修改组件的消息处理之前,必须确保这确实是你想做的。C++ Builder把大多数

Windows 消息转换为组件开发员和组件用户都能处理的事件。应尽可能修改事件处理,

而不是修改消息处理。要修改消息处理,需要重载消息处理方法。可以通过屏蔽某一消息

以禁止在特定环境下处理该消息。

6.2.1 重载处理器方法

为了修改组件对某一消息的处理,需要重载该消息的处理方法。如果组件还没有处理

该消息,可声明一个新的消息处理方法。 要重载消息处理方法可以通过下面两种途径: 1. 在组件中声明一个新方法,新方法的名字与在组件声明的 protected部分中被

它重载的方法的名字一样。比如 Click方法是用于处理OnClick事件的虚拟方法,

在派生类中重载 Click方法可以实现对 OnClick事件的重载。

第五章 控件的设计 第 290 页

2. 使用宏将该方法映射到消息。宏的形式如: BEGIN_MESSAGE_MAP

MESSAGE_HANDLER(parameter1, parameter2, parameter3)

END_MESSAGE_MAP

parameter1 是 Windows 定义的消息索引,parameter2 是消息结构的类型,

parameter3 是消息方法的名字。可以在 BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP两个宏之间放置任意个想放置的 ESSAGE_HANDLER宏。

例如,为了重载组件的 WM_PINT 消息的处理,重新声明 WMPaint 方法,然后用上

述三个宏将该方法映射到 WM_PAINT消息: class PACKAGE TMyComponent : public TComponent

{

protected:

void __fastcall WMPaint(TWMPaint* Message);

BEGIN_MESSAGE_MAP

MESSAGE_HANDLER(WM_PAINT, TWMPaint, WMPaint)

END_MESSAGE_MAP(TComponent)

};

关于 BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP 宏在前面的章节中已经作了详细的介

绍,稍加分析就会发现,经过宏展开后的形式和前面重载 WndProc 的形式基本相同,程

序设计时也可以直接重载 Dispatch来实现消息链的扩展。 比较图 6.1,可以发现,重载 WndProc 或者设置 WindowProc 属性对消息的处理要

在 Dispatch之前进行,因此采用重载 WndProc 方法响应消息要比使用消息映射表的宏

更早一步,但使用宏定义可以得到有明确含义的消息结构,因此建议 C++ Builder的初

学者使用消息映射表,而熟悉 SDK或者有经验的程序员使用重载 WndProc的方法。

6.2.2 使用消息参数

在消息处理方法的内部,你的组件可以访问消息结构的所有参数。由于传递给消息处

理器的是个指针,因此在需要时,处理器可以修改参数值。消息的返回值是被修改的最频

繁的参数,该值是 SendMessage的返回值。 由于 Message参数的类型随被处理的消息变化,因此你应参考 Windows 消息文档,

弄明白单个参数的名字和含义。如果出于某种原因,需要使用消息参数的老式名字

(wParam、lParam,诸如此类),可以将 Message转换为 TMessage类型,该类型使

用老式参数名。关于 TMessage 以及 wParam、lParam 等参数的详细内容请参考联机帮

助。

第五章 控件的设计 第 291 页

6.2.3 屏蔽消息

在某些环境下,可能希望组件忽略某些消息。这时,你想禁止组件分派消息给自己的

处理器。为了屏蔽消息,你要重载虚拟方法 WndProc或者重新指定 WindowProc属性,

前面 6.1中的例子就包含了对 F4以及 F1键盘消息的屏蔽。 WndProc方法在将消息传给 Dispatch 方法之前能筛选消息。通过重载 WndProc,

你将可以得到一个在分派消息之前过滤消息的机会。对于一个从 TWinControl 派生的控

件,重载 WndProc的代码看上去如下: void __fastcall TMyControl::WndProc(TMessage* Message)

{

//判断代码

TWinControl->WndProc(Message);

}

其中判断代码可能是一个非常复杂和多样的形式,读者可以参考 6.1中的例子进行深

入的研究。 TControl组件定义了一个鼠标消息的入口范围,当用户拖动和放下控件时,它可以

过滤消息。通常有两种方式重载 WndProc可以达到上述目的: n 过滤消息范围,而不是为每个消息指定处理器 n 阻止消息分派,因而处理器根本不会被调用 下面是 TControl的 WndProc方法,它在 VCL中用 Object Pascal实现: procedure TControl.WndProc(var Message: TMessage);

begin

... if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then

if Dragging then

//{ 为 Dragging消息定义了处理句柄}

DragMouseMsg(TWMMouse(Message))

else

//... { 按照正常处理 }

end;

//... { 否则亦按照正常处理 }

end;

上面这一段代码非常有参考价值,因为 WM_MOUSEFIRST和 WM_MOUSELAST很少被人们

使用,但却是一个非常有用的消息,有兴趣的读者可以参考 VCL的源码分析。

第五章 控件的设计 第 292 页

6.3 创建新的消息处理器

由于 C++ Builder为大多数公共 Windows 消息提供了处理器,因此,当你定义了

自己的消息的时候,你就需要创建新消息处理器。使用自定义消息有两方面内容: n 定义自己的消息。 n 声明一个新的消息处理方法。

6.3.1 定义自己的消息

标准组件定义了许多内部使用的消息。定义消息最常见的理由是广播标准的 Windows消息未能涵盖的信息以及状态改变的通知。

定义一个新消息有两个步骤。如下: 1.声明一个消息标识符 消息标识符是个整型常量。Windows为自己保留小于 1024的消息,因此,当你声明

消息时,你应从大于 1024开始。 常量 WM_APP是自定义消息的起始值。当自定义消息时,应以 WM_APP为基础。要知

道,一些 Windows 标准控件使用了自定义消息范围内的消息。这些控件有列表框、组合

框、编辑框和命令按钮。如果你从它们派生组件,并且想为组件定义一个消息,你必须检

查 MESSAGES.HPP文件,弄清楚 Windows到底为它们定义了什么消息。 下面的代码自定义了两个消息: #define WM_MYFIRSTMESSAGE (WM_APP + 400)

#define WM_MYSECONDMESSAGE (WM_APP + 401)

2.声明一个消息结构类型 这并不是一个必需的过程,但是一个好的消息结构可以使对消息参数的使用更加方

便、快捷。在早期的 Windows程序设计中,程序员不得不牢记每一个消息中 wParam和

lParam参数分别传递了什么样的信息,C++ B uilder中 TMessage 结构就是使用了最

原始的消息结构,这也是 windows消息的缺省结构。 声明消息结构类型一般遵循下列约定: n 命名消息结构类型,名字有个前缀 T 。 n 第一个数据成员为 Msg,类型为 TMsgParam。 n 定义紧接着的两个字节为 Word型参数,再紧接着的两个字节不用。或者定义紧

接着的四个字节为 Longint(WORD)型参数。 n 最后一个数据成员为 Result,类型是 Longint。 例如,下面是键盘消息的消息结构 TWMKey: struct TWMKey

{

Cardinal Msg; //第一个参数,用于标识消息的 ID

WORD CharCode; //第二个参数,相当于 wParam

WORD Unused; //相当于 lParam参数低字

第五章 控件的设计 第 293 页

WORD KeyData; //相当于 lParam参数高字

WORD Result; //第四个参数,消息的返回值

};

6.3.2 声明一个新的消息处理方法

在很多情况下,需要对某个 Windows 消息进行响应,尽管声明新的消息处理方法通

过前面讲过的重载 WndProc或者重设置 WindowProc 属性等方法均可以实现,但是通常

对以下两种情况,建议重新定义新的消息处理方法: n 你的组件需要处理标准组件未处理的 Windows消息。 n 需要对自定义的消息进行处理。 要声明一个新的消息处理方法,操作如下: n 命名消息处理方法,注意不要下划线字符并确保方法返回 void。 n 给消息处理方法传递消息结构类型的指针。 n 使用 BEGIN_MESSAGE_MAP和 END_MESSAGE_MAP宏映射方法到消息。 n 在方法实现中编写代码。 n 调用继承的消息处理器。 例如,下面是为自定义消息 CM_CHANGECOLOR声明的处理器。 #define CM_CHANGECOLOR (WM_APP + 400) class TMyControl : public TControl

{

protected:

void __fastcall CMChangeColor(TMessage &Message);

BEGIN_MESSAGE_MAP

MESSAGE_HANDLER(CM_CHANGECOLOR, TMessage, CMChangeColor)

END_MESSAGE_MAP(TControl)

};

void __fastcall TMyControl::CMChangeColor(TMessage &Message)

{

Color = Message.LParam; //从 lParam获取 Color

TControl::CMChangeColor(Message); //调用继承来的消息处理方法

}

7 使组件在设计时可用

C++ Builder的组件存在于两种不通类型的库包(Package)中,一种是运行时库,

第五章 控件的设计 第 294 页

另一种是设计时库,运行时库以.bpl结尾,设计时库以.bpi结尾,要使组件在 IDE中

可以使用,必需生成设计时库文件,如果应用程序选择了静态链结,则运行时库并不是必

需的,通常在新建一个库文件工程时,编译器缺省的设置为同时生成运行时库和设计时库。 下面的内容将描述如何使组件在 IDE中可用所需的步骤。一般来讲使组件在设计时可

用需要几个步骤: n 注册组件。 n 添加组件面板位图。 n 为你的组件提供帮助。 n 添加属性编辑器。 n 添加组件编辑器。 n 将组件编译成软件包。 并非每个组件都需要所有的步骤。例如,如果没有定义任何新的属性或事件,无需为

它们提供帮助,或者组件仅仅是共内部使用也没有必要生成帮助文件。但任何时候都必须

的步骤是注册和编译。一旦你的组件已经注册且已编译成软件包,那么它们就可以分发给

其他程序员安装到 IDE中。

7.1 注册组件

注册工作是建立在编译单元的基础上,因此如果你在一个编译单元中创建了几个组

件,你可以一次全部注册它们。为了注册组件,在单元的.CPP文件中添加一个 Register函数。在该函数内部,你注册组件并决定将它们安装到组件面板的什么地方。

如果你是通过在 IDE中选择 Component | New Component 来创建你的组件,那

么注册所需的代码已自动添加了。需要注意的是如果第一次编译并生成库文件后,组件注

册在那一个 Component 面板,它将一直不会改变,即使改变 RegisterComponents 函数

(详见后面内容)中的参数,除非是在一个全新的 C++ Builder中安装注册。 手工注册组件要经过以下几个步骤:

7.1.1 声明 Register 函数

注册过程包括.CPP文件中编写一个 Register函数,且该函数必须位于一个名字空

间中,名字空间的名字就是组件文件的名字,除第一个字母以外均为小写。 下面的代码就是一个名字空间中的 Register函数。名字空间为 Newcomp,文件名

为 Newcomp.CPP: namespace Newcomp

{

void __fastcall PACKAGE Register()

{

//注册组件的函数

}

}

第五章 控件的设计 第 295 页

在 Register 函 数 内 部 为 你 每 个 想 添 加 到 组 件 面 板 的 组 件 调 用

RegisterComponents。如果头文件和.CPP 文件包含几个组件,你可以一次把它们添

加到组件面板。由 PACKAGE宏扩展的语句使组件能被导入和导出。

7.1.2 编写 Register 函数

在 Register 函数内部,必须把想添加到组件面板的每一个组件都注册。

RegisterComponent涉及三件重要的事情: 1.指定组件 在 Register函数内部,声明一个 TComponentClass类型的数组,该数组包含你

要注册的组件。语法形如: TMetaClass classes[1] = {__classid(TNewComponent)); //生成一个组件数组

在这个例子中,类数组中只有一个组件,但是你可以把想注册的所有组件都添加到数

组中去。例如,下面的代码就有两个组件: TMetaClass classes[2] =

{__classid(TNewComponent),__classid(TAnotherComponent)};

另一种方式是采用单独的语句给组件类数组赋值。下面的语句同前面的语句作用是一

样的: TMetaClass classes[2];

classes[0] = __classid(TNewComponent);

classes[1] = __classid(TAnotherComponent);

2.指定组件面板的页面 组件面板页面的名字是个 ANSI字符串,如果你给出的名字标识的页面并不存在,C++

Builder将用此名创建一个新页面。C++ Builder 把标准页面的名字存储在字符串列表

(string-list)资源中,因此国际版本的软件可以用当地的语言为页面命名。如果你

想将组件安装于标准页面(指 C++ Builder中提供的页面)上,应调用 LoadStr函数

获取该字符串资源,传递的参数是标识该页面字符串资源的常量,例如 System 页面的标

识常量是 srSystem,关于组件面板的详细信息请参阅联机帮助。 3.使用 RegisterComponents函数 在 Register函数内部,调用 RegisterComponents注册组件类数组中的组件。

RegisterComponents 有三个参数:组件面板页面的名字、组件类数组和数组中最后一

个入口的索引。

第五章 控件的设计 第 296 页

下面的 Register 函数代码位于 NEWCOMP.CPP 文件中,它注册了一个

TMyComponent组件,并将其放在 Miscellaneous页面中: namespace Newcomp

{

void __fastcall PACKAGE Register()

{

TMetaClass classes[1] = {__classid(TMyComponent)};

RegisterComponents("Miscellaneous", classes,0);

}

}

RegisterComponents 的第三个参数是 0,它是最后一个入口的索引(数组大小减

去 1)。如果几个组件位于同一页面中,你可以一次注册它们,或者像下面的代码那样在不

同的页面中注册: namespace Mycomps

{

void __fastcall PACKAGE Register()

{

//生成包含两个元素的组件数组

TMetaClass classes1[2] = {__classid(TFirst), __classid(TSecond)};

//注册 classes1到 Miscellaneous组件面板

RegisterComponents("Miscellaneous", classes1, 1);

TMetaClass classes2[1]; //声明第二个组件数组

classes2[0] = __classid(TThird); //指定 classes2数组的第一个元素

//注册 classes2数组中的第一个元素所对应的组件到 Samples组件面板

RegisterComponents("Samples", classes2, 0);

}

}

例 子 中 声 明 了 两 个 数 组 , classes1 和 classes2 。 在 第 一 次 调 用

RegisterComponents 中,classes1有两个元素,所以第三个参数是 1;在第二次调

用 RegisterComponents中,classes2只有一个元素,所以第三个参数是 0。 在 C++ Builder中注册组件要比 Delphi中复杂的多,这是很多 C++ Builder 程

序员最恼火的地方,由于 VCL 最早是为 Delphi 而开发的,很多语法特点并没有顾及到

C++语言,读者可以参考前面讲述关于“Object Pascal语言的 C++对应”中相关内容。

第五章 控件的设计 第 297 页

7.2 添加组件面板位图

在组件面板中,每个组件都需要一个位图标识自己。如果没有指定位图,C++ Builder使用缺省位由于组件面板位图只是在设计时需要,所以不要将它们编译到组件的编译单元

中去。相反,应在一个 Windows资源文件中提供位图,资源文件名同.CPP文件名相同,

但后缀为.DCR(动态组件资源 dynamic component resource)。可以用 C++ Builder中的图像编辑器创建该位图。每个位图应是 24×24像素的。为你想安装的每个组件提供

一个组件面板位图文件,在每个组件面板位图文件中为你注册的每个组件提供一个位图。

位图图像的名字与组件类的名字相同,将组件面板位图文件与编译过的文件放在同一目录

中,这样当 C++ Builder在组件面板中安装组件时才能找到所需的位图。如果不想麻烦,

也可以将资源编译到库文件中,只需要按照正常的资源创建方法创建一个和组件名称相同

的位图资源,并将相应的资源文件包含在工程中。 例如,如果你创建了一个组件 TMyControl,需要创建一个包含 TMYCONTROL位图

的.DCR文件或.RES资源文件。资源名是大小写无关的,但是根据约定,通常使用大写。

7.3 为你的组件提供帮助

当在窗体中选择了一个了标准组件,或在对象观察器中选择了属性、事件时,你可以

通过按下 F1 键获得该项目的帮助。如果你创建了相应的帮助文件,也可以为你的组件向

开发员提供类似的文档,对于一个完整的组件,帮助文件是必不可少的,通常可以根据需

要,提供一个小的帮助文件以描述定制的组件,帮助文件将可以成为 C++ Builder帮助

系统的一部分。 可以使用任何工具创建 Windows 帮助文件的源文件(使用.rtf 格式)。然后使用

Microsoft Help Workshop 将 RTF文件编译成帮助文件,C++ Builder 在 Hellp | Tools目录中提供了该工具和使用手册。

为组件创作帮助文件有下列几个步骤: n 创建入口。 n 使组件帮助对上下文敏感。 n 添加组件帮助文件。 1.创建入口 为了能让你的组件帮助无缝地与库中其他组件的帮助集成在一起,考虑下面的约定: (1)每个组件都应有一个帮助主题,组件的主题应显示声明组件的单元以及组件的简

短描述。组件主题应链接第二个窗口,这第二个窗口描述组件在对象层次结构中的位置和

它所有属性、事件以及方法的列表。应用程序开发人员通过在窗体中选中组件然后按 F1键来访问这些主题。你可以把任意一个组件放到窗体上并按 F1键,看一下该组件的主题。

组件主题必须有一个#脚注,#脚注有一个用于该主题的唯一值,通过帮助系统唯一

地标识每个主组件主题应该有一个 K脚注,K脚注用于帮助系统索引关键字,而索引中包

含组件的类名。例如,TMemo组件的关键字脚注是 TMemo。组件主题还应有一个$脚注提

供主题的标题,该标题显示在 Topics Found 对话框、Bookmark 对话框和 History对话框中。

第五章 控件的设计 第 298 页

(2) 每个组件都应包含下列二级导航主题 n 一个层次结构主题,链接到该类在组件层次结构中的每个祖先。 n 一个组件所有可用属性的列表,链接到属性描述的入口。 n 一个组件所有可用事件的列表,链接到事件描述的入口。 n 一个组件所有可用方法的列表,链接到方法描述的入口。 可以使用 Alinks来链接 C++ Builder 帮助系统中的对象类、属性、方法或事件。

当链接对象类时,Alinks使用对象的类名,在类名后紧跟一个下划线和 object 字符串。

例如,链接 TCustomPanel对象,使用: !AL(TCustomPanel_object,1) 当链接到属性、方法或事件时,使用对象名作前缀,紧跟着的是下划线和属性、方法

或事件的名字。 例如,链接 TControl控件的 Text属性,使用: !AL(TControl_Text,1) 你可以显示任意一个组件的帮助,然后单击标为层次结构、属性、方法或事件的链接,

看看二级导航主题。 (3)每个在组件中声明的属性、方法或事件都应有一个主题 属性、方法或事件的主题应显示它的声明和使用描述。应用程序开发人员可以通过在

对象观察器中高亮显示该项目,然后按 F1,或把鼠标放到代码编辑器中的项目名上然后

按 F1来查看这些主题。在对象观察器中任意选择一个属性按 F1,看看它的主题。 属性、事件和方法主题应有一个 K脚注列出它们的名字以及它的名字与组件名的混合

体。因此,TControl组件的 Text属性有如下的 K脚注: Text,TControl;TControl,Text;Text, 属性、事件和方法主题还应有一个$脚注显示主题的标题,如 TControl::Text。所

有这些主题都有一个唯一的 ID,形如#脚注。 2.使组件帮助对上下文敏感 每个组件、属性、方法和事件主题都必须有一个 A脚注。当用户选择了组件并且按下

F1,或用户在对象观察器中选择了属性或事件且按下 F1时,A脚注用于显示主题。A脚

注必须遵循下列命名约定: 如果是组件的帮助主题,A脚注由两个被分号分隔的入口组成: ComponentClass_Object;ComponentClass 此处,ComponentClass是组件的类名。 如果是属性或事件的主题,A脚注由被分号分隔的三个入口组成: ComponentClass_Element;Element_Type;Element 此处,ComponentClass 是组件的类名,Element 是属性、方法或事件的名字,Type

指明是方法、属性还是事件。例如,TMyGrid 组件的 Background 属性的 A脚注形如: TMyGrid_BackgroundColor;BackgroundColor_Property;BackgroundCo

lor 3.添加组件帮助文件 使用 bin 目录中的 OpenHelp 工具(即 oh.exe),或使用 IDE 中的 Help |

第五章 控件的设计 第 299 页

Customize 把你的帮助文件添加到 C++ Builder 中。可以在 OpenHelp.hlp 文件中

找到 OpenHelp的使用说明,其中包括如何将你的帮助添加到帮助系统中。 关于生成帮助文件,并不是本书的主要内容,这里并不多讲,在本书提供的例程中,

有一个 TDataPick控件,提供了简单的帮助,从本书附带的光盘中可以找到其源文件和

帮助文件的所有原始内容,读者可以参考学习。

7.4 添加属性编辑器

对象观察器为所有类型的属性提供缺省的编辑。但是,可以通过编写并注册编辑器为

某一属性提供一个替代的编辑器。既可以为编写的组件属性提供专用的编辑器,也可以为

某一类型的所有属性提供编辑器。 如果简单一点,一个属性编辑器可以以下列两种形式之一进行工作:显示并允许用户

将属性作为文本字符串进行编辑,或显示一个允许以某种形式编辑的对话框。根据被编辑

的属性的类型,你可以提供一种或两种方式。 通常编写属性编辑器有以下五个步骤: n 派生属性编辑器类。 n 将属性作为文本进行编辑。 n 将属性作为整体进行编辑。 n 指定编辑器属性。 n 注册属性编辑器。

7.4.1 派生属性编辑器类

在 DSGNINTF.HPP中定义了几种属性编辑器,均是从 TPropertyEditor 派生。当

创建属性编辑器时,属性编辑器既可以直接从 TPropertyEditor 派生,也可以从下表

中描述的属性编辑器类派生。 表 5.2预定义的属性编辑器类型

类型 适用的属性

TordinalProperty 所有的序数属性编辑器(用于整型、字符、枚举属性编辑)均从 TOrdinalProperty派生

TintegerProperty 所有整型类型,包括预定义的和用户定义的子域 TcharProperty 字符类型和字符的子域,如‘A ’. . .‘Z ’ TenumProperty 任意枚举类型 TfloatProperty 所有的浮点数 TstringProperty ANSI字符串 TsetElementProperty 集合中的单个元素,以布尔值形式显示

TsetProperty 所有的集合。集合不能直接编辑,而是展开成集合元素属性列表

TclassProperty 类。显示类名并允许扩展类的属性 TmethodProperty 方法指针,大多数值得注意的事件

第五章 控件的设计 第 300 页

TcomponentProperty 同一窗体中的组件。用户不能编辑组件的属性,但是可以指向一个适当类型的组件

TcolorProperty 组件颜色。如果可能显示颜色常量,否则显示十六进制值。下拉式列表框包含颜色常量。双击打开颜色选择对话框

TFontNameProperty 字体名。下拉式列表框包含所有当前安装的字体 TfontProperty 字体。允许将单个字体属性扩展为字体对话框 DSGNINTF.HPP 中还定义了一些用于编辑具有唯一性的属性(如组件名)的属性编

辑器。列出的属性编辑器是那些对用户定义的属性非常有用的编辑器。 下面的例子声明一个名为 TMyPropertyEditor的简单属性编辑器: class PACKAGE TMyPropertyEditor : public TPropertyEditor

{

public:

virtual bool __fastcall AllEqual(void);

virtual System::AnsiString __fastcall GetValue(void);

virtual void __fastcall SetValue(const System::AnsiString Value);

__fastcall virtual ~TMyPropertyEditor(void) { }

__fastcall TMyPropertyEditor(void) : Dsgnintf::TPropertyEditor() { }

};

7.4.2 将属性作为文本进行编辑

所有的属性都要为对象观察器的显示提供一个字符串标识。大多数属性还允许用户为

属性键入一个新值。可以重载属性编辑器类提供的虚拟方法完成真实值和文本描述之间的

转换。 重载的方法名为 GetValue和 SetValue。你的属性编辑器还继承了一系列方法用于

不同类型值的赋值和读取,见下表 表 5.3读写属性值的方法 属性类型 Get方法 Set方法 浮点数 GetFloatValue SetFloatValue

Closure(事件) GetMethodValue SetMethodValue

序数类型 GetOrdValue SetOrdValue

字符串 GetStrValue SetStrValue

当重载一个 GetValue方法时,将调用 Get 方法中的一个;当重载一个 SetValue

方法时,将调用 Set方法中的一个。 1.显示属性值 属性编辑器的 GetValue方法返回属性当前值的文本描述。对象观察器在该属性的值

第五章 控件的设计 第 301 页

列中使用此文本描述。缺省时,GetValue返回“unknown”。 重载属性编辑器的 GetValue方法给你的属性提供一个文本描述。 如果属性不是字符串,GetValue必须将值转换为字符串描述。 2.设置属性值 属性编辑器的 SetValue方法将用户在对象观察器中键入的字符串转换为适当的值,

然后将该值赋给属性。如果得到的字符串描述的并不是正确的属性值,SetValue会发送

异常且不使用不正确的值。 重载属性编辑器的 SetValue方法以读取将赋给属性的值。 SetValue应对字符串进行转换,并在调用 Set方法之前对值进行验证。

7.4.3 将属性作为整体进行编辑

将属性作为整体编辑的意思是,提供一个对话框,当用户双击属性时或者单击属性旁

边的编辑按钮(...),弹出对话框,如 Font、Color等属性的编辑器。 重载属性编辑器类的 Edit方法以提供整体属性编辑器对话框。 Edit方法调用的 Set 和 Get方法与 GetValue和 SetValue 调用的是一样的。实

际上,Edit既调用 Get方法,也调用 Set方法。由于这种编辑器是类型专用的,因此通

常不需要将属性值转换为字符串。这类编辑器通常将相应的值当作“接收的值(as retrieved )”一样处理。

当用户单击属性旁边的“...”按钮或双击相应的值列时,对象观察器调用属性编辑

器的 Edit方法。 在实现 Edit方法时,遵循下列步骤: n 构造用于该属性的编辑器。 n 读取当前值,并用 Get方法将其赋给属性。 n 当用户选择了一个新值时,调用 Set方法将值赋给属性。 n 销毁编辑器。

7.4.4 指定编辑器特性

属性编辑器必须为对象观察器提供信息以决定显示什么工具。例如,对象观察器需要

知道属性是否有子属性或能否显示可能值的列表。 重载属性编辑器的 GetAttributes方法指定编辑器特性。 GetAttributes 方法返回一列 TPropertyAttributes 类型的值,值为下表中值

的任意一个或多个,甚至全部: 表 5.4属性编辑器特性标志 标志 相关的方法 含义 paValueList GetValue 编辑器可以给出枚举值的列表 PaSubPropertiess GetPropertie 编辑器有可以显示子属性

PaDialog Edit 编辑器可以显示用于编辑整个属性的对话框

第五章 控件的设计 第 302 页

paMultiSelect ____ 当用户选择了多个组件时,该属性应显示

paAutoUpdate SetValue 对于每个改变都自动更新组件,无需等待确认

PaSortList ____ 对象观察器应对值进行排序 PaReadOnly ____ 用户不能修改属性值

paRevertable ____

使 对 象 观 察 器 上 下 文 菜 单 中 的ReverttoInherited菜单项有效。该菜单项告诉属性编辑器放弃当前属性值并返回到预先确定的缺省值或标准值

Color属性比大多数属性要多变一些,因为它允许用户在对象观察器通过几种方式选

择:键入、从列表中选择和定制的编辑器。因此,TColorProperty 的 GetAttributes方法在返回值中包含几个属性:

virtual __fastcall TPropertyAttributes TColorProperty::GetAttributes()

{

return TPropertyAttributes() << paMultiSelect << paDialog << paValueList;

}

7.4.5 注册属性编辑器

一旦创建了一个属性编辑器,需要在 C++ Builder中注册它。注册一个属性编辑器

会将某一类型的属性与指定的属性编辑器联系起来。可以为给定类型的所有属性注册一个

编辑器,也可以为某一特定组件的特定属性注册一个编辑器。 调用 RegisterPropertyEditor函数注册一个属性编辑器。 RegisterPropertyEditor有四个参数: 1.一个被编辑属性的类型信息指针。指定类型信息如下: __typeinfo(TMyComponent)

2.编辑器应用的组件类型。如果该参数是 null,则编辑器应用于给定类型的所有属

性。 3.属性名。只有前一个参数指定了某一特定的组件类型时,该参数才有意义。因此,

你可以指定某一属性的名字。 4.编辑特定属性的编辑器类型。 下面的摘录为组件面板中的标准组件注册编辑器: namespace Newcomp

{ void __fastcall PACKAGE Register()

{

RegisterPropertyEditor(__typeinfo(TComponent), 0L, "",

第五章 控件的设计 第 303 页

__classid(TComponentProperty));

RegisterPropertyEditor(__typeinfo(TComponentName),

__classid(TComponent), "Name",

__classid(TComponentNameProperty));

RegisterPropertyEditor(__typeinfo(TMenuItem), __classid(TMenu), "",

__classid(TMenuItemProperty));

//...

}

}

该函数中的三个语句涵盖了 RegisterPropertyEditor函数的不同用法: n 第一个语句是非常典型的。它为 TComponent 类型(或没有注册自己的编辑器

的 TComponent后代)的所有属性注册了属性编辑器 TComponentProperty。通常,当你注册一个属性编辑器时,你就为某一特定的类型创建了一个编辑器,

并且想把它应用于这个类型的所有属性,因此,第二参数和第三个参数分别是

NULL和空字符串。 n 第二个语句是注册中非常特殊的一种。它为特定类型组件的特定属性注册了一个

编辑器。在这个例子中,是所有 TComponentName 类型组件的 Name属性的编

辑器。 n 第三个语句比第一个语句特殊,但还没有像第二个语句那样受限制。它为 TMenu

类型组件的所有 TMenuItem类型属性注册编辑器。

7.5 添加组件编辑器

组件编辑器是当组件在设计器中被双击时,确定作什么样的事情;而当在组件上单击

右键时,组件编辑器将向弹出的上下文菜单中添加命令。它还可以把你的组件以定制格式

拷贝到剪贴板。 如果你没有给你的组件设计并注册一个编辑器,C++ Builder会使用缺省的组件编

辑器。缺省的组件编辑器由类 TDefaultEditor 实现。TDefaultEditor 并不向组件

的上下文菜单添加新项目。当组件被双击时,TDefaultEditor 搜索组件的属性,生成

(或定位到)它找到的第一个事件处理程序。 为了向上下文菜单添加项目、改变组件被双击时的动作、或添加新的剪贴板格式,从

TComponentEditor 派生一个新类并注册它应用于你的组件。在你重载的方法中,你可

以使用 TComponentEditor的 Component属性访问正在编辑的组件。 添加定制的组件编辑器有以下几个步骤: n 向上下文菜单添加项目。 n 改变双击时的动作。 n 添加剪贴板格式。 n 注册组件编辑器。

第五章 控件的设计 第 304 页

7.5.1 向上下文菜单添加项目

当用户右击组件时,组件编辑器的 GetVerbCount 方法和 GetVerb 方法被调用以

建立上下文菜单。你可以重载这些方法,向上下文菜单添加命令(动词)。 向上下文菜单添加项目需要下面几个步骤: 1.指定菜单项目 重载 GetVerbCount 方法返回你向上下文菜单中添加的命令数目。重载 GetVerb

方法返回向上下文菜单添加的命令字符串。当重载 GetVerb时,在字符串中加入&将导致

紧接着的字符有一个下划线,且该字符成为该选项的快捷键。如果某个命令将弹出一个对

话框,那么在相应字符串后面添加一个省略号(...)。 GetVerb有一个参数,即命令的索引。 下面的代码重载 GetVerbCount和 GetVerb 方法,向上下文菜单中添加两个命令: int __fastcall TMyEditor::GetVerbCount(void)

{ return 2;

}

System::AnsiString __fastcall TMyEditor::GetVerb(int Index)

{ switch (Index)

{

case 0: return "&DoThis ..."; break;

case 1: return "Do&That"; break;

}

}

注意确保你的 GetVerb 方法为 GetVerbCount 指示的索引返回一个值,否则在组

件的弹出菜单中将增加一个空白的菜单项,这并不是组件设计员希望看到的。 2.实现命令 当 GetVerb 提供的命令在设计器中被选择时,ExecuteVerb 方法被调用。在

ExecuteVerb 中实现每个由 GetVerb 提供的命令。你可以提供编辑器的 Component属性访问正被编辑的组件。

例如,下面的 ExecuteVerb方法实现前面例子中的 GetVerb提供的命令: void __fastcall TMyEditor::ExecuteVerb(int Index)

{

switch (Index)

{

第五章 控件的设计 第 305 页

case 0: //执行 DoThis菜单

TMyDialog *MySpecialDialog = new TMyDialog();

MySpecialDialog->Execute();

((TMyComponent *)Component)->ThisProperty =

MySpecialDialog->ReturnValue;

delete MySpecialDialog;

break;

case 1: //执行 DoThat菜单

That(); //调用"That"方法

break;

}

}

7.5.2 改变双击时的行为

当组件被双击时,编辑器的 Edit 方法被调用。缺省情况下,Edit 执行第一个被添

加到上下文菜单中的命令。这样,在前面的例子中,双击组件将执行 DoThis命令。 虽然执行第一个命令通常是个好主意,但你可能想改变这种缺省行为。例如,你可以

提供一个替代动作,如果: n 你没有向上下文菜单中添加任何命令。 n 当组件被双击时,你想显示一个组合了多个命令的对话框。 重载 Edit方法指定一个组件被双击时执行的动作。例如,当用户双击组件时,下面

的 Edit方法弹出一个字体对话框: void __fastcall TMyEditor::Edit(void)

{ TFontDialog *pFontDlg = new TFontDialog(NULL);

pFontDlg->Execute();

((TMyComponent *)Component)->Font = pFontDlg->Font;

delete pFontDlg;

}

注意如果想在双击组件时为事件处理程序弹出代码编辑器,那么应以

TDefaultEditor 作为组件编辑器的基类,而不是 TComponentEditor。这样,要重

载的是 TComponentEditor::EditProperty法而不是 Edit方法。EditProperty方法搜索组件的事件处理程序,提出它找到的第一个事件处理程序。你可以改变这一点,

让其提出某一指定的事件。例如: void __fastcall TMyEditor::EditProperty(TPropertyEditor* PropertyEditor,

bool &Continue, bool &FreeEditor)

第五章 控件的设计 第 306 页

{ if (PropertyEditor->ClassNameIs("TMethodProperty") &&

CompareText(PropertyEditor->GetName, "OnSpecialEvent") == 0)

{

TDefaultEditor::EditProperty(PropertyEditor, Continue, FreeEditor);

}

}

7.5.3 添加剪贴板格式

缺省情况下,当某个组件在 IDE 中被选中后用户选择 Copy 时,组件会以 C++ Builder 的内部格式被拷贝。它能粘贴到另外一个窗体或数据模块中。通过重载 Copy方法,你的组件可以在剪贴板中使用另外的格式。

例如,下面的 Copy方法允许 TImage组件将它的图片拷贝到剪贴板。C++ Builder将忽略这些图片,但它们可以粘贴到另外的应用程序中。

void __fastcall TMyComponentEditor::Copy(void)

{

WORD AFormat;

int AData;

HPALETTE APalette;

((TImage *)Component)->Picture->SaveToClipboardFormat(AFormat,

AData, APalette);

TClipboard *pClip = Clipboard(); // 不要清除剪贴板

pClip->SetAsHandle(AFormat, AData);

}

7.5.4 注册组件编辑器

一旦组件编辑器被定义,它就可以被注册用于某个组件类。当该类的某一组件在设计

器中被选中时,注册过的编辑器将被应用于该组件。 为了在组件编辑器和组件类之间建立链接,调用 RegisterComponentEditor。

RegisterComponentEditor 使用组件的类名与组件编辑器的类名。例如,下面的语句

注册了用于 TMyComponent类组件的 TMyEditor编辑器: RegisterComponentEditor(__classid( TMyComponent), __classid(TMyEditor));

将 RegisterComponentEditor 的调用放在你注册组件用的名字空间中。例如,如

果新组件 TMyComponent和它的编辑器 TMyEditor 均是在 NewComp.cpp文件中实现

的,那么下面的代码(在 NewComp.cpp文件中)注册了组件以及与之关联的编辑器。

第五章 控件的设计 第 307 页

namespace Newcomp

{

void __fastcall PACKAGE Register()

{

TMetaClass classes[1] = {__classid(TMyComponent)};

RegisterComponents("Miscellaneous", classes, 0);

RegisterComponentEditor(classes[0], __classid(TMyEditor));

}

}

7.6 将组件编译成软件包

一旦你的组件已被注册,那么在你将它们安装到 IDE之前,必须将它们编译成软件包。

一个软件包可以有一个或几个组件,如同定制属性编辑器一样。有关软件包的信息以及如

何创建和编译包文件请读者参考联机帮助。将定制组件的源代码单元放到软件包包含列表

(Containlist)中。如果你的组件需要其他的软件包,那么把它们放到需求列表

(Requireslist)中。

7.7 解决定制组件问题

在注册和安装组件时一个常见的问题是,当软件包成功地安装后,组件没有显示在组

件列表中。 最普通的导致组件未能显示在组件列表或组件面板中的原因是: n 在注册函数中没有使用 PACKAGE修饰符。 n 在类中没有使用 PACKAGE修饰符。 n 在 C++源文件中没有使用#pragma package(smart_init)语句。 n 在与源文件同名的名字空间中没有找到注册函数。 n 注册函数没有成功地导出。对.BPL使用 tdump寻找导出函数: tdump -ebpl mypack.bpl mypack.dmp

在转储的导出部分,你应该看到注册函数(在名字空间内部)被导出。

8 小节

本章从应用的角度讲述了如何使用 C++ Builder中语言扩充的内容,以及一些处理

技巧和经验,对于设计组件的读者来说需要和实践结合起来,在应用中参考本章内容,对

于程序的开发者来说,本章部分内容也是需要了解的。 通过这一章的阅读,读者对 C++ Builder 的 VCL 会有一个更深入的了解,这将有

助于后面示范程序的阅读。实际,这一章所讲的内容几乎都是和 VCL 组件技术有关的,

C++ Builder的使用包括很多方面,这里也不能一一介绍,因此 C++ Builder的初学

第五章 控件的设计 第 308 页

者应该结合联机帮助或者其它书籍进一步深入了解。 下一章我们通过讲解几个实际组件设计的例子来运用和掌握本章以及前面章节所介

绍的内容。

第六章 控件设计示例 第 309 页

第六章 控件设计示例

前面几章已经就面向状态的思想以及 C++ Builder中如何使用面向状态的技术作了

一些介绍,通过阅读前面内容,相信读者已经对 C++ Builder中提供的这种新的编程技

术有所了解。本章将根据前面的内容有针对性的提供一些控件设计的示例,其目的在于示

范如何应用前面所讲的内容,而不在于应用这些控件,因此本章所给的控件都是一些非常

简单,几乎没有使用价值的示范,但通过这些控件的学习可以是读者掌握如何创建控件以

及更好的使用 C++ Builder的这些新的特征。

1 组合控件 TPickEdit

所谓组合控件是指有多个控件经过组合形成一个新的控件来完成一些特定的功能,实

际上 VCL的大多数控件都是由多个控件或组件组合而成的,下面给出的 TPickEdit就是

一个由 TEdit和 TSpeedButton组合而成的控件。 这是一个非常简单的控件,最初是一位网友问起如何实现类似 MS Office的 Excel

中数据选择功能。Excel的数据选择如图 6.1所示:

图 6.1 Excel 中数据拾取窗体

第六章 控件设计示例 第 310 页

“数据区域”输入框是一个带有一个 Button的 Edit输入框,当鼠标点击了右边的

Button时,整个窗口会缩小成下图的形状,再次点击 Button时,窗体会复原。

熟悉 C++ Builder 的读者都知道,要实现这样的功能非常简单,利用 Form 的

AutoSize,控件的 Visible、Align、Top、Left、Height 和 Width等属性按照特

定的顺序设置,同时在程序中需要保存原始的控件位置就可以完成。 但我们知道我们在 Excel 中这样的数据选择框至少有十多处,而且每一个窗体中数

据选择框的数量、位置等都不一样,因此有多处使用到这个功能时,将会出现大量的重复

代码。作一个非常简单的控件来替代重复的代码设计,是一个不错的选择。不仅如此,使

用控件会让代码修改和维护也变得非常简单,使程序的结构也变得更加清晰。我们来看如

何实现。

1.1 控件的分析

要实现这样的功能,最主要的就是这个带有 Button的 Edit选择框,我们将其命名

为 TPickEdit,含义是用于拾取数据的,我们来分析如何实现。 n 从表面上看,这个 TPickEdit 在 Button 接受鼠标的 Ckick事件后缩小,再

次点击时复原,但实际上是反映了 TPickEdit的两个状态:缩小化和复原状态。

这个状态可以使用 bool类型属性 Mined 来描述,Mined为 true时,父窗体

Form为缩小化,当 Mined为 false时,父窗体 Form复原。 n 除了点击 Button 的按钮可以使父窗体缩小和复原之外,允许其它条件改变

Mined属性的状态,比如用户拾取数据后按“回车”键等,因此 Mined属性应

该是发布的属性,是 TPickEdit的一个公共接口。 n 除了 Edit本身的属性以外,TPickEdit包含的 SpeedButton 应该允许用户

指定部分属性,如 OnClick、Graphy、Cusor 等,但 Button 的大部分属性

是不允许用户改变,比如 Left、Height、Top 等,这些属性是 TPickEdit根

据自身的状态来调整的,因此,不应该将 Button整体作为 TSpeedButton 类

型属性出现,而是将其中部分属性映射到 TPickEdit的属性中。 n 除了上述特征之外,TPickEdit 还需要解决一些辅助性的功能,比如 Edit 框

中字符显示的范围等。 正象前面所说的那样,这个控件其实并没有什么新的东西,只是将一些其它控件的某

些状态联系在一起,共同构成一个新的属性 Mined。我们来看一下这个 Mined属性和其

它控件的属性具有怎么样的约束关系: n Mined属性对自身的影响并不多,更多的是和父窗体以及父窗体上的其它控件发

生联系,这种关系是依靠 Parent属性来建立的。 n Mined为 true时代表父窗体将缩小化,这个状态的变化是由 TPickEdit造成

的,因此 TPickEdit必需有义务能够将原始状态记录下来,并且能够完全还原。

图 6.2 Excel 中数据拾取窗体缩小状态

第六章 控件设计示例 第 311 页

n 当 Mined为 true时,TPickEdit的宽度和父窗体的 ClientRect 的宽度相

等,高度不变,父窗体的 ClientRect 的高度和 TPickEdit 的高度相等,其

它控件的 Visible 均为 false。这个过程的实现可以利用 Align 属性,

AutoSize属性,Left、Top、Witdh和 Height属性来实现,实际上就是和

其它状态之间的关系。

1.2 源代码

接下来是整个 TPickEdit的源代码,在本书所附的光盘中有全部内容,读者可以参

考前面相关内容阅读并理解。

1.2.1 声明部分

PickEdit.h

#ifndef PickEditH

#define PickEditH

//-------------------------------------------------------------

#include <Buttons.hpp>

#include <Menus.hpp>

#include <Graphics.hpp>

#include <Forms.hpp>

#include <SysUtils.hpp>

#include <Messages.hpp>

#include <Controls.hpp>

#include <ExtCtrls.hpp>

#include <StdCtrls.hpp>

#include <Classes.hpp>

#include <Windows.hpp>

#include <System.hpp>

//------------------------------------------------------------- class PACKAGE TPickEdit : public TCustomEdit

{

private:

int FOrgX; //原始位置、大小属性

int FOrgY;

int FOrgWidth;

int FOrgHeight;

int FOrgParentHeight; //父窗体高度

bool FMined; //是否缩小化

第六章 控件设计示例 第 312 页

protected:

TSpeedButton * Button; //添加一个按钮

MESSAGE void __fastcall WMSize(TWMSize &Message);

//新的事件响应函数

MESSAGE void __fastcall CMEnter(TWMNoParams &Message); //……

void __fastcall BtnMouseDown(TObject *Sender, TMouseButton Button,

TShiftState Shift, int X, int Y );

//响应 Button 的 MouseDown事件

TNotifyEvent __fastcall GetButtonClick(); // A

void __fastcall SetButtonClick( TNotifyEvent Value );// A

TCursor __fastcall GetCursor(); // B

void __fastcall SetCursor( TCursor Value ); // B

Graphics::TBitmap* __fastcall GetGlyph(); // C

void __fastcall SetGlyph( Graphics::TBitmap* Value ); // C

/**************************************************************

A、B、C 分别将 TSpeedButton 的 OnClick 事件、Cursor属性和

Glyph 属性映射传递给 新的控件 TPickEdit。

/**************************************************************/

void __fastcall SetMined(bool Value); //设置 Mined属性的函数

void __fastcall SetButtonPosition(); //界面控制用

void __fastcall BtnClick(TObject *Sender);

//响应 Button 的 OnClick事件

void __fastcall CreateParams(TCreateParams &Params);

//用于窗体建立时传递参数,

void __fastcall SetEditBounder(void);//控制 Edit 中字符有效的显示范围

void __fastcall CreateWnd(void); //重载 该函数

virtual void __fastcall SetEnabled(bool Value);

//下面建立 Windows 消息影射表

BEGIN_MESSAGE_MAP

VCL_MESSAGE_HANDLER(WM_SIZE, TWMSize, WMSize);

VCL_MESSAGE_HANDLER(CM_ENTER, TWMNoParams, CMEnter);

END_MESSAGE_MAP(TCustomEdit);

public:

TForm *ParaForm; //父窗体句柄

TList *Other;

__fastcall TPickEdit(TComponent* Owner);

__published:

__property TCursor ButtonCur = {read = GetCursor,write = SetCursor };

//将 TSpeedButton 的 Cursor 属性传递给 PickEdit,

第六章 控件设计示例 第 313 页

__property TNotifyEvent OnButtonClick = {read = GetButtonClick,

write = SetButtonClick};

// 同上,事件传递

__property Graphics::TBitmap* ButtonGlyph =

{read=GetGlyph,write=SetGlyph};

//同上

__property bool Mined = {read = FMined,write = SetMined};

//建立并发布 Mined 属性。

__property Align; // 以下继承 发布 父类部分属性和事件

__property AutoSize;

__property Font;

__property Color;

__property Hint;

__property Enabled;

__property Text;

__property OnClick;

__property OnMouseDown;

__property OnMouseMove;

__property OnDblClick;

__property OnKeyDown;

__property OnKeyPress;

};

#endif

1.2.2 实现部分

.CPP文件中实现部分

#include <vcl.h>

#pragma hdrstop

#include "PickEdit.h"

#pragma package(smart_init)

//-------------------------------------------------------------

static inline void ValidCtrCheck(TPickEdit *)

{ new TPickEdit(NULL);

}

//-------------------------------------------------------------

__fastcall TPickEdit::TPickEdit(TComponent* Owner)

第六章 控件设计示例 第 314 页

: TCustomEdit(Owner) { //构造函数,创建 Button并初始化

ParaForm = (TForm *)Owner;

Button = new TSpeedButton( (TComponent *) this );

SetButtonPosition();

Button->Glyph->Handle = LoadBitmap(HInstance, "SpinDown");

Button->OnMouseDown = BtnMouseDown;

Button->OnClick = BtnClick;

Button->Cursor = crHandPoint;

Button->Parent = this;

} //-------------------------------------------------------------

namespace Pickedit

{ //注册控件

void __fastcall PACKAGE Register()

{ TComponentClass classes[1] = {__classid(TPickEdit)};

RegisterComponents("MyInterFace", classes, 0);

}

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetEditBounder(void)

{ //用于调整 TPickEdit的字符显示区域

TRect Loc;

SendMessage(Handle, EM_GETRECT, 0, long(&Loc)); Loc.Bottom = ClientHeight + 1; //这是 Windows的一个绘图 Bug,需要 + 1

Loc.Right = ClientWidth - Button->Width;

Loc.Top = 0;

Loc.Left = 0;

SendMessage(Handle, EM_SETRECTNP , 0, long(&Loc));

}

//-------------------------------------------------------------

void __fastcall TPickEdit::CreateWnd()

{ //重载 CreateWnd() 函数

TCustomEdit::CreateWnd();

SetEditBounder();

}

第六章 控件设计示例 第 315 页

//-------------------------------------------------------------

void __fastcall TPickEdit::CMEnter(TWMNoParams &Message)

{ //CM_ENTER消息的响应函数

if (AutoSelect && !(ControlState.Contains(csLButtonDown)))

SelectAll();

}

//-------------------------------------------------------------

void __fastcall TPickEdit::WMSize(TWMSize &Message)

{ //WM_SIZE消息的响应函数

SetButtonPosition(); //重新 Button 的位置、大小

SetEditBounder(); //以及 Edit框的字符显示宽度

return;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::CreateParams(TCreateParams &Params)

{ //创建窗体的参数;设置控件创建时的参数,至少应该设置

TCustomEdit::CreateParams(Params); // ES_MULTILINE,否则,

Params.Style &= ~WS_BORDER; //无法限制 Edit框中字符

Params.Style |= ES_MULTILINE | WS_CLIPCHILDREN; //的显示宽度

Params.Height = 21;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::BtnMouseDown(TObject *Sender,

TMouseButton Button,TShiftState Shift, int X, int Y)

{

//该函数预留为空

} //-------------------------------------------------------------

void __fastcall TPickEdit::BtnClick(TObject *Sender)

{

Mined = !Mined;

}

//-------------------------------------------------------------

第六章 控件设计示例 第 316 页

TCursor __fastcall TPickEdit::GetCursor()

{

return Button->Cursor;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetCursor( TCursor Value )

{

Button->Cursor = Value;

}

//-------------------------------------------------------------

TNotifyEvent __fastcall TPickEdit::GetButtonClick()

{

return Button->OnClick;

} //-------------------------------------------------------------

void __fastcall TPickEdit::SetButtonClick( TNotifyEvent Value )

{

Button->OnClick = Value;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetMined(bool Value)

{ //Mined属性的设置函数

int I;

if(!Other)

Other = new TList;

I = ParaForm->ControlCount; //获得父窗体控件的数量

Other->Count = I; if( FMined && !Value ) //已经缩小化的情况

{

Align = alNone; //恢复对齐方式

ParaForm->AutoSize = false; //恢复父窗体属性

Parent->Height = FOrgParentHeight; //同上

Width = FOrgWidth; //恢复 位置属性

Height = FOrgHeight; //同上

Top = FOrgY; //同上

第六章 控件设计示例 第 317 页

Left = FOrgX; //同上

FMined = Value; //保存状态

for( ;I > 0; I-- )

{ //恢复父窗体其他控件的可视性

ParaForm->Controls[I - 1]->Visible = bool(Other->Items[I - 1]);

}

}

else if( Value ) // 反之

{

for( ;I > 0; I-- )

{ //隐藏父窗体所有控件

Other->Items[I - ] = (void*)(ParaForm->Controls[I - ]->Visible);

ParaForm->Controls[I - ]->Visible = false;

}

Visible = true; //显示自身

FOrgParentHeight = Parent->Height ; //备份父窗体高度属性

FOrgWidth = Width; //备份自身位置属性

FOrgHeight = Height; //同上

FOrgY = Top; //同上

FOrgX = Left; //同上

Align = alTop; //向上靠近缩紧

FMined = Value; //保存状态

ParaForm->AutoSize = true; //设置父窗体自动调整大小

}

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetButtonPosition()

{ //当控件大小改变时正确设置 Button 的位置

// this->Height = 21; 用于限制 TPickEdit的高度为 21,这里没有限制

Button->Top = 0;

Button->Width = 21;

Button->Left = this->Width - Button->Width - 2; //留了边框的距离

Button->Height = this->Height - 2; //留了边框的距离

}

//-------------------------------------------------------------

Graphics::TBitmap* __fastcall TPickEdit::GetGlyph()

{

第六章 控件设计示例 第 318 页

return Button->Glyph;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetGlyph( Graphics::TBitmap* Value )

{

Button->Glyph = Value;

}

//-------------------------------------------------------------

void __fastcall TPickEdit::SetEnabled(bool Value)

{ //重载 SetEnabled方法,将 Enabled属性传递到 Button中

TCustomEdit::SetEnabled(Value);

if( Button )

Button->Enabled = Value;

}

1.3 控件的详解

在这个例子中,大部分函数不需要作解释, ValidCtrCheck Register方法是控件必需的,而且是由 IDE自动生成的; GetGlyph、SetGlyph、GetCursor、SetCursor、GetButtonClick、SetButtonClick

等都是属性的读取函数,也非常简单; SetEnabled是重载了父类的 SetEnabled函数,目的在于能将 Enabled属性传递到

Button中; SetButtonPosition 是用于调整 Button 的位置及大小的方法,使 Button 在

TPickEdit的大小改变时能够及时适应,因此在 TPickEdit 中必需有 WM_SIZE 消息的

响应函数 WMSize;

BtnMouseDown、BtnClick都是为 Button 的事件而创建的方法,其中 BtnMouseDown

预留,BtnClick为 SpeedButton的 OnCkick处理函数;

TPickEdit 构造过程需要解决以下几个问题,创建 Edit 中的 Button 按钮,设置

OnClick事件句柄、图片、光标等属性,并且设置 Parent 属性。TPickEdit 并没有显

式的声明析构函数,也就是并没有显式的释放 Button,这并不是一个 Bug,读者可能注

意到了,在构造函数中创建TSpeedButton的时候,指定了this作为 Button的Owner,前面讲过 Owner 的作用就是负责释放自己所拥有的组件,因此当 TPickEdit 的缺省析

构函数被调用时,Button会自动被释放。在后面的例子 TDataPick中有不同的情况,

请读者注意。

SetEditBounder并不是一个属性的写函数,而是用于设置 Edit中字符显示范围的函

数。要设置字符的显示范围需要一个条件:即该 Edit类必需是 ES_MULTILINE风格,TEdit并非如此,属于 ES_SIGLELINE风格,而且 TEdit已经将大部分父类继承来的属性发布

第六章 控件设计示例 第 319 页

了,而这些属性并不一定是我们 TPickEdit需要发布的,因此 TPickEdit没有从 TEdit派生,而是从 TCustomEdit直接派生而来,同时还需要重载 CreateParams方法来改变

窗体类的风格。另外,和大多数 TWinControl派生类一样,TPickEdit重载了 CreateWnd

方法,其目的只有一个,就是负责在创建 TPickEdit窗体时改变字符的显示范围,尽管

在 WM_SIZE 消息的响应函数中有了显示范围设置的功能,但是并不能将这个任务完全寄

希望于用户设计时改变缺省的大小,应该在 PickEdit窗体创建的时候就进行字符显示范

围的设置。

CMEnter是 CM_ENTER 消息的响应函数,这是在 TPickEdit 获得焦点的时候自动的

选择了全部字符,但不包含使用鼠标点击而获得焦点。 SetMined方法是 TPickEdit的一个重要设置方法,它分为两个部分,一是设置到缩

小化状态,另一种是复原状态。在 TPickEdit 中使用了 TList 类来保存每一个父窗体

中 Child 控件的 Visible 属性(并不一定是所有的控件都是运行时可见,因此需要对

Visible逐一保存),父窗体的 Height、自身的原始位置大小等,由 TPickEdit的私

有成员保存,父窗体的 Width由 TPickEdit 的新 Width保存 TPickEdit 的 Align属

性为 alTop时,父窗体的 Width不会受到父窗体 AutoSize 属性改变的影响,但这必需

保证Parent的AutoSize改变为 true之前,TPickEdit的Align必需已经是alTop,相反的 TPickEdit的 Align属性恢复 alNone之前,必需将 Parent的 AutoSize 恢

复为 false; 至此 TPickEdit已经是一个可以使用和发布的控件了,但是还有一个小小的问题:

我们在 Button 的 OnClick 事件中改变了 Mined 属性的状态,同时又将 Button 的

OnClick事件映射到了 TPickEdit的 OnButtonClick事件中,这种直接映射会导致

用户定义了 OnButtonClick事件的处理句柄时,我们在控件内部定义的 BtnClick方

法将不会得到执行,也即失去了点击 Button而改变 Mined状态的能力,尽管用户仍然

可以在 OnButtonClick 事件的处理句柄中改变 Mined的状态,但这毕竟不太妥当,我

们对这个 Bug作一个修复: n 保留用户可以自定义 OnButtonClick事件。 n 用户定义了 OnButtonClick事件后,点击 Button仍然可以改变 Mined属性

的状态。 n 在用户定义 OnButtonClick事件的处理句柄中,允许用户屏蔽上面的功能。 我们看看该如何来作。 n 首先是不能再采用直接映射的方式,创建 OnButtonClick 事件,需要去掉

OnButtonClick 的读取方法 GetButtonClick,建立一个 FOnButtonClick私有数据用来存储用户的 OnButtonClick句柄。

n 在 BtnClick方法中调用 FOnButtonClick句柄。 n 为了能够让用户屏蔽掉点击 Button 改变 Mined属性的功能,需要创建一个新

的 closure类型 TNotifyHandledEvent,该 closure可以向用户的句柄传

递一个 bool类型的引用参数 Handled,BtnClick根据 Handled 参数的值来

判断是否进行 Mined属性的设置。 完成以上功能,代码需要做如下改动:

第六章 控件设计示例 第 320 页

定义新的事件类型: typedef void __fastcall (__closure *TNotifyHandledEvent)( System::TObject*

Sender,bool &Handled);

在类的声明部分增加 FOnButtonClick私有成员: class PACKAGE TPickEdit : public TCustomEdit

{

private:

//⋯⋯⋯⋯

TNotifyHandledEvent FOnButtonClick;

} 修改 OnButtonClick属性为: __property TNotifyHandledEvent OnButtonClick = {read = FOnButtonClick,

write = SetButtonClick};

修改 SetButtonClick方法为:

void __fastcall TPickEdit::SetButtonClick( TNotifyHandledEvent Value )

{

FOnButtonClick = Value;

}

修改 BtnClick方法为:

void __fastcall TPickEdit::BtnClick(TObject *Sender)

{ bool Handled = false;

if( FOnButtonClick )

FOnButtonClick( Sender,Handled );

if( !Handled )

Mined = !Mined;

}

删去 GetButtonClick方法; 从这里我们可以看出,对于 C++ Builder而言,修改和扩充一段代码的内容是非常

第六章 控件设计示例 第 321 页

方便的,这也是面向状态的一个最大的优点。 在这一节中,我们通过了对 TPickEdit控件的学习,已经了解了属性的创建、属性

的发布;事件的创建、事件的触发、自定义事件;允许用户屏蔽控件的却省动作;窗体控

件基本设置;对继承属性的发布;通过消息映射表对 Windows 消息的响应;通过重载父

类虚拟函数对父类已经处理的 Windows 消息的响应;子控件属性和事件向父控件属性和

事件的映射等内容。在下一节将要介绍的是关于图形应用的控件设计。

2 图形控件 TDataPick

这一节将要介绍的控件 TDataPick是一个用于图形显示方面的控件,在这个控件里,

我们采用了从 TCustomControl派生,TCustomControl是从 TWinControl派生的

窗体控件,具有窗口句柄,能够接收 Windows的键盘消息,并且提供了 Canvas属性和

Paint方法,允许用户对其画布进行绘图方面的处理,下图是 TDataPick 控件运行后的

屏幕拷贝。 这个形状可能读者都不会陌生,在很多优秀的软件中都有类似的应用,比如 Adobe

的 PhotoShop中就有很多关于曲线数据选择,和 TDataPick控件比较相似,还有著名

的声音处理软件 CoolEdit也是多次使用了类似的控件,诸如此类的应用还非常的多。 很多读者可能都知道,利用 C++ Builder来实现这样的功能并不复杂,但是在一个

程序或者还希望在其它的程序中使用,则编制成一个控件使用是非常不错的。我们来看如

何在 C++ Builder中如何制作这样一个控件。

2.1 控件的分析

这个控件的主要用途是将用户提供的一些属性控制点(图中的黑色园点),经过某种

数学的拟合运算,转换成相应的曲线数据,并将拟合后的数据反映到绘图界面上。 在 TDataPick中,属性控制点记录着用户提供的原始点位信息,并将属性控制点显

示在绘图界面上,使用绘图函数根据 TDataPick的属性控制点的数据将其显示出来就可

图 6.3 TDataPick控件运行后的图形

第六章 控件设计示例 第 322 页

以达到希望的效果,这是常规的 SKD编程中经常使用的方法,但从这个控件的特点来分析,

并不合理,因为属性控制点除了纪录着 TDataPick的点位数据之外,还有一些自己的特

性,比如用户希望鼠标在经过点位的时候显示不同的 Cursor;或者用户希望可以通过鼠

标的操作,来拖动属性控制点改变位置,使用 TDataPick的鼠标消息,根据属性控制点

的数据判断也可以完成这样的功能,但是从其结构上看,属性控制点显然表达为一个新的

对象更为合理一些,这个对象恰好可以利用从 TGraphicControl 派生的图形控件

TShape控件。因此,在 TDataPick控件中包含了一个新的属性类型:TPointData。 在 TDataPick控件中,Point(TPointData 的实例)的数量并不是固定的,而是

根据用户的需要增加或者删除,因此在 TDataPick 中必须考虑对 Point 的增加、删除

以及控制等能力。 这是对 TDataPick控件的简单的分析,是该控件的基本功能。除此之外,TDataPick

还需要考虑的是其它一些可能需要允许用户进行控制或者修改的特征,比如,曲线的颜色、

背景的颜色、属性控制点的颜色,属性控制点是否显示被选中、是否显示网格、网格的颜

色等等用以适合不同用途的特征。 在我们这个例子中,将完成以下的功能: n 允许用户指定曲线、属性控制点、网格、背景、选中后的属性控制点等特征的颜

色; n 允许用户指定是否绘制网格,允许用户指定是否显示属性控制点; n 允许用户改变网格的间距; n 允许用户指定属性控制点的鼠标 Cursor n 允许用户点击鼠标左键,在鼠标位置创建新的 Point; n 允许用户用右键点击属性控制点将其删除; n 允许用户使用鼠标拖动属性控制点改变其位置; n 允许用户在设计时使用鼠标控制起始点和终点的位置;

2.2 源代码及注解

2.2.1 声明部分

以下是在 DataPick.h中的声明部分: //------------------------------------------------------------------

#ifndef DataPickH

#define DataPickH

//------------------------------------------------------------------

#include <SysUtils.hpp>

#include <Controls.hpp>

#include <Classes.hpp>

#include <Forms.hpp>

class PACKAGE TPointData : public TShape

第六章 控件设计示例 第 323 页

{ private:

int x ; //纪录鼠标按下时的位置坐标

int y ;

bool IsMouseDown; //纪录鼠标是否被按下

bool FSelected; //Selected属性的存储变量

protected: //Selected的设置函数

void __fastcall SetSelected(bool Value);

public: //声明 Selected属性

__property bool Selected = {read=FSelected,write=SetSelected};

__fastcall TPointData( TComponent* Owner ); //构造函数

__fastcall TPointData::TPointData( TComponent* Owner,int X,int Y );

virtual void __fastcall Paint(void); //重载绘图函数 Paint

//下面三个函数为重载 TControl的动态函数来实现鼠标消息响应的

DYNAMIC void __fastcall MouseMove(TShiftState Shift,

int X, int Y);

DYNAMIC void __fastcall MouseDown(TMouseButton Button,

TShiftState Shift, int X, int Y);

DYNAMIC void __fastcall MouseUp(TMouseButton Button,

TShiftState Shift, int X, int Y);

} ; //------------------------------------------------------------------

class PACKAGE TDataPick : public TCustomControl

{

private:

bool FShowPoint; //下面声明了属性的内部存储变量

TColor FForeColor;

TColor FPointColor;

TColor FGridColor;

TColor FSelectColor; bool FGrided;

TCursor FPointCur;

unsigned short FGridInterval;

//供内部使用的 TPointData类型的数据变量

TPointData *Point; protected: //以下时属性的设置函数

void __fastcall FSetFGridInterval(unsigned short Value);

void __fastcall FSetShowPoint(bool Value);

第六章 控件设计示例 第 324 页

void __fastcall FSetForeColor(TColor Value);

void __fastcall FSetPointColor(TColor Value);

void __fastcall FSetGridColor(TColor Value);

void __fastcall FSetGrided(bool Value);

void __fastcall FSetPointCur(TCursor Value);

void __fastcall FSetSelectColor(TColor Value);

int __fastcall FGetPoint(int Index); //带有索引的读取函数

void __fastcall FSetPoint(int Index,int Value);//带有索引的设置函数

public:

int DataCount;

TPointData * FStartPoint; //起始点位的内部存储变量

TPointData * FEndPoint; //终点的内部存储变量

TPointData * Selection; //纪录当前被选择中的 Point

double * DataX; //用于拟合的数据数组(动态分配内存)

double * DataY; //用于拟合的数据数组(动态分配内存)

int LeftLimit; //纪录被选中 Point移动位置的左边界

int RightLimit; //纪录被选中 Point移动位置的右边界

void __fastcall WMMouse(TWMMouse Msg); //鼠标的响应函数

__fastcall TDataPick(TComponent* Owner);

__fastcall ~TDataPick(void );

virtual void __fastcall Paint(void); //重载的 Paint函数

__published:

__property OnEnter; //继承发布父类的属性

__property OnExit;

__property OnKeyDown;

__property OnKeyPress;

__property OnKeyUp;

__property Color; //以下是自定义的属性

__property int StartPointX = {read=FGetPoint,write=

FSetPoint,index=1};

__property int StartPointY = {read=FGetPoint,write=

FSetPoint,index=2};

__property int EndPointX = {read=FGetPoint,write=

FSetPoint,index=3};

__property int EndPointY = {read=FGetPoint,write=

FSetPoint,index=4}; //以上四个属性记录了起始点和终点的位置坐标,以便设计时指定的位置可以

//被存储在资源中,最终反映在运行时

__property TCursor PointCur = {read=FPointCur,write=FSetPointCur};

第六章 控件设计示例 第 325 页

__property unsigned short GridInterval = {read = FGridInterval,write=

FSetFGridInterval,default=10};

__property bool ShowPoint = {read = FShowPoint,write =

FSetShowPoint,default=true};

__property TColor ForeColor = {read = FForeColor,write =

FSetForeColor,default=clBlack}; __property TColor PointColor = {read = FPointColor,write =

FSetPointColor,default=clBlack};

__property TColor SelectColor = {read = FSelectColor,write =

FSetSelectColor,default=clYellow};

__property TColor GridColor = {read = FGridColor,write =

FSetGridColor,default=clSilver};

__property bool Grided = {read = FGrided,write =

FSetGrided,default=false};

BEGIN_MESSAGE_MAP //对于鼠标消息采用了消息映射表的方式实现响应

VCL_MESSAGE_HANDLER(WM_LBUTTONDOWN, TWMMouse, WMMouse);

END_MESSAGE_MAP(TCustomControl);

};

//------------------------------------------------------------------

#endif

2.2.2 实现部分代码

DataPick.cpp文件的全部代码及部分注解: #include <vcl.h>

#pragma hdrstop

#include "DataPick.h"

#pragma package(smart_init)

double Enlgr(double DataX[],double DataY[],int DataCount,double ResultX)

{ //用于数据拟合的数学处理函数,参考相关 C语言算法的书籍

int i,j,k,m; //DataX为采样点 X轴坐标,DataY为 Y轴坐标

double Result,s; //DataCount为采样点的数据数量

Result = 0; //ResultX为需要插值的 X轴坐标

if( DataCount < 1 ) //返回值为插值

return(Result);

if( DataCount == 1 )

{

第六章 控件设计示例 第 326 页

Result = DataY[0]; return(Result);

}

if( DataCount == 2 )

{

Result=(DataY[0]*(ResultX-DataX[1])-DataY[1]*(ResultX-DataX[0]))/

(DataX[0]-DataX[1]);

return(Result);

}

i = 0;

while((DataX[i] < ResultX) && (i < DataCount))

i = i + 1;

k = i - 4;

if (k < 0)

k = 0;

m = i + 3; if ( m > DataCount - 1 )

m = DataCount - 1;

for( i = k;i <= m; i++ )

{

s=1.0; for(j = k;j <= m; j++)

if(j != i)

s = s * (ResultX - DataX[j]) / (DataX[i] - DataX[j]);

Result=Result+s*DataY[i];

} return Result;

}

//------------------------------------------------------------------

static inline void ValidCtrCheck(TDataPick *)

{

new TDataPick(NULL);

}

//------------------------------------------------------------------

__fastcall TDataPick::TDataPick(TComponent* Owner)

: TCustomControl(Owner)

{ //TDataPick的构造函数

第六章 控件设计示例 第 327 页

Color = clWhite; //创建并初始化起始点和终点

Width = 185;

Height = 105;

LeftLimit = 0;

RightLimit = Width;

FShowPoint = true;

FStartPoint = new TPointData(NULL,1,Height/2);

//TPointData的 Owner为空将使 TPointData在设计时能够独立响应鼠标消息

FStartPoint->Parent = this; //但是必须由析构函数负责释放

FEndPoint = new TPointData(NULL,Width,Height/2);

FEndPoint->Parent = this;

FGridInterval = 10;

FGridColor = clSilver;

FForeColor = clBlack;

FPointColor = clBlack;

FPointCur = crHandPoint;

}

//------------------------------------------------------------------

__fastcall TDataPick::~TDataPick( )

{ delete FStartPoint;

delete FEndPoint;

//TDataPick的析构函数,负责清除起始点和终点

}

//------------------------------------------------------------------

namespace Datapick

{

void __fastcall PACKAGE Register()

{ TComponentClass classes[1] = {__classid(TDataPick)};

RegisterComponents("MyInterFace", classes, 0);

}

}

//------------------------------------------------------------------

void __fastcall TPointData::SetSelected(bool Value)

{ //TPointData的 Selected属性设置函数

第六章 控件设计示例 第 328 页

if( FSelected != Value )

{

if( Value )

{

if(((TDataPick *)Parent)->Selection )

((TDataPick *)Parent)->Selection->Selected = false;

((TDataPick *)Parent)->Selection = this;

}

FSelected = Value; //设置自身的同时必须更新 TDataPick的 Selection属性

Paint();

}

}

//------------------------------------------------------------------

void __fastcall TPointData::MouseMove(TShiftState Shift,

int X, int Y)

{ //TPointData的鼠标移动消息的响应函数

if( IsMouseDown )

{ //根据鼠标是否被点下来判断是否移动属性控制点

this->Left = this->Left + X - x;

this->Top = this->Top + Y - y;

//判断是否超出边界限定,

if( Left <= ((TDataPick*)Parent)->LeftLimit )

Left = ((TDataPick*)Parent)->LeftLimit + 1;

if( Left >= ((TDataPick*)Parent)->RightLimit )

Left = ((TDataPick*)Parent)->RightLimit - 1;

((TDataPick*)Parent)->Paint();

}

}

//------------------------------------------------------------------

void __fastcall TPointData::MouseDown(TMouseButton Button,

TShiftState Shift, int X, int Y)

{

//TPointData的鼠标按下的响应函数

((TDataPick*)Parent)->RightLimit = ((TDataPick*)Parent)->Width;

((TDataPick*)Parent)->LeftLimit = 0;

if( Button == mbRight )

{ //右键则删除该属性点

第六章 控件设计示例 第 329 页

TDataPick * Temp = (TDataPick*)Parent; //纪录 Parent

if(this != Temp->FStartPoint && this != Temp->FEndPoint)

{

delete this; //销毁自身

Temp->Paint(); //更新 Parent窗体

}

}

else

{ //若是左键按下则增加新的属性控制点位

IsMouseDown = true; //纪录鼠标是否被按下

x = X; y = Y; //纪录鼠标按下的位置

for(int i = 0; i < Parent->ControlCount; i++)

{ //对鼠标 Point移动的 X轴做出限制

if( Parent->Controls[i]->Left < Left )

{ if( Parent->Controls[i]->Left >

((TDataPick*)Parent)->LeftLimit)

((TDataPick*)Parent)->LeftLimit =

Parent->Controls[i]->Left;

} else if( Parent->Controls[i]->Left > Left )

{

if( Parent->Controls[i]->Left <

((TDataPick*)Parent)->RightLimit)

((TDataPick*)Parent)->RightLimit =

Parent->Controls[i]->Left;

} //更改被选中的 Point

}

Selected = true;

}

}

//------------------------------------------------------------------

void __fastcall TPointData::MouseUp(TMouseButton Button,

TShiftState Shift, int X, int Y)

{

IsMouseDown = false; //鼠标按键释放消息的响应函数

((TDataPick*)Parent)->Paint();

第六章 控件设计示例 第 330 页

}

//------------------------------------------------------------------

__fastcall TPointData::TPointData( TComponent* Owner )

: TShape(Owner)

{ //TPointData的构造函数

Width = 5;

Height = 5;

IsMouseDown = false;

}

//------------------------------------------------------------------

__fastcall TPointData::TPointData( TComponent* Owner,int X,int Y )

:TShape(Owner)

{ //TPointData带有相关信息的构造函数

Width = 5;

Height = 5;

x = 2;

y = 2;

Left = X - 2;

Top = Y - 2;

Cursor = crHandPoint;

IsMouseDown = false;

}

//------------------------------------------------------------------

void __fastcall TPointData::Paint(void)

{ //TPointData的绘图函数,用于显示 Point,重载父类的函数

if( FSelected )

{

Shape = stRectangle;

Pen->Color = ((TDataPick *)Parent)->SelectColor;

}

else

{

Pen->Color = ((TDataPick *)Parent)->PointColor;

Shape = stCircle;

}

Brush->Color = ((TDataPick *)Parent)->PointColor;

第六章 控件设计示例 第 331 页

TShape::Paint(); //调用父类的绘图函数

}

//------------------------------------------------------------------

void __fastcall TDataPick::WMMouse(TWMMouse Msg)

{ //TDataPick的鼠标按下消息的响应函数,通过消息映射表实现

if( Msg.Keys == MK_LBUTTON )

{ //左键则创建新的 TPointData实例

Point = new TPointData(this,Msg.XPos,Msg.YPos);

Point->Parent = this;

Point->Cursor = FPointCur;

Point->Brush->Color = PointColor;

Point->Visible = ShowPoint;

Point->Selected = true;

if( Selection )

Selection->Selected = false;

Selection = Point;

Paint();

}

}

//------------------------------------------------------------------

void __fastcall TDataPick::Paint(void)

{ //TDataPick的绘图函数,重载父类函数

int i; //临时变量

int TPos; //临时变量

int Pos; //临时变量

Graphics::TBitmap * BmpTemp = new Graphics::TBitmap;

BmpTemp->Width = Width; //使用幕后位图进行绘图

BmpTemp->Height = Height;

BmpTemp->Canvas->Brush->Color = Color; BmpTemp->Canvas->FillRect(ClientRect); //填充背景

if(FGrided)

{ //绘制网格的代码

BmpTemp->Canvas->Pen->Color = FGridColor;

for(i = Height;i > 0 ;i -= FGridInterval)

{ //绘制水平网格

BmpTemp->Canvas->MoveTo(0,i);

BmpTemp->Canvas->LineTo(Width,i);

第六章 控件设计示例 第 332 页

} for(i = Width;i > 0 ;i -= FGridInterval)

{ //绘制竖直网格

BmpTemp->Canvas->MoveTo(i,0);

BmpTemp->Canvas->LineTo(i,Height);

} } //以下是将屏幕显示的 TPointData的坐标转换成为用于拟合插值的数据

DataCount = ControlCount;

DataX = new double[DataCount];

DataY = new double[DataCount];

for(i = 0; i< DataCount;i++)

{

if( i >= 0 || i == DataCount - 1)

{ //TPointData的大小为 5,属性点的中心向右、下方各偏移两个像素

DataX[i] = Controls[i]->Left + 2;

DataY[i] = Controls[i]->Top + 2;

}

}

double Temp; //以下部分是排序,按照 X的升序排列

for( Pos = 0; Pos< DataCount - 1 ; Pos++)

{

TPos = Pos;

for( i = Pos + 1; i < DataCount; i++)

{

if( DataX[i] < DataX[TPos] )

TPos = i; } //交换数据

Temp = DataX[ TPos ];

DataX[ TPos ] = DataX[ Pos ];

DataX[ Pos ] = Temp; //交换数据

Temp = DataY[ TPos ];

DataY[ TPos ] = DataY[ Pos ];

DataY[ Pos ] = Temp;

} //以下是绘制曲线

BmpTemp->Canvas->Pen->Color = ForeColor;

BmpTemp->Canvas->MoveTo( 0,Enlgr( DataX,DataY,DataCount,0 )); for( i = 1; i < Width; i++ )

{ //Temp为计算出插值

Temp = Enlgr( DataX,DataY,DataCount,i );

第六章 控件设计示例 第 333 页

BmpTemp->Canvas->LineTo( i,Temp ); } //将窗体上的控件也绘制幕后位图上

PaintControls(BmpTemp->Canvas->Handle,0);

TCustomControl::Paint(); //调用父类的绘图函数

Canvas->Draw(0,0,BmpTemp); //将幕后位图的内容反映到画布上

delete BmpTemp; //销毁幕后位图

delete DataX; //清除临时变量,双精度数组

delete DataY; //清除临时变量,双精度数组

}

//------------------------------------------------------------------ void __fastcall TDataPick::FSetShowPoint(bool Value)

{ //ShowPoint属性的设置函数

if(FShowPoint == Value)

return;

FShowPoint = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetForeColor(TColor Value)

{ //曲线颜色 ForeColor的设置函数

if(FForeColor == Value)

return;

FForeColor = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetPointColor(TColor Value)

{ //属性控制点颜色 PointColor属性的设置函数

if(FPointColor == Value)

return;

FPointColor = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetGridColor(TColor Value)

第六章 控件设计示例 第 334 页

{ //网格颜色 GridColor属性的设置函数

if(FGridColor == Value)

return;

FGridColor = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetGrided(bool Value)

{ //是否显示网格的属性 Grided的设置函数

if(FGrided == Value )

return;

FGrided = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetFGridInterval(unsigned short Value)

{ //网格间距的设置函数

if(FGridInterval == Value || !Value)

return;

FGridInterval = Value;

Paint();

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetPointCur(TCursor Value)

{ //属性控制点鼠标 PointCur的设置函数

if(FPointCur == Value)

return;

else

{

FPointCur = Value;

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

{ //逐一改变每个子控件的鼠标

Controls[i]->Cursor = FPointCur;

}

}

}

第六章 控件设计示例 第 335 页

//------------------------------------------------------------------

void __fastcall TDataPick::FSetSelectColor(TColor Value)

{ //选中的属性点的边框颜色的设置函数

if( FSelectColor != Value )

{

FSelectColor = Value;

Paint();

}

}

//------------------------------------------------------------------

int __fastcall TDataPick::FGetPoint(int Index)

{ //用于保存起始点和终点坐标的属性的读取函数

if(Index == 1)

return FStartPoint->Left + 2;

if(Index == 2)

return FStartPoint->Top + 2;

if(Index == 3)

return FEndPoint->Left + 2;

if(Index == 4)

return FEndPoint->Top + 2;

}

//------------------------------------------------------------------

void __fastcall TDataPick::FSetPoint(int Index,int Value)

{ //用于保存起始点和终点坐标的属性的设置函数

if(Index == 1)

FStartPoint->Left = Value - 2;

if(Index == 2)

FStartPoint->Top = Value - 2; if(Index == 3)

FEndPoint->Left = Value - 2;

if(Index == 4)

FEndPoint->Top = Value - 2;

Paint();

}

第六章 控件设计示例 第 336 页

2.3 控件的详解

在这个控件中,我们使用了数据处理函数 Enlgr,这是一个根据给定点位坐标,进行

插值计算的函数,有关算法的详细信息可以参考相关算法的书籍,这里只需要知道这是一

个函数曲线的拟合插值函数,而不是一个任意二维曲线的拟合函数,也就是所给定的参考

点,在沿着曲线运动时,其 X坐标必须是单调递增或者单调递减的,曲线应该是一个以 X为自变量的单值函数。因此,在曲线的属性控制点中不允许有 X坐标相等的点,并且在属

性控制点移动的时候,我们需要限定左右边界。 ValidCtrCheck和 Register是控件必需的函数,这里也不罗嗦。在这个单元里包含了

两个类,一个是 TPointData,另一个是 TDataPick,我们分别介绍。

2.3.1 TPointData

这是用于将属性控制点显示在图形中,并使其能够自己响应鼠标消息,因此使用了从

TGraphicControl的派生类 TShape作为其基类。TPointData并不单独出现,而总

是作为 TDataPick的 Child出现。 TPointData 除了自身原有的属性之外,我们只创建了一个用于记录是否被选定的属

性 Selected,是一个 bool类型的可读写属性,当被选中时,其边框颜色由 Parent 的

SelectColor属性决定,外形为正方形,当未被选中的时候,外形为圆形,边框和 Point颜色相同,由 Parent 的 PointColor 决定。TPointData 的 Selected 属性和其他

Point对象是相互排斥的关系,即同时只能有一个 Point被选中,对于 Selected属性

的排斥关系可以使用 Parent 的 Broadcast 方法来实现,在本案中,由于存在对各个

Point逐一筛选确定 Point的左右边界,因此没有使用 Broadcast方法广播消息,而

是在对每个 Point筛选时设置其 Selected属性。SetSelected方法为 Selected 属性

的设置函数,这个方法唯一需要注意的是要避免出现逻辑上的循环调用,尽管 C++语言允

许函数递归调用,但是没有出口的递归调用最终会导致堆栈的溢出而出现致命的错误,读

者参考 SetSelected方法源代码自行分析。 MouseDown、MouseMove 和 MouseUP 这三个方法是继承父类的虚拟方法,同时也

是响应 TPointData 鼠标消息的方法,这三个方法属于“动态”函数,不同于一般的虚

拟函数,“动态”函数在虚拟函数列表的结构和搜索方法上和一般的虚拟函数不同,因此

在这三个方法中没有调用父类的同名方法,但同样会触发 OnMouseDown 等事件,若调用

了父类的同名方法则会导致循环调用最终堆栈溢出。 MouseDown的主要作用是在鼠标单击的时候选中该 Point(左键)或者删除该Point

(右键),并且记录鼠标左键点击时的位置;MouseMove在鼠标被按下左键是,会随鼠标

移动来改变 Point 的位置,实现对 TPointData 对象的鼠标拖动;MouseUp 在鼠标被

释放时,更新 IsMouseDown标记。 TPointData为 TPointData的构造函数和析构函数,有两个版本,一个指定了创建

的位置,另一个继承了 TShape的构造函数。 Paint方法为响应 WM_PAINT消息的绘图函数,负责根据 Parent的相关属性和自

身的某些属性将Point绘制在 Parent的 Canvas上(参考前面关于TGraphicControl

第六章 控件设计示例 第 337 页

的介绍),最终调用了父类的 Paint方法。

2.3.2 TDataPick

在 TDataPick 中定义了若干个新的属性,大多数是供 Point 显示使用的,也有一

些是供自身显示使用的,这些属性在源代码的注解中有简单的说明,这里不多介绍,带有

FSet开头的方法是这些属性的设置函数。这里没有采用 C++ Builder 的命名规则(Set开头),而是在前面加了 F前导符,只是为了说明命名规则并不是强制性的。

在 TDataPick的属性中,StratPointX、StartPointY、EndPointX、EndPointY是用于存储起始点和终点的位置,他们将会根据设计时用户的设置将起始点和终点的位置

信息存储在.dfm 文件中,进而可以体现在运行时。需要达到这样的需求还可以使用上一

章中介绍的存储未发布的属性,或者对整个 StartPoint和 EndPoint 进行存储等方式。

在本案中,这四个属性共用了一组读写方法 FGetPoint 和 FSetPoint 来从

StartPoint 和 EndPoint中提取和设置位置信息,Index是读写方法的索引参数(参

考前面关于属性的内容)。 TDataPick(TComponent* Owner)为构造函数,主要完成对缺省属性值的设置、创建

起始点和终点对象以及对继承父类的属性的设置,在这个例子中,创建起始点和终点的时

候,指定其 Owner属性为空,这样可以使 TDataPoint 对象在设计时可以响应鼠标消息,

但不足之处是 TDataPick实例销毁的时候不能自行销毁起始点和终点对象的实例,必需

在析构函数~TDataPick 中使用 delete关键字显式的销毁,这和上一节例子中的处理方

法稍有不同。 WM_Mouse是 WM_MOUSEDOWN 消息的处理函数,当用户点击左键的时候,会在图形

上创建一个新的属性控制点 TPointData 的实例并重新绘制曲线。在 TDataPick 中,

使用了消息映射表的方式来响应鼠标消息,也可以使用 TPointData 中重载 MouseDown方法的方式,这两种方式没有本质的区别,只是不同的两种方式,但是使用消息映射表需

要调用父类的消息处理过程才能执行缺省的消息处理方法。在 WMMouse 方法中,我们没

有调用父类的消息处理方法,因此不会不会调用系统的缺省操作。 TDataPick的一个重要方法是从 TCustomeControl 继承来的 Paint方法,这是

用于将需要的图形信息绘制到画布上,在 TDataPick控件中,绘制的图形比较复杂,同

时又有其他 Child控件需要绘制,直接绘图到 Canvas上会导致图形闪烁,我们在这里

使用了幕后位图进行缓冲来克服这个问题。 Paint方法主要有三个部分构成,一是绘制背景和网格,二是绘制曲线,三是将 Child

控件绘制到 Canvas上,网格和 Child控件是根据需要进行绘图,在绘制曲线的过程中,

使用到了插值函数进行曲线的拟合。 至此,TDataPick 的界面部分已经基本构成,但还没有最终完成,因为在这个控件

中没有提供给用户数据的输出接口,无论是属性控制点的数据还是经过拟合后的插值数

据,都没有提供给用户,为了能够使用这个控件,我们还需要进一步完善。这个工作留给

读者来完成,在完成这个工作的时候需要注意的是,必需提供图像坐标和实际数据坐标的

转换过程或提供转换比例 Scale 属性,提供获取数据的方法,在某些应用中,比如频谱

其 X轴并不是线性分布,人们通常更习惯使用对数坐标系,也可以考虑增加一个用于描述

第六章 控件设计示例 第 338 页

坐标系的属性。

3 创建属性编辑器和控件的缺省动作

在控件设计中,一个很重要的内容就是为新建的控件定义缺省动作和定义新类型属性

的编辑器,以及定义拷贝粘贴等动作的数据等,前一章我们已经简单介绍过了这些内容,

在本节中将就一个非常简单的展示如何为新属性创建个性的编辑器,如何为控件在 IDE中

定义缺省动作和弹出菜单的内容。

3.1 控件简介

这个控件本来是 C++ Builder所附带的一个控件

的例子(原控件名称为 TPies,在 Sample面板中),

如右图所视,控件在父窗体上画一个饼状图形,当双击

控件或选择弹出菜单的第一项时,弹出”Pie Angels Editor”对 Pies的 Angels属性进行编辑。

Angles是一个 TAngles 类型的可读写属性,该

属性具有两个用于记录角度的属性:StartAngle 和

EndAngle。由于 TAngles 是从 TPersistent 类派

生而来,因此该属性没有自己的编辑器,甚至在

Object Inspector 中也不能进行编辑,因此需要对

TAngles类创建一个编辑器,这个例子中,TAngles有两种编辑方式:一是通过 Object Inspector编辑,

如同 TFont 那样,其子属性在 Object Inspector中展开;另一种方式是双击该属性,弹出右图 6.4 中

的对话框编辑器。 这个控件的属性和实现均没有什么特殊的,主要是

对新建的属性增加了编辑器,并定义了缺省动作,我们

来看如何实现。 首先是建立一个用于记录起始角度和终止角度的类 TAngles,然后再定义我们的主

控件 TMyPie,并且需要为 Angles 对象创建和注册一个 IDE 中使用的属性编辑器

TAnglesProperty,使 Angles 属性可以在 Object Inspector 中按照要求编辑,最

后还要创建并注册一个控件的编辑器,控件编辑器可以指定控件在 IDE中被双击或者点击

右键弹出的菜单的动作。

3.2 源代码

读者可以在 C++ Builder 的例子中找到这个例子的源代码,原文分作两个单元,

TPies控件的单元和编辑器等注册单元,作者对其稍作修改,并加入了注解,如下:

图 6.4 Tangles 的编辑器

第六章 控件设计示例 第 339 页

3.2.1 声明部分

MyPies.h文件,其中包含了五个类:TAngles、TMyPie、TAngleEditorDlg、TAnglesProperty、TPieEditor。

#ifndef MYPiesHPP

#define MYPiesHPP

#include <StdCtrls.hpp>

#include <DsgnIntf.hpp>

#include <Buttons.hpp>

#include <Windows.hpp>

#include <System.hpp>

#include <StdCtrls.hpp>

#include <Graphics.hpp>

#include <Forms.hpp>

#include <Controls.hpp>

#include <Classes.hpp>

#include <math.h>

#include <ComCtrls.hpp>

#include <memory>

class PACKAGE TAngles : public TPersistent

{ //定义 TAngles数据类型,将作为 TPie的属性

private:

int FStartAngle; //饼状的起始角度

int FEndAngle; //饼状的终止角度

TNotifyEvent FOnChange; //OnChange事件的内部存储数据

void __fastcall SetStart(int Value); //用于设置起始角度

void __fastcall SetEnd(int Value); //用于设置终止角度

public: //Assign用于对象的拷贝

void __fastcall Assign(TPersistent* Value);

void __fastcall Changed(void); //Change用于反映设置值的变化

__fastcall TAngles(void); //构造函数和析构函数

__fastcall virtual ~TAngles(void);

__published: //定义需要的属性和事件

__property int StartAngle = {read=FStartAngle, write=SetStart,

nodefault};

__property int EndAngle = {read=FEndAngle, write=SetEnd, nodefault};

__property TNotifyEvent OnChange = {read=FOnChange, write=FOnChange};

第六章 控件设计示例 第 340 页

}; //从 TCustomControl派生 TMyPie

class PACKAGE TMyPie : public TCustomControl

{

__published:

TPen *FPen; //Pen属性的内部变量

TBrush *FBrush; //Brush属性的内部变量

TAngles *FAngles; //Angles属性的内部变量

virtual void __fastcall Paint(void); //重载 Paint方法

void __fastcall SetBrush(TBrush* Value); //属性的写方法

void __fastcall SetPen(TPen* Value); //属性的写方法

void __fastcall SetAngles(TAngles* Value); //属性的写方法

void __fastcall StyleChanged(TObject* Sender);//反映改变的方法

__property Anchors ; //继承父类的属性

__property Constraints ; //继承父类的属性

__property TAngles* Angles = {read=FAngles, write=SetAngles, nodefault};

__property TBrush* Brush = {read=FBrush, write=SetBrush, nodefault};

__property TPen* Pen = {read=FPen, write=SetPen, nodefault};

__property ShowHint ; //继承父类的属性

__property OnClick ; //下同

__property OnDblClick ;

__property OnDragDrop ;

__property OnDragOver ;

__property OnEndDock ;

__property OnEndDrag ;

__property OnMouseDown ;

__property OnMouseMove ;

__property OnMouseUp ;

__property OnStartDock ;

__property OnStartDrag ;

public:

__fastcall virtual TMyPie(TComponent* AOwner);

__fastcall virtual ~TMyPie(void);

};

class PACKAGE TAngleEditorDlg : public TForm

{ //定义 TAngles的编辑对话框

__published:

第六章 控件设计示例 第 341 页

TLabel *EAngleLabel; //终止角度的显示标题

TLabel *SAngleLabel; //起始角度的显示标题

TButton *OKButton; //确定按钮

TButton *CancelButton; //取消按钮

TTrackBar *STrackBar; //编辑起始角度的 TrackBar

TTrackBar *ETrackBar; //编辑终止角度的 TrackBar

void __fastcall CancelClick(TObject* Sender); //取消按钮的 Click句柄

void __fastcall STrackBarChange(TObject* Sender);

//起始 TrackBar的 OnChage句柄

void __fastcall ETrackBarChange(TObject* Sender);

//终止 TrackBar的 OnChage句柄

private:

int FOrigStart; //存储原来的起始角度,当取消编辑时用于恢复原始值

int FOrigEnd; //存储原来的终止角度,当取消编辑时用于恢复原始值

TAngles* FAngles; //被编辑的 TAngles对象

void __fastcall SetStartAngle(int Value); //起始角度的设置函数

void __fastcall SetEndAngle(int Value); //终止角度的设置函数

void __fastcall SetAngles(TAngles* Value); //TAngles的设置函数

public:

__property TAngles* EditorAngles = {read=FAngles, write=SetAngles,

nodefault}; __fastcall virtual TAngleEditorDlg(TComponent* AOwner); //构造函数 1

__fastcall TAngleEditorDlg(TComponent* AOwner, int Dummy);//构造函数 2

__fastcall TAngleEditorDlg(HWND ParentWindow); //构造函数 3

__fastcall virtual ~TAngleEditorDlg(void); //析构函数

};

class PACKAGE TAnglesProperty : public TClassProperty

{ //定义 TAngles的属性编辑器

public:

virtual void __fastcall Edit(); //重载 Edit方法

virtual TPropertyAttributes __fastcall GetAttributes(void);

//重载 GetAttributes 方法

__fastcall virtual ~TAnglesProperty(void); //析构函数和构造函数

__fastcall TAnglesProperty(const _di_IDesigner ADesigner,

int APropCount);

};

class PACKAGE TPieEditor : public TDefaultEditor

第六章 控件设计示例 第 342 页

{ //定义 TAngles的缺省编辑器,向 IDE的弹出菜单添加项目

protected:

virtual void __fastcall EditProperty(TPropertyEditor* PropertyEditor,

bool& Continue, bool& FreeEditor);

public:

virtual void __fastcall ExecuteVerb(int Index); //用于执行菜单操作

virtual AnsiString __fastcall GetVerb(int Index); //获取菜单标题

virtual int __fastcall GetVerbCount(void); //获取菜单数目

__fastcall virtual TPieEditor(TComponent* AComponent,

_di_IDesigner ADesigner);

__fastcall virtual ~TPieEditor(void); //构造函数和析构函数

};

#define PI 3.1415926535897932385 //用于计算角度

#endif

3.2.2 实现部分

在 MyPies单元中代码如下: #pragma hdrstop

#include "Mypies.h"

#pragma resource "*.dfm"

#pragma package(smart_init)

__fastcall TAngles::TAngles(void) : TPersistent()

{ //空的构造函数

}

//-----------------------------------------------------------------

__fastcall TAngles::~TAngles(void)

{ //空的析构函数

} //-----------------------------------------------------------------

void __fastcall TAngles::Assign(TPersistent* Value)

{ //Assign方法用于 TAngles对象的数据拷贝

StartAngle = dynamic_cast<TAngles*>(Value)->StartAngle;

EndAngle = dynamic_cast<TAngles*>(Value)->EndAngle;

}

//-----------------------------------------------------------------

第六章 控件设计示例 第 343 页

void __fastcall TAngles::SetStart(int Value)

{ //属性改变通过 Changed方法放映在界面上(这里只是触发的 OmChange事件)

if (Value != FStartAngle)

{

FStartAngle = Value;

Changed();

}

}

//-----------------------------------------------------------------

void __fastcall TAngles::SetEnd(int Value)

{ //与上面的方法类似

if (Value != FEndAngle)

{

FEndAngle = Value;

Changed();

}

}

//-----------------------------------------------------------------

void __fastcall TAngles::Changed()

{ //这是标准的 VCL事件的处理方法,可以根据需要将 Changed方法定义为虚拟函数

if (FOnChange != NULL) //这样可以在其派生类重载来改变 Changed的处理过程

FOnChange(this);

}

//-----------------------------------------------------------------

__fastcall TMyPie::TMyPie(TComponent* AOwner): TCustomControl(AOwner)

{ //MyPie的构造函数,对属性的缺省值和事件进行初始化,

Width = 100;

Height = 100; FPen = new TPen(); //这里采用了新建对象方式来记录 Pen、Brush等属性,

FPen->OnChange = StyleChanged; //也可以采用前面我们使用过的将子控

FBrush = new TBrush(); //件的属性映射到父控件的方式

FBrush->OnChange = StyleChanged; //但代码需要作适当修改

FAngles = new TAngles();

FAngles->OnChange = StyleChanged;

FAngles->StartAngle = 180;

FAngles->EndAngle = 90;

第六章 控件设计示例 第 344 页

} //-----------------------------------------------------------------

__fastcall TMyPie::~TMyPie(void)

{

delete FPen; //析构函数负责将自己创建并且不能够

delete FBrush; //自动销毁的对象销毁

delete FAngles;

}

//-----------------------------------------------------------------

void __fastcall TMyPie::StyleChanged(TObject* /*Sender*/)

{

Invalidate(); //本案中只是进行了函数调用的传续

}

//-----------------------------------------------------------------

void __fastcall TMyPie::SetBrush(TBrush* Value)

{ //对象赋值需要拷贝,因为 Value不一定是永久生存的对象

FBrush->Assign(Value);

} //-----------------------------------------------------------------

void __fastcall TMyPie::SetPen(TPen* Value)

{ //同上

FPen->Assign(Value);

}

//-----------------------------------------------------------------

void __fastcall TMyPie::SetAngles(TAngles* Value)

{ //同上并且进行图形的刷新

FAngles->Assign(Value);

Invalidate();

}

//-----------------------------------------------------------------

void __fastcall TMyPie::Paint()

{ //Paint方法根据起始点和终点以及颜色等在花布上绘制饼状图形

int StartA, EndA;

第六章 控件设计示例 第 345 页

int midX, midY, stX, stY, endX, endY;

float sX, sY, eX, eY;

StartA = FAngles->StartAngle;

EndA = FAngles->EndAngle;

midX = Width/2;

midY = Height/2;

sX = cos((StartA / 180.0) * PI);

sY = sin((StartA / 180.0) * PI);

eX = cos((EndA / 180.0) * PI);

eY = sin((EndA / 180.0) * PI);

stX = floor(sX * 100);

stY = floor(sY * 100);

endX = ceil(eX * 100);

endY = ceil(eY * 100);

Canvas->Pen = FPen;

Canvas->Brush = FBrush;

Canvas->Pie(0,0,

Width,Height,

midX + stX, midY - stY,

midX + endX, midY - endY);

}

//-----------------------------------------------------------------

namespace Mypies

{ //注册控件、组件编辑器以及 TAngles的属性编辑器等

void __fastcall PACKAGE Register()

{

TComponentClass classes[1] = {__classid(TMyPie)};

RegisterComponents("MyInterFace", classes, 0);

RegisterComponentEditor(__classid(TMyPie), __classid(TPieEditor));

RegisterPropertyEditor(__typeinfo(TAngles),

NULL,

"",

__classid(TAnglesProperty));

}

}

//-----------------------------------------------------------------

第六章 控件设计示例 第 346 页

__fastcall TAngleEditorDlg::TAngleEditorDlg(TComponent* AOwner)

: TForm(AOwner)

{ //空的构造函数

}

//-----------------------------------------------------------------

__fastcall TAngleEditorDlg::TAngleEditorDlg(TComponent* AOwner, int Dummy)

: TForm(AOwner, Dummy)

{ //空的构造函数

}

//-----------------------------------------------------------------

__fastcall TAngleEditorDlg::TAngleEditorDlg(HWND ParentWindow)

: TForm(ParentWindow)

{ //空的构造函数

} //-----------------------------------------------------------------

__fastcall TAngleEditorDlg::~TAngleEditorDlg(void)

{ //空的析构函数

} //-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::STrackBarChange(TObject* /*Sender*/)

{ //当起点 TrackBar改变时更新起点角度

SetStartAngle(STrackBar->Position);

}

//-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::ETrackBarChange(TObject* /*Sender*/)

{ //当终点 TrackBar改变时更新终点角度

SetEndAngle(ETrackBar->Position);

}

//-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::SetStartAngle(Integer Value)

{ //设置起始角度的同时改变显式字符,并保证 TrackBar和 Value一致

STrackBar->Position = Value; //这里不会出现和 OnChange事件无限次循环调用,

SAngleLabel->Caption = "StartAngle = " + String(Value);

第六章 控件设计示例 第 347 页

FAngles->StartAngle = Value; //因为当 STrackBar->Position

//和 Value相等时,将不会触发 OnChange事件

}

//-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::SetEndAngle(Integer Value)

{ //同上

ETrackBar->Position = Value;

EAngleLabel->Caption = "EndAngle = " + String(Value);

FAngles->EndAngle = Value;

} //-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::SetAngles(TAngles* Value)

{ //获取被编辑的对象,并获取其包含属性反映在 Form中

FAngles = Value; FOrigStart = Value->StartAngle; //保存原始值

FOrigEnd = Value->EndAngle; //保存原始值

SetStartAngle(Value->StartAngle);

SetEndAngle(Value->EndAngle);

} //-----------------------------------------------------------------

void __fastcall TAngleEditorDlg::CancelClick(TObject* /*Sender*/)

{ //取消时恢复原始取值

SetStartAngle(FOrigStart);

SetEndAngle(FOrigEnd);

}

//-----------------------------------------------------------------

__fastcall TAnglesProperty::TAnglesProperty(const _di_Idesigner

ADesigner,int APropCount)

:TClassProperty(ADesigner, APropCount)

{ //空的构造函数

}

//-----------------------------------------------------------------

__fastcall TAnglesProperty::~TAnglesProperty(void)

{ //空的析构函数

第六章 控件设计示例 第 348 页

} //-----------------------------------------------------------------

void __fastcall TAnglesProperty::Edit()

{

//这里使用了 auto_ptr 来分配对象和内存,当作用域 结束时内存会自动释放.

std::auto_ptr<TAngleEditorDlg> AngleEditor(

new TAngleEditorDlg(Application));

AngleEditor->EditorAngles = (TAngles*)GetOrdValue();

AngleEditor->ShowModal(); //将被编辑的属性传给属性编辑对话框,

} //-----------------------------------------------------------------

TPropertyAttributes __fastcall TAnglesProperty::GetAttributes()

{ //使 TAngles的属性编辑器具有对话框编辑和子属性展开的能力

return (TPropertyAttributes() << paDialog << paSubProperties);

}

//-----------------------------------------------------------------

__fastcall TPieEditor::TPieEditor(TComponent* AComponent,

_di_IDesigner ADesigner)

: TDefaultEditor(AComponent, ADesigner)

{ //空的构造函数

}

//-----------------------------------------------------------------

__fastcall TPieEditor::~TPieEditor(void)

{ //空的析构函数

}

//-----------------------------------------------------------------

void __fastcall TPieEditor::EditProperty(TPropertyEditor* PropertyEditor,

bool& Continue, bool& /*FreeEditor*/)

{ //PropertyEditor将指向 TAnglesProperty对象,该方法被 Edit方法调用,

String PropName(PropertyEditor->GetName()); //获取属性名

if (strcmpi(PropName.c_str(),"ANGLES") == 0)

{ //属性名为"ANGLES"时,调用该属性的编辑器(PropertyEditor)的 Edit方法

PropertyEditor->Edit();

Continue = false; //不再继续枚举其它的属性和事件

第六章 控件设计示例 第 349 页

}

}

//-----------------------------------------------------------------

int __fastcall TPieEditor::GetVerbCount()

{ return 1; //获取添加的菜单项的数目

}

//-----------------------------------------------------------------

String __fastcall TPieEditor::GetVerb(Integer Index)

{

if (Index == 0) //获取添加的菜单项的显示文本(Caption)

return "Editor";

else //忽略其它非法索引,有效范围为 GetVerbCount - 1 ;

return String("");

}

//-----------------------------------------------------------------

void __fastcall TPieEditor::ExecuteVerb(Integer Index)

{ if(Index == 0) //指向菜单项,Edit为父类的虚拟函数,将调用属性的编辑器

Edit();

}

//-----------------------------------------------------------------

3.3 控件的详解

在这个控件中,总共使用了五个类,分别是 TAngles、TMyPie、TAngleEditorDlg、TAnglesProperty、TPieEditor,它们担任者不同的角色,也有着不同作用,下面我

们分别作以介绍。

3.3.1 TAngles

这是一个从 TPersistent派生的类,因此具有将数据存储在.dfm文件中的能力,

在这个类中,有两个属性 StartAngle和 EndAngle 及一个事件 OnChange,这两个属

性分别记录了两个角度用于表示起始和终止角度,然而,TAngles类不仅是给 TMyPies使用的,它应该是一种普遍适应的变量类型,而且是支持 VCL的,并且 TAngles 并不自

己将变化反映在界面上,因为它并不是一个控件,但必需为 TAngles 提供一个用于通报

或者是反映属性改变的接口,这就是 OnChange 事件,OnChang 事件是标准的

第六章 控件设计示例 第 350 页

TNotifyEvent事件类型。 在 TAngles中,我们对变化的处理方法和 DataPick控件大不一样,在 DataPick

中,TDataPoint 是我们 TDataPick专用的,而且并没有希望这个类会被其它的控件作

为属性,因此我们在 TDataPoint 的属性设置函数中对 TDataPoint 属性变化作了相应

的处理,而 TAngles将这种变化作成一个接口 OnChange,通过 OnChange 事件可以使

使用 TAngles对象作为属性的控件可以自由的指定 Angles的值,当 TAngles 变化时,

控件作出正确的反应,这就象在 TPickEdit中我们对 TSpeedButton 的 OnClick 事件

的处理一样。这是不同的处理方法,但最终的目的和结果是相同的,可以根据实际情况选

择处理方法。 OnChange事件是由 Change方法调用的,也就是在 Change 方法中触发 OnChange

事件,前面我们也讲过,这是 VCL标准的处理方式,也可以将 Change声明为虚拟函数,

这样以来,在 TAngles的派生类中,可以通过重载 Change方法使在触发 OnChange 事

件之前作某些相应的操作,由此而实现对 OnChange事件的重载。 在 TAngles类中,另一个很重要的内容是 Assign 方法,我们知道在 VCL 类库和其

派生类中,没有标准 C++的拷贝构造函数,Delphi 中为 VCl 类库供了用于拷贝的方法

Assign和 AssignTo,在后面的例子 TDBJpegImage中发现这个方法比拷贝构造更加

灵活,关于这两个方法的详细应用读者参考前面有关章节。

3.3.2 TMyPie

这是一个由 TCustomCtrol 派生的控件类,在 C++ Builder 的例程中是从

TGraphicControl 派生而来,这也是我们最终需要向 IDE注册的控件。在 TMyPie控

件中,有三个新扩展的属性:Pen、Brush、Angles,其中 Pen 和 Brush是用于显示饼

状图形的画笔和画布的刷子,SetBrush和 SetPen分别是它们的设置函数,这两个属性

也可以采用我们前面是用过的属性映射的方式,TMyPie中是对 Pen 和 Brush重新创建

了新的对象。 StyleChanged方法用于反应属性的改变,构造函数负责创建新的 Pen 和 Brush,

析构函数则进行销毁,重载的 Paint方法会相应 WM_PAINT消息会画布进行绘制。其它

属性都是从其父类继承而来。

3.3.3 TAngleEditorDlg

TMyPie控件和前面介绍的几个控件相比较,最大的特点是为新创建的属性增加了属

性编辑器,TAngleEditDlg 是从 TForm 派生而来的对话框,如前面图中所视的那个编

辑器,这个 Form对象中包含了两个 TTrackBar、两个 TLabel和两个 TButton 对象,

这些可以在 IDE中可视化编辑,包括三个声明在 publish 定义域的方法 CancelClick、STackBarChange和 ETrackBarChange方法。

另外在 TAngleEditDlg对话框中声明了一个TAngles类型的属性 EditorAngles,

它的作用是将被编辑的属性 Angles 引入到对话框以便对其编辑(设置),这个属性在

TMyPie控件的 Angles属性被编辑时,由 TAnglesProperty 的编辑方法 Edit负责获

第六章 控件设计示例 第 351 页

取并交给 EditorAngles属性,因此在 Form中对 EditorAngles 的操作将完全反应在

TMyPie的 Angles属性上。EditorAngles 的另一个作用是将 Angles属性中的起始

角度和终止角度同步的反映在 TAnglesEditDlg 的界面上,同时将图形界面上的变化同

步的反映到 Angles属性中,实现编辑器和属性的互动操作。 除此之外,TAngleEditDlg 并没有其它特殊之处,和一般的 Form基本一样,读者

自行理解。当用户在 IDE中双击控件或者在 Object Inspector 双击 Angles 属性时,

TAnglesEditDlg 将会被 TAnglesProperty 的 Edit方法创建,同时将 Angles 属性

传递给 TAngleEditDlg,用户在编辑对话框中的编辑会实时的反映在 Angles 属性和

TMyPie控件中,用户点击取消时,恢复编辑前的原始值,确定则改变为新的属性值。

3.3.4 TAnglesProperty

TAnglesProperty 是从类编辑器 TClassProperty 派生而来,需要完成的是两个

工作,一是让 Angles属性可以在 Object Inspector 被展开以及允许打开编辑器对话

框,这个工作是通过重载 GetAttrbutes方法来实现的;另一个工作是在 Edit方法中

打开编辑器对话框,也就是反映用户双击属性时的动作。这两件事都很简单,读者参考源

码理解。 关于属性编辑器的详细内容请读者参阅联机帮助。

3.3.5 TPieEditor

TPieEditor 是从 TDefualtEditor 派生而来的组件编辑器,组件编辑器的目的是

在 IDE中对组件进行编辑,诸如双击组件、拷贝组件、粘贴组件、点击右键等编辑操作所

作的反应,TDefualtEditor 为组件定义了通用的编辑功能,比如剪切、拷贝等等,但

某些时候针对不同的控件我们可能经常需要定义个性的编辑功能,比如在拷贝控件的时候

将其某些属性的数据一同拷贝,或者指定双击组件的缺省动作以及在快捷菜单中增加其它

的编辑功能。 在这个例子中,TPieEditor 在快捷菜单中增加了一个项目“ Editor”单击 Editor

菜单的时候,IDE将打开 Angles的对话框编辑器,向快捷菜单中添加菜单项并且响应需

要重载三个方法:ExecuteVerb、GetVerb 和 GetVerbCount,GetVerbCount 是 IDE用来获取用户添加的菜单项的数量,GetVerb供 IDE 来获取每一个菜单项的显示字符串,

ExecuteVerb 是供 IDE 调用某个自定义菜单项被点击时的接口。在这个例子中

GetVerbCount返回为“1”,即有一个用户自定义菜单项,GetVerb 中对应的是 Index为“0”的菜单项,返回字符串“Editor”。这时,控件在 IDE中快捷菜单就添加了一个

新的项目“Editor”,当用户点击该菜单项时,IDE调用 ExecuteVerb,并以 Index=0为参数,在这个例子中,当 ExecuteVerb 的 Index参数为“0”时,调用 TPieEditor的 Edit方法。

Edit 也是一个虚拟函数,是当用户在 IDE 中双击组件时调用的,缺省情况,

TDefultEditor 的 Edit 方法首先会寻找该组件的 OnCreate 事件,如果 OnCreate事件没有发布,再寻找 OnChanged事件,然后是 OnClick事件,若这些事件都没有发

布,则按照 Object Inspector中该组件的第一个事件来在 Code Editor 中创建该事

第六章 控件设计示例 第 352 页

件的处理方法,需要更改双击组件的缺省动作,只需要重载 EditProperty 方法。这个

例子中,EditProperty 方法将在双击控件时调用 Angles 的属性编辑器,例中

EditProperty在 Edit方法被调用后依次列出组件的每一个属性及事件供编辑,其参数

Continue决定是否继续枚举,当枚举找到 Angels 时,停止继续枚举,并根据 Angles属性打开属性编辑器。

TMyPie控件是一个比较全面的应用 IDE特征的控件,作为一个组件设计人员,必需

掌握属性编辑器、控件编辑器等方面的内容,但是本书并不是针对 VCL组件开发来编写的,

主要是通过组件来掌握如何面向状态,来掌握在 C++ Builder中如何这种语言所提供的

新功能。需要进一步了解属性编辑器和组件编辑器请读者参考相关书籍或者联机帮助。

4 创建数据控件 TDBJpegImage

接下来的一个例子是创建一个用于数据库的控件,在 C++ Builder 中,Borland为我们提供了非常强大的数据库处理能力,一个非常有用的功能就是数据库和图形显示界

面的关联,这不仅仅是一个界面的事情,而更多的是使用 VCL的面向状态的技术使各个对

象之间的关系简化了许多。在 TDBJpegImage 控件中,代码非常的短,但在这段代码的

背后隐藏着 Borland公司的 VCL中最精湛的技术,也体现了 VCL强大的功能。 在 Borland的 VCL中提供了用于图形存储的数据库控件 TDBImage,但遗憾的是这

个 DBImage 并不能支持 Jpeg格式的图形数据,VCL还提供了一个用于存取 Jpeg数据

格式的组件 TJPEGImage,既然 VCL 中有了 Jpeg 的处理能力,我们就可以编制一个用

于存取 Jpeg的数据库控件。 之所以选择数据库控件作为这一章的最后一个例子,不仅是在网上经常有网友问起如

何在数据库中存储 Jpg格式的数据,更重要的是在数据库的控件中,各个控件之间存在着

更加复杂逻辑关系,而使用了 VCL你可能根本看不到它们是如何联系在一起,却可以非常

默契的配合工作,这正是面向状态所带来的好处。当你需要设计一个新的控件的时候,也

许并不需要过多考虑 Borland是否在设计 VCL时就考虑了向前兼容,因为回答是肯定没

有,但是你却可以很容易的做到向后兼容,当然,要做到这一点,必需对 VCL有这更深入

的了解,同时对面向状态的技术也必需熟练的掌握。

4.1 控件的分析

我们先来看看一个数据库的控件需要和那些对象发生关系,以及这些个控件之间的联

系是什么?在一个数据库程序中,需要使用数据库控件必需具备至少以下几个对象: n 由 TDataSet派生而来的数据集组件,如:TTable、TQuery、ADODataSet、

ADOTable等来连接某个数据库中的数据表。 n 至少包含一个TDataSource组件,提供给数据集和数据库控件之间的必要连接。 n 一个数据库控件来显示以及编辑数据库中的数据结果,如:TDBGrid、TDBEdit

第六章 控件设计示例 第 353 页

等数据库控件。 这三个对象是必不可少的,但实际中对于数据库的操作仅有这几个对象还是不够的,

诸如 TDataBase、TDataLink、TField 或它们的派生类也是必不可少的,但并不一定

需要用户显示的创建和操作、有些是 VCL在幕后自动进行的,有些则是可以使用其它方式

变通使用的。 我们先来看一看最简单的数据库应用,假定包含一个 TDataSet 对象、一个

TDataSource 对 象 和 一 个 自 定 义 的 数 据 库 控 件 TDBJpegImage。 当 需 要 为

TDBJpegImage 指定数据库时,必需指定一个数据源 TDataSource 对象和使用数据对

应的字段名称,TDataSource 能够为数据库控件使用就必需有其连接的数据库表

TDataSet 对象。在这个连接关系中,不是一对一,一个 TDataSet 对象可以作为多个

TDataSource 的 DataSet;而且一个 TDataSource 对象又同时可以作为多个数据库

控件的数据源。这种联系并不是为了更加好看,而是要完成数据的更新、编辑、存储等等

一系列操作的同步和协调,而在这个联系中,还必须有一个幕后的中间对象 TDataLink对象。这是这种错综复杂的逻辑关系使得程序设计上必需更多的考虑之间的联系,好在

VCL使用的面向状态的技术可以是这些关系变得简单一些,使一些复杂的逻辑关系化简为

一些简单的逻辑关系。 在这个 TDBJpegImage 中,我们将不再花费大量的篇幅完善控件本身的功能,仅仅

提供了一个从剪贴板粘贴图片的接口,而且还是利用 VCL本身来完成的,有兴趣的读者可

以自行扩展功能,如剪切、清除、甚至从数字图像设备直接采集图像源的接口,这里只是

希望能通过这个例子是读者更好理解在 VCL中如何处理这种复杂的逻辑关系。

4.2 源代码

TDBJpegImage中使用其它 BPI 中的组件 TJPEGImage,建立 Package工程的时

候,必需包含其它相关的 Bpi文件,详细的信息以及源代码读者可以在本书的附带光盘中

找到。

4.2.1 声明部分

该例子中 TDBJpegImage是从 TCustomControl 派生而来,这样的话控件将可以

拥有自己的窗口句柄,可以接受系统的键盘消息,更重要的是可以作为数字图像设备如:

视频采集卡、数码相机、扫描仪、电脑眼等设备的回显窗体。如果希望在 TDBJpegImage中实现更多的编辑功能,如:放大、缩小、区域剪切、拷贝、旋转等操作,使用窗体控件

会获得比图形控件更高的执行速度。 下面是 DBJpegImage.h的内容: #ifndef DBJpegImageH

#define DBJpegImageH

//-----------------------------------------------------------------

#include <SysUtils.hpp>

#include <Controls.hpp>

第六章 控件设计示例 第 354 页

#include <Classes.hpp>

#include <Forms.hpp>

#include <Jpeg.hpp> //这个单元包含了 VCL中对 Jpeg数据的处理类 TJPEGImage等

#include <DBCtrls.hpp>

#include <dbtables.hpp>

//-----------------------------------------------------------------

class PACKAGE TDBJpegImage : public TCustomControl

{ //从 TCustomControl派生而来的 TDBJpegImage

private:

TFieldDataLink * FDataLink; //用于响应数据库变化和编辑

TJPEGImage * FJpg; //用于存储和读取 Jpeg格式的图片

protected:

void __fastcall DataChanged(TObject *Sender); //反映数据库的改变

void __fastcall UpdateData(TObject *Sender); //更新数据库的数据

AnsiString __fastcall GetDataField(void); //DataField属性的读取函数

TDataSource* __fastcall GetDataSource(void); //DataSource的读取函数

void __fastcall SetDataField(AnsiString Value); //DataField的设置函数

void __fastcall SetDataSource(TDataSource* Value);

//DataSource的设置函数

virtual void __fastcall CreateParams(TCreateParams &Params );

public: //CreateParams是重载 TWinControl的虚拟函数

__fastcall TDBJpegImage(TComponent* Owner); //构造函数

__fastcall ~TDBJpegImage(void); //析构函数

Graphics::TBitmap* FBmp; //用于存储图片的数据

void __fastcall PasteFromCliborad(void); //用于从粘贴板获取图片数据

virtual void __fastcall Paint(void); //绘制控件的函数

__published: //数据库必需的属性 DataField和 DataSource

__property AnsiString DataField =

{read=GetDataField,write=SetDataField,nodefault};

__property TDataSource* DataSource =

{read=GetDataSource,write=SetDataSource,nodefault}; __property Color ; //继承发布父类的属性

__property Constraints ; //同上

//⋯⋯ //同上

__property Visible ; //同上

__property OnClick ; //继承发布父类的事件

__property OnContextPopup ; //同上

__property OnDblClick ; //同上

//⋯⋯ //同上

第六章 控件设计示例 第 355 页

}; //-----------------------------------------------------------------

#endif

除了为TDBJpegImage定义了一个新的绘图函数 Paint和一个用于从剪贴板中粘贴

数据的 PasteFromCliborad 方法以外,没有任何新的东西,但这已经可以完成一个最

简单的 Jpeg数据库图像的存取。

4.2.2 实现部分

#include <vcl.h>

#pragma hdrstop

#include "DBJpegImage.h"

#include <clipbrd.hpp>

#pragma package(smart_init)

//-----------------------------------------------------------------

static inline void ValidCtrCheck(TDBJpegImage *)

{ new TDBJpegImage(NULL);

}

//-----------------------------------------------------------------

__fastcall TDBJpegImage::TDBJpegImage(TComponent* Owner)

: TCustomControl(Owner)

{

ControlStyle = ControlStyle << csOpaque << csReplicatable;

if(!NewStyleControls) //设置控件的风格

ControlStyle = ControlStyle << csFramed;

FDataLink = new TFieldDataLink; //创建 DataLink属性的对象

FDataLink->Control = this; //获取 DataLink的控制权

FDataLink->OnDataChange = DataChanged; //设置 OnDataChanged事件

FDataLink->OnUpdateData = UpdateData; //设置 OnUpdateData事件

FBmp = new Graphics::TBitmap(); //创建内部位图

FJpg = new TJPEGImage(); //创建 Jpeg图片对象

}

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::CreateParams(TCreateParams &Params )

第六章 控件设计示例 第 356 页

{ //使控件具有边框和 3D效果

TCustomControl::CreateParams(Params);

if( Ctl3D )

Params.ExStyle = Params.ExStyle | WS_EX_CLIENTEDGE;

else

Params.Style = Params.Style | WS_BORDER;

Params.WindowClass.style = Params.WindowClass.style &

!(CS_HREDRAW | CS_VREDRAW);

}

//-----------------------------------------------------------------

__fastcall TDBJpegImage::~TDBJpegImage(void)

{ //析构函数

FDataLink->Control = NULL;

FDataLink->OnDataChange = NULL;

FDataLink->OnUpdateData = NULL; delete FDataLink; //销毁构造函数创建的对象

delete FBmp;

delete FJpg;

}

//-----------------------------------------------------------------

namespace Dbjpegimage

{ //注册函数

void __fastcall PACKAGE Register()

{

TComponentClass classes[1] = {__classid(TDBJpegImage)};

RegisterComponents("MyInterFace", classes, 0);

}

}

//-----------------------------------------------------------------

AnsiString __fastcall TDBJpegImage::GetDataField(void)

{ //获取 DataField属性(将 FDataLink的属性映射到 TDBJpegImage)

return FDataLink->FieldName;

}

//-----------------------------------------------------------------

TDataSource* __fastcall TDBJpegImage::GetDataSource(void)

{ //获取 DataSource属性(将 FDataLink的属性映射到 TDBJpegImage)

return FDataLink->DataSource;

}

第六章 控件设计示例 第 357 页

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::SetDataField(AnsiString Value)

{ //设置 DataField属性(将 FDataLink的属性映射到 TDBJpegImage)

FDataLink->FieldName = Value;

}

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::SetDataSource(TDataSource* Value)

{ //设置 DataSource属性(将 FDataLink的属性映射到 TDBJpegImage)

FDataLink->DataSource = Value;

}

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::DataChanged(TObject *Sender)

{ //当数据库的数据发生变化的时候,将变化情况及时的反映在控件中

TBlobStream *Stream =

new TBlobStream((TBlobField*)FDataLink->Field,bmRead);

try //创建 Blob字段的流,更妥当的方法是在创建流之前确

{ //保字段是 Blob类型,或者使用 try关键字来捕获异常

if( Stream->Size != 0 )

{ //字段的长度不为 0时,将数据载入到 Jpeg对象中

FJpg->LoadFromStream(Stream);

FBmp->Assign(FJpg); //将 Jpeg数据转换成 Bitmap数据到 FBmp中

}

else //如果数据为空,则清除内部存储位图(刷成白底)

FBmp->Canvas->FillRect(ClientRect);

Paint(); //刷新显示

} catch(...)

{

//发生异常进行适当的处理

} //在__finally组合语句中销毁 Stream更合理一些,因为

delete Stream; //在__finally中,不管是否发生异常,均会销毁 Stream

}

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::UpdateData(TObject *Sender)

{ //更新被编辑的图片数据到数据库中

TBlobStream *Stream = new TBlobStream((TBlobField*)FDataLink->Field,bmWrite);

try //首先创建 TBlobStream

{

第六章 控件设计示例 第 358 页

FJpg->Assign(FBmp); //从内部位图中获取图形数据

FJpg->Compress(); //压缩 Jpeg对象的数据

FJpg->SaveToStream(Stream); //将 Jpeg数据写入流

}

catch(...)

{ //进行异常处理

}

delete Stream; //销毁 Stream

}

//-----------------------------------------------------------------

void __fastcall TDBJpegImage::Paint(void)

{

Canvas->FillRect(ClientRect); //清除控件显示的内容

Canvas->Draw(0,0,FBmp); //将内部位图的数据显示在控件上

} void __fastcall TDBJpegImage::PasteFromCliborad(void)

{ //从剪贴板中获取图形数据

if( FDataLink->Editing ) //只有数据库处于编辑状态才作如下操作

{

TClipboard *pCB = Clipboard(); //获取剪贴板

if (pCB->HasFormat(CF_BITMAP)) //检查数据类型

{

try

{

FBmp->Assign(pCB); //获取数据

Paint(); //更新显示内容

UpdateData(this); //并更新数据库

}

catch (...)

{ //异常处理

}

}

}

}

4.3 控件详解

在这个控件中几乎没有任何新的东西,只是将 VCL类库作了一个简单的组合,但在使

第六章 控件设计示例 第 359 页

用上则会方便很多,我们还是对该控件的方法作个简单介绍。

4.3.1 TDBJpegImage

ValidCtrCheck、Register以及 CreateParams不需要介绍了,前面的控件里出现过; TDBJpegImage是 TDBJpegImage 的构造函数,在数据库控件的构造函数中,必需创

建一个 TDataLink或其派生类的对象用于和 TDataSource 对象保持连接关系,并且需

要对 TDataLink对象的相关属性进行设置,主要有 Control 属性和 OnDataChange、OnUpdateData 事 件 。 Control 用以标识 TDataLink 对象所连接的控件,

OnDataChange在数据集的数据发生变化的时候触发,提供给 TDBJpegImage 来更新界

面的显示内容,OnUpdateData 在需要更新数据时触发,提供给 TDBJpegImage 将显示

的图形保存到数据集中。这两个事件的处理函数是 TDBJpegImage 控件的关键,它们决

定了如何将数据库中的数据显示到界面上,以及如何将界面上的数据以特定的格式保存在

数据库中,OnDataChange 的处理函数为 DataChange,OnUpdateData 的处理函数为

UpDateData。 析构函数~TDBJpegImage负责将构造函数创建的对象释放掉,也不需多讲。 以 Get 和 Set开头的函数是属性的读取和设置函数,前面的控件中我们讲过,读者

自行分析。 DataChange 是反应数据集改变时触发的 OnDataChange 事件的处理函数,任意一

个数据库控件都需要有这样一个处理函数来将正确的内容显示给用户,比如 DBEdit控件

可以显示字符类型、整型数据、浮点类型数据、货币、时间等不同的类型,那么在 DBEdit的 DataChange 事件中必需能够针对字段数据的类型,进行数据到文本的转换并显示到

DBEdit中,TDBJpegImage 的数据类型为 Blob类型,在 DataChange函数中,应该

包含对数据类型的判断,并且提供异常处理机制,甚至在 DataField的设置方法中就应

该存在对字段数据类型的检测。在这个控件里我们没有这样处理,这是不完整的,因为同

一字段的数据类型运行和设计时不同是完全有可能的。按照完整性的要求,在

DataChange 函数中必需先对字段的数据类型进行判断,如果不是 Blob类型,则抛出异

常,提供给使用这进行善后的处理,否则应用程序是很危险的。 在 DataChange函数中,首先创建了一个 Blob类型的 Stream,有两种方式创建,

读者可以参考帮助文件获取详细信息。然后将 BlobStream 的数据读入到 JpegImage对象 FJpg 中,JpegImage 会自行解压,使用 TBitmap 的 Assign 方法可以将

JpegImage 解压后的位图数据复制到位图对象中,这个位图对象 FBmp 供

TDBJpegImage的绘图方法 Paint 使用,而且 FBmp不能立即释放,任何时候都要用来

存放供 TDBJpegImage 显示内容的数据。这 DataChange 函数中,我们还使用了 try关键字组合来对数据的读取尝试操作,因为 Blob类型的数据也不能保证以前存储的数据

符合 Jpeg格式,但对于这个可能的异常行为,我们本身没有作任何特殊处理。 UpdateData 是 DataChange 的反向过程,当用户需要进行数据库数据的更新时,

会触发 OnUpdateData事件,UpdateData 将被调用,要知道你所看到的计算机屏幕上

的数据,也许并不是数据库中真正的数据,只有在 UpdateData 被调用之后,这些数据

才会真正的写入到数据库中,在一个完整的 UpdateData 函数中,不仅应该具有上面提

第六章 控件设计示例 第 360 页

到的数据类型的判断和异常处理功能之外,还应该具有判断 TDBJpegImage 的数据是否

真的被改变过,这是一个非常有用的判断,因为 Jpeg数据的存储不仅数据量大,还需要

对位图数据进行压缩,系统的开销绝对不允许不考虑,可以在 TDBJpegImage 的属性中

加入一个记录编辑状态的属性,比如 IsModify等,但在这个例子中我们并没有这么作,

读者可以自行分析和修改。 在 UpdateData函数中,我们又用到了那个非常有用的方法 Assign,前面介绍 VCL

类库时,我们已经分析过 Assign 方法和 AssignTo 方法的相互关系和运行机制,

JpegImage和 Bitmap 正好是这样一个例子,当 Borland 设计 TBitmap 组件时,并没

有考虑在其后面的版本中支持 Jpg 格式,当后来设计 TjpegImage 时,同时重载了

Assign和 AssignTo方法,这样以来可以是 Bitmap和 JpegImage能够相互 Assign,当调用 JpegImage的 Assign 方法拷贝一个 Bitmap 时,是真正的使用了 JpegImage的 Assign方法,反过来使用 Bitmap的 Assign方法拷贝一个 JpegImage 时,其最终

使用的是 JpegImage的 AssignTo方法。我们的 TDBJpegImage 其实也应该考虑这个

问题,那就是是否允许用户使用 Assign方法来对 TDBJpegImage和其它的图形控件,

比如 TImage、TBitmap,甚至是剪贴板中的数据进行相互的拷贝,如果希望这样,那就

应该考虑该如何重载 Assign方法和 AssignTo方法。 Paint方法是 TDBJpegImage的绘图函数,相对于前面的例子简单多,我们这里也

不多讲,需要注意的是在将位图拷贝到控件的 Canvas之前,应当清除 Canvas 上以前的

内容。这是一个再简单不过的控件,如果希望达到更好的效果,可以考虑在 TDBJpegImage控件中加入控制属性,比如是否按照比例显示,是否居中以及是否透明等属性,在 Paint函数中根据属性不同的控制要求而选择不同的绘图效果。

PasteFromCliborad是从剪贴板粘贴图像的函数,在这个例子中,我们提供更改数据

的唯一图形就是通过 Windows的剪贴板,同样也使用了 Assign这个非常有用的拷贝函

数。对于数据库的编辑,通常的控件是这样处理的:如果控件不是 ReadOnly的,将允许

控件进行数据的编辑等操作,同时还要通报数据集发生了这个变化过程,这个控件里我们

没有这么作,读者自行考虑如何实现。

4.3.2 TDataSource

在所有数据库的组件里面,TDataSource 是一个十分重要的组件,C++ Builder的不同版本中,提供了众多的数据引擎,但是无论使用什么样的数据库引擎,和数据库控

件的关联都必需使用 TDataCource 组件来完成。数据库控件和 TDataSet、TDataLink之间的关系,相对还是比较复杂的,我们这里仅就 TDataSource 组件作一个分析,简单

介绍以下这几个组件之间完成数据库的连接所经历的过程。 TDataSet、TDataSource、TDataLink 之间具有两种关联关系,一种如下图,沿

箭头方向为多对一的对应关系:TDataSet的 FdataSource 中记录着每一个和其关联的

TDataSource 对象;TDataSource 的 FDataLink 中记录着每一个和其关联的

TDataLink对象。

第六章 控件设计示例 第 361 页

-FDataSource : TList

TDataSet

-FDataSource : TList-Control

TDataLink

-FDataSet : TDataSet-FDataLink : TList

TDataSource

图 6.5 数据库对象之间的对应关系

另一种关系如下图,沿箭头方向是一对多的对应关系,正好是上面关系的反向连接,

这两中关系共同组成了双向可追溯的相互关联关系。

-FDataSource : TList

TDataSet

-FDataSource : TList-Control

TDataLink

-FDataSet : TDataSet-FDataLink : TList

TDataSource

图 6.6 数据库对象之间的反向对应关系

从上面 TDBJpegImage 的源代码中我们已经知道,当建立一个数据库控件和数据源

连接时,只需要指定其数据源和需要使用的字段就可以,而实际上当用户设置一个

TDataLink的 DataSource 属性时,上面的连接关系自动会建立,相关的事件的消息传

递等关系也会随之建立。由于这三个对象之间的联系并不是一对一的,因此在任意一个对

象的属性被改变的时候,必需保证不能破坏原有的之前和其它同等对象建立的类似的关

系。 比如一个 DataSource 可以提供给若干个数据库控件使用,任何一个控件设置其数

据源为该 DataSource对象时,必需保持这个 DataSource 和其它数据库控件之间的关

系不受影响,而且必需保证数据库的事件能够正确的分发给每一个数据库控件。 这是 VCL组件完整性的一个必需的要求,我们这里之来看一看当一个 TDataLink对

象设置其 DataSource 属性时,它们如何建立这种包含关系。完成这个一个连接过程,

TDateSource组件中有以下几个必需的属性或成员: DataSet,用于记录 TDataSource 对象所连接的 DataSet数据集。在每个数据集

对象中包含一个 TList的对象用于记录该 DataSet 连接的每一个 TDataSource 对象,

和 TDataSource对象中的 DataSet相对应,组成一个双向可追溯的逻辑包含关系。 FDataLinks,这是一个 TList列表类型的私有成员,用于记录 TDataSource对

象连接的 TLink 对象,这个列表不是属性,而且不是公开,是提供给内部使用,并且是

由内部的过程完成设置的。在每一个 TDataLink 对象中包含一个 DataSource 属性和

FDataLinks 相对应,组成双向可追溯的逻辑包含关系。当用户设置一个 TDataLink 对

第六章 控件设计示例 第 362 页

象的 DataSource属性的时候,DataSource 的设置方法将调用被设置的 DataSource对象的 AddLinks方法,将该 TDataLink 对象添加到列表中。这个过程可以用下面的图

来表示:

ADataLink->DataSource = ADataSource

ADataLink->SetDataSource(ADataSource)

ADataSource->AddDataLink(this)

FDataLinks->Add(DataLink);DataLink->FDataSource = this;

if( FDataSource ) FDataSource->RemoveDataLink(this);if( ADataSource ) ADataSource->AddDataLink(this);

图 6.7 TDataLink和 TDataSource关系的建立过程 在这个过程中,我们可以看出 VCL对于一个属性的设置是非常谨慎而且一致的,每一

步都必需保证是有效的,而且不能对使用者提出过多的约束,这样的属性才是完整的,才

是具有高可用性的。在 TDataSource 的属性中,象 DataSet 的设置过程和 TDataLink的方式非常相似,而且过程更加严谨,因为在 TDataSet 的属性中还有其它的

TDataSource 类型的属性如:MasterSource 用于建立 Master-Detail 表索引关系,

这时候必需保证不能建立环路包含关系,就是说引用自己的 DataSource 不能作为自身

的 MasterSource索引表源,因此在 TDataSet 的相关设置方法中必需对这个条件进行

判断,不能满足条件时抛出异常,而不能将这种条件的判断交给引用者,否则这个属性也

将是不完整的。 在上面图中的这个过程中,我们再次遇到了 C++和 Object Pascal 冲突的地方,在

C++代码中 ADataSource->AddDataLink(this);

方法进行了以下操作: FDataLinks->Add(DataLink);

DataLink->FDataSource = this; //C++语言中这是非法的

我们前面讲过,FDataSource 是属性的内部存储成员变量,一定是私有成员,因此

在 DataSource 的方法中对 DataLink的私有成员的访问是不允许的,但是在 Object Pascal中,这是允许的。很多网友曾经在网上提问为何 Borland 公司一直使用 Onject Pascal 来编写 VCL,而不使用 C++来编写 VCL,我想一方面的原因就是使用 C++编写

VCL将必需改变这种保持完整性的逻辑结构,可能会导致兼容性难以保证。这也是读者在

第六章 控件设计示例 第 363 页

编写组件时需要注意的地方,上面的分析说明如何保证完整性,但是并不能一成不变的拿

到 C++ Builder中来使用,只能通过其它的方式解决这种冲突。 关于 TDataSource 组件更深入的了解。读者可以在 C++ Builder 的源码中找

DB.pas文件,参考其中代码详细分析。

5 小结

本章主要从控件的编写方面对如何使用面向状态的程序设计进行了一些示范性讲解,

其中的例子覆盖了不同方面的应用和不同的语言使用技巧,C++ Builder的博大精深是

远远超过这些的,而且作者的水平也只限于目前的层次,这些例程只是希望能给读者一个

抛砖引玉的作用。对于 C++ Builder 的使用很大程度依赖于对 VCL 的了解,本书也只

能提供给读者有限的信息。 在本章中并没有按照第三章中所讲的抽象性、封装性、约束性和完整性等特征来分析

和设计控件,一方面是因为本章的控件非常简单,主要是提供对语言掌握的一些示例,牵

扯到复杂关系的属性比较少;另一方面是正象第三章小结中所说的,对于一种理论的使用,

不能局限在这几个字眼上,而是将抽象性、封装性、约束性和完整性等特性在阅读第三章

时尽可能多的理解消化,而在使用面向状态的时候,尽可能的不要被这几个特征束缚住手

脚,这样才能灵活的运行;再还有就是对于作者胡言乱语的所谓面向状态的设计方法,在

分析过程上,包括作者目前也并没有非常完善的分析方法,因此不便在此枉加宣扬,还望

读者见谅。

第七章应用程序中的面向状态 第 365 页

第七章 应用程序中的面向状态

通过上一章对组件设计的分析,相信读者对 C++ Builder中如何使用这些新的特性

已经有了大致的了解,这一章将介绍如何在应用程序中使用面向状态的技术。 原则上讲,在应用程序中使用面向状态和在组件中使用是一样的,而且比在组件中使

用更加灵活,限制更少。作为一个组件,其源代码一般是相对完整的,具有一定的独立性

和通用性,因此在设计的时候必须全面考虑,而在应用程序中,则没有这种限制,因为你

的代码是私有的,并不必考虑太多的共性,也就是说对完整性的考虑要相对灵活的多,这

在后面的例子 Sells 中我们可以看到,很多的完整性实际上是在代码中省略掉的,就如

同在一个类中,缺省值为“0”的成员我们并不需要显式的进行初始化,而这并没有违背

完整性的要求,恰恰是利用了完整性来简化了代码。 除此之外,在应用程序中使用面向状态的方法时,比在组件中使用也要简单的多,因

为属性很多组件中的特性在应用程序中是完全用不到的,甚至是不允许使用的,比如属性

的缺省值,在组件中使用可以带来很多优点,然而在应用程序中则完全使用不上。 尽管在阅读了上一章的内容之后,可以认为已经掌握了在应用程序中使用面向状态的

技术,但这两者还是有细微的差别,本章将简单的介绍一下这两者的区别,并在后面给出

了两个使用面向状态的技术设计的小程序。

1 面向状态在应用程序和组件中的区别

可以这样认为,在应用程序中的使用面向状态,是组件中使用的面向状态的一个子集,

因为面向状态的技术本身就是从组件技术发展而来的。 简单的剖析一下,面向状态在 C++ Builder中使用的主要语言特征有一下几点: n 属性的使用 n 事件的使用 n 方法的使用 实际上,方法并不能算是在面向状态中产生的概念,读者也可以将方法看作是一般的

成员函数。只有属性和事件是面向状态中新产生的概念,而且事件可以看作是一种特殊的

属性,那么在面向状态的概念中,实际上只有属性是全新的。前面我们也讲过,属性反映

的是程序的状态,是程序状态的载体,因此掌握面向状态的技术就可以归结为对属性应用

的掌握。 接下来我们就属性和事件在应用程序和组件中不同之处作以比较。

1.1 应用程序中的属性

在第三章讲解面向状态的思想时,我们就已经举过一个例子:Player,一个播放器,

甚至是最初的 HelloWorld,都是在应用程序中使用面向状态的技术。在上一章的控件示

例中,大量的使用属性来描述一个对象的特征,相信读者已经对__property 关键字以及

第七章应用程序中的面向状态 第 366 页

如何使用这个语法有了相当透彻的了解。但在应用程序中使用属性,可能会存在以下的特

点: n 在应用程序中的属性都不是 publish类型 n 应用程序中的属性没有缺省值 n 属性必须是类的成员 除此之外,在应用程序中,属性的完整性要求和组件中也有很大的区别,在组件设计

的时候,一个对象的使用,其环境是无法确定的,对属性的引用条件也是多种多样的,而

作为组件的一个重要的对外接口,属性必须是完整的,这就是所谓完整性的要求,在上一

章的例子 TDBJpegImage中,使用了 TDataSource 和 TDataLink等组件,通过对这

两个组件的分析,可以看出组件中对完整性的要求是多么的严格。然而在应用程序中,并

不是这样,当你为应用程序中的一个对象定义属性的时候,其应用的环境已经是确定的,

如果你没有打算将这部分代码过于频繁的使用,正好可以利用完整性的要求进行反向分

析,来简化代码,却可以保证在代码简化的同时没有额外的 Bug出现。关于这一点,本书

并不给出非常系统的分析过程,因为作者自己也是仅仅依靠经验来判断,而没有非常有效

分析方法,请读者通过后面例子的学习自己掌握,或许没有作者的干扰,读者更容易另辟

蹊径,找到更为合理的分析方法。

1.1.1 没有 publish 的属性

__publish 关键字是为了组件能够在 IDE 中使用而引入的,其含义就是“发布”,

组件中只有被发布的属性和事件才可以在 IDE 的 Object Inspector 中可见。而目前

C++ Builder 并不支持在应用程序设计时创建可视属性,在其它一些伪编译的开发环境

这样作则很可能是允许的。 一般而言,设计应用程序时的属性,都会是 public 类型,很少使用 private 和

protected 类型。也正是由于这个原因,在应用程序中使用属性,将不具备可视化的特

征,不可能通过 Object Inspector 对某个属性进行设置。其实这并不重要,需要知道,

在应用程序中属性所描述的程序运行状态,是随时都在改变的,在 IDE中对属性设置一个

固定的初始值并没有多大的意义,运行时仍然会有大量的代码对其操作。

1.1.2 属性没有缺省值

这里指的缺省值不是说一个属性没有初始状态,而是说在定义一个属性的时候,不能

使用类似组件设计时为属性指定缺省值。细心的读者都会发现,在 __property 关键字中

的 default 部分实际上并没有真真正正的为属性进行缺省值的初始化工作,这个工作是

在对象的构造函数中进行的,这只是为 IDE提供了判断是否将属性保存在资源中的一个依

据。在 C++ Builder中,一个组件的属性,只有处于 publish域中才可能被存储在资

源中,而应用程序的属性没有 publish的,也因此不能为其指定 default值。 与此类似的,诸如 stored也是为 publish 属性提供的,在应用程序中使用属性,

同样也不能为其指定 stored部分。而 index、read、write 等这些部分则在组件和应

第七章应用程序中的面向状态 第 367 页

用程序中都是可以使用的,用法也是一样的。

1.1.3 属性必须是类成员

属性必须是类的成员,这一点和组件中使用属性是一样的,这是由于 C++ Builder语言所限定的。作为属性描述一个对象的状态,也理所当然的应该是对象的属性,从这一

方面理解,属性自然而然的应当是类的成员。 在 C++ Builder中,并没有严格限制属性必须是 TObject派生类的成员,只要是

符合 C++语法规则的类,都可以为其定义属性,而 VCL 组件必须是从 TCommponent 派

生而来的,这也是两者的不同之处。C++语言支持多继承,因此如果应用程序中使用了自

定义的类,并且是纯 C++语言的类,那么在为其定义的属性也将具备 C++语言的多继承特

性,这和组件是大不一样的。由于属性实际上是由类成员和相应的读写函数组合而成的,

因此属性的多继承性最终是取决于类成员和读写函数的多继承性。然而这种情况是非常少

的,因为既然使用了 C++ Builder,VCL类库自然是使用最为频繁的类库,而且 VCL 也

相当齐全,基本上不需要用户重新从头定义一套自己的类库。

1.2 应用程序中的事件

从语言的角度来看,事件并不是一个新的事物,事件的类型通常是函数指针,这在 C语言里就已经存在,但是到了 C++ Builder之中,对这种函数指针进行了一些限制后,

应用在了组件技术中,使事件已经不仅仅是一个函数指针。 在组件里面,事件的类型通常都是__closure,而且事件通常都会以属性的形式出

现在声明的头文件中,但在应用程序中对这些要求都要宽松的多,因此对事件的使用也要

灵活的多。简单的说,事件在应用程序中和在组件中相比有以下的特点: n 程序中的事件不一定非得是 property和 closure n 程序中定义的事件通常都是需要根据状态更换处理句柄的

1.2.1 不是 property 的事件

在组件的设计中,所有的事件都是 proeprty,这是因为只有属性才可以实现设计时

在 Object Inspector 中设置,但是所有的事件基本上都是直接读写的属性,没有设置

函数和读取函数,也没有更新界面或者引发其它动作,因此在应用程序中定义事件并不一

定要定义成 property,这是和组件所不同的。 另外就是组件中使用__closure 类型,是因为在使用一个组件的事件时,为事件指

定的处理函数不是组件自己的,而在应用程序中为一个对象的事件指定的处理句柄,很可

能就是该对象自身的成员函数,因此不一定非要指定事件为__closure类型。 总的来说,定义事件在应用程序中比组件中要宽松的多,也灵活的多,但是事件体现

着状态的变化这个规律应该是遵循的,在后面的例子 Sells 中我们就使用了事件来载入

系统设置模块和报表模块,其类型就是一个自定义的非__closure类型。 在组件设计中,定义一个事件,通常必须使用一个 TObject 类型的参数 Sender,

第七章应用程序中的面向状态 第 368 页

这是因为在组件的事件中,处理函数往往不是自身的成员函数,因此需要使用 Sender 来

区分触发这个事件的对象,尽管在实际使用,用到 Sender的机会并不是很多,但有时候

是必须使用的,比如在函数中存在有对事件的触发者的操作(参见后面的例子 Drawing),那么 Sender则是必须的,尤其是在多个组件中使用同一个处理函数时。在应用程序的设

计中,这个参数并不是必须的,因为一个事件的处理函数很可能就是自身的成员函数。

1.2.2 事件多为经常更换处理函数句柄

事件在组件中的应用,是为了提供给外界一个处理的接口,而在应用程序中使用事件,

则并不是为了提供一个接口,因为这种接口的必要性在应用程序中并不存在。设计组件的

时候,必须考虑允许用户在某种情况下对组件的状态改变而作出反应,这就需要用到事件,

而在应用程序中,并不需要提供额外的接口,因为调用方和响应方都是你自己正在编辑的

代码,直接调用就可以了。 尽管如此,我们还是要在应用程序中使用到事件,这种情况多出现在调用的函数具有

不确定性,或者需要根据程序的不同状态进行更换。这就是我们之前所讲到面向状态中另

一个非常重要的思想:“根据不同的螺丝来更换不同的扳手”。 实际在组件的事件中,根据不同的状态更换事件的处理函数句柄,也是允许的,而且

也是非常有效的一种方法,只是大多数事件是不需要这样作的,这是一个习惯问题也是一

个处理思想的问题,大多数程序员在使用 C++ Builder的时候,仍然习惯于运行时不经

常改变在 IDE中设置的属性值,这实际上已经束缚住了设计者的手脚,希望有这个习惯的

读者可以通过后面这个例子的示范,解放自己的手脚。

2 示范程序 Sells

这是作者第一次将组件设计技术用到应用程序中的一段代码,也正是这一个小小的程

序才使作者最终产生了撰写这一本书的想法。因此作者将这个程序作为例程示范的第一

个,尽管距离敲入这段代码的第一个字符已经过去了好几年,C++ Builder的版本也从

4到 5再发展到了最新的版本 6,关于面向状态的思想也和最初编写这段代码时所想的大

相径庭,但是作者还是决定将几年前的代码一刀未剪的提供给读者,因为这段代码实际上

体现了作者如何从被动使用到主动掌握的一个过程。

2.1 程序功能的介绍

首先说明,这并不是一段优秀的代码,也非常短,编写这段代码的初衷是一位朋友想

获取一个适合于单位或者企业餐厅使用的餐饮销售系统,要求非常简单,系统使用的是 P C或者第三代 POS机,仅允许使用磁卡消费,而不支持现金消费,在这个例子中磁卡刷卡器

采用了最常用的键盘接口,本书的光盘中给出了所有的源代码,包括对对这个程序编制的

帮助文件,这里不再对程序的需求进行过多的描述,读者参考光盘内容。下面对这个程序

所实现的功能和结构作一个介绍。

第七章应用程序中的面向状态 第 369 页

这个程序中包括了三个主要的功能模块: n 销售管理系统 n 系统设置 n 报表系统 系统设置部分和报表我们采用了外挂的动态链接库来完成,在主程序中并不包含,本

书中我们仅对销售管理部分作详细的介绍,系统设置部分光盘中给出了源代码,有兴趣的

读者自己阅读。接下来我们对这个程序的销售管理部分的功能作个简单介绍,有助于读者

阅读后面的代码。

2.1.1 数据库表集

完成这一个销售的过程,至少有四个表集是必须的,他们分别是: n 记录用户(消费者)信息的表集 Customer n 记录商品信息的表集 Food n 记录订单的表集 Sell n 记录每个订单商品列表的表集 SellDetail 除了这四个表集以外,程序还使用了用于操作员信息存储的表集 System_User,这

几个表集的关系如图 7.1所示:

在 Sells 中并不是全部的字段都被使用了,其数据的类型基本可以根据名称进行判

Sell

PK Sells_No

DateTimeUser_NameCard_NoTotal_Price

FK1 User_ID

Food

PK Food_No

PriceFood_NameFoodKind

Customer

PK User_ID

No_CardPhotoUserTpyeCatchPriceCustomer_NameMoneyMemo

Sell_Detail

FK1,I1 Sells_NoFK2 Food_No

Food_NameFood_CountFood_PriceTotal_Price

System_User

User_NameRightsPhotoPassWordSysUser_ID

图 7.1 Sells中数据库的结构

第七章应用程序中的面向状态 第 370 页

断,除了 PassWord使用了 Blob类型之外,其它的字段基本都可以从名称看出来,或者

读者可以从源码中获取详细信息,这里不再过多讲解。

2.1.2 程序第一次运行

在这个程序中,数据库使用的 Paradox,而且有一些设置都是保存在注册表里面的,

数据库的配置信息也是保存在注册表中的,当程序运行的时候,会从注册表中读取相应的

配置,并创建数据库连接组件 TDataBase对象。如果系统中没有正确的配置数据库,或

者还没有配置数据库信息,程序在运行是会自动打开数据库配置对话框,如图 7.2所示: 用 户 选 择 该 程 序 使 用 的

Paradox数据库的存储路径,确定

后程序对路径的合法性进行验证,

若配置正确则保存信息进入主界

面,设置的路径不合法则提示用户;

若用户点击取消,程序将关闭;这

个例子中没有提供该对话框的帮助

内容。 程序第一次运行的时候,其操

作员的数据集是空的,用户登录可

以采用缺省的用户:“ADM”和缺省

的用户密码:“ADM”登录,登录后

可对系统进

行配置,系

统配置的界

面如图 7.3所示:

可以在

系统维护窗

体来配置用

户信息(如

上界面)、操

作员信息、

商 品 信 息

等,当操作

员的数据库

不为空的时

候,缺省用

户和缺省密

码将自动失

图 7.2 Sells 第一次运行的界面

图 7.3 Sells系统设置界面

第七章应用程序中的面向状态 第 371 页

图 7.5 Sells 的登录窗口

效,不能再次用来登录。 系统维护模块是通过动态链接库来实现的,本书中不再讲述这部分内容,光盘中有全

部的代码。

2.1.3 程序主界面

程序启动后的界面如图 7.4所示:

中央的矩形窗体为启动画面 Splash,启动画面中包含一个图片和一个动画效果,图

片是设计在代码中的,动画效果是从硬盘载入的文件。主界面中 6个功能按钮和用户操作

菜单中的 6个菜单项一一对应,其中只“用户登录”、“帮助”和“退出系统”3个按钮有

效,其它均为无效状态,当用户正确登录以后,其它

的按钮才变为有效状态。 用户登录窗体非常简单,和一般程序的登录窗体

没有什么两样如图 7.5所示:

2.1.4 销售管理系统

进入销售管理系统后界面如下图 7.6:

图 7.4 Sells 的程序主界面

第七章应用程序中的面向状态 第 372 页

左边显示的是可供选择的食物列表,右边上面部分是每一次交易的详细商品列表,中

间部分为客户的相关信息如照片、预存款金额、本次消费总额等,下面是信息提示栏,提

示当前可进行的操作。 这个程序运行的硬件环境虽然安装了鼠标,但是所有销售的操作都是通过键盘操作来

完成的,而且是通过代有磁卡刷卡器的小型键盘。当进入销售管理状态后,程序处于等候

刷卡状态,刷卡的信息是通过键盘接口输入到计算机的,当客户的磁卡刷卡成功时,程序

会将用户的相关信息显示在界面上,如上图;这时提示框中提示输入餐号,操作员根据左

边商品信息选择购买的商品序号,当用户键入“+”或者“*”时,程序认为商品序号输入

完毕,商品的缺省数量为 1;当操作员键入“+”时,程序认为用户将要继续购买其它商

品,继续同上操作输入商品序号;当操作员键入“*”,程序认为用户更改当前输入商品的

数量,程序转入接收商品数量状态,数量的精确度为 0.1个单位,在修改商品数量后,操

作员可以键入“+”继续添加商品;购买完毕时,键入回车则完成订单,任意时刻按 ESC键可以取消订单,任意时刻均可以通过退格健或者是“-”来删除当前的商品。整个操作

如同计算器一样使用,如用户购买半份米饭和一份炒菜,在刷卡后键入: 1*0.5+2 然后回车即可。详细的使用说明参见本例程的帮助文件。

图 7.6 Sells 进入销售状态的界面

第七章应用程序中的面向状态 第 373 页

2.2 状态的分析

在这个例子的设计过程中,确实是使用了本书所要介绍的“面向状态”的技术,就是

将组件设计中的技术应用到了应用程序中,但是在设计的时候,作者并没有从头开始就使

用了状态的分析方法,似乎没有经过分析过程,一方面是因为程序非常简单,几乎不用特

别的对状态进行分析,另一方面是因为采用这种技术对逻辑关系处理要简单的多。但在介

绍例子之前,我们还是对程序中的状态进行一个大概分析,确切的讲应该是设计后的一个

总结。 n IsLogin 属性,记录是否处于登录状态。在这个程序中层次最高的状态应该是

登录和非登录状态,在程序运行后只有登录后才可以进行其它的操作,否则任何

工作都不能进行,而且登录状态对其它的状态具有最高的约束力。与登录状态相

关的其它组件有 Button 的 Enabled,MenuItem 的 Enabled,Button 和

MemuItem的 Caption 等属性,甚至包括了登录用户的操作权限(例程中并没

有作关于用户权限设置的代码,但是作了这方面的考虑)等。因此 bool类型的

IsLogin属性代表了整个程序最高层次的状态。 n IsSelling 属性,标记是否处于销售状态。从程序状态的层次来讲,紧接者登

录状态的应该是处于那个功能模块,在 Sells 中,报表模块和系统设置模块是

通过动态链接库载入的,而且是调用了模态对话框实现的,因此并没有为每一个

模块定义相应的状态,而只对销售状态定义了一个属性 IsSelling,bool类型

的属性 IsSelling为真的必要条件是程序必须已经登录(并且登录用户具有相

应的权限),受到 IsSelling状态约束的其它属性有,相关数据集的 Active、销售界面元素的 Visible等。

n Status属性,用于记录在销售状态的键盘处理的状态,这个属性的命名并没有

非常明确的含义,这时由于作者在设计的时候考虑不够充分而造成的。实际上使

用 SellingStatus 可能会更确切一些。根据前面的介绍,这个属性总共有 3种状态:等候刷卡、等候商品序号、等候商品数量,例子中为 Status 属性预定

义了四个状态,其中_NONE_SELLING和 IsSelling 实质上是重复的,程序中

并没有使用。受到 Status约束的有操作提示信息等界面元素,主要是在不同的

Status状态中,使用不同的子键盘处理函数来进行键盘消息的处理。 n SubKeyProc 键盘处理事件,这是一个类似事件的函数处理句柄,受到

IsSelling和 Status 的约束。在销售状态的时候,程序只支持键盘方式操作,

鼠标不能进行销售,因此在程序中使用了 Application 的 OnMessage 事件来

接收程序的所有 Windows消息并进行的预处理,然后根据不同 Status状态,

会将预处理过的键盘消息送给子键盘处理函数进行处理。SubKeyProc 总共有三

个不同的处理函数,分别对应等候刷卡、接收商品、改变商品数量这三个状态,

这三个函数是本程序的核心代码,所有销售相关的数据处理都是在这三个函数中

完成的。 除了上面几个关键的状态属性之外,程序中大部分组件的属性也与程序的状态建立了

相互约束和关联的关系,这和一般程序中使用属性没有区别,这里不再一一介绍。

第七章应用程序中的面向状态 第 374 页

2.3 实现代码及介绍

实现整个程序总共使用了 4个 Form单元、一个数据库模块和一个辅助单元 Extend,Extend单元是将 Main单元中不能可视化编辑的函数提取出来放在一起的。其它单元各

有用途,我们分别介绍。

2.3.1 Main 主单元

主单元是整个程序的主界面,设计时 Form如下图:

界面上的元素并不所有的 Visible都是 true;而是根据运行时的状态;来决定那些

元素是可视的,那些不是可视的。 这个 Form的 dfm文件特别庞大,这里不再给出,读者可以参考光盘中的源码。在主

Form中包含了一些非可视化的组件,供数据库等组件使用,读者可以参考下图理解: 这是在 C++ Builder 6 中生成的类――属性关系图,其中符号的含义参考 C++

Builder 6的帮助内容。

图 7.7 Sells设计时的主窗体

第七章应用程序中的面向状态 第 375 页

Main单元的头文件如下:

图 7.8 Sells 的主窗体中组件的相对关系

第七章应用程序中的面向状态 第 376 页

#ifndef MainH

#define MainH

#define _WAIT_CARD 0x01 //定义状态_WAIT_CARD的标识符

#define _WAIT_FOOD_NO 0x02 //状态_WAIT_FOOD_NO的标示符标识符

#define _WAIT_FOOD_COUNT 0x04 //状态_WAIT_FOOD_COUNT的标识符

#define _NONE_SELLING 0x08 //状态_NONE_SELLING的标识符

//------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <ExtCtrls.hpp>

#include <Graphics.hpp>

#include <Buttons.hpp>

#include <Db.hpp>

#include <DBTables.hpp>

#include <Menus.hpp>

#include <ComCtrls.hpp>

#include <DBGrids.hpp>

#include <Grids.hpp>

#include <Mask.hpp>

#include <DBCtrls.hpp>

#include "DataBase.h" //数据库单元

typedef void __fastcall (__closure *TKeyProc)(Word &Key);

//用于键盘消息处理的 Closure类型;

typedef void __stdcall (*TExtProc)(HWND,AnsiString);

//用于挂入动态连接库的 Proc类型,供对话框使用时,传递一个窗口句柄,和数据库的路径

//------------------------------------------------------------------

class TForm1 : public TForm

{

__published:

TImage *Image1; //显示背景图片的 Image

TMainMenu *MainMenu1; //主窗体菜单

TMenuItem *N1; //主菜单项

TMenuItem *N2;

TMenuItem *N3;

TMenuItem *N4;

第七章应用程序中的面向状态 第 377 页

TMenuItem *N5;

TMenuItem *N6;

TMenuItem *N7;

TMenuItem *N8;

TMenuItem *N9;

TMenuItem *N10;

TMenuItem *N12;

TPanel *Panel1;

TPanel *Panel2;

TPanel *Panel3;

TStatusBar *StatusBar1;

TTimer *Timer1;

TEdit *Edit1;

TDBText *DBText1;

TDBText *DBText2;

TLabel *Label1;

TLabel *Label2;

TDBGrid *DBGrid1;

TDBGrid *DBGrid2;

TDBImage *DBImage1;

TSplitter *Splitter1;

TSpeedButton *SpeedButton1;

TSpeedButton *SpeedButton2;

TSpeedButton *SpeedButton3;

TSpeedButton *SpeedButton4;

TSpeedButton *SpeedButton5;

TSpeedButton *SpeedButton6;

TSpeedButton *SpeedButton7;

TBevel *Bevel1;

void __fastcall Splitter1CanResize(TObject *Sender, int &NewSize,

bool &Accept);

void __fastcall FormCreate(TObject *Sender);

void __fastcall Timer1Timer(TObject *Sender);

void __fastcall FormClose(TObject *Sender, TCloseAction &Action);

void __fastcall SpeedButton1Click(TObject *Sender);

void __fastcall SpeedButton2Click(TObject *Sender);

void __fastcall SpeedButton5Click(TObject *Sender);

void __fastcall SpeedButton6Click(TObject *Sender);

void __fastcall SpeedButton4Click(TObject *Sender);

第七章应用程序中的面向状态 第 378 页

void __fastcall SpeedButton3Click(TObject *Sender);

void __fastcall SpeedButton7Click(TObject *Sender);

void __fastcall FormCanResize(TObject *Sender, int &NewWidth,

int &NewHeight, bool &Resize);

void __fastcall N10Click(TObject *Sender);

private:

int FStatus; //程序运行状态的内部数据

bool FIsLogIn; //记录登录状态的内部数据

bool FIsSelling; //销售状态的内部数据

AnsiString FFoodNo; //提供内部使用的键盘字符串缓存,记录商品序号

AnsiString FFoodCount; //提供内部使用的键盘缓存,记录商品数量

TKeyProc FSubKeyProc; //子键盘处理事件的内部存储数据

TExtProc FSysManProc; //外挂系统设置的入口指针

TExtProc FReportProc; //外挂报表系统的入口指针

public:

Currency TotalPriceBuf; //用于记录当前合计商品价格

Currency LeftMoney; //记录磁卡剩余金额,

int SerialNo; //用于记录交易的系统流水号

AnsiString CardNoBuf; //刷卡信息的内部缓存

//Status属性用于记录程序的运行状态,_WAIT_CARD、_WAIT_FOOD_NO

//以及 _WAIT_FOOD_COUNT三个状态。

__property int Status = {read = FStatus,write = SetStatus};

//记录是否为登录状态的属性

__property bool IsLogIn = {read= FIsLogIn,write = SetLogIn};

//IsSelling记录是否处于销售状态

__property bool IsSelling = {read = FIsSelling,write = SetSelling};

//FoodNO用于记录用户键入的商品序号

__property AnsiString FoodNo = {read = FFoodNo,write = SetFoodNo};

//FoodCount用于记录用户键入的商品数量

__property AnsiString FoodCount = {read = FFoodCount,write =

SetFoodCount}; //SubKeyProc在程序的键盘消息处理触发的子键盘处理事件

__property TKeyProc SubKeyProc = {read = FSubKeyProc,write =

FSubKeyProc};

//调用系统设置的函数指针

__property TExtProc SysManProc = {read = FSysManProc,write =

FSysManProc};

//调用报表系统的函数指针

__property TExtProc ReportProc = {read = FReportProc,write =

第七章应用程序中的面向状态 第 379 页

FReportProc}; __fastcall TForm1(TComponent* Owner); //构造函数

void __fastcall SetStatus( int Value ); //Status的设置函数

void __fastcall SetLogIn( bool Value ); //Login的设置函数

void __fastcall SetSelling( bool Value ); //Selling的设置函数

void __fastcall SetFoodNo( AnsiString Value ); //FoodNo的设置函数

void __fastcall SetFoodCount( AnsiString Value );

//FoodCount的设置函数

//主键盘处理函数,当程序出在 Selling状态时,下面函数为 OnMessage的处理句柄

void __fastcall MyKeyProc( tagMSG &Msg, bool &Handled );

//用于接收磁卡输入的键盘子处理函数

void __fastcall WaitCardKeyProc(WORD &Key);

//用于接收输入商品序号的键盘子处理函数

void __fastcall WaitFoodNoProc(WORD &Key);

//用于接收输入商品数量的键盘子处理函数

void __fastcall WaitFoodCountProc(WORD &Key);

};

extern PACKAGE TForm1 *Form1;

#endif

上面这段代码中,定义了两个事件类型,一个用于载入的报表模块和系统设置模块,

另一个是用于核心代码的子键盘处理函数。在程序的 Status属性的设置函数中,不同的

状态值在更新界面显示的同时,也更换相应的子键盘处理函数,因此在 Application 的

OnMessage 事件的处理函数中并不需要根据当前状态来选择将要调用的键盘子处理函

数,而只是简单的触发 SubKeyProc事件就可以了。 下面是 Main单元的实现代码: #include <vcl.h>

#pragma hdrstop

#include "Main.h" //主单元的头文件

#include "Pass.h" //密码验证单元的头文件

#include "Path.h" //路径设置单元的头文件

#include "jpeg.hpp" //C++ Builder中 Jpeg文件的处理单元

#include "Splash.h" //启动画面的头文件

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm1 *Form1;

//------------------------------------------------------------------

第七章应用程序中的面向状态 第 380 页

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

Form7 = new TForm7(NULL); //在主窗体创建之前,显示 Splash界面

Form7->Refresh(); //必须更新方能显示。

} //------------------------------------------------------------------

void __fastcall TForm1::FormCreate(TObject *Sender)

{ //在 Form1创建时,根据注册表来加载响应的系统设置模块和报表模块

TRegistry *Reg = new TRegistry;

AnsiString ReportFile; //报表模块的文件名

AnsiString SysManFile; //系统设置模块的文件名

Panel2->Top = 10; //Panel2在设计时位置偏离运行时需要的位置

Panel2->Left = 8;

SubKeyProc = NULL; //设置子键盘处理句柄为空

try //打开 PlugIn子键

{

Reg->RootKey = HKEY_LOCAL_MACHINE; //根键

if ( Reg->OpenKey("\\Software\\Lewolf\\PlugIn",false) )

{

ReportFile = Reg->ReadString("Reports"); //读取报表模块文件名

SysManFile = Reg->ReadString("SysMan"); //读取系统设置模块文件名

Reg->CloseKey();

}

if( ReportFile == "" ) //设置为空时,按照确省文件加载

ReportFile = "Reports.dll"; //缺省的报表模块文件名

if( SysManFile == "" ) //设置为空时,按照确省文件加载

SysManFile = "SysMan.dll"; //缺省的系统设置模块文件名

//加载系统设置模块并导入入口函数,入口函数为"Manager"

SysManProc = (TExtProc)GetProcAddress(

LoadLibrary(SysManFile.c_str()),"Manager"); //加载报表模块并导入入口函数 ,入口函数为"Reports"

ReportProc = (TExtProc)GetProcAddress(

LoadLibrary(ReportFile.c_str()),"Reports");

//Reg->RootKey = HKEY_LOCAL_MACHINE;

//根据注册表设置加载主界面的背景图片

if ( Reg->OpenKey("\\Software\\Lewolf\\BackGround",false) )

{

AnsiString FileName = Reg->ReadString("MainPicture");

第七章应用程序中的面向状态 第 381 页

Image1->Picture->LoadFromFile( FileName );

Reg->CloseKey();

}

else //未设置背景键值时,按照确省的图片加载

Image1->Picture->LoadFromFile( "..\\Images\\Main.jpg" );

} catch(...)

{ /*有异常时忽略,但需要释放资源*/

delete Reg;

} //还可以使用 finally关键字来处理 Reg的资源释放

delete Reg;

}

//------------------------------------------------------------------

void __fastcall TForm1::Splitter1CanResize(TObject *Sender, int &NewSize,

bool &Accept)

{ //Splitter1是一个多余的控件,最早设计时考虑要作界面分割而保留该控件。

Accept = false; //这个界面中,不允许改变 Splitter的分割区域

}

//------------------------------------------------------------------

void __fastcall TForm1::Timer1Timer(TObject *Sender)

{ //定时器每隔一秒钟刷新一次当前时间的显示

StatusBar1->Panels->Items[6]->Text =

TDateTime::CurrentDateTime().TimeString();

}

//------------------------------------------------------------------

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)

{

Status = _NONE_SELLING; //关闭前必须保证没有处于销售状态

}

//------------------------------------------------------------------

void __fastcall TForm1::SpeedButton1Click(TObject *Sender)

{ //登录按钮和登录菜单共用的事件句柄

if( !IsLogIn ) //"登录"或"退出登录"按钮按下时,

Form2->Visible = true; //等同于相应菜单的操作,登录成功的话,IsLogin

else //属性的状态在 Form2中更改

IsLogIn = false;

}

//------------------------------------------------------------------

void __fastcall TForm1::SpeedButton2Click(TObject *Sender)

第七章应用程序中的面向状态 第 382 页

{ //SpeedButton2为调入系统设置的按钮

if(SysManProc) //系统设置按钮或菜单响应的函数

SysManProc(Application->Handle,DM->Sell->Directory );

else

ShowMessage("系统管理单元没有载入!");

} //------------------------------------------------------------------

void __fastcall TForm1::SpeedButton7Click(TObject *Sender)

{ //SpeedButton7为调入报表的按钮

if(ReportProc)

ReportProc(Application->Handle,DM->Sell->Directory );

else

ShowMessage("报表单元没有载入!");

}

//------------------------------------------------------------------

void __fastcall TForm1::SpeedButton5Click(TObject *Sender)

{ //SpeedButton5为退出销售系统的按钮,

IsSelling = false; //将程序状态设置为非销售状态

} //------------------------------------------------------------------

void __fastcall TForm1::SpeedButton6Click(TObject *Sender)

{ //SpeddButton6为打开销售系统

IsSelling = true; //将程序的状态设置为销售状态

}

//------------------------------------------------------------------

void __fastcall TForm1::SetFoodNo( AnsiString Value )

{ //FoodNo的设置函数,这里没有额外的处理

FFoodNo = Value;

}

//------------------------------------------------------------------

void __fastcall TForm1::SetFoodCount( AnsiString Value )

{ //FoodCount的设置函数,这里没有额外的处理

FFoodCount = Value;

}

//------------------------------------------------------------------

void __fastcall TForm1::SpeedButton4Click(TObject *Sender)

第七章应用程序中的面向状态 第 383 页

{ //SpeedButton为退出程序的按钮

Close();

}

//------------------------------------------------------------------

void __fastcall TForm1::SpeedButton3Click(TObject *Sender)

{ //SpeedButton3为帮助按钮

Application->HelpCommand(HELP_CONTENTS, 0);

}

//------------------------------------------------------------------

void __fastcall TForm1::FormCanResize(TObject *Sender, int &NewWidth,

int &NewHeight, bool &Resize)

{ //阻止 Form1的大小改变

Resize = false;

}

//------------------------------------------------------------------

void __fastcall TForm1::N10Click(TObject *Sender)

{ //N10菜单为"关于"菜单 ,显示 Splash窗口

Form7->Show();

}

以上代码都是作为运行时的流程代码,即决定了什么样的条件下要干什么,是和用户

的交互过程,基本上没有涉及到程序内部的象的状态之间的约束关系,这些代码都是比较

简单的,决定内部逻辑关系的代码我们放在了扩充单元 Extend.cpp中,在这个单元中,

包括了核心的键盘处理代码,以及主要属性的设置函数,这些代码一般设计成功之后变化

不会太大,除非改变程序的功能。

2.3.2 Extend 单元

在这个单元中包含了一个消息处理函数,用于 Application 的 OnMessage事件;

三个子键盘处理函数;分别为销售状态的三个子状态键盘处理函数;IsLogin 的设置函

数;IsSelling的设置函数;Status的设置函数。 其中消息处理函数和子键盘处理函数实际上是整个程序功能实现的核心代码,但这三

个函数的内容和处理方法和我们要讲述的面向状态并没有特别大的关系,只是在每个处理

函数中都包含了对状态的设置。读者只要仔细分析不难理解,这里也不给出详细的注解说

明,代码如下: #include <vcl.h>

#pragma hdrstop

第七章应用程序中的面向状态 第 384 页

#include "Main.h" //主单元头文件

#include "DataBase.h" //数据库单元的头文件

#pragma package(smart_init)

//------------------------------------------------------------------

void __fastcall TForm1::MyKeyProc( tagMSG &Msg, bool &Handled )

{ //这是程序在 Selling 状态时 Application的 OnMessage事件的处理函数

if( Msg.message == WM_KEYDOWN )

{ //只获取键盘按下的消息

WORD Key;

Key = Msg.wParam;

switch (Msg.wParam) //下面代码将键盘扫描码转换为 Ascii码,

{ //由于 Windows98和 Winnt以及 Windows 2000的

case VK_NUMPAD0: //键盘消息有所不通,这个键盘处理函数将主要完成

Key = '0'; //不同操作系统的兼容性问题,

break; //同时,由于这个程序运行的计算机只安装着带有磁卡刷

case VK_NUMPAD1: //卡器的小型键盘,这个函数还要解决键盘键的映射问题

Key = '1'; //关于刷卡器键盘的映射参考 Sells的帮助文件

break;

case VK_NUMPAD2:

Key = '2';

break;

case VK_NUMPAD3:

Key = '3';

break;

case VK_NUMPAD4:

Key = '4'; break;

case VK_NUMPAD5:

Key = '5';

break;

case VK_NUMPAD6:

Key = '6';

break;

case VK_NUMPAD7:

Key = '7';

break;

case VK_NUMPAD8:

Key = '8';

break;

第七章应用程序中的面向状态 第 385 页

case VK_NUMPAD9:

Key = '9';

break;

case VK_F2: //以下几个 Case中是将小键盘中的功能键映射为

Key = '*'; //所需要的 Ascii字符编码

break; //关于详细的映射关系,在本书的原代码中找到该例

case VK_F3: //子的帮助文件,帮助文件中有详细的解释

Key = '-';

break;

case VK_F4:

Key = '+'; break;

case VK_MULTIPLY:

Key = '*';

break;

case VK_ADD:

Key = '+';

break;

case VK_SUBTRACT:

Key = '-';

break;

case VK_DECIMAL:

Key = '.';

break;

}

if(SubKeyProc) //将处理后的键盘信息,以 Ascii码

SubKeyProc(Key); //的形式交给子键盘处理函数

}

}

//------------------------------------------------------------------

void __fastcall TForm1::WaitCardKeyProc(WORD &Key)

{ //等候刷卡状态的键盘子处理函数,

switch (Key)

{ //当接收到回车键(刷卡器可以设置为以回车键结尾)的时候,

case VK_RETURN: //认为刷卡结束

if( DM->Customer_Table->Locate("No_Card",Variant( CardNoBuf ),

TLocateOptions()<< loCaseInsensitive ))

{ //在用户信息表中匹配该卡号的用户,以下是该用户合法时的代码

第七章应用程序中的面向状态 第 386 页

Status = _WAIT_FOOD_NO; //将程序的状态设置为等候输入商品序号

StatusBar1->Panels->Items[1]->Text =

DM->Customer_Table->FieldValues["Customer_Name"];

//更新相应的 StatusBar信息

DM->Sell_Table->Append(); //增加新的销售记录

DM->Sell_TableSells_No->Value = SerialNo + 1;//设置销售流水号

DM->Sell_TableDate->Value = TDateTime::CurrentDate();

//记录交易日期

DM->Sell_TableTime->Value = TDateTime::CurrentTime();

//记录交易时间

DM->Sell_TableCard_No->Value = CardNoBuf; //记录交易卡号

DM->Sell_TableUser_Name->Value = //记录交易人姓名

DM->Customer_Table->FieldValues["Customer_Name"];

DM->Sell_TableUser_ID->Value =

DM->Customer_Table->FieldValues["User_ID"];

DM->SellDetail_Table->Append(); //增加商品列表记录

DM->Sells_No->Value = DM->Sell_TableSells_No->Value;

//记录商品列表的订单编号

CardNoBuf = ""; //清空刷卡信息缓存

TotalPriceBuf = 0; //设置商品总交易额为 0

DM->Customer_Table->Edit(); //将客户信息表置为可编辑,因为

} //销售后需要更新客户的预存款金额

else

{

CardNoBuf = ""; //卡号无效时,清空缓存

} break; //取消时,清空缓存

case VK_ESCAPE:

CardNoBuf = "";

break;

default : //确省处理,即 Key为有效的 Ascii字符码时

CardNoBuf = CardNoBuf + (char)Key ; //将该字符续在卡号缓存的最后

}

}

//------------------------------------------------------------------

void __fastcall TForm1::WaitFoodNoProc(WORD &Key)

{ //该函数为等候输入商品序号的状态时,键盘的子处理函数

if( Key >= '0' && Key <= '9' ) //有效输入 Ascii字符为 0~9,

{ //因为序号为整型数据

第七章应用程序中的面向状态 第 387 页

AnsiString Temp; //临时变量,存放新键入的商品序号

Temp = FoodNo + (char)Key;

//根据接收到的商品序号,查找相应的商品信息,

//根据商品信息填写商品列表的相应的字段

//并且计算总价,以及客户剩余金额

if( DM->Food_Table->Locate("Food_No",

Variant(StrToInt( Temp )),TLocateOptions()<<

loCaseInsensitive) )

{

DM->Food_Price->Value = DM->Food_Table->FieldValues["Price"];

DM->Food_Name->Value = DM->Food_Table->FieldValues["Food_Name"]; DM->Food_Count->Value = 1; //确省情况下,商品数量为 1

DM->Food_No->Value = DM->Food_Table->FieldValues["Food_No"];

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->SellDetail_TableTotal_Price->Value = DM->Food_Count->Value *

DM->Food_Price->Value;

TotalPriceBuf = TotalPriceBuf +

DM->SellDetail_TableTotal_Price->Value;

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

FoodNo = Temp; //若商品有效时,则更新商品编号缓存,

} //否则保持不变,忽略用户键入的字符

}

else if( Key == VK_ESCAPE )

{ //用户按 ESC键,则取消本次交易,并删除本次交易的商品列表

//恢复程序状态到等候刷卡状态

DM->SellDetail_Table->Cancel();

DM->SellDetail_Table->Last();

for( ; !DM->SellDetail_Table->Bof ;DM->SellDetail_Table->Delete());

DM->Sell_Table->Cancel();

DM->Customer_Table->Cancel();

FoodNo = "";

Status = _WAIT_CARD; //设置状态到等候刷卡状态

第七章应用程序中的面向状态 第 388 页

} else if( Key == VK_RETURN )

{ //用户按回车键,结束本次交易,并将程序状态设置到

//等候刷卡状态,继续下一个交易

if( FoodNo == "" && DM->SellDetail_Table->RecordCount == 0)

return; //商品列表为空时,不能结束交易,只能取消交易

TUpdateKind UpdtKind;

DM->SellDetail_Table->Post();

DM->Sell_Table->Post();

DM->Customer_Table->Post();

SerialNo = SerialNo + 1; Status = _WAIT_CARD; //设置到等候刷卡状态

FoodNo = ""; //将 FoodNo商品序号清空

}

else if( Key == VK_BACK || Key == '-' )

{ //当用户安下退格键或者减号(小键盘的支持),时删除商品列表中

//当前的商品,并重新计算商品总价和用户余额

if( DM->SellDetail_Table->Modified && FoodNo != "")

{ //当前商品正在输入的时候,删除当前商品所进行的操作

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] + (double)TotalPriceBuf;

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->SellDetail_Table->Cancel();

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

}

else if( DM->SellDetail_Table->RecordCount )

{ //商品列表不为空时的操作(可能会是前面的商品记录)

DM->SellDetail_Table->Cancel();

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

DM->SellDetail_Table->Delete();

第七章应用程序中的面向状态 第 389 页

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

}

//删除了当前的商品后,必须重新处于接收商品信息的最初状态等候商品序号

DM->SellDetail_Table->Append();

DM->Sells_No->Value = DM->Sell_TableSells_No->Value;

FoodNo = ""; //清空商品序号缓存

Status = _WAIT_FOOD_NO; //设置到接收商品序号的状态

} else if( Key == '+' && DM->SellDetail_TableTotal_Price->Value != 0 )

{ //当用户按下加号键,并且当前商品的合计价格不为 0时,

//进入下一个商品的输入,将程序的状态置于等候商品序号的状态

DM->SellDetail_Table->Post();

DM->SellDetail_Table->Append();

DM->Sells_No->Value = DM->Sell_TableSells_No->Value;

FoodNo = "";

Status = _WAIT_FOOD_NO;

}

else if( Key == '*')

{ //当用户按下星号键时,表示改变当前商品的数量,

//将程序的状态设置为等候商品的数量状态

if( FoodNo != "" )

{

Status = _WAIT_FOOD_COUNT;

FoodCount = "";

}

}

}

//------------------------------------------------------------------

void __fastcall TForm1::WaitFoodCountProc(WORD &Key)

{ //该函数为等候商品数量状态的键盘子处理函数,有效的输入字符为 0~9和小数点号,

//小数点只能有一个,并精确到小数点后一位

if( Key >= '0' && Key <= '9' )

{ if( FoodCount.Pos('.') < FoodCount.Length() )

return; //该判断忽略小数点后第二位以上的输入。

FoodCount = FoodCount + (char)Key;

第七章应用程序中的面向状态 第 390 页

DM->Food_Count->Value = StrToFloat( FoodCount ); //以下为重新计算商品价格、总价及客户剩余金额

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->SellDetail_TableTotal_Price->Value = DM->Food_Count->Value *

DM->Food_Price->Value;

TotalPriceBuf = TotalPriceBuf +

DM->SellDetail_TableTotal_Price->Value;

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->SellDetail_TableTotal_Price->Value = DM->Food_Count->Value *

DM->Food_Price->Value;

TotalPriceBuf = TotalPriceBuf +

DM->SellDetail_TableTotal_Price->Value;

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

//价格计算结束

}

else if( Key == '.' && !FoodCount.Pos('.') )

{ //当用户输入小数点时,判断是否已经包含小数点,已包含则忽略输入

FoodCount = FoodCount + '.';

}

else if( Key == VK_ESCAPE )

{ //用户按下 ESC键,同等候商品序号的操作

DM->SellDetail_Table->Cancel();

DM->SellDetail_Table->Last();

for( ; !DM->SellDetail_Table->Bof ;DM->SellDetail_Table->Delete());

第七章应用程序中的面向状态 第 391 页

DM->Sell_Table->Cancel();

DM->Customer_Table->Cancel();

FoodNo = "";

Status = _WAIT_CARD;

}

else if( Key == '+' && DM->SellDetail_TableTotal_Price->Value != 0 )

{ //用户按下加号键,同等候商品序号的操作

DM->SellDetail_Table->Post();

DM->SellDetail_Table->Append();

DM->SellDetail_Table->FieldValues["Sells_No"] =

DM->Sell_TableSells_No->Value;

FoodNo = "";

Status = _WAIT_FOOD_NO;

}

else if( Key == VK_RETURN )

{ //用户按下回车键,结束交易,同等候商品序号的操作

DM->SellDetail_Table->Post();

DM->Sell_Table->Post();

DM->Customer_Table->Post();

SerialNo = SerialNo + 1;

Status = _WAIT_CARD;

FoodNo = "";

}

else if( Key == VK_BACK || Key == '-')

{ //用户按下退格键或者减号,同等候商品序号的操作

if(DM-> SellDetail_Table->Modified )

{

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->SellDetail_Table->Cancel();

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] – (double)TotalPriceBuf;

}

else if( DM->SellDetail_Table->RecordCount )

第七章应用程序中的面向状态 第 392 页

{

TotalPriceBuf = TotalPriceBuf –

DM->SellDetail_TableTotal_Price->Value;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] +

(double)TotalPriceBuf;

DM->SellDetail_Table->Delete();

DM->Sell_TableTotal_Price->Value = TotalPriceBuf;

DM->Customer_Table->FieldValues["Money"] =

DM->Customer_Table->FieldValues["Money"] –

(double)TotalPriceBuf;

}

DM->SellDetail_Table->Append();

DM->Sells_No->Value = DM->Sell_TableSells_No->Value;

FoodNo = "";

Status = _WAIT_FOOD_NO;

}

}

//------------------------------------------------------------------

void __fastcall TForm1::SetLogIn( bool Value )

{ //IsLogin的设置函数

if( FIsLogIn == Value )

return;

if( Value )

{ //更新界面

SpeedButton2->Enabled = true;

SpeedButton6->Enabled = true;

SpeedButton7->Enabled = true;

N3->Enabled = true;

N4->Enabled = true;

N12->Enabled = true;

SpeedButton1->Caption = "退出登录";

N2->Caption = "退出登录";

}

else

{ //若处于销售状态,则退出销售状态,并更新界面,IsSelling受到 IsLogin的约束

if( IsSelling )

IsSelling = false;

SpeedButton2->Enabled = false;

第七章应用程序中的面向状态 第 393 页

SpeedButton6->Enabled = false;

SpeedButton7->Enabled = false;

SpeedButton1->Caption = "用户登录";

N3->Enabled = false;

N4->Enabled = false;

N12->Enabled = false;

N2->Caption = "用户登录";

}

FIsLogIn = Value;

}

//------------------------------------------------------------------

void __fastcall TForm1::SetSelling( bool Value )

{ //IsSelling的设置函数

FIsSelling = Value;

if( Value )

{ //设置为销售状态的操作

if( !IsLogIn )

{ //未登录时,不允许处于销售状态,在这个程序中,这种情况不会发生,但是

FIsSelling = false; //IsSelling属性是公开,这种约束的完整性是必须考虑的

}

else //若已经登录,销售状态则显示 Panel1、Panel2,打开相应的数据集

{ //并设置 Application的 OnMessage事件,同时将子状态设置为

Panel1->Visible = true; //等候刷卡状态

Panel2->Visible = true;

DM->Sell_Table->Open();

DM->Customer_Table->Open();

DM->Food_Table->Open();

DM->SellDetail_Table->Open();

SpeedButton5->Visible = true;

Status = _WAIT_CARD; //设置为等候刷卡状态

Application->OnMessage = MyKeyProc; for(DM->Sell_Table->First();!DM->Sell_Table->Eof;

DM->Sell_Table->Next())

{ //取数据库中的流水号

if( DM->Sell_Table->FieldValues["Sells_No"] > SerialNo )

SerialNo = DM->Sell_Table->FieldValues["Sells_No"];

}

}

}

第七章应用程序中的面向状态 第 394 页

else //设置为非销售状态,关闭相应的数据集,

{ //恢复 OnMessage事件,更新界面

Panel1->Visible = false; //在 IsSelling属性中并不对数据集进行判断

Panel2->Visible = false; //比如是否更改,是否可以保存等条件是由数

DM->Sell_Table->Close(); //据集自行判断和设置的

DM->Customer_Table->Close();

DM->Food_Table->Close();

DM->SellDetail_Table->Close();

SpeedButton5->Visible = false;

Application->OnMessage = NULL;

}

}

//------------------------------------------------------------------

void __fastcall TForm1::SetStatus( int Value )

{ //设置销售状态下的子状态 Status的函数,完整的设置函数应该包含

//对上一级约束状态 IsSelling的检测和设置,在这个例子中,实际

//上这种约束关系已经包含在前面的代码中,因为对子状态的应用,

//只存在于键盘处理函数和子处理函数中,只有 Selling为销售状态时

//OnMessage事件才会调用键盘处理函数。

FStatus = Value;

switch ( Value )

{

case _WAIT_FOOD_NO: ; //设置为等候商品序号状态

Edit1->Text = "请输入餐号...更改相应餐号数量请按*键";

DBText1->Visible = true;

DBText2->Visible = true;

DBImage1->DataField ="Photo";

DBGrid2->DataSource = DM->Sell_Detail;

SubKeyProc = WaitFoodNoProc; //设置键盘子处理函数

break;

case _WAIT_FOOD_COUNT: ; //设置为等候商品数量状态

Edit1->Text = "请输入数量...";

SubKeyProc = WaitFoodCountProc; //设置键盘子处理函数

break;

case _WAIT_CARD: //设置为等候刷卡状态

Edit1->Text = "等候刷卡...";

StatusBar1->Panels->Items[1]->Text = "";

StatusBar1->Panels->Items[3]->Text = "";

StatusBar1->Panels->Items[5]->Text = "";

第七章应用程序中的面向状态 第 395 页

DBText1->Visible = false;

DBText2->Visible = false;

DBImage1->DataField = "";

DBGrid2->DataSource = NULL;

SubKeyProc = WaitCardKeyProc; //设置键盘子处理函数

break;

default :

Edit1->Text = "";

}

} 在这个 Sells 的例子中,关键的几个函数和属性设置都包含在这个单元中,程序的

逻辑结构也正是在这部分代码中体现出来的,读者需要结合例程的运行状况进行了解。

2.3.3 Pass 密码验证单元

Pass 单元是密码验证单元,比较简单,其 Form 的形状如同前面图中所示,头文件

也非常简单,这里只给出实现文件,如下: #include <vcl.h>

#pragma hdrstop

#include "Pass.h"

#include "Main.h"

#include "DataBase.h"

#pragma package(smart_init)

#pragma resource "*.dfm"

TForm2 *Form2;

//------------------------------------------------------------------

__fastcall TForm2::TForm2(TComponent* Owner)

: TForm(Owner)

{

}

//------------------------------------------------------------------

void __fastcall TForm2::BitBtn2Click(TObject *Sender)

{

Close();

}

//------------------------------------------------------------------

void __fastcall TForm2::BitBtn1Click(TObject *Sender)

{

第七章应用程序中的面向状态 第 396 页

unsigned char Temp[256]; //密码的缓存

TLocateOptions Opts;

Variant locvalues; //Local方法的匹配条件

Opts.Clear();

Opts << loCaseInsensitive;

locvalues = Variant(Edit1->Text); //Local方法的匹配参数

DM->SysTemUser->Open();

if( DM->SysTemUser->RecordCount == 0)

{ //如果操作员的数据库为空,通常是第一次运行,按照确省的用户和密码验证

if( Edit1->Text == "ADM" && Edit2->Text == "ADM" )

{ //确省的用户和密码均为"AMD"

Form1->IsLogIn = true;

Close();

Edit1->Text = "";

Edit2->Text = "";

} return; //第一次运行则忽略后面的代码

}

if( DM->SysTemUser->Locate( "SysUser_ID",locvalues,Opts ))

{ //先按照用户名进行匹配,如果招到该用户,再进行密码的匹配验证

int nBytes;

TBlobStream *Stream = new TBlobStream(DM->SysTemUserPassWord,

bmRead);

Stream->Read(Temp,1); //读取密码长度的标志,Blob字段的第一个字节

nBytes = Temp[0]; //以二进制保存着整个密码或加密存放的密码长度

Stream->Read(Temp,nBytes + 1 ); //读取密码或加密数据

//此处可对 Temp插入相应的解密算法,完成密码的加密保存

//密码的解密需要和用户设置密码时的加密算法对应。

if( Edit2->Text == AnsiString( (char*)Temp ) )

{

Form1->IsLogIn = true; //设置程序为登录状态

Close(); //关闭登录窗口时清空用户和密码

Edit1->Text = "";

Edit2->Text = "";

}

else

{

Edit2->Text = ""; //密码不匹配时,清空密码输入;

}

第七章应用程序中的面向状态 第 397 页

delete Stream; //销毁临时的 BlobStream

}

else //当用户名不匹配时,清空所有输入

{

Edit1->Text = "";

Edit2->Text = "";

}

DM->SysTemUser->Close(); //登录成功后关闭相应数据集

}

//------------------------------------------------------------------

void __fastcall TForm2::EditKeyPress(TObject *Sender, char &Key)

{ //用户名和密码的 Edit共用一个 KeyPress事件句柄。

if( Key == 13 ) //回车按照登录操作,0D13=VK_RETRUN

BitBtn1Click( Sender );

if( Key == VK_ESCAPE ) //ESC按照取消操作,VK_ESCAPE=0D28

BitBtn2Click( Sender );

}

//------------------------------------------------------------------

void __fastcall TForm2::FormShow(TObject *Sender)

{

Edit1->SetFocus(); //登录窗口显示后 Edit1获得焦点

}

2.3.4 Path 路径设置单元

Path单元是当程序运行时数据库设置不正确,供用户设置 Paradox 数据库路径的窗

体,其窗体也是比较简单,头文件也非常简单,同样我们只给出实现部分: #include <vcl.h>

#pragma hdrstop

#include "Path.h"

#include "Main.h"

#include "DataBase.h"

#pragma package(smart_init)

#pragma link "cdiroutl"

#pragma resource "*.dfm"

AnsiString Tables[5] =

{

"Customer", //这是数据库中所所使用的 5个表集的名称;

第七章应用程序中的面向状态 第 398 页

"Food", //当用户设置了数据库时,需要逐一验证数据库的有效性

"Sell", //更可靠的验证方法是对每一个数据集的字段以及每个字

"Sell_Detail", //段的数据类型逐一进行验证

"System_User"

};

TForm5 *Form5;

//------------------------------------------------------------------

__fastcall TForm5::TForm5(TComponent* Owner)

: TForm(Owner)

{

}

//------------------------------------------------------------------

void __fastcall TForm5::BitBtn2Click(TObject *Sender)

{ //若程序提示设置数据库,而用户取消时,程序将关闭

if( Reg )

delete Reg ;

Form1->Visible = false; //以上三行代码可以省去,当程序关闭时,所有

Close(); //分配的资源均被释放,但是如果该方法不是将

Form1->Close(); //程序关闭,则不一定可以省略

} //------------------------------------------------------------------

void __fastcall TForm5::BitBtn1Click(TObject *Sender)

{

TTable *TempTable = new TTable(NULL); //用于测试数据库的临时表。

TempTable->DatabaseName = DM->Sell->DatabaseName; DM->Sell->Directory = DirectoryListBox1->Directory; //指定的数据库路径;

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

{

TempTable->TableName = Tables[i];

try

{

TempTable->Open(); //进行数据集存在性的验证,可以在 Open和 Close方

TempTable->Close(); //法之间加入对数据集字段和类型的验证

}

catch(...)

{ //当数据集不存在时,报告用户

Application->MessageBox("指定的路径非法!","错误!",0);

return;

第七章应用程序中的面向状态 第 399 页

}

}

if( !Reg )

Reg = new TRegistry; //检验是否已经打开注册表

try //设置的数据库有效时将信息写入注册表

{

Reg->RootKey = HKEY_LOCAL_MACHINE;

if ( Reg->OpenKey("\\Software\\Lewolf\\DataBase",true) )

{

Reg->WriteString("DataPath",

DirectoryListBox1->Directory); Reg->CloseKey(); //关闭注册表

}

else //无法写入注册表时,提醒用户并关闭程序

{

Application->MessageBox( "注册表出现错误,程序即将关闭","错误",0);

Close();

Form1->Close();

}

}

catch(...) //出现异常时,关闭程序

{

Application->MessageBox("程序出现错误\n\n即将退出","错误",0);

Close();

Form1->Close();

} if( Reg ) //释放分配的资源

delete Reg;

delete TempTable;

Close();

}

2.3.5 Splash 启动画面单元

Splash 是在启动程序的时候显示的启动画面,这个单元也是十分简单,Form 不是

在程序主文件中自动创建运行的,而是在 Form1的构造函数中使用 new关键字创建的,

这样可以是启动画面在主窗体 Form1显示之前显示出来。这个 Form中包含一个 Image用于存放图片,另外有一个动画,是从文件载入的,这个单元读者参考源代码文件。

第七章应用程序中的面向状态 第 400 页

2.3.6 DataBase 数据库模块单元

DataBase是该程序的数据库模块,这个模块中定义了程序所用到的数据库组件和数

据集组件,并为部分数据集组件定义了 TField字段,模块的结构如图 7.9:

图 7.9 DM 单元的结构

第七章应用程序中的面向状态 第 401 页

头文件如下:

#ifndef DataBaseH

#define DataBaseH

//------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <Db.hpp>

#include <DBTables.hpp>

#include "registry.hpp"

#include <DB.hpp>

//------------------------------------------------------------------

class TDM : public TDataModule

{ __published:

TTable *SellDetail_Table;

TIntegerField *Sells_No;

TSmallintField *Food_No;

TStringField *Food_Name;

TFloatField *Food_Count;

TCurrencyField *Food_Price;

TCurrencyField *SellDetail_TableTotal_Price;

TDataSource *Sell_Detail;

TTable *Food_Table;

TDataSource *Food;

TTable *Customer_Table;

TDataSource *Customer;

TTable *Sell_Table;

TIntegerField *Sell_TableSells_No;

TDateField *Sell_TableDate;

TTimeField *Sell_TableTime;

TStringField *Sell_TableCard_No;

TCurrencyField *Sell_TableTotal_Price;

TStringField *Sell_TableUser_Name;

TSmallintField *Sell_TableUser_ID;

TDataSource *Sells;

TDatabase *Sell;

第七章应用程序中的面向状态 第 402 页

TTable *SysTemUser;

TStringField *SysTemUserUser_Name;

TStringField *SysTemUserSysUser_ID;

TBlobField *SysTemUserPassWord;

TDataSource *Sys;

TCurrencyField *Customer_TableMoney;

TSmallintField *Customer_TableUser_ID;

TStringField *Customer_TableNo_Card;

TGraphicField *Customer_TablePhoto;

TStringField *Customer_TableUserType;

TBooleanField *Customer_TableCatchPrice;

TStringField *Customer_TableCustomer_Name;

TMemoField *Customer_TableMemo;

void __fastcall MD(TObject *Sender);

void __fastcall Sell_TableSells_NoChange(TField *Sender);

void __fastcall Food_CountChange(TField *Sender);

void __fastcall Food_NoChange(TField *Sender);

void __fastcall Sell_TableUser_NameChange(TField *Sender);

void __fastcall SellDetail_TableAfterEdit(TDataSet *DataSet);

void __fastcall SellDetail_TableAfterInsert(TDataSet *DataSet);

public:

bool SellDetailCanPost;

inline __fastcall TDM(TComponent* Owner): TDataModule(Owner){};

};

extern PACKAGE TDM *DM;

//------------------------------------------------------------------

#endif

实现部分代码如下:

#include <vcl.h>

#pragma hdrstop

#include "DataBase.h"

#include "Main.h"

#include "Path.h"

//------------------------------------------------------------------

#pragma package(smart_init)

#pragma resource "*.dfm"

TDM *DM;

第七章应用程序中的面向状态 第 403 页

//------------------------------------------------------------------

void __fastcall TDM::MD(TObject *Sender)

{ //TDataModule创建时,根据注册表信息设置数据库 Sell的参数

TRegistry *Reg = new TRegistry;

try

{

Reg->RootKey = HKEY_LOCAL_MACHINE;

if ( Reg->OpenKey("\\Software\\Lewolf\\DataBase",false) )

{ //若找到相应的注册表键,则按照注册表设置 Sell的参数

//使用 Sell->Params和 Sell->Directory可以获得相同的效果,

//但 Params要求在数据库打开之前设置,并且 Params中必须有"PATH"字段

//Sell->Params->Values["PATH"] = Reg->ReadString("DataPath");

Sell->Connected = true;

Sell->Directory = Reg->ReadString("DataPath");

Reg->CloseKey();

} else //如果注册表中没有相应的键值,则打开数据库设置对话框 Form5

Form5->ShowModal();

}

catch(...)

{ Application->MessageBox("数据库出错\n\n即将退出\n\n请与软件提供商联系",

"错误",0);

delete Reg;

Form1->Close();

} delete Reg;

}

//------------------------------------------------------------------

void __fastcall TDM::Sell_TableSells_NoChange(TField *Sender)

{ //当销售订单改变是,更新销售列表的过滤器,也可以使用 Master-Detail关系实现

SellDetail_Table->Filter = "Sells_No=" +

IntToStr( Sell_TableSells_No->Value );

}

//------------------------------------------------------------------

void __fastcall TDM::Food_CountChange(TField *Sender)

{ //将销售列表中的当前商品数量更新显示到 StatusBar

Form1->StatusBar1->Panels->Items[5]->Text =

FloatToStr( Food_Count->Value );

第七章应用程序中的面向状态 第 404 页

} //------------------------------------------------------------------

void __fastcall TDM::Food_NoChange(TField *Sender)

{ //将销售列表中的当前商品序号更新显示到 StatusBar

Form1->StatusBar1->Panels->Items[3]->Text =

IntToStr( DM->Food_No->Value );

}

//------------------------------------------------------------------

void __fastcall TDM::Sell_TableUser_NameChange(TField *Sender)

{ //将刷卡后用户信息更新显示到 StatusBar

Form1->StatusBar1->Panels->Items[1]->Text =

Customer_Table->FieldValues["Customer_Name"];

}

//------------------------------------------------------------------

void __fastcall TDM::SellDetail_TableAfterEdit(TDataSet *DataSet)

{ //禁止用户使用方向键移动销售列表数据集的游标,避免移动游标时将记录 Post

SellDetailCanPost = false;

}

//------------------------------------------------------------------

void __fastcall TDM::SellDetail_TableAfterInsert(TDataSet *DataSet)

{ //禁止用户使用方向键移动销售列表数据集的游标,避免移动游标时将记录 Post

SellDetailCanPost = false;

}

//------------------------------------------------------------------

2.4 总结

在这个例子 Sells 中,我们使用了属性和事件来完成不同状态下,界面显示和代码

执行条件的转换,在这个例子中也可以看出应用程序中的属性和组件中的属性之间的差

别,应用程序中的事件和组件中的事件也有所不同,但这种程序的设计思想却是从组件的

设计思想发展而来。 在这个例子中,每个对象之间的关系相对较为简单一些,主要的应用都在 Main单元

中,程序也是比较简单的,但是程序的状态之间的约束关系却是比较明确的,首先是 Login状态,这是整个程序运行的首要条件,也是对程序的其它状态具有约束力的状态,然后是

在登录状态下的几个运行状态,在销售状态下又具有三种不同时刻的销售状态,这些状态

之间具有一些不同的约束关系,以及在不同的状态下,程序有着不同的执行动作,我们正

是利用了面向状态技术中的属性以及事件来将这些相对复杂的关系简约化,使整个程序在

修改维护上要便利的多。不难想像,对于复杂的程序,这种对象相互之间的约束关系也将

会复杂的多,如果仅仅采用常规的设计方法,自然也是可以实现的,但是在实现的过程中

将会遇到更多的麻烦,对维护和修改也是极为不利。

第七章应用程序中的面向状态 第 405 页

接下来我们要看的是一个关于图形编辑方面的例子,在这个例子中,所实现的功能相

对要多一些,各个对象之间的关系也要比 Sells中的复杂一些,但是使用了 VCL的组件

之后,加上在应用程序中使用了面向状态的设计方法,整个程序实现起来要简单的多。

3 示范程序 Drawing

这个例子是在 C++ Builder 6环境下设计的,也是作者专门为本书而编写的例程,

由于程序中使用了 C++ Builder 6 中新增加的组件,因此这部分代码不能兼容以前版本

的,程序运行如图 7.10所示:

这是一个简单的多文档(MDI)应用程序,其中 MDIChild 窗口为一个简单的矢量绘

图窗口,例程提供了三种绘图工具,矩形、椭圆和线段,其中线段涉及到更复杂的派生对

象,为了节省篇幅,例程中没有给出代码,但其实现的方法和矩形以及椭圆是基本类同的。 程序总共有三大功能: n 编辑功能,可以分作整体图形编辑和个体编辑; n 存储功能,对文件的保存和读取的功能;

图 7.10 Drawing运行后的界面

第七章应用程序中的面向状态 第 406 页

n 数据交换,使用 Windows剪贴板进行数据交换,可以实现拷贝、粘贴等功能。 在这个程序中,涉及到的对象有四种,在本程序中实际上就是四个对象类: n 一是图形对象,为每一个绘制到窗口上的图形,以矢量数据的方式记录; n MDIChild对象,是为每一个绘制的图形集合定义的显示窗体,是和存储文件对

应的; n 针对我们的图形格式而定义的剪贴板对象,来完成特定格式的数据交换; n MDI主窗体,是整个应用程序的框架。 在这个程序的设计过程中,首先是建立整个应用程序的框架结构,然后在建立每一个

所需要的数据类型和结构,最后完成每一个功能的实现。我们也按照这个顺序来逐步介绍

这个例子。

3.1 MDI 主窗体对象 TForm2 类

MDI主窗体是整个应用程序的框架,工具条和菜单等都在这个单元内,这个类中基本

都是由 IDE生成的可视化组件对象,只有一个用户定义的成员 ClipBoardFormat,用来注

册剪贴板中特定的数据格式,之外就是一个 ActiveDraw 属性,纯粹是为了使用方便,

不需要频繁的进行数据类型的转换。 这个单元中的方法都属于事件的处理句柄,而且都比较简单,因此这个单元的代码我

们将不作详细的介绍,读者参考源码和注解,我们仅对功能和对象之间的关系作个介绍。 在 ToolMain 单元中,除了几个运行时不可见的组件之外,只有一个 ControlBar

是顶层控件,这个 ControlBar上一共有四个 ToolBar,分别是主菜单、常用按钮、绘

图工具和图形属性的工具条,声明文件如下: #ifndef ToolMainH

#define ToolMainH

//------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <ComCtrls.hpp>

#include <ExtCtrls.hpp>

#include <ToolWin.hpp>

#include <ImgList.hpp>

#include <Menus.hpp>

#include "Main.h"

#include "ShapeClass.h"

#include <ActnCtrls.hpp>

#include <ActnMan.hpp>

#include <ActnMenus.hpp>

第七章应用程序中的面向状态 第 407 页

#include <Buttons.hpp>

#include <Dialogs.hpp>

class TForm2 : public TForm

{

__published:

TControlBar *ControlBar1; //ControlBar对象

TImageList *ImageList1; //为绘图工具提供位图

TImageList *ImageList2; //为线型和线宽列表框提供位图

TImageList *ImageList3; //为常用工具条提供按钮位图

TToolBar *ToolBar1; //主菜单的工具条,包含一下菜单

TMainMenu *MainMenu1; //主菜单对象

TMenuItem *File; //主菜单中的 File菜单项

TMenuItem *New; //File菜单中的 New新建菜单项

TMenuItem *Open; //File菜单中的 Open打开文件菜单项

TMenuItem *N1; //分割符菜单项

TMenuItem *Save1; //File中的 Save保存菜单项

TMenuItem *SaveAs1; //File中的 SaveAs另存为菜单项

TMenuItem *N2; //分割符菜单项

TMenuItem *Close1; //File中的 Close关闭菜单项

TMenuItem *Exit1; //File中的 Exit退出菜单项

TMenuItem *Edit1; //主菜单的 Edit菜单项

TMenuItem *Cut1; //Edit中的 Cut剪切菜单项

TMenuItem *Copy1; //Edit中的 Copy复制菜单项

TMenuItem *Paste1; //Edit中的 Paste粘贴菜单项

TMenuItem *Delete1; //Edit中的 Delete删除菜单项

TMenuItem *View1; //主菜单中的 View菜单项

TMenuItem *MemuBar2; //View中的 MenuBar菜单项

TMenuItem *EditTools2; //View中的 EditTool菜单项

TMenuItem *DrawTools2; //View中的 DrawTools菜单项

TMenuItem *PropertyTools2; //View中的 PropertyTool菜单项

TMenuItem *Window1; //主菜单中的 Window菜单

TMenuItem *Tite1; //Window中的 Tite菜单项

TMenuItem *Cascade1; //Window中的 Cascade菜单项

TMenuItem *Help1; //主菜单的 Help菜单项

TMenuItem *About1; //Help的 About菜单项

//绘图工具的 ToolBar,该工具条包含以下按钮

TToolBar *ToolBar2; //绘图工具的 ToolBar

TToolButton *SelectButton; //Select工具

第七章应用程序中的面向状态 第 408 页

TToolButton *RectButton; //绘制 Rectangle的工具

TToolButton *CircleButton; //绘制椭圆的工具

TToolButton *LineButton; //绘制线段的工具

//常用工具的 ToolBar,该工具条包含一下按钮

TToolBar *ToolBar3; //常用工具的 ToolBar

TToolButton *ToolButton3; //新建文件按钮

TToolButton *ToolButton4; //打开文件按钮

TToolButton *SaveButton; //保存按钮

TToolButton *SaveAsButton; //另存为按钮

TToolButton *CutButton; //剪切按钮

TToolButton *CopyButton; //复制按钮

TToolButton *PasteButton; //粘贴按钮

TToolButton *DeletButton; //删除按钮

//图形属性的 ToolBar,该工具条包含一下按钮

TToolBar *ToolBar4; //图形属性工具条

TToolButton *FillButton; //填充格式按钮

TColorBox *ColorBox1; //图形颜色下拉框

TComboBoxEx *ComboBoxEx1; //线宽下拉框

TComboBoxEx *ComboBoxEx2; //线型下拉框

//ControlBar的弹出菜单,基本同 View菜单,功能也同 View菜单项

TPopupMenu *ToolMenu; //

TMenuItem *MemuBar1; //ToolMenu菜单项,检查是否显示主菜单

TMenuItem *EditTools1; //决定是否显示常用工具条

TMenuItem *DrawTools1; //决定是否显示绘图工具条

TMenuItem *PropertyTools1; //决定是否显示图形属性工具条

//DMIChild窗体的弹出菜单,在 Edit菜单项的内容中增加了改变图形叠放层次的菜单项

TPopupMenu *Popup; //

TMenuItem *Cut2; //用于剪切

TMenuItem *Copy2; //用于复制

TMenuItem *Paste2; //用于拷贝

TMenuItem *Delete2; //用于删除

TMenuItem *N5; //分割符菜单项

TMenuItem *MenuItem1; //图形上移一层,未使用

TMenuItem *MenuItem2; //图形下移一层,未使用

TMenuItem *N3; //将所选的图形移到顶层

TMenuItem *N4; //将所选的图形移到底层

TOpenDialog *OpenDialog1; //打开文件对话框

TSaveDialog *SaveDialog1; //保存文件对话框

void __fastcall RectButtonClick(TObject *Sender);

第七章应用程序中的面向状态 第 409 页

void __fastcall CircleButtonClick(TObject *Sender);

void __fastcall SelectButtonClick(TObject *Sender);

void __fastcall ComboBoxEx1Change(TObject *Sender);

void __fastcall ColorBox1Change(TObject *Sender);

void __fastcall ComboBoxEx2Change(TObject *Sender);

void __fastcall Cascade1Click(TObject *Sender);

void __fastcall Title1Click(TObject *Sender);

void __fastcall NewClick(TObject *Sender); //新建文件按钮的句柄

void __fastcall ToolMenuPopup(TObject *Sender);

void __fastcall MemuBar1Click(TObject *Sender);

void __fastcall EditTools1Click(TObject *Sender);

void __fastcall DrawTools1Click(TObject *Sender);

void __fastcall PropertyTools1Click(TObject *Sender);

void __fastcall DeletButtonClick(TObject *Sender);

void __fastcall FormCreate(TObject *Sender);

void __fastcall CopyButtonClick(TObject *Sender);

void __fastcall PasteButtonClick(TObject *Sender);

void __fastcall CutButtonClick(TObject *Sender);

void __fastcall PopupPopup(TObject *Sender);

void __fastcall FillButtonClick(TObject *Sender);

void __fastcall N3Click(TObject *Sender);

void __fastcall N4Click(TObject *Sender);

void __fastcall SaveClick(TObject *Sender);

void __fastcall SaveAsClick(TObject *Sender);

void __fastcall OpenClick(TObject *Sender); //打开按钮的句柄

void __fastcall CloseClick(TObject *Sender);

void __fastcall ExitClick(TObject *Sender);

protected:

void __fastcall SetActiveDraw(TForm1* Value); //ActiveDraw的写方法

TForm1 * __fastcall GetActiveDraw(void); //ActiveDraw的读方法

public:

WORD ClipBoardFormat; //向剪贴板注册数据格式

__property TForm1* ActiveDraw={read=GetActiveDraw,

write=SetActiveDraw};

__fastcall TForm2(TComponent* Owner);

}; extern PACKAGE TForm2 *Form2;

#endif

第七章应用程序中的面向状态 第 410 页

MenuBar: 主菜单中包含 5个菜单项,分别是 File、Edit、View、Window 和 Help,其结构

如下图:

File菜单中包含了 New、Open、Save等常用的菜单项,其功能不需要进一步介绍,

其中的 Save菜单项是和当前活动的 MDIChild窗体的编辑状态有关,每一个 MDIChild对象中都有一个记录编辑状态的属性 Modified,文件保存后其 Modified 属性变为

false,当任何的编辑动作执行后,Modified属性变为 true,Save菜单项的 Enabled属性和活动 MDIChild的 Modified属性一致。

Edit菜单中包含一般的编辑命令:Cut、Copy、Paste、Delete 等,其中 Cut、Copy、Delete 菜单项的 Enabled 和当前活动 MDIChild中的被选择图形有关,只有被

选择图形的数量不为 0的时候,这几个菜单的 Enabled 属性为 true,否则为 false,这几个菜单的操作对象也是当前活动 MDIChild 所选中的图形,Paste 菜单项的

Enabled 和当前剪贴板中的内容有关,只有剪贴板中包含程序能够识别的数据格式时,

Paste的 Enabled为 true,否则为 false。 View菜单中的几个菜单项都是 Checked 类型的,他们的 Checked 属性分别和主窗

体的四个 ToolBar的 Visible 属性一致,这就类似 Word等程序中的工具条,当在工具

条上点击右键的时候,弹出的快捷菜单 ToolMenu可以选择显示的工具条,和 View菜单

的内容相同,因此对 ToolMenu我们不再介绍。 Window菜单是多文档界面中基本的两个窗体排列方法 Tite、Cascade;Help菜

单中只有一个 About菜单项。

图 7.11 MainMenu的结构

第七章应用程序中的面向状态 第 411 页

绘图工具条: 绘图工具条中包含四个按钮,分别是 Select工具、矩形绘图工具、椭圆绘图工具和

线段绘图工具,这四个按钮为相互排斥的,不允许自行弹起,同时只有一个按钮的 Down属性为 true,这四个按钮组成一个组和当前活动的 MDIChild 窗体中的 Shape 属性相

对应,当活动 MDIChild改变时,工具栏中的按钮状态随之改变,用户点击不同工具时,

可以改变活动 MDIChild的 Shape属性的状态。Shape 属性记录着用户操作的绘图工具

类型,在后面的 Main单元中我们再详细介绍。

图 7.12 绘图工具 ToolsBar的结构

常用工具栏: 这个工具条实际上就是一些常用功能的快捷按钮,一共有 8个按钮:新建文档、打开

文档、保存、另存为、删除、剪切、拷贝、粘贴等,其功能和主菜单中相应的菜单项的功

能相同,这几个按钮的 Enabled 属性和主菜单中相应的菜单项的 Enabled 属性是一致

的,都和当前活动的 MDIChild的状态以及剪贴板中的内容相关。

图 7.13常用工具条的结构

图形属性工具条: 这个工具是一个显示所选择图形的属性的工具条,同时也是一个编辑所选择图形属性

的工具条,在我们这个例子中,一个图形对象的属性,指的是图形固有的特征,同时也是

程序代码中对象的属性,比如一个矩形的位置、大小、颜色、边框的线宽、线型等。在图

形属性的工具条中,包含一个按钮,用于标识和编辑图形填充方式,一个 Color 下拉列

表框、一个线宽的下拉列表框和一个线型的下拉列表框,分别用于显示当前活动文档中所

选择图形的填充方式、颜色、线宽和线型,当活动文档没有任何图形被选中的时候,这四

个控件分别显示活动文档的缺省填充方式、颜色、线宽和线型,每次创建新的图形对象时,

均按照缺省的这几个属性来绘制新的图形,通过这四个控件可以改变一个 MDIChild窗体

第七章应用程序中的面向状态 第 412 页

中相应的缺省属性;当活动文档中有图形被选中的时候,这四个控件将显示被选中图形的

相应属性,通过改变控件的属性可以改变被选择图形的相应属性,当被选择图形为多个,

并且它们的颜色、线宽和线型不一致的时候,对应下拉列表框的索引为-1,显示列表中的

内容为空。

图 7.13 属性工具条的结构

ActiveDraw属性: 这 个 属 性 和 ActiveMDIChild 是 一 致 的 , 这 里 只 是 将 类 型 转 换 后 的

ActiveMDIChild 作为读取方法的返回值,设置方法本身是可以省略的,我们在例子中

提供了一个设置方法,这样程序中可以通过属性的设置来改变活动的 MDIChild窗体。

3.2 MDIChild 对象 Main 单元

这个单元定义了 MDIChild 子窗体的对象类 TForm1,应用程序中可以包含多个

TForm1对象,每一个 TForm1对象和一个数据文件对应,同时 TForm1 对象还承担着对

图形对象进行整体编辑任务。 Main单元中除了 TForm1 类之外,还有一个枚举数据类型,用于标识 TForm1 当前

所处的绘图工具的状态,和一个用于剪贴板使用的 TDrawClipborad 类,整个声明文件

如下: #ifndef MainH

#define MainH //------------------------------------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <ComCtrls.hpp>

#include <ExtCtrls.hpp>

#include <ToolWin.hpp>

#include <ImgList.hpp>

#include <Buttons.hpp>

#include <Menus.hpp>

#include "Clipbrd.hpp"

#include "ShapeClass.h"

第七章应用程序中的面向状态 第 413 页

enum TShapeStyle

{

_Select,_Rect,_Circle,_Line

};

//定义用于标识当前画布图形类型的枚举类型,画布提供四种

//状态:选择、矩形、椭圆、线

struct TFileHeader //定义一个顺序存储的文件头标识结构

{ //在这个结构中至少包含图像数量的信息

char Sign[24]; //用于存放文件数据的标识

int DrawCount; //Sign是一个标签,用于区分文件类型的,

}; //本例中没有使用 Sign,

//DrawCount记录着整个文件中图形对象的数量

//------------------------------------------------------------------

//本例中,Form1为画布,是一个 MDI窗体,所有的图形都绘制在画布上

class TForm1 : public TForm

{

__published:

//OnMouseDown事件的处理函数

void __fastcall DrawMouseDown(TObject *Sender, TMouseButton Button,

TShiftState Shift, int X, int Y);

//OnMouseMove事件的处理函数

void __fastcall DrawMouseMove(TObject *Sender, TShiftState Shift,

int X, int Y);

//OnMouseUp事件的处理函数

void __fastcall DrawMouseUp(TObject *Sender, TMouseButton Button,

TShiftState Shift, int X, int Y);

//OnActive事件的处理函数

void __fastcall FormActivate(TObject *Sender);

//OnClose事件的处理函数,改变 Action使 MDI窗口关闭时一并销毁。

void __fastcall FormClose(TObject *Sender, TCloseAction &Action);

//OnCloseQuery事件的处理函数,对于为保存数据的提示

void __fastcall FormCloseQuery(TObject *Sender, bool &CanClose);

private:

TShapeStyle FShape; //Shape属性的内部存储数据

bool FModified; //Modified属性的内部存储数据

int FScale; //显示比例的内部存储数据,本例子中没有使用显示比例

protected:

第七章应用程序中的面向状态 第 414 页

void __fastcall SetShape(TShapeStyle Value); //Shape的设置函数

void __fastcall SetModified(bool Value); //Midified的设置函数

void __fastcall SetScale(int Value); //Scale的设置函数

public:

bool MouseDown; //记录鼠标键是否按下

bool AutoUpDate; //标识是否需要将图形显示自动更新

TPoint OrMousePos; //原始鼠标位置

TPoint MousePos; //记录当前鼠标位置

TList *Draw; //图形数据的存储列表

TList *Selection; //被选中绘图列表

AnsiString File; //存储数据的文件名称

TFileStream * FileStream; //存储数据的文件流

TColor DefaultColor; //画布的缺省绘图颜色

int DefaultLineWidth; //画布的缺省绘图线宽

TPenStyle DefaultPenStyle; //画布的缺省画笔类型

TBrushStyle DefaulyBrushStyle; //画布的缺省 Brush填充类型

__property Active; //继承的 Active属性

//用于记录画布内容是否被更改的属性,

__property bool Modified = {read=FModified,write=SetModified};

//记录画布绘图工具的属性

__property TShapeStyle Shape={read=FShape,write=SetShape};

//控制显示比例的属性

__property int Scale = {read=FScale,write=SetScale};

//用于更新 MDI主窗体工具按钮的函数

void __fastcall UpDateButton(void);

//删除被选择图形的函数

void __fastcall Delete();

//清除所有被选择内容(但并不删除图形)

void __fastcall UnSelected();

//整体移动被选择的图形

void __fastcall MoveSelection(int DeltaX,int DeltaY);

//向被选择图形列表中添加一项,Sole标识添加的图形是否是唯一的被选图形

void __fastcall AddSelect(TDraw* Draw,bool Sole);

//从被选择图形列表中移去一个指定的图形对象,但并不删除

void __fastcall RemoveSelect(TDraw* Draw,bool Sole);

//存储文件,若未指定文件名或未打开指定文件,调用 SaveAs进行存储

void __fastcall Save(void);

//按照指定的文件名存储图形数据

void __fastcall SaveAs(AnsiString FileName);

第七章应用程序中的面向状态 第 415 页

//打开指定文件中的图形

void __fastcall OpenFile(AnsiString FileToOpen);

//构造函数和析构函数

__fastcall TForm1(TComponent* Owner);

__fastcall ~TForm1( );

};

class TDrawClipborad : public TClipboard

{ //针对 TForm1定义的剪贴板对象,

public:

HANDLE Handle; //交换数据的内存区域句柄,并非必要的,可以使用局部变量

virtual __fastcall TDrawClipborad(); // 空的构造函数

virtual __fastcall ~TDrawClipborad(); //空的析构函数

//重载 Assign方法,用于拷贝 TForm1对象中的数据

virtual void __fastcall Assign(Classes::TPersistent* Source);

//重载 AssignTo方法,供 TForm1对象拷贝数据时使用

virtual void __fastcall AssignTo(Classes::TPersistent* Dest);

};

extern PACKAGE TForm1 *Form1;

#endif 在这个单元中,TForm1是和 ToolMian 单元中的 Form2关系最为密切的,Form2

在一个每个进程中只有一个对象,而每个 Form2对象可以包含多个 TForm1对象,这就

是多文档程序。Form2和 Form1对象使用 MDIChild和 Parent 相互建立这种逻辑包含

关系的,作为控件设计,如果 Form1要获取 Form2只能使用 Parent 并进行类型转换而

得到,而在应用程序中,我们的 Form1是直接对 Form2进行操作的,这是因为它们的这

种包含关系在这个程序中是固定的,在编写第一行代码的时候,就已经确立了,而编写控

件的时候,这种关系是无法确定的,这也是控件设计和应用程序设计的一个区别之处。 TShapeStyle 是一个简单的枚举类型,不需要过多介绍,TFrom1 中有一个该类型的

属性,体现着当前的绘图工具的状态,同时该属性和 Form2 中的绘图工具的按钮状态是

一致的,Shape属性的设置函数完成了这一工作。 这个单元中 MouseDown、 AutoUpDate、OrMousePos、MousePos、 File、

FileStream 、 DefaultColor 、 DefaultLineWidth 、 DefaultPenStyle 、

DefaulyBrushStyle、Draw和 Selection 等不同类型的成员都是供运行时内部使用

的变量,部分成员的功能前面已经提到了,后面的内容中有进一步的说明。 Active 是一个继承而来的属性,用于判断该文档(MDIChild)是否为当前活动的

内容,Modified属性记录当前文档是否在存储后被更改过,和 MDI主窗体的相应菜单项

及按钮的状态相关。 Scale是控制显示比例属性,这个例子中并没有在 MDI主窗体中使用,当改变 Scale

第七章应用程序中的面向状态 第 416 页

属性的时候,会改变整个 Form1 中图形在屏幕上的显示比例,这个功能是通过 Form 提

供的 ChangeScale 方法来完成的,因此功能也比较简单,更符合实际的是在改变显示位

置、大小等比例的同时也要改变线宽等属性的显示比例,有兴趣的读者可以自行完善。 TForm1 类提供的更多的接口是使用方法来实现的,除了 UpDateButton 方法是共

内部更新按钮使用的以外,其它方法基本都是提供编辑功能的方法,我们在后面作详细介

绍。UpDateButton是在文档别激活、编辑状态改变等时候,来更新 MDI主窗体各个按

钮状态的方法。 TDrawClipborad对象是为 TForm1 使用剪贴板对象而定义的专用类,为了能够正

确的使用剪贴板,我们在程序运行的最初,Form2创建的时候,需要向系统注册一个专用

的剪贴板数据类型。这个类型的注册结果保存在 Form2的 ClipBoardFormat成员中。

关于 TDrawClipborad的详细使用我们在后面介绍。

3.3 TDraw 对象

有了框架(TForm2)和画布(TForm1)对象之后,剩下来所需的就是图形对象 TDraw。所谓矢量绘图实际上就是按照一定的数据格式将图形存储在文件中,并能够正确的显示在

计算机屏幕上。 数据的存储没有什么问题,完全可以是自己定义的格式和数据结构。而图形显示无非

实在画布上将预先定义的数据格式绘制出来。在早先的程序设计中,这些工作都是有画布

来完成的,诸如编辑、移动等使用鼠标进行操作也都是画布根据不同的坐标,来判断对那

一个图形进行操作,并改变数据重新绘制图形。但自从面向对象的技术产生以后,解决和

分析问题就有了新的思路,将每一个图形理解为一个对象,是符合实际,也使程序的设计

更加简单化,现在的问题是我们如何来创建图形对象? 从我们要实现的功能来看,图形对象需要具备一下几个基本的特点: n 能够记录自身的数据,并根据数据在画布上正确绘图; n 能够独立响应鼠标信息,并完成基本的个体编辑功能; n 需要和画布建立一种特定的逻辑关系。 这几点是必须的,否则程序无法摆脱复杂的逻辑结构和设计代码。 要记录自身的数据是没有问题,任何一个对象最根本的就是数据成员,要正确的绘制

图形,就必须和画布发生关系,同时也会和其它的图形对象发生关系,这一要求我们可以

通过图形和画布之间的逻辑关系来实现; 图形对象没有必要独立的响应 Windows 消息,因为仅仅是对部分鼠标的消息能够响

应,这个可以通过对画布的消息分解,然后分发给各个图形对象来实现。个体编辑功能实

际上就是通过鼠标的操作来完成的,只要图形对象能够响应鼠标消息,就能够完成个体的

编辑功能。因此第二个要求实际上也是画布和图形对象之间关系的问题。 图形是属于画布的图形,这种逻辑关系是显而易见,画布还必须对每一个图形对象具

有完全的控制能力,诸如刷新显示、移动图形、删除图形等。在建立这种逻辑关系的时候,

必须为前两个要求创建条件。 由此看来 TDraw 对象是比较复杂的一个类,其实不用担心,我们完全没有必要从头

开始定义一套图形对象的类,还要满足以上的几个条件,VCL中为我们提供的类库就有满

第七章应用程序中的面向状态 第 417 页

足其基本要求的对象。这就是 TGraphicControl,不要觉得是个控件就会很庞大,其实

TGraphicControl 是相当简洁而高效的,除非你是一个图形方面程序设计多年的专家,

否则不要企图重新设计一套用于画布和图形对象的类库,因为即使设计的类库在使用性能

上比 TGraphicControl更高,但在开发周期和类库的完整性上是很难保证的。 在本例程中,我们使用了 TShape作为 TDraw的基类,因为 TShape不仅对鼠标的

消息进行了相应的处理,还提供了绘图的 Canvas、Pen、Brush 属性以及 Paint 方法

等,可以使代码更加简化一些,我们仅需要对其中一些方法重载就可以实现大部分要求,

如果希望获得更高的程序性能,可以使用 TControl作为 TDraw的基类。 TDraw所在的单元 ShapeClass的定义文件如下: #ifndef ShapeClassH

#define ShapeClassH

#include <Controls.hpp>

#include <ExtCtrls.hpp>

enum TMouseProperty

{

_MOVE,_LEFT,_RIGHT,_TOP,_BOTTOM,_TOPLEFT,_TOPRIGHT,_BOTTOMLEFT,

_BOTTOMRIGHT

};

//定义图形内部鼠标位置属性的枚举类型,每一个图形对象除了本身显示的图形之外,

//还有 8个表示位置大小等信息的属性控制点。鼠标拖动属性控制点可以对图形的大小

//等进行编辑

//------------------------------------------------------------------

struct TDataToSave

{ //定义用于存储文件和剪贴板交换数据的图形结构,定义这一结构的目的是将

int Top; //需要保存的类成员转换成为顺序存储结构类型

int Left; //在这个结构中没有指针的存在,都是原始的数

int Width; //据类型

int Height; //以上四个成员同控件中的属性,

TColor PenColor; //图形中 Pen的 Color属性

int PenWidth; //Pen画笔的 Width

TPenStyle PenStyle; //画笔的类型

TColor BrushColor; //图形的 Brush的 Color属性

TBrushStyle BrushStyle; //Brush的类型,即填充格式

TShapeType Shape; //图形类型标记

}; //这个结构中的成员是图形对象需要存储或者必须记录

//的信息,属于固有信息,或者叫图形的固有属性,而 Selected则只是在运行时使用的

第七章应用程序中的面向状态 第 418 页

//定义图形对象的类,在本例中,图形对象是从 TShape派生而来

class TDraw : public TShape

{

private:

int Ox; //用于记录鼠标的历史位置,比如鼠标按下时,

int Oy; //用 Ox和 Oy记录鼠标键按下的位置

bool IsMouseDown; //用于记录鼠标是否按下

TRect PropertyRect[8]; //用于记录 8个属性控制点的矩形区域

protected:

void __fastcall SetSelected(bool Value); //Selected属性的设置函数

void __fastcall UpdatePropertyRect(); //用于更新属性控制点的区域

public: //当图形的位置和大小改变时,需要更新属性控制点

virtual TMouseProperty __fastcall MousePosClass(int X,int Y);

//返回这指定点相对图形的位置属性,该函数返回位置属性的同时,也更新

//图形上鼠标显示,对于不同的图形对象从 TDraw派生的可以重载该函数

bool FSelected; //Selected属性的内部存储数据,与其它的

//属性不一样,Selected是和画布背景相关的属性,FSelected使用了公开的数据

virtual void __fastcall SetBounds(int ALeft, int ATop, int AWidth,

int AHeight); //虚拟函数,用于设置控件的

//位置和大小,在这个函数中需要对图形的属性控制点进行更新

virtual void __fastcall Paint(void); //重载的绘图函数

DYNAMIC void __fastcall MouseDown(TMouseButton Button,

TShiftState Shift, int X, int Y); //重载的鼠标函数

DYNAMIC void __fastcall MouseMove(TShiftState Shift,

int X, int Y); //重载的鼠标函数

DYNAMIC void __fastcall MouseUp(TMouseButton Button,

TShiftState Shift, int X, int Y); //重载的鼠标函数

__property bool Selected = {read=FSelected,write=SetSelected};

//Selected属性,用于记录图形是否每选中,同时画布对象中的

__property Color; //发布 Color属性

__fastcall TDraw(); //免参数的构造函数

__fastcall TDraw(int ALeft,int ATop,int ARight,int AButtom);

//指定了位置和大小的构造函数

__fastcall TDraw(int ALeft,int ATop );

//指定了起始位置的构造函数

};

#endif 在这个单元中,TMouseProperty 枚举类型定义了鼠标位于图形中的几种不同状态。

第七章应用程序中的面向状态 第 419 页

对于一个图形对象,当被选择的时候,不仅要将整个图形高亮显示,还需要提供几个用于

编辑的“属性控制点”,使用鼠标拖动属性控制点的时候可以对一个图形对象的大小、长、

宽等特征进行编辑,TDraw对象中将有一个判断当前鼠标位置特征的方法或者属性。 TDataToSave 结构是用于保存数据或者使用剪贴板时使用的,TDraw 的成员很多,

但只有很少一部分是用户真正关心的数据,这些数据就是矢量图形的参数,保存文件或者

使用剪贴板只关心这些参数数据就足够,另一个方面是存储和使用剪贴板的时候,需要一

个顺序存储的数据结构,而 TDraw 类并不是顺序存储的,很多图形参数,比如线型、线

宽等都是对象成员的属性,我们知道对象成员在 VCL类库中只能以指针方式出现,因此定

义这个结构是完成存储和数据交换所必要的。 尽管将 TShape的部分方法重载就可以满足 TDraw的大部分功能,但这远远不够的,

作为 TDraw对象,还应该具备选择、高亮显示、移动等编辑功能,这些是 TShape原本

不具备的,而且在重载的方法中,还需要一些内部使用的成员变量,我们逐一介绍。 Ox、Oy、IsMouseDown 是处理鼠标消息的函数内部使用的,PropertyRect 是 8

个用于高亮显示的属性控制点的位置区域,当图形被选中的时候,这几个矩形区域都处于

高亮显示,鼠标位于属性控制点区域内时,将可以通过鼠标拖动来改变图形的大小。 Selected 属性标识该图形对象是否被选择,SetSelected 为其设置函数,

Selected属性不仅和自身的显示有关,而且和画布中的 Selection属性建立对应关系,

这种关系是一对多的,画布中的 Selection列表中可以为空,也可以包含多个被选择的

对象,同时,当 Selection属性发生改变的时候,也会影响到 MDI主窗体的菜单和按钮

的状态。在这里,Selected 属性的内部存储数据和以前的有所不同,FSelected 是一

个公开的成员,这也是我们前面讲过的,Delphi 中和 C++ Builder 的一个差异。

SetSelected的源代码如下: void __fastcall TDraw::SetSelected(bool Value)

{ //Selected的设置函数

if(FSelected != Value)

{ //在设置图形对象的 FSelected的同时,需要将图像添加到画布的

FSelected = Value; //被选图像列表总

if( Value ) //添加唯一的一个被选图形

((TForm1*)Parent)->AddSelect(this,true);

else //从画布的被选图形列表中移去该图形

((TForm1*)Parent)->RemoveSelect(this,true);

Refresh(); //更新绘图

}//除了 SetSelected方法可以更改 FSelected之外,TForm1的方法也是允许改变这个

} //成员的,因为 Selected和 Selection是一对多的关系,为了不产生环路调用,并且允许 //是唯一的被选择图形或者多个被选择图形之一,必须允许 Form1修改 FSelected

UpdatePropertyRect 方法用于更新 PropertyRect数据的,当控件的位置和大

小等属性发生改变的时候,图形的属性控制点也会跟随变化,在 TDraw 中我们通过重载

第七章应用程序中的面向状态 第 420 页

SetBounds方法,并在 SetBounds 方法中调用 UpdatePropertyRect 方法来完成属

性控制点的更新。UpdatePropertyRect方法的源代码如下: void __fastcall TDraw::UpdatePropertyRect()

{ //属性控制点的计算函数

PropertyRect[0] = TRect(0,0,5,5); //左上角

PropertyRect[1] = TRect(Width/2-2,0,Width/2 + 2,5); //上边中点

PropertyRect[2] = TRect(Width-4,0,Width,5); //右上角

PropertyRect[3] = TRect(Width - 4,Height //右边中点

/2 - 2,Width,Height/2 + 2);

PropertyRect[4] = TRect(Width - 4, Height - 4,Width,Height); //右下角

PropertyRect[5] = TRect(Width/2-2,Height - 4,

Width/2 +2,Height); //下边中点

PropertyRect[6] = TRect(0,Height - 4,5,Height); //左下角

PropertyRect[7] = TRect(0,Height/2 - 2,5, Height /2 + 2); //左边中点

}

SetBounds的源代码如下: void __fastcall TDraw::SetBounds(int ALeft, int ATop, int AWidth, int AHeight)

{

TShape::SetBounds(ALeft,ATop,AWidth,AHeight); //继承父类的方法

if(Parent) //画布是有效的时候,需要将 Modified标记为 true

((TForm1*)Parent)->Modified = true;

UpdatePropertyRect(); //更新属性控制点区域

}

MousePosClass 用于判断指定的坐标处于图形的位置特征是属于图形内部的还是

属性控制点,并且根据位置特征更新图形的鼠标 Cursor类型。这是一个完全供内部使用

的方法,也可以定义为保护类型的。MouseClass的源代码如下: TMouseProperty __fastcall TDraw::MousePosClass(int X,int Y)

{ //根据指定点所在的区域,返回

TPoint Pt = TPoint(X,Y); //位置属性,并设置鼠标指针

if(PtInRect(PropertyRect[0],Pt))

{

Cursor = crSizeNWSE;

第七章应用程序中的面向状态 第 421 页

return _TOPLEFT;

}

else if(PtInRect(PropertyRect[1],Pt))

{

Cursor = crSizeNS;

return _TOP;

}

//其它点位代码类同,请读者参考源码

else if(PtInRect(PropertyRect[7],Pt))

{

Cursor = crHSplit; return _LEFT;

}

else //缺省的状态,是 Move

{

Cursor = crSizeAll; return _MOVE;

}

}

Paint方法是重载 TShape的方法,并且完全抛弃了原有的 Paint,没有继承,在

Piant方法中,图形需要根据线型、线宽、颜色、填充方式以及图形的形状等参数将图形

正确的绘制在 Canvas上,当 Selected属性为 true的时候,图形还有进行高亮显示并

绘制属性控制点。Paint方法源代码如下: void __fastcall TDraw::Paint(void)

{ //图形对象的绘图函数,TForm1对象为画布,所有图形均绘制在 Panret对象上

//本例中 TDraw只能在 TForm1对象上绘制

if(!((TForm1*)Parent)->AutoUpDate )

return; //AutoUpDate用于记录是否自动更新,这个目的是防止多个

//图形对象同时操作的时候,逐一触发更新消息,而引起图形闪烁或者效率下降

Canvas->Pen->Assign(Pen); //使用图形对象中的画笔 Pen

Canvas->Brush->Assign(Brush); //使用图形对象中的 Brush

switch (Shape) //根据不同的 Shape选择的绘图方法

{

case stRectangle: //绘图区域比控件的区域小 2个像素,这是由于

Canvas->Rectangle(2,2,Width-2,Height-2); //属性控制点的区域可能会在

break; //图形区域之外,但是属于对象中的点

case stEllipse: //椭圆同样缩小两个像素

第七章应用程序中的面向状态 第 422 页

Canvas->Ellipse(2,2,Width-2,Height-2); break;

}

if(Selected) //若图形被选择,则使用 clLime颜色绘制出

{ //属性控制点,并且使用 psDot线型以单线

Canvas->Brush->Style = bsSolid; //覆盖绘制图像区域

Canvas->Brush->Color = clLime;

Canvas->Pen->Width = 2; //线宽为 2可以将矩形绘制成实心

Canvas->Pen->Color = clLime;

Canvas->Rectangle(PropertyRect[0]); //左上角

Canvas->Rectangle(PropertyRect[1]);

Canvas->Rectangle(PropertyRect[2]);

Canvas->Rectangle(PropertyRect[3]);

Canvas->Rectangle(PropertyRect[4]);

Canvas->Rectangle(PropertyRect[5]);

Canvas->Rectangle(PropertyRect[6]);

Canvas->Rectangle(PropertyRect[7]);

Canvas->Brush->Style = bsClear; //bsClear填充格式不进行内部填充

Canvas->Pen->Width = 1; //只绘制出外框

Canvas->Pen->Style = psDot;

Canvas->Rectangle(2,2,Width-2,Height-2);

}

}

TDraw的 MouseDown、MouseUp 和 MouseMove都属于编辑使用的方法,我们在后

面介绍,TDraw有三个不同版本的构造函数,属于创建新图形的方法,我们也放在后面介

绍。

3.4 个体编辑

我们将图形的编辑划分为个体编辑和整体编辑是为了明确编辑动作的实施者,所谓个

体编辑只和图形自身有关,不涉及到其它的图形对象,而整体编辑则是对一个图形或者几

个图形同时进行编辑,并且编辑是和其它图形有关联的,因此整体编辑适合由 TForm1 对

象来实现,而个体编辑适合由 TDraw 对象来完成。在个体编辑的功能中有创建新图形和

拖动编辑两种,实际上创建新图形并不是 TDraw单独完成的,也是需要 TForm1来发起

的。

3.4.1 创建新的图形

创建一个新图形的过程是这样的:首先,TForm1的绘图工具不处于 Select 状态时,

第七章应用程序中的面向状态 第 423 页

在画布上按下鼠标左键,拖动到适当位置释放,释放鼠标左键时,根据所选择的绘图工具

在鼠标拖动的区域创建相应的图形对象,并按照缺省的线宽、线型、颜色、填充方式等设

置新建图形的属性,与此同时还要正确的建立图形对象和 TForm1对象之间的逻辑关系,

VCL对象所必须的逻辑关系是 Parent 和 Child的关系,作为我们图形编辑还存在的逻

辑关系是 Form1的 Draw属性必须添加新建的图形。 TForm1的 OnMouseDown事件处理函数如下: void __fastcall TForm1::DrawMouseDown(TObject *Sender, TMouseButton Button,

TShiftState Shift, int X, int Y)

{ //鼠标事件的处理函数,当左键按下时进行操作

if(Button == mbLeft)

{

MouseDown = true; //记录鼠标被按下

OrMousePos.x = X; //记录鼠标始按下的原位置

OrMousePos.y = Y;

MousePos.x = X; //记录鼠标消息的当前位置

MousePos.y = Y; //按下时原始位置和当前位置相同

UnSelected(); //清除画布上被选择的图形列表

UpDateButton(); //更新 MDI主窗体的按钮显示

Invalidate(); //强迫重新绘图

}

}

TForm1的 OnMouseMove事件处理函数如下: void __fastcall TForm1::DrawMouseMove(TObject *Sender, TShiftState Shift,

int X, int Y)

{ //鼠标移动事件的处理函数

if(MouseDown)

{ //当左键被按下时移动鼠标按下面操作

Canvas->Rectangle(TRect(OrMousePos,MousePos)); MousePos.x = X; //绘制鼠标轨迹的区域,蓝色虚线矩形

MousePos.y = Y; //按照异或算法绘图,二次绘制复原

Canvas->Rectangle(TRect(OrMousePos,MousePos));

} //画布上始终显示原始鼠标位置和鼠标当前移动到的位置

} //所定义的矩形

上面这两个事件的处理方法并不复杂,最复杂的是释放鼠标按键时需要根据不同的绘

图工具类型进行不同的操作,TForm1的 OnMouseUp事件处理函数如下:

第七章应用程序中的面向状态 第 424 页

void __fastcall TForm1::DrawMouseUp(TObject *Sender, TMouseButton Button,

TShiftState Shift, int X, int Y)

{ //释放鼠标按键的处理函数,在不同的状态下,所作的操作不同

if(MouseDown && Button == mbLeft) //仅在鼠标左键按下并释放时动作

{ //忽略其它按键以及键盘状态

TDraw * NewDraw; //临时使用的图形对象指针

int Temp;

Canvas->Rectangle(TRect(OrMousePos,MousePos)); //释放鼠标时,清除

if( OrMousePos.x > MousePos.x ) //鼠标移动轨迹的矩形

{ //调整按下鼠标按键的位置和释放鼠标

Temp = OrMousePos.x; //按键的位置,

OrMousePos.x = MousePos.x; //使 OrMousePos为鼠标移动区域矩形的

MousePos.x = Temp; //左上角

} //MousePos为右下角

if( OrMousePos.y > MousePos.y )

{

Temp = OrMousePos.y;

OrMousePos.y = MousePos.y;

MousePos.y = Temp;

} //若鼠标的 X方向和 Y方向的移动距离不为 0时有效。

if(OrMousePos.x != MousePos.x && OrMousePos.y != MousePos.y)

{ //根据不同的 Shape类型作不同的操作

if(Shape != _Select) //绘图工具不为"选择"的时候

{ //则创建新的图形对象

NewDraw = new TDraw(OrMousePos.x,OrMousePos.y,

MousePos.x + 1,MousePos.y + 1);

NewDraw->Pen->Width = DefaultLineWidth; //按照缺省的线宽

NewDraw->Pen->Color = DefaultColor; //缺省的颜色

NewDraw->Pen->Style = DefaultPenStyle; //缺省的线型

NewDraw->Brush->Style = DefaulyBrushStyle; //以及缺省的填充

NewDraw->Parent = this; //在画布上绘制新建的图形

NewDraw->Color = clWhite; //填充的颜色始终为白色

Draw->Add(NewDraw); //添加到图形列表中

Modified = true; //设置画布的状态为被修改

} switch (Shape) //根据 Shape工具类型继续进行以下操作

{

case _Select: //Shape为选择工具时, 选择区域内的图形,

第七章应用程序中的面向状态 第 425 页

{ //按照鼠标移动的方向,分作两种选择方式,

TRect Rect; //从左向右的时候,选择鼠标移动区域内包含的图形

Rect = TRect(OrMousePos,MousePos); //即图形对象的四个

TDraw* DrawTemp; //临时变量 //顶点都在矩形区域之内

if(MousePos.x > X) //从左到右的时候,MousePos.x和 X相等

for(int i = 0;i < Draw->Count; i++ )

{ //逐一遍历图形列表的每一个元素

DrawTemp = (TDraw*)Draw->Items[i];

if(PtInRect(Rect,TPoint(DrawTemp->Left,

DrawTemp->Top))||

PtInRect(Rect,TPoint(DrawTemp->Left +

DrawTemp->Width,DrawTemp->Top))||

PtInRect(Rect,TPoint(DrawTemp->Left,DrawTemp->Top +

Height))||

PtInRect(Rect,TPoint(DrawTemp->Left +

DrawTemp->Width,DrawTemp->Top +

DrawTemp->Height)))

{ //只要上下左右四个顶点之一在鼠标区域之内的图形都添加到

Selection->Add(DrawTemp); //选择列表中

DrawTemp->FSelected = true; //并设置 FSelected标记

}

}

else //反之则是从左向右,逐一遍历图形列表中的元素

for(int i = 0;i < Draw->Count; i++ )

{ //上下左右四个顶点所有的都在矩形区域之内的时候

DrawTemp = (TDraw*)Draw->Items[i]; if(PtInRect(Rect,TPoint(DrawTemp->Left,

DrawTemp->Top))&&

PtInRect(Rect,TPoint(DrawTemp->Left +

DrawTemp->Width,DrawTemp->Top))&&

PtInRect(Rect,TPoint(DrawTemp->Left,DrawTemp->Top +

DrawTemp->Height))&&

PtInRect(Rect,TPoint(DrawTemp->Left +

DrawTemp->Width,DrawTemp->Top +

DrawTemp->Height)))

{ //则将该图形添加到选择列表清单中

Selection->Add(DrawTemp); //并设置 FSelected

DrawTemp->FSelected = true; //标记

}

第七章应用程序中的面向状态 第 426 页

} Refresh(); //刷新图形显示

UpDateButton(); //更新 MDI主窗体按钮

break;

} //当 Shape工具为矩形时

case _Rect:

NewDraw->Shape = stRectangle; //将新建的图形对象设置为

break; //矩形

case _Circle: //工具为椭圆时

NewDraw->Shape = stEllipse; //将新建的图形对象设置为

break; //椭圆

case _Line: //画线工具空置,未作相应

//的处理代码

break;

}

} MouseDown = false; //将鼠标按键标记为释放

}

} TForm1 的 MouseUp 事件中,除了创建图形之外,还有选择图形的工具,本程序中

画布的工具处于 Select状态的时候,提供了两中不同的选择方式,一种是从左向右拖动

鼠标,这时要求图形完全包含在鼠标区域之内才能够被选择,另一种方式是从右向左拖动

鼠标,此时只要图形的上、下、左、右四个顶点之一包含在鼠标区域之内的图形都会被选

中。当图形工具为矩形、椭圆或者线段的时候,Form1根据鼠标位置创建新的图形,TDraw的构造函数如下:

__fastcall TDraw::TDraw(int ALeft,int ATop,int ARight,int AButtom)

:TShape(NULL)

{

Left = ALeft - 2; //构造函数给出的是图形的区域

Top = ATop - 2; //控件的实际大小比图形区域大

Width = ARight - ALeft + 2; //两个像素

Height = AButtom - ATop + 2;

UpdatePropertyRect(); //计算属性控制点的区域

} __fastcall TDraw::TDraw(int ALeft,int ATop ) : TShape(NULL)

{

Left = ALeft - 2; //只指定了位置左上角时按照

第七章应用程序中的面向状态 第 427 页

Top = ATop - 2; //缺省的大小创建对象

Width = 100;

Height = 30;

UpdatePropertyRect();

}

TDraw总共有三个构造函数,其中一个是空的,我们就不介绍了,那是为文件读取和

剪贴板而使用的,程序中用户创建新图形的构造函数是带有四个位置坐标的构造函数,

TDraw的实际大小要比图形大两个像素,因为图形对象被选中时显示的属性控制点要超出

图形的范围。带有起始位置的构造函数在本例程中并没有使用,和带有四个参数的构造函

数大致一样,只是使用了缺省的大小来创建新图形。

3.4.2 拖动改变位置及大小

拖动编辑是一个纯粹的由 TDraw 自身完成的编辑动作,主要功能是鼠标位于属性控

制点内的时候,可以使用左键拖动改变属性控制点的位置,实际上就是改变了图形的上、

下、左、右边界或者四个顶点的位置,TDraw的鼠标处理我们使用了重载函数来实现,而

不是 TForm1中那样使用事件句柄来实现的,两者实现起来没有本质的区别,但也稍有差

异,在 Form1 中使用事件句柄是为了在不同的状态可以通过更改事件的处理句柄来改变

鼠标的响应动作,而在 TDraw 中使用重载函数的方法来响应鼠标消息,则是为了更好的

体现多态性优点,当 TDraw的派生类具有和 TDraw不太一样的鼠标动作时,只需要重载

该函数就可以实现,这比使用事件句柄更方便合理一些。 鼠标按键按下的处理函数如下: void __fastcall TDraw::MouseDown(TMouseButton Button,

TShiftState Shift, int X, int Y)

{ //重载鼠标的点击函数,用于处理鼠标按下的动作

if(Button == mbLeft) //只对左键反应

{

if(Shift.Contains(ssShift)) //若 Shift键同时按下,则允许多个图形

{ //对象被同时选中

IsMouseDown = true; //记录鼠标已经按下

Ox = X; //同时记录按下的位置坐标

Oy = Y;

if(FSelected) //如果对象原本就处于

{ //被选择状态

FSelected = false; //则从被选图形列表中移去该图像

((TForm1*)Parent)->RemoveSelect(this,false);

Refresh(); //更新图形显示

第七章应用程序中的面向状态 第 428 页

} else //若还没有处于选择状态,则加入

{ //画布的被选择图像列表中

FSelected = true;

((TForm1*)Parent)->AddSelect(this,false);

Refresh(); //刷新图像显示

}

}

else //若没有按下 Shift键

{ //则记录鼠标已经按下,并记录

IsMouseDown = true; //相应坐标,同时设置 Selected属性

Selected = true; //Selected属性会自动清除其它

Ox = X; //被选择的图形对象

Oy = Y;

}

}

}

鼠标指针移动的处理函数如下: void __fastcall TDraw::MouseMove(TShiftState Shift,

int X, int Y)

{

TMouseProperty MouseProperty; //记录鼠标位置属性的临时变量

if(IsMouseDown) //若鼠标左键被按下

{ //获取按下鼠标键时鼠标的位置属性

MouseProperty = MousePosClass(Ox,Oy);

switch (MouseProperty) //根据不同的位置属性来决定不同

{ //的操作

case _LEFT: //根据鼠标的移动改变图形的左边界

SetBounds(Left - Ox + X,Top,Width + Ox - X,Height); break;

case _RIGHT: //根据鼠标的移动改变图形的右边界

SetBounds(Left,Top,Width - Ox + X,Height);

Ox = X;

break;

case _TOP: //根据鼠标的移动改变图形的上边界

SetBounds(Left,Top - Oy + Y,Width,Height + Oy - Y);

break;

第七章应用程序中的面向状态 第 429 页

case _BOTTOM: //根据鼠标的移动改变图形的下边界

SetBounds(Left,Top,Width,Height - Oy + Y);

Oy = Y;

break;

case _TOPLEFT: //根据鼠标的移动改变图形的左、上边界

SetBounds(Left - Ox + X,Top - Oy + Y,Width + Ox - X,

Height + Oy - Y);

break;

case _TOPRIGHT: //根据鼠标的移动改变图形的右、上边界

SetBounds(Left,Top - Oy + Y,Width - Ox + X,Height + Oy - Y);

Ox = X; break;

case _BOTTOMLEFT: //根据鼠标的移动改变图形的左、下边界

SetBounds(Left - Ox + X,Top,Width + Ox - X,Height - Oy + Y);

Oy = Y;

break;

case _BOTTOMRIGHT: //根据鼠标的移动改变图形的右、下边界

SetBounds(Left,Top,Width - Ox + X,Height - Oy + Y);

Ox = X;

Oy = Y;

break;

case _MOVE: //根据鼠标的移动改变图形的位置

((TForm1*)Parent)->MoveSelection(X - Ox,Y - Oy);

} //使用画布的方法进行整体移动

}

else //利用该函数根据当前鼠标位置

MousePosClass(X,Y); //进行鼠标指针的设置

}

释放鼠标按键的数量函数如下: void __fastcall TDraw::MouseUp(TMouseButton Button,

TShiftState Shift, int X, int Y)

{

if(IsMouseDown && Button == mbLeft)

IsMouseDown = false; //鼠标键弹起,只对左键反应

}

第七章应用程序中的面向状态 第 430 页

3.5 整体编辑

整体编辑通常是通过 TForm1对象来完成的,整体编辑指的是对一个或多个图形对象

同时进行某种编辑,而且是相对于整个画布的编辑动作,包含移动图形、删除图形和改变

图形的属性,和个体编辑一样,整体编辑首先需要选中被编辑的对象,整体编辑是对所有

被选中的图形进行的。

3.5.1 移动所选图形

移动图形是由被选择的图形所发起的,而且只有鼠标位于被选择的图形的内部,并不

在属性控制点内的时候,才能进行的,但是操作是由 TForm1对象来完成的,读者可以参

考 TDraw的 MouseMove方法。 在 TForm1中我们提供了移动被选择图形的方法 MoveSelection,源代码如下: void __fastcall TForm1::MoveSelection(int DeltaX,int DeltaY)

{ //移动被选择图形的方法

AutoUpDate = false; //禁止图形对象自动更新而闪烁

for(int i = 0; i < Selection->Count; i++)

((TDraw*)Selection->Items[i])->SetBounds(((

TDraw*)Selection->Items[i])->Left + DeltaX,

((TDraw*)Selection->Items[i])->Top + DeltaY,

((TDraw*)Selection->Items[i])->Width,

((TDraw*)Selection->Items[i])->Height);

AutoUpDate = true; //使用图形的 SetBounds方法进行移动

Modified = true; //设置 Modified标记并更新画布

Refresh();

}

AutoUpDate 用于 TDraw图形的 Paint方法是否绘图,整体移动的时候,需要使之

无效,这样 TDraw 不会在每一个图形移动的时候都刷新显示界面而产生闪烁,整体移动

结束后,统一使用 Refresh进行画布的刷新。

3.5.2 删除所选图形

删除动作是由 MDI主窗体发起的,但是操作也是有画布完成的,当用户选择了图形对

象的时候,MDI主窗体的 Delete按钮和菜单项都会变为有效,删除动作就是由删除按钮

和菜单发起的,相应的代码如下: void __fastcall TForm2::DeletButtonClick(TObject *Sender)

{

第七章应用程序中的面向状态 第 431 页

if(ActiveDraw)

ActiveDraw->Delete();

}

void __fastcall TForm1::Delete()

{ //将画布中所选择的图形删除

if( Selection->Count > 0) //必须使用逆序循环,否则代码复杂的多

for(int i = Selection->Count - 1; i >= 0; i--)

{ //首先从图形列表中清除

Draw->Delete(Draw->IndexOf(Selection->Items[i]));

delete (TDraw*)Selection->Items[i]; //销毁对象

Selection->Delete(i); //从被选择列表中清除

}

}

3.5.3 改变图形属性

改变图形的属性包括了四个部分的内容,分别和图形属性工具条上的四个控件对应,

这部分功能可以和 Delete、Move一样,由 TForm1 来完成,甚至是由 TForm1的属性

来完成,同样 TForm2的控件状态只和这几个属性发生关系,通过改变属性就可以完成图

形属性的编辑。本例中没有这样作,因为多文档界面中一个 MDI主窗体和多个 MDIChild窗体对应,使用 TForm1 的属性并不能完全实现对 MDI 主窗体界面的刷新,因此使用了

方法实现界面的刷新,编辑图形的属性也是由 TForm2发起并完成的,具体代码如下: 改变填充方式的方法如下: void __fastcall TForm2::FillButtonClick(TObject *Sender)

{

if(!ActiveDraw) //确保当前有活动的图形文件

return;

if(FillButton->Down) //改变后的状态为实心填充时

if(ActiveDraw->Selection->Count > 0) //并且活动图形文件中有选择图形

for(int i = 0; i < ActiveDraw->Selection->Count; i++)

((TDraw*)ActiveDraw->Selection->Items[i])->Brush->Style

= bsSolid; //逐一改变填充方式

else //若没有被选择图形时,改变缺省的填充方式

ActiveDraw->DefaulyBrushStyle = bsSolid;

else //若改变为 bsClear不填充时

if(ActiveDraw->Selection->Count > 0) //并且活动图形文件中有选择图形

for(int i = 0; i < ActiveDraw->Selection->Count; i++)

第七章应用程序中的面向状态 第 432 页

((TDraw*)ActiveDraw->Selection->Items[i])->Brush->Style = bsClear; //逐一改变填充方式

else //若没有被选择图形时,改变缺省的填充方式

ActiveDraw->DefaulyBrushStyle = bsClear;

}

改变线宽的的方法如下:

void __fastcall TForm2::ComboBoxEx1Change(TObject *Sender)

{

if(!ActiveDraw) //确保当前有活动的图形文件

return; //当选择的图形不为 0时,逐一改变图形的线宽

if(ActiveDraw->Selection->Count > 0)

for(int i = 0; i < ActiveDraw->Selection->Count; i++)

((TDraw*)ActiveDraw->Selection->Items[i])->Pen->Width =

ComboBoxEx1->ItemIndex + 1; else //当选择的图形为 0时,改变画布缺省的线宽

ActiveDraw->DefaultLineWidth = ComboBoxEx1->ItemIndex + 1;

}

改变颜色的方法如下:

void __fastcall TForm2::ColorBox1Change(TObject *Sender)

{

if(!ActiveDraw) //确保当前有活动的图形文件

return; //当选择的图形不为 0时,逐一改变图形的颜色

if(ActiveDraw->Selection->Count > 0)

for(int i = 0; i < ActiveDraw->Selection->Count; i++)

((TDraw*)ActiveDraw->Selection->Items[i])->Pen->Color =

ColorBox1->Selected;

else //当选择的图形为 0时,改变画布缺省的颜色

ActiveDraw->DefaultColor = ColorBox1->Selected;

}

改变线型的方法如下: void __fastcall TForm2::ComboBoxEx2Change(TObject *Sender)

{

if(!ActiveDraw) //确保当前有活动的图形文件

第七章应用程序中的面向状态 第 433 页

return; //当选择的图形不为 0时,逐一改变图形的线型

if(ActiveDraw->Selection->Count > 0)

for(int i = 0; i < ActiveDraw->Selection->Count; i++)

((TDraw*)ActiveDraw->Selection->Items[i])->Pen->Style =

(TPenStyle)ComboBoxEx2->ItemIndex ;

else //当选择的图形为 0时,改变画布缺省的线型

ActiveDraw->DefaultPenStyle = (TPenStyle)ComboBoxEx2->ItemIndex;

}

3.6 文件存取功能

文件存取功能是一个编辑软件的基本功能,如果不能保存数据,所作的一切编辑工作

都等于 0,幸好在 C++ Builder中,Borland 为我们提供了更加便捷的文件操作方式,

这就是流式文件对象 TFileStream。 要实现文件操作,除了 TFileStream 对象之外,还需要确定要保存的数据格式,对

于一个图形对象而言,尽管 TShape对象中有很多成员,但是只有很少一部分是真正用户

创建的图形的要素,TDraw 的大多数成员都是在程序运行时所用的,包括我们定义的 8个属性控制点的区域以及 Selected 属性,这些都没有必要保存的,有一些属性比如

Parent,每次运行都是不一样的,因此我们必须筛选图形对象中属于图形参数的数据进

行保存,同时在保存的时候,需要将这些数据转换为顺序存储结构的数据,而不能是指针

等非顺序存储的结构。 顺序存储数据结构我们前面的声明文件中已经包括了,这里不在介绍。文件操作功能

至少包括三个功能:一是保存文件;二是另存为新的文件;三是读取文件。保存文件是按

照一定的个数和顺序将数据和相应的文件信息保存到文件中,另存为是按照新指定的文件

将数据存储,读取则是存储的逆向过程,文件的保存、读取和另存为新文件都是由 TForm1完成的,整个程序中这几个操作都是由 TForm2发起的。

3.6.1 保存文件

我们在 TForm1中提供了用于保存文件的方法 Save,其源代码如下: void __fastcall TForm1::Save(void)

{ //画布对象的存储方法

TFileHeader Header; //文件的标记头,同剪贴板中的标记头

TDataToSave Data;

TDraw* DrawItem; Header.DrawCount = Draw->Count; //记录图形的数量

if(!FileStream) //若没有打开文件,即新建的图形文件

{ //则调用 MDI主窗体的 SaveAs处理句柄

Form2->SaveDialog1->FileName = Caption;

第七章应用程序中的面向状态 第 434 页

Form2->SaveAsClick(this); //并以画布自身为 Sender触发

}

else if(Modified) //图形已经属于打开的文件

{ //则存储图形

delete FileStream; //重新创建 FileStream可以完整的覆盖

FileStream = new TFileStream(File,fmCreate); //原有文件的内容

FileStream->Write(&Header,sizeof(TFileHeader)); //写入标记头

for(int i = 0; i < Draw->Count; i++) //逐一写入图形对象

{

DrawItem = (TDraw*)Draw->Items[i];

Data.Top = DrawItem->Top;

Data.Left = DrawItem->Left;

Data.Width = DrawItem->Width;

Data.Height = DrawItem->Height;

Data.PenColor = DrawItem->Pen->Color;

Data.PenStyle = DrawItem->Pen->Style;

Data.PenWidth = DrawItem->Pen->Width;

Data.BrushColor = DrawItem->Brush->Color;

Data.BrushStyle = DrawItem->Brush->Style;

Data.Shape = DrawItem->Shape;

FileStream->Write(&Data,sizeof(TDataToSave)); //写入文件

}

Modified = false; //存储后标记为未被更改

}

}

在 Save方法中,要对文件进行保存,必须确保文件对象已经存在,如果没有文件流

式对象,说明是新建的图形文档,因此调用 Form2 的 SaveAs事件的处理句柄,并且以

this作为 Sender参数,在 Form2的 SaveAs事件处理函数中根据 Sender参数来判

断是菜单和按钮触发的 SaveAs事件还是由 Form1对象触发的 SaveAs事件,这样作的

目的是为了使 Form1的 Save方法消除相关性,不管 Form1是否处于激活状态,其 Save方法总是可以保证在调用 Form2 的 SaveAs 句柄时,能够的到正确的处理。Form2 的

SaveAs句柄在打开存储对话框后,会调用活动文档的 SaveAs或者 Sender 的 SaveAs方法进行存储,这取决于 SaveAs事件句柄是由谁触发的。

3.6.2 另存为其它文件

另存为其它文件的发起者有两种情况,一种是用户点击 MDI主窗体的菜单项和另存为

按钮,另一种情况是 Form1 对象在 Save 方法中发起的。第一种情况,由事件的发起者

第七章应用程序中的面向状态 第 435 页

直接调用活动文档的 SaveAs方法进行存储,第二种情况则是由 Form1 的 Save方法调

用 Form2的 SaveAs 处理函数,然后间接的调用 Form1对象的 SaveAs方法进行存储。 Form1对象的 SaveAs方法需要重新按照指定的文件名创建新的文件,然后调用自身

的 Save 方法将数据保存。以下是 TForm2 的 SaveAs 事件的处理函数和 TForm1 的

SaveAs方法。 void __fastcall TForm2::SaveAsClick(TObject *Sender)

{

if(!ActiveDraw) //确保有活动的文档

return;

if(SaveDialog1->Execute()) //打开存储对话框

if(Sender == SaveAsButton || Sender == SaveAs1) //判断事件的触发者

ActiveDraw->SaveAs(SaveDialog1->FileName);

else //当该事件不是按钮和菜单项触发的时候,调用 Sender的 SaveAs方法

((TForm1*)Sender)->SaveAs(SaveDialog1->FileName);

}

void __fastcall TForm1::SaveAs(AnsiString FileName)

{ //画布的 SaveAs方法 ,需要指定文件名称

File = FileName; //保存文件名称到成员 File中

Caption = File; //更换画布窗体的标题

FileStream = new TFileStream(File,fmCreate);

Modified = true; //创建指定名称的文件,文件已经存在则覆盖

Save(); //设置 Modified并调用 Save方法进行存储

}

3.6.3 读取文件

读取文件是存储过程的逆向过程,这个功能是由 MDI 主窗体 TForm2 对象发起的,

同时 TForm2对象需要创建一个新的 TForm1对象,并指定文件,调用 Form1对象的 Open方法打开文件读取数据。

TForm2的打开文件的菜单和按钮的处理函数如下: void __fastcall TForm2::OpenClick(TObject *Sender)

{ if(OpenDialog1->Execute()) //打开文件对话框

{

if(ActiveDraw) //如果有空文件,则将其关闭

if(ActiveDraw->File == "" && !ActiveDraw->Modified)

第七章应用程序中的面向状态 第 436 页

ActiveDraw->Close(); TForm1* NewDraw = new TForm1(NULL); //创建新的画布对象 TForm1

NewDraw->OpenFile(OpenDialog1->FileName); //打开指定文件

}

}

TForm1的 Open方法如下: void __fastcall TForm1::OpenFile(AnsiString FileToOpen)

{ //打开文件并读入图形数据

TFileHeader Header; //文件标记头的临时变量

TDataToSave Data; //临时变量

TDraw * DrawItem; //临时变量

File = FileToOpen; //记录打开文件的名称

Caption = File; //设置窗体标题

FileStream = new TFileStream(File,fmOpenReadWrite); //打开文件

FileStream->Read(&Header,sizeof(TFileHeader)); //读取文件头

for(int i = 0; i < Header.DrawCount; i++)

{ //根据文件头的信息逐一读入数据

DrawItem = new TDraw();

FileStream->Read(&Data,sizeof(TDataToSave));

DrawItem->Top = Data.Top;

DrawItem->Left = Data.Left;

DrawItem->Width = Data.Width;

DrawItem->Height = Data.Height;

DrawItem->Pen->Color = Data.PenColor;

DrawItem->Pen->Style = Data.PenStyle ;

DrawItem->Pen->Width = Data.PenWidth;

DrawItem->Brush->Color = Data.BrushColor;

DrawItem->Brush->Style = Data.BrushStyle;

DrawItem->Shape = Data.Shape; DrawItem->Parent = this;

Draw->Add(DrawItem);

}

Modified = false; //新打开的文件是未被更改的

}

第七章应用程序中的面向状态 第 437 页

3.7 使用剪贴板

对剪贴板的使用和文件操作一样,也是编辑的一个基本功能,我们可以采用类似文件

操作的方式,但在这个例子中,我们采用了 VCL提供的类 TClipboard作为基类,派生

了一个专门用于该程序的剪贴板类 TDrawClipborad。

对 Windows 剪贴板的使用需要分配全局的堆内存,C++ Builder 中提供了对

TComponent 对象的拷贝、粘贴的方法,但每次只能对一个对象操作,而且是将所有的数

据拷贝,并不适合我们的程序,我们将在 TClipboard 的基础上建立自己的专用剪贴板

对象。 使用通用拷贝函数 Assign是一个不错的选择,前面我们也讲过,Assign方法具有

非常好的向后兼容性和扩展性,在 TDrawClipborad 中重载 Assign和 AssignTo 方法

就可以到达这个目的。为了能够正确的交换数据,我们必须向 Windows 注册特定的剪贴

板格式来区分其它程序的数据,关于这一部分内容请读者参考 Windows SDK。 另外,剪贴板中交换数据实际上和文件类似,最后使用顺序存储结构的数据,前面定

义的 TDataToSave在这里也同样能够使用。

3.7.1 复制数据

使用TDrawClipborad的 Assing方法将 TForm1对象的图形数据拷贝到剪贴板中,

这一个过程是由 TForm2对象发起的,下面是它们的源代码: 复制按钮和菜单的 OnClick事件的处理函数: void __fastcall TForm2::CopyButtonClick(TObject *Sender)

{

if(ActiveDraw) //确保当前活动画布有效

{

((TDrawClipborad*)Clipboard())->Assign(ActiveDraw);

ActiveDraw->UpDateButton();

}

}

TDrawClipboard的 Assign方法 void __fastcall TDrawClipborad::Assign(Classes::TPersistent* Source)

{ //剪贴板的拷贝函数

TForm1* Src; //临时变量

TDataToSave * DataItem; //临时变量

TFileHeader * Header; //临时变量

char *DataPoint; //临时变量

TDraw* DrawCopyData; //临时变量

第七章应用程序中的面向状态 第 438 页

int i; //临时变量

Src = (TForm1*)Source; //目的使代码中不出现太多的类型转换

if(Src->Selection->Count == 0) //指定的画布 Selection列表为空

return; //则放弃操作

Open(); //打开剪贴板,禁止其它程序访问

Handle = (void*)GetAsHandle(Form2->ClipBoardFormat);

if(Handle) //获取相应数据格式的句柄,将原有的

GlobalFree(Handle); //数据清除

Handle = GlobalAlloc(GMEM_SHARE|GMEM_MOVEABLE, //重新分配全局内存

sizeof(TDataToSave) * Src->Selection->Count +

sizeof(TFileHeader)); //按照图形数量和标记头

DataPoint = (char*)GlobalLock(Handle); //所需要的大小分配内存

Header = (TFileHeader*)DataPoint; //锁定内存并转化为标记头指针

Header->DrawCount = Src->Selection->Count; //用于记录拷贝图形的数量

DataItem = (TDataToSave*)(DataPoint + sizeof(TFileHeader));

for( i = 0 ; i < Src->Selection->Count; i++ ) //从标记头的结尾处开始

{ //写入被拷贝图形的相应数据

DrawCopyData = (TDraw*)Src->Selection->Items[i];

DataItem[i].Top = DrawCopyData->Top; //以下是需要存储所必须的数据

DataItem[i].Left = DrawCopyData->Left; //也是图形对象的用户数据

DataItem[i].Width = DrawCopyData->Width;

DataItem[i].Height = DrawCopyData->Height;

DataItem[i].PenColor = DrawCopyData->Pen->Color;

DataItem[i].PenWidth = DrawCopyData->Pen->Width;

DataItem[i].PenStyle = DrawCopyData->Pen->Style;

DataItem[i].BrushColor = DrawCopyData->Brush->Color;

DataItem[i].BrushStyle = DrawCopyData->Brush->Style;

DataItem[i].Shape = DrawCopyData->Shape;

} //释放内存指针,将内存的

GlobalUnlock(DataPoint); //句柄交给剪贴板

SetAsHandle(Form2->ClipBoardFormat,(int)Handle);

Close(); //关闭剪贴板

}

3.7.2 粘贴数据

粘贴数据是复制数据的逆向过程,当需要从剪贴板中粘贴数据的时候,只需要调用

TForm1的 Assign方法即可,在 TForm1中我们并没有重新定义 Assign 方法,当使用

第七章应用程序中的面向状态 第 439 页

TDrawClipboard 对 象 作 为 TForm1 的 复 制 源 的 时 候 , 最 终 实 际 调 用 的 是

TDrawClipboard 的 AssignTo方法来完成数据复制的,这个机制我们在前面章节中已

经讲过了,这里不在赘述,TDrawClipboard的 AssignTo方法的代码如下: void __fastcall TDrawClipborad::AssignTo(Classes::TPersistent* Dest)

{ //剪贴板的赋值方法

TForm1* DestDraw; //是和拷贝方法相反的

TDataToSave * DataItem; //过程

TFileHeader * Header;

char *DataPoint;

TDraw* DrawCopyData; int i;

DestDraw = (TForm1*)Dest;

Open(); //打开剪贴板,获取所需

Handle = (void*)GetAsHandle(Form2->ClipBoardFormat);

if( !Handle ) //数据格式的句柄

{ //为空表示没有相应格式的

Close(); //数据,关闭剪贴板并返回

return;

}

DataPoint = (char*)GlobalLock(Handle); //锁定内存为指针

if(!DataPoint) //若锁定后指针无效

{ //说明拷贝的数据块

GlobalUnlock(DataPoint); //无效,释放指针、

Close(); //关闭剪贴板并返回

return;

}

Header = (TFileHeader*)DataPoint; //读取标记头

if(Header->DrawCount == 0) //若复制的图形数量

{ //为 0,表示数据无效

GlobalUnlock(DataPoint); //释放指针,关闭剪贴板

Close(); //并返回,本应加入对标记头

return; //验证的代码,这里省略

} //从标记头结尾处开始

DataItem = (TDataToSave*)(DataPoint + sizeof(TFileHeader));

DestDraw->UnSelected(); //读取拷贝的数据,并清除

for( i = 0 ; i < Header->DrawCount; i++ ) //原有被选择图形

{

DrawCopyData = new TDraw();

第七章应用程序中的面向状态 第 440 页

DrawCopyData->Top = DataItem[i].Top;

DrawCopyData->Left = DataItem[i].Left;

DrawCopyData->Width = DataItem[i].Width;

DrawCopyData->Height = DataItem[i].Height;

DrawCopyData->Pen->Color = DataItem[i].PenColor;

DrawCopyData->Pen->Width = DataItem[i].PenWidth;

DrawCopyData->Pen->Style = DataItem[i].PenStyle;

DrawCopyData->Brush->Color = DataItem[i].BrushColor;

DrawCopyData->Brush->Style = DataItem[i].BrushStyle;

DrawCopyData->Shape = DataItem[i].Shape;

DrawCopyData->Parent = DestDraw; //设置画布为指定的 Dest

DrawCopyData->FSelected = true; //将拷贝获取的图形设置为

DestDraw->Draw->Add(DrawCopyData); //新的被选择对象

DestDraw->AddSelect(DrawCopyData,false);

}

GlobalUnlock(DataPoint); //释放指针,关闭剪贴板

Close();

}

TForm2的粘贴事件的处理函数如下: void __fastcall TForm2::PasteButtonClick(TObject *Sender)

{

if(ActiveDraw) //确保活动文档有效

{

ActiveDraw->Assign((TDrawClipborad*)Clipboard());

ActiveDraw->UpDateButton();

}

}

3.8 总结

在这个例程中,我们使用了多个对象类来完成最终的功能,每一个对象之间都存在着

一定的逻辑关系,从这段代码中也可以看出,并不是所有的关系或者功能都一定要使用属

性来完成,属性只是一种方式,最主要的是我们在设计程序的过程中,将对象的状态作为

了程序的核心,围绕着状态之间的变化规律以及约束规律来进行代码的设计。 在应用程序中并没有一种特定的方式来确定如何设计才是合理,只要达到的最终的目

的,而且可扩充性强,维护方便,那么就是合理的代码,在这个例子中 TForm1 的鼠标移

动的处理方法中,我们完全可以采用类似 Sells 中的处理方法,引入一个鼠标消息的子

第七章应用程序中的面向状态 第 441 页

处理函数的事件,当不同的工具被选择的时候,更换不同的鼠标消息子处理函数句柄,这

对于程序拥有很多绘图工具的时候是非常有利的,这个例子中并没有这样作,因为绘图工

具只有三个,处理起来并不复杂,有兴趣的读者可以将 Fotm1 的 OnMouseUp 方法中的

部分代码提取出来,声明一个用于鼠标消息子处理事件的类型,再为每一个种绘图工具编

写鼠标处理子函数,用类似 Sells例子中键盘消息处理的方式来实现。 在这个例子中我们缺少的是编辑中另一支柱,Redo(重复)和 Undo(撤销),这需

要使用到另一种数据结构“栈”,C++ Builder 中为 TStack 类,重复和撤销是编辑中

稍微复杂的功能,不仅需要预先规划好重复和撤销的路径,还有对每一种可能的操作定义

相应的数据格式,代码比以上介绍的编辑功能要复杂的多,代码量也会大一些,我们在这

个例子中将不再示范。 除此之外,本例程中还有一些辅助功能的方法,上面没有介绍,比如程序关闭时的

CloseQuery 方法,用于保证更改的数据在退出程序之前需要提醒用户保存等,我们不再

一一介绍,本书所附光盘中有本程序的所有代码,读者自行阅读。

4 小节

本章从程序设计方面应用面向状态的技术作了两个简单是示例,实际中的程序比这两

个例子要复杂的多,而且对象之间的关系也复杂的多,本章的例子仅仅是在几个方面将这

种设计程序的思想和方法作个示范,这两个例子的代码并不是优秀的代码,只能给读者在

将 VCL组件的设计技术应用到应用程序的设计中起到一个抛砖引玉的作用。 本章将是本书的最后的内容,希望整本书给读者会在某些方面有所帮助。C++

Builder是一个非常优秀的程序设计工具,C++语言也是一种非常强大的程序设计语言,

其语言的博大精深是本书所难以叙述,涉及到的方方面面内容。Windows 系列操作系统

是目前最为流行的桌面操作系统,任何一个程序员都不可能全面掌握,但是作为应用程序

的设计师,至少应当对 Windows的底层原理有所了解。 程序设计是一个系统的工程,并非一种两种技巧和技术所能决定的,任何一种应用的

设计者都应该是该应用领域的专家,否则不可能出现 PhotoShop、AutoCAD 等这些非常

优秀的软件。任何一个程序员都应该将精力放在需要解决的问题上,其次才是如何使用工

具来解决。本书虽然是就 C++ Builder的使用来编写的,但最终并不是为了向读者介绍

如何使用 C++ Builder,这方面的书籍比较多,而且也相当全面,本书只是希望通过对

C++ Builder 的介绍,能够让读者站在一个更高的层次来分析、来组织、来面对程序设

计,这样思路会更加开阔,对问题的分析也会更加透彻。 世界上的万事万物本来就是相通的,其运动的基本规律是一致的,我国的古代的哲学

思想是这样认为的,而西方的近代哲学思想,包括唯物主义哲学也是这样认为的。对于一

个程序设计者而言,主导思想往往才是关键。 本书的所有内容到这里就全部结束了,作者希望本书能够对读者有所帮助,但以作者

第七章应用程序中的面向状态 第 442 页

的水平,目前力所能及的只有这些,如果一个 C++ Builder的初学者通过阅读本书而掌

握了如何使用__property 关键字,那么作者就已经心满意足了。对于本书中出现的错误、

遗漏、疏忽之处,希望读者能够谅解并给予批评指正。 参考资料及书籍: 《C++ Builder连接帮助》 Borland公司随机产品 《Borland C++ Builder 5 高级开发技术》 中国水利水电出版社 李冬 王宏等著 《面向对象的理论与 C++实践》 清华大学出版社 王燕 编著 《C/C++程序设计大全》 机械工业出版社 H.M.Deitel D.J.Deitel著 薛万鹏 等译 《C++ Builder 5 开发人员指南》 电子版由 China-pub.com提供 梁志刚、汪浩、康向东、刘存根等译