Delphi Open Tools API 浅探 - 元件编辑器(100分)

  • 主题发起人 主题发起人 savetime
  • 开始时间 开始时间
S

savetime

Unregistered / Unconfirmed
GUEST, unregistred user!
Delphi Open Tools API 浅探 - 元件编辑器

savetime2k@yahoo.com 2004.1.28
http://savetime.delphibbs.com


今天开始学习元件编辑器,感觉比属性编辑器简单许多,但还是遇到了一些疑问。如果你能解答文中记录的问题,请告诉我答案,谢谢!


目 录
===============================================================================
⊙ TBaseComponentEditor class
⊙ IComponentEditor interface
⊙ TComponentEditor class
⊙ TComponentEditor.ExecuteVerb 方法
⊙ TComponentEditor.PrepareItem 方法
⊙ TComponentEditor.Edit 方法
⊙ TComponentEditor.IsInInlined 方法
⊙ TDefaultEditor class
⊙ TSelectionEditor class
===============================================================================

本文排版格式为:
正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。

(作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。)


正 文
===============================================================================
⊙ TBaseComponentEditor class
===============================================================================
TBaseComponentEditor 是所有元件编辑器的基类,它的构造函数由 Delphi IDE 在选中一个元件时被 IDE 调用。所有的元件编辑器必须实现 TBaseComponentEditor.Create 函数和 IComponentEdiotr 接口。元件编辑器的构造函数中传入当前选中的元件和 IDesigner 接口。

{ DesignIntf.pas }
TBaseComponentEditor = class(TInterfacedObject)
public
constructor Create(AComponent: TComponent
ADesigner: IDesigner)
virtual;
end;

(* 为什么要以 TInterfacedObject 为基类呢?)

===============================================================================
⊙ IComponentEditor interface
===============================================================================
IComponentEditor 定义了元件编辑器需要实现的接口:

IComponentEditor = interface
['{ECACBA34-DCDF-4BE2-A645-E4404BC06106}']
procedure Edit;
双击元件时触发
procedure ExecuteVerb(Index: Integer);
执行一个自定义动作
function GetVerb(Index: Integer): string;
获取自定义动作的字符串名称
function GetVerbCount: Integer;
获取自定义动作的数量
procedure PrepareItem(Index: Integer
const AItem: IMenuItem);
自定义动作的菜单项
procedure Copy;
此方法在元件信息被拷贝到剪贴板之后被调用
(* 试验结果:好像不会被调用?)
function IsInInlined: Boolean;
返回元件的 Owner 是否是 csInline 状态
function GetComponent: TComponent;
获取当前选中的元件
function GetDesigner: IDesigner;
获取当前 IDE Designer
end;

===============================================================================
⊙ TComponentEditor class
===============================================================================
TComponentEditor 是自定义元件编辑器的基类,它定义了 IComponentEditor 接口的虚方法,后继类只需要重载这些虚方法。

{ DesignEditors.pas }
TComponentEditor = class(TBaseComponentEditor, IComponentEditor)
private
FComponent: TComponent;
FDesigner: IDesigner;
public
procedure Edit
virtual;
procedure ExecuteVerb(Index: Integer)
virtual;
function GetComponent: TComponent;
function GetDesigner: IDesigner;
function GetVerb(Index: Integer): string
virtual;
function GetVerbCount: Integer
virtual;
function IsInInlined: Boolean;
procedure Copy
virtual;
procedure PrepareItem(Index: Integer
const AItem: IMenuItem)
virtual;
property Component: TComponent read FComponent;
property Designer: IDesigner read GetDesigner;
end;

TComponentEditor 在构造函数中保存当前编辑的元件指针和 IDE 接口指针,可以使用 Component 属性和 Designer 属性访问。

{ TComponentEditor }
constructor Create(AComponent: TComponent
ADesigner: IDesigner);
begin
FComponent := AComponent
// 保存当前元件指针
FDesigner := ADesigner
// 保存当前 IDE 接口
end;

注意:如果元件编译器中的任意方法修改了 Component 元件的属性,必须调用 Designer.Modified 方法通知 IDE 刷新 Object Inspector。

TComponentEditor 需要用 RegisterSelectionEditor 注册后才能使用:

{ DesignIntf.pas }
procedure RegisterComponentEditor(ComponentClass: TComponentClass;
ComponentEditor: TComponentEditorClass);

