Delphi 的持續機制淺探

Delphi 的持續機制淺探


轉自:http://blog.csdn.net/coolbaby/article/details/503917

[email protected]  2004.1.24
http://savetime.delphibbs.com


目 錄
===============================================================================
⊙ DFM 文件與持續機制(persistent)
⊙ ReadComponentResFile / WriteComponentResFile 函數
⊙ Delphi 持續機制框架簡述
⊙ 一個 TForm 對象的創建過程
⊙ TStream Class 和 TStream.ReadComponent 方法
⊙ TReader Class 和 TReader.ReadRootComponent 方法
⊙ TReader.ReadPrefix 方法
⊙ TComponent.ReadState 虛方法
⊙ TReader.ReadData 方法
⊙ TReader.ReadDataInner 方法
⊙ TReader.ReadProperty 方法
⊙ TPersistent.DefineProperties 虛方法
⊙ TReader.ReadComponent 方法
⊙ TReader.ReadValue / TReader.NextValue 系列方法
⊙ TReader.ReadStr 方法
⊙ TReader.ReadInteger / ReadString / ReadBoolean 系列方法
⊙ TReader.Read 方法
⊙ ObjectBinaryToText / ObjectTextToBinary 函數
===============================================================================


本文排版格式爲:
    正文由窗口自動換行;所有代碼以 80 字符爲邊界;中英文字符以空格符分隔。

(作者保留對本文的所有權利,未經作者同意請勿在在任何公共媒體轉載。)


正 文
===============================================================================
⊙ DFM 文件與持續機制(persistent)
===============================================================================
我們在使用 Delphi 的 IDE 進行快速開發的時候,可以方便地從元件面板上拖放元件(component)至表單,完成表單的界面和事件設計。Delphi 將這些界面的設計期信息保存在表單相應的 DFM 文件中,方便程序員隨時讀取和修改。

DFM 文件根據元件在表單上的嵌套層次存放元件屬性,以下是一個 DFM 文件的示例:

  object Form1: TForm1
    ...
    Left = 192
    Top = 107
    Width = 544
    Caption = 'Form1'
    object Button1: TButton
      Left = 24
      Top = 16
      Caption = 'Button1'
      OnClick = Button1Click
    end
    ...
  end

應用程序編譯之後,DFM 文件的信息被二進制化了,這些二進制信息存儲在應用程序的資源(resource)段中。每個表單(也就是 class)及表單上的元件在資源段中存儲爲與表單同名的資源,可以使用 FindResource API 獲得。應用程序在運行期創建表單實例的時候,會從資源段中讀取表單的屬性,還原設計期的設置。這種將類型信息保存在文件中,並且可以在運行期恢復類型的操作,在本文中被稱之爲持續(persistent)機制。持續機制是 Delphi 成爲 RAD 工具的原因之一。

持續機制和 RTTI 是緊密結合的,但本文不討論 RTTI(關於 RTTI 可參考我前幾天寫的兩篇筆記),只討論實現持續機制的總體框架及相關類(class)。這些類包括 TStream、TFiler、TReader、TWriter、TParser、TPersisetent、TComponent、TCustomForm 等。

===============================================================================
⊙ ReadComponentResFile / WriteComponentResFile 函數
===============================================================================
讓我們從一個比較直觀的例子開始。

Classes.pas 中定義了兩個函數 ReadComponentResFile 和 WriteComponentResFile,它們的功能是“把元件的屬性信息保存到文件”和“從文件中恢復元件屬性信息”。

先做個試驗。新建一個項目,在 Form1 上放置兩個 Button 和一個 Memo。Button 的 Click 事件代碼如下。按 F9 運行該項目,先在 Memo1 中輸入一些字符,然後按下 Button1,再按下 Button2,你會看一個新建的 Form。它的屬性幾乎和 Form1 一樣,甚至連 Memo1 中的字符都保存下來了,唯一的不同只是它的 Name 屬性變成了“Form1_1”。你可以查看 FORM1.RES 文件的內容看看 Delphi 是如何存儲元件信息的。

  procedure TForm1.Button1Click(Sender: TObject);
  begin
    WriteComponentResFile('C:/FORM1.RES', Form1);
  end;
  
  procedure TForm1.Button2Click(Sender: TObject);
  var
    NewForm: TForm1;
  begin
    NewForm := TForm1.CreateNew(Application);
    ReadComponentResFile('C:/FORM1.RES', NewForm);
    NewForm.Left := NewForm.Left + 100;
  end;

