Delphi面向对象编程的20条规则【转】

规则一:为每一个类创建一个单元(One Class,One Unit)
请始终牢记这一点:类的私有(private)和保护(protected)的部分只对于其他单元中的类和过程(procedure)才是隐藏的.因此,如果你想得到有效的封装性,你应该为每一个类使用一个不同的单元。对于一些简单的
类,比如那些继承其他类的类,你可以使用一个共享的单元。不过共享同一个单元的类的数目是受到限制的:不要在一个简单的单元里放置超过20个复杂的类,虽然Borland公司的VCL代码曾经这样做过。
如果你使用窗体的时候,Delphi会默认的遵循“一个类使用一个单元”的规则,这对于程序员来说也是十分方便的。当你向你的项目中添加一个没有窗体的类时,Delphi也会创建一个新的独立的单元。


规则二:为组件命名(Name Components)
为每一个窗体和单元给出一个有意义的名字是十分重要的。窗体和单元的名字必须是不同的,不过我趋向于为他们两者使用相似的名字,如对于关于窗体和单元可以为他们使用AboutForm 和About.pas.
为组件使用带有描述性的名字同样十分重要。最常见的命名方式是使用类的小写字母开头,再加上组件的功能,如BtnAdd 或者editName。采用这样的命名方式为组件命名可能会有很多相似的名字,而且也没有一个最好的名字,到底应该选择那一个应该依据你的个人爱好而定。


规则三:为事件命名(Name Events)
对于事件处理方法给出合适的名字更加重要。如果你对于组件给出了一个合适的名字,那么系统默认的名字ButtonClick将变成 BtnAddClick。虽然从这个名字中我们可以猜到这个事件处理程序的功能,但是我认为使用一个能够描述该方法的作用的名字,而不是采用Delphi 附加的名字是一种更好的方式。例如,BtnAdd按钮的OnClick事件可以命名成AddToList。这会使得你的程序可读性更强,特别是当你在这个 类的其他方法中调用这个事件处理程序时,而且这会帮助程序员为类似的事件或是不同的组件选用相同的方法。不过我必须声明,使用动作(Actions)是目 前开发重要的程序时我最喜欢的方法。


规则四:使用窗体方法(Use Form Methods)
窗体都是一些类,因此窗体的代码是以方法组织的。你可以向窗体中添加事件处理程序,这些处理程序完成一些特别的功能,而且他们能被其他方法调用。 除了事件处理方法外,你还可以向窗体添加完成动作的特别定义的方法以及访问窗体状态的方法。在窗体中添加一些公共的(Public)方法供其他窗体调用要 比其他窗体直接操作他的组件要好。


规则5:添加窗体构造器(Add Form Constructors)
在运行时创建的第二个窗体除了一个默认的构造器(从Tcomponent 类继承而来)外还会提供其他特殊的构造器。如果你不需要考虑和Delphi4以前的版本的兼容性问题,我建议你重载(Overload)Create方 法,添加必要的初始化参数。具体代码可参见下面的代码:

Public
Constructor Create(Text:string): reintroduce ; overload;
Constructor TformDialog.Create(Text:string);
Begin
Inherited Create(Application);
Edit1.Text:=Text;
End;


规则6:避免全局变量(Avoid Global Variables)
应该避免使用全局变量(就是那些在单元的interface 部分定义的变量)。下面将会有一些建议帮助你如何去做。
如果你需要为窗体存储额外的数据,你可以向窗体类中添加一些私有数据。这种情况下,每一个窗体实例都会有自己的数据副本。你可以使用单元变量(在单元的implementation部分定义的变量)声明那些供窗体类的多个实例共享的数据。
如果你需要在不同类型的窗体之间共享数据,你可以把他们定义在主窗体里来实现共享,或者使用一个全局变量,使用方法或者是属性来获得数据。


规则7:永远不要在Tform1类中使用Form1(Never Use Form1 in Tform1)
你应该避免在类的方法中使用一个特定的对象名称,换句话说,你不应该在TForm1类的方法中直接使用Form1.如果你确实需要使用当前的对象,你可以使用Self关键字。请牢记:大多数时候你都没有必要直接使用当前对象的方法和数据。
如果你不遵循这条规则,当你为一个窗体类创建多个实例的时候,你会陷入麻烦当中。


规则8:尽量避免在其他的窗体中使用Form1(Seldom Use Form1 In Other Forms )
即使在其他窗体的代码中,你也应该尽量避免直接使用全局变量,如Form1.定义一些局部变量或者私有域供其他窗体使用会比直接调用全局变量要好。
例如,程序的主窗体能够为对话框定义一个私有域。很显然,如果你计划为一个派生窗体创建多个实例,这条规则将是十分有用。你可以在主窗体的代码范围内保持一份清单,也可以更简单地使用全局Sreen对象的窗体数组。

规则9:移除Form1(Remove Form1)
事实上,我的建议是在你的程序中移除Delphi自动创建的全局窗体对象。即使你禁止了窗体的自动添加功能,这也有可能是必要的,因为在Delphi随后仍然可能添加这样的窗体。我给你的建议是应该尽量避免使用全局窗体对象。
我认为对于Delphi新手而言,移除全局窗体对象是十分有用的,这样他们不至于对类和全局对象两者的关系感到疑惑。事实上,在全局窗体对象被移除后,所有与它有关的代码都会产生错误。