===============================================================================
⊙ TComponentEditor.ExecuteVerb 方法
===============================================================================
ExecuteVerb 方法和 GetVerbCount / GetVerb 方法互相配合,完成右键点击元件时的快捷菜单操作。

GetVerbCount 返回自定义动作的数量,缺省值为 0,即没有自定义动作。GetVerb 返回第 Index 项的菜单字符串。ExecuteVerb 执行第 Index 项动作。重载这些方法以实现自定义的元件编辑菜单。

{ TComponentEditor }
function GetVerbCount: Integer
virtual;
function GetVerb(Index: Integer): string
virtual;
procedure ExecuteVerb(Index: Integer)
virtual;

注意:如果在 ExecuteVerb 方法中修改了元件属性,必须调用 Designer.Modified 方法通知 IDE 刷新 Object Inspector。

===============================================================================
⊙ TComponentEditor.PrepareItem 方法
===============================================================================
PrepareItem 在 GetVerb 之后被调用,用于建立更复杂的动作菜单项。IDE 传入的 Index 参数是指菜单序号,AItem 参数是新建菜单项的接口句柄。

procedure PrepareItem(Index: Integer
const AItem: IMenuItem)
virtual;

IMenuItems 接口在 DesignMenus.pas 中定义,可以被元件设计者使用的主要方法有:

IMenuItem = interface(IMenuItems)
['{DAF029E1-9592-4B07-A450-A10056A2B9B5}']
// 新增子菜单
function AddItem(const ACaption: WideString
AShortCut: TShortCut;
AChecked, AEnabled: Boolean
AOnClick: TNotifyEvent = nil;
hCtx: THelpContext = 0
const AName: string = ''): IMenuItem
overload;
function AddItem(AAction: TBasicAction;
const AName: string = ''): IMenuItem
overload;
// 新增菜单分隔符
function AddLine(const AName: string = ''): IMenuItem;
// 设置菜单项是否被 Checked
property Checked: Boolean read GetChecked write SetChecked;
// 设置菜单项是否 Enabled
property Enabled: Boolean read GetEnabled write SetEnabled;
// 设置菜单项是否 Visible
property Visible: Boolean read GetVisible write SetVisible;
end;

===============================================================================
⊙ TComponentEditor.Edit 方法
===============================================================================
TComponentEditor.Edit 方法在元件被双击时调用,重载此方法可以实现自定义的属性编辑器。缺省的操作是调用 TComponentEditor 的第一个动作。

{ TComponentEditor }
procedure TComponentEditor.Edit
virtual;
begin
if GetVerbCount > 0 then ExecuteVerb(0)
// 缺省调用第一个动作
end;

注意:如果在 Edit 方法中修改了元件属性,必须调用 Designer.Modified 方法通知 IDE 刷新 Object Inspector。

===============================================================================
⊙ TComponentEditor.IsInInlined 方法
===============================================================================
IsInInlined 判断当前元件是否处于内嵌的 top-class 对象中。例如,如果当前元件位于嵌入在 Form 中的 Frame 中,则返回 True,否则返回 False。IsInInlied 方法不是虚方法,不能被重载。在 TComponentEditor 中它判断 Owner 是否包含 csInline 状态。

{ TComponentEditor }
function TComponentEditor.IsInInlined: Boolean;
begin
Result := csInline in Component.Owner.ComponentState;
end;

(* 这个方法可用在何处?估计是被 IDE 使用。缺省不能在嵌入表单中的 Frame 中插入元件。)

===============================================================================
⊙ TDefaultEditor class
===============================================================================
TDefaultEditor 是 IDE 缺省的元件编辑器,提供对元件双击事件的增强处理。它重载了 Edit 方法实现在双击元件时生成元件的缺省事件代码。

TDefaultEditor 继承自 TComponentEditor 和 IDefaultEditor:

{ DesignEditors.pas }
TDefaultEditor = class(TComponentEditor, IDefaultEditor)

{ DesignIntf.pas }
IDefaultEditor = interface(IComponentEditor)
['{5484FAE1-5C60-11D1-9FB6-0020AF3D82DA}']
end;

IDefaultEditor 没有声明任何方法。(我猜想) 这是为了标识当前的元件编辑器是从 TDefaultEditor 继承下来的,但是为什么不直接使用 TObject.InheritsFrom 来识别呢?

