开发新的VCL 组件 -1
Delphi
是一个快速的开发工具,利用它可以很容易地开发出各种应用程序,程序员可以开发自己
的组件,并且可以将新的组件加到IDE 组件面板中,这也是Delphi 最重要的特性之一。从广义上来说,
Delphi 的用户可以分为两类:应用程序员和组件开发者。本章内容特别适用于准备自行开发Delphi 组
件的用户。
本章首先介绍VCL(Visual Componet Library)组件及其基本知识,然后依次详细介绍开发一个
新VCL 组件的基本步骤、接着介绍如何为组件添加属性、事件和方法,还介绍了在编写和使用新组件
过程中需要注意的事项,最后用几个不同方面的实例说明制作组件的基本过程。其中大部分内容也适
用于开发新的CLX 组件。
16.1 开发组件简介
16.1.1 什么是组件
组件是Delphi 应用程序的程序元素。尽管大多数组件代表用户界面的可见元素,但组件也可以是
程序中的不可见元素,如数据库组件。
从应用程序员的角度看,组件是可以在组件面板上选择,在窗体设计窗口和代码窗口中操作的元
素,并且可以对其编写一定的事件处理代码,从而满足一定的功能需求。在实际编程中,组件是能插
入Delphi 开发环境的任何元素,它具有程序的各种复杂性。组件定义只是接口描述。
对于一个组件开发者,Delphi 组件是代码中的对象,可以直接或间接地从TComponent 派生出来
的一个ObjectPascal 类。TComponent 定义了所有组件必须具备的、最基本的行为。例如可以显示在组
件面板上以及可以在表单设计窗口中编辑。但是TComponent 并不知道如何处理组件的具体功能,因
此必须自己描述它。
严格说来,组件是具有下列能力的持续化(Persistence)对象。所谓持续化对象是与临时(Transitory)
对象相对的。临时对象即没有办法保存销毁之前状态的对象,持续化对象即可以从表单文件(*.dfm
或*.xfm 文件)或数据模块中读取,或向其中写入自身状态的对象。组件应该具有如下特性。
*IDE 集成:可以在IDE 面板中显示并且可以在表单设计器中操作。
*拥有者属性:可以管理其他组件。如果组件A 拥有组件B,则当A 被销毁时,A 有责任销毁B。
*对输入/输出流和文件的支持:这是TPersistent 类的持续化特性的加强。
*COM 支持:可以通过使用Windows 提供的向导,转化为ActiveX 控件或其他COM 对象。
16.1.2 为什么使用组件
程序员制作组件的目的之一,是把大量的重复劳动用组件的方法定制起来,加快软件开发的效率。
当在Delphi 中添加一个新的软件包时,实际上就是使用了一个新的类扩展了VCL。这个新类从一个已
有组件的相关类中派生出来,并添加了新的功能。
Delphi 提供了很多现成组件,而且随着版本更新不断增加新组件。另外还可以买到第三方开发的
特色组件,或从Internet 下载免费组件。这些组件足以支持一般应用系统开发。
但应用开发人员仍有必要自己制作组件。采用组件形式可以把对象严密封装,并加上一层直观外
壳,有利于软件调试和代码重用。开发群体以组件为功能单位分工协作,比较容易实现工程化管理,
从软件规划设计到测试修改都可以减少意外差错,大大提高工作效率。成熟的组件还可以作为商品软
件出售,带来附加效益,有利于软件开发的社会化分工协作。
·412·
16.1.3 Delphi 的组件库基础
Delphi 的组件库包括VCL 和CLX(Component Library for Cross-Platform)。VCL
只用于Windows
开发,CLX 可以用于Windows 和Linux 平台开发。组件库的范围非常广,可以在IDE 中使用,也可
以在运行代码中创建和使用。有些组件可以用于所有应用程序,而有些只能用于部分应用程序。
1.组件库
Delphi 的组件库是由分散在几个子库中的对象组成,每个子库有不同的应用目的,这些子库的组
成及描述如表16-1 所示。
表16-1 Delphi 组件子库的组成及描述
组成部分 描述
BaseCLX 所有CLX 应用程序的底层类。包括运行时库(Runtime Library,RTL),并包括了Classes
单元
DataCLX
客户数据访问组件。包含与数据库相关的组件的一部分。这些组件可以用于跨平台访问数据库的应用程
序,可以从一个磁盘文件访问数据库,也可以从使用dbExpress 的数据库服务器访问数据
NetCLX 建立Web 服务器应用程序的组件,包括了对Apache 或CGI Web 服务器的支持
VisualCLX 跨平台的GUI 组件和图形类。它底层使用跨平台widget 库(Qt)
WinCLX
只用于Windows 平台的类。包括经过封装的本地Windows 控件、使用不能用于Linux 的数据访问机制
(如BDE 或ADO 等)进行数据库访问的组件以及只能在Windows 上使用的组件(如COM、NT 服务)
VCL 和CLX 包含许多同样的子库,它们都包括BaseCLX、DataCLX、NetCLX。VCL
包括WinCLX,
而CLX 包括VisualCLX。当需要使用本地的Windows 控件、Windows 的相关特性或扩展一个现存的
VCL 应用程序时应该使用VCL。当需要编写一个跨平台的应用程序或者使用可以在CLX 应用程序中
得到的控件(例如TLCDNumber)时应该使用CLX。
组件库中所有对象都是TObject 的后代。TObject 引入了执行基本操作的方法如创建方法、销毁方
法以及消息处理方法。组件是从类TComponent 派生的,是组件库的子集。组件可以放在表单或者数
据模块中,并且设计时可以修改它们的属性。使用Delphi IDE 中的对象编辑器(Object
Inspector),
可以不用写任何代码改变组件的属性值。
运行时可见的组件可以叫作“可视组件”(Visual Component),而运行时不可见的组件叫作“非可
视组件”(Nonvisual Component)。一些组件可以在IDE 的组件面板中显示。
可视组件,如TForm、TSpeedButton,习惯上都可以叫作“控件”(Control),都是从TControl
派
生的。控件在GUI 应用程序中使用,并且运行时用户可以见到它们。TControl 提供了指定一个控件显
示特性的属性,如高度和宽度。
非可视组件,习惯上也可以叫作“组件”,可以用于不同的任务。例如当开发一个连接数据库的
程序时,可以将一个TDataSource 组件放在表单中并且将组件与数据库联系起来,这种联系在运行时
用户是看不到的,因此TDataSource 是非可视组件。设计的时候,非可视组件显示为一个图标,程序
员可以像可视组件一样设置它们的属性和事件。
非组件的类(即从TObject 而不是TComponent 派生的类)也有各种不同的用途。其中比较典型
的是用于访问系统对象(如文件和剪贴板)或者执行一些临时任务(如储存数据到一个列表中)的类。
虽然这些类可以由组件创建,但是设计时不能创建这些类的实例。
2.组件的属性、方法和事件
VCL 和CLX 都是与IDE 集成的,因此可以快速开发应用程序。组件库中的所有类都是基于属性、
方法和事件的。每个类都包括数据成员(属性)、操作数据的函数(方法)以及与用户交互的途径(事
件)。VCL 基于Windows API 函数,而CLX 是基于Qt widget 库的。组件库本身也是用Delphi
写的。
(1)属性
属性允许在一个简单、一致的界面下隐藏数据,也可以在不通知应用程序开发者的情况下改变属
第16 章 开发新的VCL 组件
·413·
性信息的结构。它使应用程序开发者可以像读写一个变量一样对属性进行读写,同时允许组件开发者
隐藏底层的数据结构以及访问值时的特殊的处理过程。
属性决定对象显示出来的行为和操作。例如Visible 属性决定一个对象在应用程序界面中是否可
见。设计良好的属性可以使组件更容易被人使用和维护。下面列出了属性的一些特征。
*方法只在运行时可用,而属性在设计时就可用并且可以改变它的值,并且在IDE 中组件发生变
化时可以迅速地反馈到它的属性中。
*可以在对象编辑器中访问一些属性,并且可以在其中可视地改变对象的值。在设计时改变属性
的值比使用代码改变容易的多并且容易维护。
*属性能检查应用程序开发者指定给属性的值的格式和值的有效性,设计时就可以验证输入,预
防错误。
*属性可以根据需要在创建时设置合适的值。程序员容易犯的一个错误就是引用没有初始化的变
量的值。而通过使用属性代表数据,可以保证需要时值永远是可用的。
*因为数据被封装了,所以在实际对象中属性是受保护的(protected)或私有的(private)的。
*可以调用方法设置或读取属性值,因此可以使对象的使用者在不知道的情况下进行特别的处
理。例如数据可能保存在一个表中,但是可以像一个正常数据成员一样提供给程序员。
*在访问属性时可以触发事件和修改数据,例如在程序中修改一个属性值的同时要求修改其他数
据时,可以修改属性的读/写方法。
*属性可以是虚拟的。
*属性可以不限于单个对象,修改一个对象的一个属性能够影响其他几个属性。例如在一个
RadioButton 中设置Checked 属性时可影响同组中的其他按钮。
(2)方法
方法是一个与类相关联的过程。方法定义了一个对象的行为。类的方法可以访问所有的public、
protected 和private 属性,以及类的域,并且通常是作为一个成员函数。
组件的方法包括类方法和组件方法。类方法是在一个类中而不是在一个特定的类的实例中执行的
过程或函数。例如每个组件的构造方法(Create)是类方法。组件方法是在组件的实例中执行的过程
或函数。应用程序开发者使用方法来使组件执行一个操作或得到需要根据属性计算才能获得的值。
因为方法要求执行代码,所以方法只能在运行时调用。使用方法主要有以下优点。
*方法封装了组件的功能。
*方法可以在一个简单、一致的界面下隐藏复杂的过程。例如应用程序开发者可以调用组件的
AlignControls 方法,而不需要知道方法如何工作,也不需要知道它和其他组件的AlignControls
方法有
什么区别。
*方法可以在一次调用中修改几个属性。
(3)事件
在程序中,程序员无法精确预测一个用户将要执行的动作,用户可以选择一个菜单项、单击一个
按钮或者输入一些文本。因此可以编写一些代码处理感兴趣的动作而不是写一些按照指定顺序执行的
代码。
现在许多应用程序都是事件驱动(Event Driven)的,因为它们都会对事件作出响应。
在组件中,事件是一个特殊的属性,它在运行时根据输入或其他行为调用执行代码。事件使应用
程序员能够将运行时发生的特定事件,如鼠标和键盘动作,与一段代码联系起来。事件发生时执行的
代码称为事件处理过程(Event Handler)。无论事件如何触发,VCL 对象都会查找是否存在程序员编
写的事件处理过程。如果存在,代码就会执行,否则执行默认的事件处理过程。
事件允许应用程序员不需要定义新的组件来对不同的输入作出不同的反应。事件可以分为如下3
类。
*用户事件:即用户触发的事件,用户事件的例子有OnClick(用户单击鼠标)、OnKeyPress(用
·414·
户按了键盘上的一个键)、OnDblClick(用户双击了鼠标)。
*系统事件:即操作系统触发的事件。例如OnTimer(在预定时间间隔到达时,由Timer 组件触
发)、OnPaint(组件或窗口需要重画时)等。通常系统事件不由用户行为发起。
*内部事件:即由应用程序内部的对象触发的事件。内部事件的一个例子是OnPost,它是当应用
程序向数据库中提交当前记录时产生的。
3.对象、组件和控件
图16-1 所示是简化了的对象、组件和控件关系图。
TObject TPersistent TComponent TControl TWincontrol*
[Objects]
[Objects]
Exception [Objects]
[Objects] [Objects] TGraphicControl [Objects]
*在跨平台应用程序中是T荳莍莇莋莈莟荂
图16-1 简化的对象、组件和控件关系图
每个对象(类)都是从TObject 继承。其中可以在表单设计器中显示的对象都是从TPersistent 或
TComponent 继承的。而控件是从TControl
继承的。有两种类型的控件:图形控件,从TGraphicControl
继承;窗口控件,从TWinControl 或TWidgetControl 继承。因此,一个类似TCheckBox
的控件继承了
所有TObject、TPersistent、TComponent、TControl 以及TWinControl
或TWidgetControl 的属性,并且
自己增加了特别的属性。
对几个重要的基类作了简单的解释,如表16-2 所示。
表16-2 组件库中几个重要基类的说明
类 描述
TObject
VCL 或CLX 中所有对象的祖先和基类。它封装了所有VCL/CLX 对象的共同的基本方法,如创建、
维护或销毁一个对象的实例等
Exception 与VCL 异常相关的所有类的基类。它为错误处理提供了一致的接口,使应用程序方便地处理错误
TPersistent
实现发布属性的所有对象地基类。它的子孙类允许将数据发送到数据流,并且允许将对象的值赋
给另一个对象
TComponent
所有组件的基类。组件可以添加到组件面板中,并且可以在设计时进行操作,组件也可以拥有其
他的组件
TControl
代表了所有运行时可见的控件的基类。它是所有提供标准的、可视的属性(如位置、光标)的控
件的祖先类。它也提供了对鼠标动作的反应
TWinControl 或
TWidgetControl
可以获得键盘焦点的所有控件的基类。TWinControl 的后代都叫作窗口控件,而TWidgetControl
的后代都叫作widget
4.TObject 类及TObject 分支类
TObject 封装了一个对象的基本行为,提供了下面的方法。
*通过分配、初始化、释放内存引入了创建、维护和销毁一个对象实例的方法。
*返回一个类类型和一个对象的实例信息以及关于它的Published 属性运行时的类型信息(RTTI)
的方法。
*消息处理方法。
*支持对象运行的接口。
对象的很多行为都是在TObject 引入的方法基础上实现的。它的很多方法都是在IDE 内部使用的,
一般用户不需要使用。TObject 是抽象类,程序中不能直接创建TObeject 的实例。虽然TObject 是组
件框架的基础对象,但是并不是所有的对象都是组件,所有的组件都是从TComponent 继承的。
第16 章 开发新的VCL 组件
·415·
TObject 分支类包括了所有从TObject 但不是从TPersistent 派生的VCL 和CLX 类。TObject
是许
多简单类的直接祖先。TObject 分支类有一个共同的重要属性,它们都是临时对象,而不是持续化对
象,这意味着这些类没有办法保存在销毁之前的状态。
这个分支的主要类之一是Exception 类,这个类提供了大量的内建的异常类,可以自动处理程序
中的错误,包括除以0 错误、文件I/O 错误、无效的类型转换等。
TObject 分支类的另一个重要组成部分是封装数据结构的类。
*TBits:储存Boolean 值的“数组”类。
*TList:链表类。
*TStack:堆栈类。
*TQueue:队列类。
TObject 分支类还包括了封装外部对象的类,如TPrinter 封装了一个打印机接口,TIniFile 封装了
INI 文件的读写接口。
而TStream 代表了这个分支的其他类型的类。TStream 是可以从各种存储介质如磁盘文件、动态
内存等读写数据的流对象的基类。
5.TPersistent 类及TPersistent 分支类
TPersistent
是所有具有“指派”(Assignment)和“流”(Stream)属性的对象的祖先类。所谓“指
派”属性可以将一个对象指定给其他对象,而“流”属性能够从一个表单文件(.xfm 或.dfm 文件)中
读/写属性值。相应地,TPersisten 中也封装了获得这两个属性的方法,包括以下几种:
*定义了从一个输入流中装载和存储非published 属性的数据的过程。
*提供了给属性指定值的方法。
*提供了将一个对象的内容指派给另一个对象的方法。
程序中不能创建一个TPersistent 的实例。声明一个不是组件的对象时可以将TPersistent 作为基类,
但是需要将该对象存储到一个流中或将它们的属性指定给其他的对象。
TPersistent 分支类包括所有从TPersistent 但是不从TComponent 派生的VCL 和CLX
类。这些类
都是持续化对象,即它们可以将数据保存到表单文件或数据模块中,也可以从表单文件或数据模块中
获得数据,因此运行时它们可以按照设计时设定的特性显示。
然而这些类不能独立存在,也就是说它们只能是组件的属性(如果它们有拥有者),只能从一个
表单中读写。它们的拥有者必须是一个组件。TPersistent 引入了GetOwner 方法,它可以让表单编辑器
决定对象的拥有者。
这些类也是首先引入了发布属性的类,发布属性可以自动地装载和保存。DefineProperties 方法指
明了每个类如何装载和保存属性。下面是TPersistent 分支中的一些具有代表性的类。
*Graphics:例如TBrush、TFont 和TPen。
*TBitmap 和TIcon 可以保存和显示图形;TClipboard 包含了应用程序中剪切和拷贝的文本或图
形。
*字符串列表:如TStringList,它代表了在设计时可以指定的字符串文本或者列表。
*集合和集合项:它们是从TCollection 或TCollectionItem
中派生,这些类维护了属于一个组件的
特别定义的索引集合。例如THeaderSections 和THaderSection,或者TLstColumns
和TListColumn。
6.TComponent 类和TComponent 分支类
TComponent 是所有组件的共同祖先类。它不提供任何用户界面和显示的特性。这些特性由直接
从TComponent 派生的两个类提供:QControls 单元中的TControl
是跨平台的应用程序中“可视化”组
件的基类;Controls 单元中的TControl 是Windows 应用程序中“可视化”组件的基类。
程序中不能创建TComponent 的实例。
TComponent 分支类包含了所有从TComponent 但不是从TControl
派生的所有类,这个分支的对象
是在设计时可以进行设定但是在运行时不能在用户界面显示的组件。它们都是持续化对象,可以完成
·416·
下列功能。
*在组件面板中显示并且可以在表单中显示。
*可以拥有并且管理其他组件。
*自动装载和保存。
TComponent 的几个方法指示了组件在设计时的行为以及如何获得组件保存的信息。
首先在这个分支中引入“流”,一个对象具有“流”的能力即可以将它的属性信息保存在一个表
单文件中并且可以从表单文件中读取属性信息。发布属性都是持续化的,并且自动成为“流”。
TComponent 分支类引入了拥有者的概念。它们有两个属性支持拥有者的实现:Owner 和
Components。每个组件都有Owner 属性,指明了将另外一个组件作为它的拥有者。一个组件拥有其他
组件时,拥有的组件都在Components 属性中。每个组件的构造方法都有一个指明新组件的拥有者的
参数。如果参数中传入的拥有者存在,新组件会加入到拥有者的Components 列表中,这个属性也会
自动提供给拥有者的析构方法中。如果一个组件有一个拥有者,那么拥有者被销毁时,它也会被销毁。
例如TForm 是TComponent 的后代,当一个表单拥有的所有组件在表单被销毁时也会被销毁并且释放
它们的内存(当然是假设组件设计正确并且析构方法正确地清理)。
如果一个属性的类型是TComponent 或它的后代,“流”将创建这个类型的一个实例并且读入它们。
如果一个属性是TPersistent 但不是TComponent 的后代,“流”将使用现存可用的实例并且读取该实例
的属性值。
TComponent 分支类包括的一些代表类如下:
*TActionList:维护一个行为列表的类,它对程序中对用户输入的反应进行了抽象化。
*TMainMenu:提供表单的菜单条以及下拉菜单。
*TOpenDialog、TsaveDialog、TfontDialog、TfindDialog、TcolorDialog
等:这些是从通常使用的
对话框显示和收集信息的类。
*TScreen:对应用程序创建的表单和数据模块、活动表单、表单中活动控件、屏幕的大小和解析
度、应用程序使用的光标和字体等进行跟踪的类。
在需要创建能在组件面板中显示并且在表单设计器中使用的非可视组件时可以使用TComponent
作为基类。例如想编写一个类似于TTimer 的组件时,可以从TComponent 派生。这种类型的组件可以
显示在组件面板中,可以通过代码执行内部函数,但是运行时不能显示在用户界面。
7.TControl 类及TControl 分支类
TControl 是所有运行时可见的组件的基类。控件都是可视的组件,即在程序运行时用户可以看见
它并且可能进行交互操作。所有的控件都有描述它们显示特性的属性、方法和事件,如控件的位置、
控件相关的光标或提示、绘制或移动控件的方法以及响应用户行为的事件。
TComponent 定义了所有组件的行为,而TControl 定义了所有控件的行为,包括绘制方法、标准
事件和容器属性。TControl
引入了很多所有控件都继承的显示特性的属性,包括Caption、Color、Font、
以及HelpContext 或者HelpKeyword。当这些属性从TControl 继承时,它们都是published
的,因此可
以显示在对象编辑器中。但是它的后代可以决定是否发布这些属性,例如TImage 没有发布Color 属性,
因此它的颜色由它显示的图形决定。
TControl 分支类包括从TControl 但不是从TWinControl(CLX
应用程序中是TWidgetControl)派
生的组件。通常,所有的控件都有对鼠标动作进行反应的事件,而这个分支的控件不能接受键盘输入。
TControl 也引入了Parent 属性,它指明了这个控件包含在另一个控件中。
TControl 分支中的控件也叫作图形控件,它们都从TControl 的直接后代TGraphicControl
中派生
而来。虽然这些控件在运行时显示在用户面前,但它们并没有自己的底层窗口,只是使用它们的父窗
口。这是因为图形控件不能接受键盘输入或作为其他控件的父控件的限制。由于它们没有自己的窗口,
因此图形控件使用系统资源较少。
第16 章 开发新的VCL 组件
·417·
8.TWinControl/TWidgetControl 类及TWinControl/TWidgetControl
分支类
TWinControl 是可以显示在微软的Windows 屏幕上的所有控件的基类。它提供了显示在Windows
屏幕上的公用功能,包括以下几种。
*控件可以与底层的窗口协同工作,例如如果底层的屏幕对象是一个文本编辑器,控件可以协同
编辑器管理和显示缓冲区的文本。
*控件可以接受用户的输入焦点,获得焦点的控件可以处理键盘输入事件(图形控件控件只能显
示数据和对鼠标作出反应)。一些控件在获得输入焦点时可以改变它们的外观,例如按钮控件在获得焦
点时在它的文字周围绘制一个矩形。
*控件能够成为一个或许多子控件的父控件,可以作为其他控件的容器。这个关系可以由子控件
的Parent 属性表示。容器控件为它们的孩子提供重要的服务,包括为没有自己画布的控件提供显示服
务。容器控件的例子包括Form、Panel 和Toolbar。
*控件有一个句柄(handle),或者说是独一无二的标识符,这使它们可以访问底层的窗口或
Widget。
基于TWinControl 的控件可以显示Windows 提供的标准屏幕对象或由VCL 程序员定制的屏幕对
象。每一个TWinControl 都有一个Handle 属性,它提供了访问Windows 屏幕的窗口句柄。可以使用
Handle 属性调用VCLAPI 以及直接访问底层窗口。
组件库中大部分控件派生于TWinControl/TWidgetControl 分支。与图形控件不同,这个分支的控
件拥有自己的窗口或Widget,因此它们有时也叫作窗口控件或Widget 控件。窗口控件都从TWinControl
派生,而TWinControl 是从只用于Windows 版本的TControl 派生的。Widget
控件都从TWidgetControl
派生,而TWidgetControl 是从TControl 的CLX 版本派生。
TWinControl/TWidgetControl
分支包括了可以自动绘制的控件(如TEdit、TListBox、TCmboBox、
TPgeControl 等) 以及不与单个底层控件或widget 对应的控件。后一种控件包括TStringGrid
和
TDBNavigator,它们必须自己处理绘制的细节。因为这个原因,它们从TCustomControl 类派生,而
TCustomControl 类引入了Canvas 属性以绘制它们自己。
当定义一个新的控件类时,TControl 的子类型用于非窗口控件,TWinControl 的子类型则用于窗
口控件。除非特殊需要,一般不直接从TControl 和TWinControl 派生新控件。TWinControl
的后代包
括支持多种用户界面对象的抽象基类。最重要的后代是TCustomControl,它提供了画布和处理绘制消
息的代码。其他一些重要的抽象后代包括TScrollingWinControl、TButtonControl、TCustomComboBox、
TCustomEdit 和TCustomListBox 等,定义新的控件时可以考虑这些从TWinControl
直接派生的抽象类,
这样可以充分利用原有的属性、事件和方法,减少很多工作量。
16.1.4 组件和类
简单地说,组件就是Delphi 的组件库中的一个类,开发一个新组件实际上是从现有类层次中的某
个类派生一个新类加入到类库中。
16.1.5 开发组件的要求
开发组件对程序员提出了更高的要求,主要体现在以下几个方面。
1.开发组件是非可视化的
开发组件与开发Delphi 应用程序最明显的区别是组件开发完全以代码的形式进行,即非可视化的。
Delphi 应用程序的可视化设计需要已完成的组件,而开发这些组件就需要使用Object Pascal 代码编写。
虽然无法使用可视化工具来开发组件,但是开发过程能运用Delphi IDE 的所有编程特性,如代码编辑
器、集成化调试和对象浏览。
·418·
2.开发组件需要更深入的有关面向对象编程的知识
开发新组件和使用它们的最大区别在于当开发新组件时,需要从已存在的组件中继承产生一个新
对象类型,并增加新的属性和方法。而组件使用者在开发Delphi 应用程序时,只是在设计阶段通过改
变组件属性和描述响应事件的方法来定制已有组件的行为。因此,组件编写者必须对面向对象编程有
更深入的了解,这主要体现在以下几方面:
(1)组件编写者可以访问祖先对象中的更多部分
当继承产生一个新对象时,程序员有权访问祖先对象中对最终用户不可见的部分,即protected 的
属性、方法。在很大部分的实现上,后代对象也需要调用它们的祖先对象的方法。因此,开发组件者
应相当熟悉面向对象编程特性。
(2)组件编写者需要设置组件的属性、方法和事件
不考虑在表单编辑器中的可视化操作,一个组件最明显的特征就在于它的属性、事件和方法。
(3)组件编写可能还需要封装图形
Delphi 通过把各种各样的图形工具封装到一个画布(Canvas)中而简化了Windows 的图形操作。
Canvas 代表了一个窗口或控件的可以显示图形的部分,并且包含了其他的类。
如果曾经开发过图形化的Windows 应用程序,应该会对Windows 图形设备接口(Graphics
Device
Interface,GDI)比较熟悉。GDI 限制了可用的设备上下文的数目,并要求在销毁它们之前将图形设备
恢复到初始状态。
使用Delphi 则不用担心这些事情。为了绘制一个表单或组件,可以使用组件的Canvas 属性。如
果定制了Pen 或者Brush,则可以设置它的颜色和样式。设置完成后,Delphi 负责分配资源。如果程
序中需要反复调用这些资源,Delphi 在缓存中保存这些资源以避免重复创建。
当然仍然可以完全控制Windows GDI,但是使用Delphi 组件中的Canvas 将会使代码更简单并且
运行更快。
3.开发组件要遵循更多的规则
开发组件比开发可视化应用程序采用的编程方法更传统,与使用已有组件相比有更多的规则要遵
循。在开始编写组件之前,最重要的事莫过于熟练应用Delphi 自带的组件,以得到对命名规则以及组
件用户所期望功能等的直观认识。组件用户期望组件做到的最重要的事情莫过于他们在任何时候能对
组件做任何事。编写满足这些期望的组件并不难,只要预先想到和遵循一些规则。
使组件可用的一个重要方面是减少组件的依赖性。自然地,在应用程序中组件可以在不同的组合、
顺序、上下文中协同工作。因此设计组件时应该使其在任何环境下都可以使用,不需要预先设定条件。
减少依赖性的一个例子是TWinControl 的Handle 属性。如果原来开发过Windows 应用程序,那
么就可以知道最困难并且最容易犯错误的是确保不要访问一个没有调用CreateWindow API 函数的窗
口控件。Delphi 窗口控件使用户从这里解放出来,它确保一个窗口句柄在需要调用时永远是有效的。
控件可以检查窗口是否已经创建,如果句柄无效,控件创建一个窗口并且返回句柄。因此无论程序代
码何时访问Handle 属性,它都保证获得一个有效的句柄。
通过减少诸如创建窗口之类的后台任务,Delphi 组件允许程序员将注意力集中在他们真正想做的
事情上。在将一个窗口句柄传递给API 函数之前,程序员不需要验证句柄是否存在或者创建一个窗口。
应用程序开发者可以假定一切正常,而不需要经常检查可能出错的事物。虽然可能创建没有依赖性的
组件需要花费很多时间,但通常是值得的。它不仅仅减轻了程序开发者的负担,而且也减少了控件本
身的文档和技术支持的负担。
4.组件必须注册才能使用
在向IDE 安装组件之前,必须进行注册。注册告诉Delphi 把组件放在组件面板的哪个位置,也可
以定制Delphi 将组件保存在表单文件中的方法。
第16 章 开发新的VCL 组件
·419·
16.1.6 如何选择新组件的基类
组件几乎可以是在应用程序开发者在设计时想要操作的所有的应用程序的元素。开发一个组件意
味着从现存的类中派生一个新类,如表16-3 所示,可以作为新开发组件的基类的简单列表。
表16-3 开发组件的基类选择
开发组件的起点 可选基类
修改现存的控件
任意现存的组件如TButton、TListBox,或者是一个抽象组件类型,如TCustomListBox
开发窗口控件(或CLX 应用
程序中的基于widget 的控件)
TWinControl(在CLX 应用程序中使用TWidgetControl)
开发图形控件 TGraphicControl
开发窗口类的子类 任何Windows 控件(在VCL 应用程序中)或基于widget 的控件(CLX 应用程序中)
开发非可视组件 TComponent
也可以从一个非组件的类中派生,以开发不能在表单中操作的组件,如TRegIniFile 和TFont。
1.修改现存的控件
开发组件最简单的方法就是定制现存的组件。组件库中任何一个组件都可以作为新组件的父类。
例如改变标准控制(如TButton)的默认属性值。
一些控件,如列表框、表格可能用到许多相同变量。在这种情况下,组件库包含了一个抽象类用
于派生定制的版本,抽象类在名字中包含了“Custom”,如TCustomGrid。
例如开发一个去掉标准TListBox 类中一些属性的新的列表框,但是由于Delphi 的属性可见性的
规定,开发过程中不能删除(或者隐藏)从祖先类中派生的属性,因此只能从类层次中TListBox 之上
而不是TListBox 派生新组件。组件库提供了TCustomListBox 抽象类,它实现了所有的列表框的属性,
但是所有的属性都没有发布( Publish ), 因此新组件可以从TCustomListBox 派生。如果没有
TCustomListBox 抽象类,则新组件必须从TWinControl 类(在CLX 中是TWidgetControl
类)派生然
后重新编写所有的列表框函数。当从像TCustomListBox 这样的抽象类派生时,可以只发布需要在组件
中可以修改的属性而把其他属性设置为protected。
2.开发窗口控件
组件库中基于窗口的控件是在运行时可见并且可以与用户进行交互的对象。每个基于窗口的控件
都有一个可以通过Handle 属性访问的窗口句柄,它可以让操作系统识别和操作该控件。如果使用VCL
控件,句柄允许控件接收输入焦点并且能传递给Windows API 函数。
所有窗口控件都从TWinControl 类(CLX 中是TWidgetControl)派生。这包括了最常用的标准窗
口控件,如按钮、列表框、编辑框。当需要直接从TWinControl(CLX
中是TWidgetControl)中派生
一个新的窗口控件(它与任何现存的控件无关)时,Delphi 提供了TCustomControl
组件。TCustomControl
是一个特别的窗口组件,它可以更加容易地绘制复杂的图形。
3.开发图形控件
如果新创建的控件不需要接受输入焦点,那么可以使用图形控件。图形控件与窗口控件很类似,
但是没有窗口句柄,因此消耗系统资源较少。像TLabel 这样的组件就是图形控件。虽然这些控件不能
接收焦点,但是可以对鼠标消息作出反应。
可以通过TGraphicControl 组件创建定制的图形控件(TGraphicControl 是从TControl
派生的抽象
类)。虽然可以直接从TControl 派生组件,但是从TGraphicControl
派生更好,因为它提供了Canvas
属性以在窗口中绘制图形、处理WM_PAINT 消息,需要做的只是重载Paint 方法。
4.创建窗口类的子类
Windows 中有一种称之为窗口类的概念,窗口类是Windows 中相同窗口或控件的不同实例之间共
·420·
享的信息集合。
传统的Windows 编程中,创建定制的控件需要定义一个新的窗口类并且在Windows 注册。以已
存在的窗口类为基础建立一个新类,即创建窗口类的子类;然后将控件放到动态链接库中,就像标准
Windows 控件一样,并且提供访问界面。也可以创建一个“包装”现存的窗口控件的组件。
如果已经有一个在Delphi 中使用的定制的控件库,可以创建与控件行为类似的Delphi 组件,并且
像使用其他控件一样从它们派生新的控件。
使用Windows 窗口类的子类技术的例子可以参见StdCtls 单元中标准Windows 控件的组件的代码,
如TEdit。CLX 应用程序请参见QStdCtls 单元。
5.开发非可视组件
非可视组件通常用作程序中某些元素(如数据库)的接口(如TDataSet 和TSQLConnection)、系
统时钟(如TTimer)以及对话框界面(在VCL 应用程序中是TCommonDialog,在CLX 应用程序中是
TDialog 以及它们的子类)。一般大部分开发的组件都是可视组件,非可视组件可以直接从所有组件的
基抽象类TComponent 派生。TComponent 定义了组件在FormDesigner
中所需的基本的属性和方法。
因此,从TComponent 继承来的任何组件都具备设计能力。
16.1.7 开发新组件的基本步骤
可以从两条途径开发新组件。
*使用组件向导开发组件。
*手动开发组件。
这两种方法都可以开发一个能安装到组件面板中的最小功能的组件。安装之后就可以将组件加入
到一个表单中并在设计时和运行时进行测试。
无论使用哪种方法进行组件开发都有一些基本步骤,可以简要描述如下。
*为新组件创建一个单元(Unit)。
*从一个现存的组件类型派生新组件。
*加入属性、方法和事件。
*将组件注册到IDE 中。
*为组件创建一个位图。
*创建一个包(一个特殊的动态链接库)以便安装组件到IDE 中。
*创建组件的方法、属性和事件的帮助文件。
注意:为组件创建帮助文件是可选的步骤。
组件开发完成之后,完全的组件包括下列文件。
*一个包(*.bpl)或包集合(*.dpc)文件。
*一个编译的包文件(*.dcp)。
*一个编译的单元文件(*.dcu)。
*一个面板位图文件(*.dcr)。
*一个帮助文件(*.hlp)。
1.使用组件向导开发新组件
组件向导简化了开发组件的最开始的步骤,使用组件向导时需要指定以下内容。
*新组件的基类。
*新组件的类名。
*新组件显示在组件面板的哪一页。
*新开发组件的单元名。
*单元的查找路径。
第16 章 开发新的VCL 组件
·421·
*组件放置在哪个包内。
组件向导与手动开发组件时完成一样的功能,包括以下内容。
*创建一个单元。
*派生组件。
*注册组件。
组件向导不能将组件加入到已存在的单元中。必须手动加入。
使用组件向导开发组件的基本步骤如下。
(1)启动新建组件对话框
使用下面两种方法之一,可以启动新建组件对话框。
*从Delphi IDE 菜单中选择“Component”*“New Component”。
*从Delphi IDE 菜单中选择“File”*“New”*“Other”并双击Component。
启动之后的新建组件对话框如图16-2 所示。
图16-2 新建组件对话框
(2)填写新建组件对话框中的信息
*在Ancestor Type 下拉选择框中指定新组件的基类。
注意:在下拉菜单中,许多组件都在不同的单元名中列了两次,其中一个是基于VCL 的,
而另一个是基于CLX 的。CLX 相关的单元名都以Q 开始(例如QGraphics 代替了
Graphics)。选择时应保证从正确的组件派生。
*在Class Name 输入框中填写新组件的类名。
*在Palette Page 下拉选择框中指明新组件安装在组件面板的哪一页中。
*在Unit file name 输入框中填写组件在哪个单元中声明。如果单元不在查找路径中,可以根据需
要修改Search Path 输入框中的查找路径。
(3)填写完成之后
*单击“Install”按钮,则将组件放到一个新的或已经存在的包中。
*单击“OK”按钮,IDE 将创建一个单元。
注意:如果从一个名字以Custom 开始的类(例如TCustomControl)派生新组件,那么在重
载原组件的抽象方法之前,不能将新组件放到表单中。Delphi 不能创建一个有抽象属
性或方法的实例对象。
此时可以单击“View Unit”按钮查看代码,如果新建组件对话框已经关闭,那么在代码编辑器中
选择“File|Open”打开单元也可以查看代码。Delphi 创建一个包含类声明和注册过程的新单元,并且
加入了包含在所有Delphi 单元中的uses 语句。
·422·
单元类似于这样:
unit MyControl;
interface
uses
Windows, Messages, SysUtils, Types, Classes, Controls;
type
TMyControl = class(TCustomControl)
private
{ Private declarations }
protected
{ Protected declarations }
public
{ Public declarations }
published
{ Published declarations }
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents(’Samples’, [TMyControl]);
end;
end.
2.手动创建组件
最简单的方法是使用组件向导创建新组件,当然也可以手动执行同样的步骤。
(1)创建一个单元文件
单元(unit)是Delphi 中可以独立编译的代码模块。每个表单都有自己的单元,许多组件(或相
关的组件组)也有自己的单元。
当创建一个组件时,可以选择为组件创建一个新单元,或将新组件加入到一个现存单元中。
为组件创建一个新单元的步骤如下。
*从IDE 菜单中选择“File”*“New”*“Unit”或 “File”*“New”*“Other”以显示
New Items 对话框,选择“Unit”,然后单击“OK”。IDE 创建一个新单元并且在代码编辑器中打开它。
*使用一个有意义的名字保存单元。
*派生组件类。
打开一个现存单元则需要以下步骤。
*从IDE 菜单中选择“File”*“Open”并选择需要加入组件的源代码单元。
注意:当向一个现存单元中加入组件时,保证单元中只包含组件代码。向一个包含表单的单
元中加入组件代码会引起组件面板错误。
*派生组件类
(2)派生组件
每个组件都是从TComponent 派生的一个类,可以从它的特殊后代(如TControl
或TGraphicControl)
第16 章 开发新的VCL 组件
·423·
或一个现存的组件类派生。
在需要包含组件的单元的interface 部分加入一个对象类型声明,并指明它的基类。
一个最简单的组件类是直接从TComponent 派生的非可视类。
(3)注册组件
注册是一个告诉IDE 哪些组件加入到它的组件库、显示在组件面板的哪一页的简单过程。注册组
件包括两个步骤。
*在组件单元的interface 部分加入一个名为Register 的过程。Register
不需要任何参数,因此声
明非常简单。
procedure Register;
如果是在已经包含组件的单元中加入组件,则Register 过程应该已经声明,不需要再进行声明。
注意:虽然Delphi 本身是大小写不敏感的,但Register 过程是大小写敏感的,并且必需以大
写R 开头。
*在单元的implementation 部分加入Register 过程的代码。
在代码中为每个需要注册的组件调用RegisterComponents 过程。RegisterComponents
有两个参数:
组件面板页的名称和组件类型的集合。如果是在一个现存的注册过程中加入一个新组件,则可以在现
存语句的组件类型集合中加入新组件,也可以新写一个调用RegisterComponents 的语句。例如:
RegisterComponents(’Samples’, [TMyControl]);
将组件TMyControl 加入到组件面板的Sample 页中。一旦注册完毕,Delphi 自动将组件图标显示
在组件面板上。
16.1.8 测试未安装的组件
在安装组件到组件面板之前就可以对组件运行时的行为进行测试。这在调试一个新创建的组件时
特别有用,但同样的技术也可以用于任何组件不论它是否在组件面板中。
测试一个未安装的组件可以通过模拟Delphi 中在组件面板中选择组件并放置到一个表单中的行为
进行。其步骤如下:
*在表单单元的uses 语句中加入组件单元的名称。
*在表单中加入一个对象声明代表组件。
这是自己加入组件和让Delphi 加入组件的主要区别之一。自己加入组件应在表单的type 声明的
public 的结束部分加入对象声明,而Delphi 将会把它加到所管理的类型声明的部分。
注意:不要在Delphi 管理的表单的类型声明部分加入对象声明。类型声明部分相关的项目存
储在表单文件中,在表单中加入表单中不存在的组件将会导致表单文件无效。
*在表单的OnCreate 事件处理代码中创建组件。
当调用组件的构造方法时,必须传递组件的拥有者(需要时销毁该组件的组件)的参数。可以将
Self 作为拥有者的参数。在一个方法中,Self 是包含这个方法的对象的引用。在本例中由于是在表单
的OnCreate 事件处理代码中,因此Self 是表单的引用。
*设定组件的Parent 属性的值。
表单通常在设置控件的其他属性之前设置Parent,因此设置Parent 属性值是创建一个控件后应该
做的第一件事。Parent 是显示时包含控件的组件,通常是控件显示的表单,但是也可以是一个GroupBox
或Panel。一般可以设置Parent 为Self。
警告:如果组件不是一个控件(也就是说TControl 不是它的祖先),则可以忽略这一步。如
果设置了表单(不是控件)的Parent 属性为Self,将会引起操作系统错误。
·424·
*根据需要设置组件的其他属性。
16.1.9 测试已安装的组件
安装之后测试组件可以测试开发应用程序时将组件从组件面板中拖到表单中时是否产生异常。
测试已经安装的组件可以使用Delphi 的第2 个IDE 运行实例,即从Delphi IDE 环境中再次启动
一个Delphi IDE 实例,则在第1 个实例中可以对第2 个实例进行调试、跟踪。具体步骤如下:
*打开组件源文件并设置断点。
*从IDE 菜单中选择“Run”*“Parameters”并设置Host Application 为Delphi
的启动程序。
*单击“Load”按钮可调用Delphi 的第2 个运行实例。
*在表单中加入需要测试的组件,这将在运行到设置的源文件的断点时中断。
16.2 组件开发过程中的面向对象编程
从应用程序员的角度看,一个类包括数据和代码,并且可以在设计和运行的时候对类进行操作。
但是当开发新组件时,需要从与应用程序员不同的角度去处理类。组件开发者需要试图隐藏组件的内
部工作不让组件使用者知道。只有恰当地选择组件的祖先、仔细设计只提供给使用者需要的属性和方
法的界面,才能开发通用的、可重用的组件。
16.2.1 定义新类
组件的开发者与应用程序员的区别在于组件开发者开发新的类,而应用程序员操作这些类的实
例。
一个类基本上就是一个类型。作为一个程序员经常要用类型和实例工作,例如定义一个类型(如
整数)的变量。而类常常比这些简单的数据类型要复杂,但是它们的工作方式是一样的,即通过给相
同类型的实例模板赋予不同的值,就可以执行不同的任务。
例如设计一个包含“OK”和“Cancel”按钮的表单。每一个按钮都是类TButton 的一个实例,但
是通过对它们的Caption 属性赋予不同的值和对OnClick 事件定义不同的事件处理代码,这样两个实
例的行为就不同了。
1.派生新类
派生新类主要有以下两个方面的原因。
*改变类的默认值以避免重复。
*给类增加新功能。
两种情况都是为了设计可重用的对象。如果在设计组件时就需要时刻记得可重用,那样以后就可
以省去很多工作。给类定义一个可用的默认值,同时也应该允许定制这些默认值。
(1)改变类的默认值以避免重复
许多程序员都试图避免重复。如果发现自己在反复重写同一代码,可以将代码放在一个子程序或
函数中,或者建一个可以在许多程序中使用的程序库。对于组件来说同样如此。如果发现需要改变同
样的特性或设计同样的调用方法,就可以开发默认完成这些行为的新组件。
例如每设计一个程序,就要增加一个对话框以执行一个特定的操作。而利用改变默认值的方法可
以简化这些操作。可以设计一次这个对话框,设置它的属性,在组件面板上安装一个封装这个组件的
新组件。通过使这个对话框变为一个可重用的组件,不仅减少了重复的工作,而且可以使程序标准化,
减少每次设计对话框时可能出现的错误。
修改一个已存在的组件本身就是改变一个组件默认属性的一个实例。
注意:如果只想改变一个已存在的组件中的published 属性,或者是保存一个或一组组件中
特定的事件处理代码,则使用组件模板将会更容易。
第16 章 开发新的VCL 组件
·425·
(2)给类增加一些新的功能
开发组件的常见原因就是增加已有的组件中没有的新功能。此时可以从已有的组件或抽象基类
(如TComponent 或TControl)派生来开发新组件。
派生组件时应选择包含尽可能多的所需要的功能的类作为父类。由于可以给类增加新功能,但是
不能取消它们,因此当一个已存在的组件类包含不想引入新组件的属性时,应该从组件的祖先中派生
新组件。例如需要在一个列表控件上增加一些特性,可以从TListBox.派生。但是,如果在增加新的功
能的同时又不想包含标准列表框中一些功能时,就必须从TCustomListBox(TListBox 的祖先)派生。
然后就可以重新开发所要的列表框的功能,增加新特性。
2.声明一个新的组件类
除标准的组件外,Delphi 提供了许多抽象的类作为派生新组件的基类。声明一个新组件类需要在
组件单元文件中增加一个类的声明。例如:
TNewComponent = class(TComponent)
private
{Private 属性、方法、事件声明部分}
protected
{Protected 属性、方法、事件声明部分}
public
{Public 属性、方法、事件声明部分}
published
{Published 属性、方法、事件声明部分}
end;
16.2.2 祖先、后代及类层次
应用程序员理所当然的认为每一个控件都具有Top 和Left 属性,以便确定它在表单中的位置。对
他们来说,不用关心所有控件的这些属性是从共同的祖先TControl 继承的。然而开发一个组件时,必
须知道从哪个类派生以便继承适宜的特性。而且必须知道控件继承的所有东西,这样就可以利用继承
的特性而不需重新开发。
直接派生组件的类叫作直接祖先。每个组件从它的直接祖先及直接祖先的直接祖先依此类推继
承。因此派生一个组件的所有的类都叫它的祖先。组件是它的祖先的后代。
同样,在一个程序中所有祖先-后代关系就组成了类层次。由于一个类从它的祖先继承了所有的
特性,而且增加了新的属性、方法或者重新定义了已存在的属性、方法,因此类层次中的每一代比它
的祖先包含了更多的功能。
如果不指定直接祖先,Delphi 从默认的祖先TObject 派生组件。TObject 是对象层次中所有类的最
终祖先。
选择从哪个对象中获取的一般原则很简单,就是选择包含新对象想要的最多而不需要的最少的对
象。通常可以在对象中增加东西,但是不能取消一些东西。
16.2.3 访问控制
属性、方法和作用域有5 种水平的访问控制,也叫可见度。可见度决定了哪些代码可以访问类的
哪个部分,通过设定可见度可以定义组件的界面。
如表16-4 所示,列出了访问控制的不同层次,从限制最严到最松。
表16-4 访问控制的不同层次
可见度 访问范围 目的
private 只能在定义类的单元内访问 隐藏实现细节
·426·
续表
可见度 访问范围 目的
protected 可以在类单元内部以及被类的后代访问 定义组件开发者界面
public 所有代码都可以访问 定义运行时界面
automated 所有代码均可访问,并产生Automation 类型信息 仅用于OLE 自动化对象
published
所有代码均可访问,并且可以通过对象编辑器访问。保存在一个表单文件
中
定义设计时界面
只允许在类定义的单元内部访问的成员定义为prviate,只允许在类及其后代中可用的成员定义为
protected。但是记住,如果一个成员定义在一个单元文件中可用,那么它在整个文件中都可用。所以,
如果在同一个单元中定义两个类,那么这两类就可以互相访问对方的private 方法。如果从祖代的不同
单元派生类,那么新单元中的所有类都可以访问祖先的protected 方法。
1.隐藏实现细节
把类的部分定义为private 使类单元文件外部的代码不能见到这些部分,但包含这些声明的单元内
部的代码可以访问这些部分。
2.定义组件开发者界面
定义一个类的某部分为protected 使这部分只能被该类及其子类(以及共享单元文件的其他类)可
见。可以用protected 定义类中组件开发者的界面。应用程序单元无法访问protected 部分,但是派生
类可以。这意味着组件开发者可以改变类的作用方式而无需使应用程序员知道细节。
程序员常犯的一个错误是试图从事件处理代码访问protected 方法。Windows 编程中,组件通常不
接收事件,所以事件处理过程一般都是表单的方法,而不是组件的方法。所以事件处理过程一般也不
能访问组件的protected 方法(除非组件与表单在同一单元中被声明)。
3.定义运行界面
将类的一部分定义为public,使其对任何可以访问这个类的代码都可见。
public 部分在运行时对所有代码都是可见的,因此类的public 部分定义了它的运行界面。运行界
面对那些设计时没有意义或不适当的项目很有用,比如依赖运行时输入的信息或只读的属性。打算提
供给应用程序开发者的方法也必须是public 的。
4.定义设计界面
定义类的属性或事件为published 的,则这些属性和事件可以对外发布。同时编译器也将产生它们
的运行时类型信息,运行时类型信息允许对象编辑器访问这些属性或事件。
由于它们在对象编辑器中显示,类的published 部分被称为类的设计界面。设计界面应该包括应用
程序员在设计时需要定制的所有部分,但是应该排除依赖运行时特定信息的所有属性。
由于应用程序员不能直接给它们赋值,只读属性不能成为设计界面的一部分,因此只读属性只能定
义为public,而不能定义为published。
16.2.4 分派方式
“分派”(Dispatch)是指当遇到一个方法调用时,程序决定一个方法应该从哪儿调用的方式。调
用一个方法的代码看起来就和其他过程和函数调用类似。但是类有不同的分派方法。
类的3 种分派方式为静态的(Static)、虚拟的(Virtual)和动态的(Dynamic)。
1.静态方法
除非声明时另外指定,否则所有的方法默认都是静态的。静态方法作用类似于常规的过程或函数。
编译时编译器就可以决定方法的确切地址,同时将方法连接到可执行代码中。
静态方法的主要优点是分派速度很快。由于编译器可以确定方法的确切地址,因此可以直接连接
第16 章 开发新的VCL 组件
·427·
方法。相对地,虚拟和动态方法则是运行时通过间接的方法去寻找方法的地址,因而花费的时间较长。
静态方法在被子类继承时不改变。如果定义类时包括静态方法,然后从它派生一个新类,派生类
在相同的地址下共享相同的方法。这就意味着不能重载静态方法,无论如何调用,静态方法只做相同
的事情。如果在派生类中定义与祖先类中静态方法名称一样的方法,新方法只是简单替代派生类中继
承的方法。
声明一个静态方法只需在类定义的部分加入类似下面的代码:
procedure MyProcedure;
这段代码声明了一个名称为MyProcedure 的静态方法。
2.虚方法
虚方法与静态方法相比,其分派机制更复杂,更富有弹性。在子类中虚方法可以重新定义,但仍
可以在祖先类中调用。在编译时虚方法的地址不能被确定,而是由定义方法的对象在运行时寻找它的
地址。
使一个方法成为虚方法需要在声明方法语句后面增加virtual 指令。virtual 指令在对象虚方法列表
(Virtual Method Table,VMT)创建了一个入口。VMT
保留了一个对象类型中所有虚方法的地址。例
如声明一个虚方法的代码:
procedure MyProcedure;virtual;
当从已有的类中派生新类时,新类具有自己的VMT,它包括所有的祖代的VMT 及在新类中增加
的所有虚方法。
3.重载方法
重载一个方法就是指扩展或重新定义它,而不是替代它。子类可以重载所有继承的虚方法。在子
类中重载某方法,只需要在方法声明的末尾增加override 指令。声明一个重载方法的例子如下:
procedure MyProcedure;override;
在下面的情况下重载一个方法会产生编译错误:
*方法在祖类中不存在。
*这个名字的祖先方法是静态的。
*方法声明不一致(包括调用参数的顺序、类型不同)。
4.动态方法
在分派机制上,动态方法与虚方法有一些细小的差别。因为动态方法在VMT 中没有入口,这样
可以减少对象消耗的内存。然而,分派动态方法通常要比分派常规的虚方法慢。如果某种方法需经常
调用,或者执行时间非常关键,就应该把这种方法定义为虚方法而不是动态方法。
对象必须存储动态方法的地址。但是与接收VMT 中的一个入口不同,动态方法是独立列出来的。
动态方法列表包含的只是一个特殊类中引入的或重载的方法,与之对比的是,VMT 包括对象所有的虚
方法(继承的或新引入的)。继承的动态方法通过搜索每个祖先的动态方法列表而进行分派,通过向上
查找继承树来完成。
使一个方法成为动态方法需要在方法声明的后面直接增加dynamic 指令。下面代码声明了一个动
态方法:
procedure MyProcedure; dynamic;
16.2.5 抽象类成员
当在祖先类中把一种方法声明为abstract 时,则必须在程序使用这些组件之前在子组件中实现它
的接口(通过重定义和实现)。Delphi 不能创建包含了抽象成员的类的实例。声明一个抽象方法的示例
代码如下:
procedure MyProcedure; abstract;
·428·
值得注意的是,如果一个组件中有方法声明为抽象方法,则不能在设计时和运行时创建这个组件
的实例。这个组件只能作为派生其他组件的父类。
16.2.6 类和指针
每个类(因此也是每个组件)实际上就是一个指针。编译器自动解释类指针,所以大多时候不必
考虑它。但当把类作为参数时,类作为指针就显得很重要。通常应该通过传值而不是传引用调用类(即
不需要var 指示),原因就是类已经是指针,即已经是引用了。把类作为引用传递就意味着把引用传递
给引用。例如一个表单的构造方法中的参数Sender 是一个TObject 对象,不需要使用var 指示:
procedure FormCreate(Sender: TObject);
16.3 创建属性
属性(Property)是组件中最特殊的部分,在设计应用程序时被可以看见和操作,并且在交互过程
中能立即得到返回结果。一个好的属性设计可使组件用户使用起来更容易,同时也便于开发者自己维
护。
16.3.1 属性的类型
属性可以是任何类型。不同类型在对象编辑器中显示不同,它们可以在设计时验证所设置的属性。
对象编辑器支持的属性类型,如表16-5 所示。
表16-5 对象编辑器支持的属性类型
属性类型 对象编辑器中的处理
简单类型 Numeric、character 和string 属性以数字、字符和串显示,应用程序员可以直接编辑
枚举类型
枚举类型属性(包括布尔值)显示为可编辑的字符串。程序员可以通过双击鼠标取得可能的取值或通过
下拉式列表框显示所有的可能取值
集合类型
集合类型属性的设置就如同一个集合。通过在属性列双击,程序员可以展开集合,将每一个元素作为一
个布尔值,如果集合中含有它则为True
对象类型
对象类型属性本身有属性编辑器。如果一个靠属性支撑的类有自己的published 属性,对象编辑器可以通
过鼠标双击让程序员展开包含这些属性的列表并单独编辑它们。对象属性必须从TPersistent 继承
界面类型
只要值是一个组件实现的界面,界面类型的属性就可以显示在对象编辑器中。界面属性常常有自己的属
性编辑器
数组类型
数组类型属性必须有自己的属性编辑器,对象编辑器中没有内嵌对数组属性编辑的支持。注册组件时可
指定属性编辑器
16.3.2 发布继承的属性
所有组件都从祖先类继承属性。当从已有组件派生新组件时,新组件将继承祖先类型的所有属性。
如果继承的是抽象类,则继承的属性是protected 或public,但不是published。
如果想在设计时在对象编辑器访问protected 或public 属性,必须将该属性重定义为published,即
对子类继承的属性重新声明。
16.3.3 定义属性
1.属性的声明
属性应该在定义组件类时声明,声明属性需要描述以下3 个内容。
*属性名称;
*属性类型;
*属性值读/写的方法。如果没有声明写方法,那么属性就是只读的。
第16 章 开发新的VCL 组件
·429·
如果要使属性能在设计时在对象编辑器中被编辑,应当在published 部分声明该属性。表单中组件
的published 属性的值与组件一起保存在表单文件中。定义在组件对象声明的public 部分的组件属性,
可以在运行时访问以及在程序代码中读或赋值。
2.内部数据存储
Delphi 没有特别规定如何存储属性的数据值,通常Delphi 组件遵循下列惯例。
*属性数据存储在类的数据域处。
*用于存储属性值的数据域是private,只能被组件自身访问。派生的组件只应使用继承的属性,
不需要直接访问内部数据存储。
*属性对象域的标识符以F 开头,例如定义在TControl 中的属性Widthd 的值存储在FWidth 域中。
这些惯例的基本原则是只有属性的实现方法可以访问这些数据。如果一个方法或另外一个属性需
要更改数据,那么都要通过属性,而不是直接访问已存储的数据,这样就保证可以改变继承属性的实
现而不影响派生的组件。
3.直接访问
得到属性数据最简单的办法是直接访问。属性声明的read 和write 部分描述了怎样不通过调用访
问方法来给内部数据域赋值。当需要在对象编辑器中使用属性并且可以改变它的值而不触发其他过程
时,可采用直接访问。
在属性声明中,一般都在读取时进行直接访问,而在写入时使用方法访问,这样组件的状态就会
随着属性值而改变。
4.访问方法
可以在属性声明的read 和write 部分描述访问方法。访问方法应该是protected,通常被定义为虚
拟的,这样子组件可以重载属性的实现。
应该避免访问方法为public,因为使它为protected 可以保证应用程序员在调用这些方法时不改变
属性。
(1)读方法
属性的读方法是不带参数的函数(下面注明的除外),并且返回与属性相同类型的值。通常读函
数的名字是“Get”后加属性名,例如属性Count 的读方法是GetCount。需要时读方法可以通过处理
内部存储的数据产生属性值。
带参数的读方法的惟一属性是数组属性,属性用索引描述,并将索引值作为参数。
如果不定义read 方法,则属性是只写的。只写属性很少使用。
(2)写方法
属性的写方法总是只带一个参数的过程(下面注明的除外)。参数可以是引用或值,可以任意取
名。通常写方法名是“Set”加属性名。例如属性Count 的写方法名是SetCount。参数的值将设置为属
性的新值,因此写方法需要对内部存储数据中进行写操作。
写方法也可能不只带一个参数,这种情况就是数组属性,数组属性用索引描述,并将索引值作为
写方法的第2 个参数。
通常在改变属性前写方法要检测新值是否与当前值不同。例如下面是一个简单的整数属性Count
的写方法,它的存储域是FCount.。如果没有声明写方法,那么属性是只读的。
procedure TMyComponent.SetCount(Value: Integer);
begin
if Value <> FCount then
begin
FCount := Value;
Update;
·430·
end;
end;
5.属性的默认值
声明一个属性的同时可以指定属性的默认值。VCL 用默认值决定是否在表单文件中存储属性。如
果不指定属性默认值,VCL 将存储属性值。
指定属性的默认值的方法是在属性声明或重声明的后面直接加default 指令,再跟默认值。例如:
property Cool Boolean read GetCool write SetCool default
True;
注意:声明默认值并不是要求将属性设置为该值。
组件的构造方法可以初始化属性的值。
6.不指定属性的默认值
当重声明一个属性时,即使继承的属性在祖先类中已指定了默认值,也可以不指定属性的默认值。
指定属性无默认值的方法是直接在属性声明后面加nodefault 指令,如:
property FavoriteFlavor string nodefault;
如果是第1 次声明属性,则没有必要加nodefault 指令,因为没有声明默认值即指无默认值。
7.创建数组属性
许多属性像数组一样对自己进行索引。例如TMemo 的Lines 属性就是一个索引列表,可将其看作
是数组。在数据量较大时,Lines 为特殊的元素(一个字符串)提供了很自然的访问。
数组属性的声明与像其他属性相同,除非声明时包括一个或多个特殊的索引。索引可以是任何类
型。但是如果需要指定属性的读和写部分,则它们必须是方法,不能是数据域。
对于数组属性的读和写方法采用附加的参数,其与索引相对应。在声明中参数必须与声明中索引
的顺序和类型相同。下面是声明一个数组属性的例子:
property Day: Integer index 3 read GetDateElement write
SetDateElement;
property Month: Integer index 2 read GetDateElement write
SetDateElement;
property Year: Integer index 1 read GetDateElement write
SetDateElement;
数组属性和数组有许多不同之处。如索引不同,数组属性的指数不必是整型,可以用字符串给一
个属性建立索引。另外,程序员只能引用数组属性的单个元素,而不能是整个数组。
8.创建属性的子组件
当一个属性值是另外一个组件时,通常可以将另一个组件的实例增加到表单或数据模块中,然后
将该组件作为属性值。但是组件可以创建自己的对象实例来实现属性值,这样的组件就叫子组件。
子组件可以是任何持续化的对象(TPersistent 的任一后代)。与独立组件作为属性值不同,子组件
的published 属性保存在创建它们的组件中。但是为了保证能运行,必须处理好下面的情况。
子组件的Owner 必须是创建它的组件,并将它作为published 属性。如果子组件是TComponent
的后代,可设置子组件的Owner 属性为父组件。对于其他子组件,必须重载持续化对象的GetOwner
方法,以便返回创建它的组件。
如果子组件是TComponent 的后代,则设置子组件是通过调用SetSubComponent 方法来完成的。
这个方法可在创建子组件时或在子组件的构造方法中调用。
一般来说,如果一个属性的值是子组件,那么它应该是只读的。如果允许对它赋值,那么当设置
另一个组件为属性值时,属性设置者必须释放子组件。另外,当属性设置为nil 时,组件会经常初始
化子组件。而且一旦属性改变为另一组件,设计时子组件将不能恢复。下面的代码介绍了如何给一个
属性值为TTimer 对象的属性赋值的过程:
procedure TDemoComponent.SetTimerProp(Value: TTimer);
begin
第16 章 开发新的VCL 组件
·431·
if Value <> FTimer then
begin
if Value <> nil then
begin
if Assigned(FTimer) and (FTimer.Owner = Self) then
FTimer.Free;
FTimer := Value;
FTimer.FreeNotification(self);
end
else //空值
begin
if Assigned(FTimer) and (FTimer.Owner
<> Self) then
begin
FTimer := TTimer.Create(self);
FTimer.Name := ’Timer’; //可选,可以使结果更友好
FTimer.SetSubComponent(True);
FTimer.FreeNotification(self);
end;
end;
end;
end;
注意上面代码中,属性原来的子组件调用了FreeNotification 方法。这样保证了在它即将销毁时可
以发送一个消息。发送消息通过调用Notification 方法实现。可重载Notification
方法来处理这个调用,
下面是一个例子:
procedure TDemoComponent.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = FTimer) then
FTimer := nil;
end;
9.为接口创建属性
接口可以作为published 属性的值。但是,组件从接口接收消息的机制是不同的。在创建子组件属
性时,属性设置者调用被设置为属性值的组件的FreeNotification 方法。这样当释放作为属性值的子组
件时,组件可以更新。然而当属性值是接口时,程序不能访问实现接口的组件,因此就不能调用它的
FreeNotification 方法。
为处理这种情况,可调用组件的ReferenceInterface 方法。
procedure TDemoComponent.SetMyIntfProp(const Value:
IMyInterface);
begin
ReferenceInterface(FIntfField, opRemove);
FIntfField := Value;
ReferenceInterface(FIntfField, opInsert);
end;
调用一个指定接口的ReferenceInterface 与调用另一组件的FreeNotification
方法相同。因此在从属
性设置器中调用了ReferenceInterface 后,可重载Notification 方法以便处理来自接口实现的消息:
·432·
procedure TDemoComponent.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Assigned(MyIntfProp)) and
(AComponent.IsImplementorOf(MyInftProp)) then
MyIntfProp := nil;
end;
注意Notification 代码分配nil 给属性MyIntfProp
而不是给私有的数据域FintfField。这保证
Notification 调用属性设置者,然后调用ReferenceInterface
以删除在以前设置属性值时建立的请求。所
有赋值给接口属性的过程必须通过属性设置。
16.3.4 存储和装载属性
Delphi 可以在表单文件(在VCL 中的为*.dfm,在CLX 中为*.xfm)中存储窗体和组件。一个表
单文件可存储表单和它的组件。当Delphi 开发者向表单增加组件并保存时,组件必须具有把属性写入
表单文件的功能,同样当装载Delphi 或执行应用程序时,组件必须从表单文件中恢复。
由于存储和装载能力是从组件继承,因此大多时候程序员不必做什么就可以使表单文件中的组件
工作。但是如果想改变组件存储方式或装载时的初始化方式,那就必须了解内部机制。
1.存储和装载机制的运用
表单描述由表单属性列表组成,表单中每个组件中的描述也是相似的。每个组件,包括表单自身,
负责存储和装载自身的描述。
存储时,组件可写入与默认值不同的所有published 属性的值。装载时,组件首先创建自己,设置
所有的属性为默认值,然后读出存储的非默认属性值。
这个默认机制满足大多组件的需要,因此一般并不需要修改存储组件到表单文件的方法。然而有
几种方法可以定制适合特殊组件要求的存储和装载过程。
2.指定默认值
只要属性值不同于默认值,Delphi 组件就会保存这些值。如果不指定,Delphi 就假定属性无默认
值,也就是不管取什么值组件都要存储属性值。
指定属性的默认值可以通过在属性声明最后添加default 指令和新的默认值来完成,也可以在属性
重声明中指定默认值。实际上,属性重声明的一个原因就是要设置不同的默认值。
注意:创建对象时指定的默认值并不自动赋给属性。
因此必须肯定组件构造方法能赋给属性需要的值。组件构造方法没有设置的属性值都被假定为0
值,也就是无论属性类型是什么,其存储内存都是0。这样数值型的默认值为0,布尔型为False,指
针为nil 等。如果有问题,则要在构造方法中指定属性值。
3.决定存储内容
Delphi 是否存储组件的每个属性是可以控制的。默认地,在类声明的published 部分的属性要存储。
也可以选择根本不存储已有的属性,或设定由一个函数动态决定是否存储属性。为控制Delphi 是否存
储属性,可以在属性声明后面添加stored 指令,后面跟True、False 或一个布尔函数。
4.装载后的初始化
在组件从存储中读取所有的属性值后,将调用名为Loaded 的虚方法,它可以执行要求的初始化。
Loaded 的调用发生在表单和控件显示之前,所以不必考虑由初始化产生的屏幕闪烁。
为了在装载完属性值后初始化组件,可以重载Loaded 方法。
注意:在所有Loaded 方法中首先要调用继承的Loaded 方法,保证在初始化自己的组件前正
第16 章 开发新的VCL 组件
·433·
确初始化所有继承的属性。
下面代码来自TDatabase 组件。在装载后,Database 要重新建立存储时打开的连接并指明如何处
理连接中出现的异常。
procedure TDatabase.Loaded;
begin
inherited Loaded; {首先调用继承方法}
try
if FStreamedConnected then Open {重建连接}
else CheckSessionName(False);
except
if csDesigning in ComponentState then {设计时}
Application.HandleException(Self) {由Delphi 处理异常}
else raise; {否则抛出异常}
end;
end;
5.存储和装载非published 属性
在默认情况下,只有组件的published 属性可以装载和保存到表单文件中。实际上非published 属
性也可以保存到表单文件中,但是需要告诉Delphi 如何装载和保存属性值。需要保存到表单文件的非
published 属性通常有两种情况:一种是不希望出现在对象编辑器但是是持续化的属性;另一种是由于
保存和装载“太复杂”,导致Delphi 不知如何读写的属性,例如当一个属性是TString 对象时,则它不
能依赖Delphi 自动存储和装载它代表的字符串,必须使用这种机制。
(1)创建存储和装载属性值的方法
为存储和装载非published 属性,必须首先创建存储和装载属性值的方法。可有两种选择。
*创建TWriterProc 类型的方法保存属性值,创建TReaderProc 类型的方法装载属性值。
这样可以充分利用Delphi 内建的保存和装载简单类型的功能。如果创建的属性值不是Delphi 已知
的可以自动保存和装载的属性,可以采用这个办法。
*创建两个TStreamProc 类型的方法。一个用于存储,一个用于装载属性值。TStreamProc 将一个
stream 作为参数,可通过stream 的方法去读写属性值。
例如在运行时创建代表某组件的一个属性,由于组件不是表单设计者创建的,即使Delphi 知道如
何写值,也不能自动写值。由于stream 可以存储和保存组件,因此可以采用第1 种方法。
下面是装载和存储一个动态创建的、具有名为MyCompProperty 的属性的组件的方法:
procedure TSampleComponent.LoadCompProperty(Reader:
TReader);
begin
if Reader.ReadBoolean then
MyCompProperty := Reader.ReadComponent(nil);
end;
procedure TSampleComponent.StoreCompProperty(Writer:
TWriter);
begin
Writer.WriteBoolean(MyCompProperty <>
nil);
if MyCompProperty <> nil
then
Writer.WriteComponent(MyCompProperty);
end;
·434·
(2)重载DefineProperties 方法
一旦创建方法去存储和装载属性值,就要重载组件DefineProperties 方法。当存储和装载组件时,
Delphi 就会调用此方法。在DefineProperties 方法中,必须调用当前文件对象的DefineProperty
或
DefineBinaryProperty 方法, 并将它传给装载和存储属性值的方法。如果装载和存储的方法是
TWriterProc 和TReaderProc 类型的,那么调用的是文件对象的DefineProperty
方法。如果创建的是
TStreamProc 类型的方法,那么调用的是DefineBinaryProperty。
无论选哪种方法去定义属性,都必须将它传给存储和装载属性值的方法,并使用一个布尔值指明
属性值是否需要写(如果值可以被继承或有一个默认值,则不需要写)。
例如采用TReaderProc 中的LoadCompProperty 方法和TWriterProc
中的StoreCompProperty 方法,
可以按照下面方法去重载DefineProperties:
procedure TSampleComponent.DefineProperties(Filer:
TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
{检查祖先的继承值}
begin
if TSampleComponent(Filer.Ancestor).MyCompProperty = nil
then
Result := MyCompProperty <>
nil
else if MyCompProperty = nil or
TSampleComponent(Filer.Ancestor).MyCompProperty.Name
<> MyCompProperty.Name
then
Result := True
else Result := False;
end
else {没有继承值,检查默认值}
Result := MyCompProperty <>
nil;
end;
begin
inherited; {允许基类定义属性}
Filer.DefineProperty(’MyCompProperty’, LoadCompProperty,
StoreCompProperty, DoWrite);
end;
16.4 创建事件
事件是系统发生的事件与组件响应的该系统事件的一段代码之间的连接。响应代码被称为事件处
理过程(event handler),它几乎总是由应用程序员来编写。通过使用事件,应用程序员不需要改变组
件本身就能定制组件的行为,这可以理解为一种授权。
通常的用户行为(例如鼠标的行动)都已经内建到所有的标准组件之中,但是组件开发者也可以
定义新的事件。事件的实现与属性类似,因此在创建或改变组件的事件之前应熟悉如何创建属性。
16.4.1 事件定义
事件是联接发生的事情与某些代码的机制,更明确的说,是方法指针,一个指向特定对象实例的
特定方法的指针。
从应用程序员的角度看,事件只是与系统事件(如OnClick)有关的名称,能给该事件赋特定的
方法供调用,或者把事件看作是由应用程序员编写的代码,而事件发生时由系统调用的处理办法。例
如按钮Buttonl 有OnClick 方法。默认情况下,当指定一个值给OnClick 事件时,表单编辑器产生一个
ButtonlClick 事件处理过程,并将其赋给OnClick。当在按钮上发生Click
事件时,按钮调用赋给OnClick
第16 章 开发新的VCL 组件
·435·
的方法ButtonlClick。
但是,从组件开发者角度看,事件有更多的含义。最重要的是提供了一个让用户编写代码响应特
定事情的场所。
1.事件是方法指针
Delphi 使用方法指针实现事件。一个方法指针是指向特定对象实例的特定方法的特定指针。作为
组件开发者,能将方法指针作为一个代理。一发现事件发生,就调用由应用程序员定义的该事件的方
法。
方法指针的工作方式与其他的过程类型类似,但它们保持一个隐含的指向对象实例的指针。当应
用程序员将一个事件处理过程赋值给一个组件的事件,这个赋值不只是一个具有特定名字的方法,而
且是一个特定类的实例的方法(那个实例通常是包含组件的表单,但不是一定的)。
例如调用Click 事件的处理过程。所有的控件都继承了一个名为Click 的动态方法,以处理Click
事件。它的声明如下:
procedure Click; dynamic;
Click 方法调用应用程序员的Click 事件处理过程。如果应用程序员给控件的OnClick 事件赋予了
处理过程(Handle),那么当鼠标单击控件时将导致方法被调用。
2.事件是属性
组件采用属性的形式实现事件。与大多数其他属性不同,事件不能使用方法来实现read 和write
部分。事件属性使用了相同类型的私有对象域作为属性。习惯上,域名在属性名前加“F”。例如OnClick
方法的指针,保存在TNotifyEvent 类型FOnClick 域中。OnClick 事件属性的声明如下:
type
TControl = class(TComponent)
private
FOnClick: TNotifyEvent; {声明保存方法指针的域}
...
protected
property OnClick: TNotifyEvent read FOnClick write
FOnClick;
end;
与其他类型的属性一样,运行时可以设置和改变事件的值。将事件做成属性的主要好处是应用程
序员能在设计时使用对象编辑器设置事件处理过程。
3.事件类型是方法指针类型
因为一个事件是指向事件处理过程的指针,因此事件属性必须是方法指针类型。相似地,所有被
用作事件处理过程的代码,必须是相应的对象的方法。
所有的事件方法都是过程。为了与所给类型的事件兼容,一个事件处理过程必须有相同数目、类
型和顺序的参数,并以相同的方式传递。
Delphi 定义了它本身所有标准事件处理过程的方法类型。当创建自己的事件时,可以使用已有的
事件类型,也可以创建新的。
4.事件处理方法的类型都是过程
虽然编译器允许声明事件指针类型为函数,但不应该这样声明事件处理方法。因为空函数的返回
值是不确定的,所以如果空的事件处理方法是函数的话,则事件处理不一定是有效的。正因为如此,
所有的事件和与它们相关的事件处理方法都应该是过程。
虽然不能用函数做事件处理过程,但可以用var 参数得到返回信息。但是这样处理时必须在调用
事件处理方法之前给参数赋予有效的值,以保证应用程序代码不一定要改变这个值。
在事件处理过程中传递var 参数的典型例子是TKeyPressEvent 类型的KeyPressed 事件。
·436·
TKeyPressEvent 定义中含有两个参数。一个指示哪个对象产生该事件,另一个指示哪个键被按下。
type
TKeyPressEvent = procedure(Sender: TObject; var Key: Char) of
object;
通常Key 参数包含用户按下键的字符。在某些情况下,应用程序员可能想改变字符值。例如在编
辑器中强制所有字符为大写,在这种情况下,应用程序员能定义下列的事件处理过程:
procedure TForm1.Edit1KeyPressed(Sender: TObject; var Key:
Char);
begin
Key := UpCase(Key);
end;
5.事件处理过程是可选的
在为组件创建事件时,要记住应用程序员可能并不编写该事件的处理过程。这意味着组件不能因
为组件用户没有编写特定事件的处理代码而出错。
这种事件处理过程的可选性表现在如下两个方面。
(1)组件用户并不需要处理所有事件
事件总是不断地发生在GUI 应用程序中。例如在可视组件上方移动鼠标就引起Windows 发送大
量的鼠标移动消息给组件,组件将鼠标移动消息转换为OnMouseMove 事件。在大多数情况下,应用
程序员不需要关心鼠标移动事件,因为组件不依赖鼠标事件的处理过程。同样,组件也不能依赖于它
们的事件处理过程。
(2)组件用户能在事件处理过程中加入任意的代码
更进一步,应用程序员可以在事件处理过程中加入任何代码。Delphi 组件库的组件都支持这种方
式以使所写代码产生错误的可能性最小。显然,不能防止用户代码出现逻辑错误,但是可以保证在调
用事件之前数据结构得到初始化,以使应用程序员不能访问无效数据。