规则10:添加窗体属性(Add Form Properties)
正如我已经提到过的,当你需要为你的窗体添加数据时,请添加一个私有域。如果你需要访问其他类的数据,可以为你的窗体添加属性。使用这种方法你就能够改变当前窗体的代码和数据(包含在它的用户界面中)而不必改变其他窗体或类的代码。
你还应该使用属性或是方法来初始化派生窗体或是对话框,或是访问他们的最终状态。正如我前文所说的,你应该使用构造器来完成初始化工作

规则11:显示组件属性(Expose Components Properties)
当你需要访问其他窗体的状态时,你不应该直接访问它的组件。因为这样会将其他窗体或其它类的代码和用户界面结合在一起,而用户界面往往是一个应用 程序中最容易发生改变的部分。最好的方法是,为你需要访问的组件属性定义一个窗体属性。要实现这一点,可以通过读取组件状态的Get方法和设置组件状态的 Set方法实现。
假如你现在需要改变用户界面,用另外一个组件替换现有的组件,那么你只需做的是修改与这个组件属性相关的Get方法和Set方法,而不必查找,修改所有引用这个组件的窗体和类的源码。详细实现方法请参见下面的代码:

private
function GetText:String;
procedure SetText(const Value:String);
public
property Text:String;
read GetText write SetText;
function TformDialog.GetText:String;
begin
Result:=Edit1.Text;
end;
procedure TformDialog.SetText(const Value:String);
begin
Edit1.Text;=Value;
end;


规则12:属性数组(Array Properties)
如果你需要处理窗体中的一系列变量,你可以定义一个属性数组。如果这些变量是一些对于窗体很重要的信息,你还可以把他们定义成窗体默认的属性数组,这样你就可以直接使用SpecialForm[3]来访问他们的值了。
下面的代码显示了如何将一个listbox组件的项目定义成窗体默认的属性数组。

type
TformDialog =class(TForm)
private
listItems:TlistBox;
function GetItems(Index:Integer):String;
procedure SetItems(Index:Integer:const Value:String);
public
property Items[Index:Integer]:string;
end;
function TFormDialog.GetItems(Index:Integer):string;
begin
if Index >=ListItems.Items.Count then
raise Exception.Create(‘TformDialog:Out of Range’);
Result:=ListItems.Items[Index];
end;
procedure TformDialog.SetItems(Index:Integer;const alue:string);
begin
if Index >=ListItems.Items.Count then
raise Exception.Create(‘TformDialog:Out of Range’);
ListItems.Items[Index]:=Value;
end;


规则13:使用属性的附加作用(Use Side-Effects In Properties)
请记住:使用属性而不是访问全局变量(参见规则10、11、12)的好处之一就是当你设置或者读取属性的值时,你还可能有意想不到的收获。
例如,你可以直接在窗体界面上拖拉组件,设置多个属性的值,调用特殊方法,立即改变多个组件的状态,或者撤销一个事件(如果需要的话)等等。


规则14:隐藏组件(Hide Components)
我经常听见那些面向对象编程的狂热追求者抱怨Delphi窗体中包含一些在published部分声明的组件,这是和面向对象思想的封装性原理不 相符合的。他们确实提出了一个重要的议题,但是他们中的大多数人都没有意识到解决方法其实就在他们手边,完全不用重写Delphi代码,也不用转向其他语 言。
Delphi向窗体中添加的组件参照可以被移到private部分,使得其他窗体不能访问他们。如果你这样做,你就有必要设置一些指向组件的窗体属性(请参见规则11),并且使用它们来访问组件的状态。
Delphi将所有的这些组件都放在published部分,这是因为使用这种方式能够保证这些域一定是在.DFM文件中创建的组件。当你改变一 个组件的名称时,VCL能够自动地将这个组件对象与它在窗体中的参照关联起来。因为delphi使用RTTI和Tobject方法来实现这种功能,所以如 果想要使用这种自动实现功能,就必须把参照放置在published部分(这也正是为什么delphi将所有的组件都放在published部分的缘 故)。
如果你想知道的更详细一点,可以参看下面的代码:

procedure Tcomponent.SetReference(Enable:Boolean);
var
Field:^Tcomponent;
begin
If Fowner<> nil then begin
Field:=Fowner.FieldAddress(Fname);
If Field<>nil then
Field^:=Self
else
Field^:=nil;
end;
end;


上面的代码是Tcomponent类的SetReference方法,这个方法可以被InserComponent,RemoveComponent和SetName等方法调用。
当你理解了这一点后,你应该不难想到如果你将组件参照从published部分移到了private段,你将失去VCL的自动关联功能。为了解决这个问题,你可以通过在窗体的OnCreate事件中添加如下代码解决:
Edit1:=FindComponent(‘Edit1’) as Tedit;
你接下来应该做的就是在系统中注册这些组件类,当你为他们注册过后就能使RTTI包含在编译程序中并且能够被系统所使用。当你将这些类型的组件参 照移到private部分时,对于每一个组件类,你只需为他们注册一次。即使为他们注册不是一定必要的时候,你也可以这样做,因为对于 RegisterClasses的额外调用有益无害。通常你应该在单元中负责生成窗体的初始化部分添加以下的代码:
RegisterClass([TEdit]);


