VCL HardCore ——VCL窗口函數注冊機制研究手記,兼與MFC比較
By cheka [email protected] (轉載請保留此信息)
這個名字起的有些聳人聽聞,無他意,只為吸引眼球而已,如果您對下列關鍵詞有興趣,希望不要錯過本文:
1. VCL可視組件在內存中的分頁式管理;
2. 讓系統回調類的成員方法
3. Delphi 中匯編指令的使用
4. Hardcore
5. 第4條是騙你的
我們知道Windows平台上的GUI程序都必須遵循Windows的消息響應機制,可以簡單概括如下,所有的窗口控件都向系統注冊自身的窗口函數,運行期間消息可被指派至特定窗口控件的窗口函數處理。對消息相應機制做這樣的概括有失嚴密,請各位見諒,我想趕緊轉向本文重點,即在利用Object Pascali或是C++這樣的面向對象語言編程中,如何把一個類的成員方法向系統注冊以供回調。
在注冊窗口類即調用RegisterClass函數時,我們向系統傳遞的是一個WindowProc 類型的函數指針
WindowProc 的定義如下
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
如果我們有一個控件類,它擁有看似具有相同定義的成員方法TMyControl.WindowProc,可是卻不能夠將它的首地址作為lpfnWndProc參數傳給RegisterClass,道理很簡單,因為Delphi中所有類成員方法都有一個隱含的參數,也就是Self,因此無法符合標准 WindowProc 的定義。
那麼,在VCL中,控件向系統注冊時究竟傳遞了一個什麼樣的窗口指針,同時通過這個指針又是如何調到各個類的事件響應方法呢?我先賣個關子,先看看MFC是怎麼做的。
在調查MFC代碼之前,我作過兩種猜想:
一,作注冊用的函數指針指向的是一個類的靜態方法,
靜態方法同樣不需要隱含參數 this (對應 Delphi中的 Self ,不過Object Pascal不支持靜態方法)
二,作注冊用的函數指針指向的是一個全局函數,這當然最傳統,沒什麼好說的。
經過簡單的跟蹤,我發現MFC中,全局函數AfxWndProc是整個MFC程序處理消息的“根節點”,也就是說,所有的消息都由它指派給不同控件的消息響應函數,也就是說,所有的窗口控件向系統注冊的窗口函數很可能就是 AfxWndProc (抱歉沒做深入研究,如果不對請指正)。而AfxWndProc 是如何調用各個窗口類的WndProc呢?
哈哈,MFC用了一種很樸素的機制,相比它那麼多稀奇古怪的宏來說,這種機制相當好理解:使用一個全局的Map數據結構來維護所有的窗口對象和Handle(其中Handle為鍵值),然後AfxWndProc根據Handle來找出唯一對應的窗口對象(使用靜態函數CWnd::FromHandlePermanent(HWND hWnd) ),然後調用其WndProc,注意WndProc可是虛擬方法,因此消息能夠正確到達所指定窗口類的消息響應函數並被處理。
於是我們有理由猜想VCL也可能采用相同的機制,畢竟這種方式實現起來很簡單。我確實是這麼猜的,不過結論是我錯了......
開場秀結束,好戲正式上演。
在Form1上放一個Button(缺省名為Button1),在其OnClick事件中寫些代碼,加上斷點,F9運行,當停留在斷點上時,打開Call Stack窗口(View->Debug Window->Call Stack, 或者按Ctrl-Alt-S )可看到調用順序如下(從底往上看,stack嘛)
( 如果你看到的 Stack 和這個不一致,請打開DCU 調試開關 Project->Options->Compiler->Use Debug DCUs, 這個開關如果不打開,是沒法調試VCL源碼的 )
TForm1.Button1Click(???)
TControl.Click
TButton.Click
TButton.CNCommand ((48401, 3880, 0, 3880, 0))
TControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TButtonControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TControl.Perform (48401,3880,3880)
DoControlMsg (3880,(no value))
TWinControl.WMComman d((273, 3880, 0, 3880, 0))
TCustomForm.WMCommand ((273, 3880, 0, 3880, 0))
TControl.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TCustomForm.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.MainWndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
StdWndProc (3792,273,3880,3880)
可見 StdWndProc 看上去象是扮演了MFC中 AfxWndProc 的角色,不過我們先不談它,如果你抑制不住好奇心,可以提前去看它的源碼,在Forms.pas中,看到了麼? 是不是特~~~~別有趣阿。
實際上,VCL在RegisterClass時傳遞的窗口函數指針並非指向StdWndProc。那是什麼呢?
我跟,我跟,我跟跟跟,終於在Controls.pas的TWindowControl的實現代碼中
(procedure TWinControl.CreateWnd;) 看到了RegisterClass的調用,hoho,終於找到組織了......別忙,發現了沒,這時候注冊的窗口函數是InitWndProc,看看它的定義,嗯,符合標准,再去瞧瞧代碼都干了些什麼。
發現這句:
SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
我Faint,搞了半天InitWndProc初次被調用(對每一個Wincontrol來說)就把自個兒給換了,新上崗的是FObjectInstance。下面還有一小段匯編,是緊接著調用FObjectInstance的,調用的理由不奇怪,因為以後調用FObjectInstace都由系統CallBack了,但現在還得勞InitWndProc的大駕去call。調用的方式有些講究,不過留給您看完這篇文章後自個兒琢磨去吧。
接下來只能繼續看FObjectInstance是什麼東東,它定義在 TWinControl 的 Private 段,是個Pointer也就是個普通指針,當什麼使都行,你跟Windows說它就是 WndProc 型指針 Windows 拿你也沒轍。
FObjectInstance究竟指向何處呢,鏡頭移向 TWincontrol 的構造函數,這是FObjectInstance初次被賦值的地方。 多余的代碼不用看,焦點放在這句上
FObjectInstance := MakeObjectInstance(MainWndProc);
可以先告訴您,MakeObjectInstance是本主題最精彩之處,但是您現在只需知道FObjectInstance“指向了”MainWndProc,也就是說通過某種途徑VCL把每個MainWndProc作為窗口函數注冊了,先證明容易的,即 MainWndProc 具備窗口函數的功能,來看代碼:
( 省去異常處理 )
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
WindowProc(Message);
FreeDeviceContexts;
FreeMemoryContexts;
end;
FreeDeviceContexts; 和 FreeMemoryContexts 是保證VCL線程安全的,不在本文討論之列,只看WindowProc(Message); 原來 MainWndProc 把消息委托給了方法 WindowProc處理,注意到 MainWndProc 不是虛擬方法,而 WindowProc 則是虛擬的,了解 Design Pattern 的朋友應該點頭了,嗯,是個 Template Method , 很自然也很經典的用法,這樣一來所有的消息都能准確到達目的地,也就是說從功能上看 MainWndProc 確實可以充作窗口函數。您現在可以回顧一下MFC的 AfxWindowProc 的做法,同樣是利用對象的多態性,但是兩種方式有所區別。
是不是有點亂了呢,讓我們總結一下,VCL 注冊窗口函數分三步:
1. [ TWinControl.Create ]
FObjectInstance 指向了 MainWndProc
2. [ TWinControl.CreateWnd ]
WindowClass.lpfnWndProc 值為 @InitWndProc; <