WriteComponentResFile 函數的代碼如下,它只是調用 Stream 對象的 WriteComponentRes 方法將對象屬性保存到資源文件中的:

  procedure WriteComponentResFile(const FileName: string; Instance: TComponent);
  begin
    Stream := TFileStream.Create(FileName, fmCreate);
    Stream.WriteComponentRes(Instance.ClassName, Instance);
    Stream.Free;
  end;

ReadComponentResFile 函數也是調用 Stream 的方法實現從文件中讀取對屬信息:

  function ReadComponentResFile(const FileName: string; Instance: TComponent):
    TComponent;
  begin
    Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
    Result := Stream.ReadComponentRes(Instance);
    Stream.Free;
  end;

ReadComponentResFile 函數可以通過 Instance 參數傳入對象句柄,也可以通過返回值獲得對象句柄。Instance 參數只能是已實例化的對象或 nil。如果是 nil,那麼 ReadComponentResFile 會自動根據文件信息創建對象實例,但必須使用 RegisterClass 函數註冊將要被載入的類,否則會觸發異常。

有個類似的函數 ReadComponentRes,它從應用程序的資源段中恢復對象的屬性信息。它的 ResName 參數就是表單類的名稱:

  function ReadComponentRes(const ResName: string; Instance: TComponent):
    TComponent;

===============================================================================
⊙ Delphi 持續機制框架簡述
===============================================================================
持續機制的實現必須由 IDE、編譯器、表單類、元件類和輔助類合作完成。

這裏的表單類不是指一般所指的 TForm class,在 Delphi 的幫助文件中,稱之爲“root class”。root class 是指能在設計期被 Form Designer 作爲最上層編輯表單的類(如 TCustomForm、TFrame、TDataModule 等)。Delphi 在設計期將元件的 published 屬性的值保存在 .DFM 文件中,也只有 published 的屬性才能被 Object Insepector 設置賦值。

Form Designer 設計的 root class 對象在編譯時,Delphi 將對象的屬性以及其所包含的元件的屬性保存在應用程序的資源段(RT_RCDATA)中。

輔助類包括 TStream、TReader、TWriter、TParser 等。這些類起着中間層的作用,用於存儲和讀取對象屬性的信息。雖然我稱它們爲輔助類,但是保存和恢復對象信息的實際操作是由它們完成的。

===============================================================================
⊙ 一個 TForm 對象的創建過程
===============================================================================
下面是一個典型的表單 Form1 的創建過程,縮進代表調用關係(Form1.ReadState 例外,防止縮進太多),帶“?”的函數表示我尚未仔細考察的部分,帶“*”表示元件編寫者需要注意的函數。

Application.CreateForm(TForm1, Form1);
  |-Form1.NewInstance;
  |-Form1.Create(Application);
    |-Form1.CreateNew(Application);
    |-InitInheritedComponent(Form1, TForm);
      |-InternalReadComponentRes(Form1.ClassName, Form1ResHInst, Form1);
        |-TResourceStream.Create(Form1ResHInst, Form1.ClassName, RT_RCDATA);
        |-TResourceStream.ReadComponent(Form1);
          |-TReader.Create(ResourceStream, 4096);
          |-TReader.ReadRootComponent(Form1);
            |-TReader.ReadSignature;
           *|-TReader.ReadPrefix(Flags, ChildPos);
            |-IF Form1 = nil THEN Form1 := FindClass(ReadStr).Create;
            |-Include(Form1.FComponentState, csLoading);
            |-Include(Form1.FComponentState, csReading);
            |-Form1.Name := FindUniqueName(ReadStr);
           ?|-FFinder := TClassFinder.Create;
           *|-Form1.ReadState(Reader);
              |-TCustomForm.ReadState(Reader);
                { DisableAlign; }
              |-TWinControl.ReadState(Reader);
                { DisableAlign; }
             *|-TControl.ReadState(Reader);
                { Include(FControlState, csReadingState); }
                { Parent := TWinControl(Reader.Parent);   }
             *|-TComponent.ReadState(Reader);
                |-Reader.ReadData(Form1);
                  |-Reader.ReadDataInner(Form1);
                    |-WHILE NOT EndOfList DO Reader.ReadProperty(Form1);
                      |-IF PropInfo <> nil THEN ReadPropValue(Form1, PropInfo);
                     *|-ELSE Form1.DefineProperties(Reader);
                    |-WHILE NOT EndOfList DO ReadComponent(nil);
                      |-ReadPrefix(Flags, Position);
                      |-IF ffInherited THEN FindExistingComponent
                      |-ELSE CreateComponent;
                     *|-SubComponent.ReadState(Reader); (Like Form1.ReadState)
                 ?|-DoFixupReferences;