规则15:面向对象编程的窗体向导(The OOP Form Wizard)
为每一个窗体的每一个组件重复上述两个操作不仅十分的烦人,而且相当的浪费时间。为了避免额外的负担,我已经为此写了一个简单的向导程序。这个程序将会生成一些可以完成以上两步工作的代码,你需要做的仅仅是做几次复制和粘贴就行了。
遗憾的是这个向导程序不能自动将代码放置到单元中合适的地方,我目前正在修改这个向导程序,希望能实现这个功能。你可以到我的网站(www.marcocantu.com)查找更加完善的程序。

规则16:可视化窗体继承(Visual Form Inheritance)
如果应用得当,这将是一个强大的工具。根据我的经验,你所开发的项目越大,越能体现它的价值。在一个复杂的程序中,你可以使用窗体的不同等级关系来处理一组相关窗体的多态性(polymorphism)。
可视化窗体继承允许你共享多个窗体的一些公共的动作:你可以使用共享的方法,公用的属性,甚至是事件处理程序,组件,组件属性,组件事件处理方法等等。


规则17:限制保护域数据的使用(Limit Protected Data)
当创建一些具有不同分级体系的类时,一些程序员趋向于主要使用保护域,因为私有数据不能被子类访问。我不能说这没有其合理性,但是这肯定是和封装性不相容和的。保护数据的实现能够被所有继承的窗体所共享,而且一旦这些数据的原始定义发生改变,你必须更改所有的相关部分。
请注意,如果你遵循隐藏组件这样一条规则(Rule 14),继承窗体就不可能访问基类的私有组件。在一个继承窗体中,类似Edit1.Text:=’’的代码就不会被编译。虽然这是相当的不方便,但是至少 在理论上这是值得肯定的事情,而不是否定的。如果你感觉到实现封装性是最主要,最需要的,就请将这些组件参照放在基类的私有段。


规则18:保护域中的访问方法(Protected Access Methods)
在基类中将组件参照放置在私有域中,而为这些组件添加一些访问函数来得到他们的属性,这将是一种更好的方法。如果这些访问函数仅仅在这些类内部使 用而且不是类接口的一部分,你应该在保护域声明他们。例如Rule 11中描述过的GetText和SetText方法就可以声明成protected,并且我们可以通过调用SetText(’’)来编辑文本。
事实上,当一个方法被镜像到一个属性时,我们可以简单地采用如下代码就可以达到编辑文本地目的:Text:=’’;


规则19:保护域中的虚拟方法(Protected Virtual Methods)
实现一个灵活的分级制度的另一个关键点是定义一些你可以从外部类调用的虚拟方法来得到多态性。如果这个方法使用得当,将会很少出现其他公共的方法调用保护域中的虚拟方法的情况。这是一个重要的技巧,因为你可以定制派生类的虚拟方法,来修改对象的动作。


规则20:用于属性的虚拟方法(Virtual Methods For Properties)
即使是访问属性的方法也能定义成virtual,这样派生类就能改变属性的动作而不必重定义他们。虽然这种方法在VCL当中很少使用,但是它确实 十分灵活、强大。为了实现这一点,仅仅需要将Rule 11当中的Get 和Set 方法定义成Virtual。基类的代码如下所示:

type
TformDialog = class ( TForm)
Procedure FormCreate(Sender:Tobject);
Private
Edit1:Tedit;
Protected
function GetText:String;virtual;
procedure SetText(const Value:String);virtual;
public
constructor Create(Text :String):reintroduce;overload;
property Text:String read GetText write SetText;
end;


在继承窗体中,你可以添加一些额外的动作来重载虚拟方法SetText:
procedure TformInherit.SetText(const Value:String);
begin
inherited SetText(Value);
if Value=’’ then
Button1.Enabled:=False;
end;



小结
要做到一个好的Delphi面向对象编程程序员远非我在上面提到的这些规则这么简单。上面的这20条规则中有一些可能需要足够的耐性和时间来实 现,因此,我也不能强求你能遵循所有的这些规则。但是这些规则应该被合适的运用到你的程序中,而且当你开发的应用程序越大,参与的程序员越多,这些规则越 重要。不过,即使是一些小程序,始终记住这些规则并在合适的地方使用他们也会对你有所帮助。
当然,还有很多其他的经验规则我没有涉及到,特别是存储器处理和RTTI问题,因为他们十分的复杂,需要专门的说明。
我的结论是要遵循我上面列出的规则会付出一定的代价,特别是额外的代码,但是这些代价会让你得到一个更加灵活强壮的程序。希望Delphi的后续版本能够帮组我们减少这些代价。




发表评论 发表评论   |   permalink   |   ( 3 / 5 )

 

Delphi 的RTTI机制浅探
Monday, August 8, 2005, 05:50 AM

Delphi 的RTTI机制浅探

