VCLHardCore——VCL窗口函数注册机制研究手记,兼与MFC比较
VCL HardCore ——VCL窗口函数注册机制研究手记,兼与MFC比较
By cheka cheka@yeah.net (转载请保留此信息)
这个名字起的有些耸人听闻,无他意,只为吸引眼球而已,如果您对下列关键词有兴趣,希望不要错过本文:
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; <
补充:软件开发 , Delphi ,