最近一段時間似乎流行源碼分析:)我也來談談在過去一段時間裡對VCL源碼的分析方法方面的一點體會,本文將不探討VCL類庫的構架和設計模式方面的東本,只是以我們常見的控件屬性/方法的實現過程作簡單的說明,希望對初學者有所幫助
VCL分析方法
例:TButton.Caption屬性的由來
(本文僅以此獻給Delphi初學者)
用過一段時間Delphi的朋友,都會對VCL源碼感興趣。本人也常常在各大論壇見到一些網友研究討論過關於VCL源碼的貼子。不過,很多網友很努力的想看懂,可最後還是半途而廢,因為他們總是理不出個頭緒、看得雲裡霧裡。筆者我也有看源碼的習慣,沒事的時候就點點鼠標右鍵,總是希望得到一些僥幸的收獲和開發技巧。
不過萬事都得先有個基本前題,就像人上學的過程一樣(這裡指正常人)要按部就班的來,一般不可能小學一畢業就直接去念大學,除非他(她)是個天才或經過特別培訓。所以各位GGJJDDMM,看VCL源碼也是有個基本前題的,首先你得熟悉WIN32 API/SDK,如果你說不知道的話,可以參考書籍《Programming Windows》(中文名《Windows 程序設計》)。其次是你應當對Object Pascal比較熟悉,或者你曾經對DELPHI的組件進行過擴展(做過組件開發),那麼我相信你對Object Pascal已經熟悉。不熟也不要緊,Delphi的在線幫助就有對Object Pascal的講述,如果英文太差也不要緊,網上也有很多熱心網友翻譯過來的中文幫助和語言參考書。
呵呵,本人寫技術文章就像在寫散文:)
言歸正傳,我們這篇文章的主題是對VCL源碼的分析,分析當然有一個分析方法的問題,總不能隨便打開一個源程序,逮著一個函數就分析一個函數吧:)所以我們也應該有選擇,有目的的分析。
想想我們每天編碼時都會遇到的屬性有哪些?呵呵,NAME,CAPTION,VISIBLE,還有一些控件的TEXT(如EDIT1.TEXT)。那麼我們就以控件的CAPTION來分析吧。
當然不是每個控件都有CAPTION屬性的,我們這裡就用TButton類的Caption屬性進行分析。
打開每天我們都會使用的Delphi,在FORM窗體上放一個按鈕,得到一個Button1的按鈕控件,按F12打天源程序,有沒有找到這段代碼呢:
Button1: TButton;
對了,在TButton上點擊鼠標右鍵,在彈出的上下文菜單中選擇第一項Find Declaration,找到TButton類的定義,如下所示:
TButton = class(TButtonControl)
private
FDefault: Boolean;
FCancel: Boolean;
FActive: Boolean;
FModalResult: TModalResult;
procedure SetDefault(Value: Boolean);
。。。。。。
原來TButton繼承於TButtonControl類,呵呵:)
在左邊的對象窗口(Exploring Unit.pas窗口)中找到TButton的CAPTION屬性,如下圖:
雙擊CAPTION屬性,找到定義CAPTION屬性的源碼,大家可能發現什麼都沒有,只有一個
property Caption;
呵呵,寫過組件的朋友都知道,按理Caption屬性應該有讀/寫文本的方法啊?在哪裡去了呢,呵呵,這裡沒有出現,當然應該在它的父類裡了(這裡只是申明Caption出來的地方),我們順著剛才的方法繼續在TButtonControl,發現也沒有,最終我們在TControl類裡找到了這個CAPTION,至於為什麼是protected成員,我就不多說了:
protected
procedure ActionChange(Sender: TObject; CheckDefaults: Boolean); dynamic;
procedure AdjustSize; dynamic;
procedure AssignTo(Dest: TPersistent); override;
procedure BeginAutoDrag; dynamic;
function CanResize(var NewWidth, NewHeight: Integer): Boolean; virtual;
function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; virtual;
procedure Changed;
procedure ChangeScale(M, D: Integer); dynamic;
。。。。。。
property Caption: TCaption read GetText write SetText stored IsCaptionStored;
看看GetText、SetText就是操作文本屬性的函數了,我們找到GetText、SetText定義如下:
function GetText: TCaption;
procedure SetText(const Value: TCaption);
還有TCaption,它的定義居然是一個自定義類型:
TCaption = type string;
說明GetText返回值和SetText的調用參數本來也就是一個string型的:)
下面我們來看看GetText源碼:
function TControl.GetText: TCaption;
var
Len: Integer;
begin
Len := GetTextLen;//得到文本長度
SetString(Result, PChar(nil), Len);// 設置Result返回以Len指定的長度
if Len <> 0 then GetTextBuf(Pointer(Result), Len + 1);//長度不為空,Result得到文本數據
end;
如果不明白GetTextBuf的用法,看看如下的代碼:
procedure TForm1.Button1Click(Sender: TObject);
var
Buffer: PChar;
Size: Byte;
begin
Size := Edit1.GetTextLen; //得到EDIT1的文本長
Inc(Size);
GetMem(Buffer, Size); //創建EDIT1文本長度大小的緩存空間
Edit1.GetTextBuf(Buffer,Size); //由緩存得到文本,Buffer裡的值就是Edit1.Text
Edit2.Text := StrPas(Buffer); //Buffer轉換為PASCAL字符類型數據
FreeMem(Buffer, Size); //釋放內存
end;
以上程序的行為同以下程序相當:
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit2.Text := Edit1.Text;
end;
回到GetText函數,其中GetTextLen的作用是得到文本長度,GetTextBuf得到文本數據。
SetText就更簡單了,定義如下:
procedure TControl.SetText(const Value: TCaption);
begin
if GetText <> Value then SetTextBuf(PChar(Value));
end;
意思是如果設定的Value與原來的不同,則重新設置緩存文本。
為了更深入VCL底部,我們再看看GetTextLen如何實現的(其實SetTextBuf和GetTextLen的實現過程相似):
function TControl.GetTextLen: Integer;
begin
Result := Perform(WM_GETTEXTLENGTH, 0, 0);//WM_派發的是Windows標准消息
end;
看到這裡想必大家都明白了,如果還不明白(沒用過Perform),我看再看看Perform,它到底做了什麼:
function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;
var
Message: TMessage;
Begin
{你的消息賦予TMessage }
Message.Msg := Msg; ;
Message.WParam := WParam;
Message.LParam := LParam;
Message.Result := 0;//0表示返回不處理
if Self <> nil then WindowProc(Message);//不為空,將消息交給TControl的窗口過程WindowProc處理
Result := Message.Result;//返回結果
end;
這裡主要再看看WindowProc做了什麼,TControl裡面WindowProc是這樣定義的:
property WindowProc: TWndMethod read FWindowProc write FWindowProc;
在TControl的Create函數中:
constructor TControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FWindowProc := WndProc;
。。。。。。
可見我們還要找到TControl 的WndProc過程才能明白究竟,
WndProc過程定義如下:
procedure WndProc(var Message: TMessage); override;
實現:
procedure TControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
KeyState: TKeyboardState;
WheelMsg: TCMMouseWheel;
begin
if (csDesigning in ComponentState) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and (Form.Designer <> nil) and
Form.Designer.IsDesignMsg(Self, Message) then Exit
end;
if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit;
end
else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
begin
if not (csDoubleClicks in ControlStyle) then
case Message.Msg of
WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
end;
case Message.Msg of
WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);
WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:
begin
if FDragMode = dmAutomatic then
begin
BeginAutoDrag;
Exit;
end;
Include(FControlState, csLButtonDown);
end;
WM_LBUTTONUP:
Exclude(FControlState, csLButtonDown);
else
with Mouse do
if WheelPresent and (RegWheelMessage <> 0) and
(Message.Msg = RegWheelMessage) then
begin
GetKeyboardState(KeyState);
with WheelMsg do
begin
Msg := Message.Msg;
ShiftState := KeyboardStateToShiftState(KeyState);
WheelDelta := Message.WParam;
Pos := TSmallPoint(Message.LParam);
end;
MouseWheelHandler(TMessage(WheelMsg));
Exit;
end;
end;
end
else if Message.Msg = CM_VISIBLECHANGED then
with Message do
SendDockNotification(Msg, WParam, LParam);
Dispatch(Message);//派發消息
end;
這裡主要講講Dispatch方法,它根據傳入的消息調用消息的句柄方法,如果在組件類和它的父類都沒有找到消息的處理句柄,Dispatch方法便會調用Defaulthandler(默認的消息處理方法),如下:
procedure TObject.Dispatch(var Message);
asm
PUSH ESI
MOV SI,[EDX]
OR SI,SI
JE @@default
CMP SI,0C000H
JAE @@default
PUSH EAX
MOV EAX,[EAX]
CALL GetDynaMethod
POP EAX
JE @@default
MOV ECX,ESI
POP ESI
JMP ECX
@@default:
POP ESI
MOV ECX,[EAX]
JMP DWord PTR [ECX] + VMTOFFSET TObject.DefaultHandler//調用默認的消息處理方法
end;
而默認的消息處理如下,在SYSTEM.PAS單元裡:
procedure TObject.DefaultHandler(var Message);
begin
end;
由以上代碼看好像是沒有任何處理過程,跟蹤Object.DefaultHandler的匯編執行動作call dWord ptr[ecx-$10],即調用Object.DefaultHandle,看看做何處理:
{Object.DefaultHandle}
Ret
Lea eax,[eax+$00]
即一個返回處理!
從最表面的Button.caption,我們走到了編譯器層,可見所有東西都能找到它固有的原點!以caption的分析為基礎,我們可以繼續分析name屬性和其它一些方法/函數。
希望我這篇‘散文’能給大家理出點頭緒:)