作者:Savetime:savetime2k@yahoo.com 转贴自:Delphibbs.com
目录
===========================================================
⊙ RTTI 简介
⊙ 类(class) 和 VMT 的关系
⊙ 类(class)、类的类(class of class)、类变量(class variable) 的关系
⊙ TObject.ClassType 和 TObject.ClassInfo
⊙ is 和 as 运算符的原理
⊙ TTypeInfo – RTTI 信息的结构
⊙ 获取类(class)的属性(property)信息
⊙ 获取方法(method)的类型信息
⊙ 获取有序类型(ordinal)、集合(set)类型的 RTTI 信息
⊙ 获取其它数据类型的 RTTI 信息
===========================================================
本文排版格式为:
正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。

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


正文
===========================================================
⊙ RTTI 简介
===========================================================

RTTI(Run-Time Type Information) 翻译过来的名称是“运行期类型信息”,也就是说可以在运行期获得数据类型或类(class)的信息。这个 RTTI 到底有什么用处,我现在也说不清楚。我是在阅读 Delphi 持续机制的代码中发现了很多 RTTI 的运用,只好先把 RTTI 学习一遍。下面是我的学习笔记。如果你发现了错误请告诉我。谢谢!

Delphi 的 RTTI 主要分为类(class)的 RTTI 和一般数据类型的 RTTI,下面从类(class)开始。

===========================================================
⊙ 类(class) 和 VMT 的关系
===========================================================

一个类(class),从编译器的角度来看就是一个指向 VMT 的指针(在后文用 VMTptr 表示)。在类的 VMTptr 的负地址方向存储了一些类信息的指针,这些指针的值和指针所指的内容在编译后就确定了。比如 VMTptr - 44 的内容是指向类名称(ClassName)的指针。不过一般不使用数值来访问这些类信息,而是通过 System.pas 中定义的以 vmt 开头的常量,如 vtmClassName、vmtParent 等来访问。

类的方法有两种:对象级别的方法和类级别的方法。两者的 Self 指针意义是不同的。在对象级别的方法中 Self 指向对象地址空间,因此可以用它来访问对象的成员函数;在类级别的方法中 Self 指向类的 VMT,因此只能用它来访问 VMT 信息,而不能访问对象的成员字段。

===========================================================
⊙ 类(class)、类的类(class of class)、类变量(class variable) 的关系
===========================================================

上面说到类(class) 就是 VMTptr。在 Delphi 中还可以用 class of 关键字定义类的类,并且可以使用类的类定义类变量。从语法上理解这三者的关键并不难,把类当成普通的数据类型来考虑就可以了。在编译器级别上表现如何呢?

为了简化讨论,我们使用 TObject、TClass 和 TMyClass 来代表上面说的三种类型:

type
TClass = class of TObject;
var
TMyClass: TClass;
MyObject: TObject;
begin
TMyClass := TObject;
MyObject := TObject.Create;
MyObject := TClass.Create;
MyObject := TMyClass.Create;
end;

在上面的例子中,三个 TObject 对象都被成功地创建了。编译器的实现是:TObject 是一个 VMTPtr 常量。TClass 也是一个 VMTptr 常量,它的值就是 TObject。TMyClass 是一个 VMTptr 变量,它被赋值为 TObject。TObject.Create 与 TClass.Create 的汇编代码完全相同。但 TClass 不仅缺省代表一个类,而且还(主要)代表了类的类型,可以用它来定义类变量,实现一些类级别的操作。

===========================================================
⊙ TObject.ClassType 和 TObject.ClassInfo
===========================================================

function TObject.ClassType: TClass;
begin
Pointer(Result) := PPointer(Self)^;
end;

TObject.ClassType 是对象级别的方法,Self 的值是指向对象内存空间的指针,对象内存空间的前 4 个字节是类的 VMTptr。因此这个函数的返回值就是类的 VMTptr。

class function TObject.ClassInfo: Pointer;
begin
Result := PPointer(Integer(Self) + vmtTypeInfo)^;
end;

TObject.ClassInfo 使用 class 关键字定义,因此是一个类级别的方法。该方法中的 Self 指针就是 VMTptr。所以这个函数的返回值是 VMTptr 负方向的 vmtTypeInfo 的内容。

TObject.ClassInfo 返回的 Pointer 指针,实际上是指向类的 RTTI 结构的指针。但是不能访问 TObject.ClassInfo 指向的内容(TObject.ClassInfo 返回值是 0),因为 Delphi 只在 TPersistent 类及 TPersistent 的后继类中产生 RTTI 信息。(从编译器的角度来看,这是在 TPersistent 类的声明之前使用 {$M+} 指示字的结果。)

TObject 还定义了一些获取类 RTTI 信息的函数,列举在下,就不一一分析了:

TObject.ClassName: ShortString; 类的名称
TObject.ClassParent: TClass; 对象的父类
TObject.InheritsFrom: Boolean; 是否继承自某类
TObject.InstanceSize: Longint; 对象实例的大小

===========================================================
⊙ is 和 as 运算符的原理
===========================================================

我们知道可以在运行期使用 is 关键字判断一个对象是否属于某个类,可以使用 as 关键字把某个对象安全地转换为某个类。在编译器的层次上,is 和 as 的操作是由 System.pas 中两个函数完成的。

{ System.pas }
function _IsClass(Child: TObject; Parent: TClass): Boolean;
begin
Result := (Child <> nil) and Child.InheritsFrom(Parent);
end;