TDefaultEditor 在 Edit 方法中调用 GetComponentProperties 函数获取属性列表。GetComponentProperties 调用 TDefaultEditor.CheckEdit 过滤合适的方法:

{ TDefaultEditor.pas }
procedure CheckEdit(const Prop: IProperty);

CheckEdit 方法将从 GetComponentProperties 传进来的 IProperty 参数传递给 TDefaultEditor.EditProperty 虚方法:

{ TDefaultEditor }
procedure EditProperty(const Prop: IProperty
var Continue: Boolean)
virtual;

EditProperty 使用 IProperty 判断属性的名称是否是:ONCREATE、ONCHANGE、ONCHANGE、ONCLICK 之一,如果是则将该 IProperty 句柄保存在私有成员 FFirst、FBest 之中。EditProperty 还传入一个 Continue 参数,如果设置为 False,则 Edit 方法不会执行任何操作。否则 Edit 方法调用 IProperty 的 Edit 方法,生成事件句柄的代码。

* 可以从 TDefaultEditor 继承以实现特殊的元件编辑操作。

===============================================================================
⊙ TSelectionEditor class
===============================================================================
TSelectionEditor 与 TComponentEditor 类似,它实现 ISelectionEditor 接口,可以为 IDE 中选中的一批不同类型的元件生成动作菜单项。比如,如果选中表单上若干个从 TControl 类派生的不同类型的元件,TSelectionEditor 会被创建。

{ DesignEditors.pas }
TSelectionEditor = class(TBaseSelectionEditor, ISelectionEditor)

TSelectionEditor 的 GetVerb、GetVerbCount、PrepareItem 方法与 TComponentEditor 类似,只是 ExecuteVerb 方法有些不同。因为 TSelectionEditor 实现对一批元件的操作,所以 ExecuteVerb 中多传递了一个元件列表接口句柄:

{ TSelectionEditor }
procedure ExecuteVerb(Index: Integer
const List: IDesignerSelections);

IDesignerSelections 代表当前被选中的一批元件,可被使用的主要属性是 Count 和 Items 数组。Count 表示选中的元件数量,Items 数组表示选中的元件。

{ DesignIntf.pas }
IDesignerSelections = interface
['{7ED7BF30-E349-11D3-AB4A-00C04FB17A72}']
function Add(const Item: TPersistent): Integer;
function Equals(const List: IDesignerSelections): Boolean;
function Get(Index: Integer): TPersistent;
function GetCount: Integer;
property Count: Integer read GetCount;
property Items[Index: Integer]: TPersistent read Get
default;
end;

* 为什么 Items 声明为 TPerisistent 而不是 TComponent ?

TSelectionEditor 还有一个 RequiresUnits 虚方法,需要重载以返回可能使用到的元件所在的单元名称,不太清楚这个方法如何使用,如果只使用标准的 VCL 元件应该不用重载,至少我设计 TControl 的 TSelectionEditor 没有出错。

{ TSelectionEditor }
procedure RequiresUnits(Proc: TGetStrProc)
virtual;

TSelectionEditor 需要用 RegisterSelectionEditor 注册后才能使用:

{ DesignIntf.pas }
procedure RegisterSelectionEditor(AClass: TClass
AEditor:
TSelectionEditorClass);

===============================================================================
⊙ 结 束
===============================================================================
 
好贴

Open Tools 涉及Delphi核心内容,懂这个不是很多阿~~

非常感谢楼主,收藏之!
 
(* 为什么要以 TInterfacedObject 为基类呢?)
似乎只是为了用TInterfacedObject来实现IInterface的三个基本方法,也就是做到接口查询和引用计数吧。
 
补充一下:TBasePropertyEditor也是已TInterfaceObject为基类的,这似乎是使用接口和实现接口的一些规矩:所有接口都从IUnknown或IInterface继承,而IInterface都有三个基本方法:

IInterface = interface
['{00000000-0000-0000-C000-000000000046}']
function QueryInterface(const IID: TGUID
out Obj): HResult
stdcall;
function _AddRef: Integer
stdcall;
function _Release: Integer
stdcall;
end;

所以凡是实现了某个接口的类,必须在自身或父类中实现这三个方法以实现接口查询和引用计数功能。
TInterfaceObject和TComponent都通过两种不同的方式实现了这三个方法,因此如果我们的要实现某个接口的类继承自这俩类,便不用考虑如何实现这三个基本方法。

