Delphi 的组件读写机制 一、流式对象 (Stream) 和读写对象( Filer )的介绍 在面向对象程序设计中,对象式数据管理占有很重要的地位。在 Delphi 中,对对象式数据管理的支持方式是其一大特色。 Delphi 是一个面向对象的可视化设计与面向对象的语言相结合的集成开发环境。 Delphi 的核心是组件。组件是对象的一种。 Delphi 应用程序完全是由组件来构造的,因此开发高性能的 Delphi 应用程序必然会涉及对象式数据管理技术。 对象式数据管理包括两方面的内容: ● 用对象来管理数据 ● 对各类数据对象(包括对象和组件)的管理 Delphi 将对象式数据管理类归结为 Stream 对象 (Stream) 和 Filer 对象 (Filer) ,并将它们应用于可视组件类库( VCL )的方方面面。它们提供了丰富的在内存、外存和 Windows 资源中管理对象的功能, Stream 对象,又称流式对象,是 TStream 、 THandleStream 、 TFileStream 、 TMemoryStream 、 TResourceStream 和 TBlobStream 等的统称。它们分别代表了在各种媒介上存储数据的能力,它们将各种数据类型 ( 包括对象和组件 ) 在内存、外存和数据库字段中的管理操作抽象为对象方法,并且充分利用了面向对象技术的优点,应用程序可以相当容易地在各种 Stream 对象中拷贝数据。 读写对象( Filer )包括 TFiler 对象、 TReader 对象和 TWriter 对象。 TFiler 对象是文件读写的基础对象,在应用程序中使用的主要是 TReader 和 TWriter 。 TReader 和 TWriter 对象都直接从 TFiler 对象继承。 TFiler 对象定义了 Filer 对象的基本属性和方法。 Filer 对象主要完成两大功能: ● 存取窗体文件和窗体文件中的组件 ● 提供数据缓冲,加快数据读写操作 为了对流式对象和读写对象有一个感性的认识,先来看一个例子。 a) 写文件 procedure TFomr1.WriteData (Sender: TObject); r;VarFileStream:TFilestream;Mywriter:TWriter;i: integerBeginFileStream:=TFilestream.create( ' c:Test.txt ' ,fmopenwrite);// 创建文件流对象 Mywriter:=TWriter.create(FileStream,1024); // 把 Mywriter 和 FileStream 联系起来 Mywriter. writelistbegin; // 写入列表开始标志 For i:=0 to Memo1.lines.count-1 do Mywriter.writestring(memo1.lines [ i ] ); // 保存 Memo 组件中文本信息到文件中 Mywriter.writelistend; // 写入列表结束标志 FileStream.seek(0,sofrombeginning); // 文件流对象指针移到流起始位置 Mywriter.free; // 释放 Mywriter 对象 FileStream.free; // 释放 FileStream 对象 End;b) 读文件 procedure TForm1.ReadData(Sender: TObject);VarFileStream:TFilestream;Myreader:TReader;BeginFileStream:=TFilestream.create( ' c:Test.txt ' ,fmopenread);Myreader:=TRreader.create(FileStream,1024); // 把 Myreader 和 FileStream 联系起来 Myreader.readlistbegin; // 把写入的列表开始标志读出来 Memo1.lines.clear; // 清除 Memo1 组件的文本内容 While not myreader.endoflist do // 注意 TReader 的一个方法 :endoflistBeginMemo1.lines.add(myreader.readstring); // 把读出的字符串加到 Memo1 组件中 End;Myreader.readlistend; // 把写入的列表结束标志读出来 Myreader.free; // 释放 Myreader 对象 FileStream.free; // 释放 FileStream 对象 End; 上面两个过程,一个为写过程,另一个为读过程。写过程通过 TWriter ,利用 TFilestream 把一个 Memo 中的内容(文本信息)存为一个保存在磁盘上的二进制文件。读过程刚好和写过程相反,通过 TReader ,利用 TFilestream 把二进制文件中的内容转换为文本信息并显示在 Memo 中。运行程序可以看到,读过程忠实的把写过程所保存的信息进行了还原。 下图描述了数据对象(包括对象和组件)、流式对象和读写对象之间的关系。 图(一) 值得注意的是,读写对象如 TFiler 对象、 TReader 对象和 TWriter 对象等很少由应用程序编写者进行直接的调用,它通常用来读写组件的状态,它在读写组件机制中扮演着非常重要的角色。 对于流式对象 Stream ,很多参考资料上都有很详细的介绍,而 TFiler 对象、 TReader 对象和 TWriter 对象特别是组件读写机制的参考资料则很少见,本文将通过对 VCL 原代码的跟踪而对组件读写机制进行剖析。 二、读写对象 (Filer) 与组件读写机制 Filer 对象主要用于存取 Delphi 的窗体文件和窗体文件中的组件,所以要清楚地理解 Filer 对象就要清楚 Delphi 窗体文件 (DFM 文件 ) 的结构。 DFM 文件是用于 Delphi 存储窗体的。窗体是 Delphi 可视化程序设计的核心。窗体对应 Delphi 应用程序中的窗口,窗体中的可视组件对应窗口中的界面元素,非可视组件如 TTimer 和 TOpenDialog ,对应 Delphi 应用程序的某项功能。 Delphi 应用程序的设计实际上是以窗体的设计为中心。因此, DFM 文件在 Delphi 应用设计中也占很重要的位置。窗体中的所有元素包括窗体自身的属性都包含在 DFM 文件中。 在 Delphi 应用程序窗口中,界面元素是按拥有关系相互联系的,因此树状结构是最自然的表达形式;相应地,窗体中的组件也是按树状结构组织;对应在 DFM 文件中,也要表达这种关系。 DFM 文件在物理上,是以文本方式存储的(在 Delphi2.0 版本以前是存储为二进制文件的),在逻辑上则是以树状结构安排各组件的关系。从该文本中可以看清窗体的树状结构。下面是 DFM 文件的内容: object Form1: TForm1Left = 197Top = 124 …… PixelsPerInch = 96TextHeight = 13object Button1: TButtonLeft = 272 …… Caption = ' Button1 ' TabOrder = 0endobject Panel1: TPanelLeft = 120 …… Caption = ' Panel1 ' TabOrder = 1object CheckBox1: TCheckBoxLeft = 104 …… Caption = ' CheckBox1 ' TabOrder = 0endendend 这个 DFM 文件就是 TWriter 通过流式对象 Stream 来生成的,当然这里还有一个二进制文件到文本信息文件的转换过程,这个转换过程不是本文要研究的对象,所以忽略这样的一个过程。 在程序开始运行的时候, TReader 通过流式对象 Stream 来读取窗体及组件,因为 Delphi 在编译程序的时候,利用编译指令 {$R *.dfm} 已经把 DFM 文件信息编译到可执行文件中,因此 TReader 读取的内容实际上是被编译到可执行文件中的有关窗体和组件的信息。 TReader 和 TWriter 不仅能够读取和写入 Object Pascal 中绝大部分标准数据类型,而且能够读写 List 、 Variant 等高级类型,甚至能够读写 Perperties 和 Component 。不过, TReader 、 TWriter 自身实际上提供的功能很有限,大部分实际的工作是由 TStream 这个非常强大的类来完成的。也就是说 TReader 、 TWriter 实际上只是一个工具,它只是负责怎么去读写组件,至于具体的读写操作是由 TStream 来完成的。 由于 TFiler 是 TReader 和 TWriter 的公共祖先类,因为要了解 TReader 和 TWriter ,还是先从 TFiler 开始。 先来看一下 TFiler 类的定义: TFiler = class(TObject)privateFStream: TStream;FBuffer: Pointer;FBufSize: Integer;FBufPos: Integer;FBufEnd: Integer;FRoot: TComponent;FLookupRoot: TComponent;FAncestor: TPersistent;FIgnoreChildren: Boolean;protectedprocedure SetRoot(value: TComponent); virtual;publicconstructor Create(Stream: TStream; BufSize: Integer);destructor Destroy; override;procedure DefineProperty(const Name: string;ReadData: TReaderProc; WriteData: TWriterProc;HasData: Boolean); virtual; abstract;procedure DefineBinaryProperty(const Name: string;ReadData, WriteData: TStreamProc;HasData: Boolean); virtual; abstract;procedure FlushBuffer; virtual; abstract;property Root: TComponent read FRoot write SetRoot;property LookupRoot: TComponent read FLookupRoot;property Ancestor: TPersistent read FAncestor write FAncestor;property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;end;TFiler 对象是 TReader 和 TWriter 的抽象类,定义了用于组件存储的基本属性和方法。它定义了 Root 属性, Root 指明了所读或写的组件的根对象,它的 Create 方法将 Stream 对象作为传入参数以建立与 Stream 对象的联系, Filer 对象的具体读写操作都是由 Stream 对象完成。因此,只要是 Stream 对象所能访问的媒介都能由 Filer 对象存取组件。 TFiler 对象还提供了两个定义属性的 public 方法: DefineProperty 和 DefineBinaryProperty ,这两个方法使对象能读写不在组件 published 部分定义的属性。下面重点介绍一下这两个方法。 Defineproperty ( ) 方法用于使标准数据类型持久化,诸如字符串、整数、布尔、字符、浮点和枚举。 在 Defineproperty 方法中。 Name 参数用于指定应写入 DFM 文件的属性的名称,该属性不在类的 published 部分定义。 ReadData 和 WriteData 参数指定在存取对象时读和写所需数据的方法。 ReadData 参数和 WriteData 参数的类型分别是 TReaderProc 和 TWriterProc 。这两个类型是这样声明的: TReaderProc = procedure(Reader: TReader) of object;TWriterProc = procedure(Writer: TWriter) of object;HasData 参数在运行时决定了属性是否有数据要存储。 DefineBinaryProperty 方法和 Defineproperty 有很多的相同之处,它用来存储二进制数据,如声音和图象等。 下面来说明一下这两个方法的用途。 我们在窗体上放一个非可视化组件如 TTimer ,重新打开窗体时我们发现 TTimer 还是在原来的地方,但 TTimer 没有 Left 和 Top 属性啊,那么它的位置信息保存在哪里呢? 打开该窗体的 DFM 文件,可以看到有类似如下的几行内容: object Timer1: TTimerLeft = 184Top = 149endDelphi 的流系统只能保存 published 数据,但 TTimer 并没有 published 的 Left 和 Top 属性,那么这些数据是怎么被保存下来的呢? TTimer 是 TComponent 的派生类,在 TComponent 类中我们发现有这样的一个函数: procedure TComponent.DefineProperties(Filer: TFiler);varAncestor: TComponent;Info: Longint;beginInfo := 0;Ancestor := TComponent(Filer.Ancestor);if Ancestor <> nil then Info := Ancestor.FDesignInfo;Filer.DefineProperty( ' Left ' , ReadLeft, WriteLeft,LongRec(FDesignInfo).Lo <> LongRec(Info).Lo);Filer.DefineProperty( ' Top ' , ReadTop, WriteTop,LongRec(FDesignInfo).Hi <> LongRec(Info).Hi);end;TComponent 的 DefineProperties 是覆盖了它的祖先类 TPersistent 的方法,在 TPersistent 类中该方法为空的虚方法。 在 DefineProperties 方法中,我们可以看出,有一个 Filer 对象作为它的参数,当定义属性时,它引用了 Ancestor 属性,如果该属性非空,对象应当只读写与从 Ancestor 继承的不同的属性的值。它调用 TFiler 的 DefineProperty 方法,并定义了 ReadLeft , WriteLeft , ReadTop , WriteTop 方法来读写 Left 和 Top 属性。 因此,凡是从 TComponent 派生的组件,即使它没有 Left 和 Top 属性,在流化到 DFM 文件中,都会存在这样的两个属性。 在查找资料的过程中,发现很少有资料涉及到组件读写机制的。由于组件的写过程是在设计阶段由 Delphi 的 IDE 来完成的,因此无法跟踪它的运行过程。所以笔者是通过在程序运行过程中跟踪 VCL 原代码来了解组件的读机制的,又通过读机制和 TWriter 来分析组件的写机制。所以下文将按照这一思维过程来讲述组件读写机制,先讲 TReader ,而后是 TWriter 。 TReader 先来看 Delphi 的工程文件,会发现类似这样的几行代码: beginApplication.Initialize;Application.CreateForm(TForm1, Form1);Application.Run;end. 这是 Delphi 程序的入口。简单的说一下这几行代码的意义: Application.Initialize 对开始运行的应用程序进行一些必要的初始化工作, Application.CreateForm(TForm1, Form1) 创建必要的窗体, Application.Run 程序开始运行,进入消息循环。 现在我们最关心的是创建窗体这一句。窗体以及窗体上的组件是怎么创建出来的呢?在前面已经提到过:窗体中的所有组件包括窗体自身的属性都包含在 DFM 文件中,而 Delphi 在编译程序的时候,利用编译指令 {$R *.dfm} 已经把 DFM 文件信息编译到可执行文件中。因此,可以断定创建窗体的时候需要去读取 DFM 信息,用什么去读呢,当然是 TReader 了! 通过对程序的一步步的跟踪,可以发现程序在创建窗体的过程中调用了 TReader 的 ReadRootComponent 方法。该方法的作用是读出根组件及其所拥有的全部组件。来看一下该方法的实现: function TReader.ReadRootComponent(Root: TComponent): TComponent; …… beginReadSignature;Result := nil;GlobalNameSpace.BeginWrite; // Loading from stream adds to name spacetrytryReadPrefix(Flags, I);if Root = nil thenbeginResult := TComponentClass(FindClass(ReadStr)).Create(nil);Result.Name := ReadStr;end elsebeginResult := Root;ReadStr; { Ignore class name }if csDesigning in Result.ComponentState thenReadStr elsebeginInclude(Result.FComponentState, csLoading);Include(Result.FComponentState, csReading);Result.Name := FindUniqueName(ReadStr);end;end;FRoot := Result;FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True);tryFLookupRoot := Result;G := GlobalLoaded;if G <> nil thenFLoaded := G elseFLoaded := TList.Create;tryif FLoaded.IndexOf(FRoot) < 0 thenFLoaded.Add(FRoot);FOwner := FRoot;Include(FRoot.FComponentState, csLoading);Include(FRoot.FComponentState, csReading);FRoot.ReadState(Self);Exclude(FRoot.FComponentState, csReading);if G = nil thenfor I := 0 to FLoaded.Count - 1 do TComponent(FLoaded[I]).Loaded;finallyif G = nil then FLoaded.Free;FLoaded := nil;end;finallyFFinder.Free;end; …… finallyGlobalNameSpace.EndWrite;end;end;ReadRootComponent 首先调用 ReadSignature 读取 Filer 对象标签(' TPF0 ')。载入对象之前检测标签,能防止疏忽大意,导致读取无效或过时的数据。 再看一下 ReadPrefix(Flags, I) 这一句, ReadPrefix 方法的功能与 ReadSignature 的很相象,只不过它是读取流中组件前面的标志 (PreFix) 。当一个 Write 对象将组件写入流中时,它在组件前面预写了两个值,第一个值是指明组件是否是从祖先窗体中继承的窗体和它在窗体中的位置是否重要的标志 ; 第二个值指明它在祖先窗体创建次序。 然后,如果 Root 参数为 nil ,则用 ReadStr 读出的类名创建新组件,并从流中读出组件的 Name 属性;否则,忽略类名,并判断 Name 属性的唯一性。 FRoot.ReadState(Self); 这是很关键的一句, ReadState 方法读取根组件的属性和其拥有的组件。这个 ReadState 方法虽然是 TComponent 的方法,但进一步的跟踪就可以发现,它实际上最终还是定位到了 TReader 的 ReadDataInner 方法,该方法的实现如下: procedure TReader.ReadDataInner(Instance: TComponent);varOldParent, OldOwner: TComponent;beginwhile not EndOfList do ReadProperty(Instance);ReadListEnd;OldParent := Parent;OldOwner := Owner;Parent := Instance.GetChildParent;tryOwner := Instance.GetChildOwner;if not Assigned(Owner) then Owner := Root;while not EndOfList do ReadComponent(nil);ReadListEnd;finallyParent := OldParent;Owner := OldOwner;end;end; 其中有这样的这一行代码: while not EndOfList do ReadProperty(Instance); 这是用来读取根组件的属性的,对于属性,前面提到过,既有组件本身的 published 属性,也有非 published 属性,例如 TTimer 的 Left 和 Top 。对于这两种不同的属性,应该有两种不同的读方法,为了验证这个想法,我们来看一下 ReadProperty 方法的实现。 procedure TReader.ReadProperty(AInstance: TPersistent); …… begin …… PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);if PropInfo <> nil then ReadPropvalue(Instance, PropInfo) elsebegin{ Cannot reliably recover from an error in a defined property }FCanHandleExcepts := False;Instance.DefineProperties(Self);FCanHandleExcepts := True;if FPropName <> '' thenPropertyError(FPropName);end; …… end; 为了节省篇幅,省略了一些代码,这里说明一下: FPropName 是从文件读取到的属性名。 PropInfo := GetPropInfo(Instance.ClassInfo, FPropName); 这一句代码是获得 published 属性 FPropName 的信息。从接下来的代码中可以看到,如果属性信息不为空,就通过 ReadPropvalue 方法读取属性值,而 ReadPropvalue 方法是通过 RTTI 函数来读取属性值的,这里不再详细介绍。如果属性信息为空,说明属性 FPropName 为非 published 的,它就必须通过另外一种机制去读取。这就是前面提到的 DefineProperties 方法,如下: Instance.DefineProperties(Self); 该方法实际上调用的是 TReader 的 DefineProperty 方法: procedure TReader.DefineProperty(const Name: string;ReadData: TReaderProc; WriteData: TWriterProc; HasData: Boolean);beginif SameText(Name, FPropName) and Assigned(ReadData) thenbeginReadData(Self);FPropName := '' ;end;end; 它先去比较读取的属性名是否和预设的属性名相同,如果相同并且读方法 ReadData 不为空时就调用 ReadData 方法读取属性值。 好了,根组件已经读上来了,接下来应该是读该根组件所拥有的组件了。再来看方法: procedure TReader.ReadDataInner(Instance: TComponent); 该方法后面有一句这样的代码: while not EndOfList do ReadComponent(nil); 这正是用来读取子组件的。子组件的读取机制是和上面所介绍的根组件的读取一样的,这是一个树的深度遍历。 到这里为止,组件的读机制已经介绍完了。 再来看组件的写机制。当我们在窗体上添加一个组件时,它的相关的属性就会保存在 DFM 文件中,这个过程就是由 TWriter 来完成的。 ? TWriterTWriter 对象是可实例化的往流中写数据的 Filer 对象。 TWriter 对象直接从 TFiler 继承而来,除了覆盖从 TFiler 继承的方法外,还增加了大量的关于写各种数据类型 ( 如 Integer 、 String 和 Component 等 ) 的方法。 TWriter 对象提供了许多往流中写各种类型数据的方法, TWrite 对象往流中写数据是依据不同的数据采取不同的格式的。 因此要掌握 TWriter 对象的实现和应用方法,必须了解 Writer 对象存储数据的格式。 首先要说明的是,每个 Filer 对象的流中都包含有 Filer 对象标签。该标签占四个字节其值为" TPF0 "。 Filer 对象为 WriteSignature 和 ReadSignature 方法存取该标签。该标签主要用于 Reader 对象读数据 ( 组件等 ) 时,指导读操作。 其次, Writer 对象在存储数据前都要留一个字节的标志位,以指出后面存放的是什么类型的数据。该字节为 TvalueType 类型的值。 TvalueType 是枚举类型,占一个字节空间,其定义如下: TvalueType = (VaNull, VaList, VaInt8, VaInt16, VaInt32, VaEntended, VaString, VaIdent, VaFalse, VaTrue, VaBinary, VaSet, VaLString, VaNil, VaCollection); 因此,对 Writer 对象的每一个写数据方法,在实现上,都要先写标志位再写相应的数据 ; 而 Reader 对象的每一个读数据方法都要先读标志位进行判断,如果符合就读数据,否则产生一个读数据无效的异常事件。 VaList 标志有着特殊的用途,它是用来标识后面将有一连串类型相同的项目,而标识连续项目结束的标志是 VaNull 。因此,在 Writer 对象写连续若干个相同项目时,先用 WriteListBegin 写入 VaList 标志,写完数据项目后,再写出 VaNull 标志;而读这些数据时,以 ReadListBegin 开始, ReadListEnd 结束,中间用 EndofList 函数判断是否有 VaNull 标志。 来看一下 TWriter 的一个非常重要的方法 WriteData : procedure TWriter.WriteData(Instance: TComponent); …… begin …… WritePrefix(Flags, FChildPos);if UseQualifiedNames thenWriteStr(GetTypeData(PTypeInfo(Instance.ClassType.ClassInfo)).UnitName + ' . ' + Instance.ClassName)elseWriteStr(Instance.ClassName);WriteStr(Instance.Name);PropertiesPosition := Position;if (FAncestorList <> nil) and (FAncestorPos < FAncestorList.Count) thenbeginif Ancestor <> nil then Inc(FAncestorPos);Inc(FChildPos);end;WriteProperties(Instance);WriteListEnd; …… end; 从 WriteData 方法中我们可以看出生成 DFM 文件信息的概貌。先写入组件前面的标志 (PreFix) ,然后写入类名、实例名。紧接着有这样的一条语句: WriteProperties(Instance); 这是用来写组件的属性的。前面提到过,在 DFM 文件中,既有 published 属性,又有非 published 属性,这两种属性的写入方法应该是不一样的。来看 WriteProperties 的实现: procedure TWriter.WriteProperties(Instance: TPersistent); …… beginCount := GetTypeData(Instance.ClassInfo)^.PropCount;if Count > 0 thenbeginGetMem(PropList, Count * SizeOf(Pointer));tryGetPropInfos(Instance.ClassInfo, PropList);for I := 0 to Count - 1 dobeginPropInfo := PropList^[I];if PropInfo = nil thenBreak;if IsStoredProp(Instance, PropInfo) thenWriteProperty(Instance, PropInfo);end;finallyFreeMem(PropList, Count * SizeOf(Pointer));end;end;Instance.DefineProperties(Self);end; 请看下面的代码: if IsStoredProp(Instance, PropInfo) thenWriteProperty(Instance, PropInfo); 函数 IsStoredProp 通过存储限定符来判断该属性是否需要保存,如需保存,就调用 WriteProperty 来保存属性,而 WriteProperty 是通过一系列的 RTTI 函数来实现的。 Published 属性保存完后就要保存非 published 属性了,这是通过这句代码完成的: Instance.DefineProperties(Self);DefineProperties 的实现前面已经讲过了, TTimer 的 Left 、 Top 属性就是通过它来保存的。 好,到目前为止还存在这样的一个疑问:根组件所拥有的子组件是怎么保存的?再来看 WriteData 方法(该方法在前面提到过): procedure TWriter.WriteData(Instance: TComponent); …… begin …… if not IgnoreChildren thentryif (FAncestor <> nil) and (FAncestor is TComponent) thenbeginif (FAncestor is TComponent) and (csInline in TComponent(FAncestor).ComponentState) thenFRootAncestor := TComponent(FAncestor);FAncestorList := TList.Create;TComponent(FAncestor).GetChildren(AddAncestor, FRootAncestor);end;if csInline in Instance.ComponentState thenFRoot := Instance;Instance.GetChildren(WriteComponent, FRoot);finallyFAncestorList.Free;end;end;IgnoreChildren 属性使一个 Writer 对象存储组件时可以不存储该组件拥有的子组件。如果 IgnoreChildren 属性为 True ,则 Writer 对象存储组件时不存它拥有的子组件。否则就要存储子组件。 Instance.GetChildren(WriteComponent, FRoot); 这是写子组件的最关键的一句,它把 WriteComponent 方法作为回调函数,按照深度优先遍历树的原则,如果根组件 FRoot 存在子组件,则用 WriteComponent 来保存它的子组件。这样我们在 DFM 文件中看到的是树状的组件结构。