_IsClass 很简单,它使用 TObject 的 InheritsForm 函数判断该对象是否是从某个类或它的父类中继承下来的。每个类的 VMT 中都有一项 vmtParent 指针,指向该类的父类的 VMT。TObject.InheritsFrom 实际上是通过[递归]判断父类 VMT 指针是否等于自己的 VMT 指针来判断是否是从该类继承的。

{ System.pas }
class function TObject.InheritsFrom(AClass: TClass): Boolean;
var
ClassPtr: TClass;
begin
ClassPtr := Self;
while (ClassPtr <> nil) and (ClassPtr <> AClass) do
ClassPtr := PPointer(Integer(ClassPtr) + vmtParent)^;
Result := ClassPtr = AClass;
end;

as 操作符实际上是由 System.pas 中的 _AsClass 函数完成的。它简单地调用 is 操作符判断对象是否属于某个类,如果不是就触发异常。虽然 _AsClass 返回值为 TObject 类型,但编译器会自动把返回的对象改变为 Parent 类,否则返回的对象没有办法使用 TObject 之外的方法和数据。

{ System.pas }
function _AsClass(Child: TObject; Parent: TClass): TObject;
begin
Result := Child;
if not (Child is Parent) then
Error(reInvalidCast); // loses return address
end;

===========================================================
⊙ TTypeInfo – RTTI 信息的结构
===========================================================

RTTI 信息的结构定义在 TypInfo.pas 中:

TTypeInfo = record // TTypeInfo 是 RTTI 信息的结构
Kind: TTypeKind; // RTTI 信息的数据类型
Name: ShortString; // 数据类型的名称
{TypeData: TTypeData} // RTTI 的内容
end;

TTypeInfo 就是 RTTI 信息的结构。TObject.ClassInfo 返回指向存放 class TTypeInfo 信息的指针。Kind 是枚举类型,它表示 RTTI 结构中所包含数据类型。Name 是数据类型的名称。注意,最后一个字段 TypeData 被注释掉了,这说明该处的结构内容根据不同的数据类型有所不同。

TTypeKind 枚举定义了可以使用 RTTI 信息的数据类型,它几乎包含了所有的 Delphi 数据类型,其中包括 tkClass。

TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray);

TTypeData 是个巨大的记录类型,在此不再列出,后文会根据需要列出该记录的内容。

===========================================================
⊙ 获取类(class)的属性(property)信息
===========================================================

这一段是 RTTI 中最复杂的部分,努力把本段吃透,后面的内容都是非常简单的。

下面是一个获取类的属性的例子:

procedure GetClassProperties(AClass: TClass; AStrings: TStrings);
var
PropCount, I: SmallInt;
PropList: PPropList;
PropStr: string;
begin
PropCount := GetTypeData(AClass.ClassInfo).PropCount;
GetPropList(AClass.ClassInfo, PropList);
for I := 0 to PropCount - 1 do
begin
case PropList^.PropType^.Kind of
tkClass : PropStr := '[Class] ';
tkMethod : PropStr := '[Method]';
tkSet : PropStr := '[Set] ';
tkEnumeration: PropStr := '[Enum] ';
else
PropStr := '[Field] ';
end;
PropStr := PropStr + PropList^.Name;
PropStr := PropStr + ': ' + PropList^.PropType^.Name;
AStrings.Add(PropStr);
end;
FreeMem(PropList);
end;

你可以在表单上放置一个 TListBox ,然后执行以下语句观察执行结果:

GetClassProperties(TForm1, ListBox1.Items);

该函数先使用 GetTypeData 函数获得类的属性数量。GetTypeData 是 TypInfo.pas 中的一个函数,它的功能是返回 TTypeInfo 的 TypeData 数据的指针:

{ TypInfo.pas }
function GetTypeData(TypeInfo: PTypeInfo): PTypeData; assembler;

class 的 TTypeData 结构如下:

TTypeData = packed record
case TTypeKind of
tkClass: (
ClassType: TClass; // 类 (VMTptr)
ParentInfo: PPTypeInfo; // 父类的 RTTI 指针
PropCount: SmallInt; // 属性数量
UnitName: ShortStringBase; // 单元的名称
{PropData: TPropData}); // 属性的详细信息
end;

其中的 PropData 又是一个大小可变的字段。TPropData 的定义如下:

TPropData = packed record
PropCount: Word; // 属性数量
PropList: record end; // 占位符,真正的意义在下一行
{PropList: array[1..PropCount] of TPropInfo}
end;

每个属性信息在内存中的结构就是 TPropInfo,它的定义如下:

PPropInfo = ^TPropInfo;
TPropInfo = packed record
PropType: PPTypeInfo; // 属性类型信息指针的指针
GetProc: Pointer; // 属性的 Get 方法指针
SetProc: Pointer; // 属性的 Set 方法指针
StoredProc: Pointer; // 属性的 StoredProc 指针
Index: Integer; // 属性的 Index 值
Default: Longint; // 属性的 Default 值
NameIndex: SmallInt; // 属性的名称索引(以 0 开始计数)
Name: ShortString; // 属性的名称
end;

为了方便访问属性信息,TypInfo.pas 中还定义了指向 TPropInfo 数组的指针:

