DELPHI基础教程 第十九章 Delphi自定义部件开发

第十九章 Delphi自定义部件开发

   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环境中建立部件和在应用程序中使用部件有三个重要差别:

  ● 编写部件的过程是非可视化的

  ● 编写部件需要更深入的关于对象的知识

  ● 编写部件需要遵循更多的规则

 

  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控制的子类

  ● 建立非可视部件

 

  下表列出了不同建立途径的起始类

 

表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;

 

  ⑵ 虚方法

  调用虚方法与调用任何其它方法一样,但派送机制有所不同。虚方法支持在后代对象中重定义方法,但调用方法完全相同,虚方法的地址不是在编译时决定,而是在运行时才查找方法的地址。

  为声明一个新的方法,在方法声明后增加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参数,理由是对象已经是指针引用了。

19.2.2 Delphi部件编程 

19.2.2.1 创建属性 

 属性(Property)是部件中最特殊的部分,主要因为部件用户在设计时可以看见和操作它们,并且在交互过程中能立即得到返回结果。属性也很重要,因为如果将它们设计好后,将使用户更容易地使用,自己维护起来也很容易。

  为了使你在部件中更好地使用属性,本部分将介绍下列内容:

 ● 为什么要创建属性

  ● 属性的种类

 ● 公布(publishing)继承的属性

  ● 定义部件属性

  ● 编写属性编辑器

 

  1. 为什么要创建属性

  属性提供非常重要的好处,最明显的好处是属性在设计时能出现在Object Inspector窗口中,这将简化编程工作,因为你只需读用户所赋的值,而不要处理构造对象的参数。

  从部件使用者的观点看,属性象变量。用户可以给属性赋值或读值,就好象属性是对象的域。

  从部件编写者的观点看属性比对象的域有更强的功能;

  ⑴ 用户可以在设计时设置属性

  这是非常重要的,因为不象方法,只能在运行时访问。属性使用户在运行程序之前就能定制部件,通常你的部件不应包含很多的方法,它们的功能可以通过属性来实现。

  ⑵ 属性能隐藏详细的实现细节

  ⑶ 属性能引起简单地赋值之外的响应,如触发事件

  ⑷ 用于属性的实现方法可以是虚拟方法,这样看似简单的属性在不同的部件中,将实现不同的功能。

 

 2. 属性的类型

  属性可以是函数能返回的任何类型,因为属性的实现可以使用函数。所有的Pascal类型,兼容性规则都适用属性。为属性选择类型的最重要的方面是不同的类型出现在Object Inspector窗口中的方式不同。Object Inspector将按不同的类型决定其出现的方式。

你也能在注册部件时描述不同的属性编辑器。

  下表列出属性出现在Object Inspector窗口中的方式

 

表19.3 属性出现在Object Inspector窗口中的方式

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

属性类型       处 理 方 式

───────────────────────────────────────

简单类型   Numeric、Character和 String属性出现在Object Inspector中,用户可

以直接编辑

枚举类型 枚举类型的属性显示值的方式定义在代码中。选择时将出现下拉  

式列表框,显示所有的可能取值。

集合类型 集合类型出现在Object Inspector窗口中时正如一个集合,展开后,用         

户通过将集合元素设为True或False来选择。

对象类型 作为对象的属性本身有属性编辑器,如果对象有自己的published属

性,用户在Object Inspector中通过展开对象属性列,可以独立编辑它们,

对象类型的属性必须从TPersistent继承。

数组类型 数组属性必须有它们自己的属性编辑器,Object Inspector没有内嵌对数

组属性编辑的支持。  

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

3. 公布继承的属性

  所有部件都从祖先类型继承属性。当你从已有部件继承时,新部件将继承祖先类型的所有属性。如果你继承的是抽象类,则继承的属性是protected或public,但不是published。如想使用户访问protected或public属性,可以将该属性重定义为published。如果你使用TWinControl继承,它继承了Ctl3D属性,但是protected的,因此用户在设计和运行时不能访问Ctl3D,通过在新部件中将Ctl3D重声明为published,就改变了Ctl3D的访问级别。下面的代码演示如何将Ctl3D声明为published,使之在设计时可被访问。

 

  type

TSampleComponent=class(TWinControl)

published

property Ctl3D;

end;

 

4. 定义部件属性

  ⑴ 属性的声明

  声明部件的属性,你要描述:

 ● 属性名

  ● 属性的类型

  ● 读和设置属性值的方法

 

  至少,部件属性应当定义在部件对象声明的public部分,这样可以在运行时很方便地从外部访问;为了能在设计时编辑属性,应当将属性在published部分声明,这样属性能自动显示在Object Inspector窗口中。下面是典型的属性声明:

 

  type

TYourComponent=class(TComponent)

private

FCount: Integer { 内部存储域 }

function GetCount: Integer; { 读方法 }

procedure SetCount(ACount: Integer); { 写方法 }

pubilic

property Count: Integer read GetCount write SetCount;

end;

 

  ⑵ 内部数据存储

  关于如何存储属性的数据值,Delphi没有特别的规定,通常Delphi部件遵循下列规定:

 ● 属性数据存储在对象的数据域处

  ● 属性对象域的标识符以F开头,例如定义在TControl中的属性FWidth

  ● 属性数据的对象域应声明在private部分

 

  后代部件只应使用继承的属性自身,而不能直接访问内部的数据存储。

  ⑶ 直接访问

  使属性数据可用的最简单的办法是直接访问。属性声明的read 和write部分描述了怎样不通过调用访问方法来给内部数据域赋值。但一般都用read进行直接访问,而用write进行方法访问,以改变部件的状态。

  下面的部件声明演示了怎样在属性定义的read 和write部分都采用直接访问:

 

  type

TYourComponent=class(TComponent)

private { 内部存储是私有 }

FReadOnly: Boolean; { 声明保存属性值的域 }

published { 使属性在设计时可用 }

property ReadOnly: Boolean read FReadOnly write FReadOnly;

end;

 

  ⑷ 访问方法

  属性的声明语法允许属性声明的read和write部分用访问方法取代对象私有数据域。不管属性是如何实现它的read 和write部分,方法实现应当是private,后代部件只能使用继承的属性访问。

  ① 读方法

属性的读方法是不带参数的函数,并且返回同属性相同类型的值。通常读函数的名字是“Get”后加属性名,例如,属性Count的读方法是GetCount。不带参数的唯一例外是数组属性。如果你不定义read方法,则属性是只写的。

  ② 写方法

  属性的写方法总是只带一个参数的过程。参数可以是引用或值。通常过程名是"Set"加属性名。例如,属性Count的写方法名是SetCount。参数的值采用设置属性的新值,因此,写方法需要执行在内部存储数据中写的操作。

  如果没有声明写方法,那么属性是只读的。

  通常在设置新值前要检测新值是否与当前值不同。

  下面是一个简单的整数属性Count的写方法:

 

  procedure TMyComponent.SetCount( value: Integer);

begin

if value <>FCount then

begin

FCount := Value;

update;

end;

end;

 

⑸ 缺省属性值

  当声明一个属性,能有选择地声明属性的缺省值。部件属性的缺省值是部件构造方法中的属性值集。例如,当从Component Palette选择某部件置于窗体中时,Delphi通过调用部件构造方法创建部件,并决定部件属性初始值。

  Delphi使用声明缺省值决定是否将属性值存在DFM文件中。如果不描述缺省值,Delphi将总是保存该属性值。声明缺省值的方法是在属性声明后加default指令,再跟缺省值。

  当重声明一个属性时,能够描述没有缺省值的属性。如果继承的属性已有一个,则设立没有缺省值的属性的方法是在属性声明后加nodefault指令。如果是第一次声明属性,则没有必要加nodefault指令,因为没有default指令即表示如此。

  下例是名为IsTrue的布尔类型属性设置缺省值True的过程:

 

  type

TSampleComponent=class(TComponent)

private

FIsaTrue: Boolean;

pubilic

constructor Create (AOwner: TComponent); Overvide;

published

property Istrue: Boolean read FIsTrue write FIsTrue default True;

end;

 

constructor TSampleComponent.Create (AOwner: TComponent);

begin

inherited Create ( Aowner);

Fistvue := True; { 设置缺省值 }

end;

 

5. 编写属性编辑器

Object Inspector提供所有类型属性的缺省编辑器,Delphi也支持通过编写和注册属性编辑器的方法为属性设计自己的编辑器。可以注册专门为自定义部件的属性设计的编辑器,也可设计用于所有某类型的属性。编写属性编辑器需要下列五个步骤:

