|
导读一、流式对象(Stream)和读写对象(Filer)的介绍 在面向对象程序设计中,对象式数据管理占有很重要的地位。在Delphi中,对对象式数据管理的支持方式是其一大特色。 Delphi是一个面向... 一、流式对象(Stream)和读写对象(Filer)的介绍 对象式数据管理包括两方面的内容: Delphi将对象式数据管理类归结为Stream对象(Stream)和Filer对象(Filer),并将它们应用于可视组件类库(VCL)的方方面面。它们提供了丰富的在内存、外存和Windows资源中管理对象的功能, 为了对流式对象和读写对象有一个感性的认识,先来看一个例子。 值得注意的是,读写对象如TFiler对象、TReader对象和TWriter对象等很少由应用程序编写者进行直接的调用,它通常用来读写组件的状态,它在读写组件机制中扮演着非常重要的角色。 二、读写对象(Filer)与组件读写机制 先来看一下TFiler类的定义: TFiler = class(TObject) private FStream: TStream; FBuffer: Pointer; FBufSize: Integer; FBufPos: Integer; FBufEnd: Integer; FRoot: TComponent; FLookupRoot: TComponent; FAncestor: TPersistent; FIgnoreChildren: Boolean; protected procedure SetRoot(Value: TComponent); virtual; public constructor 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: TTimer Left = 184 Top = 149 end Delphi的流系统只能保存published数据,但TTimer并没有published的Left和Top属性,那么这些数据是怎么被保存下来的呢? TTimer是TComponent的派生类,在TComponent类中我们发现有这样的一个函数: procedure TComponent.DefineProperties(Filer: TFiler); var Ancestor: TComponent; Info: Longint; begin Info := 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。 先来看Delphi的工程文件,会发现类似这样的几行代码: begin Application.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; …… begin ReadSignature; Result := nil; GlobalNameSpace.BeginWrite; // Loading from stream adds to name space try try ReadPrefix(Flags, I); if Root = nil then begin Result := TComponentClass(FindClass(ReadStr)).Create(nil); Result.Name := ReadStr; end else begin Result := Root; ReadStr; { Ignore class name } if csDesigning in Result.ComponentState then ReadStr else begin Include(Result.FComponentState, csLoading); Include(Result.FComponentState, csReading); Result.Name := FindUniqueName(ReadStr); end; end; FRoot := Result; FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True); try FLookupRoot := Result; G := GlobalLoaded; if G <> nil then FLoaded := G else FLoaded := TList.Create; try if FLoaded.IndexOf(FRoot) < 0 then FLoaded.Add(FRoot); FOwner := FRoot; Include(FRoot.FComponentState, csLoading); Include(FRoot.FComponentState, csReading); FRoot.ReadState(Self); Exclude(FRoot.FComponentState, csReading); if G = nil then for I := 0 to FLoaded.Count - 1 do TComponent(FLoaded[I]).Loaded; finally if G = nil then FLoaded.Free; FLoaded := nil; end; finally FFinder.Free; end; …… finally GlobalNameSpace.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); var OldParent, OldOwner: TComponent; begin while not EndOfList do ReadProperty(Instance); ReadListEnd; OldParent := Parent; OldOwner := Owner; Parent := Instance.GetChildParent; try Owner := Instance.GetChildOwner; if not Assigned(Owner) then Owner := Root; while not EndOfList do ReadComponent(nil); ReadListEnd; finally Parent := 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) else begin { Cannot reliably recover from an error in a defined property } FCanHandleExcepts := False; Instance.DefineProperties(Self); FCanHandleExcepts := True; if FPropName <> '' then PropertyError(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); begin if SameText(Name, FPropName) and Assigned(ReadData) then begin ReadData(Self); FPropName := ''; end; end; 它先去比较读取的属性名是否和预设的属性名相同,如果相同并且读方法ReadData不为空时就调用ReadData方法读取属性值。 好了,根组件已经读上来了,接下来应该是读该根组件所拥有的组件了。再来看方法: procedure TReader.ReadDataInner(Instance: TComponent); 该方法后面有一句这样的代码: while not EndOfList do ReadComponent(nil); 这正是用来读取子组件的。子组件的读取机制是和上面所介绍的根组件的读取一样的,这是一个树的深度遍历。 到这里为止,组件的读机制已经介绍完了。
再来看组件的写机制。当我们在窗体上添加一个组件时,它的相关的属性就会保存在DFM文件中,这个过程就是由TWriter来完成的。
Ø TWriter TWriter 对象是可实例化的往流中写数据的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 then WriteStr(GetTypeData(PTypeInfo(Instance.ClassType.ClassInfo)).UnitName + '.' + Instance.ClassName) else WriteStr(Instance.ClassName); WriteStr(Instance.Name); PropertiesPosition := Position; if (FAncestorList <> nil) and (FAncestorPos < FAncestorList.Count) then begin if 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); …… begin Count := GetTypeData(Instance.ClassInfo)^.PropCount; if Count > 0 then begin GetMem(PropList, Count * SizeOf(Pointer)); try GetPropInfos(Instance.ClassInfo, PropList); for I := 0 to Count - 1 do begin PropInfo := PropList^[I]; if PropInfo = nil then Break; if IsStoredProp(Instance, PropInfo) then WriteProperty(Instance, PropInfo); end; finally FreeMem(PropList, Count * SizeOf(Pointer)); end; end; Instance.DefineProperties(Self); end; 请看下面的代码: if IsStoredProp(Instance, PropInfo) then WriteProperty(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 then try if (FAncestor <> nil) and (FAncestor is TComponent) then begin if (FAncestor is TComponent) and (csInline in TComponent(FAncestor).ComponentState) then FRootAncestor := TComponent(FAncestor); FAncestorList := TList.Create; TComponent(FAncestor).GetChildren(AddAncestor, FRootAncestor); end; if csInline in Instance.ComponentState then FRoot := Instance; Instance.GetChildren(WriteComponent, FRoot); finally FAncestorList.Free; end; end; IgnoreChildren属性使一个Writer对象存储组件时可以不存储该组件拥有的子组件。如果IgnoreChildren属性为True,则Writer对象存储组件时不存它拥有的子组件。否则就要存储子组件。 Instance.GetChildren(WriteComponent, FRoot); 这是写子组件的最关键的一句,它把WriteComponent方法作为回调函数,按照深度优先遍历树的原则,如果根组件FRoot存在子组件,则用WriteComponent来保存它的子组件。这样我们在DFM文件中看到的是树状的组件结构。 |
温馨提示:喜欢本站的话,请收藏一下本站!