過程簡述:

TCustomForm.Create 函數中先調用 CreateNew 設置缺省的表單屬性,然後調用Classes.InitInheritedComponent 函數。

InitInheritedComponent 用於初始化一個 root class 對象。該函數的功能就是從應用程序的資源中恢復設計期的表單信息。InitInheritedComponent 的聲明如下:

  { Classes.pas }
  function InitInheritedComponent(Instance: TComponent;
    RootAncestor: TClass): Boolean;

InitInheritedComponent 傳入兩個參數:Instance 參數代表將要從資源段中恢復信息的對象,RootAncestor 表示該對象的祖先類。如果從資源中恢復信息成功,則返回 True,否則返回 False。InitInheritedComponent 通常只在 root class 的構造函數中調用。

  constructor TCustomForm.Create(AOwner: TComponent);
  begin
    ...
    CreateNew(AOwner);                              // 初始化缺省的 Form 屬性
    Include(FFormState, fsCreating);                // 標記爲 Creating 狀態
    if not InitInheritedComponent(Self, TForm) then // 從資源中恢復 Form 信息
      raise EResNotFound.CreateFmt(SResNotFound, [ClassName]);
    ...
    Exclude(FFormState, fsCreating);                // 取消 Creating 狀態
  end;

InitInheritedComponent 調用自身內置的函數:InitComponent(Instance.ClassType)。InitComponent 先判斷 Instance.ClassType 是否是 TComponent 或 RootAncestor,如果是則返回 False 並退出,否則調用 InternalReadComponentRes。

* InitComponent 遞歸調用自己檢查類信息。沒看懂爲什麼要這樣設計,如果有誰看懂了請告訴我。

  function InitComponent(ClassType: TClass): Boolean;
  begin
    Result := False;
    if (ClassType = TComponent) or (ClassType = RootAncestor) then Exit;
    Result := InitComponent(ClassType.ClassParent);
    Result := InternalReadComponentRes(ClassType.ClassName,
      FindResourceHInstance(FindClassHInstance(ClassType)), Instance) or Result;
  end;

InternalReadComponentRes 使用 Instance.ClassName 作爲 ResourceName,調用 FindResourceHInstance 找到 class 資源所在模塊的 HInst 句柄(因爲 class 可能是在動態鏈接庫中),並通過引用方式傳遞 Instance 對象(* 好像沒有必要使用引用方式,InitInheritedComponent 也沒有使用引用方式):

  { Classes.pas }
  function InternalReadComponentRes(const ResName: string; HInst: THandle;
    var Instance: TComponent): Boolean;

InternalReadComponentRes 先檢查 class 資源是否存在,如果存在則創建一個 TResourceStream 對象(TResourceStream 的 Create 構造函數把 class 信息的資源內存地址和大小記錄在成員字段中),然後使用 TResourceStream.ReadComponent 方法從資源中讀取 Instance 的信息。TResourceStream 並沒有定義 ReadComponent 方法,而是使用祖先類 TStream 的方法。TStream.ReadComponent 創建一個 TReader 對象,然後使用自己的對象地址(Self)作爲參數,調用 TReader.ReadRootComponent 讀取 Instance 對象的內容。

  { TReader }
  function ReadRootComponent(Root: TComponent): TComponent;

