附录A与第11章的内容前后承继。阅读第11章之后,您已经了解了创建定制组件的大部分知识。附录A也很重要,它示范了如何创建组件编辑器以及使用OpenTools API对Delphi自身进行扩展。二者分属不同的主题:一个与组件相关,另一个则是要扩展Delphi。之所以将二者放到附录中,是因为它们没有其他技术那样常用。但要用到二者的时候,它们都是很有用的。
定制组件编辑器可以定义设计时对话框,编辑器在Object Inspector不够用时,使得用户能够可视化地修改特定于该组件的每个方面。一个很好的例子就是TChart组件,由Dave Berneda开发。另外,在设计时您还可以从组件的上下文菜单中运行该组件所包含的代码。
假定您使用Delphi已经有一段时间了,而您认为Delphi缺乏某些必要的特征。我三年前在一个工程上工作时,就发生了这样的情况。当时正在对Rational Rose所定义的系统结构模型进行编码,我们已经厌烦了手工定义类并编写函数体。实在是太烦了。创建一个类来读取类的声明并编写函数体,这看来是个不错的主意。使用OpenTools API,有时候再借助一下Ray Lischner的书《Hidden Paths of Delphi 3: Experts, Wizards, and the Open Tools API》,我们最终向Delphi添加了一个能够调用类生成器的菜单项。结果终于摆脱了这本来可以自动完成的、烦人的任务(可惜的是我们没有一本语法分析方面的好书,我有点离题了)。
这准确地描述了Inprise公司在决定向Delphi专业版和企业版用户提供OpenTools API时的想法。当需要Delphi具有某些功能时,添加上去就行了。Delphi现在还具有“Complete class at cursor”的代码生成功能,因此我们可以创建一个尚不存在的专家:可以生成专家的专家。
当您阅读本章后,可以了解到如何创建组件编辑器以及怎样使用专家对Delphi进行定制。有一个工具可用于开发定制专家,这使得创建专家与创建组件一样容易。
OpenTools API原来定义为抽象虚类,即它使用了Delphi接口,而我们可以继承它以便向Delphi添加扩展。原来的那些单元仍然存在于你安装的Delphi的Source\ToolsAPI子目录下,但在大多数情况下它们已经让位于ToolsAPI.pas单元中定义的COM接口。
注意:ToolsAPI单元与Delphi专业版和企业版一同发布。您也可以对Delphi标准版进行定制,只是包含相应接口的单元在Delphi标准版中是没有的。
如果您对Delphi抽象接口比较熟悉,那么比从零开始要好一些。不管怎样,您都应该学习COM接口,这正是我们在本章中要做的。
大多数情况下,OpenTools接口都是位于Source\ToolsAPI\ToolsAPI.pas单元中的COM接口。为提高后向兼容性,该目录下也定义了风格较老的Delphi接口。表A.1完整地列出了ToolsAPI中的所有单元。带有星号的单元包含了风格较老的Delphi接口,通常应该避免在较新的代码中使用。
警告:很差的是,这些单元在帮助文件中并没有很好的文档。首先要参考单元中的代码;代码中的注释很有帮助,但默认某些知识;而经过仔细查找,我们发现几乎完全没有集成化的帮助。这真是个不幸,如果要进行扩展,您必须阅读许多代码并进行实验。
表A.1 Delphi ToolsAPI单元列表。通过实现ToolsAPI.pas单元中
定义的COM接口,可以访问Delphi的大部分功能
单元 |
描述 |
toolsapi.pas |
包含了新的COM接口,它替换了在其他单元中可以找到的风格较老的接口(本章中将广泛地使用该单元的接口) |
vcsintf.pas |
包含了与版本控制系统进行链接的COM接口 |
dsgnintf.pas |
包含了特性编辑器、组件编辑器以及注册过程所需的接口(例如,RegisterComponentEditor) |
editintf.pas* |
风格较老的Delphi抽象接口,用于访问编辑器缓存,例如单元的文本 |
exptintf.pas* |
风格较老的单元,其中包括了用于定义专家的抽象虚类TIExpert;新代码应使用ToolsAPI单元中的COM接口 |
fileintf.pas* |
风格较老的单元,其中包括了用于访问文件系统功能的抽象虚接口 |
istreams.pas* |
包含了流、内存流、文件流的接口 |
toolintf.pas* |
与Delphi菜单和ToolServices相关的接口;在新代码中应使用ToolsAPI单元中的BorlandIDEServices COM对象以及IOTAMenuWizard |
virtinft.pas* |
包含了TInterface的定义,以及Delphi对基本的COM接口IUnknown的实现 |
注意:本章中可能会交替使用向导和专家这两个词。它们都是指Delphi中的专家。之所以使用两个词,是因为Inprise也并未确定使用单个词。注册过程使用向导这个词,而COM接口也包含了向导这个词。在Delphi中进行讨论时,对这两个词进行区分是没有意义的。
现在已经无需了解进一步的细节了,我们来创建一个Delphi专家。
对Delphi向导进行扩展的最为直接的途径就是实现IOTAWizard和IOTAMenuWizard接口。这两个接口都定义在ToolsAPI单元中,而且您可以看到,它们非常容易实现。
注意:首字母缩略词前缀IOTA指的是Interface for OpenTools API(我是这样认为的!),它也可能是指一幕希腊剧,意思是指非常小的数量(因为只有很少量的代码需要实现)。
实现IOTAWizard和IOTAMenuWizard
最容易实现的向导是非常基本的IOTAWizard接口,它使用IOTAMenuWizard类来实现。IOTAWizard接口需要实现四个方法,而IOTAMenuWizard则把一个菜单项放置到Help菜单上。由于刚刚起步,我们将以向导的形式实现一个Hello World例子。为使读者不至于失望,将在下一节实现一个较为有用的向导。
下面的代码定义IOTAWizard和IOTAMenuWizard。实现基本的向导并显示在Help菜单上,需要实现IOTAWizard接口的四个方法:GetIDString、GetName、GetState和Execute。由于IOTAWizard继承了IOTANotifier接口,您还需要实现IOTANotifier接口。可以使用TNotifierObject存根类作为IOTANotifier接口的实现。IOTANotifier接口引入了AfterSave、BeforeSave、Destroyed和Modified方法,以便对事件进行响应。对这个练习而言,该存根类就足够了。IOTAMenuWizard继承了IOTAWizard接口。在IOTAMenuWizard类中,惟一需要实现的方法是GetMenuText,该方法返回在Help菜单上显示的文本。
IOTAWizard = interface(IOTANotifier)
['{B75C0CE0-EEA6-11D1-9504-00608CCBF153}']
{ Expert UI strings }
function GetIDString: string;
function GetName: string;
function GetState: TWizardState;
{ Launch the AddIn }
procedure Execute;
end;
IOTAMenuWizard = interface(IOTAWizard)
['{B75C0CE2-EEA6-11D1-9504-00608CCBF153}']
function GetMenuText: string;
end;
这个没有实际功能的向导定义为TDummyWizard类,该类是TNotifierObject、IOTAWizard以及IOTAMenuWizard的子类。它实现了上面代码所列出的接口中的五个方法。完整的实现代码如下。
unit UDummyWizard;
// UDummyWizard.pas - Demonstrates basic wizard interface
// Copyright (c) 2000. All Rights Reserved.
// By Software Conceptions, Inc. http://www.softconcepts.com
// Written by Paul Kimmel. Okemos, MI USA
interface
uses
Windows, ToolsAPI;
type
TDummyWizard = class(TNotifierObject, IOTAWizard,
IOTAMenuWizard)
public
function GetIDString : String;
function GetName : String;
function GetState : TWizardState;
procedure Execute;
function GetMenuText : String;
end;
procedure Register;
implementation
uses
Dialogs;
procedure Register;
begin
RegisterPackageWizard(TDummyWizard.Create);
end;
{ TDummyWizard }
procedure TDummyWizard.Execute;
begin
MessageDlg( 'Building Delphi 6 Applications', mtInformation,
[mbOk], 0 );
end;
function TDummyWizard.GetIDString: String;
begin
result := 'SoftConcepts.DummyWizard';
end;
function TDummyWizard.GetMenuText: String;
begin
result := 'Dummy Wizard';
end;
function TDummyWizard.GetName: String;
begin
result := 'Dummy Wizard';
end;
function TDummyWizard.GetState: TWizardState;
begin
result := [wsEnabled];
end;
end.
Register过程以TDummyWizard的一个实例为参数调用了RegisterPackageWizard。您可以像安装组件一样把专家安装到包中,如上例。实际上,进行安装最容易的方法就是使用Delphi中的Component | Install Component菜单项。当用户单击添加的菜单项时,即可调用这个非常基本的向导。当单击菜单项时,将调用向导实现的Execute方法来响应。TDummyWizard在一个TMessageDlg对话框中显示本书的标题。当然,如果您确定的话,可以在Execute方法中加入几乎任何级别的复杂行为。GetIDString方法返回向导的字符串标识符。按照惯例,该ID的前缀是您公司的名字,这里使用了Software Concepts, Inc公司的注册商标SoftConcepts,并将其通过圆点连接到向导的名字。
GetMenuText的实现代码中包含了显示在帮助菜单上的菜单项文本。当每次单击Delphi的Help菜单上相应菜单项时,都会调用该方法。GetName方法返回向导的名字,而GetState方法则返回TWizardState类型值。该类型定义如下:
TWizardState = set of [wsEnabled, wsChecked];
wsEnable表示该向导是否是活动的,而wsChecked值则在菜单项上放置一个检查标记。从代码可以看到,基本的Help菜单向导所需的代码非常少。当安装向导后,Delphi的帮助菜单上出现了一个新的菜单项Dummy Wizard。当用户单击向导时,将调用Execute方法,从而在TMessageDlg对话框上显示文本“Building Delphi 6 Applications”。关于相应的菜单项和单击后的反应,可以参见图A.1和A.2。
图A.1 Dummy Wizard添加到Delphi的Help菜单
图A.2 当单击Dummy Wizard菜单项时,将显示TMessageDlg
对话框,其代码可以参见Execute方法
注册向导
把向导添加到包,并像组件一样对其进行安装,即可扩展Delphi。当把包编译为BPL库之后,将调用上一节的Register过程来进行安装。如上一小节的代码所示,RegisterPackageWizard需要向导的一个实例作为参数。RegisterPackageWizard定义在ToolsAPI.pas单元中,其参数为IOTAWizard类型的常量引用,该过程声明如下:
procedure RegisterPackageWizard(const Wizard: IOTAWizard);
要安装向导,可以按照下列步骤进行。
1. 在Delphi中,单击Component | Install Component菜单项。
2. 在Install Component对话框中,如果单元尚未显示在Unit file name域中,则单击Browse按钮找到相应的单元。
3. 如果要在当前包中安装专家,单击OK。否则单击Into new package属性页(见图A.3),并给出包的名字及描述。
图A.3 图中为Install Component向导,用于将向
导安装到包。所需步骤与安装组件时相同
4. 当单击OK后,给出的包将在包编辑器中打开(见图A.4)。单击Compile按钮(见图A.4)。
5. 对包进行编译之后,Install已经可用,单击该按钮。
要记住,包在本质上是动态链接库,也是一种应用程序。因此,可以而且应该像其他程序一样对选项进行设置。要加入路径和版本消息,并记得设置对所处的开发阶段可用的编译器选项。可以参考前面的第18章,在测试时使用运行时错误和调试选项,而在测试结束后、应用程序打包之前去掉这些选项。
图A.4 包编辑器用于编译并安装包
像New Component Wizard这样有用的向导,可以减少编写代码的数量,使得不必手工编写一些可以自动完成的代码;而且有助于开发者在越过障碍之后发现Delphi的一些新的功能。为保持向导的这种功能,本节给出了一个New Expert专家;正如同New Component对话框跳过了开始组件单元的步骤一样,New Expert的作用也是类似的。
New Expert的功能是,它可以生成一些与本章开头的Dummy Wizard类似的专家。而New Expert本身将安装在Component菜单上New Component菜单项之后。要建立该向导并将其安装到Component菜单上,我们需要实现IOTACreator和IOTAModuleCreate,而且还需要查询BorlandIDEServices以获得INTAServices40。INTAServices40对象定义了向特定的Delphi菜单添加菜单项的行为。
向导的类是TNewExpertWizard。当单击Component | New Expert菜单项(见图A.5)时,它生成一个与TDummyWizard几乎相同的类。将特定的行为添加到生成的Execute方法,然后就可以了。完整的代码列表如下,对相关部分的描述分为小节,以便使您能够清楚地了解其作用。
图A.5 将New Expert向导添加到INTAServices40对象之后
unit UNewExpertWizard;
// UNewExpertWizard.pas - An example of a wizard that generates
the code for a wizard
// Copyright (c) 2000. All Rights Reserved.
// By Software Conceptions, Inc. http://www.softconcepts.com
// Written by Paul Kimmel. Okemos, MI USA
interface
uses
Windows, Controls, ToolsAPI, Forms, Menus, Classes, SysUtils;
type
TNewExpertWizard = class(TNotifierObject, IOTAWizard,
IOTACreator,
IOTAModuleCreator)
private
FNewClassName : string;
FMenuText : string;
FExpertIDString : string;
FExpertName : string;
FUnitName : String;
FWizardState : TWizardState;
FMenuItem : TMenuItem;
procedure AddMenuItem;
procedure OnClick( Sender : TObject );
procedure GenerateCode;
public
constructor Create; virtual;
destructor Destroy; override;
{ IOTAWizard }
function GetIDString : String;
function GetName : String;
function GetState : TWizardState;
procedure Execute;
{ IOTACreator }
function GetCreatorType : string;
function GetExisting : Boolean;
function GetFileSystem : string;
function GetOwner : IOTAModule;
function GetUnnamed : Boolean;
{ IOTAModuleCreator }
function GetAncestorName : string;
function GetImplFileName : string;
function GetIntfFileName : string;
function GetFormName : string;
function GetMainForm : Boolean;
function GetShowForm : Boolean;
function GetShowSource : Boolean;
function NewFormFile( const FormIdent, AncestorIdent : string
) : IOTAFile;
function NewImplSource( const ModuleIdent, FormIdent,
AncestorIdent : string ) : IOTAFile;
function NewIntfSource( const ModuleIDent, FormIdent,
AncestorIdent : string ) : IOTAFile;
procedure FormCreated( const FormEditor : IOTAFormEditor );
end;
procedure Register;
implementation
uses UFormMain, Dialogs, UExpertUnit;
{$R *.RES}
procedure Register;
begin
RegisterPackageWizard(TNewExpertWizard.Create);
end;
{ TNewExpertWizard }
constructor TNewExpertWizard.Create;
begin
inherited;
AddMenuItem;
end;
destructor TNewExpertWizard.Destroy;
begin
if( Assigned(FMenuItem)) then
FMenuItem.Free;
inherited;
end;
procedure TNewExpertWizard.OnClick( Sender : TObject );
begin
Execute;
end;
procedure TNewExpertWizard.AddMenuItem;
var
NTAServices40 : INTAServices40;
ComponentMenuItem : TMenuItem;
begin
NTAServices40 := BorlandIDEServices As INTAServices40;
if( Not Assigned(NTAServices40)) then exit;
ComponentMenuItem :=
NTAServices40.MainMenu.Items.Find('&Component');
if( Not Assigned( ComponentMenuItem)) then Exit;
FMenuItem := TMenuItem.Create( ComponentMenuItem );
try
FMenuItem.Caption := 'New &Expert...';
FMenuItem.OnClick := OnClick;
ComponentMenuItem.Insert( 1, FMenuItem );
except
FreeAndNil(FMenuItem);
end;
end;
procedure TNewExpertWizard.Execute;
var
Form : TFormMain;
begin
Form := TFormMain.Create(Application);
try
if( Form.ShowModal = mrOK ) then
begin
FNewClassName := Form.NewClassName;
FMenuText := Form.MenuText;
FExpertIDString := Form.ExpertIDString;
FExpertName := Form.ExpertName;
FUnitName := Form.UnitName;
FWizardState := Form.WizardState;
GenerateCode;
end;
finally
Form.Free;
end;
end;
function TNewExpertWizard.GetIDString: String;
begin
result := 'SoftConcepts.NewExpertWizard';
end;
function TNewExpertWizard.GetName: String;
begin
result := 'Expert';
end;
function TNewExpertWizard.GetState: TWizardState;
begin
result := [wsEnabled];
end;
function TNewExpertWizard.NewImplSource(const ModuleIdent,
FormIdent,
AncestorIdent: string): IOTAFile;
begin
result := TExpertUnit.Create( FNewClassName, FMenuText,
FExpertIDString,
FExpertName, FUnitName, FWizardState );
end;
procedure TNewExpertWizard.GenerateCode;
begin
(BorlandIDEServices as IOTAModuleServices).CreateModule(Self);
end;
procedure TNewExpertWizard.FormCreated(const FormEditor:
IOTAFormEditor);
begin
// Intentionally left blank
end;
function TNewExpertWizard.GetAncestorName: string;
begin
result := '';
end;
function TNewExpertWizard.GetFormName: string;
begin
result := '';
end;
function TNewExpertWizard.GetImplFileName: string;
begin
result := FUnitName;
end;
function TNewExpertWizard.GetIntfFileName: string;
begin
result := '';
end;
function TNewExpertWizard.GetMainForm: Boolean;
begin
result := False;
end;
function TNewExpertWizard.GetShowForm: Boolean;
begin
result := False;
end;
function TNewExpertWizard.GetShowSource: Boolean;
begin
result := True;
end;
function TNewExpertWizard.NewFormFile(const FormIdent,
AncestorIdent: string): IOTAFile;
begin
result := Nil;
end;
function TNewExpertWizard.NewIntfSource(const ModuleIDent,
FormIdent,
AncestorIdent: string): IOTAFile;
begin
result := Nil;
end;
function TNewExpertWizard.GetCreatorType: string;
begin
result := sUnit;
end;
function TNewExpertWizard.GetExisting: Boolean;
begin
result := False;
end;
function TNewExpertWizard.GetFileSystem: string;
begin
result := '';
end;
function TNewExpertWizard.GetOwner: IOTAModule;
begin
result := nil;
end;
function TNewExpertWizard.GetUnnamed: Boolean;
begin
result := False;
end;
end.
New Expert的类定义
TNewExpertWizard类继承了TNotifierObject存根类,以及IOTAWizard、IOTACreator和IOTAModuleCreator接口。IOTAWizard是基本的向导接口,而TNotifierObject则对该接口中某些基本的事件处理程序实现了一个存根。要创建一个基本的向导,您得实现IOTAWizard接口。IOTACreator和IOTAModuleCreator用于与Delphi的文件视图协同工作,它们也包含了创建窗体和单元的能力。过一会儿我们继续讨论各个接口的实现。
向导类的私有部分包含了几个字段,用于正确地创建单元。FNewClassName存储将要生成的向导的类名。而FMenuText则存储要生成的向导的菜单文本。FExpertIDString包含了专家的ID字符串。FExpertName字段包含了专家名。FUnitName是生成的.PAS单元的名字,而FWizardState包含了wsEnabled和wsChecked值。上述的每个特性都用于生成对接口的IOTAWizard部分的基本响应。例如,IOTAMenuWizard需要用所显示的菜单文本来响应。代码生成器将从IOTAMenuWizard.GetMenuText得到FMenuText的值。私有部分还有FMenuItem,该字段用于维护对向导所添加菜单的引用。菜单项是通过构造函数中调用的AddMenuItem过程添加到Delphi的。OnClick事件处理程序包含了当用户单击New Expert菜单项时(见图A.6)的响应代码。
当用户填写好如图A.6所示的New Expert对话框之后,将调用私有方法GenerateCode。对话框中询问了一些用于完成专家的必要的问题。专家类的名字是什么?用于触发Execute方法的菜单项的文本是什么?创建的ID字符串是什么?专家的名字是什么?专家所对应的单元名是什么?Wizard State中的复选框用于生成GetState的实现代码。
类的公有部分包括一些方法的声明,这些方法是必须实现的,以履行接口继承所形成的契约。另外,除了从接口继承的方法之外,还包括构造函数和析构函数。构造函数将New Expert菜单项添加到Delphi,而析构函数负责释放相应的内存。
图A.6 用于生成Delphi专家的New Expert对话框
实现IOTAWizard IOTAWizard接口需要实现GetIDString、GetName、GetState以及Execute方法。GetIDString方法返回‘SoftConcepts.NewExpertWizard’。按照惯例,ID字符串包括公司名和向导名,用圆点连接。GetName返回‘Expert’,这是该向导所显示的名字。GetState返回包含wsEnabled的TWizardState集合,确保Component菜单上的向导是可用的。
IOTAWizard接口中惟一较为困难的方法是Execute。当单击New Expert菜单项时(见图A.5),将调用Execute方法。Execute方法显示一个对话框,如图A.6所示。填写New Expert对话框的所有域,然后单击OK。由New Expert对话框得到的数据存储在相关的私有字段中,然后调用GenerateCode方法。GenerateCode调用IOTAModuleServices.CreateModule方法。由于TNewExpertWizard继承了IOTACreator,因此它可用作CreateModule的参数。接着,CreateModule调用IOTAModuleCreator的方法,包括用于生成代码的NewImplSource方法(参考“向Delphi的菜单添加菜单项”一节,那里提供了关于如何从COM对象BorlandIDEServices查询ToolsAPI服务的简要讨论)。
实现IOTACreator IOTACreator定义了用于与Delphi的文件系统视图协同工作的接口。IOTACreator用于为生成代码提供方便。对于IOTACreator,我们实现了GetCreatorType、GetExisting、GetFileSystem、GetOwner以及GetUnnamed方法。
从本节开头列出的代码可用看出,这些方法相对较为直观。GetCreatorType返回ToolsAPI.pas中定义的sUnit。常量sUnit包含了值‘Unit’,表示将创建单元。GetExisting返回False,因为我们正在创建新的单元;如果引用已有的单元,那么该方法将返回True。GetFileSystem返回TFileSystem对象的ID字符串,向导将使用该对象来读写文件。它并不是必须的,因此该方法返回了空字符串。
GetOwner返回对拥有模块的引用。例如要将该模块添加到一个已有的工程,我们需要查询BorlandIDEServices以获得已有的工程或包。我们将使用包编辑器来将新的专家添加到某个特定的包。GetOwner方法可以返回Nil。最后是GetUnnamed方法,如果我们返回未命名的单元,该方法将返回True。如果GetUnnamed返回True,Delphi将在第一次保存单元时提示用户输入文件名。如图A.6的New Expert对话框所示,我们已经提供了单元名。
要自动生成模块并添加到打开的包,您可以修改GetOwner方法,查找活动工程组并将其作为GetOwner的结果返回。下面的代码就足够了。
function TNewExpertWizard.GetOwner: IOTAModule;
var
ModuleServices : IOTAModuleServices;
ProjectGroup : IOTAProjectGroup;
I : Integer;
begin
result := Nil;
ModuleServices := BorlandIDEServices As IOTAModuleServices;
for I := 0 to ModuleServices.ModuleCount - 1 do
begin
with ModuleServices.Modules[I] do
if( Pos( '.bpg', FileName ) > 0 ) then
if( QueryInterface( IOTAProjectGroup, ProjectGroup ) =
S_OK )
then
begin
result := ProjectGroup.GetActiveProject;
exit;
end;
end;
end;
ModuleServices对象是由BorlandIDEServices对象返回的。将查找所有的模块,以找到包含‘.bpg’包扩展名的模块。当找到一个包以后,QueryInterface会测试该模块是否实现了IOTAProjectGroup接口。如果已经实现了该接口,那么将把ActiveProject作为函数结果返回。
实现IOTAModuleCreator 在这个练习中,IOTAModuleCreator包含的方法数目是最多的。为与IOTA前缀的来源相一致,这些方法也都相对直观而易于实现。表A.2包含了用于实现IOTAMoudleCreator接口的方法。虽然该表较小,但也够用了,因为描述都相对较短。
表A.2 为IOTAModuleCreator接口所实现的方法。对New Expert向导
最重要的是NewImplSource方法,该方法返回生成的源代码
接口方法 |
描述 |
GetAncestorName |
返回模块所继承的祖先名;我们使用了空字符串 |
GetFormName |
该函数返回窗体名;由于并不生成窗体,所以我们也返回空字符串 |
GetImplFileName |
返回由New Expert对话框读取的、存储在FUnitName中的单元名 |
(续表)
接口方法 |
描述 |
GetIntfFileName |
CPP头文件名,这里返回空字符串,要记得Delphi与C++ Builder共享VCL代码 |
GetMainForm |
由于本模块并非主窗体,所以GetMainForm返回False |
GetShowForm |
由于该向导并不与窗体相关联,所以仍然返回False |
GetShowSource |
由于我们需要完成生成的专家向导中的Execute行为,因此返回True以显示新的单元 |
NewFormFile |
返回IOTAFile对象,表示窗体文件的实例;如果生成的专家不需要窗体,则返回Nil |
NewImplSource |
这是个关键性的方法:该方法返回IOTAFile的子类,包含了生成的专家的源代码。从代码可以看到,该方法返回了TExpertUnit类的实例(在“建立代码生成器”一节中,我们将继续讨论TExpertUnit单元) |
NewIntFSource |
该方法返回C++头文件的源代码;它与我们的目的是不相关的 |
FormCreated |
FormCreated是有窗体创建时所调用的事件方法;由于New Expert向导没有窗体,因此该事件处理程序是空白的 |
现在我们已经实现了向导,并涵盖了所有用于定义向导的接口,接下来我们需要讨论使用BorlandIDEServices对象添加菜单项。最后我们将实现生成代码的IOTAFile部分以及IOTARepository接口,并结束本节。存储库接口将把我们创建的向导放置到New Items对话框中,这样可以使得它与New Component功能相一致。
BorlandIDEServices全局变量定义在ToolsAPI.pas单元中。当对COM对象使用as操作符时,它与向一个对象查询是否支持某接口是等效的。例如,如果BorlandIDEServices实现了IOTAModuleServices接口,BorlandIDEServices as IOTAModuleServices将返回IOTAModuleServices的实例。可以使用BorlandIDEServices对象来访问所有与ToolsAPI相关的COM对象,名字形如somenameservices。
下面的代码片断摘自定义New Expert向导一节。
procedure TNewExpertWizard.AddMenuItem;
var
NTAServices40 : INTAServices40;
ComponentMenuItem : TMenuItem;
begin
NTAServices40 := BorlandIDEServices As INTAServices40;
if( Not Assigned(NTAServices40)) then exit;
ComponentMenuItem :=
NTAServices40.MainMenu.Items.Find('&Component');
if( Not Assigned( ComponentMenuItem)) then Exit;
FMenuItem := TMenuItem.Create( ComponentMenuItem );
try
FMenuItem.Caption := 'New &Expert...';
FMenuItem.OnClick := OnClick;
ComponentMenuItem.Insert( 1, FMenuItem );
except
FreeAndNil(FMenuItem);
end;
end;
我们首先需要通过BorlandIDEServices获取对INTAServices40接口的访问权限。INTAServices40可以访问Delphi的主菜单。TMainMenu.Items.Find方法可用于获取Items集合中Component菜单的引用。加速键字符&会被忽略,使用与否都是可以的。两种情况下find都能正确工作。如果找到了Component菜单,将创建一个新的菜单项并将其赋值给FMenuItem字段变量。再初始化菜单项的Caption和OnClick特性,然后插入菜单项。当以索引位置1插入菜单项时,该菜单项将出现在New Component菜单项之后。最后,如果发生异常,将释放FMenuItem对象。
假定其他的开发者经常添加专家(现在他们已经创建了New Expert专家,这是可能的)。为确保您的专家总是放在正确的位置上,您可以使用一点搜索逻辑来查找相对于另一菜单的正确位置。下面的修改确保了New Expert总是出现在New Component菜单项之后(原来的代码是ComponentMenuItem.Insert( 1, FMenuItem);)。
var
I : Integer; // added an integer variable
NewComponentMenuItem : TMenuItem;
// original variables
begin
// … original code
NewComponentMenuItem := ComponentMenuItem.Find('New Component…');
I := ComponentMenuItem.IndexOf( NewComponentMenuItem );
ComponentMenuItem.Insert( I + 1, FMenuItem );
修改后的版本替换掉了原来对Insert的单独一行的调用。
ComponentMenuItem.Insert(
ComponentMenuItem.IndexOf(
ComponentMenuItem.Find('Install Component...')) + 1,
FMenuItem );
当然,如果使用原来的版本,可以添加一个注释,而且不需要额外的局部变量I和NewComponentMenuItem。
在很大程度上,每个向导都包含了一些相同的基本代码。新的向导需要一个单元,其中包括单元名、接口和实现部分、引用ToolsAPI单元的uses子句、注册过程、以及对IOTAWizard和IOTAMenuWizard接口的实现。
有个方便的方法能够定义要生成的代码,即使用源代码的参数化的字符串,并将其放置在资源文件中。当通过IOTAModuleCreator.CreateModule方法创建IOTAFile的后代类的实例时,该实例可读出资源字符串并对参数化的空白部分进行填充。TExpertUnit类就是这样实现的。我们首先看一下资源文件的定义。
定义代码资源文件
可以将源代码定义为常量,但使用资源文件更为可取。定义源代码的过程可分为四步。第一步,我们从Demos\Experts目录下的例子中找一些源代码,然后创建名为codegen.txt的文本文件,其中包括参数化的源代码。通过使用参数,可以用Delphi的format函数插入实际的值。第二步:创建资源代码文件,资源编译器将使用该文件生成codegen.res,这就是要链接到New Expert 向导的资源文件。第三步:使用Brcc32.exe资源编译器编译资源文件。第四步:使用{$R CODEGEN.RES}编译指令,以确保将codegen.res链接到程序中。
下面列出了参数化的源代码。仔细观察,可以注意到该文本与TDummyWizard非常相似。实际上,它就是从TDummyWizard向导复制过来的,一些因向导而异的关键值使用参数进行了替换。例如,单元名替换为%0:s。因此,如果对资源字符串使用Format函数,那么使用%0:s之处将替换为Format调用中的第一个字符串参数,如此等等。
unit %0:s;
// %0:s.pas - Raison d^ etre
// Copyright (c) 2000. All Rights Reserved.
// By Your Company Name Here, Inc. http://www.yourwebsite.com
// Written by Your Name Here. City, State USA
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
ToolsAPI;
type
%1:s = class(TNotifierObject, IOTAWizard, IOTAMenuWizard)
public
function GetIDString : String;
function GetName : String;
function GetState : TWizardState;
procedure Execute;
function GetMenuText : String;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterPackageWizard(%1:s.Create);
end;
{ %1:s }
procedure %1:s.Execute;
begin
MessageDlg( 'Add execute behavior here!', mtInformation, [mbOK],0);
end;
function %1:s.GetIDString: String;
begin
{By Convention CompanyName.WizardName}
result := '%5:s';
end;
function %1:s.GetMenuText: String;
begin
{ Add menu text here! }
result := '%3:s';
end;
function %1:s.GetName: String;
begin
{ Add wizard name here! }
result := '%4:s';
end;
function %1:s.GetState: TWizardState;
begin
result := [%2:s];
end;
end.
CODEGEN.RC .RC(资源代码)文件包含了编译后资源文件的名字、表示该资源项所引用的数据种类的宏值,而CODEGEN.TXT文件将使用资源编译器集成进来。
CODEGEN RCDATA CODEGEN.TXT 要编译资源文件,打开DOS命令行并将当前目录改变到包含CODEGEN.RC和CODEGEN.TXT文件的目录。确认系统中的path语句包含了含有brcc32.exe的目录(如果并非如此,brcc32.exe位于Delphi目录下的Bin子目录中)。运行brcc32 codegen.rc。资源编译器将输出codegen.res。
使用资源指令 在包含IOTAFile实现的UExpertUnit.pas模块中可以看到,资源指令引用了CODEGEN.RES文件。TExpertUnit.GetSource将读入资源文件并进行格式化。
实现代码生成器单元:TExpertUnit
代码生成器是一个实现了IOTAFile接口的TInterfaceObject对象。实现IOTAFile接口只需两个方法:GetSource和GetAge。很大程度上,我们只需从CODEGEN.RES读入参数化文本,然后使用从New Expert对话框获取的值填写其中的参数。完整的代码如下列出。
unit UExpertUnit;
// UExpertUnit.pas - Contains the expert unit generator
// Copyright (c) 2000. All Rights Reserved.
// By Software Conceptions, Inc. http://www.softconcepts.com
// Written by Paul Kimmel. Okemos, MI USA
interface
{ UnitName is %0, NewClassName is %1, WizardState is %2,
MenuText is %3,
and ExpertName (WizardName) is %4 as defined in CODEGEN.TXT.
PTK
}
uses
ToolsAPI, Windows;
type
TExpertUnit = class( TInterfacedObject, IOTAFile )
private
FNewClassName : String;
FMenuText : String;
FExpertIDString : String;
FExpertName : String;
FUnitName : String;
FWizardState : TWizardState;
function WizardStateString : string;
public
constructor Create( const NewClassName, MenuText,
ExpertIDString,
ExpertName, UnitName : String; WizardState : TWizardState );
function GetSource : string;
function GetAge : TDateTime;
end;
implementation
uses
SysUtils, Dialogs;
{$R CODEGEN.RES}
{ TExpertUnit }
constructor TExpertUnit.Create(const NewClassName, MenuText,
ExpertIDString, ExpertName, UnitName: String; WizardState:
TWizardState);
begin
inherited Create;
FNewClassName := NewClassName;
FMenuText := MenuText;
FExpertIDString := ExpertIDString;
FExpertName := ExpertName;
FUnitName := UnitName;
FWizardState := WizardState;
end;
function TExpertUnit.GetAge: TDateTime;
begin
result := -1;
end;
function TExpertUnit.WizardStateString : string;
begin
if( wsEnabled in FWizardState ) then
result := 'wsEnabled';
if( wsChecked in FWizardState ) then
if( Length(result) > 0 ) then
result := result + ', wsChecked'
else
result := 'wsChecked';
end;
function TExpertUnit.GetSource : string;
var
Text : string;
Instance : THandle;
HRes : HRSRC;
UnitName : String;
begin
Instance := FindResourceHInstance(HInstance);
HRes := FindResource( Instance, 'CODEGEN', RT_RCDATA );
Text := PChar(LockResource(LoadResource(Instance, HRes)));
SetLength( Text, SizeOfResource(Instance, HRes));
UnitName := ExtractFileName(FUnitName);
if( Pos( '.', UnitName ) > 0 ) then
UnitName := Copy( UnitName, 1, Pos('.', UnitName ) - 1);
Result := Format( Text,
[UnitName, FNewClassName, WizardStateString, FMenuText,
FExpertName,
FExpertIDString]);
end;
end.
注意:在本书光盘可以找到New Expert向导的输入窗体的完整代码。其代码量非常少。要实现该窗体,只需按照图A.6添加标签和编辑控件,当用户单击OK按钮时读取相应的值即可。
注意到TExpertUnit的构造函数将所有从New Expert窗体获取的值都作为参数。这些值都被复制到类的本地变量中。CreateModule将调用TNewExpertWizard实现的NewImplSource方法。NewImpleSource返回创建的IOTAFile实例,然后调用GetSource方法来生成代码。
GetSource只是用FUnitName填充参数%0,FNewClassName填充参数%1,FWizardState转换为字符串并填充参数%2,FMenuText填充参数%3,FExpertName填充参数%4,FExpertIDString填充参数%5,然后返回参数化文本。例如,资源文本:
unit %0:s;
替换参数%0后成为:
unit filename;
其中filename是FUnitName字段的文件名部分。所有这些都发生于GetSource的最后一行。
GetSource方法的其余部分执行了一些必要的步骤从CODEGEN.RES装载资源字符串。FindResourceHInstance使用HInstance模块句柄作为参数,并返回模块的资源句柄。FindResource的参数包括资源句柄、资源名CODEGEN以及资源类型。RT_RCDATA是定义在Windows.pas中的常量,它返回MakeIntResource(10)的值。RCDATA是对应原始二进制数据的资源值。CODEGEN.RC文件包含了CODEGEN RCDATA CODEGEN.TXT,该文件描述了我们要装载的资源。下面一行代码:
Text := Pchar(LockResource(LoadResource(Instance, HRes)));
以内层的函数调用开始,首先把资源装载到全局内存中,然后锁定资源,并将二进制资源数据转换为PChar。对SetLength的调用重新设置Text变量的大小,以便为资源中读出的数据保留空间。
如果将UNewExpertWizard、UExpertUnit、UFormMain单元添加到一个新的包,然后安装该包,则按照定义,New Expert向导将显示在Component菜单上。回忆一下,我们还可以从New Items对话框启动New Component向导,只需单击File | New | Other菜单项,然后从New Items对话框的New属性页中选择Component即可。为确保完备,我们把New Expert也加入到New Items对话框。
注意:UFormMain指的是New Expert对话框的窗体和单元文件。
为使得New Expert向导在New Items对话框中可用,我们必须继承并实现IOTARepository和IOTAFormWizard接口。
实现存储库接口
要将存储库接口和窗体向导接口包括在内,只需在TNewExpertWizard接口声明的父类列表中添加这两个接口即可,如下所示。
TNewExpertWizard = class(TNotifierObject, IOTAWizard,
IOTACreator,
IOTAModuleCreator, IOTAFormWizard, IOTARepositoryWizard)
由于添加了新的接口,还需要实现相应的接口。IOTARespository声明了四个需要实现的方法:GetAuthor、GetComment、GetPage以及GetGlyph。在TNewExpertWizard类的公有部分添加这四个声明(如代码所示),并实现这几个方法(实现也在TNewExpertWizard的代码中列出)。
type
TNewExpertWizard = class(TNotifierObject, IOTAWizard,
IOTACreator,
IOTAModuleCreator, IOTAFormWizard, IOTARepositoryWizard)
// …
public
// …
{ IOTARepositoryWizard }
function GetAuthor : string;
function GetComment : string;
function GetPage : string;
function GetGlyph : HICON;
end;
implementation
function TNewExpertWizard.GetAuthor: string;
begin
result := 'Paul Kimmel/Building Delphi 6 Applications';
end;
function TNewExpertWizard.GetComment: string;
begin
result := 'Creates a custom expert using ToolsAPI';
end;
function TNewExpertWizard.GetGlyph: HICON;
begin
result := 0;
end;
function TNewExpertWizard.GetPage: string;
begin
result := 'SoftConcepts';
end;
注意:存储库函数会返回用于在New Items对话框的上下文菜单中对各项进行排序的信息,或返回视图从显示大图标转变为显示细节时所要显示的信息。
GetAuthor方法应包含创建向导的人或实体的字符串信息。GetComment包含简要描述该向导的注释。对于现在而言,GetGlyph返回0,即使用默认图标来表示向导;而GetPage则返回该向导所处的属性页。根据定义New Expert向导在New Items对话框中的外观如图A.7所示。双击New Items对话框中的该专家,即可运行。
图A.7 New Items对话框中的TNewExpertWizard向导。通过实现ToolsAPI.pas
中的IOTARepository和IOTAFormWizard接口,即可作到这一点
向New Items对话框添加定制图标
最后需要完成的就是从IOTARepository.GetGlyph方法返回图标的实际句柄。首先需要向资源文件添加一个图标。这可以通过Image Editor完成,与第10章添加组件图标所用的方法和步骤相似。可以按照下列步骤向New Items对话框添加定制图标。
1. 从Delphi的Tools菜单运行Image Editor。
2. 在Image Editor中选择File | New | Resource File菜单项,创建新的资源文件。
3. 单击File | New | Icon菜单项,添加新的图标。选择默认的32×32像素、16色图标。单击OK按钮。
4. 双击该图标,打开新图标的编辑器窗口。
5. 使用Image Editor的绘图工具绘制新的图标,或将已有的图标复制并粘贴到新图标的画布上。
6. 关闭图标编辑器,并将图标重命名为TNEWEXPERTWIZARD。
7. 将资源文件保存为UNewExpertWizard.res。现在,UNewExpertWizard.pas和UNewExpertWizard.res是同名的。
8. 编辑UNewExpertWizard.pas单元,在实现部分的uses子句之后添加资源指令{$R *.RES}。
9. 修改GetGlyph以返回图标的句柄(修改后的代码在第10步之后)。
10. 建立并安装修改后的向导包(结果参见图A.8)。
function TNewExpertWizard.GetGlyph: HICON;
begin
result := LoadIcon(HInstance, 'TNEWEXPERTWIZARD');
end;
当建立并安装New Expert向导之后,New Items对话框将包含新的图标(显示在图A.8中)而不是默认的图标(显示在图A.7中)。
图A.8 New Expert向导的新图标是factory.ico,图标文件位于
Delphi安装目录的Borland Shared\Images\Icons子目录下
组件编辑器可以对特定组件类添加上下文菜单项。当单击该菜单项时,组件编辑器可以通过调用组件的一个方法或显示一个对话框来进行响应,向用户提供了另一种在设计时修改组件的途径。所有的组件编辑器都由TComponentEditor子类化而来。上下文菜单项是通过实现GetVerb、GetVerbCount和ExecuteVerb方法而添加的。注册组件以后,组件的每个实例都会有一个由GetVerb所描述的上下文菜单项。
当调用组件编辑器后,您可以提供简单或复杂的编辑器,也可以在设计时调用所引用组件的方法。所有的组件编辑器实例都通过一个一般性的Component特性,维护了对所关联组件的引用。可以将一般性的组件引用类型转换为特定的组件类,这样就可以调用方法或访问特定的属性。如果修改了组件的特性,然后要调用Designer.Modified方法,以确保组件被更新。
本节我们将创建一个基本的组件及其编辑器,示范如何调用组件的方法,还为第9章创建的名为TLabelExternedFont的阴影标签组件定义了组件编辑器对话框。
出于演示的目的,我们创建了一个简单的TComponent组件,命名为TComponentWithEditor,并添加了一个简单的文本特性。相应的组件编辑器为TMyEditor,其中定义了三个上下文菜单项。第一个菜单项将显示组件的About对话框,第二个将发出蜂鸣声,而第三个将使用InputQuery函数显示输入对话框。如果用户选择第三项,然后在对话框中输入文本并单击OK,将显示该文本并在设计时更新组件的Text特性。完整的代码如下。
unit UComponentWithEditor;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs,
DsgnIntf;
type
TComponentWithEditor = class(TComponent)
private
FText : String;
procedure About;
protected
public
published
property Text : string read FText write FText;
end;
TMyEditor = class(TComponentEditor)
public
procedure ExecuteVerb(Index: Integer); override;
function GetVerb(Index: Integer): string; override;
function GetVerbCount: Integer; override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('PK Misc', [TComponentWithEditor]);
RegisterComponentEditor( TComponentWithEditor, TMyEditor );
end;
{ TComponentWithEditor }
resourcestring
sAboutText = 'Example Component with Editor' + #13#10 +
'Delphi 6 Developer''s Guide' + #13#10 +
'[email protected]' + #13#10 +
'Copyright (c) 2000. All Rights Reserved.';
sAboutMenuText = 'About %s';
procedure TComponentWithEditor.About;
begin
MessageDlg( sAboutText, mtInformation, [mbOK], 0 );
end;
{ TMyEditor }
procedure TMyEditor.ExecuteVerb(Index: Integer);
var
Default : String;
begin
case Index of
0: TComponentWithEditor(Component).About;
1: Beep;
2: if( InputQuery( 'Dialog Example', 'Enter some text:',
Default )) then
begin
ShowMessage( Format( 'You entered %s', [Default] ));
TComponentWithEditor(Component).Text := Default;
Designer.Modified;
end;
end;
end;
function TMyEditor.GetVerb(Index: Integer): string;
begin
case index of
0: result := Format( sAboutMenuText, [Component.ClassName] );
1: result := 'Beep';
2: result := 'Input Text';
end;
end;
function TMyEditor.GetVerbCount: Integer;
begin
result := 3;
end;
end.
注意:需要在uses子句中包括DesignIntf.pas,该文件可以在Delphifny专业版和企业业版的Source\ToolsAPI目录下找到。
提示:当读到Verb时,如GetVerb,可以想一想菜单。
第一个类TComponentWithEditor定义在单元的接口部分,它有一个私有方法About和一个字段FText。公开部分的特性Text将显示在Object Inspector中。前面提到过,组件编辑器TMyEditor重载了GetVerb、GetVerbCount以及ExecuteVerb方法。GetVerbCount返回值是3。因此有三个零基点的动词需要显示。GetVerb使用case语句,返回要在上下文菜单中显示的文本;您还可以使用数组。参考图A.9,看一下组件的上下文菜单。最后,将被单击的动词(即菜单项)的索引值传递给ExecuteVerb方法,并使用case语句来确定要运行的代码。
提示:我们不需要调用GetVerbCount、GetVerb或ExecuteVerb的继承版本,因为这些方法在父类TComponentEditor中都是空函数。
图A.9 TMyEditor定义了图中显示的上下文菜单的前三项
惟一并不显然的是,当调用ExecuteVerb方法时,可以加入任意级别的复杂行为。组件编辑器到底能有多复杂,一个很好的例子是TChart的组件编辑器,您看一下就知道了。
本例中索引0对应的组件编辑器将Component引用转换为TComponentWithEditor类型,然后调用About方法。索引1对应的是Beep菜单项,它将调用Delphi的Beep过程。如果扬声器连接良好,您可以听到铃声。索引2所对应的菜单项Input Text,将显示InputQuery对话框。输入文本并单击OK。将显示该文本,然后将Component引用转换为组件的实际类型并更新组件的Text特性,最后调用Designer.Modified方法。如果组件可用,您可看到组件的改变反映出来,如果特性是公开的,还可在Object Inspector中看到。
组件编辑器有自身的注册过程。在全局过程Register中添加对RegisterComponentEdit的调用,即可注册组件编辑器。由上一小节的代码可知,第一个参数类型为TComponentClass,第二个参数类型为TComponentEditorClass。在示例代码中,第一个参数是组件的类TComponentWithEditor,第二个参数是编辑器类TMyEditor。
本节所说的阴影标签指的是第9章的TLabelExternedFont组件。该组件的编辑器对话框(如图A.10所示)可用于定制扩展的字体标签,在对话框中把与该目的相关的特性经过组织后显示出来。组件编辑器窗体部分的完整代码位于本书光盘上,它示范了如何使用基本的VCL控件以及一些新的控件,包括TColorBox和TCheckListBox控件。
图A.10 第9章中的TLabelExternedFont组件的编辑器对话框
该组件编辑器的类定义重载了TComponentEditor类的三个基本方法来创建上下文菜单。
TLabelExtendedFontEditor = class(TComponentEditor)
private
procedure EditLabel;
public
function GetVerbCount : Integer; override;
function GetVerb( Index : Integer ) : string; override;
procedure ExecuteVerb( Index : Integer ); override;
end;
私有方法EditLabel用于实现在单击EditLabel菜单项时所需的编辑行为。
procedure TLabelExtendedFontEditor.EditLabel;
var
F : TFormEditor;
begin
F := TFormEditor.Create(Application);
try
F.TestLabel := Component As TLabelExtendedFont;
if( F.ShowModal = mrOK ) then
begin
TLabelExtendedFont(Component).Assign( F.TestLabel );
Designer.Modified;
end;
finally
F.Free;
end;
end;
procedure TLabelExtendedFontEditor.ExecuteVerb(Index: Integer);
begin
case Index of
0: TLabelExtendedFont(Component).About;
1: EditLabel;
end;
end;
function TLabelExtendedFontEditor.GetVerb(Index: Integer):
string;
const
VERBS : array[0..1] of string = ( 'About', 'Edit Label');
begin
result := VERBS[Index];
end;
function TLabelExtendedFontEditor.GetVerbCount: Integer;
begin
result := 2;
end;
前面已经看到过,GetVerbCount的实现是非常直观的,这里它返回2,表示扩展字体标签的上下文菜单上有两个菜单项。两个菜单项分别是About和EditLabel,从GetVerb定义的字符串数组中可以返回这两项。ExecuteVerb调用TLabelExternedFont.About方法或自身的EditLabel方法,这依赖于所传递的索引值。
EditLabel会创建如图A.10所示的窗体的一个实例,该窗体将使用组件编辑器所引用的扩展字体标签组件来初始化编辑器窗体TFormEditor的TestLabel特性。用户可以修改窗体上给出的任意特性。如果用户单击OK,那么将把TFormEditor.TestLabel赋值给所引用的组件。
注意:TLabelExtendedFont组件原来的版本可以在本书光盘第9章中找到,修改后的版本可以在光盘上附录A的目录下找到。其中还包括了一个测试程序,可以将TFormEditor作为一个单独的程序运行。
为方便这个练习,还需要向TLabelExtenedFont组件添加About和Assign方法,并将RegisterComponents调用添加到Register过程。这里列出了修改后的代码,在光盘上也有。
procedure Register;
begin
RegisterComponents('PK Labels', [TLabelExtendedFont]);
RegisterComponentEditor( TLabelExtendedFont,
TLabelExtendedFontEditor );
end;
{ TLabelExtendedFont }
procedure TLabelExtendedFont.About;
resourcestring
sAboutText = 'Extended Font Label Component' + #13#10 +
'Delphi 6 Developer''s Guide' + #13#10 +
'(c) 2000. All Rights Reserved.' + #13#10 +
'Written by Paul Kimmel. [email protected]';
begin
MessageDlg( sAboutText, mtInformation, [mbOK], 0 );
end;
procedure TLabelExtendedFont.Assign( Source : TPersistent );
begin
if( Source Is TLabelExtendedFont ) then
begin
with Source As TLabelExtendedFont do
begin
Self.Caption := Caption;
Self.Transparent := Transparent;
Self.Font.Assign( Font );
Self.FHasShadow := HasShadow;
Self.FShadowColor := ShadowColor;
Self.FShadowDepth := ShadowDepth;
Self.Invalidate;
end
end
else
inherited Assign(Source);
end;
Assign方法将Caption、Font和几个Shadow效果特性从参数中的标签复制到调用者标签。如果要完整的练习一下,可以自行实现组件编辑器的窗体编辑器部分,或从本书光盘上装载来试验一下。
本附录并非是事后聪明。这些高级的功能使Delphi更为强大,并使用户更加乐于使用Delphi。在本附录中,您学到了如何使用OpenTools API来扩展Delphi,以及如何编写组件编辑器。这个附录是真正的关于编写软件的内容,它是为开发者所写的。您不可能每天都作这个,但在需要的情况下,它可以使您从普通的开发者中脱颖而出。
不幸的是,OpenTools API的大部分文档都以代码或注释的形式存在。也许当本书发行时,这些问题已经得到补救。而组件编辑器在集成帮助中的文档就更为完整且易于使用。在空间许可的情况下,我试图提供尽可能多的知识。如果您需要OpenTools API或高级定制组件方面的书籍,请告知出版商。