|
永遠記住,無論你是用 SDK 還是借用 VCL 來創(chuàng)建窗口,都要遵循 Windows 的游戲規(guī)則,即先注冊窗口類,然后再創(chuàng)建窗口實例,在消息循環(huán)中寫實現(xiàn)代碼。你還要知道 Windows 已經(jīng)為了我們預(yù)注冊了多個窗口類,例如“Edit”、“ComboBox”,這時候我們要做的就是直接創(chuàng)建這些窗口,無需注冊窗口類了;在 Delphi 中這一切更簡單了,VCL 全部為你做好了,你只需簡單地在設(shè)計窗體上拖動你要的控件再寫實現(xiàn)代碼就可以了,是不是很 cool? 一、窗口的創(chuàng)建
VCL 中,具有句柄(Handle) 屬性的真正窗口控件全部繼承自 TWinControl,那就從 TWinControl 的創(chuàng)建開始說起。
VCL 中窗口的建立不是按照我們想象中的流程創(chuàng)建的,即先把所有的窗口都創(chuàng)建好,然后再調(diào)用,而是在需要時才創(chuàng)建。可能你還不能理解我這句話的意思,慢慢看。繼承自 TWinControl 的窗口控件都會有 Handle 屬性,當(dāng)代碼中需要 Handle 值時,通過該屬性的 getter 調(diào)用 TWinControl.HandleNeeded 來獲得句柄,這時如果窗體已經(jīng)建立,直接返回句柄,否則先創(chuàng)建窗口實例,再返回句柄,因此窗口創(chuàng)建是在 TWinControl.HandleNeeded 中實現(xiàn)的。Borland 這樣做的目的我想是最大程度地來節(jié)省系統(tǒng)資源吧。
TWinControl.HandleNeeded 中有幾個重要的方法,通過他們才得以創(chuàng)建窗口。TWinControl.HandleNeeded 調(diào)用TWinControl.CreateHandle 來獲得 Handle。但 CreateHandle 只是個包裝函數(shù),它首先調(diào)用 TWinControl.CreateWnd 來創(chuàng)建窗口,CreateWnd 是一個重要的過程,它先調(diào)用 TWinControl.CreateParams 設(shè)置創(chuàng)建窗口的參數(shù),通過這些參數(shù)調(diào)用 RegisterClass API 注冊窗口類,CreateWnd 然后調(diào)用 TWinControl.CreateWindowHandle,CreateWindowHandle 才是真正調(diào)用 CreateWindowEx API 創(chuàng)建窗口實例的函數(shù)。CreateHandle、CreateWnd、CreateParams、CreateWindowHandle都是虛方法,派生類可以重載這些方法以獲得更多的功能 ,其中 CreateParams 被重載的幾率最大。
上面提到的方法源碼我建議你都要仔縛匆槐椋由鈑∠螅竺嫖姨岬降姆椒ǎ鬩捕家純叢綽耄芤嫖耷鈦劍醫(yī)輝傯崾盡?BR> 至此一個窗口算是建立起來了,但是還是無法正確運行,因為它還沒有消息循環(huán)。
二,消息循環(huán)的實現(xiàn)
消息循環(huán)的實現(xiàn)是整個 VCL 消息框架中寫得最精彩的地方,因為傳統(tǒng)的 Windows 回調(diào)函數(shù)是一個靜態(tài)函數(shù),而 VCL 中的窗體是類,調(diào)用類方法時,除了函數(shù)本身的地址,還需一個 Self,在它們之間建立關(guān)聯(lián)真不是一件容易的事情,需要大量的代碼技巧,同時消息循環(huán)還要保證每秒鐘能處理幾百到幾萬次的消息量,因此代碼更需要寫得精巧。 研習(xí)這部分代碼可能會花比較多的時間。
我們知道注冊窗體類時就要提供窗體回調(diào)函數(shù)入口地址,那么可以想象到 VCL 中這個過程是發(fā)生在對 TWinControl.CreateWnd 的調(diào)用中,在該方法中,靜態(tài)函數(shù)指針 @InitWndProc 被賦值給 WNDCLASSEX 結(jié)構(gòu)中的 lpfnWndProc,這是 VCl 窗體首次建立消息循環(huán)的地方。 InitWndProc 第一次被調(diào)用時,通過 SetWindowLong API 將消息回調(diào)函數(shù)替換成 TWindowControl.FObjectInstance,而TWinControl.FObjectInstance 就是一個普通的 Pointer,賦值是在 TWinControl.Create 中通過那個最具 Magic 的函數(shù) MakeObjectInstance 完成的,這個過程非常復(fù)雜,詳細描述見參考[3]。
替換的結(jié)果是類方法 TWinControl.MainWndProc 成為真正的消息處理 Handler,隨后的對應(yīng)窗體實例的消息處理全部在 TWinControl.MainWndProc 中完成。其中還有一個細節(jié)就是消息在被 MainWndProc 處理之前還要調(diào)用一個純匯編寫的靜態(tài)函數(shù) -- StdWndProc 將消息統(tǒng)一派發(fā)[1]。至此完成消息回調(diào)從普通的靜態(tài)函數(shù)到類方法的轉(zhuǎn)變。
事實上 TWinControl.MainWndProc 是調(diào)用 WindowsProc 來實際處理窗口消息,在 TControl.Create 中 WindowsProc 是被指定成類中虛擬方法 WndProc。從 TControl 到實際的 VCL 窗體類這條繼承鏈上,很多派生類都重載了 WndProc,從而每個重載該方法的派生類都會增加一些功能。當(dāng)然在繼承鏈的末端,例如 TForm,也可以重載 WndProc,來完成一些 tricky 代碼。記住,如果你重載 WndProc,總是先處理自己想要的消息,然后將不處理的消息遞交到父類的 WndProc 中處理。
在每一個繼承類的 WndProc 中應(yīng)該只處理維持窗體運作的最基本的消息,其他不處理的消息最終會在 TControl.WndProc 中被傳遞到 TObject.Diapatch。TObject.Diapatch 在自己和父類的動態(tài)方法表中查詢相應(yīng)消息 ID,如果找到了,則調(diào)用相應(yīng)的方法。所有處理消息的類方法都應(yīng)該以關(guān)鍵字 message 定義,這可以保證其入口地址都是存在動態(tài)方法表中,從而也保證需要處理的消息 可以在 TObject.Diapatch 執(zhí)行過程中被調(diào)用。
如果在動態(tài)方表中還是無法查詢到需要處理的消息,那么 TObject.Diapatch 會繼續(xù)調(diào)用虛方法 DefaultHandler,TObject.DefaultHandler 只是個 PlaceHolder,該方法在 TWinControl 中被重載, TWinControl 繼承類中鮮有繼續(xù)重載該方法的類,可以認為消息最后一次被處理的機會就是發(fā)生在 TWinControl.DefaultHandler 中。我們知道在消息循環(huán)中不處理的消息最后都應(yīng)該交給 Windows 的默認回調(diào)函數(shù) DefWindowProc API 來處理, TWinControl.DefaultHandler 最主要的工作就是完成這個,除此之外,還完成幾個額外的消息處理[2]。
VCL 的消息流程至此為止。
可能你還在為整個消息分派流程犯暈,讓我用實例來分析一下吧。 三、VCL 完整的消息分派流程
1. TButton
新建一個 Application,在 Form1 上放一個 Button (缺省名為Button1),在其 OnClick 事件中隨便寫點代碼,加上斷點,在調(diào)試之前,請打開 DCU 調(diào)試開關(guān)(Project->Options->Compiler->Use Debug DCUs), 這個開關(guān)如果不打開,是沒法調(diào)試 VCL 的,然后 F9 運行,當(dāng)停留在斷點上時,打開Call Stack 窗口(View->Debug Window->Call Stack)可看到調(diào)用順序如下(從底往上看):
TForm1.Button1Click($9637C0) TControl.Click TButton.Click TButton.CNCommand((48401, 660, 0, 524948, 0)) TControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TButtonControl.WndProc((48401, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TControl.Perform(48401,660,524948) DoControlMsg(524948,(no value)) TWinControl.WMCommand((273, 660, 0, 524948, 0)) TCustomForm.WMCommand((273, 660, 0, 524948, 0)) TControl.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TCustomForm.WndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) TWinControl.MainWndProc((273, 660, 524948, 0, 660, 0, 660, 8, 0, 0)) StdWndProc(918056,273,660,524948) TWinControl.DefaultHandler((no value)) TControl.WMLButtonUp((514, 0, 48, 13, (48, 13), 0)) TControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TWinControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TButtonControl.WndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) TWinControl.MainWndProc((514, 0, 852016, 0, 0, 0, 48, 13, 0, 0)) StdWndProc(524948,514,0,852016) TApplication.HandleMessage TApplication.Run Project1 |
一個 Button 被點擊,在 TButton 內(nèi)部會發(fā)生兩個消息:WM_LBUTTONDOWN/WM_LBUTTONUP, TButton 沒有處理 WM_LBUTTONUP(問題:為什么只響應(yīng) WM_LBUTTONUP,這兩個消息只應(yīng)該發(fā)生在 Windows 原生控件內(nèi),除非 TButton subclass 了 "Button",這部分代碼我沒看),只是交給 TWinControl.DefaultHandler,隨后 TButton 又將生成的 WM_COMMAND 消息發(fā)送給它的 Parent,即 TForm,經(jīng)過一系列消息傳遞, WM_COMMAND 在 TWinControl.WMCommand 中被處理,通過 DoControlMsg 將 WM_COMMAND 加工成 CN_COMMAND,再利用 TControl.Perform 將 CN_COMMAND 傳回 TButton,又通過一系列的消息傳遞到 TButton 中的 Dispatch,通過查詢動態(tài)方法表找到 Handler -- TButton.CNCommand,它又調(diào)用虛方法 TButton.Click,繼而調(diào)用 TControl.Click,在這個方法中會調(diào)用 FOnClick,而 FOnClick 特性值的內(nèi)容就是當(dāng)程序員使用對象查看器撰寫 TButton 的 OnClick 事件處理函數(shù)時 Delphi 便會自動指定給 TButton 的 OnClick 特性,例子中 OnClick 被指定為 TForm1.Button1Click,因此 TForm1.Button1Click 最終被調(diào)用。
2. TForm
新建一個 Application,為 Form1 的 OnMouseDown 事件隨便寫一點代碼,在這個方法上設(shè)斷點,F(xiàn)9 運行,看看 Call Stack
TForm1.FormMouseDown(???,???,[ssLeft],346,212) TControl.MouseDown(mbLeft,[ssLeft],346,212) TControl.DoMouseDown((513, 1, 346, 212, (346, 212), 0),mbLeft,[]) TControl.WMLButtonDown((513, 1, 346, 212, (346, 212), 0)) TControl.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TWinControl.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TCustomForm.WndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) TWinControl.MainWndProc((513, 1, 13893978, 0, 1, 0, 346, 212, 0, 0)) StdWndProc(2687598,513,1,13893978) TApplication.HandleMessage TApplication.Run Project1 |
鼠標在 Form 上點擊,產(chǎn)生兩個消息 WM_LBUTTONDOWN/WM_LBUTTONUP,但我們只截獲 WM_LBUTTONDOWN。產(chǎn)生的 WM_LBUTTONDOWN 經(jīng)過一系列的消息傳遞到達 TObject.Dispatch,通過查詢動態(tài)方法表在 TForm 的父類 TControl 中找到了 Handler -- TControl.WMLButtonDown,在 TControl.WMLButtonDown 中又經(jīng)過 TControl.DoMouseDown、TControl.MouseDown 一系列方法調(diào)用,最終調(diào)用到 FOnMouseDown,F(xiàn)OnMouseDown 被賦值為 TForm1.FormMouseDown,調(diào)用 FOnMouseDown 即調(diào)用 TForm1.FormMouseDown。 講了一大堆消息實現(xiàn)過程,那么在實際中到底有哪些應(yīng)用? 四、消息的實際應(yīng)用
如果你是共享軟件作者,經(jīng)常會為你的軟件被 Crack 掉所煩惱,你能做的就是要加強你的軟件的 Anti-Crack 功能,今天就交你一招。
如果你用過 Delphi 的專用反匯編工具 DEDE,那么你肯定知道像 Button1Click 這種 Event Handler 的方法入口地址 極容易被定位,其原理是根據(jù)TForm 的 RTTI 信息獲取的(通過分析 dfm 資源文件就可以獲得地址),其實 VCL 窗體只有 published 過的類成員才會生成 RTTI 信息。知道這個關(guān)鍵點加上對 VCL 消息機制的深入了解你就可以防止這一切發(fā)生。
1. Anti-Crack
新建一個 Application,在 Form1 上放兩個 Button,命名為 btnRegister、btnCancel,雙擊這兩個按鈕,分別生成TForm1.btnCancelClick、TForm1.btnRegisterClick 兩個 Event Handler 骨架代碼,然后在對象查看器中取消 btnRegister.OnClick 與 TForm1.btnRegisterClick 的關(guān)聯(lián), 隨后將 TForm1.btnCancelClick 的聲明放入 TForms1 聲明的 private 區(qū)段。再按照下面的代碼 內(nèi)容加入其他部分:
unit Unit1;
interface
uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type TForm1 = class(TForm) btnRegister: TButton; btnCancel: TButton; procedure btnCancelClick(Sender: TObject); private procedure btnRegisterClick(Sender: TObject); procedure WMCommand(var Message: TWMCommand); message WM_COMMAND; public { Public declarations } end;
var Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnCancelClick(Sender: TObject); begin Close; end;
procedure TForm1.btnRegisterClick(Sender: TObject); begin ShowMessage('Thx for ur registration.'); end;
procedure TForm1.WMCommand(var Message: TWMCommand); begin if Message.NotifyCode = BN_CLICKED then if FindControl(Message.Ctl) = btnRegister then begin btnRegisterClick(Self); Exit; end; inherited; end;
end. |
這個方法的本質(zhì)就是截獲 TForm1 的 WM_COMMAND 消息并自己處理,請自行分析代碼,我就不多說了。編譯完后你可以用 DEDE 反匯編一下,看看還能不能那么容易地找到 TForm1.btnRegisterClick 的入口地址。
結(jié)束語
VCL 消息機制你理解了嗎?是不是感到特別復(fù)雜?一個消息往往要經(jīng)過10幾個方法才能傳到 Event Handler,別看消息傳遞經(jīng)過這么漫長的路途,但是 VCL 消息機制的效率還是非常高的,因為很多關(guān)鍵的代碼都是用匯編直接寫成的,每一個中途站花費的時間也非常少,因此需要處理的消息還是能很快地到達目的地。
我最開始學(xué) Windows 編程是從 SDK 開始學(xué)起的,那時候會寫了基本的 Windows 程序,一段時間內(nèi)總認為會 SDK 比會用 Delphi 牛X,現(xiàn)在想起來真傻,比起直來直去的 SDK 編程,VCL 消息機制要復(fù)雜得多得多,看完 VCL 源碼后最大感受就是覺得以前跟沒學(xué)過編程似的,但不可否認的是,只有在你掌握了 OOP/ASM/SDK 這些基礎(chǔ)知識后,你才有看懂 VCL 源碼的資本,這些基礎(chǔ)知識你都掌握了嗎?
|