归纳一句就是,要使用接口,我们的类最好继承自TInterfaceObject或TComponent。
 
to Passion,
你这样点拨一下,使我突然明白了 interface 的用处。
回忆这几天学习的属性编辑器和元件编辑器的实现,它们都是在基类定义构造函数,然后通过接口实现具体的功能。IDE 通过接口查询这些编辑器是否实现了某个接口。这样,如果 Borland 需要增加新的功能,只需要重新设计一个接口,而不用修改原来的类实现,保证 Delphi 升级后用户设计的代码可以正常运行。
使用 interface 不但可以隐藏实现的细节,还主要是为了使功能模块化,解决了面向对象编程中浪费 VMT 空间的问题。
 
我的理解是interface可以用在不同的类结构体系间以封装类之间的关系(这些类我们很可能没法都接触到,所以没法以类的方式直接互相发生关系)。
 
翻开《Delphi4开发大全》,上面说 Open Tools 接口对象遵循 COM 标准,这些接口能在任何支持 COM 的编程语言中使用。看来我应该先学习 COM,再学 Open Tools API。[?]
 
刚才浏览了 TIExpert,单元文件中提示“不推荐”:
unit ExptIntf deprecated;
^^^^^^^^^^
是不是有新的版本,哪里有《Delphi7 开发大全》的电子版下载?
 
又是一篇好文章,真佩服你。
 
楼主能否举一些例子,我想这样理解起来也比较快。
 
* 为什么 Items 声明为 TPerisistent 而不是 TComponent ?
从TComponent继承的可以作为控件被安装到Delphi面板上,而从TPerisistent继承的不行,很多属性一般都从TPerisistent来的。比如说TButton的Constraints属性是TObject->TPersistent->TSizeConstraints。没有必要从TComponent,我想大概是这样子吧。
 
to wjh_wy,
原来 IDesignerSelections 不仅被元件编辑器使用,而且也可以被属性编辑器使用,这时就会遇见你说的情况。谢谢!
The form designer and its property editors and component editors use TDesignerSelections to manage lists of persistent objects in the IDE.
 
刚才看到 yysun 对 interface 的说明:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=596317

来自:yysun, 时间:2001-8-15 20:43:00, ID:598847
Delphi 6 的一个趋势是对 Interface 的应用. 现在 COM/DCOM 用 Interface, SOAP 也用,
CORBA 也用, 不同的协议最后都能统一到 Delphi 的 Object Pascal. 也就是说您用 OP 的 class + Interface
来开发您的 business logic, 通过不同的 Module - WebModule, COMModule, SOAPModule, CORBAModule
就能发布 CGI/ISAPI, COM/DCOM, SOAP Web Servie, CORBA ...

来自:yysun, 时间:2001-9-5 21:53:00, ID:612730
今天读 Design Patterns 文章的时候, 发现 interface 竟是 Design Patterns 理论
所推崇的第一大应用的原则:
Program to an interface and not to an implementation.
The idea behind design patterns is simple-- write down and catalog common interactions
between objects that programmers have frequently found useful

后面还有很多讨论。
 
CSDN 上 Open-Tools API 的好文章:Delphi Open Tools Api实例研究
http://www.csdn.net/develop/article/21/21725.shtm
http://www.csdn.net/develop/article/22/22881.shtm
 
TIExpert这些不是完全面向接口的,是老式的OpenToolsApi的东西,D4以前支持,D5后虽然也可能支持但已经不推荐了。
 
是的 Passion。我看了《Delphi Open Tools Api实例研究》上推荐的网站,知道有几个老式单元已经被淘汰了,新的 Open Tools API 好像全是用 interface。我现在在学 COM,学完 COM 再继续。
可怜我手上只有老的《D4 开发大全》,真想找本 D7 的看看,谁知道电子版的下载地址请告诉我,谢谢了!
 
>>我的理解是interface可以用在不同的类结构体系间以封装类之间的关系(这些类我们很
>>可能没法都接触到,所以没法以类的方式直接互相发生关系)。
同意 Passion 说的,我原来对 interface 的看法错了。
 
这篇笔记价值不是很大,先结束了。
不过,Passion 的贴子对我认识接口有很大的帮助。
 
后退
顶部