ReadRootComponent 先調用 TReader.ReadSignature。ReadSignature 從 stream 中讀取 4 字節的內容,如果讀出來的內容不是 'TPF0',則觸發異常(SInvalidImage),表示該 stream 的內容是錯誤的。然後 ReadRootComponent 調用 ReadPrefix 讀取元件的狀態信息。

如果 Root 參數是 nil,也就是說 Root 對象還沒被創建,則直接從流中讀取 Root 的類名,再使用 FindClass 函數找到該類在內存中的地址,並調用該類的構造函數創建 Root 的實例。

接下來 ReadRootComponent 調用 Root 的 ReadState 虛函數從流中讀取 Root 對象的屬性。TComponent.ReadState 只有一行代碼:Reader.ReadData(Self);。

ReadData 調用 ReadDataInner 讀取 root 元件及 root 的子元件的屬性信息。

ReadDataInner 先循環調用 ReadProperty 從流中讀取 root 元件的屬性,直到遇到 EndOfList 標誌(vaNull)。ReadProperty 使用 RTTI 函數,將從流中讀出的數據設置爲對象的屬性。ReadProperty 中還調用了 Instance.DefineProperties,用於實現自定義的屬性存儲。ReadDataInner 然後循環調用 ReadComponent(nil) 讀取子元件的信息。

ReadComponent 的執行過程與 ReadRootComponent 的過程很相似,它根據流中的信息使用 FindComponentClass 找到元件類在內存中的地址,然後調用該元件類的構造函數創建對象,接下來調用新建對象的 ReadState -> TReader.ReadData -> ReadDataInner -> TReader.ReadProperty,重複 ReadRootComponent 的過程。

TReader.ReadComponent 和 TComponent.ReadState 形成遞歸調用過程,把表單上嵌套的元件創建出來。

最後 InitInheritedComponent 函數返回,一個 root class 對象從資源中實例化的過程完成。

===============================================================================
⊙ TStream Class 和 TStream.ReadComponent 方法
===============================================================================
TStream 在對象持續機制扮演的角色是提供一種存儲媒介,由 TFiler 對象使用。TStream 是一個虛類,它定義了數據的“流式”讀寫方法。它的繼承類 TFileStream、TMemoryStream、TResourceStream 等實現對不同媒體的讀寫。對象的 persistent 信息可以存儲在任何 TStream 類中,也可以從任何 TStream 中獲得。由於 Delphi 缺省的對象信息存儲在應用程序的資源段中,因此,可以從程序的資源段中讀取數據的 TResourceStream 類就顯得更加重要。

TStream 定義兩個讀寫緩衝的方法:ReadBuffer 和 WriteBuffer。這兩個方法封裝了 TStream.Read 和 TStream.Write 純虛方法(必須被後繼類重載)。

  { TStream }
  procedure ReadBuffer(var Buffer; Count: Longint);
  procedure WriteBuffer(const Buffer; Count: Longint);

可以看到這兩個方法的 Buffer 參數都是無類型的,也就是使用引用的方式傳入的,所以不管是使用單個字符或自定義的結構都是正確的(當然,不能使用常量)。Count 指示要讀或寫入的 Buffer 的大小(Bytes)。

TStream 還定義了兩個元件信息的讀寫方法:ReadComponent 和 WriteComponent。由於 WriteComponent 通常是由 Delphi 的 IDE/編譯器調用的,很難跟蹤它的執行過程,所以我們以後主要考察 ReadComponent 方法。我們可以很容易想像這兩個方法互爲逆過程,理解了其中一個也就能知道另一個所做的工作。

  { TStream }
  function ReadComponent(Instance: TComponent): TComponent;
  procedure WriteComponent(Instance: TComponent);

TStream.ReadComponent 創建了一個 TReader 對象,將自己的對象地址作爲參數傳遞給 Reader,並調用 Reader.ReadRootComponent 創建對象實例。

  function TStream.ReadComponent(Instance: TComponent): TComponent;
  var
    Reader: TReader;
  begin
    Reader := TReader.Create(Self, 4096);        // 4096 是緩衝區大小
    Result := Reader.ReadRootComponent(Instance);
    Reader.Free;
  end;