PPropList = ^TPropList;
TPropList = array[0..16379] of PPropInfo;

我们可以使用 GetPropList 获得所有属性信息的指针数组,数组用完以后要记得用 FreeMem 把数组的内存清除。

{ TypInfo.pas }
function GetPropList(TypeInfo: PTypeInfo; out PropList: PPropList): Integer;

GetPropList 传入类的 TTypeInfo 指针和 TPropList 的指针,它为 PropList 分配一块内存后把该内存填充为指向 TPropInfo 的指针数组,最后返回属性的数量。

上面的例子演示了如何获得类的所有属性信息,也可以根据属性的名称单独获得属性信息:

{ TypInfo.pas }
function GetPropInfo(TypeInfo: PTypeInfo; const PropName: string): PPropInfo;

GetPropInfo 根据类的 RTTI 指针和属性的名称字符串,返回属性的信息 TPropInfo 的指针。如果没有找到该属性,则返回 nil。GetPropInfo 很容易使用,举个例子:

ShowMessage(GetPropInfo(TForm, 'Name')^.PropType^.Name);

这句调用显示了 TForm 类的 Name 属性的类型名称:TComponentName。

===========================================================
⊙ 获取方法(method)的类型信息
===========================================================

所谓方法就是以 of object 关键字声明的函数指针,下面的函数可以显示一个方法的类型信息:

procedure GetMethodTypeInfo(ATypeInfo: PTypeInfo; AStrings: TStrings);
type
PParamData = ^TParamData;
TParamData = record // 函数参数的数据结构
Flags: TParamFlags; // 参数传递规则
ParamName: ShortString; // 参数的名称
TypeName: ShortString; // 参数的类型名称
end;
function GetParamFlagsName(AParamFlags: TParamFlags): string;
var
I: Integer;
begin
Result := '';
for I := Integer(pfVar) to Integer(pfOut) do begin
if I = Integer(pfAddress) then Continue;
if TParamFlag(I) in AParamFlags then
Result := Result + ' ' + GetEnumName(TypeInfo(TParamFlag), I);
end;
end;
var
MethodTypeData: PTypeData;
ParamData: PParamData;
TypeStr: PShortString;
I: Integer;
begin
MethodTypeData := GetTypeData(ATypeInfo);
AStrings.Add('---------------------------------');
AStrings.Add('Method Name: ' + ATypeInfo^.Name);
AStrings.Add('Method Kind: ' + GetEnumName(TypeInfo(TMethodKind),
Integer(MethodTypeData^.MethodKind)));
AStrings.Add('Params Count: '+ IntToStr(MethodTypeData^.ParamCount));
AStrings.Add('Params List:');
ParamData := PParamData(@MethodTypeData^.ParamList);
for I := 1 to MethodTypeData^.ParamCount do
begin
TypeStr := Pointer(Integer(@ParamData^.ParamName) +
Length(ParamData^.ParamName) + 1);
AStrings.Add(Format(' [%s] %s: %s',[GetParamFlagsName(ParamData^.Flags),
ParamData^.ParamName, TypeStr^]));
ParamData := PParamData(Integer(ParamData) + SizeOf(TParamFlags) +
Length(ParamData^.ParamName) + Length(TypeStr^) + 2);
end;
if MethodTypeData^.MethodKind = mkFunction then
AStrings.Add('Result Value: ' + PShortString(ParamData)^);
end;

作为实验,在表单上放置一个 TListBox,然后执行以下代码,观察执行结果:

type
TMyMethod = function(A: array of Char; var B: TObject): Integer of object;
procedure TForm1.FormCreate(Sender: TObject);
begin
GetMethodTypeInfo(TypeInfo(TMyMethod), ListBox1.Items);
GetMethodTypeInfo(TypeInfo(TMouseEvent), ListBox1.Items);
GetMethodTypeInfo(TypeInfo(TKeyPressEvent), ListBox1.Items);
GetMethodTypeInfo(TypeInfo(TMouseWheelEvent), ListBox1.Items);
end;

由于获取方法的类型信息比较复杂,我尽量压缩代码也还是有这么长,让我们看看它的实现原理。GetMethodTypeInfo 的第一个参数是 PTypeInfo 类型,表示方法的类型信息地址。第二个参数是一个字符串列表,可以使用任何实现 TStrings 操作的对象。我们可以使用 System.pas 中的 TypeInfo 函数获得任何类型的 RTTI 信息指针。TypeInfo 函数像 SizeOf 一样,是内置于编译器中的。

GetMethodTypeInfo 还用到了 TypInfo.pas 中的 GetEnumName 函数。这个函数通过枚举类型的整数值得到枚举类型的名称。

function GetEnumName(TypeInfo: PTypeInfo; Value: Integer): string;

与获取类(class)的属性信息类似,方法的类型信息也在 TTypeData 结构中

TTypeData = packed record
case TTypeKind of
tkMethod: (
MethodKind: TMethodKind; // 方法指针的类型
ParamCount: Byte; // 参数数量
ParamList: array[0..1023] of Char // 参数详细信息,见下行注释
{ParamList: array[1..ParamCount] of
record
Flags: TParamFlags; // 参数传递规则
ParamName: ShortString; // 参数的名称
TypeName: ShortString; // 参数的类型
end;
ResultType: ShortString}); // 返回值的名称
end;