● 继承一个属性编辑器对象

 ● 将属性作为文本编辑

  ● 将属性作为整体编辑

  ● 描述编辑器属性

 ● 注册属性编辑器

 

  ⑴ 继承属性编辑器对象

  DsgnIntf库单元中定义了几种属性编辑器。它们都是从TPropertyEditor继承而来。当创建属性编辑器时,可以直接从TPropertyEditor中继承或从表中的任一属性编辑器中继承。

 

 

表19.4 属性编辑器的类型

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  类型 编辑的属性

─────────────────────────────────────

TOrdinalProperty    所有有序的属性(整数、字符、枚举)

TIntegerProperty    所有整型,包括子界类型

TCharProperty     字符类型或字符子集

TEnumProperty   任何枚举类型

TFloatProperty   所有浮点数

TStringProperty   字符串,包括定长的字符串

TSetElementProperty 集合中的独立元素

TSetElementProperty 所有的集合,并不是直接编辑集合类型,而是展开成一列

集合元素属性

TClassProperty 对象,显示对象名,并允许对象属性的展开

TMethodPropevty 方法指针,主要指事件

TComponentProperty 相同窗体中的部件,用户不能编辑部件的属性,

但能指向兼容的部件

TColorProperty    部件颜色,显示颜色常量,否则显示十六进制数

TFontNameProperty 字体名称

TFontProperty 字体,允许展开字体的属性或弹出字体对话框

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

 

 

下面是TFloatPropertyEditor的定义:

 

type

TFloatProperty=Class(TPropertyEditor)

public

function AllEqual: Boolean; override;

function GetValue: String; override;

procedure SetValue ( Const Value: string ); override;

end;

 

⑵ 象文本一样编辑属性

  所有的属性都需要将它们的值在Object Inspector窗口中以文本的方式显示。属性编辑器对象提供了文本表现和实际值之间转换的虚方法。这些虚方法是GetValue和SetValue,你的属性编辑器也能继承了一系列的方法用于读和写不同类型的值。见下表:

 

表19.5 读写属性值的方法

━━━━━━━━━━━━━━━━━━━━━━━━━━

属性类型   "Get"方法 "Set"方法

──────────────────────────

浮点数 GetFloatValue SetFloatVallue

方法指针 GetMethodValue SetMehodValue

有序类型 GetOrdValue SetOrdValue

字符串 GetStrValue SetStrValue

━━━━━━━━━━━━━━━━━━━━━━━━━━

 

当覆盖GetValue方法时,调用一个"Get"方法;当覆盖SetValue方法时调用一个"Set"方法。

  属性编辑器的GetValue方法返回一个字符串以表现当前属性值。缺省情况下GetValue返回"unknown"。

属性编辑器的SetValue接收Object Inspector窗口String类型的参数,并将其转换成合适的类型,并设置属性值。

  下面是TIntegerProperty的GetValue和SetValue的例子:

 

function TIntegerProperty GetValue: string;

begin

Result := IntToStr (GetOrdValue);

end;

 

proceduve TIntegerPropertySetValue (Const Value: string);

var

L: Longint;

begin

L := StrToInt(Value); { 将字符串转换为数学 }

with GetTypeData (GetPropType)^ do

if ( L < Minvalue ) or ( L > MaxValue ) then

Raise EPropertyError.Create (FmtloadStr(SOutOfRange,

[MinValue,MaxValue]));

SetOrdValue (L);

end;

 

⑶ 将属性作为一个整体来编辑

  Delphi支持提供用户以对话框的方式可视化地编辑属性。这种情况常用于对对象类型属性的编辑。一个典型的例子是Font属性,用户可以找开Font对话框来选择字体的属性。

  提供整体属性编辑对话框,要覆盖属性编辑对象的Edit方法。Edit方法也使用"Get"和"Set"方法。

 在大多数部件中使用的Color属性将标准的Windows颜色对话框作为属性编辑器。下面是TColorProperty的Edit方法

 

procedure TColorProperty.Edit

var

ColorDialog: TColorDialog;

begin

ColorDialog := TColorDialog.Create(Application); { 创建编辑器 }

try

ColorDialog.Color := GetOrdValue; { 使用已有的值 }

if ColorDialog.Execute then

  SetOrdValue (ColorDialog.Color);

finally

ColorDialog.Free;

end;

 end;

 

⑷ 描述编辑器的属性

  属性编辑必须告诉Object Inspector窗口如何采用合适的显示工具。例如Object Inspector窗口需要知道属性是否有子属性,或者是否能显示可能取值的列表。描述编辑器的属性通常覆盖属性编辑器的GetAttributes方法。

GetAttributes返回TPropertyAttributes类型的集合。集合中包括表中任何或所有的值:

 

表19.6 属性编辑器特征标志

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  标志 含 义 相关方法

   ──────────────────────────────

paValuelist 编辑器能给予一组枚举值 GetValues

paSubPropertie 属性有子属性 GetPropertises

paDialog 编辑器能显示编辑对话框 Edit

PaMultiSelect 当用户选择多于一个部件

时,属性应能显示 N/A

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

Color属性是灵活的,它允许在Object Inspector窗口中以多种方式选择他们。或者键入,或者从列表中选择定编辑器。因此TColorProperty的GetAttributes方法在返回值中包含多种属性。

 

  function TColorProperty.GetAttributes: TProrertyAttributes;

begin

Result := [PaMultiselect, paDialog, paValuelist];

end;

 

 ⑸ 注册属性编辑器

  一旦创建属性编辑器,必须在Delphi中注册。注册属性编辑器时,要与某种属性相联。

  调用RegisterPropertyEditor过程来注册属性编辑器。该过程接受四个参数:

  ● 要编辑的属性的类型信息的指针。这总是通过调用调用TypeInfo函数得到,如TypeInfo ( TMyComponent )

  ● 编辑器应用的部件类型,如果该参数为nil则编辑器应用于所给的类型的所有属性

  ● 属性名,该参数只有在前一参数描述了部件的情况下才可用

  ● 使用该属性编辑器的属性的类型

 

  下面引用了注册标准部件的过程:

 

procedure Register;

begin