TStream 把自己的對象句柄交給 TReader 之後,就成了 TReader 讀取對象屬性資料的來源。此後 TStream 對象只由 TReader 來掌控,自己不再主動進行其它工作。

===============================================================================
⊙ TReader Class 和 TReader.ReadRootComponent 方法
===============================================================================
TReader 和 TWriter 都是從 TFiler 繼承下來的類。TFiler 是個純虛類,它的構造函數被 TReader 和 TWrite 共享。TFiler.Create 先把 Stream 參數保存在 FStream 字段中,然後生成一個自己的緩衝區:

  constructor TFiler.Create(Stream: TStream; BufSize: Integer);
  begin
    FStream := Stream;          // 保存 stream 對象
    GetMem(FBuffer, BufSize);   // 創建自己的緩衝區,加速數據訪問
    FBufSize := BufSize;        // 設置緩衝區大小
  end;

上面說到 TStream.ReadComponent 在創建 TReader 對象之後,立即調用 TReader.ReadRootComponent 方法。TReader.ReadRootComponent 方法的功能是從 stream 中讀取 root class 對象的屬性。並返回該對象的指針。

  { TReader }
  function ReadRootComponent(Root: TComponent): TComponent;

ReadRootComponent 先調用 TReader.ReadSignature。

TReader.ReadSignature 方法從 stream 中讀取 4 字節的內容,如果讀出來的內容不是 'TPF0',則觸發異常(SInvalidImage),表示該 stream 的內容是錯誤的。'TPF0' 就是 root class 對象的標記。

然後 ReadRootComponent 調用 ReadPrefix 讀取元件的繼承信息。

如果 Root 參數是 nil,也就是說 Root 對象還沒被創建,則直接從流中讀取 Root 的類名,再使用 FindClass 函數找到該類在內存中的地址,並調用該類的構造函數創建 Root 的實例。如果 Root 實例已存在,則調用內嵌的 FindUniquName 函數檢查 Root.Name 是否與已有的實例重複,如有重複則在 Root.Name 後加上序號使其唯一。

接下來 ReadRootComponent 調用 Root 的 ReadState 虛方法從流中讀取 Root 對象的屬性。

===============================================================================
⊙ TReader.ReadPrefix 方法
===============================================================================
ReadPrefix 方法用於讀取元件的狀態信息,這些信息是由 Writer 在寫入元件屬性之前寫入的。

  { TReader }
  procedure ReadPrefix(var Flags: TFilerFlags; var AChildPos: Integer); virtual;

Flags 參數是以引用方式傳遞的,用於設置元件的在表單中的狀態,元件的狀態在這裏包含三種情況:

  ffInherited:表示元件存在於表單的父類之中
  ffChildPos :表示元件在表單中的創建次序(creation order)是重要的
  ffInline   :表示元件是最上級(top-level)的元件,比如表單或數據模塊

如果元件的狀態中包含 ffChildPos,ReadPrefix 還會讀取元件的創建次序值,存放在 AChildPos 參數中。

===============================================================================
⊙ TComponent.ReadState 虛方法
===============================================================================
設置 ReadState 方法的主要目的是在讀取屬性信息的前後可以讓元件進行一些處理工作。ReadState 是 Component Writer 需要注意的方法。

  { TComponent }
  procedure ReadState(Reader: TReader); virtual;

由於 ReadState 是虛函數,在 TControl、TWinControl、TCustomForm 等後續類中都被重載,進行自己需要的操作(比如 DisableAlign、UpdateControlState)。

TComponent.ReadState 只有一行代碼:Reader.ReadData(Self);

注意:自己重載 ReadState 方法必須調用 inherited 。

===============================================================================
⊙ TReader.ReadData 方法
===============================================================================
上面說到 TComponent.ReadState 又回頭調用 TReader.ReadData 方法。它的主要代碼如下:

  { TReader }
  procedure TReader.ReadData(Instance: TComponent);
  begin
    ...
    ReadDataInner(Instance);
    DoFixupReferences;
    ...
  end;

TReader.ReadData 基本上是個包裝函數,它調用 TReader.ReadDataInner 讀取 root 對象及 root 所包含的元件的屬性信息。

