Delphi中的Serialiation |
|
conundrum
尊榮會員 發表:893 回覆:1272 積分:643 註冊:2004-01-06 發送簡訊給我 |
http://wlbookwl.myrice.com/jck/1212delphi.htm
《Mastering Delphi6》學習筆記下
http://wlbookwl.myrice.com/jck/1212delphi.htm
構件自動清除的內幕 我們已經知道,TComponent在釋放自身的時候會自動刪除它Owns的所有Component,這是Delphi程式的一個極大特點。否則的話,任何動態生成的物件比如Menu Items都要自己維護和刪除,那將是多l痛苦。VCL是如何做到這一點的呢?可以想象到,每個Component內部應該保持一個列表,記錄它所擁有的Components,在自身的析構函數中將列表中的所有物件同時刪除即可。基本上這個想法是正確的,不過應該考慮到兩種可能的情況:(1)構件刪除自身的時候,將它所擁有的子構件全部清除,這種情況下子構件是被動的;(2)某個子構件主動地調用了Free或者Destroy,這個時候,父構件應該有辦法得到通知,並且維持列表的同步。否則的話,就可能出現某個子構件被析構兩次以上的情況,其後果如何大概也可以想到了。
要保持列表的同步,子構件在建立的時候就必須向父構件報告,退出的時候也必須給父構件打個招呼才行。
原理不複雜,現在來看看VCL是如何實現的。TComponent的構造方法叫做Create,帶有一個AOwner參數:
constructor TComponent.Create(AOwner: TComponent);
begin
FComponentStyle := [csInheritable];
if AOwner <> nil then AOwner.InsertComponent(Self);
end;
InsertComponent正是我們關心的地方。找找看它的具體實現:
procedure TComponent.InsertComponent(AComponent: TComponent);
begin
AComponent.ValidateContainer(Self);
ValidateRename(AComponent, '', AComponent.FName);
Insert(AComponent);
AComponent.SetReference(True);
if csDesigning in ComponentState then
AComponent.SetDesigning(True);
Notification(AComponent, opInsert);
end;
雖然有些地方還不大明白,不過關鍵的部分看來是Insert(AComponent)一句。再找到Insert:
procedure TComponent.Insert(AComponent: TComponent);
begin
if FComponents = nil then FComponents := TList.Create;
FComponents.Add(AComponent);
AComponent.FOwner := Self;
end;
我們所猜想的內部列表終於現身,它是一個通用的,可以容納任何TObject或者Pointer的TList類型。
另外,從上面的代碼段也可以看到VCL中比較典型的一種處理方法,那就是物件“只有在需要的時候才建立”,比如上面建立FComponents就是屬於這種情況。當然,在每次建立一個構件時都檢查一遍FComponent的有效性將會稍稍影響程式的運行速度,但是考慮到程式所佔用的記憶體(Form和Application一般有應該有Components列表,而一般的構件則完全沒有必要,如果每個構件都建立一個List的話,那l佔用的記憶體是相當可觀的),這種處理方法也有它的道理。
現在再來看看析構時的情況:
destructor TComponent.Destroy;
begin
Destroying;
if FFreeNotifies <> nil then
begin
while Assigned(FFreeNotifies) and (FFreeNotifies.Count > 0) do
TComponent(FFreeNotifies[FFreeNotifies.Count - 1]).Notification(Self, opRemove);
FreeAndNil(FFreeNotifies);
end;
DestroyComponents;
if FOwner <> nil then FOwner.RemoveComponent(Self);
inherited Destroy;
end;
這裏有兩個重要的地方:(1)DestroyComponents一句,顯然是刪除其所擁有的Components;(2)FOwner.RemoveComponents(Self),看來是把自身從父構件的FComponents列表中移走。
procedure TComponent.DestroyComponents;
var
Instance: TComponent;
begin
while FComponents <> nil do
begin
Instance := FComponents.Last;
if (csFreeNotification in Instance.FComponentState)
or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then
RemoveComponent(Instance)
else
Remove(Instance);
Instance.Destroy;
end;
end;
procedure TComponent.RemoveComponent(AComponent: TComponent);
begin
ValidateRename(AComponent, AComponent.FName, '');
Notification(AComponent, opRemove);
AComponent.SetReference(False);
Remove(AComponent);
end;
RemoveComponent內部又調用了Remove,再看看這個函數: procedure TComponent.Remove(AComponent: TComponent);
begin
AComponent.FOwner := nil;
FComponents.Remove(AComponent);
if FComponents.Count = 0 then
begin
FComponents.Free;
FComponents := nil;
end;
end;
很簡單,是l?通過這一番遊歷,相信“構件在何種情況下會被自動清除和如何被清除”這個問題應該有了明確的答案,以後寫起代碼來也應該放心多了。
附記:通過上面的例子,我才體會到Delphi中的導航鍵是多l方便!不論是自己的單元還是VCL內部單元,用Ctrl+點擊和Ctrl+Shift+Up/Down,以及Code Editor中的Back/Forword按鈕,三兩下就可以定位到任何地方。在Visual C++中就沒有這樣的方便,不論是用Trace還是用Source Browser,都必須先編譯通過才行,即使用Find In Files也不見得快多少。相比起來,在Delphi中跟蹤源代碼要方便多了。雖說Delphi的妍妘t度確實慢了點,不過它在其他方面提供的快捷方便還是應該肯定的。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆ 無名稱構件的使用
我曾在許多Delphi資料中看到這樣的說法:每一個Component都必須有一個不d空的Name,用於和其他Component相區分。過去我也對這種說法深信不疑。但是看過《Mastering Delphi 6》後,我知道我錯了。Component的Name屬性可以是空的,特別是對於Menu Separator,Static Label,Bevel,Shape和Panel等等。雖然IDE會給所有的構件起一個默認的名稱,不過你可以在Object Inspector中把它的Name清空,這樣的後果是:
1.構件在Object Inspector和Object TreeView中將變成Unnamed或者Component<0>這樣的表示方法;
2.該構件的聲明將從Form的聲明中完全刪除;
3.如果用View As Text看看.DFM文件的定義,發現無名稱的Component物件定義d這樣的形式:
object : TMenuItem
caption = ‘-’
end
而一般的聲明d:
object File1: TMenuItem
caption = ‘File’
end
這個特點的真正好處在於,可以讓Form的聲明簡潔許多,再也不會充斥大量無用的N1,N2之類識別字,從而混淆你的視線。當然,可執行文件的大小也會稍有減少。
不過,假如將Form上面所有Label的Name都設d空,那l運行該程式將{生一個錯誤,因d編譯器看到聲明中沒有Label,所以不會將TLabel的有關TLabel的庫函數連接進來。(編譯器在絕大多數時候都很聰明,不過這個時候只能說它是自作聰明。)解決辦法是至少保留一個有名字的Label。
我們甚至可以手動將Form中某個Component的聲明刪除。不用擔心,在Form建立起來的時候這個Component不會消失,只是不能夠用通常的辦法來引用它了。真的有需要的時候,可以用Form1.FindComponent(‘Button1’)這樣的方法找到特定的構件。如果Form中的構件實在太多,可以用這個技巧來刪除一部分不太重要的聲明,保持單元的簡潔。
其實,要減少Label聲明的數量,用Delphi 6中新增的LabledEdit是一個行之有效的辦法,而且LabeledEdit可以簡化表單的佈局管理。不過,LabeledEdit只對Edit有效。要減少其他不需要回應事件和處理輸入的構件(如Menu Separator,Panel和Bevel等等)的聲明,就只能用這種方法。
不同於一般的procedure或者function,在Delphi中屬於某個類的方法稱dmethod,它們的聲明一般d
AProcMethod:procedure(Sender:TObject) of object;
d什l一定要加上of object呢?實際上,Object Pascal中一般的函數指標就相當於普通的指標,而AProcMethod則是一個對指標,它在Delphi中有一個對應的類型:TMethod。
TMethod=record
Data, Code:Pointer;
end;
所以,我們在單元中可以這樣:
procedure TForm1.Button1Click2(Sender:TObject);
begin
ShowMessage(‘Click2!’);
end;
procedure TForm1.FormCreate(Sender:TObject);
var
Method:TMethod;
begin
Method.Data := Self;
Method.Code := MethodAddress(‘Button1Click2’);
Button1.OnClick := TNotifyEvent(Method);
end;
當然,實際上只需要Button1.Button1Click2即可;不過,這樣可以讓你明白在幕後發生的事情。值得注意的一點是,Button1Click2應該聲明在published段(或者和其他構件的聲明放在一起,因d默認作用域就是published),以便編譯器d它{生RTTI資訊,否則程式可能達不到預期的效果。
☆☆☆☆☆☆☆☆☆☆☆☆☆☆ Delphi中的Serialiation
熟悉MFC的人對Serialize這個虛擬函數大概不會陌生。在MFC中,如果要用Serialization機制讀取和保存資料,大概是這樣:
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << myString;
ar << myInt;
}
else
{
ar >> myString;
ar >> myInt;
}
}
Object Pascal中有沒有類似的機制呢?接觸到TReader和TWriter這兩個類之後,我知道我找到答案了。
不需多講理論,來看一個實際的例子。在Form上面放三個Edit和兩個Button,另外再加一個OpenDialog和SaveDialog。代碼如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
if SaveDialog1.Execute then
Serialize(SaveDialog1.FileName,True);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
if OpenDialog1.Execute then
Serialize(OpenDialog1.FileName,False);
end;
procedure TForm1.Serialize(const FileName:string; bSave: Boolean);
var
Reader : TReader;
Writer : TWriter;
Stream : TFileStream;
begin
if bSave then begin
Stream := TFileStream.Create(FileName,fmOpenWrite or fmCreate);
Writer := TWriter.Create(Stream,4096);
Writer.WriteString(Edit1.Text);
Writer.WriteInteger(StrToInt(Edit2.Text));
Writer.WriteFloat(StrToFloat(Edit3.Text));
Writer.Free;
Stream.Free;
end
else begin
Stream := TFileStream.Create(FileName,fmOpenRead);
Reader := TReader.Create(Stream,4096);
Edit1.Text := Reader.ReadString;
Edit2.Text := IntToStr(Reader.ReadInteger);
Edit3.Text := FloatToStr(Reader.ReadFloat);
Reader.Free;
Stream.Free;
end;
end;
如果願意的話,完全可以將Serialize包裝成一個virtual method,從而讓派生類中的實現更加簡潔。
TReader和TWriter不僅能夠讀取和寫入Object Pascal中絕大部分標準資料類型,而且能夠讀寫Collection/List/Variant這些高級類型,甚至能夠讀寫Perperties和Component。不過,TReader/TWriter自身實際上提供的功能很有限,大部分實際的工作是由TStream這個非常強大的類來完成的。從TReader和TWriter的聲明中可以看到一些特別dComponent而設計的方法,不難猜想,Delphi開發環境本身很可能就是利用TReader/TWriter,將構件的屬性寫入.DFM文件以及從.DFM文件中讀取屬性值的。
下面的例子很有用也非常有趣,它的效果完全相當於Form Designer中的View As Text命令:
var
DFMBuf, TextBuf : TStream;
begin
DFMBuf := TMemoryStream.Create;
DFMBuf.WriteComponent(Self);
TextBuf := TMemoryStream.Create;
DFMBuf.Seek(0,soFromBeginning);
ObjectBinaryToText(DFMBuf,TextBuf);
TextBuf.Seek(0,soFromBeginning);
Memo1.Lines.LoadFromStream(TextBuf);
TextBuf.Free;
DFMBuf.Free;
甚至可以從可執行文件中讀取Form的資訊:
var
DFMBuf, TextBuf : TStream;
buf : pointer;
begin
DFMBuf := TResourceStream.Create(HInstance,'TForm1',RT_RCDATA);
DFMBuf.Position := 0;
TextBuf := TMemoryStream.Create;
DFMBuf.Seek(0,soFromBeginning);
ObjectBinaryToText(DFMBuf,TextBuf);
TextBuf.Seek(0,soFromBeginning);
Memo1.Lines.LoadFromStream(TextBuf);
TextBuf.Free;
DFMBuf.Free;
end;
(說明:如果表單不是TForm1,那l請將TResourceStream.Create一句中的第二個參數改d相應的表單類名。)
好好讀懂這些代碼,相信自己做一個DFM Viewer也不是遙不可及的事情了吧! ☆☆☆☆☆☆☆☆☆☆☆☆☆☆ CLX及其它
《Mastering Delphi 6》中提到Object Pascal中有一個TBits類,用來進行二進位位元操作。我看過這一段之後馬上去找,果然在Classes單元中找到了它。這個類的聲明相當簡單,不過我看過後感慨很多。如果不知道這個類的存在,我以後可能還要在需要操作二進位位元的時候辛辛苦苦的寫一大串代碼(我以前也寫過不少這樣的代碼),要是能夠早點知道有這個類的話該有多好!我相信還可能有很多人和我一樣,從來不知道TBits的存在。因此,我決定以後一定要抽個時間,好好把Classes和System這些單元瀏覽一遍;雖然在沒有多少文檔的情況下去讀這些文件不會輕鬆,但是很可能會發現一些有用的東西,從而避免去做重新發明輪子的傻事。 我過去總是覺得,Delphi的幫助系統比起MSDN來是有很大差距的。這次我倒是發現了一個相反的例證。通過對Classes單元一個走馬觀花的瀏覽,我發現一個ExtractStrings函數,毫無疑問是非常有用的。雖然在一般的教材中都找不到關於它的介紹,不過在線幫助中確實有關於它的完整描述。自己作個例子試試看,和猜想的用法相差不遠。這讓我想起MFC中也有一個類似的AfxExtractSubString,同樣有用,但龐大的MSDN中就是找不到它的幫助(其實,MFC中類似的Afx函數,只有很少的一部分才有文檔資料,其他的都屬於Undocumented。)雖然這一個小地方不能證明什l,但至少說明Borland在幫助系統的完整性方面還是用心了的,這讓我對Delphi幫助系統稍稍有了一些正面印象。
Delphi 6中幫助系統的主題一般都有VCL Reference和CLX Reference兩種,如果按F1的話,首先會彈出一個對話方塊詢問你到底選擇那一個。時間長了就很煩人。這裏有一個方法可以避免:如果你希望一直使用VCL的話,選擇Help->Custom,將列表中所有有關CLX的專案一概刪除,然後保存專案(當然,最好先將原先的設置備份一個,留給以後恢復用。)最後關閉再重新打開Delphi就行了。
Delphi 6現在支援VCL和CLX兩個類庫。CLX在底層是基於Qt這個類庫的,可能有些朋友還沒有聽說過它的名字,不過在Linux系統上面Qt的名字可是非常響亮哦。你可以在新建一個專案的時候指定是使用VCL Application還是CLX Application,構件面板會自動變化,只顯示對當前專案可用的構件。CLX構件和VCL構件從外觀上總的來說是驚人的相似,當然在背後它們還是很不同的。CLX中有一些有意思的構件在VCL中是找不到的,比如TextViewer和TextBrowser。而VCL中有一些構件在CLX中也沒有對應,主要是Win32面板上的構件,不過CLX中也有類似的構件,雖然叫法不同。(《Mastering Delphi 6》中提到,和Windows系統通過消息機制通信的方法不同,Qt中採用的是更高層的面向物件封裝,比如要操作一個按鈕,不用像按鈕發送消息,直接調用按鈕的相應方法即可。應該把Qt也看作一種Application Framework,而且這種結構一定很值得學習,有機會的話應該看看。)雖然VCL和CLX都有不少好的構件,但它們是無法共同使用的,也就是說,沒有辦法在VCL Application中使用CLX構件,反之亦然。
《Mastering Delphi 6》中有一個很妙的例子,用到CLX中的TextViewer。這個構件的特點是完全基於HTML的,而它有Text屬性,因而又和Memo相相容。所以,只要建立一個CLX Application,在表單上面放上一個Memo和一個TextViewer,並修改Memo的OnChange事件:
procedure TForm1.Memo1Change(Sender: TObject);
begin
TextViewer1.Text := Memo1.Text;
end;
簡簡單單的一句話,{生出來的可是一個非常強的工具,你可以在Memo中按照HTML格式寫內容,比如“>”在
|
本站聲明 |
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。 2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。 3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇! |