Ø TReader 先來看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 ); 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.Component then ReadStrgnore name } if csDe☺ , csLoading); Include(Result.FComponentState, csReading); Result.Name := FindUniqueName(ReadStr); end; end; FRoot := Result; FFinder := TClassFinder.Create(TPersistentClass(Result.ClassType), True); ; 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.FLoonentState, csLoading); 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(nList ; 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, PropName) else begin { Cannot begini inrecorr. 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: TReaderProc; WriteData: TWriterProc; HasData: TReaderProc; WriteData: TWriterProc; 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(InClassstance).BClassInfo(pClass). .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); ); 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 * (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; 完成; Instance.GetChildren(WriteComponent, FRoot); finally FAncestorList.Free; end;end; IgnoreChildren屬性使一個Writer物件儲存元件時可以不儲存該元件擁有的子元件。如果IgnoreChildren屬性為True,則Writer物件儲存元件時不存它擁有的子元件。否則就要存儲子組件。 Instance.GetChildren(WriteComponent, FRoot); 這是寫子組件的最關鍵的一句,它把WriteComponent方法作為回調函數,按照深度優先遍歷樹的原則,如果根組件FRoot存在子組件,則用WriteComponent來保存它的子組件。這樣我們在DFM檔案中看到的是樹狀的元件結構。