TMethodKind 是方法的类型,定义如下:

TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor,
mkClassProcedure, mkClassFunction,
{ Obsolete }
mkSafeProcedure, mkSafeFunction);

TParamsFlags 是参数传递的规则,定义如下:

TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut);
TParamFlags = set of TParamFlag;

由于 ParamName 和 TypeName 是变长字符串,不能直接取用该字段的值,而应该使用指针步进的方法,取出参数信息,所以上面的代码显得比较长。

===========================================================
⊙ 获取有序类型(ordinal)、集合(set)类型的 RTTI 信息
===========================================================

讨论完了属性和方法的 RTTI 信息之后再来看其它数据类型的 RTTI 就简单多了。所有获取 RTTI 的原理都是通过 GetTypeData 函数得到 TTypeData 的指针,再通过 TTypeInfo.TypeKind 来解析 TTypeData。任何数据类型的 TTypeInfo 指针可以通过 TypeInfo 函数获得。

有序类型的 TTypeData 定义如下:

TTypeData = packed record
tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (
OrdType: TOrdType; // 有序数值类型
case TTypeKind of
case TTypeKind of
tkInteger, tkChar, tkEnumeration, tkWChar: (
MinValue: Longint; // 类型的最小值
MaxValue: Longint; // 类型的最大值
case TTypeKind of
tkInteger, tkChar, tkWChar: ();
tkEnumeration: (
BaseType: PPTypeInfo; // 指针的指针,它指向枚举的 PTypeInfo
NameList: ShortStringBase; // 枚举的名称字符串(不能直接取用)
EnumUnitName: ShortStringBase)); // 所在的单元名称(不能直接取用)
tkSet: (
CompType: PPTypeInfo)); // 指向集合基类 RTTI 指针的指针
end;

下面是一个获取有序类型和集合类型的 RTTI 信息的函数:

procedure GetOrdTypeInfo(ATypeInfo: PTypeInfo; AStrings: TStrings);
var
OrdTypeData: PTypeData;
I: Integer;
begin
OrdTypeData := GetTypeData(ATypeInfo);
AStrings.Add('------------------------------------');
AStrings.Add('Type Name: ' + ATypeInfo^.Name);
AStrings.Add('Type Kind: ' + GetEnumName(TypeInfo(TTypeKind),
Integer(ATypeInfo^.Kind)));
AStrings.Add('Data Type: ' + GetEnumName(TypeInfo(TOrdType),
Integer(OrdTypeData^.OrdType)));
if ATypeInfo^.Kind <> tkSet then begin
AStrings.Add('Min Value: ' + IntToStr(OrdTypeData^.MinValue));
AStrings.Add('Max Value: ' + IntToStr(OrdTypeData^.MaxValue));
end;
if ATypeInfo^.Kind = tkSet then
GetOrdTypeInfo(OrdTypeData^.CompType^, AStrings);
if ATypeInfo^.Kind = tkEnumeration then
for I := OrdTypeData^.MinValue to OrdTypeData^.MaxValue do
AStrings.Add(Format(' Value %d: %s', [I, GetEnumName(ATypeInfo, I)]));
end;

在表单上放置一个 TListBox,运行以下代码查看结果:

type TMyEnum = (EnumA, EnumB, EnumC);
procedure TForm1.FormCreate(Sender: TObject);
begin
GetOrdTypeInfo(TypeInfo(Char), ListBox1.Items);
GetOrdTypeInfo(TypeInfo(Integer), ListBox1.Items);
GetOrdTypeInfo(TypeInfo(TFormBorderStyle), ListBox1.Items);
GetOrdTypeInfo(TypeInfo(TBorderIcons), ListBox1.Items);
GetOrdTypeInfo(TypeInfo(TMyEnum), ListBox1.Items);
end;

(如果枚举元素没有按缺省的 0 基准定义,那么将不能产生 RTTI 信息,为什么?)

===========================================================
⊙ 获取其它数据类型的 RTTI 信息
===========================================================

上面讨论了几个典型的 RTTI 信息的运行,其它的数据类型的 RTTI 信息的获取方法与上面类似。由于这些操作更加简单,就不一一讨论。下面概述其它类型的 RTTI 信息的情况:

LongString、WideString 和 Variant 没有 RTTI 信息;
ShortString 只有 MaxLength 信息;
浮点数类型只有 FloatType: TFloatType 信息;
TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr);
Int64 只有最大值和最小值信息(也是 64 位整数表示);
Interface 和动态数组不太熟悉,就不作介绍了。

===========================================================
⊙ 结束
===========================================================

 

 

 

 

Delphi Enum to String                                                                                 
Sunday, August 7, 2005, 11:37 AM
From Zarko Gajic,
Your Guide to Delphi Programming.
FREE Newsletter. Sign Up Now!
Here's how to display the string represention of a Delphi enum type at runtime.
~~~~~~~~~~~~~~~~~~~~~~~~~
{
Suppose enum:

TProgrammerType = (tpDelphi, tpVisualC, tpVB, tpJava) ;
}

