標籤:
Delphi的組件讀寫機制(一)
一、流式對象(Stream)和讀寫對象(Filer)的介紹
在物件導向程式設計中,對象式資料管理佔有很重要的地位。在Delphi中,對對象式資料管理的支援方式是其一大特色。
Delphi是一個物件導向的可視化設計與物件導向的語言相結合的整合式開發環境。Delphi的核心是組件。組件是對象的一種。Delphi應用程式完全是由組件來構造的,因此開發高效能的Delphi應用程式必然會涉及對象式資料管理技術。
對象式資料管理包括兩方面的內容:
● 用對象來管理資料
● 對各類資料對象(包括對象和組件)的管理
Delphi將對象式資料管理類歸結為Stream對象(Stream)和Filer對象(Filer),並將它們應用於可視組件類庫(VCL)的方方面面。它們提供了豐富的在記憶體、外存和Windows資源中管理對象的功能,Stream對象,又稱流式對象,是TStream、THandleStream、TFileStream、TMemoryStream、TResourceStream和TBlobStream等的統稱。它們分別代表了在各種媒介上儲存資料的能力,它們將各種資料類型(包括對象和組件) 在記憶體、外存和資料庫欄位中的管理操作抽象為對象方法,並且充分利用了物件導向技術的優點,應用程式可以相當容易地在各種Stream對象中拷貝資料。
讀寫對象(Filer)包括TFiler對象、TReader對象和TWriter對象。TFiler對象是檔案讀寫的基礎對象,在應用程式中使用的主要是TReader和TWriter。TReader和TWriter對象都直接從TFiler對象繼承。TFiler對象定義了Filer對象的基本屬性和方法。
Filer對象主要完成兩大功能:
● 存取表單檔案和表單檔案中的組件
● 提供資料緩衝,加快資料讀寫操作
為了對流式對象和讀寫對象有一個感性的認識,先來看一個例子。
a)寫檔案
procedure TFomr1.WriteData (Sender: TObject);
Var
FileStream:TFilestream;
Mywriter:TWriter;
i: integer
Begin
FileStream:=TFilestream.create(‘c:\Test.txt’,fmopenwrite);//建立檔案流對象
Mywriter:=TWriter.create(FileStream,1024); //把Mywriter和FileStream聯絡起來
Mywriter. writelistbegin; //寫入列表開始標誌
For i:=0 to Memo1.lines.count-1 do
Mywriter.writestring(memo1.lines[i]); //儲存Memo組件中文本資訊到檔案中
Mywriter.writelistend; //寫入列表結束標誌
FileStream.seek(0,sofrombeginning); //檔案流對象指標移到流起始位置
Mywriter.free; //釋放Mywriter對象
FileStream.free; //釋放FileStream對象
End;
b)讀檔案
procedure TForm1.ReadData(Sender: TObject);
Var
FileStream:TFilestream;
Myreader:TReader;
Begin
FileStream:=TFilestream.create(‘c:\Test.txt’,fmopenread);
Myreader:=TRreader.create(FileStream,1024); //把Myreader和FileStream聯絡起來
Myreader.readlistbegin; //把寫入的列表開始標誌讀出來
Memo1.lines.clear; //清除Memo1組件的常值內容
While not myreader.endoflist do //注意TReader的一個方法:endoflist
Begin
Memo1.lines.add(myreader.readstring); //把讀出的字串加到Memo1組件中
End;
Myreader.readlistend; //把寫入的列表結束標誌讀出來
Myreader.free; //釋放Myreader對象
FileStream.free; //釋放FileStream對象
End;
上面兩個過程,一個為寫過程,另一個為讀過程。寫過程通過TWriter,利用TFilestream把一個Memo中的內容(文本資訊)存為一個儲存在磁碟上的二進位檔案。讀過程剛好和寫過程相反,通過TReader,利用TFilestream把二進位檔案中的內容轉換為文本資訊並顯示在Memo中。運行程式可以看到,讀過程忠實的把寫過程所儲存的資訊進行了還原。
值得注意的是,讀寫對象如TFiler對象、TReader對象和TWriter對象等很少由應用程式編寫者進行直接的調用,它通常用來讀寫組件的狀態,它在讀寫組件機制中扮演著非常重要的角色。
對於流式對象Stream,很多參考資料上都有很詳細的介紹,而TFiler對象、TReader對象和TWriter對象特別是組件讀寫機制的參考資料則很少見,本文將通過對VCL原代碼的跟蹤而對組件讀寫機制進行剖析。
二、讀寫對象(Filer)與組件讀寫機制
Filer對象主要用於存取Delphi的表單檔案和表單檔案中的組件,所以要清楚地理解Filer對象就要清楚Delphi 表單檔案(DFM檔案)的結構。
DFM檔案是用於Delphi儲存表單的。表單是Delphi可視化程式設計的核心。表單對應Delphi應用程式中的視窗,表單中的可視組件對應視窗中的介面元素,非可視組件如TTimer和TOpenDialog,對應Delphi應用程式的某項功能。Delphi應用程式的設計實際上是以表單的設計為中心。因此,DFM檔案在Delphi應用設計中也占很重要的位置。表單中的所有元素包括表單自身的屬性都包含在DFM檔案中。
在Delphi應用程式視窗中,介面元素是按擁有關係相互聯絡的,因此樹狀結構是最自然的表達形式;相應地,表單中的組件也是按樹狀結構組織;對應在DFM檔案中,也要表達這種關係。DFM檔案在物理上,是以文本方式儲存的(在Delphi2.0版本以前是儲存為二進位檔案的),在邏輯上則是以樹狀結構安排各組件的關係。從該文本中可以看清表單的樹狀結構。下面是DFM檔案的內容:
object Form1: TForm1
Left = 197
Top = 124
……
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 272
……
Caption = ‘Button1‘
TabOrder = 0
end
object Panel1: TPanel
Left = 120
……
Caption = ‘Panel1‘
TabOrder = 1
object CheckBox1: TCheckBox
Left = 104
……
Caption = ‘CheckBox1‘
TabOrder = 0
end
end
end
這個DFM檔案就是TWriter通過流式對象Stream來產生的,當然這裡還有一個二進位檔案到文本資訊檔的轉換過程,這個轉換過程不是本文要研究的對象,所以忽略這樣的一個過程。
在程式開始啟動並執行時候,TReader通過流式對象Stream來讀取表單及組件,因為Delphi在編譯器的時候,利用編譯指令{$R *.dfm}已經把DFM檔案資訊編譯到可執行檔中,因此TReader讀取的內容實際上是被編譯到可執行檔中的有關表單和組件的資訊。
TReader和TWriter不僅能夠讀取和寫入Object Pascal中絕大部分標準資料類型,而且能夠讀寫List、Variant等進階類型,甚至能夠讀寫Perperties和Component。不過,TReader、TWriter自身實際上提供的功能很有限,大部分實際的工作是由TStream這個非常強大的類來完成的。也就是說TReader、TWriter實際上只是一個工具,它只是負責怎麼去讀寫組件,至於具體的讀寫操作是由TStream來完成的。
由於TFiler是TReader和TWriter的公用祖先類,因為要瞭解TReader和TWriter,還是先從TFiler開始。
TFiler
先來看一下TFiler類的定義:
TFiler = class(TObject)
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。
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(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對象的實現和應用方法,必須瞭解WriterObject Storage Service資料的格式。
首先要說明的是,每個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結束,中間用ndofList函數判斷是否有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屬性使一個WriterObject Storage Service組件時可以不儲存該組件擁有的子組件。如果IgnoreChildren屬性為True,則WriterObject Storage Service組件時不存它擁有的子組件。否則就要儲存子組件。
Instance.GetChildren(WriteComponent, FRoot);
這是寫子組件的最關鍵的一句,它把WriteComponent方法作為回呼函數,按照深度優先遍曆樹的原則,如果根組件FRoot存在子組件,則用WriteComponent來儲存它的子組件。這樣我們在DFM檔案中看到的是樹狀的組件結構。
http://blog.csdn.net/henreash/article/details/7274717
http://blog.csdn.net/henreash/article/details/7280322
Delphi的組件讀寫機制