RegisterPropertyEditor (TypeInfo(TComponent), nil, TComponentProperty,

RegisterPropertyEditor(TypeInfo(TComponentName), TComponent,

\'Name\', (ComponentNamePropety);

RegisterPropertyEditor (TypeInfo(TMenuItem), TMenu, \'\', TMenuItemProperty);

end;

 

  这三句表达式使用RegisterPropertyEditor三种不同的用法:

  ● 第一种最典型

它注册了用于所有TComponent类型属性的属性编辑器TComponentProperty。通常,当为某种类型属性注册属性编辑器时,它就能应用于所有这种类型的属性,因此,第二和第三个参数为nil。

  ● 第二个表达式注册特定类型的属性编辑器

它为特定部件的特定属性注册属性编辑器,在这种情况下,编辑器用于所有部件的Name属性。

 ● 第三个表达式介于第一个和第二个表达式之间

它为部件TMenu的TMenuItem类型的所有属性注册了属性编辑器。

 

19.2.2.2 创建事件

 

  事件是部件的很重要的部分。事件是部件必须响应的系统事件与响应事件的一段代码的联接。响应代码被称为事件处理过程,它总是由部件用户来编写。通过使用事件,应用开发者不需要改变部件本身就能定制部件的行为。作为部件编写者,运用事件能使应用发者定制所有的标准Delphi部件。要创建事件,应当理解:

  ● 什么是事件

  ● 怎样实现标准事件

  ● 怎样定义自己的事件

 

1. 什么是事件

事件是联接发生的事情与某些代码的机制,或者说是方法指针,一个指向特定对象实例的特定方法的指针。从部件用户的角度,事件是与系统事件(如OnClick)有关的名称,用户能给该事件赋特定的方法供调用。例如,按钮Buttonl有OnClick方法,缺省情况下Delphi在包含该按钮的窗体中产生一个为ButtonlClick的方法,并将其赋给OnClick。当一个Click事件发生在按钮上时,按钮调用赋给OnClick的方法ButtonlClick:

部件用户将事件看作是由用户编写的代码,而事件发生时由系统调用的处理办法。

  从部件编写者角度事件有更多的含义。最重要的是提供了一个让用户编写代码响应特定事情的场所。

  要编写一个事件,应当理解:

 ● 事件和方法指针

  ● 事件是属性

  ● 事件处理过程类型

 ● 事件处理过程是可选的

 

  ⑴ 事件是方法指针

  Delphi使用方法指针实现事件。一个方法指针是指向特定对象实例的特定方法的特定指针。作为部件编写者,能将方法指针作为一种容器。你的代码一发现事情发生,就调用由用户定义的方法。  

  方法指针的工作方式就象其它的过程类型,但它们保持一个隐含的指向对象实例的指针。所有的控制都继承了一个名为Click的方法,以处理Click事件。Click方法调用用户的Click事件处理过程。

 

  procedure TControl.Click;

begin

if Assigned(OnClick ) then OnClick( Self );

end;

 

如果用户给Control的OnClick事件赋了处理过程(Handle),那鼠标点按Control时将导致方法被调用。

  ⑵ 事件是属性

  部件采用属性的形式实现事件。不象大多数其它属性,事件不使用方法来使实现read和write部分。事件属性使用了相同类型的私有对象域作为属性。按约定域名在属性名前加“F”。例如OnClick方法的指针,存在TNotifyEvent类型FOnClick域中。OnClick事件属性的声明如下:

 

type

TControl=class ( TComponent )

private

FOnClick: TNofiFyEvent; { 声明保存方法指针的域 }

protected

property OnClick: TNotifyEvent read FOnClick write FOnClick;

end;

 

 象其它类型的属性一样,你能在运行时设置和改变事件的值。将事件做成属性的主要好处是部件用户能在设计时使用Object Inspector设置事件处理过程。

  ⑶ 事件处理过程类型

  因为一个事件是指向事件处理过程的指针,因此事件属性必须是方法指针类型,被用作事件处理过程的代码,必须是相应的对象的方法。

  所有的事件方法都是过程。为了与所给类型的事件兼容,一个事件处理过程必须有相同数目和相同类型的相同顺序的参数。Delphi定义了所有标准事件处理过程的方法类型,当你创建自己的事件时,你能使用已有的事件类型,或创建新的。虽然不能用函数做事件处理过程,但可以用var参数得到返回信息。

  在事件处理过程中传递var参数的典型例子是TKeyPressEvent类型的KeyPressed事件。TKeyPressEvent定义中含有两个参数。一个指示哪个对象产生该事件。另一个指示那个键按下:

 

  type

TKeyPressEvent=procedure( Sender: TObject; var key: char) of Object;

 

通常key参数包含用户按下键的字符。在某些情况下,部件的用户可能想改变字符值。例如在编辑器中强制所有字符为大写,在这种情况下,用户能定义下列的事件处理过程:

 

 procedure TForml.EditlKeyPressed( Sender: TObject; var key: char);

begin

key := Upcase( key );

end;

 

也可使用var参数让用户覆盖缺省的处理。

  ⑷ 事件处理过程是可选的

  在为部件创建事件时要记住部件用户可能并不编写该事件的处理过程。这意味着你的部件不能因为部件用户没有编写处理代码而出错。这种事件处理过程的可选性有两个方面:

  ① 部件用户并非不得不处理事件

事件总是不断地发生在Windows应用程序中。例如,在部件上方移动鼠标就引起Windows发送大量的Mouse-Move消息给部件,部件将鼠标消息传给OnMouseMove事件。在大多数情况下,部件用户不需要关心MouseMove事件,这不会产生问题,因为部件不依赖鼠标事件的处理过程。同样,自定义部件也不能依赖用户的事件处理过程。

  ② 部件用户能在事件处理过程写任意的代码

  一般说来,对用户在事件处理过程中的代码没有限制。Delphi部件库的部件都支持这种方式以使所写代码产生错误的可能性最小。显然,不能防止用户代码出现逻辑错误。

  2. 怎样实现标准事件

  Delphi带的所有控制继承了大多数Windows事件,这些就是标准事件。尽管所有这些事件都嵌在标准控制中,但它们缺省是protected,这意味着用户无法访问它们,当创建控制时,则可选择这些事件使用户可用。将这些标准事件嵌入自定义控制需要考虑如下:

  ● 什么是标准事件

  ● 怎样使事件可见

  ● 怎样修改标准事件处理过程

 

  ⑴ 什么是标准事件

  有两种标准事件:用于所有控制和只用于标准Windows控制。

  最基本的事件都定义在对象TControl中。窗口控制、图形控制和自定义控制都继承了这些事件,下面列出用于所有控制的事件:

  OnClick OnDragDrop OnEndDrag OnMouseMove

 OnDblClick OnDragOver OnMouseDown OnMouseUp

 

  所有标准事件在TControl中都定义了相应的protected动态方法,只是没有加“On”例如OnClick事件调用名为Click的方法。

  标准控制(从TWinControl继承)具有下列事件:

 OnEnter OnKeyDown OnkeyPress OnKeyUp OnExit

 

正如TControl中的标准事件,窗口控制也有相应protected动态方法。

  ⑵ 怎样使事件可见

  标准事件的声明是protected,如果想使用户在运行时或设计时能访问它们,就需要将它们重声明为public和 published。重声明属性而不描述它的实现将继承相同的实现方法,只是改变了访问级别。例如,创建一个部件并使它的OnClick事件出现在运行时,你可增加下面的部件声明:

 

  type

TMyControl=class(TCustomControl)

published

property OnClick; { 使OnClick在objectinspector中可见 }

end;

 

⑶ 怎样修改标准事件处理过程

  如果想修改自定义部件响应某种事件的方法,可以重写代码并将其赋给事件。将联接每个标准事件的方法声明的protected是出于慎密的考虑。通过,覆盖实现方法,能修改内部事件处理过程,通过调用继承的方法,能保持标准事件处理过程。

  调用继承的方法的顺序是很重要的。一般首先调用继承的方法,允许用户的事件处理过程代码在你的定制代码前执行。然而也有在调用继承的方法之前执行自己的代码情况出现。

  下面是一个覆盖Click事件的例子:

 

procedure TMyControl.Click;

begin

inherited Click; { 执行标准处理,包括调用事件处理过程你自己的定制代码 }

end;

 

3. 定义自己的事件

  定义全新的事件的情况是很少见的。只有当部件的行为完全不同于任何其它事件才需要定义新事件。定义新事件一般包含三个步骤:

  ● 触发事件

   ● 定义处理过程类型

   ● 声明事件

  ● 调用事件

 

⑴ 触发事件

  定义自己的事件要遇到的第一个关键是:当使用标准事件时你不需要考虑由什么触发事件。对某些事件,问题是显然的。例如:一个MouseDown事件是在用户按下鼠标的左键时发生,Windows给应用发送WM_LBUTTONDOWN消息。接到消息后,一个部件调用它的MouseDown方法,它依次调用用户的OnMouseDown事件处理过程代码。但是有些事件却不是那么可以描述清楚的。例如:滚行杠有一个OnChange事件,可被各种情况触发,包括按键、鼠标点按或其它按制中的改变。当定义事件时,你必须使各种情况的发生调用正确的事件。

  这里有TControl处理WM_LBUTTONDOWN消息的方法,DoMouseDown是私有的实现方法,它提供了一般的处理左、右和中按钮的方法,并将Windows消息的参数转换为MouseDown方法的值。

 

type

TControl = class(TComponent)

private

FOnMouseDown: TMouseEvent;

procedure DoMouseDown(var Message: TWMMouse; Button: TMouseButton;

Shift: TShiftState);

procedure WMLButtonDown(var Message: TWMLButtonDown);

message M_LBUTTONDOWN;

protected

procedure MouseDown(Button: TMouseButton; Shift: TShiftState;

X, Y: Integer); dynamic;

end;

 

procedure TControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);

begin

if Assigned(FOnMouseDown) then

FOnMouseDown(Self, Button, Shift, X, Y); { 调用事件处理过程 }

end;

 

procedure TControl.DoMouseDown(var Message: TWMMouse; Button: TMouseButton;

Shift: ShiftState);

begin

with Message do

MouseDown(Button, KeysToShiftState(Keys) + Shift, XPos, YPos); { 调用动态方法 }

end;

 

procedure TControl.WMLButtonDown(var Message: TWMLButtonDown);

begin

inherited; { perform default handling }

if csCaptureMouse in ControlStyle then

MouseCapture := True;

if csClickEvents in ControlStyle then

Include(FControlState, csClicked);

DoMouseDown(Message, mbLeft, []); { 调用常规的mouse-down 方法 }

end;

 

  当两种事情-状态变化和用户交互—发生时,处理机制是相同的,但过程稍微不同。用户交互事件将总是由Windows消息触发。状态改变事件也与Windows消息有关,但它们也可由属性变化或其它代码产生。你拥有对自定义事件触发的完全控制。

⑵ 定义处理过程类型

  一旦你决定产生事件,就要定义事件如何被处理,这就是要决定事件处理过程的类型。在大多数情况下,定义的事件处理过程的类型是简单的通知类型(TNotifyEvent)和已定义的事件类型。

  通知事件只是告诉你特定的事件发生了,而没有描述什么时候和什么地方。通知事件使用时只带一个TObject类型的参数,该参数是Sender。然而所有通知事件的处理过程都知道是什么样的事件发生和发生在那个部件。例如:Click事件是通知类型。当编写Click事件的处理过程时,你知道的是Click事件发生和哪个部件被点按了。通知事件是单向过程。没有提供反馈机制。

  在某些情况下,只知道什么事件发生和发生在那个部件是不够的。如果按键事件发生,事件处理过程往往要知道用户按了哪个键。在这种情况下,需要事件处理过程包含有关事件的必要信息的参数。如果事件产生是为了响应消息,那么传递给事件的参数最好是直接来自消息参数。

  因为所有事件处理过程都是过程,所以从事件处理过程中返回信息的唯一方法是通过var参数。自定义部件可以用这些信息决定在用户事件处理过程执行后是否和怎样处理事件。

  例如,所有的击键事件(OnKeyDown、OnKeyUp和OnKeyPressed)通过名为key的var参数传递键值。为了使应用程序看见包含在事件中的不同的键,事件处理过程可以改变key变量值。

 

⑶ 声明事件

  一旦你决定了事件处理过程的类型,你就要准备声明事件的方法指针和属性。为了让用户易于理解事件的功能,应当给事件一个有意义的名字,而且还要与部件中相似的属性的名称保持一致。

  Delphi中所有标准事件的名称都以“On”开头。这只是出于方便,编译器并不强制它。Object Inspector是看属性类型来决定属性是否是事件,所有的方法指针属性都被看作事件,并出现在事件页中。

⑷ 调用事件

  一般说来,最好将调用集中在事件上。就是说在部件中创建一个虚方法来调用用户的事件处理过程和提供任何缺省处理。当调用事件时,应考虑以下两点:

  ● 必须允许空事件

● 用户能覆盖缺省处理

 

不能允许使空事件处理过程产生错误的情况出现。就是说,自定义部件的正常功能不能依赖来自用户事件处理过程的响应。实际上,空事件处理过程应当产生与无事件处理过程一样的结果。

  部件不应当要求用户以特殊方式使用它们。既然一个空事件处理过程应当与无事件处理过程一样动作,那么调用用户事件处理过程的代码应当象这样:

 

  if Assigned(OnClick) then OnClick(Self);

{ 执行缺省处理 }

 

而不应该有这样的代码:

 

if Assigned(OnClick) then

OnClick(Self)

else

…; { 执行缺省处理 }

 

  对于某些种类的事件,用户可能想取代缺省处理甚至删除所有的响应。为支持用户实现这种功能,你需要传递var参数给事件处理过程,并在事件处理过程返回时检测某个值。空事件处理过程与无事件处理过程有相同作用。因为空事件处理过程不会改变任何var参数值。所以缺省处理总是在调用空事件处理过程后发生。

  例如在处理Key-Press事件,用户可以通过将var参数key的值设置为空字符(#0)来压制部件的缺省处理,代码如下:

 

  if Assigned(OnkeyPress) then OnkeyPress(Self key);

if key <> #0 then { 执行缺省处理 } ;

 

实际的代码将与这稍有不同,因为它只处理窗口消息,但处理逻辑是相同的。在缺省情况下,部件先调用任何用户赋予的事件处理过程,然后执行标准处理。如果用户的事件处理过程将key设为空,则部件跳过缺省处理。

 

19.2.2.3 处理消息

 

  在传统Windows编程中,一个很关键的方面是处理Windows发送给应用程序的消息。Delphi已经帮你处理了大多数的普通消息,但是在创建部件的过程中有可能Delphi没有处理方法,得由自己处理消息,也可能创建了新的消息需要处理它们。

  学习掌握Delphi的消息处理,要掌握以下三个方面:

 ● 理解消息处理系统

● 修改(改变)消息处理方法

● 建立新的消息处理方法

 

1. 理解消息处理系统

  所有的Delphi对象内部具有处理消息的机制,如调用消息处理方法或消息处理过程。消息处理的基本思想是对象接收某种消息并派送它们,这是通过调用与接收的消息相应的方法来实现的,如果没有相应于消息的指定的方法,那就调用缺省处理。下面的图解表示消息派送系统:

Delphi部件库定义了将所有Windows消息(包括用户自定义消息)直接转换到对象方法调用的消息派送系统。一般没有必要改变这种消息派送系统,只要建立消息处理方法。

  ⑴Windows消息中有什么?

Windows消息是包含若干有用的域的数据记录。记录中最重要的是一个整型大小的值,该值标识消息。Windows定义了大量的消息。库单元Messages声明了所有消息的标识。消息中其它的有用信息包括两个域参数和结果域。两个参数分别是16位和32位的。Windows代码总是以wParam和lParam来引用它们。

  最初,Windows程序员不得不记住包含的每一个参数。现在,微软公司已经命名了这参数。这样理解伴随这些消息的信息就更简单了。例如,WM_KEYDOWN消息的参数被称为vkey和keydata,这就比wParam和lParam给出了更多的描述信息。

  Delphi为不同类型的消息定义了指定的记录类型。如鼠标消息在long参数中传递鼠标事件的x、y座标,一个在高字,一个在低字。使用鼠标消息记录,你不需要自己关心哪个字是哪个座标,因为引用这些参数时通过名子Xpos和Ypos取代了lParamLo和lParamHi。

  ⑵ 派送方法

  当应用程序创建窗口时,在Windows Kernel中注册了一个窗口过程。窗口过程是处理窗口消息的函数。传统上,窗口过程包括了Case表达式,表达式的每个入口是窗口要处理的每一条消息。当你每次创建窗口时,必须建立完整的窗口过程。

  Delphi在下列三方面简化了消息派送:

  ● 每个部件继承了完整的消息派送系统

● 派送系统具有缺省处理。用户只需定义想响应的消息的处理方法

● 可以修改消息处理的一部分,依靠继承的方法完成大多数处理

 

这种消息派送系统的最大优点是用户能在任何时候安全地发送任何消息给任何部件。如果部件没有为该消息定义处理方法,那缺省处理方法会解决这个问题,通常是忽略它。

  Delphi为应用程序每种类型的部件注册了名为MainWndProc的方法作为窗口过程。MainWndProc包含了异常处理块,它完成从Windows到名为WndProc的虚方法传送消息记录,并且通过调用应用程序对象的HandleException方法处理异常。

  MainWndProc是静态方法,没有包含任何消息的指定处理方法。定制过程发生在WndProc中,因为每个部件类型都能覆盖该方法以适合特定的需要。

  WndProc方法为每个影响它们处理的任何条件进行检查,以捕捉不要的消息。例如,当被拖动时,部件忽略键盘事件,因此,TWinControl的WndProc只在没有拖动时传送键盘事件。最后WndProc调用Dispatch方法,该方法是从TObject继承来的静态方法,决定什么方法来处理消息。

  Dispatch使用消息记录的Msg域来决定怎样派送特定消息。如果部件已经给该消息定义了处理方法,则Dispatch调用该方法,反之,Dispatch调用缺省处理方法。

  2. 改变消息处理方法

  在改变自定义部件的消息处理方法之前,先要弄清楚你真正想要做什么。Delphi将大多数的Windows消息转换成部件编写者和部件用户都能处理的事件。一般来说,你应当改变事件处理行为而不是改变消息处理行为。

为了改变消息处理行为,要覆盖消息处理方法。也能提供捕获消息防止部件处理该消息。

⑴ 覆盖处理方法

为了改变部件处理特定消息的方法,要覆盖那个消息的处理方法。如果部件不处理该消息,你就需要声明新的消息处理方法。

为了覆盖消息处理方法,要在部件中以相同的消息索引声明新的方法。不要使用override指令,你必须使用Message指令和相应的消息索引。

  例如,为了覆盖一个处理WM_PAINT消息的方法,你要重声明WMPaint方法:

 

type

TMyComponent=class(…)

procedure WMPaint(var Message: TWMPaint); message WM_PAINT;

end;

 

⑵ 使用消息参数

  在消息处理方法内部,自定义部件访问消息记录的所有参数。因为消息总是var参数,如果需要的话,事件处理过程可以改变参数的值。Result域是经常改变的参数。Result是Windows文档中所指的消息的返回值:由SendMessage返回。

  因为消息处理方法的消息参数的类型随着被处理的消息的变化而变化,所以应当参考Windows消息文档中的参数的名字和含义。如果出于某种原因要使用旧风格的消息参数(wParam、lParam),可以配合通用类型TMessage来决定Message。

  ⑶ 捕获消息

  在某种情况下,你可能希望自定义部件能忽略某种消息。就是说,阻止部件将该消息派送给它的处理方法。为了那样来捕获消息,可以覆盖虚方法WndProc。

  WndProc方法在将消息传给Dispatch方法前屏蔽该消息。它依次决定哪一个方法来处理消息。通过覆盖WndProc,部件得到了派送消息之前过滤它们的机会。

  通常,象下面这样覆盖WndProc:

 

procedure TMyControl.WndProc(var Message: TMessage);

begin

{ 决定是否继续处理过程 }

inherited WndProc (Message);

end;

 

下面的代码是TControl的WndProc的一部分。TControl定义整个范围内的鼠标消息,当用户拖动和放置控制时,它们将被滤过。

 

  procedure TControl WndProc(var Message:TMessage);

begin

if (Message.Msg >= WM_MOVSEFIRST) and

(Message.Msg <= WM_MOUSELAST) then

if Dragging then

DragMouseMsg(TWMMOUSE(Message)) { 处理拖动 }

else

…   { 正常处理其它 }

…   { 否则正常处理 }

end;

3. 创建新的消息处理方法

因为Delphi只为大多数普通Windows消息提供了处理方法,所以当你定义自己的消息时,就要创建新的消息处理方法。

 用户自定义消息的过程包括两个方面:

  ● 定义自己的消息

● 声明新的消息处理方法

 

⑴ 定义自己的消息

许多标准部件为了内部使用定义了消息。定义消息的最一般的动因是广播信息和状态改变的通知。

  定义消息过程分两步:

  ● 声明消息标识符

● 声明消息记录类型

 

① 声明消息标识

消息标识是整型大小的常量。Windows保存了小于1024的消息用于自己使用,因此当声明自己的消息时,你应当大于1024。

  常量WM_USER代表用于自定义消息的开始数字。当定义消息标准时,你应当基于WM_USER。

  某些标准Windows控制使用用户自定义范围的消息,包括ListBox、ComboBox、EditBox和Button。如果从上述部件中继承了一个部件,在定义新的消息时,应当检查一下Message单元是否有消息用于该控制。

  定义消息的方法如下:

 

  Const

WM_MYFIRSTMESSAGE=WM_USER+0;

WM_MYSECONDMESSAGE=WM_USER+1;

 

② 声明消息记录类型

  如果你想给予自定义消息的参数有含义的名字,就要为该消息声明消息记录类型。消息记录是传给消息处理方法的参数的类型。如果不使用消息参数或者想使用旧风格参数,可以使用缺省的消息记录。

  声明消息记录类型要遵循下列规则

● 以消息名命名消息记录类型,以T打头

● 将记录中第一个域命名为Msg,类型为TMsgPraram

● 将接着的两个字节定义为word 以响应word大小的参数

● 将接着的四个字节与long参数匹配

● 将最后的域命名为Result,类型为Longint

 

下面是TWMMouse的定义

 

type

TWMMouse=record

Msg: TMsgParam; { 第一个是消息ID }

Keys: Word; { wParam }

case Integer of { 定义lParam的两种方式 }

o: (

Xpos: Integer; { 或者以x,y座标 }

Ypos: Integer);

1: (

Pos : TPoint; { 或者作为单个点 }

Result: Longint; ) { 最后是Result域 }

end;

 

TWMMouse使用变长记录定义了相同参数的不同名字集。

  ⑵ 声明新的消息处理方法

  有两类环境需要你定义新的消息处理方法:

  ● 自定义新部件需要处理没有被标准部件处理的Windows消息

● 已定义了自定义部件使用的新消息

 

声明消息处理方法的办法如下:

● 在部件声明中的protected部分声明方法

● 将方法做成过程

● 以要处理的消息名命名方法 但不带下划线

● 传递一个命名为Message的var参数,类型为消息记录类型

● 编写用于该部件的特别处理代码

● 调用继承的消息方法

 

下面是用于用户自定义消息CM_CHANGECOLOR的消息处理代码:

 

type

TMyComponent=class(TControl)

protected

procedure CMChangeColor(var Message:TMessage);

message CM_CHANGECOLOR;

end:

 

procedure TMyComponent.CMChangeColor(var Message: TMessage);

begin

color := Message lParam;

inherited;

end;

 

19.2.2.4 注册部件

 

  编写部件及其属性、方法和事件只是部件创建过程的一部分。尽管部件具有这些特征就可用,但部件真正功能强大的是在设计时操作它们的能力。

  使部件在设计时可用需要经过如下几步:

  ● 用Delphi注册部件

● 增加选择板位图

● 提供有关属性和事件的帮助

● 存贮和读取属性

 

1. 用Delphi注册部件

为了让Delphi识别自定义部件,并将它们放置于Component Palette上,你必须注册每一个部件。

注册一个部件要在部件所在单元里加入Register方法,这包括两个方面的内容:

● 声明注册过程

● 实现注册过程

 

一旦安装了注册过程,就可以将部件安装在选择板上。

  注册过程要在部件所在单元中写一个过程,该过程必须以Register命名。Register必须出现在库单元的interface部分,这样Delphi就能定位它。在Register过程中,可以为每个部件调用过程RegisterComponents。

下面的代码演示了建立和注册部件的概略方法:

 

unit MyBtns;

 

interface

 

type

… { 声明自定义部件 }

procedure Register;

 

Implementation

 

procedure Register;

begin

… { 注册部件 }

end;

 

end.

 

在Register过程中,必须注册每一个要加入Component Palette的部件,如果库单元包含若干部件,就要将它们一次性注册。

  注册一个部件时,为部件调用RegisterComponents过程。RegisterComponents告诉Delphi两件有关所注册的部件的事::

● 要注册部件所在的Component Palette的页名

● 要安装的部件的名字

 

选择板的页名是个字符串。如果你所给名字的页不存在,Delphi就用该名字创建新的页。

下面的Register过程注册了一个名为TMyComponent的部件,并将其放在名为“Miscellaneous”的Component Palette页上。

 

procedure Register;

begin

RegisterComponents(\'Miscellaneous\', [TFirst, TSecond]);

end;

 

也可以在相同的页上,或者在不同的页上,一次注册多个部件:

 

procedure Register;

begin

RegisterComponents(\'Miscellaneous\', [TFirst, TSecond]);

RegisterComponents(\'Assorted\', [TThird]);

end;

 

2. 增加Component Palette上的位图

每个部件都需要一个位图来在Component Palette上代表它。如果安装时没有描述自己的位图,则Delphi会自动套用缺省位图。

  因为选择板位图只有在设计时需要,所以没有必要将它们编译进库单元。而是将它们提供在与库单名相同的Windows资源文件中,扩展名为.DCR。用Delphi的位图编辑器来生成资源文件,每个位图边长24个象素。

  为每个要安装的库单元提供一个选择板位图文件,在每个文件中为每个要注册的部件提供一个位图。位图图象名与部件名相同,将文件放在与库单元相同的目录中,这样在安装部件时Dephi就能发现位图。

  例如,如果你在ToolBox单元中创建一个名为TMyControl的部件,就需要建立名为TOOLBOX.DCR的资源文件,文件中包含名为TMyControl的位图。

  3. 提供有关属性和事件的帮助

当在窗体中选择一个部件或在Object Inspector中选择事件或属性时,能够按F1得到有关这一项的帮助。如果创建了相应的Help文件的话,自定义部件的用户能得到有关你的部件的相应的文档。

  因为Delph使用了特殊的Help引擎支持跨多个Help文件处理主题搜索,所以你能提供关于自定义部件的小的Help文件,用户不需要额外的步骤就能找到你的文档。你的Help成了Delphi Help系统的一部分。

  要给用户提供帮助,要理解下列两方面:

● Delphi怎样处理HELP请求

● 将HELP插入Delphi

 

⑴ Delphi怎样处理HELP请求

Delphi基于关键词查询HELP请求。就是说,当用户在窗体设计窗口的已选部件上按F1键时,Delpdi将部件的名字转换成一个关键词,然后调用Windows Help引擎查找那个关键词的帮助主题。关键词是Windows Help系统的标准部分。实际上 ,WinHelp使用Help中的关键词产生Search对话框中的列表。因为用于上下文敏感搜索中的关键词不是实际供用户读的,所以要输入关键词的替代词。

例如,一个查找名为TSomething的部件的详细信息的用户可能打开WinHelp的Search对话框并输入TSomething。但不会使用用于窗体设计窗口的上下文查找的替代形式class-TSomething。因此,这个特殊的关键词Class-TSomething对用户是不可见的,以免弄乱了搜索列表。

  ⑵ 将Help插入Delphi

Delphi提供了创建和插入Windows Help文件的工具,包括Windows Help编译器HC.EXE。为自定义部件建立Help文件的机制与建立任何Help文件没什么不同,但需要遵循一些约定以与库中其它Help兼容。

  保持兼容性的方法如下:

  ● 建立Help文件

● 增加特殊的注脚

● 建立关键词文件

● 插入Help索引

 

当你为自定义部件建立完Help,有下列几个文件:

● 编译过的Help(.HLP)文件

● Help关键词(.KWF)文件

● 一个或多个Help源文件(.RTF)

● Help工程文件(.HLJ)

 

编译过的Help文件和关键词文件应当与库单元在同一目录。

  ① 建立Help文件

你可以使用任何的工具创建Windows Help文件。Delphi的多文件搜索引擎,可以包含任何数目的Help文件的要素。在编译的Help文件之外,你应当拥有RTF源文件,这样才能生成关键词文件。

  为使自定义部件的Help同库中其它部件一起工作,要遵循下列约定:

  ● 每个部件有占一页的帮助

部件帮助页应当给出部件目的的简单描述,然后列出最终用户可用的属性、事件和方法的描述。应用开发者通过在窗体上选择部件并按F1访问这一页。

  部件帮助页应当有一个用于关键词搜索的“K”脚注,脚注中包含部件名。例如,TMemo的关键词脚注读作"TMemo Component"

● 部件增加和修改的每一个属性,事件和方法应当有一页帮助

  属性、事件或方法的帮助页应当指出该项用于哪个部件,显示声明语法和描述它的使用方法。

  属性、事件或方法的帮助页应当有一个用于关键词搜索的“K”脚注,该脚注中包含该项的名字和种类。例如,属性Top的关键词脚注为“Top property”。

  Help文件的每一页也需要用于多文件索引搜索的特殊脚注。

  ② 增加特殊脚注

Delphi需要特殊的搜索关键词以区别用于部件的帮助页和其它项目。你应当为每一项提供标准的关键词搜索项。但你也需要用于Delphi的特殊脚注。

  要为来自Object Inspector窗口或代码编辑器F1的搜索增加关键词,就得为Help文件帮助页增加"B"脚注。

  “B”脚注与用于标准WinHelp关键词搜索的“K”脚注很相象,但它们只用于Delphi搜索引擎。下表列出怎样为每种部件帮助页建立“B”脚注:

 

表19.7 部件帮助页搜索注脚

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

帮助页类型 "B"脚注内容 示 例

   ──────────────────────────────────

主部件页 \'class_\'+部件类型名 class_TMemd

一般属性或事件页 \'prop_\'+属性名 prop_WordWrap

\'event_\'+事件名 event_OnChange

部件特有的属性 \'prop_\'+部件类型名 prop_TMemoWordWrap

或事件页 +属性名

\'event_\'+部件类型名 event_TMemoOnChange

+事件名

   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 

  区别一般帮助页和部件特有的帮助页是很重要的。一般帮助页应用于所有部件上的特定属性和事件。例如Left属性是所有部件中的标识。因此,它用字符串Prop-Left进行搜索。而Borde-style依赖于所属的部件,因此,BorderStyle属性拥有自己的帮助页。例如,TEdit有BorderStyle属性的帮助页,搜索字符串为Prop_TEditBorderStyle。

  ③ 建立关键词文件

  建立和编译了Help文件,并且增加了脚注之后,还要生成独立的关键词文件,这样Delphi才能将它们插入主题搜索的索引。

  从Help资源文件RTF创建关键词文件的方法如下:

● 在DOS提示行下,进入包含RTF文件的目录

● 运行关键词文件产生程序——KWGEN.EXE,后跟Help工程文件,如KWGEN SPECIAL.HPJ。当KWGEN运行完毕后,就有了与Help工程文件相同的关键词文件,但以.KWF为扩展名

● 将关键词文件放在编译完的库单元和Help文件相同的目录

当你在Component Palette上安装部件时,希望关键词插入Delphi Help系统的搜索索引。

 

④ 插入Help索引

以自定义部件建立关键词文件后,要将关键词插入Delphi的Help索引。

  将关键词文件插入Detphi Help索引的方法如下:

● 将关键词文件放在与编译完的库单元和Heph文件相同的目录中

● 运行HELPINST程序

 

HELPINST运行完后,Delphi的Help索引文件(.HDX)包含自定义部件帮助页的关键词。

⑶ 存储和装入属性

Delphi将窗体及其拥有的部件存储在窗体文件(.DFM)中,DFM文件用二进制表示窗体的属性和它的部件。当Delphi用户将自定义部件加入窗体中时,自定义部件应当具有存储它们的属性的能力。同样,当被调入Delphi或应用程序时,部件必须能从DFM文件中恢复它们。

  在大多数时候,不需要做任何使部件读写DFM文件的事。存储和装入都是继承的祖先部件的行为的一部分。然而在某些情况下,你可能想改变部件存储和装入时初始化的方法。因此,应当理解下述的机制:

● 存储和装入机制

● 描述缺省值

● 决定存储什么

● 装入后的初始化

 

① 存储和装入机制

当应用开发者设计窗体时,Delphi将窗体的描述存储在DFM文件中。当用户运行程序时,它读取这些描述。

  窗体的描述包含了一系列的窗体属性和窗体中部件的相似描述。每一个部件,包括窗体本身,负责存储和装入自身的描述。

  在缺省情况下,当存储时,部件将所有public和published属性的不同于缺省值的值以声明的顺序写入。当装入时,部件首先构造自己,并将所有属性设为缺省值;然后,读存储的、非缺省的属性值。

  这种缺省机制,满足了大多数部件的需要,而又不需部件编写者的任何工作。然而自己定义存储和装入过程以适合自定义部件需要的方法也有几种。

  ② 描述缺省值。

  Delphi部件只存储那些属性值不同于缺省值的属性。如果你不描述,Delphi假设属性没有缺省值,这意味着部件总是存储属性。

  一个属性的值没被构造函数设置,则被假设为零值。为了描述一个缺省值,在属性声明后面加default指令和新的缺省值。

  你也能在重声明属性时描述缺省值。实际上,重声明属性的一个原因是指定不同的缺省值。只描述缺省值,那么在对象创建时并不会自动地给属性赋值,还需要在部件的Create方法中赋所需的值。

  下面的代码用Align属性演示了描述缺省值的过程.

 

type

TStatusBar=class(TPanel)

public

constructor Create(Aowner: TComponent); override; { 覆盖以设置新值 }

published

property Align default alBottom; { 重新声明缺省值 }

end;

 

constructor TStatusBar.Create(Aowner: TComponent);

begin

inherited Create(Aowner); { 执行继承的初始化过程 }

Align := alBottom; { 为Align赋新的缺省值 }

end;

 

③ 决定存储什么

用户也可以控制Delphi是否存储部件的每一个属性。缺省情况下,在对象的published部分声明的所有属性都被存储。然而,可以选择不存储所给的属性,或者设计一个函数在运行时决定是否存储属性。

  控制Delphi是否存储属性的方法是在属性声明后面加stored指令,后跟True或False,或者是布尔方法名。你可以给任何属性的声明或重声明加stored表达式。下面的代码显示了部件声明三种新属性。一个属性是总是要存储,一个是不存,第三个则决定于布尔方法的值:

 

type

TSampleCompiment = class(TComponent)

protected

function storeIt: Boolean;

public { 正常情况下在不存 }

property Important: Integer stored True; { 总是存储 }

published { 正常情况下保存 }

property UnImportant: Integer stored False; { 不存 }

property Sometimes: Integer stored StoreIt; { 存储依赖于函数值 }

end;

 

④ 载入后的初始化

在部件从存储的描述中读取所有的属性后,它调用名为Loaded的虚方法,这提供了按需要执行任何初始化的机会。调用Loaded是在窗体和它的控制显示之前,因此,不需要担心初始化会带来屏幕闪烁。

  在部件载入属性时初始化它,要覆盖Loaded方法。

  在Loaded方法中,要做的第一件事是调用继承的Loaded方法。这使得在你的部件执行初始化之前,任何继承的属性都已初始化。

  下面的代码来自于TDatabase部件。在装入后,TDatabase试图重建在它存储时已打开的连接,并描述在连接发生异常时如何处理。

 

  procedure TDatabase.Loaded

begin

inherited Loaded; { 总是先调用继承的方法 }

Modified; { 设置内部标志 }

try

if FStreamedConnected then Open; { 重建联接 }

except

if csDesigning in ComponentState then { 在设计时 }

Application.HandleException(self) { 让Delphi处理异常 }

else raise; { 否 则 }

end;

end;

 

 

19.3 Delphi部件编程实例

 

19.3.1 创建数据库相关的日历控制-TDBCalendar

 

  当处理数据库联接时,将控制和数据直接相联是很重要的。就是说,应用程序可以建立控制与数据库之间的链。Delphi包括了数据相关的标签、编辑框、列表框和栅格。用户可以使自己的控制与数据相关。

  数据相关有若干等级。最简单的是只读数据相关或数据浏览,以及反映数据库当前状态的能力。比较复杂的是数据相关的编辑,也即用户可以在控制上操作数据库中的数据。

  在本部分中将示例最简单的情况,即创建联接数据库的单个字段的只读控制。本例中将使用Component Palette的Samples页中的TCalendar部件。

创建数据相关的日历控制包括下列几步:

● 创建和注册部件

● 使控制只读

● 增加数据联接(Data Link)

● 响应数据改变

 

19.3.1. 1创建和注册部件

 

每个部件的创建都从相同的方式开始,在本例中将遵循下列过程:

● 将部件库单元命名为DBCal

● 从TCalendar继承一个新部件,名为TDBCalendar

● 在Component Palette的Samples页中注册TDBCalendar

 

下面就是创建的代码:

 

unit DBCal;

 

interface

 

uses SysUtils, WinTypes, WinProc, Messages, Classes, Graphics, Controls,

Forms, Grids, Calendar;

type

TDBCalendar=class(TCalendar)

end;

 

procedure Register;

 

implementation

 

procedure Register;

begin

RegisterComponents(Samples,[TDBabendar]);

end;

 

end.

 

19.3.1.2 使控制只读

 

因为这个数据日历以只读方式响应数据,所以用户不能在控制中改变数据并指望它们反映到数据库中。

使日历只读包含下列两步:

● 增加只读属性

● 允许所需的更新

 

1. 增加只读属性

给日历控制增加只读选项是直接过程。通过增加属性,可以提供在设计时使控制只读的方法,当属性值被设为True,将使控制中所有元素不可被选。

⑴ 增加属性声明和保存值的private域:

 

type

TDBCalendar=class(TClendar)

private

FReadOnly: Boolean;

public

constructor Create (Aowner: TComponent); override;

published

property ReadOnly: Boolean read FReadOnly write FReadOnly default True;

end;

 

constructor TDBCalendar.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

end;

 

⑵ 覆盖SelectCell方法,使得当控制是只读时,不允许选择:

 

function TDBCalendar.SelectCell(ACol, Arow: Longint): Boolean;

begin

if FReadOnly then

Result := False

else

Result := inherited SelectCell(Acol,ARow);

end;

 

还要在TDBcalendar的声明中声明SelectCell。

如果现在将Calendar加入窗体,会发现部件完全忽略鼠标和击键事件,而且当改变日期时,也不能改变选择的位置。下面将使控制响应更新。

2. 允许所需的更新

只读日历使用SelectCell方法实现各种改变,包括设置Row和Col的值。当日期改变时,UpdateCalendar方法设置Row和Col的值,但因为SelectCell不允许你改变,即使日期改变了,选择仍留在原处。

可以给日历增加一个Boolean标志,当标志为True时允许改变:

 

type

TDBCalendar=class(TCalendar)

private

Fupdating: Boolean;

protected

function SelectCell(Acol, Arow: Longint); Boolean; override;

public

procedure UpdateCalendar; override;

end;

 

function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;

begin

if (not FUpdating) and FReadOnly then

Result := False { 如果更新则允许选择 }

else

Result := inherited SelectCell(ACol, ARow); { 否则调用继承的方法 }

end;

 

procedure UpdateCalendar;

begin

FUpdating := True; { 将标志设为允许更新 }

try

inherited UpdateCalendar; { 象通常一样更新 }

finally

FUpdating := False; { 总是清除标志 }

end;

end;

 

  现在日历仍旧不允许用户修改,但当改变日期属性时能正确反映改变;目前已有了一个真正只读控制,下一步是增加数据浏览能力。

 

  3. 增加数据联接

  控制和数据库的联接是由一个名为DataLink的对象处理。Delphi提供了几种类型的Datalink。将控制与数据库单个域相联的DataLink对象是TFieldDatalink。Delphi也提供了与整个表相联的DataLink。

  一个数据相关控制拥有DataLink对象,就是说,控制负责创建和析构DataLink。

  要建立作为拥有对象的Datalink,要执行下列三步:

  ● 声明对象域

  ● 声明访问属性

  ● 初始化DataLink

 

  ⑴ 声明对象域

  每个部件要为其拥有对象声明一个对象域。因此,日历对象DataLink 声明TFieldDataLink类型的域。

  日历部件中DataLink的声明如下:

 

type

TDBCalendar = class(TSampleCalendar)

private

FDataLink: TFieldDataLink;

end;

 

  ⑵ 声明访问属性

  每一个数据相关控制有一个DataSource属性,该属性描述应用程序给控制提供数据的数据源。而且,访问单个域的数据库还需要一个DataField 属性描述数据源中的域。

  下面是DataSource和DataField的声明和它们的实现方法:

 

type

TDBCalendar = class(TSampleCalendar)

private { 属性的实现方法是 }

function GetDataField: string; { 返回数据库字段的名字 }

function GetDataSource: TDataSource; { 返回数据源(Data source)的引用 }

procedure SetDataField(const Value: string); { 给数据库字段名赋值 }

procedure SetDataSource(Value: TDataSource); { 给数据源赋值 }

published { 使属性在设计时可用 }

property DataField: string read GetDataField write SetDataField;

property DataSource: TDataSource read GetDataSource write SetDataSource;

end;

 

……

 

function TDBCalendar.GetDataField: string;

begin

Result := FDataLink.FieldName;

end;

 

function TDBCalendar.GetDataSource: TDataSource;

begin

Result := FDataLink.DataSource;

end;

 

procedure TDBCalendar.SetDataField(const Value: string);

begin

FDataLink.FieldName := Value;

end;

 

procedure TDBCalendar.SetDataSource(Value: TDataSource);

begin

FDataLink.DataSource := Value;

end;

 

  现在,就建立了日历和DataLink的链,此外还有一个更重要的步骤。你必须在日历构建时创建DataLink对象,在日历析构时,撤消DataLink对象。

  ⑶ 初始化DataLink

  在数据相关控制在其存在的期间要不停地访问DataLink对象,因此,必须在其构建函数中创建DataLink创建并且在析构时,撤消DataLink对象,因此要覆盖日历的Create和Destroy方法。

 

type

TDBCalendar=class(TCalendar)

public

constructor Create(Aowna: TComponent); override;

destructor Destroy; override;

end;

 

constructor TDBCalendar Create (Aowner: TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

FDataLink := TFieldDataLink.Create;

end;

 

destructor TDBCalendar Destroy;

begin

FDataLink.Free;

inherited Destroy;

end;

 

现在,部件已拥有完整的DataLink,但部件还不知从相联的域中读取什么数据。

 

19.3.1.4 响应数据变化

 

  一旦控制拥有了数据联接(DataLink)和描述数据源和数据域的属性。就需在数据记录改变时响应域中数据的变化。

  DataLink对象都有个名为OnDataChange的事件。当数据源指示数据发生变化时,DataLink对象调用任何OnDataChange所联接的事件处理过程。

  要在数据改变时更新数据,就需要给DataLink对象的OnDataChange事件增加事件处理过程。

  下面声明了DataChange方法,并将其赋给DataLink对象的OnDataChange事件:

 

type

TDBCalendar=class(TCalendar)

private

procedure Datachange(Sender: TObject);

end;

 

constructor TDBCalendar Create(AOwner:TComponent);

begin

inherited Create(AOwner);

FReadOnly := True;

FDataLink := TFieldDataLink.Create;

FDataLink.OnDataChange := DataChange;

end;

 

destructor TDBcalendar.Destroy;

begin

FDataLink.OnDataChange := nil;

FDataLink.Free;

inherited Destroy

end;

 

procedure TDBCalendar.DataChange(Sender: TObject);

begin

if FDataLink.Filed=nil then

CalendarDate := 0;

else

CalendarDate := FDataLink.Field.AsDate;

end;

19.3.2 创建图形部件 

图形控制是一类简单的部件。因为纯图形部件从不需要得到键盘焦点,所以它没有也不要窗口句柄。包含图形控制的应用程序用户仍然可以用鼠标操作控制,但没有键盘界面。

  在本例中提供的图形部件是TShape。Shape部件位于Component Palette的Additional页。本例中的Shape部件有所不同,因此称其为TSampleShape。

  创建图形部件需要下列三个步骤:

  ● 创建和注册部件

  ● 公布(publishing)继承的属性

● 增加图形功能

 

19.3.2.1 创建和注册部件

 

每个部件的创建都从相同的方式开始,在本例中如下:

● 建立名为Shapes的部件单元

● 从TGraphicControl 继承,将新部件称为TSampleShape

● 在Component Palette的Samples页上注册TSampleShape

 

unit Shapes

 

intertace

 

use SysUtils, WinTypes, WinProcs, Messages, Classes,

Graphics,Controls,Forms;

 

type

TSampleShape=class(TGraphicControl)

end;

 

implementation

 

procedure Register;

begin

RegisterComponents(\'Samples\',[TSampleShape]);

end;

 

end.

 

19.3.2.2 公布继承的属性

 

一旦决定了部件类型,就能决定在父类的protected部分声明哪些属性和事件能为用户可见。TGraphicControl已经公布了所有作为图形控制的属性,因此,只需公布响应鼠标和拖放事件的属性。

 

  type

TSampleShape=class(TGraphicControl)

published

property DragCursor;

property DragMode;

property OnDragDrop;

property OnDragOver;

property ONEndDrag;

property OnMouseDown;

property OnMouseMove;

property OnMouseup;

end;

 

这样,该Shape控制具有通过鼠标和拖放与用户交互的能力。

 

19.3.2.3 .增加图形能力

 

一旦你声明了图形部件并公布了继承的属性,就可以给部件增加图形功能。这时需要知道两点:

  ● 决定画什么

  ● 怎样画部件图形

 

  在Shape控制的例子中,需要增加一些能使用户在设计时改变形状的属性。

 

1. 决定画什么

  图形部件通常都具有改变外观的能力,图形控制的外观取决于其某些属性的结合,例如Gauge控制具有决定其形状、方向和是否图形化地显示其过程的能力。同样,Shape控制也应有决定显示各种形状的能力.

给予Shape控制这种能力,增加名为Shape的属性。这需要下列三步:

● 声明属性类型

● 声明属性

● 编写实现方法

 

  ⑴ 声明属性类型

  当声明一个用户自定义类型的属性时,必须首先声明属性类型。最普通地用于属性的自定义类型是枚举类型。

  对Shape控制来说,需要声明一个该控制能画形状的枚举,下面是枚举类型的声明:

 

type

TSampleShapeType=(sstRectangle, sstSquare, sstRoundRect,

sstRoundSquare, sstEllipse, sstCircle);

TSampleShape = class(TGraphicControl)

end;

 

这样,就可以用该类型来声明属性。

⑵ 声明属性

当声明一个属性时,通常需要声明私有域来保存属性值,然后描述读写属性值的方法。

对于Shape控制,将声明一个域保存当前形状,然后声明一个属性通过方法调用来读写域值。

 

type

TSampleShape=class(TGrahpicControl)

private

FShape: TSampleShapeType;

procedure SetShape(value: TSampleShapeType);

published

property Shape: TSampleShapeType read FShape write SetShape;

end;

 

现在,只剩下SetShape的实现部分了。

⑶ 编写实现方法

下面是SetShape的实现:

 

procedure TSampleShape.SetShape(value: TSampleShapeType);

begin

if FShape<>value then

begin

FShape := value;

Invalidate(True); { 强制新形状的重画 }

end;

end;

 

2. 覆盖constructor和destructor

为了改变缺省属性值和初始化部件拥有的对象,需要覆盖继承的constructor和destructor方法。

图形控制的缺省大小是相同的,因此需要改变Width和Height属性。

本例中Shape控制的大小的初始设置为边长65个象素点。

⑴ 在部件声明中增加覆盖constructor

 

type

TSampleShape=class(TGraphicControl)

public

constructor Create(Aowner: TComponent); override;

end;

 

⑵ 用新的缺省值重新声明属性Height和width

 

type

TSampleShape=class(TGrahicControl)

published

property Height default 65;

property Width default 65;

end;

 

⑶ 在库单元的实现部分编写新的constructor

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

width := 65;

Height := 65;

end;

 

3. 公布Pen和Brush

在缺省情况下,一个Canvas具有一个细的、黑笔和实心的白刷,为了使用户在使用Shape控制时能改变Canvas的这些性质,必须能在设计时提供这些对象;然后在画时使用这些对象,这样附属的Pen或Brush被称为Owned对象。

管理Owned对象需要下列三步:

● 声明对象域

● 声明访问属性

● 初始化Owned对象

 

⑴ 声明Owned对象域

拥有的每一个对象必须有对象域的声明,该域在部件存在时总指向Owned对象。通常,部件在constructor中创建它,在destructor中撤消它。

Owned对象的域总是定义为私有的,如果要使用户或其它部件访问该域,通常要提供访问属性。

下面的代码声明了Pen和Brush的对象域:

 

type

TSampleShape=class(TGraphicControl)

private

FPen: TPen;

FBrush: TBrush;

end;

 

⑵ 声明访问属性

可以通过声明与Owned对象相同类型的属性来提供对Owned对象的访问能力。这给使用部件的开发者提供在设计时或运行时访问对象的途径。

下面给Shape控制提供了访问Pen和Brush的方法

 

type

TSampleShape=class(TGraphicControl)

private

procedure SetBrush(Value: TBrush);

procedure SetPen(Value: TPen);

published

property Brush: TBrush read FBrush write SetBrush;

property Pen: TPen read FPen write SetPen;

end;

 

然后在库单元的implementation部分写SetBrush和SetPen方法:

 

procedure TSampleShape.SetBrush(Value: TBrush);

begin

FBrush.Assign(Value);

end;

 

procedure TSampleShape.SetPen(Value: TPen);

begin

FPen.Assign(Value);

end;

 

⑶ 初始化Owned对象

部件中增加了的新对象,必须在部件constructor中建立,这样用户才能在运行时与对象交互。相应地,部件的destructor必须在撤消自身之前撤消Owned对象。

因为Shape控制中加入了Pen和Brush对象,因此,要在constructor中初始化它们,在destructor中撤消它们。

① 在Shape控制的constructor中创建Pen和Brush

 

constructor TSampleShape.Create(Aowner: TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

FPen := TPen.Create;

FBrush := TBrush.Create;

end;

 

② 在部件对象的声明中覆盖destructor

 

type

TSampleShape=class(TGraphicControl)

public

construstor.Create(Aowner: TComponent); override;

destructor.destroy; override;

end;

 

③ 在库单元中的实现部分编写新的destructor

 

destructor TSampleShape.destroy;

begin

FPen.Free;

FBrush.Free;

inherited destroy;

end;

 

④ 设置Owned对象的属性

处理Pen和Brush对象的最后一步是处理Pen和Brush发生改变时对Shape控制的重画问题。Pen和Brush对象都有OnChange事件,因此能够在Shape控制中声明OnChange事件指向的事件处理过程。

下面给Shape控制增加了该方法并更新了部件的constructor以使Pen和Brush事件指向新方法:

 

type

TSampleShape = class(TGraphicControl)

published

procdeure StyleChanged(Sender: TObject);

end;

 

implemintation

 

constructor TSampleShape.Create(AOwner:TComponent);

begin

inherited Create(AOwner);

Width := 65;

Height := 65;

Fpen := TPen.Create;

FPen.OnChange := StyleChanged;

Fbrush := TBrush.Create;

FBrush.OnChange := StyleChanged;

end;

 

procedure TSampleShape.StyleChanged(Sender: TObject);

begin

Invalidate(true);

end;

 

当变化发生时,部件重画以响应Pen或Brush的改变。

 

4. 怎样画部件图形

图形控制基本要素是在屏幕上画图形的方法。抽象类TGraphicControl定义了名为Paint的虚方法,可以覆盖该方法来画所要的图形。

Shape控制的paint方法需要做:

● 使用用户选择的Pen和Brush

● 使用所选的形状

● 调整座标。这样,方形和圆可以使用相同的Width和Height

 

覆盖paint方法需要两步:

● 在部件声明中增加Paint方法的声明

● 在implementation部分写Paint方法的实现

 

下面是Paint方法的声明:

 

type

TSampleShape = class(TGraphicControl)

protected

procedure Paint; override;

end;

 

然后,编写Paint的实现:

 

procedure TSampleShape.Paint;

begin

with Canvas do

begin

Pen := FPen;

Brush := FBrush;

case FShape of

sstRectangle, sstSquare :

Rectangle(0, 0, Width, Height);

sstRoundRect, sstRoundSquare:

RoundRect(0, 0, Width, Height, Width div 4, Height div 4);

sstCircle, sstEllipse :

Ellipse(0, 0, Width, Height);

end;

end;

end; 

无论任何控制需要更新图形时,Paint就被调用。当控制第一次出现,或者当控制前面的窗口消失时,Windows会通知控制画自己。也可以通过调用Invalidate方法强制重画,就象StyleChanged方法所做的那样。