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 下载免费组件。这些组件足以支持通常应用系统开发。
但应用开发人员仍有必要本身制做组件。采用组件形式能够把对象严密封装,并加上一层直观外
壳,有利于软件调试和代码重用。开发群体以组件为功能单位分工协做,比较容易实现工程化管理,
从软件规划设计到测试修改均可以减小意外差错,大大提升工做效率。成熟的组件还能够做为商品软
件出售,带来附加效益,有利于软件开发的社会化分工协做。node
·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 组件库的组件都支持这种方 式以使所写代码产生错误的可能性最小。显然,不能防止用户代码出现逻辑错误,可是能够保证在调 用事件以前数据结构获得初始化,以使应用程序员不能访问无效数据。