===============================================================================
⊙ TReader.ReadDataInner 方法
===============================================================================
ReadDataInner 負責讀取元件的屬性和子元件的屬性,它的主要代碼如下:

  procedure TReader.ReadDataInner(Instance: TComponent);
    begin
    ...
    while not EndOfList do ReadProperty(Instance);
    ...
    while not EndOfList do ReadComponent(nil);
    ...
  end;

ReadDataInner 先循環調用 ReadProperty 從流中讀取對象的屬性,直到遇到 EndOfList 標誌(vaNull)。再循環調用 ReadComponent(nil) 讀取子元件的信息。這兩個方法都是 TReader 的重要方法,後面分兩節討論。ReadDataInner 在ReadProperty 調用之後還設置了元件的 Parent 和 Owner 關係。

===============================================================================
⊙ TReader.ReadProperty 方法
===============================================================================
ReadProperty 使用 RTTI 函數將從流中讀出的數據設置爲對象的屬性。它先解析從流中讀出的屬性名稱,然後判斷該屬性是否有 RTTI 信息,如果有則調用 TReader.ReadPropValue 方法從流中讀取屬性值;如果該屬性沒有 RTTI 信息,說明該屬性不屬於 published 段,而是由元件自己寫入的,因此調用 TPersistent.DefineProperties 讀取自定義的元件信息。ReadProperty 的關鍵代碼:

  procedure TReader.ReadProperty(AInstance: TPersistent);
  begin
    ...
    PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
    if PropInfo <> nil then                             // 檢查屬性 RTTI 信息
      ReadPropValue(Instance, PropInfo)                 // 從流中讀取屬性
    else begin
      Instance.DefineProperties(Self);                  // 調用自定義存儲過程
      if FPropName <> '' then PropertyError(FPropName); // 注意這裏
    end;
    ...
  end;

ReadPropValue 方法基本上是使用 SetOrdProp、SetFloatProp、SetStrProp、GetEnumValue 等 RTTI 函數設置元件的屬性值,它的代碼冗長而簡單,不再單獨列出。下面介紹比較重要的 DefineProperties 函數。

===============================================================================
⊙ TPersistent.DefineProperties 虛方法
===============================================================================
DefineProperties 虛方法用於元件設計者自定義非 published 屬性的存儲和讀取方法。 TPersistent 定義的該方法是個空方法,到 TComponent 之後被重載。

   procedure TPersistent.DefineProperties(Filer: TFiler); virtual;

下面以 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;

DefineProperties 調用 Filer.DefineProperty 或 DefineBinaryProperty 方法讀寫流中屬性值。

TReader.DefineProperty 方法檢查傳入的屬性名稱是否與當前流中讀到的屬性名稱相同,如果相同,則調用傳入的 ReadData 方法讀取數據,並設置 FPropName 爲空,用以通知 ReadProperty 已經完成讀屬性值的工作,否則將會觸發異常。

  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;

TWriter.DefineProperty 根據 HasData 參數決定是否需要寫屬性值。

  procedure TWriter.DefineProperty(const Name: string;
    ReadData: TReaderProc; WriteData: TWriterProc; HasData: Boolean);
  begin
    if HasData and Assigned(WriteData) then
    begin
      WritePropName(Name);
      WriteData(Self);
    end;
  end;

如果 Filer.Ancestor 不是 nil,表示當前正在讀取的元件繼承自表單父類中的元件,元件設計者可以根據 Ancestor 判斷是否需要寫屬性至流中。例如:當前元件的屬性值與原表單類中的元件屬性值相同的時候,可以不寫入(通常是這樣設計)。

ReadData、WriteData 參數是從 Filer 對象中讀寫數據的方法地址,它們的類型是:

  TReaderProc = procedure(Reader: TReader) of object;
  TWriterProc = procedure(Writer: TWriter) of object;

比如:

  procedure TComponent.ReadLeft(Reader: TReader);
  begin
    LongRec(FDesignInfo).Lo := Reader.ReadInteger;
  end;

  procedure TComponent.WriteLeft(Writer: TWriter);
  begin
    Writer.WriteInteger(LongRec(FDesignInfo).Lo);
  end;
  