uses TypInfo;
var s: string;
s := GetEnumName(TypeInfo(TProgrammerType),
integer(tpDelphi)) ;

//s='tpDelphi'




Using RTTI in Delphi                                                                                   
Sunday, August 7, 2005, 11:27 AM
Using RTTI in Delphi
From Zarko Gajic,
Your Guide to Delphi Programming.
FREE Newsletter. Sign Up Now!

Runtime Type Information Unleashed
Delphi provided Runtime Type Information (RTTI) more than a decade ago. Yet even today many developers aren't fully aware of its risks and benefits. This article provides an overview of the RTTI usage in Delphi for Win32.

In short, Runtime Type Information is information about an object's data type that is set into memory at run-time.
You are probably using this powerful but unfortunately inadequately documented feature of Delphi in your every day projects.

RTTI provides a way to determine if an object's type is that of a particular class or one of its descendants. The is and as operators commonly used in Delphi language, rely on the presence of some form of RTTI. The is operator, which performs dynamic type checking, is used to verify the actual runtime class of an object. The as operator performs checked typecasts.
In Delphi 7 (and 2005) the typinfo unit defines type, classes and objects you use when working with the RTTI.

RTTI in Delphi - Explained
The "Run-Time Type Information In Delphi - Can It Do Anything For You?" article by Brian Long provides a great introduction to the RTTI capabilities of Delphi. Brian explains that the RTTI support in Delphi has been added first and foremost to allow the design-time environment to do its job, but that developers can also take advantage of it to achieve certain code simplifications.
This article also provides a great overview of the RTTI classes along with a few examples. Examples include: Reading and writing arbitrary properties, common properties with no common ancestor, copying properties from one component to another, etc.

RTTI in Delphi - An Example
Here's a small example of the powers of the RTTI. This example shows how to obtain the ancestry of a component using the ClassType and ClassParent properties. It uses a button and a list box on a form. When the user clicks the button, the name of the button’s class and the names of its parent classes are added to the list box (found in the Delphi Help).
~~~~~~~~~~~~~~~~~~~~~~~~~
procedure TForm1.Button1Click(Sender: TObject) ;

var
ClassRef: TClass;
begin
ListBox1.Clear;
ClassRef := Sender.ClassType;
while ClassRef <> nil do
begin
ListBox1.Items.Add(ClassRef.ClassName) ;
ClassRef := ClassRef.ClassParent;
end;

end;
~~~~~~~~~~~~~~~~~~~~~~~~~
The list box contains the following strings after the user clicks the button:

TButton
TButtonControl
TWinControl
TControl
TComponent
TPersistent
TObject



RTTI in Delphi - More Examples
How to execute a method (procedure/function) by name
Converting between enumerated type values and strings
How to set the DataSource property to several db-aware controls in one call
Get the list of events with attached event handlers
How to to write to a read-only property
RTTI is really great. You can use it in many different ways to those provided here. I hope you'll find plenty other ways to exploit this remarkably under-used feature of Delphi.



Transparency in Delphi 6                                                                
Saturday, August 6, 2005, 04:44 AM
Transparency in Delphi 6

How to create a transparent form with Delphi 6 - allowing windows behind the form to completely show through.

One of the great new features Delphi 6 introduced to Windows developers is the ability to create forms (windows) that are (semi) transparent. In Delphi 6, the TForm class supports layered forms with AlphaBlend, AlphaBlendValue, TransparentColor, and TransparentColorValue properties.

For a form being transparent means that a user can see through the form - the form merges its content with what's behind it on the screen.

To "prepare" a form for transparency you need to set the AlphaBlend property to True. Once AlphaBlend is True, the AlphaBlendValue property specifies the degree of transparency. This property accepts values from 0 to 255. A value of 0 indicates a completely transparent window. A value of 255 indicates complete opacity.

As with all published properties you set the AlphaBlend and AlphaBlendValue at design time (or run time) with the Object Inspector.

The following About box form has the AlphaBlendValue set to 210.

You might think that you'll seldom need this new Delphi feature in a business application, here is an example that, even in a business application, will attract your users:

procedure TAboutBox.FormClose
(Sender: TObject; var Action: TCloseAction);
var
i, cavb : 0..255;
begin
if AlphaBlend=False then
begin
AlphaBlendValue:=255;
AlphaBlend:=True;
end;
cavb:=AlphaBlendValue;

for i := cavb downto 0 do
begin
AlphaBlendValue := i;
Application.ProcessMessages;
end
end;

The above code, the OnClose event of an about box form, creates a fading effect. When the user tries to close the form - the form slowly vanishes. The trick is simple, just decrease the AlphaBlendValue to 0.

Another two new form properties in Delphi 6 are TransparentColor and TransparentColorValue. The TransparentColor is a boolean property that specifies whether a color on a form, specified in the TransparentColorValue property, appears transparent.

As an example, set TransparentColor for our About box form to True. Let the TransparentColorValue be clWhite - that is white. The Memo component has a white background, this is how our About box now looks:

And finally, there's one glitch. All the properties discussed in this article have no effect if your application is not running under Windows 2000 or better, with the machine P90 or better.

If you have any questions, or you know of a great example where AlphaBlending can be handy, please post to the Delphi Programming Forum.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值