delphi(9)
时间:2006-06-10 来源:许我一个信仰
第10章 高级组件设计
第10章将示范一些高级技巧,使得可以创建种类更多的组件,并更好地控制组件的工作方式。高级组件设计包括如何动态装载资源以创建出色的图形化控件、怎样公开被拥有的组件——Delphi 6所引入的新技术、创建对话框组件,持久化非公开特性,以及如何创建特性编辑器。
公开被拥有的组件可以节省很多工作,而且可以比以前的Delphi版本更加易于创建由许多控件衍生出来的组件。
10.1 动态装载资源
像TMediaPlayer(如图10.1所示)一样具有专业外观、富于吸引力的控件需要动态创建组件,并在创建组件时将图形资源装载到组件中。在第9章中,您已经学会如何使用Image Editor来创建Delphi组件资源(dcr)文件。如果把24×24像素的位图命名为与类相同的名字,并将DCR文件存储为与组件单元文件相同的名字——当然,扩展名是不同的;这样,在把单元添加到包的时候,Delphi将自动地装载相应的DCR文件。这时,这些位图将显示在VCL面板代表对应组件的按钮上(细节可以参考9.7.1节“用Image Editor 创建组件资源文件”)。
图10.1 TMediaPlayer组件的外观非常专业,它使用了位图,
在运行时从资源文件中动态装载(图中所示的speedis.avi
与Delphi一同发布,位于demos\coolstuf文件夹下)
通过将额外的光标、图标和位图添加到同一DCR文件中,Delphi会把这些资源文件编译到组件的.DCU文件中,并将其链接到.bpl库(请记住:BPL是一种特定的动态链接库)。将组件编译到包中之后,资源可以通过API过程访问,并使用组件方法来装载。通常具有资源装载方法的组件会包含代表资源的对象,如TSpeedButton的Glyph特性。
在数据库应用程序中通常会遇到的可视化结构是包含四个按钮的可视化控件(如图10.2所示),按钮用于在左右两栏之间来回移动相应的项。如图10.2所示的控件相当有用,可以用于几个窗体或工程,具有明显的累积效应。本节将使用TButtonPanel组件来示范如何动态地装载源,在下一节讨论如何公开被拥有的组件。
图10.2 按钮导航组件。方向箭头表示移动方向
注意:有一个谜语是这样的:您愿意现在得到一百万美元,还是第一天得一美分,以后每一天的钱是前一天的两倍,连续30天呢?答案当然是后一个。直到第20天到第30天之间时,才能看出累积的效果,最后的结果非常巨大,有10737418美元。使用组件来建立应用程序的效果与此类似。最初的效应并不明显,但累积到最后的结果是惊人的。
图10.2中显示的组件使用了由TMediaPlayer组件得到的位图。在组件中使用资源的第一步是将其加入到Delphi 组件资源文件,即DCR文件中。TButtonPanel组件的单元名是UButtonPanel.pas。因此,DCR文件是UButtonPanel.dcr,而且与组件单元位于相同的目录下。四个按钮可以用TSpeedButton组件来实现。TSpeedButton具有Glyph特性,可用于图像。Glyph可以是位图;如图10.2所示,共有四幅位图。
10.1.1 创建Delphi组件资源文件
Image Editor可用于为TButtonPanel组件创建位图资源。按下列步骤可创建DCR文件。
1.从Delphi的Tools菜单中启动Image Editor。
2.在Image Editor中,单击File,New,Component Resource File菜单项以创建资源文件。
3.选择Resource,New,Bitmap菜单项,并接受缺省的大小和颜色来创建四个位图(缺省的大小是32×32像素和16色 VGA模式)。
4.可以画出位图或复制并粘贴已有的位图(如果组件中包含可能受第三方版权保护的位图,在分发组件之前向公司的法律顾问查询一下)。如果想从mplayer.res文件中复制资源,可以在Image Editor中打开该文件,然后复制并粘贴要使用的四幅图像(mplayer.res文件位于Delphi安装目录的Lib子文件夹中)。
5.使用合适的名字重新命名每个位图(我们将用枚举列表和RTTI来索引和装载资源;因此可以参考图10.2,将位图从上到下依次命名为bpFirst, bpPrior, bpNext, bpLast。使用bp作为前缀是Delphi命名枚举元素的惯例——从构成枚举的单词的首字母中挑选两个作为前缀。枚举类型的名字是TButtonPosition)。
6.最后,把DCR文件保存为UButtonPanel.dcr,与组件位于同一目录。
如果DCR文件与组件位于同一目录里,当把组件添加到包时,DCR文件将与组件一同添加,如图10.3所示。编译器将编译该单元,并将包和DCR资源文件链接到BPL库。资源文件包括在库中,可通过方法调用访问。
图10.3 包编辑器,其中是dclusr60.dpk包,包含
了UButtonPanel单元的DCR和PAS文件
10.1.2 装载资源
装载资源的较为方便的方法是将资源命名为与枚举列表中的序数值同名。继续讨论TButtonPanel,按钮的类型定义命名如下:
type
{$M+}
TButtonPosition = (bpFirst, bpPrior, bpNext, bpLast);
{$M-}
可以注意到枚举元素的名字和前面小节中的资源名是相同的。$M是运行时类型信息所对应的编译器指令。如果将typinfo.pas单元添加到uses子句,可以从枚举序数值得到枚举值的字符串名,正好可用于读取相应的资源。
unit UEnumerationDemo;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
StdCtrls, ExtCtrls;
type
{$M+}
TButtonPosition = (bpFirst, bpPrior, bpNext, bpLast );
{$M-}
TForm1 = class(TForm)
Image1: TImage;
Image2: TImage;
Image3: TImage;
Image4: TImage;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FImages : array[TButtonPosition] of TImage;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{$R UButtonPanel.Res}
uses
typinfo;
procedure TForm1.FormCreate(Sender: TObject);
begin
FImages[bpFirst] := Image1;
FImages[bpPrior] := Image2;
FImages[bpNext] := Image3;
FImages[bpLast] := Image4;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
I : TButtonPosition;
Begin
for I := Low(TButtonPosition) to High(TButtonPosition) do
FImages[I].Picture.Bitmap.LoadFromResourceName( HInstance,
GetEnumName( TypeInfo(TButtonPosition), Ord(I)));
end;
end.
单元在接口部分定义了枚举类型TButtonPosition,并在编译时对其添加了运行时类型信息。四幅图像可用做位图的仓库。类中定义了一个TImage的私有数组,并在FormCreate事件方法中进行了初始化,以指向四个图像组件。
在实现部分,UButtonPosition.res文件通过$R编译器指令引入。ButtonClick事件方法迭代使用TButtonPosition枚举作为循环范围,并使用图像引用数组装载位图。一个TPicture对象包含在一个TImage对象中。而每个TPicture包含一个TBitmap对象,TBitmap对象具有LoadFromResourceName方法。HInstance是在System单元中定义的全局Windows句柄,可以分配到应用程序或库;而GetEnumName使用RTTI将枚举值转换为对应的字符串值。要记住:位图资源与枚举元素是同名的,这样对资源编程更为容易。
10.2 公开所拥有的组件
由于组件的复杂性不断增长,更多组件定义为聚合关系。较早版本的Delphi要求定义特性编辑器以便在设计时访问所包含的对象;或至少需要进行必要的属性提升,才能在设计时对所拥有对象的数据和事件特性进行访问。
考虑在上一节提到的TButtonPanel组件。在含有按钮的面板上对齐按钮,是创建按钮面板组件的一个直接途径。使用该技巧可以在设计时访问按钮的属性。下面列出的部分代码演示了这个技巧。
注意:属性提升是一种有效的技术,可用于将所包含对象的属性提升到容器的接口部分——称为使接口扁平化,这样在设计时对所包含的组件提供访问接口就不必要了。
type
TButtonPosition = (bpFirst, bpPrior, bpNext, bpLast);
TButtonPanel = class(TPanel)
private
FButtons : array[TButtonPosition] of TSpeedButtons;
protected
procedure SetClickEvent( Index : TButtonPosition; const Value :
TNotifyEvent );
function GetClickEvent( index : TButtonPosition) :
TNotifyEvent;
published
property FirstOnClick : TNotifyEvent index bpFirst read
GetClickEvent
write SetClickEvent;
property PriorOnClick : TNotifyEvent index bpPrior read
GetClickEvent
write SetClickEvent;
// etc…
end;
列出的部分代码示范了属性提升。按钮是被包含的对象,但在以前的版本中无法为它们声明公开属性。因此不能在 Object Inspector中直接操纵这些对象。结果,所有在设计时需要被修改的被包含组件的特性都需要属性提升,因此会增加许多额外的方法。虽然生成这些提升的属性及其相关联的访问方法很简单,但是这些是必须编写的代码,让人感觉沉闷之极。Delphi 6提供了组件所有权机制,减少了继续按上述方式创建组件的必要性。
10.2.1 声明公开的组件特性
Delphi的当前版本能够正确地将所拥有的组件流化到.DFM文件,而且设计时可以在Object Inspector中对其进行修改。回顾一下TButtonPanel组件,可以发现:将对象数据声明为私有的,经由代表对象的特性提供访问接口,仍然是个好习惯。但现在特性定义可能位于公开部分,而对象可以在设计时被修改,这是第3章中讨论TComponent类所引入的新的特征。下面的代码示范了基本正确的解决方案。
unit UImagePanel;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
ExtCtrls;
type
TImagePanel = class(TPanel)
private
FImage : TImage;
public
constructor Create(AOwner : TComponent); override;
published
property Image : TImage read FImage;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('PKTools', [TImagePanel]);
end;
constructor TImagePanel.Create(AOwner: TComponent);
begin
inherited;
FImage := TImage.Create(Self);
FImage.Parent := Self;
FImage.Align := alClient;
end;
end.
从语法上来说,代码是正确的。TImagePanel组件可以编译并安装,图像属性将显示为TImagePanel的特性。在设计时,如果改变了TImage的Picture特性,图像面板如图10.4所示,其功能是正确的。
图10.4 设计时的TImagePanel组件,该组件是在前面的代码中定义的
但更进一步来看,应用程序运行时,图像是空白的。如果以文本方式查看DFM文件(列表如下),可以注意到没有与图像特性相关的流化数据。
object Form1: TForm1
Left = 342
Top = 116
Width = 316
Height = 400
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 120
TextHeight = 16
object ImagePanel1: TImagePanel
Left = 64
Top = 64
Width = 201
Height = 257
Caption = 'ImagePanel1'
TabOrder = 0
end
end
很清楚,某些地方出了错。实际上也是如此。如果要持久化公开特性的状态,要记得调用TComponent类的新方法SetSubComponent。
10.2.2 调用SetSubComponent以持久化公开对象
上一节乍看起来没什么问题。仔细查看一下,可以发现图像面板并未持久化组件的图像部分。当代码添加加到包、安装组件、在窗体上测试组件时,这一点是显然的,但在这之前却很难看出来。如果我们添加一个对SetSubComponent的调用,设计时对公开对象的属性的修改将持久化到DFM文件中。
将该调用添加到上一节的构造函数中,即可使组件正常工作。修改后的构造函数如下。
constructor TImagePanel.Create(AOwner: TComponent);
begin
inherited;
FImage := TImage.Create(Self);
FImage.Parent := Self;
FImage.Align := alClient;
FImage.SetSubComponent( True );
end;
TComponent类定义在Classes.pas单元中,SetSubComponent方法的代码如下:
procedure TComponent.SetSubComponent(Value: Boolean);
begin
if Value then
Include(FComponentStyle, csSubComponent)
else
Exclude(FComponentStyle, csSubComponent);
end;
SetSubComponent将csSubComponent添加到ComponentStyle集合中。更为重要的是,它改变了子组件特性流化到窗体资源文件的方式。当通过调用SetSubComponent向窗体添加了csSubComponent风格后,窗体将呈现正确的行为,下面以文本方式列出了窗体资源文件的一部分。
object Form1: TForm1
Left = 457
Top = 125
Width = 316
Height = 400
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 120
TextHeight = 16
object ImagePanel1: TImagePanel
Left = 40
Top = 40
Width = 209
Height = 161
Caption = 'ImagePanel1'
TabOrder = 0
Image.Left = 1
Image.Top = 1
Image.Width = 207
Image.Height = 159
Image.Align = alClient
Image.Picture.Data = {
07544269746D6170E62B0000424DE62B00000000000076000000280000009900
注意:为节省空间,列出的窗体资源文件被截短了。长的数字序列是位图的比特表示,大约要15张纸才能完全列出。
要使特性流化机制能够正确地流化所包含的图像组件,在构造函数中调用SetSubComponent即可。
如果想让用户可以访问对象的特性,可以使用property语句使该对象具有公开权限。如果只需要使被包含对象的某些属性可访问,可以从被包含对象的TCustom类型祖先子类化,并且只提升想在Object Inspector中显示的那些属性的可见性。然后在容器组件中定义新的子类的对象,即使是公开权限仍然能够有效地限制在设计时可以修改的特性。
10.3 创建对话框组件
对话框组件包括一个被该组件对象所包含的窗体。在组件面板的Dialogs属性页上可以找到几个对话框组件的例子,包括打开文件、保存文件对话框,打印、打印机设置对话框,字体、颜色、查找、替换对话框组件。如果组件需要复杂的界面,有可能用户需要输入许多信息才能正确设置组件的状态(见图10.5),那么您就需要对话框组件。
图10.5 TFontDialog组件需要在对话框界面中设置与字体相关的多种特性
注意:TForm是TComponent的派生类。您可能会得出结论:窗体可以作为组件安装到VCL中。按照惯例从不这样做。如果向窗体添加了注册过程,窗体将安装到VCL中;但特性在设计时无法正确流化,控件则会在运行时消失。
对话框组件在组件类中包含了一个窗体。需要将一个非可视化组件安装到VCL中,通常从 TComponent派生,由它来负责显示体现对话框行为的窗体。下面的代码示范了一个组件化的AboutBox对话框,它使用了第9章的VersionLabel组件(见图10.6)。
图10.6 AboutBoxDialog组件
unit UAboutBoxDialog;
interface
uses Windows, SysUtils, Classes, Graphics, Forms, Controls,
StdCtrls,
Buttons, ExtCtrls, UVersionLabel;
type
TAboutBox = class(TForm)
Panel1: TPanel;
ProgramIcon: TImage;
ProductName: TLabel;
OKButton: TButton;
VersionLabel1: TVersionLabel;
Copyright: TLabel;
Comments: TLabel;
private
{ Private declarations }
public
{ Public declarations }
end;
TAboutBoxDialog = class(TComponent)
private
AboutBox : TABoutBox;
FProductName, FCopyright, FComments : TCaption;
FPicture : TPicture;
protected
procedure SetPicture(const Value: TPicture);
public
function Execute : Boolean;
published
constructor Create( AOwner : TComponent ); override;
destructor Destroy; override;
property Copyright : TCaption read FCopyright write
FCopyright;
property Comments : TCaption read FComments write FComments;
property ProductName : TCaption read FProductName write
FProductName;
property Picture : TPicture read FPicture write SetPicture;
end;
procedure Register;
implementation
{$R *.DFM}
procedure Register;
begin
RegisterComponents('PKTools', [TAboutBoxDialog]);
end;
{ TAboutBoxDialog }
constructor TAboutBoxDialog.Create(AOwner: TComponent);
begin
inherited;
FPicture := TPicture.Create;
end;
destructor TAboutBoxDialog.Destroy;
begin
FPicture.Free;
inherited;
end;
function TAboutBoxDialog.Execute: Boolean;
begin
AboutBox := TAboutBox.Create(Screen);
try
AboutBox.ProductName.Caption := FProductName;
AboutBox.Copyright.Caption := FCopyright;
AboutBox.Comments.Caption := FComments;
AboutBox.VersionLabel1.FileName := Application.EXEName;
AboutBox.ProgramIcon.Picture.Assign( FPicture );
AboutBox.ShowModal;
finally
AboutBox.Free;
end;
result := True;
end;
procedure TAboutBoxDialog.SetPicture(const Value: TPicture);
begin
if( Value = FPicture ) then exit;
FPicture.Assign(Value);
end;
end.
使用Delphi的New Items对话框Forms属性页上的AboutBox模板,启动一个新的窗体作为组件。使用第9章的TVersionLabel组件替换表示版本的TLabel。组件的窗体部分不需要其他代码。按照惯例,对话框组件通过一个返回 Boolean的函数方法来显示。在该惯例并不违反直觉的情况下,将execute方法添加到实际的组件TAboutBoxDialog中。然后声明用来初始化窗体的包含数据的字段。由图10.6可知,图像、产品名、注释、版权等特性显然是需要的。因为所有的标签都使用了TCaption类型,您只需在组件的构造函数中创建TPicture对象并在析构函数中将其释放。
Execute方法使得对话框组件易于使用,如同VCL中Dialog属性页上的组件一样。Execute实例化对话框窗体,将组件数据复制到窗体特性,并把窗体作为模式对话框显示。大多数情况下,这就是通常的对话框组件。创建窗体、定义行为、将窗体实例包裹在组件中,当调用Execute方法时显示窗体,并将窗体状态作为组件的特性值返回。这个例子很简单但相当有用,它示范了对话框组件设计的技术。
10.4 重载Notification方法
在设计时,有些类型的组件会引用其他的组件。一个很好的例子是TTable和TQuery组件,它们引用了TDataSource组件。在TComponent中引入了Notification方法,定义如下。
procedure Notification(AComponent: TComponent; Operation:
TOperation); virtual;
上面的组件参数是即将创建或删除的组件。TOperation是枚举类型,包括元素 opInsert 和opRemove。当组件被删除或创建时,其所有者TWinControl对象对其拥有的所有组件迭代并调用相应的Notification方法(参见图10.7,其中显示了当一个TTable组件被创建时的调用栈)。如果某些被拥有的组件引用了即将删除的组件,Notification方法使得它们可以将引用设置为空。如果他们没有将引用设置为空,那么该引用的使用将导致访问违例。
图10.7 组件插入到存储器时的调用栈
如果组件在本身与其他对象之间维护了引用关系,那么应该在组件重载Notification方法。如果是HasA或聚合关系,则不必使用Notification方法。
下面的引用的代码来自两个类,一个是TLed图形类,另一个是TLedTimer。假定TLedTimer是TTimer的子类,并维护了一个TList,其中包含了一个或多个定时器。每个TLed组件包含一个对定时器的引用。对定时器的每次滴答声,LED根据闪烁的频率开或关。
如果组件有一个对象特性,那么可以在Object Inspector中使用下拉框。下拉框将显示组件作用域中所有具有合适类型的可访问的对象。这使得可以对引用特性分配一个对象。在LED的例子中,定时器用来使LED闪烁;如果LED引用了定时器而定时器被删除,那么必须更新LED对定时器的引用。
Procedure TLed.Notification( AComponent : TComponent; Operation :
TOperation );
begin
inherited Notification( AComponent, Operation );
if( Operation = opRemove ) and (AComponent = FLedTimer ) then
FLedTimer := Nil;
end;
在例子中,Notification方法将测试AComponent参数是否与FLedTimer相同;如果相同,那么FLedTimer字段将被设置为零。按照规则,首先调用Notification的继承版本,测试是否维护了该组件的引用,然后看一下该操作组件是否重要。如果满足这些条件,那么更新引用。
10.5 创建特性编辑器
内部数据类型特性有预定义特性编辑器。通常特性编辑器以简单的文本域的形式出现。用户输入数据,属性编辑器执行基本的校验工作。在Object Inspector中,数据输入域位于右侧,与相应的特性相邻,它是由TPropertyEditor类派生而来。图10.8演示了组合框特性编辑器,可以在组件作用域内选取对象。另一个特性编辑器使用了如图10.5所示的TFontDialog对话框。
图10.8 上一节提及的LED组件的LedTimer特性
由于属性编辑器是对象,可以按照需要使其简单或复杂。您还可能遇到过其他的特性编辑器,如TStrings Items特性编辑器、TTable和TQuery组件的字段编辑器、字体编辑器、DBGrids的Columns编辑器等。要定义特性编辑器,必须子类化Delphi的Source\ToolsAPI目录下dsgnintf.pas中定义的TPropertyEditor,或者子类化TPropertyEditor的某个后代。定义特性编辑器后,需要使用组件中定义的Register过程注册特性编辑器。
procedure Register;
begin
RegisterComponents('PKTools', [TVersionLabel]);
RegisterPropertyEditor( TypeInfo(TFileName), TVersionLabel,
'FileName',TFileNameProperty);
end;
上述代码示范了RegisterPropertyEditor的用法, 该过程为第9章中的TVersionLabel组件注册了一个特性编辑器类TFileNameProperty。第一个参数是与特性编辑器相关联的数据类型的RTTI记录。在例子中,编辑器与TFileName类型的特性相关。第二个参数是TComponentClass,表示为哪个组件类注册了特性编辑器。第三个参数是特性名,第四个参数类型为TPropertyEditorClass,它表示TPropertyEditor的一个子类。
10.5.1 子类化已有的特性编辑器
类最大的好处之一就是可以重用。相对于从零开始定义一个类(或算法),找到已有的代码并进行重用总是个好主意。要定制已有的特性编辑器,尽可能找一个与你的类的需求最为接近的编辑器。第9章的TVersionLabel类定义了一个TFileName类型的特性FileName,该类型是由Delphi在SysUtils单元中定义的,TFileName=string。用户可以很方便地浏览文件位置、指定路径和文件名、执行必要的验证,而无需输入复杂的文件路径。
注意:要将特性编辑器与使用编辑器的组件分别在不同的单元中定义。这样,如果其他组件特性的类型与该编辑器相适应,就可以重用该编辑器。
幸运的是,发现有一个合适的特性编辑器与TMediaPlayer相关联,而且提供了TFileName所需要的行为。特性编辑器TMPFileNameProperty是由dsgnintf.pas中定义的TStringProperty子类化而来。除了数据类型是TMPFileNameProperty外,该编辑器与我们的需求相当接近;另外该编辑器显示的文件名过滤器是不正确的。在过滤器列表MPFileName特性显示的是*.avi、*.wav和*.mid文件。需要对该类做一些小的修改,以解决所存在的问题。
当调用特性编辑器时,会调用特性编辑器的Edit方法。例如:可以在 TMediaPlayer 的 FileName 特性中输入媒体文件的文件名。但如果在Object Inspector中双击相应的编辑域,或单击省略号按钮,将触发特性编辑器的Edit方法。下面的代码示范了如何子类化TMPFileName类,并重载Edit方法以显示适合 TVersionList 组件的对话框。
unit UFileNameProperty;
interface
Uses
Dialogs, DsgnIntf, Forms;
Type
TFileNameProperty = class(TMPFileNameProperty)
public
Procedure Edit; override;
end;
implementation
Procedure TFileNameProperty.Edit;
var
OpenDialog : TOpenDialog;
begin
OpenDialog := TOpenDialog.Create(Application);
try
OpenDialog.FileName := GetValue;
OpenDialog.Filter := 'Log Files (*.log)|*.log|All Files
(*.*)|*.*';
OpenDialog.Options := OpenDialog.Options + [ofPathMustExist];
if OpenDialog.Execute then SetValue(OpenDialog.Filename);
finally
OpenDialog.Free;
end;
end;
end.
重载Edit方法的代码仅作为演示。使用超类的Edit方法作为示例,特性编辑器创建了一个TOpenDialog类的实例来完成大部分的工作。需要将ofPathMustExist选项添加到OpenDialog.Options中。然后调用由TPropertyEditor继承的GetValue方法来得到与该编辑器关联的特性的当前值,使用Execute方法显示OpenDialog 对象。如果用户单击OK,则调用继承的SetValue方法来更新相关联的特性。最后要释放TOpenDialog的实例。当把RegisterPropertyEditor语句添加到TVersionLabel的Register过程,并把特性编辑器单元添加到组件实现部分的uses语句时,将把TFileName编辑器与每个版本标签组件的FileName特性关联起来。
10.5.2 定义定制的特性编辑器
当定义定制特性编辑器时,可以重载TPropertyEditor中定义的许多虚方法。表10.1描述了所有的虚方法,您可以在子类重载。要记住:表10.1中的方法不包括在已经子类化的特性编辑器中所添加的额外的虚的保护或公有方法。
表10.1 为特性编辑器定义的虚方法,重载特定的方法可以创建定制的特性编辑器
方法 |
描述 |
Create |
保护权限的构造函数,用于创建特性编辑器的实例 |
Destroy |
公有权限的析构函数,如果在特性编辑器中实例化了所拥有的对象,则需重载该函数 |
Activate |
当在Object Inspector激活相关特性时,调用该方法 |
(续表)
方法 |
描述 |
AllEqual |
当选定多个对象时,调用该方法。如果所有的对象都具有相等的特性值,则返回该值 |
AutoFill |
当GetAttributes返回的TPropertyAttributes包含paValueList时,调用该方法验证是否允许自动完成特性值输入 |
Edit |
当双击编辑域或单击省略号按钮(…)触发特性编辑器时,调用该方法 |
GetAttributes |
返回TPropertyAttributes值;例如,如果TPropertyAttributes包含paDialog,则该编辑器用于对话框,与TFileNameProperty相似 |
GetEditLimit |
返回用户可输入字符的最大数目 |
GetName |
返回特性名 |
GetProperties |
如果特性包含子特性(如字体),将显示子特性;向TPropertyAttributes添加paSubProperties值 |
GetPropInfo |
返回指向RTTI特性信息记录的指针 |
GetValue |
返回特性的字符串值 |
GetValues |
当有多个已定义的合适值时,对特性设置所有可接收的值;该特性的TPropertyAttributes值必须包含paValueList |
Initialize |
在使用特性编辑器之前进行初始化 |
SetValue |
设置特性值的字符串表示 |
ListMeasureWidth |
在显示特性值的下拉列表之前,确定其宽度 |
ListMeasureHeight |
在显示特性值列表之前,返回其高度 |
ListDrawValue |
允许在特性值列表中绘出图形数据 |
PropDrawName |
用于在Object Inspector中画出特性的名字 |
PropDrawValue |
用于在Object Inspector中画出特性的值 |
TTableBox
假设已经定义了一个组件,可以列出给定数据库的所有可用的表。TTableBox是TComboBox类的后代,它在响应下拉动作时,使用给定数据库中所有的表填充下拉列表。新组件可以如下定义。
unit UTableBox;
// UTableBox.pas - Contains a tablename combobox
// Copyright (c) 2000. All Rights Reserved.
// by Software Conceptions, Inc. Okemos, MI USA (800) 471-5890
// Written by Paul Kimmel
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
DBTables, StdCtrls;
type
TTableBox = class(TCustomComboBox)
private
{ Private declarations }
FSession : TSession;
FDatabaseName : string;
protected
{ Protected declarations }
procedure DropDown; override;
procedure Notification( AComponent : TComponent;
Operation : TOperation ); override;
procedure SetSession( const Value : TSession );
procedure SetDatabaseName(const Value: string);
public
{ Public declarations }
published
{ Published declarations }
property Text;
property OnChange;
property Session : TSession read FSession write SetSession;
property DatabaseName : string read FDatabaseName
write SetDatabaseName;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('PKTools', [TTableBox]);
end;
{ TTableBox }
procedure TTableBox.DropDown;
begin
inherited;
if( FSession = Nil ) then FSession := DBTables.Session;
if( Items.Count = 0 ) then
FSession.GetTableNames( FDatabaseName, '*.*', True, True,
Items );
end;
procedure TTableBox.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if( Operation = opRemove ) and (AComponent = FSession) then
Session := Nil;
end;
procedure TTableBox.SetDatabaseName(const Value: string);
begin
FDatabaseName := Value;
end;
procedure TTableBox.SetSession(const Value: TSession);
begin
if( Value = FSession ) then exit;
Items.Clear;
FSession := Value;
end;
end.
TTableBox是由TCustomComboBox子类化而来,可以限制对某些特性的服务,如Items,这样可以将非表数据放置到列表中。两个特性分别是Session和DatabaseName。Session是一个TSession组件,DatabaseName是一个字符串。这两个实现可用于在数据库中进行查找,数据库可能是ODBC别名、物理数据库或者其他类似的东西,然后得到可用表的列表。这里重载了Notification方法,以确保组件能够知道Session组件是否被删除。Text和 OnChange特性由TCustomComboBox中的保护权限提升为公开权限,既保持了组件的简单性,又可以对用户选择的数据进行访问。为确保下拉列表中的数据尽可能新,每次出现下拉动作时都重新填充列表。
组件的工作是可靠的。一个明显的不足是DatabaseName特性。输入数据库名很容易出现错误。从可用的数据库列表中进行选择可能更为方便。为此,我们需要为数据库名字特性定义特性编辑器。
TDatabaseName 特性编辑器
使用上一节的TTableBox,我们添加一个特性编辑器以方便数据库名的选取。当我们对DatabaseName特性进行修改时,对字符串类型添加了一个新的别名,其类型定义如下。
type TDatabaseName = string;
将所有与DataBaseName特性相关的方法、字段、以及特性都转换为使用该类型,这样可以使特性编辑器更好地与特性关联在一起。需要修改的代码如下:
FDatabaseName : TDatabaseName;
procedure SetDatabaseName(const Value: TDatabaseName);
property DatabaseName : TDatabaseName read FDatabaseName write
SetDatabaseName;
还需要修改SetDatabaseName的实现,来与上面的声明相匹配。
TDatabaseName特性的属性编辑器将显示下拉列表框,其中包括数据库、ODBC别名、Excel数据以及其他符合要求的数据源。该特性编辑器定义如下。
unit UDBPropertyEditors;
// UDBPropertyEditors.pas - Contains property editors for
database related objects
// Copyright (c) 2000. All Rights Reserved.
// by Software Conceptions, Inc. Okemos, MI USA (800) 471-5890
// Written by Paul Kimmel
interface
uses
Classes, SysUtils, Dialogs, DsgnIntf, DBTables,
StdCtrls;
type
TDBPropertyEditor = class(TStringProperty)
protected
Procedure GetNames(Strings : TStrings);virtual; abstract;
public
Procedure GetValues(Proc: TGetStrProc); override;
Function GetAttributes: TPropertyAttributes; override;
end;
TDatabaseNameProperty = class(TDBPropertyEditor)
protected
Procedure GetNames(Strings : TStrings); override;
end;
procedure Register;
implementation
uses
UTableBox;
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(TDatabaseName), TTableBox,
'DatabaseName', TDatabaseNameProperty );
end;
{ TDBPropertyEditor }
function TDBPropertyEditor.GetAttributes: TPropertyAttributes;
begin
result := [paRevertable, paMultiselect, paValueList];
end;
procedure TDBPropertyEditor.GetValues(Proc: TGetStrProc);
var
I: Integer;
Strings : TStrings;
begin
Strings := TStringList.Create;
try
GetNames( Strings );
for I := 0 to Strings.Count - 1 do Proc( Strings[I] );
finally
Strings.Free;
end;
end;
{ TDatabaseNameProperty }
procedure TDatabaseNameProperty.GetNames(Strings: TStrings);
begin
Session.GetDatabaseNames(Strings);
end;
end.
TDatabaseName特性的编辑器是TDBPropertyEditor,由TStringProperty子类化而来。 TDBPropertyEditor重载了GetValues 和 GetAttribute。由于编辑器需要提供一个列表,paValueList作为编辑器的属性之一返回(其他属性的描述请参见表10.1)。GetValues创建一个TStringList对象,调用GetNames(一会儿会讨论到),然后用字符串来填充编辑器的下拉列表框。出于这个目的,GetValues的参数为过程类型TGetStrProc。
TDBPropertyEditor将GetNames定义为抽象方法。结果永远无法实例化TDBPropertyEditor,而必须使用子类。子类只需要定义GetNames。TDatabaseNameProperty是我们要讨论问题,它使用Session.GetDatabaseNames(Strings)来实现GetNames方法。 Session对象是在DBTables.pas中定义的,它是个全局对象。
实现部分开始定义的Register过程负责注册特性编辑器。其中RegisterPropertyEditor的参数分别是TDatabaseName的 RTTI TypeInfo记录、编辑器响应的组件、相关联的特性、以及编辑器类自身。子类化TDBPropertyEditor类,可以用全局会话对象的方法来填充一个会话或表的列表。这些特性编辑器留作练习。也可以创建组件一级的编辑器。组件编辑器是类,可以在右键单击组件时显示菜单;由TComponentEditor派生而来,并使用TRegisterComponentEditor进行注册(关于组件编辑器和库专用向导的讨论,可以参见附录B)。
10.6 持久化非公开特性
除非储存指令是False或调用了一个返回False的函数,否则公开特性一般都流化到DFM文件。通过重载由TComponent继承的DefineProperties方法,可以流化非公开的组件特性。DefineProperties被定义如下:
procedure DefineProperties(Filer: TFiler); override;
TFiler类可子类化为TReader和TWriter类。TFiler.DefineProperty方法可以指定一个特性, 并传递两个分别具有TReader参数和TWriter参数的过程类型参数,分别用于读写DFM文件。持久化非公开对象的关键在于,在自己的组件中重载DefineProperties,并添加将特性流化到DFM文件的读写方法。
10.6.1 重载DefineProperties
通过重载DefineProperties,可以修改特性流入和流出DFM文件的方法,还可以流化非公开的特性。下面的例子示范了如何将一个私有的整数流化到任意的具有该组件实例的窗体。
TStoreProperties = class(TComponent)
private
FAnInt : Integer;
protected
procedure ReadAnInt( Reader : TReader );
procedure WriteAnInt( Writer : TWriter );
procedure DefineProperties( Filer : TFiler ); override;
public
constructor Create(AOwner : TComponent ); override;
end;
DefineProperties语句重载了继承的方法,而ReadAnInt 和 WriteAnInt分别是从DFM文件读写特性的方法。
procedure TStoreProperties.DefineProperties(Filer: TFiler);
begin
inherited;
Filer.DefineProperty( 'AnInt', ReadAnInt, WriteAnInt, True );
end;
在DefineProperties的实现中,它向Filer对象通知了数据的名字、用于流化数据的读写方法。最后一个参数表示特性是否具有数据。在上面的代码中,AnInt字段总是被流化。
procedure TStoreProperties.ReadAnInt(Reader: TReader);
begin
FAnInt := Reader.ReadInteger;
end;
procedure TStoreProperties.WriteAnInt(Writer: TWriter);
begin
Writer.WriteInteger( FAnInt );
end;
object StoreProperties1: TStoreProperties
Left = 256
Top = 160
AnInt = 100
end
读写方法根据数据类型来调用TReader和TWriter的适当方法。当调用这些方法时,TReader和TWriter已处于正确的数据流位置,可以读写AnInt值。DFM文件片断以文本形式显示了流化的AnInt字段值。
10.6.2 TReader和TWriter
TReader和TWriter都有几个方法可用于读写内部数据类型。例如:ReadString读取字符串数据,ReadChar读取一个字符。读、写两个类是对称的,因此如果使用某个特定的写方法来写入数据,那么也可以使用对应的读方法来读出数据。
对于每个要流化的额外的字段或属性,都要添加一个相应的Filer.DefineProperty调用;还要为每个数据类型分别传递读写过程。这意味着,对每个要流化的额外的值,都需要一对读写方法。
10.6.3 写入复杂类型的数据
对于写入内部类型数据,有一些特定的方法可用。聚集类型或复杂类型可分解为几个简单类型;而对于组件,还有可读写组件的方法。本小节示范了如何流化简单类型以外的数据,并讨论与流对象相关的一些问题。
流化列表
使用本节开头讲到的技术,也能把数据列表存储到DFM文件中。在下面的例子中,示范了将TStringList中的字符串存储到DFM文件。
private
FStrings : Strings;
protected
procedure ReadStrings( Reader : TReader );
procedure WriteStrings( Writer : TWriter );
procedure DefineProperties( Filer : TFiler ); override;
public
constructor Create(AOwner : TComponent ); override;
destructor Destroy; override;
将前面的声明添加到TStoreProperties类对该过程进行测试。与以前相同,有一个私有字段而没有公开的特性。声明了读写方法,并重载了DefineProperties。由于TStrings是对象,需要定义构造函数和析构函数来初始化和清除TStrings对象。其实现如下。
constructor TStoreProperties.Create(AOwner : TComponent );
begin
inherited;
FAnInt := 100;
FStrings := TStringList.Create;
FStrings.Text := ROBERT_HERRICK;
end;
destructor TStoreProperties.Destroy;
begin
FStrings.Free;
inherited;
end;
构造函数和析构函数用来初始化TStrings对象,并把常数ROBERT_HERRICK赋值给对象的Text特性。
procedure TStoreProperties.DefineProperties(Filer: TFiler);
begin
inherited;
Filer.DefineProperty( 'AnInt', ReadAnInt, WriteAnInt, True );
Filer.DefineProperty( 'Strings', ReadStrings, WriteStrings,
True );
end;
注意:无须在调用DefineProperty时使用与字段或特性相同的名字。由于可读性的原因,去掉了字段前缀F,只使用名字的其余部分也是一样。
代码中修改了DefineProperties,用于把TStrings字段添加到对DefineProperty的调用。
procedure TStoreProperties.ReadStrings(Reader: TReader);
begin
Reader.ReadListBegin;
FStrings.Clear;
while( Reader.EndOfList = False ) do
FStrings.Add( Reader.ReadString );
Reader.ReadListEnd;
end;
procedure TStoreProperties.WriteStrings(Writer: TWriter);
var
I : Integer;
begin
Writer.WriteListBegin;
for I := 0 to FStrings.Count - 1 do
Writer.WriteString( FStrings[I] );
Writer.WriteListEnd;
end;
回忆一下,读写方法必须是对称的,可以看到每个方法都调用适当的ListBegin和ListEnd方法来读写列表的标志。读方法清除字符串,读取字符串值直到出现EndOfList标志;将列表中的每个字符串都添加到TStrings对象。写方法知道列表中有多少项,因此可以用for循环写出所有的字符串。
对象流化与窗体继承
当把一个窗体添加到存储库时,该窗体即成为可继承窗体模板。对父窗体的改变会影响子窗体。如果对组件进行流化,而这些组件也添加到存储库的窗体中,那么您可能需要确保在子窗体不再流化同样的特性值;否则对祖先窗体的改变将无法反映到子窗体中。为确保这一点,可以对DefineProperties 方法添加一个条件写函数。
为确保TStrings字段值不在子窗体中进行流化,可将下面的嵌套函数添加到DefineProperties中。
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
result := not (Filer.Ancestor is TStoreProperties) or
not (TStoreProperties(Filer.Ancestor).FStrings.Text =
FStrings.Text)
else
result := not (FStrings.Text = EmptyStr);
end;
该函数检查Filer.Ancestor是否不为空。在祖先不为空的情况下,如果祖先类型或相应的文本不同,文本将写出到文件。如果祖先为空而字符串非空,字段值就写出到文件。修改后的DefineProperties完整代码如下列出。
procedure TStoreProperties.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
result := not (Filer.Ancestor is TStoreProperties) or
not (TStoreProperties(Filer.Ancestor).FStrings.Text =
FStrings.Text)
else
result := not (FStrings.Text = EmptyStr);
end;
begin
inherited;
Filer.DefineProperty( 'AnInt', ReadAnInt, WriteAnInt, True );
Filer.DefineProperty( 'Strings', ReadStrings, WriteStrings,
DoWrite );
end;
请注意,DoWrite仅用于DefineProperties,因此嵌套在DefineProperties中。在前面的代码中,HasData(第四个参数)为True,而现在的代码中使用了DoWrite的返回值。可以用功能相似的函数来有条件地写出字段数据,以确保在且仅在符合条件的情况下写出数据。
流化二进制数据
流化二进制特性时,使用DefineBinaryProperty 方法,以及参数为TSream类型的读写方法。在重载的DefineProperties方法中,使用同样的Filer对象调用DefineBinaryProperty方法,就可以使用流对象读写二进制特性。在DefineBinaryProperties方法中可能出现的对DefineBinaryProperty的调用如下。
Filer.DefineBinaryProperty( 'Image', ReadImage, WriteImage, True );
ReadImage 和 WriteImage参数是有一个TStream参数的过程。
procedure TStoreProperties.ReadImage(Stream : TStream);
begin
FImage.Picture.Bitmap.LoadFromStream(Stream);
end;
procedure TStoreProperties.WriteImage(Stream : TStream);
begin
FImage.Picture.Bitmap.SaveToStream( Stream );
end;
将ReadImage和WriteImage的声明添加到类中并像上述代码中那样进行定义,即可向DFM文件流化Image字段。还需要添加图像字段,并在构造函数和析构函数中相应地创建和释放它。
10.7 小 结
第10章包含了许多内容。你学习了创建高级组件的技术,像动态定义并加载资源、如何公开所拥有的组件(Delphi 6 中的新技术)、怎样创建对话框组件以及定义特性编辑器,还有如何流化对象的非公开属性。
VCL演示了Delphi设计得多么高明。除了在第10章中学到的高级技巧(包括如何使用Notification方法)之外,还有一些可利用的VCL特性。在第11章中可以学到更多的关于使用组件扩展用户界面的知识。而附录A则涵盖了OpenTools API、创建组件编辑器、以及库向导的注册等内容。