對於二進制格式的屬性值,可以使用 TFiler.DefineBinaryProperty 方法讀寫:

  procedure DefineBinaryProperty(const Name: string;
    ReadData, WriteData: TStreamProc; HasData: Boolean); override;

  TStreamProc = procedure(Stream: TStream) of object;

Stream 參數是從流中讀出的二進制數據或要寫入二進制數據的流對象句柄。

注意:自己定義屬性的讀寫方法時要記得調用 inherited DefineProperties(Filer),否則祖先類的自定義屬性讀寫操作不會進行。TControl 是個例外,因爲它已經定義了 published Left 和 Top 屬性。

===============================================================================
⊙ TReader.ReadComponent 方法
===============================================================================
ReadComponent 的執行過程與 ReadRootComponent 的過程很相似,它根據流中的信息使用 FindComponentClass 方法找到元件類在內存中的地址,然後調用該元件類的構造函數創建對象,接下來調用新建對象的 ReadState -> TReader.ReadData -> ReadDataInner -> TReader.ReadProperty,重複 ReadRootComponent 的過程。

  { TReader }
  function ReadComponent(Component: TComponent): TComponent;

TReader.ReadComponent 和 TComponent.ReadState 形成遞歸調用過程,把表單上嵌套的元件創建出來。

===============================================================================
⊙ TReader.ReadValue / TReader.NextValue 系列方法
===============================================================================
ReadValue 方法從流中讀出一個 TValueType 類型的數據,它主要由其它的方法調用。

TValueType 中只有 vaList 比較特殊,它表示後面的數據是一個屬性值系列,以 vaNull 結束。其餘的枚舉值的都是指屬性的數據類型或值。

  TValueType = (vaNull, vaList, vaInt8, vaInt16, vaInt32, vaExtended,
    vaString, vaIdent, vaFalse, vaTrue, vaBinary, vaSet, vaLString,
    vaNil, vaCollection, vaSingle, vaCurrency, vaDate, vaWString,
    vaInt64, vaUTF8String);

  function TReader.ReadValue: TValueType;
  begin
    Read(Result, SizeOf(Result));
  end;

NextValue 方法調用 ReadValue 返回流中下一個數據的類型,然後將流指針回退至讀數據之前。通常用於檢測流中下一個數據的類型。

  function TReader.NextValue: TValueType;
  begin
    Result := ReadValue;
    Dec(FBufPos);
  end;

CheckValue 方法調用 ReadValue 檢查下一個數據類型是否是指定的類型,如果不是則觸發異常。

ReadListBegin 方法檢查下一個數據是否是 vaList,它調用 CheckValue 方法。

ReadListEnd 方法檢查下一個數據是否是 vaNull,它調用 CheckValue 方法。

SkipValue 方法使用 ReadValue 獲得下一個數據的類型,然後將流指針跳過這個數據。

===============================================================================
⊙ TReader.ReadStr 方法
===============================================================================
ReadStr 方法讀出流中的短字符串,TReader 內部使用它讀取屬性名稱等字符串,元件設計者應該使用 ReadString 函數讀取屬性值。

  function ReadStr: string;

===============================================================================
⊙ TReader.ReadInteger / ReadString / ReadBoolean 系列方法
===============================================================================
TReader 有一系列讀取屬性值的函數,可供元件設計者使用。

    function ReadInteger: Longint;
    function ReadInt64: Int64;
    function ReadBoolean: Boolean;
    function ReadChar: Char;
    procedure ReadCollection(Collection: TCollection);
    function ReadFloat: Extended;
    function ReadSingle: Single;
    function ReadCurrency: Currency;
    function ReadDate: TDateTime;
    function ReadIdent: string;
    function ReadString: string;
    function ReadWideString: WideString;
    function ReadVariant: Variant;

===============================================================================
⊙ TReader.Read 方法
===============================================================================
TReader 中所有的數據都是通過 TReader.Read 方法讀取的。TReader 不直接調用 TStream 的讀方法是因爲 TReader 的讀數據操作很頻繁,它自己建立了一個緩衝區(4K),只有當緩衝區中的數據讀完之後纔會調用 TStream.Read 再讀入下一段數據,這樣可以極大地加快讀取速度。Read 是個彙編函數,編寫得很巧妙,它的代碼及註釋如下:

