Ø TReader
先来看Delphi的工程文件,会发现类似这样的几行代码:
begin
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
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文件中看到的是树状的组件结构。