Delphi Open Tools API 浅探 - 属性编辑器 |
|
allenchan
資深會員 發表:10 回覆:306 積分:283 註冊:2004-01-06 發送簡訊給我 |
最近在測試 VCL RTTI & Persistent 機制,上網找尋不少資料,發現對岸有幾篇不錯的文章,分享給大家參考: 原文出處:
http://www.delphibbs.com/delphibbs/dispq.asp?LID=2423144 Delphi Open Tools API 浅探 - 属性编辑器 savetime2k@yahoo.com 2004.1.26
http://savetime.delphibbs.com
昨天开始学习 Delphi Open Tools API,第一站是 TPropertyEditor。在阅读 VCL 源码时发现了不熟悉的 interface 操作,只好临时学习了 interface。花了二天时间大概了解了属性编辑器的工作原理,作此笔记留存。希望读者在发现错误时及时通知我,先行谢过。 这段时间我写了几篇笔记,开始二篇还有人参与讨论,后面几篇尚无人提出意见,希望只是过年休息的原因。我把笔记公开是基于两种考虑:一是觉得还有些价值,可以与人分享;二是我现在没有太多时间在一个主题上深究、检察,希望您在阅读的过程中也稍加思考,发现并指出笔记中的错误、遗漏,似乎这样比较符合大富翁的精神。 目 录 =============================================================================== ⊙ RegisterPropertyEditor 函数 ⊙ TPropertyEditor 的工作过程 ⊙ IProperty interface ⊙ IProperty70 interface ⊙ TPropertyEditor.GetAttributes 方法 ⊙ TPropertyEditor.GetValue / SetValue 方法 ⊙ TPropertyEditor.Get / Set 系列方法 ⊙ TPropertyEditor.GetValues 方法 ⊙ TPropertyEditor.Edit 方法 ⊙ TPropertyEditor.GetProperties 方法 * ⊙ TPropertyEditor.Modified 方法 ⊙ 基本数据类型的属性编辑器 ⊙ VCL 元件的属性编辑器 ⊙ ICustomPropertyDrawing interface ⊙ ICustomPropertyListDrawing interface ⊙ 未完成的工作 =============================================================================== 本文排版格式为: 正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。 (作者保留对本文的所有权利,未经作者同意请勿在在任何公共媒体转载。) 正 文 =============================================================================== ⊙ RegisterPropertyEditor 函数 =============================================================================== RegisterPropertyEditor 函数用于注册一个属性编辑器至 IDE 环境。 { DesignIntf.pas } procedure RegisterPropertyEditor(PropertyType: PTypeInfo; ComponentClass: TClass; const PropertyName: string; EditorClass: TPropertyEditorClass); begin if Assigned(RegisterPropertyEditorProc) then RegisterPropertyEditorProc(PropertyType, ComponentClass, PropertyName, EditorClass); end; 这个函数本身很简单,那么它是如何与 IDE 交互的呢?原来 RegisterPropertyEditorProc 函数指针在 DesignEditors.pas 的 initialization 段被赋值为 RegisterPropertyEditor 函数: { DesignEditors.pas - initialization } DesignIntf.RegisterPropertyEditorProc := RegisterPropertyEditor; { DesignEditors.pas } { 注意:虽然这个函数的声明与 DesignIntf.pas 的一样,但它是在 implementation 段 } procedure RegisterPropertyEditor(PropertyType: PTypeInfo; ComponentClass: TClass; const PropertyName: string; EditorClass: TPropertyEditorClass); var P: PPropertyClassRec; begin if PropertyClassList = nil then PropertyClassList := TList.Create; New(P); P.Group := CurrentGroup; P.PropertyType := PropertyType; P.ComponentClass := ComponentClass; P.PropertyName := ''; P.ClassGroup := nil; if Assigned(ComponentClass) then P^.PropertyName := PropertyName; P.EditorClass := EditorClass; PropertyClassList.Insert(0, P); // 注意:最后注册的属性编辑器存放在第一位 // 这是否与重复注册属性编辑器的有效性相关? end; 这个函数把属性编辑器的注册信息保存在 PropertyClassList: TList 全局变量中,因此,(我猜想) 最后生成的 .bpl 文件在被 Delphi IDE 载入的时候,可以被 IDE 读取到 PropertyClassList 中的注册信息。 =============================================================================== ⊙ TPropertyEditor 的工作过程 =============================================================================== TPropertyEditor 是从 TBasePropertyEditor 继承下来的。虽然 TBasePropertyEditor 是祖先类,可是它没有实现任何功能。因此,可以把 TPropertyEditor 看作最初的 property editor class。 { DesignEditors.pas } TPropertyEditor = class(TBasePropertyEditor, IProperty, IProperty70) 从上面的声明中可以看到 TPropertyEditor 实现了两个接口。IProperty 和 IProperty70 接口是 Object Inspector 与属性编辑器通信的接口。每当在 Delphi IDE 中选中了元件之后,IDE 自动调用选中元件的属性编辑器的构造函数,生成属性编辑器的实例。然后,通过这两个接口与属性编辑器交互。 { TPropertyEditor } constructor Create(const ADesigner: IDesigner; APropCount: Integer); override; begin FDesigner := ADesigner; // 保存 IDE 接口 GetMem(FPropList, APropCount * SizeOf(TInstProp)); // 创建元件属性指针数组 FPropCount := APropCount; // 保存编辑中的属性数量 end; TInstProp = record Instance: TPersistent; // 选中的元件 PropInfo: PPropInfo; // 元件的属性 RTTI 指针 end; ADesigner 参数实际上就是 IDE 的接口对象,APropCount 是当前选中元件的需要编辑的属性的数量(一次可以选中多个元件)。TPropertyEditor 的构造函数分配了 APropCount 数量的元件指针和元件的 TPropInfo 指针的内存。注意:构造函数中并没有初始化 FPropList 中的信息。(我猜想) IDE 在 TPropertyEditor.Create 之后,还调用了 APropCount 次数的 SetPropEntry 方法。这个方法初始化 Create 中建立的数组。 { TPropertyEditor } procedure SetPropEntry(Index: Integer; AInstance: TPersistent; APropInfo: PPropInfo); override; 经过试验,IDE 还调用了 initialize 虚方法。用于属性编辑器创建之后进行自己需要的特殊操作。 { TPropertyEditor } procedure Initialize; virtual; 上面说的这三个方法就是 TBasePropertyEditor 的全部内容,只是在 TBasePropertyEditor 中没有实现任何功能,都是在 TPropertyEditor 中重载的。因此 Delphi 设计这个基类的目的就是明确 IDE 创建属性编辑器的必要操作,之后 IDE 对属性编辑器的所有的操作都是通过 IProperty 和 IProperty70 进行的。 (我猜想) 创建属性编辑器有两种情况,一是显示属性的值,一是编辑属性的值。 (测试和推测的结果) IDE 与 TPropertyEditor 的交互情况大致如下: 显示属性时: TPropertyEditor.Create; // 创建 TPropertyEditor 对象 TPropertyEditor.SetPropEntry; // 设置选中的元件信息 TPropertyEditor.Initialize; // 自定义初始化 IProperty.GetAttributes; // 获得属性编辑器的特性设置 IProperty.GetValue; // 获得属性字符串值 编辑属性时: TPropertyEditor.Create; // 创建 TPropertyEditor 对象 TPropertyEditor.SetPropEntry; // 设置选中的元件信息 TPropertyEditor.Initialize; // 自定义初始化 IProperty.Activate; // 准备编辑属性 IProperty.GetAttributes; // 获得属性编辑器的特性设置 IProperty.GetProperties; // 如果拥有子属性,则调用获得子属性信息 IProperty.GetValues; // 如果是 ValueList 类型,则获得值列表 IProperty.SetValue; // 设置属性值 =============================================================================== ⊙ IProperty interface =============================================================================== IProperty interface 是 Object Inspector 与 TPropertyEditor 之间交互的接口。 IProperty interface 注解: IProperty = interface ['{7ED7BF29-E349-11D3-AB4A-00C04FB17A72}'] procedure Activate; 属性编辑器即将被激活,在 GetAttributes 方法之前被调用; function AllEqual: Boolean; 在多个元件被选中的情况下,是否所有的属性值都相等; 如果 AllEqual = True,GetValue 将被调用,否则 Object Inspector 显示空白; function AutoFill: Boolean; 在 GetAttributes 返回 paValueList 的情况下,是否允许属性值自动增量选择; procedure Edit; 在 GetAttributes 返回 paDialog 的情况下,点击编辑按钮或双击将调用此方法; function HasInstance(Instance: TPersistent): Boolean; 当前属性编辑器是否是正在编辑某个元件; function GetAttributes: TPropertyAttributes; 提示 Object Inspector 当前属性编辑器的编辑特性; function GetEditLimit: Integer; 允许用户输入的最大字符串长度; function GetEditValue(out Value: string): Boolean; 是否允许编辑属性值,如果是则返回属性值,否则返回错误信息; function GetName: string; 返回属性的名称,可以重载此方法以改变 Object Inspector 中显示的属性名称; procedure GetProperties(Proc: TGetPropProc); 获得子属性列表; function GetPropInfo: PPropInfo; 获得当前属性信息的 RTTI 指针; function GetPropType: PTypeInfo; 获得当前属性数据类型的 RTTI 指针; function GetValue: string; 获得当前属性的字符串值; procedure GetValues(Proc: TGetStrProc); 通过回调函数返回枚举字符串列表; procedure Revert; 调用 Object Inspector 恢复该属性的原值; procedure SetValue(const Value: string); 使用传入的属性字符串值更新属性值; function ValueAvailable: Boolean; 检查属性值是否能被读取(COM); end; =============================================================================== ⊙ IProperty70 interface =============================================================================== IProperty70 提供 IsDefault 属性,用于 IDE 判断是否需要保存该属性至 DFM 文件中。TPropertyEditor 使用 RTTI 函数已经实现了这个接口(很复杂),一般不用重载。如果你想让 IDE 一定要保存该属性值,可以重载 GetIsDefault 方法并返回 False。 IProperty70 = interface(IProperty) ['{57B97F18-B47F-4635-94CB-3344783E7069}'] function GetIsDefault: Boolean; property IsDefault: Boolean read GetIsDefault; end; =============================================================================== ⊙ TPropertyEditor.GetAttributes 方法 =============================================================================== GetAttributes 方法返回一个 TPropertyAttributes 集合值,Object Inspector 使用这个值设置属性编辑时的一些特性。 { TPropertyEditor } function GetAttributes: TPropertyAttributes; virtual; TPropertyAttribute = (paValueList, paSubProperties, paDialog, paMultiSelect, paAutoUpdate, paSortList, paReadOnly, paRevertable, paFullWidthName, paVolatileSubProperties, paVCL, paNotNestable); TPropertyAttributes = set of TPropertyAttribute; TPropertyAttribute 注解: paValueList 属性编辑器可以返回枚举列表,必须重载 GetValues 方法 paSortList Object Inspector 将对 GetValues 方法的返回值排序 paSubProperties 表示当前属性还有子属性,重载 GetProperties 返回子属性列表 paDialog 可以弹出对话框,Edit 方法会被调用 paMultiSelect 如果多个元件被选中,是否允许显示本属性 paAutoUpdate Object Inspector 中的值修改后,立即自动调用 SetValue 方法更新属性值, 比如窗口的 Caption 属性。 paReadOnly 属性值不允许修改 paRevertable 是否允许 Revert to inhertied 菜单恢复属性原始继承值 paFullWidthName 属性值不显示在 Object Insperctor 中,只显示属性名称 paVolatileSubProperties 属性值的任意变动都导致显示的子属性重新计算 paVCL 属性编辑器是 WinClx 元件,否则是 VisualCLX 元件 paNotNestable Object Inspector 不在嵌套的属性列表中显示本属性 =============================================================================== ⊙ TPropertyEditor.GetValue / SetValue 方法 =============================================================================== Object Inspector 需要显示一个属性值时,会调用 TPropertyEditor 的 GetValue 方法。GetValue 是虚方法,它返回一个字符串。在 TPropertyEditor 中,GetValue 返回 '(Unknown)' 字符串,因此必须被后续类重载。 { TPropertyEditor } function GetValue: string; virtual; 用户使用 Object Inspector 修改了一个属性值时,Object Inspector 会以属性的字符串值为参数调用 SetValue 方法。SetValue 是虚方法,必须被后续类重载。 { TPropertyEditor } procedure SetValue(const Value: string); virtual; =============================================================================== ⊙ TPropertyEditor.Get / Set 系列方法 =============================================================================== TPropertyEditor 使用 RTTI 函数定义了一系列可以获取和更改属性值的函数,它们声明在 protected 段。程序员可以在 GetValue 和 SetValue 函数被重载时使用这些方法设置属性值。 function GetFloatValue: Extended; function GetInt64Value: Int64; function GetMethodValue: TMethod; function GetOrdValue: Longint; function GetStrValue: string; function GetVarValue: Variant; function GetIntfValue: IInterface; procedure SetFloatValue(Value: Extended); procedure SetMethodValue(const Value: TMethod); procedure SetInt64Value(Value: Int64); procedure SetOrdValue(Value: Longint); procedure SetStrValue(const Value: string); procedure SetVarValue(const Value: Variant); procedure SetIntfValue(const Value: IInterface); 所有的 GetSomeValue 方法都是调用 GetSomeValueAt(Index) 方法实现的,其中的 Index 是指选中的元件的索引值,使用 0 代表第一个选中的元件。 所有的 SetSomeValue 方法都是循环设置所有选中元件的属性值为 Value 参数值。 =============================================================================== ⊙ TPropertyEditor.GetValues 方法 =============================================================================== GetValues 返回一组枚举字符串,通常在 GetAttributes 方法返回值中包含 paValueList 标志时被重载,也可以不使用 paValueList 标志,这时可以通过双击属性转到下一个值。 Object Inspector 在需要显示枚举列表或编辑属性值时调用 GetValues,并传入一个 TGetStrProc 函数指针。程序员可以使用这个函数指针加入需要显示的字符串列表。 { TPropertyEditor } procedure GetValues(Proc: TGetStrProc); virtual; { Classe.pas } TGetStrProc = procedure(const S: string) of object; =============================================================================== ⊙ TPropertyEditor.Edit 方法 =============================================================================== 在用户双击属性编辑框时,或者点击编辑按钮时(GetAttributes 返回值包含 paDialog),Object Inspector 将调用此方法。 { TPropertyEditor } procedure Edit; virtual; TPropertyEditor 的 Edit 方法实现的内容是返回枚举列表的下一个值。可以重载 Edit 方法,弹出对话框以实现特殊的属性编辑方法。 =============================================================================== ⊙ TPropertyEditor.GetProperties 方法 * =============================================================================== 如果 GetAttributes 方法返回值包含 paSubProperties,Object Inspector 会呼叫 GetProperties 方法建立子属性列表。 我猜想 GetProperties 可能是属性编辑器中最难于设计的方法。因为不但需要熟悉 IDesigner、IProperty 接口,还需要充分掌握 RTTI 的使用。所幸 Delphi 已经为 TSetProperty、TClassProperty、TComponentProperty 等属性编辑器完成了这项工作,我们只需要拿来使用就可以了。 { TPropertyEditor } procedure GetProperties(Proc: TGetPropProc); virtual; { DesignIntf.pas } TGetPropProc = procedure(const Prop: IProperty) of object; =============================================================================== ⊙ TPropertyEditor.Modified 方法 =============================================================================== Modified 方法通知 Object Inspector 属性值已改变,Object Inspector 响应这个方法刷新属性值的显示。 { TPropertyEditor } procedure Modified; =============================================================================== ⊙ 基本数据类型的属性编辑器 =============================================================================== 通常不需要从 TPropertyEditor 开始设计属性编辑器,Delphi 在 DesignEditors.pas 中定义了所有基本数据类型的属性编辑器,可以直接使用: TPropertyEditor = class(TBasePropertyEditor, IProperty, IProperty70) |-TOrdinalProperty = class(TPropertyEditor) | |-TCharProperty = class(TOrdinalProperty) | |-TIntegerProperty = class(TOrdinalProperty) | |-TSetProperty = class(TOrdinalProperty) | |-TEnumProperty = class(TOrdinalProperty) | |-TBoolProperty = class(TEnumProperty) (obsolete! use TEnumProperty) |-TStringProperty = class(TPropertyEditor) |-TFloatProperty = class(TPropertyEditor) |-TInt64Property = class(TPropertyEditor) *|-TMethodProperty = class(TPropertyEditor, IMethodProperty) *|-TClassProperty = class(TPropertyEditor) |-TDateTimeProperty = class(TPropertyEditor) |-TDateProperty = class(TPropertyEditor) |-TTimeProperty = class(TPropertyEditor) *|-TVariantProperty = class(TPropertyEditor) *|-TComponentProperty = class(TPropertyEditor, IReferenceProperty) | |-TInterfaceProperty = class(TComponentProperty) *|-TNestedProperty = class(TPropertyEditor) |-TSetElementProperty = class(TNestedProperty) |-TVariantTypeProperty = class(TNestedProperty) * 表示需要重点考虑,但现在没时间看 =============================================================================== ⊙ VCL 元件的属性编辑器 =============================================================================== Delphi 在 VCLEditors.pas 定义了元件的属性编辑器,也可以从这些类中继承。 TBrushStyleProperty 画刷风格 TPenStyleProperty 画笔风格 TColorProperty 颜色 TCursorProperty 光标 TCaptionProperty 标题 TFontProperty 字体 TFontCharsetProperty 字体字符集 * TFontNameProperty 字体名称 TImeNameProperty 输入法 * TShortCutProperty 快捷方式 (热键) * TTabOrderProperty Tab Order * TModalResultProperty Modal Result TMPFilenameProperty TMediaPlayer FileName * 表示可能它的实现方法很有趣,但现在没时间看 =============================================================================== ⊙ ICustomPropertyDrawing interface =============================================================================== ICustomPropertyDrawing 接口用于实现自定义的属性显示。它提供了一个 TCanvas 对象用于绘图。 { VCLEditors.pas } ICustomPropertyDrawing = interface ['{E1A50419-1288-4B26-9EFA-6608A35F0824}'] procedure PropDrawName(ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean); procedure PropDrawValue(ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean); end; PropDrawName 用于显示属性名称,PropDrawValue 用于显示属性值。这两个方法由 Object Inspector 自动调用。如果 GetAttributes 返回值包含 paFullWidthName,那么 PropDrawName 函数将被调用,否则 PropDrawName 和 PropDrawValue 都会被调用。 如果属性编辑器没有实现这个接口,将调用 DefaultPropertyDrawName 和 DefaultPropertyDrawValue 方法。它们只是简单地把字符值显示在 Canvas 上。 { VCLEditors.pas } procedure DefaultPropertyDrawName(Prop: TPropertyEditor; Canvas: TCanvas; const Rect: TRect); begin Canvas.TextRect(Rect, Rect.Left 1, Rect.Top 1, Prop.GetName); end; { VCLEditors.pas } procedure DefaultPropertyDrawValue(Prop: TPropertyEditor; Canvas: TCanvas; const Rect: TRect); begin Canvas.TextRect(Rect, Rect.Left 1, Rect.Top 1, Prop.GetVisualValue); end; 这两个函数也可以被元件设计者使用,实现缺省的属性显示。 注意:如果从一个已实现 ICustomPropertyDrawing 的类继承属性编辑器,也必须在类的声明中加上 ICustomPropertyDrawing 接口。因为 VCL 元件的属性编辑器实现该接口时没有使用虚函数定义,所以要覆盖祖先类对 ICustomPropertyDrawing 接口的实现。 这个接口很容易使用,具体的设计方法可以参考 TColorProperty。 =============================================================================== ⊙ ICustomPropertyListDrawing interface =============================================================================== ICustomPropertyListDrawing 接口很类似,只是它用于自定义绘制属性列表框的内容。当 GetAttributes 返回值包含 paValueList 时,属性编辑器会显示列表框。 ListMeasureWidth 和 ListMeasureHeight 以引用方式传入列表项的高度和宽度值,用户可以更改这个两个值。 重载 ListDrawValue 方法可以自定义的绘制列表。DefaultPropertyListDrawValue 函数实现缺省的绘制工作。 ICustomPropertyListDrawing = interface ['{BE2B8CF7-DDCA-4D4B-BE26-2396B969F8E0}'] procedure ListMeasureWidth(const Value: string; ACanvas: TCanvas; var AWidth: Integer); procedure ListMeasureHeight(const Value: string; ACanvas: TCanvas; var AHeight: Integer); procedure ListDrawValue(const Value: string; ACanvas: TCanvas; const ARect: TRect; ASelected: Boolean); end; 具体的使用方法可以参考 TColorProperty、TFontNameProperty。TFontNameProperty 还有一个有趣的功能,如果你把 FontNamePropertyDisplayFontNames 全局变量设置为 True,那么字体名称属性会像 MS Word 中的字体列表一样按实际的字体显示字体名称。 =============================================================================== ⊙ 未完成的工作 =============================================================================== 大概查看了一下 VCL 源码,还有以下函数或接口与属性编辑器有关,目前没有时间分析,记录在此留作日后考虑。 IMethodProperty = interface IReferenceProperty = interface procedure RegisterPropertyMapper; procedure SetPropertyEditorGroup; procedure RegisterPropertyInCategory; procedure RegisterPropertiesInCategory; procedure UnlistPublishedProperty; =============================================================================== ⊙ 结 束 =============================================================================== |
allenchan
資深會員 發表:10 回覆:306 積分:283 註冊:2004-01-06 發送簡訊給我 |
本站聲明 |
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。 2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。 3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇! |