Delphi的元件讀寫機制(三) (轉)

amyz發表於2007-08-15
Delphi的元件讀寫機制(三) (轉)[@more@]

:namespace prefix = o ns = "urn:schemas--com::office" />

Ø  TReader

  先來看的工程,會發現類似這樣的幾行程式碼:

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的ReadComponent方法。該方法的作用是讀出根元件及其所擁有的全部元件。來看一下該方法的實現:

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 := TClasinder.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, Vant,

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 (FAncestorP< 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檔案中看到的是樹狀的元件結構。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-957921/,如需轉載,請註明出處,否則將追究法律責任。

相關文章