DELPHI基础教程
第十九章 Delphi自定义部件开发(一)
● 教你如何自定义部件 ● 使你的部件成为 Delphi 环境的有机组合部分
19.1 Delphi 部件原理
19.1.1 什么是部件
部件是 Delphi 应用程序的程序构件。尽管大多数部件代表用户界面的可见元素,但部件也可以是程序中的不可见元素,如数据库部件。为弄清什么是部件可以从三个方面来考察它:功能定义、技术定义和经验定义。 1. 部件的功能定义 从最终用户角度,部件是在 Component Palette 上选择的,并在窗体设计窗口和代码窗口中操作的元素。从部件编写者角度,部件是代码中的对象。在编写部件之前,你应用相当熟悉已有的 Delphi 部件,这样才能使你的部件适合用户的需要。编写部件的目标之一是使部件尽可能的类似其它部件。 2. 部件的技术定义 从最简单的角度看,部件是任何从 TComponent 继承的对象。 TComponent 定义了所有部件必须要的、最基本的行为。例如,出现在 Component Palette 上和在窗体设计窗口中编辑的功能。但是 TComponent 并不知如何处理你的部件的具体功能,因此,你必须自己描述它。 3. 部件编写者自己的定义。 在实际编程中,部件是能插入 Delphi 开发环境的任何元素。它可能具有程序的各种复杂性。简而言之,只要能融入部件框架,部件就是你用代码编写的一切。部件定义只是接口描述,本章将详细阐述部件框架,说明部件的有限性,正如说明编程的有限性。本章不准备教你用所给语言编写每一种部件,只能告诉编定代码的方法和怎样使部件融入 Delphi 环境。
19.1.2 编写部件的不同之处
在 Delphi 环境中建立部件和在应用程序中使用部件有三个重要差别: ● 编写部件的过程是非可视化的 ● 编写部件需要更深入的关于对象的知识 ● 编写部件需要遵循更多的规则
lang="ZH-CN" 1. 编写部件是非可视化的 编写部件与建立 Delphi 应用最明显的区别是部件编写完全以代码的形式进行,即非可视化的 。因为 Delphi 应用的可视化设计需要已完成的部件,而建立这些部件就需要用 Object Pascal 代码编写。 虽然你无法使用可视化工具来建立部件,但你能运用 Delphi 开发环境的所有编程特性如代码编辑器、集成化调试和对象浏览。 2. 编写部件需要更深的有关对象的知识 除了非可视化编程之外,建立部件和使用它们的最大区别是:当建立新部件时,需要从已存部件中继承产生一个新对象类型,并增加新的属性和方法。另一方面,部件使用者,在建立 Delphi 应用时,只是使用已有部件。在设计阶段通过改变部件属性和描述响应事件的方法来定制它们的行为。 当继承产生一个新对象时,你有权访问祖先对象中对最终用户不可见的部分。这些部分被称为 protected 界面的。在很大部分的实现上,后代对象也需要调用他们的祖先对象的方法,因此,编写部件者应相当熟悉面向对象编程特性。 3. 编写部件要遵循更多的规则 编写部件过程比可视化应用生成采用更传统的编程方法,与使用已有部件相比,有更多的规则要遵循。在开始编写自己的部件之前,最重要的事莫过于熟练应用 Delphi 自带的部件,以得到对命名规则以及部件用户所期望功能等的直观认识。部件用户期望部件做到的最重要的事情莫过于他们在任何时候能对部件做任何事。编写满足这些期望的部件并不难,只要预先想到和遵循规则。
19.1.3 建立部件过程概略
简而言之,建立自定义部件的过程包含下列几步: ● 建立包含新部件的库单元 ● 从已有部件类型中继承得到新的部件类型 ● 增加属性、方法和事件 ● 用 Delphi 注册部件 ● 为部件的属性方法和事件建立 Help 文件
如果完成这些工作,完整的部件包含下列 4 个文件 ● 编译的库单元 ( .DCU 文件 ) ● 选择板位图 (.DCR 文件 ) ● Help 文件 (.HLP 文件 ) ● Help-keyword 文件 (.KWF 文件 )
19.2 Delphi 部件编程方法
19.2.1 Delphi 部件编程概述
19.2.1.1 Delphi 可视部件类库
Delphi 的部件都是可视部件类库( VCL )的对象继承树的一部分,下面列出组成 VCL 的对象的关系。 TComponent 是 VCL 中每一个部件的共同祖先。 TComponent 提供了 Delphi 部件正常工作的最基本的属性和事件。库中的各条分支提供了其它的更专一的功能。 当建立部件时,通过从对象树中已有的对象继承获得新对象,并将其加入 VCL 中。 19.2.1.2 建立部件的起点 部件是你在设计时想操作的任意程序元素。建立新部件意味着从已有类型中继承得到新的部件对象类。 建立新部件的主要途径如下: ● 修改已有的控制 ● 建立原始控制 ● 建立图形控制 ● 建立 Windows 控制的子类 ● 建立非可视部件
下表列出了不同建立途径的起始类 lang="ZH-CN" 表 19.1 定义部件的起始点 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 途 径 起 始 类 ───────────────────────────── 修改已有部件 任何已有部件,如 TButton 、 TListBox 或抽象部件对象如 TCustomListBox 建立原始控制 TCustomControl 建立图形控制 TGraphicControl 建立窗口控制的子类 TWinControl 建立非可视部件 TComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
也可以继承非部件的其它对象,但无法在窗体设计窗口中操作它们。 Delphi 包括许多这种对象,如 TINIFile 、 TFont 等。 1. 修改已有控制 建立部件的最简单的方法是继承一个已有的、可用的部件并定制它。可以从 Delphi 提供的任何部件中继承。例如,可以改变标准控制的缺省属性值,如 TButton 。 有些控制,如 Listbox 和 Grid 等有许多相同变量,在这种情况下, Delphi 提供了抽象控制类型,从该类型出发可定制出许多的类型。例如,你也许想建立 TListBox 的特殊类型,这种部件没有标准 TListBox 的某些属性,你不能将属性从一个祖先类型中移去,因此你需要从比 TListBox 更高层次的部件继承。例如 TCustomListBox ,该部件实现了 TCustomListBox 的所有属性但没有公布 (Publishing) 它们。当从一个诸如 TCustomListBox 的抽象类中继承时,你公布那些你想使之可获得的属性而让其它的保护起来 (protected) 。 2. 建立原始控制 标准控制是在运行时可见的。这些标准控制都从 TWinControl ,继承来的,当你建立原始控制时,你使用 TCustomControl 作为起始点。标准控制的关键特征是它具有窗口句柄,句柄保存在属性 Handle 中,这种控制: ● 能接受输入焦点 ● 能将句柄传送给 Windows API 函数
如果控制不需要接受输入焦点,你可把它做成图形控制,这可能节省系统资源。 3. 建立图形控制 图形控制非常类似定制的控制,但它们没有窗口句柄,因此不占有系统资源。对图形控制最大的限制是它们不能接收输入焦点。你需要从 TGraphicControl 继承,它提供了作图的 Canvas 和能处理 WM_PAINT 消息,你需要覆盖 Paint 方法。 4. 继承窗口控制 Windows 中有一种称之为窗口类的概念,类似于面向对象的对象和类的概念。窗口类是 Windows 中相同窗口或控制的不同实例之间共享的信息集合。当你用传统的 Windows 编程方法创建一种新的控制,你要定义一个新的窗口类,并在 Windows 中注册。你也能基于已有的窗口类创建新的窗口类。这就称为从窗口类继承。在传统的 Windows 编程中,如果你想建立客户化的控制,你就必须将其做在动态链接库里,就象标准 Windows 控制,并且提供一个访问界面。使用 Delphi ,你能创建一个部件包装在已有窗口类之上。如果你已有客户化控制的库,并想使其运行在你的 Delphi 应用中,那你就能创建一个使你能使用已有控制和获得新的控制的部件。在库单元 StdCtrls 中有许多这样的例子。 5. 建立非可视化的部件 抽象对象类型 TComponent 是所有部件的基础类型。从 TComponent 直接继承所创建的部件就是非可视化部件。你编写的大多数部件都是可视控制。 TComponent 定义了部件在 FormDesigner 中所需的基本的属性和方法。因此,从 TComponent 继承来的任何部件都具备设计能力。 非可视部件相当少,主要用它们作为非可视程序单元(如数据库单元)和对话框的界面。
19.2.1.3 建立新部件的方法
建立新部件的方法有两种: ● 手工建立部件 ● 使用 Component Expert
一旦完成建立后,就得到所需的最小功能单位的部件,并可以安装在 Component Palette 上。安装完后,你就能将新部件放置在窗体窗口,并可在设计阶段和运行阶段进行测试。你还能为部件增加新的特征、更新选择板、重新测试。 1. 手工创建部件 显然创建部件最容易的方法是使用 Component Expert 。然而,你也能通过手工来完成相同步骤。 手工创建部件需要下列三步: ● 创建新的库单元 ● 继承一个部件对象 ● 注册部件
⑴ 创建新的库单元 库单元是 Object Pascal 代码的独立编译单位。每一个窗体有自己的库单元。大多数部件(在逻辑上是一组)也有自己的库单元。 当你建立部件时,你可以为部件创建一个库单元,也可将新的部件加在已有的库单元中。 ① 为部件创建库单元,可选择 File/New... ,在 New Items 对话框中选择 Unit , Delphi 将创建一个新文件,并在代码编辑器中打开它 ② 在已有库单元中增加部件,只须选择 File/OPen 为已有库单元选择源代码。在该库单元中只能包含部件代码,如果该库单元中有一个窗体,将产生错误
⑵ 继承一个部件对象 每个部件都是 TComponent 的后代对象。也可从 TControl 、 TGraphicControl 等继承。 为继承一个部件对象,要将对象类型声明加在库单元的 interface 部分。 例如,建立一个最简单的从 TComponent 直接继承非可视的部件,将下列的类型定义加在部件单元的 interface 部分。
type TNewComponent=class(TComponent) …… end;
现在你能注册 TNewComponent 。但是新部件与 TComponent 没什么不同,你只创建了自己部件的框架。 ⑶ 注册部件 注册部件是为了告诉 Delphi 什么部件被加入部件库和加入 Component Palette 的哪一页。 为了注册一个部件: ① 在部件单元的 interface 部分增加一个 Register 过程。 Register 不带任何参数,因此声明很简单:
procedure Register;
如果你在已有部件的库单元中增加部件,因为已有 Register 过程,因此不须要修改声明。 ② 在库单位的 implementation 部件编写 Register 过程为每一个你想注册的部件调用过程 RegisterComponents ,过程 RegisterComponents 带两个参数: Component Palette 的页名和部件类型集。例如,注册名为 TNewComponent 的部件,并将其置于 Component Palette 的 Samples 页,在程序中使用下列过程:
procedure Register; begin RegisterComponents('Samples', [TNewComponent]); end;
一旦注册完毕, Delphi 自动将部件图标显示在 Component Palette 上。 2. 使用 Component Expert (部件专家) 你能使用 Component Expert 创建新部件。使用 Component Expert 简化了创建新部件最初阶段的工作,因为你只需描述三件事: ● 新部件的名字 ● 祖先类型 ● 新部件要加入的 Component Palette 页名
Component Expert 执行了手工方式的相同工作: ● 建立新的库单元 ● 继承得到新部件对象 ● 注册部件
但 Component Expert 不能在已有单元中增加部件。 可选择 File/New... ,在 New Items 对话框中选择 Component ,就打开 Component Expert 对话框。 填完 Component Expert 对话框的每一个域后,选择 OK 。 Delphi 建立包括新部件和 Register 过程的库单元,并自动增加 uses 语句。 你应该立刻保存库单元,并给予其有意义的名字。
19.2.1.4. 测试未安装的部件
在将新部件安装在 Component Palette 之前就能测试部件运行时的动作。这对于调试新部件特别有用,而且还能用同样的技术测试任意部件,无论该部件是否出现在 Component Palette 上。 从本质上说,你通过模仿用户将部件放置在窗体中的 Delphi 的动作来测试一个未安装的部件。 可按下列步骤来测试未安装的部件 1. 在窗体单元的 uses 语句中加入部件所在单元的名字 2. 在窗体中增加一个对象域来表示部件 这是自己增加部件和 Delphi 增加部件的方法的主要不同点。 你将对象域加在窗体类型声明底部的 public 部分。 Delphi 则会将对象域加在底部声明的上面。 你不能将域加在 Delphi 管理的窗体类型的声明的上部。在这一部分声明的对象域将相应在存储在 DFM 文件中。增加不在窗体中存在的部件名将产生 DFM 文件无效的错误。 3. 附上窗体的 OnCreate 事件处理过程 4. 在窗体的 OnCreate 处理过程中构造该部件 当调用部件的构造过程时,必须传递 Owner 参数(由 Owner 负责析构该部件)一般说来总是将 Self 作为 Owner 的传入参数。在 OnCreate 中, Self 是指窗体。 5. 给 Component 的 Parent 属性赋值 设置 Parent 属性往往是构造部件后要做的第一件事时。 Parent 在形式上包含部件,一般来说 Parent 是窗体或者 GoupBox 、 Panel 。通常给 Parent 赋与 Self ,即窗体。在设置部件的其它属性之前最好先给 Parent 赋值。 6. 按需要给部件的其它属性赋值 假设你想测试名为 TNewComponent 类型的新部件,库单元名为 NewTest 。窗体库单元应该是这样的;
unit Unitl;
interface
uses SysUtils, Windows, Messages, Classes, Grophics, Controls, Forms, Dialogs, Newtest; type Tforml = class(TForm) procedure FormCreate(Sender: TObject); private { private 申 明 } public { public 申 明 } NewComponent: TNewComponent; end;
var Forml: TForml;
implementation
{$R *.DFM }
procedure TForml.FormCreate ( Sender: TObject ) ; begin NewComponent := TNewComponent.Create ( Self ); NewCompanent.Parent := Self; NewCompanent.Left := 12; end;
end.
19.2.1.5 编写部件的面向对象技术
部件使用者在 Delphi 环境中开发,将遇到在包含数据和方法的对象。他们将在设计阶段和运行阶段操作对象,而编写部件将比他们需要更多的关于对象的知识,因此,你应当熟悉 Delphi 的面向对象的程序设计。 1. 建立部件 部件用户和部件编写者最基本的区别是用户处理对象的实例,而编写者创建新的对象类型。这个概念是面向对象程序设计的基础。例如,用户创建了一个包含两个按钮的窗体,一个标为 OK ,另一个标为 Cancel ,每个都是 TButton 的实例,通过给 Text 、 default 和 Cancel 等属性赋不同的值,给 OnClick 事件赋予不同的处理过程,用户产生了两个不同的实例。 建立新部件一般有两个理由 ● 改变类型的缺省情况,避免反复 ● 为部件增加新的功能
目的都是为了建立可重用对象。如果从将来重用的角度预先计划和设计,能节省一大堆将来的工作。 在程序设计中,避免不必要的重复是很重要的。如果发现在代码中一遍又一遍重写相同的行,就应当考虑将代码放在子过程或函数中,或干脆建立一个函数库。 设计部件也是这个道理,如果总是改变相同的属性或相同的方法调用,那应创建新部件。 创建新部件的另一个原因是想给已有的部件增加新的功能。你可以从已有部件直接继承(如 ListBox )或从抽象对象类型继承(如 TComponent , TControl )。你虽然能为部件增加新功能,但不能将原有部件的属性移走,如果要这样做的话,就从该父对象的祖先对象继承。 2. 控制部件的访向 Object Pascal 语言为对象的各部分提供了四个级别的访问控制。访问控制让你定义什么代码能访问对象的哪一部分。通过描述访问级别,定义了部件的接口。如果合理安排接口,将提高部件的可用性和重用性。 除非特地描述,否则加在对象里的域、方法和属性的控制级别是 published ,这意味着任何代码可以访问整个对象。 下表列出各保护级别:
表 19.2 对象定义中的保护级别 ━━━━━━━━━━━━━━━━━━━ 保护级 用处 ─────────────────── private 隐藏实现细节 protected 定义开发者接口 public 定义运行时接口 published 定义设计时接口 ━━━━━━━━━━━━━━━━━━━
所有的保护级都在单元级起作用。如果对象的某一部分在库单元中的一处可访向,则在该库单元任意处都可访向。 ⑴ 隐藏实现细节 如果对象的某部分被声明为 private ,将使其它库单元的代码无法访问该部分,但包含声明的库单元中的代码可以访问,就好象访问 public 一样,这是和 C++ 不同的。 对象类型的 private 部分对于隐藏详细实现是很重要的。既然对象的用户不能访问, private 部分,你就能改变对象的实现而不影响用户代码。 下面是一个演示防止用户访问 private 域的例子:
unit HideInfo;
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs;
type TSecretForm = class(TForm) { 声明新的窗体窗口 } procedure FormCreate(Sender: TObject); private { declare private part } FSecretCode: Integer; { 声明 private 域 } end;
var SecretForm: TSecretForm;
implementation
procedure TSecretForm.FormCreate(Sender: TObject); begin FSecretCode := 42; end;
end.
unit TestHide; { 这是主窗体库单元 }
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, HideInfo; { 使用带 TSecretForm 声明的库单元 } type TTestForm = class(TForm) procedure FormCreate(Sender: TObject); end;
var TestForm: TTestForm;
implementation
procedure TTestForm.FormCreate(Sender: TObject); begin SecretForm.FSecretCode := 13; { 编译过程将以 "Field identifier expected" 错误停止} end;
end.
⑵ 定义开发者接口 将对象某部分声明为 protected ,可使在包含该部件声明的库单元之外的代码无法访问,就象 private 部分。 protected 部分的不同之处是,某对象继承该对象,则包含新对象的库单元可以访问 protected 部分,你能使用 protected 声明定义开发者的接口。也就是说。对象的用户不能访向 protected 部分,但开发者通过继承就可能做到,这意味着你能通过 protected 部分的可访问性使部件编写者改变对象工作方式,而又不使用户见到这些细节。 ⑶ 定义运行时接口 将对象的某一部分定义为 public 可使任何代码访问该部分。如果你没有对域方法或属性加以 private 、 protected 、 public 的访问控制描述。那么该部分就是 published 。 因为对象的 public 部分可在运行时为任何代码访问,因此对象的 public 部分被称为运行接口。运行时接口对那些在设计时没有意义的项目,如依靠运行时信息的和只读的属性,是很有用的。那些设计用来供用户调用的方法也应放在运行时接口中。 下例是一个显示两个定义在运行时接口的只读属性的例子:
type TSampleComponent = class(TComponent) private FTempCelsius: Integer; { 具体实现是 private } function GetTempFahrenheit: Integer; public property TempCelsius: Integer read FTempCelsius; { 属性是 public } property TempFahrenheit: Integer read GetTempFahrenheit; end;
function GetTempFahrenheit: Integer; begin Result := FTempCelsius * 9 div 5 + 32; end;
既然用户在设计时不能改变 public 部分的属性的值,那么该类属性就不能出现在 Object Inspector 窗口中。 ⑷ 定义设计时接口 将对象的某部分声明为 published ,该部分也即为 public 且产生运行时类型信息。但只有 published 部分定义的属性可显示在 Object Inspector 窗口中。对象的 published 部分定义了对象的设计时接口。设计时接口包含了用户想在设计时定制的一切特征。 下面是一个 published 属性的例子,因为它是 published ,因此可以出现在 Object Inspector 窗口:
TSampleComponent = class(TComponent) private FTemperature: Integer; { 具体实现是 private } published property Temperature: Integer read FTemperature write FTemperature; { 可写的 } end;
3. 派送方法 派送 (Dispatch) 这个概念是用来描述当调用方法时,你的应用程序怎样决定执行什么样的代码,当你编写调用对象的代码时,看上去与任何其它过程或函数调用没什么不同,但对象有三种不同的派送方法的方式。 这三种派送方法的类型是: ● 静态的 ● 虚拟的 ● 动态的
虚方法和动态方法的工作方式相同,但实现不同。两者都与静态方法相当不同。理解各种不同的派送方法对创建部件是很有用的。 ⑴ 静态方法: 如果没有特殊声明,所有的对象方法都是静态的 . 。静态方法的工作方式正如一般的过程和函数调用。在编译时,编译器决定方法地址,并与方法联接。 静态方法的基本好处是派送相当快。因为由编译器决定方法的临时地址,并直接与方法相联。虚方法和动态方法则相反,用间接的方法在运行时查找方法的地址,这将花较长的时间。 静态方法的另一个不同之处是当被另一类型继承时不做任何改变,这就是说如果你声明了一个包含静态方法的对象,然后从该对象继承新的对象,则该后代对象享有与祖先对象相同的方法地址,因此,不管实际对象是谁,静态方法都完成相同的工作。 你不能覆盖静态方法,在后代对象中声明相同名称的静态方法都将取代祖先对象方法。 在下列代码中,第一个部件声明了两静态方法,第二个部件,声明了相同名字的方法取代第一个部件的方法。
type TFirstComponent = class(TComponent) procedure Move; procedure Flash; end;
TSecondComponent = class(TFirstComponent) procedure Move; { 尽管有相同的声明,但与继承的方法不同 } function Flash(HowOften: Integer): Integer; { 同 Move 方法一样 } end;
⑵ 虚方法 调用虚方法与调用任何其它方法一样,但派送机制有所不同。虚方法支持在后代对象中重定义方法,但调用方法完全相同,虚方法的地址不是在编译时决定,而是在运行时才查找方法的地址。 lang="ZH-CN" 为声明一个新的方法,在方法声明后增加 virtual 指令。方法声明中的 virtual 指令在对象虚拟方法表( VMT )中创建一个入口,该虚拟方法表保存对象类所有虚有拟方法的地址。 当你从已有对象获得新的对象,新对象得到自己的 VMT ,它包含所有的祖先对象的 VMT 入口,再增加在新对象中声明的虚拟方法。后代对象能覆盖任何继承的虚拟方法。 覆盖一个方法是扩展它,而不是取代它。后代对象可以重定义和重实现在祖先对象中声明的任何方法。但无法覆盖一个静态方法。覆盖一个方法,要在方法声明的结尾增加 override 指令,在下列情况,使用 override 将产生编译错误: ● 祖先对象中不存在该方法 ● 祖先对象中相同方法是静态的 ● 声明与祖先对象的(如名字、参数)不匹配
下列代码演示两个简单的部件。第一个部件声明了三个方法,每一个使用不同的派送方式,第二个部件继承第一个部件,取代了静态方法,覆盖了虚拟方法和动态方法。
type TFirstComponent = class(TCustomControl) procedure Move; { 静态方法 } procedure Flash; virtual; { 虚 方 法 } procedure Beep; dynamic; { 动态虚拟方法 } end;
TSecondComponent = class(TFirstComponent) procedure Move; { 声明了新的方法 } procedure Flash; override; { 覆盖继承的方法 } procedure Beep; override; { 覆盖继承的方法 } end;
⑶ 动态方法 动态方法是稍微不同于虚拟方法的派送机制。因为动态方法没有对象 VMT 的入口,它们减少了对象消耗的内存数量。派送动态方法比派送一般的虚拟方法慢。因此,如果方法调用很频繁,你最好将其定义为虚方法。 定义动态方法时,在方法声明后面增加 dynamic 指令。 与对象虚拟方法创建入口不同的是 dynamic 给方法赋了一数字,并存储相应代码的地址,动态方法列表只包含新加的和覆盖的方法入口,继承的动态方法的派送是通过查找每一个祖先的动态方法列表(按与继承“反转的顺序”),因此动态方法用于处理消息(包括 Windows 消息)。实际上,消息处理过程的派送方式与动态方法相同,只是定义方法不同 ⑷ 对象与指针 在 Object Pascal 中,对象实际上是指针。编译器自动地为程序创建对象指针,因此在大多数情况下,你不需要考虑对象是指针。但当你将对象作为参数传递时,这就很重要了。通常,传递对象是按值而非按引用,也就是说,将对象声明为过程的参数时,你不能用 var 参数,理由是对象已经是指针引用了。 |