procedure TReader.Read(var Buf; Count: Longint); assembler;
asm
        PUSH    ESI
        PUSH    EDI
        PUSH    EBX
        MOV     EDI,EDX                    ; EDI <- @Buf
        MOV     EBX,ECX                    ; EBX <- Count
        MOV     ESI,EAX                    ; ESI <- Self
        JMP     @@6                        ; check if Count = 0
      { @@1: 檢查 TReader 的緩衝數據是否用盡 }
@@1:    MOV     ECX,[ESI].TReader.FBufEnd  ; ECX <- FBufEnd
        SUB     ECX,[ESI].TReader.FBufPos  ; if FBufEnd > FBufPos jmp @@2
        JA      @@2
        MOV     EAX,ESI                    ; else EAX <- Self
        CALL    TReader.ReadBuffer         ; call ReadBuffer
        MOV     ECX,[ESI].TReader.FBufEnd  ; ECX <- FBufEnd
      { @@2: 檢查要讀出的數量是否超過緩衝區大小,如是則分批讀取 }
@@2:    CMP     ECX,EBX                    ; if FBufEnd < Count jmp @@3
        JB      @@3
        MOV     ECX,EBX                    ; else ECX <- Count
      { @@3: 分批讀取緩衝區 }
@@3:    PUSH    ESI
        SUB     EBX,ECX                    ; Count = Count - FBufEnd
        MOV     EAX,[ESI].TReader.FBuffer  ; EAX <- FBuffer
        ADD     EAX,[ESI].TReader.FBufPos  ; EAX = FBuffer + FBufPos
        ADD     [ESI].TReader.FBufPos,ECX  ; FBufPos = FBufPos + FBufEnd
        MOV     ESI,EAX                    ; ESI <- Curr FBuffer Addr
        MOV     EDX,ECX                    ; EDX <- FBufEnd
        SHR     ECX,2                      ; ECX <- FBufEnd / 4
        CLD
        REP     MOVSD                      ; Copy Buffer
        MOV     ECX,EDX                    ; ECX <- FBufEnd
        AND     ECX,3                      ; Check if FBufEnd Loss 3
        REP     MOVSB                      ; Copy left Buff
        POP     ESI                        ; ESI <- Self
      { @@6: 檢查是否讀完數據,然後重複 @@1 或退出 }
@@6:    OR      EBX,EBX                    ; if Count = 0 then Exit
        JNE     @@1                        ; Repeat ReadBuffer
        POP     EBX
        POP     EDI
        POP     ESI
end;

===============================================================================
⊙ ObjectBinaryToText / ObjectTextToBinary 函數
===============================================================================
Classes.pas 中的 ObjectBinaryToText 和 ObjectTextToBinary 函數用於把對象屬性信息轉換爲文本形式或二進制形式。

  procedure ObjectBinaryToText(Input, Output: TStream);
  procedure ObjectTextToBinary(Input, Output: TStream);

新建一個項目,在表單上放置一個 TMemo 控件,然後執行以下代碼,就能明白這兩個函數的作用了。在 Delphi 的 IDE 中,將 DFM 文件進行二進制和文本方式的轉換應該是通過這兩個函數進行的。

  var
    InStream, OutStream: TMemoryStream;
  begin
    InStream := TMemoryStream.Create;
    OutStream := TMemoryStream.Create;
    InStream.WriteComponent(Self);
    InStream.Seek(0, soFromBeginning);
    ObjectBinaryToText(InStream, OutStream);
    OutStream.Seek(0, soFromBeginning);
    Memo1.Lines.LoadFromStream(OutStream);
  end;

上面的兩個函數還有一對增強版本,它們增加了對資源文件格式的轉換,實際上也是調用了上面的函數:

  procedure ObjectResourceToText(Input, Output: TStream);
  procedure ObjectTextToResource(Input, Output: TStream);

Delphi 編譯程序生成應用程序的資源數據段,應該是用 ObjectTextToResource 函數進行的。

注:ObjectTextToBinary 調用了 TParser 對象進行字符串解析工作。

===============================================================================
⊙ 結束
===============================================================================
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章