===============================================================================
⊙ VCL 的消息处理从 TWinControl
.
MainWndProc 开始
===============================================================================
通过对 Application
.
Run、TWinControl
.
Create、TWinControl
.
Handle 和 TWinControl
.
CreateWnd 的讨论,我们现在可以把焦点转向 VCL 内部的消息处理过程。VCL 控件的消息源头就是 TWinControl
.
MainWndProc 函数。(如果不能理解这一点,请重新阅读上面的讨论。)
让我们先看一下 MainWndProc 函数的代码(异常处理的语句被我删除):
procedure
TWinControl
.
MainWndProc(
var
Message: TMessage);
begin
WindowProc(Message);
end
;
TWinControl
.
MainWndProc 以引用(也就是隐含传地址)的方式接受一个 TMessage 类型的参数,TMessage 的定义如下(其中的WParam、LParam、Result 各有 HiWord 和 LoWord 的联合字段,被我删除了,免得代码太长):
TMessage =
packed
record
Msg:
Cardinal
;
WParam:
Longint
;
LParam:
Longint
;
Result:
Longint
);
end
;
TMessage 中并没有窗口句柄,因为这个句柄已经在窗口创建之后保存在 TWinControl
.
Handle 之中。TMessage
.
Msg 是消息的 ID 号,这个消息可以是 Windows 标准消息、用户定义的消息或 VCL 定义的 Control 消息等。
WParam 和 LParam 与标准 Windows 回调函数中 wParam 和 lParam 的意义相同,Result 相当于标准 Windows 回调函数的返回值。
注意 MainWndProc 不是虚函数,所以它不能被 TWinControl 的继承类重载。(思考:为什么 Borland 不将 MainWndProc 设计为虚函数呢?)
MainWndProc 中建立两层异常处理,用于释放消息处理过程中发生异常时的资源泄漏,并调用默认的异常处理过程。被异常处理包围着的是 WindowProc(Message)。WindowProc 是 TControl(而不是 TWinControl) 的一个属性(
property
):
property
WindowProc: TWndMethod read FWindowProc
write
FWindowProc;
WindowProc 的类型是 TWndMethod,所以它是一个对象相关的消息处理函数指针(请参考前面 TWndMethod 的介绍)。在 TControl
.
Create 中 FWindowProc 被赋值为 WndProc。
WndProc 是 TControl 的一个函数,参数与 TWinControl
.
MainWndProc 相同:
procedure
TControl
.
WndProc(
var
Message: TMessage); virtual;
原来 MainWndProc 只是个代理函数,最终处理消息的是 TControl
.
WndProc 函数。
那么 Borland 为什么要用一个 FWindowProc 来存储这个 WndProc 函数,而不直接调用 WndProc 呢?我猜想可能是基于效率的考虑。还记得上面 TWndMethod 的讨论吗?一个 TWndMethod 变量可以被赋值为一个虚函数,编译器对此操作的实现是通过对象指针访问到了对象的虚函数表,并把虚函数表项中的函数地址传回。由于 WndProc 是一个调用频率非常高的函数(可能要用“百次/秒”或“千次/秒”来计算),所以如果每次调用 WndProc 都要访问虚函数表将会浪费大量时间,因此在 TControl 的构造函数中就把 WndProc 的真正地址存储在 WindowProc 中,以后调用 WindowProc 将就转换为静态函数的调用,以加快处理速度。
===============================================================================
⊙ TWinControl
.
WndProc
===============================================================================
转了层层弯,到现在我们才刚进入 VCL 消息系统处理开始的地方:WndProc 函数。如前所述,TWinControl
.
MainWndProc 接收到消息后并没有处理消息,而是把消息传递给 WindowProc 处理。由于 WindowProc 总是指向当前对象的 WndProc 函数的地址,我们可以简单地认为 WndProc 函数是 VCL 中第一个处理消息的函数,调用 WindowProc 只是效率问题。
WndProc 函数是个虚函数,在 TControl 中开始定义,在 TWinControl 中被重载。Borland 将 WndProc 设计为虚函数就是为了各继承类能够接管消息处理,并把未处理的消息或加工过的消息传递到上一层类中处理。
这里将消息处理的传递过程和对象的构造函数稍加对比:
对象的构造函数通常会在第一行代码中使用
inherited
语句调用父类的构造函数以初始化父类定义的成员变量,父类也会在构造函数开头调用祖父类的构造函数,如此递归,因此一个 TWinControl 对象的创建过程是 TComponent
.
Create -> TControl
.
Create -> TWinControl
.
Create。
而消息处理函数 WndProc 则是先处理自己想要的消息,然后看情况是否要递交到父类的 WndProc 中处理。所以消息的处理过程是 TWinControl
.
WndProc -> TControl
.
WndProc。
因此,如果要分析消息的处理过程,应该从子类的 WndProc 过程开始,然后才是父类的 WndProc 过程。由于 TWinControl 是第一个支持窗口创建的类,所以它的 WndProc 是很重要的,它实现了最基本的 VCL 消息处理。
TWinControl
.
WndProc 主要是预处理一些键盘、鼠标、窗口焦点消息,对于不必响应的消息,TWinControl
.
WndProc 直接返回,否则把消息传递至 TControl
.
WndProc 处理。
从 TWinControl
.
WndProc 摘抄一段看看:
WM_KEYFIRST
..
WM_KEYLAST:
if
Dragging
then
Exit;
这段代码的意思是:如果当前组件正处于拖放状态,则丢弃所有键盘消息。
再看一段:
WM_MOUSEFIRST
..
WM_MOUSELAST:
if
IsControlMouseMsg(TWMMouse(Message))
then
begin
if
(Message
.
Result =
0
)
and
HandleAllocated
then
DefWindowProc(Handle, Message
.
Msg, Message
.
wParam, Message
.
lParam);
Exit;
end
;
这里的 IsControlMouseMsg 很关键。让我们回忆一下:TControl 类的对象并没有创建 Windows 窗口,它是怎样接收到鼠标和重绘等消息的呢?原来这些消息就是由它的 Parent 窗口发送的。
在上面的代码中,TWinControl
.
IsControlMouseMsg 判断鼠标地址是否落在 TControl 类控件上,如果不是就返回否值。TWinControl 再调用 TControl
.
WndProc,TControl
.
WndProc 又调用了 TObject
.
Dispatch 方法,这是后话。
如果当前鼠标地址落在窗口上的 TControl 类控件上,则根据 TControl 对象的相对位置重新生成了鼠标消息,再调用 TControl
.
Perform 方法把加工过的鼠标消息直接发到 TControl
.
WndProc 处理。TControl
.
Perform 方法以后再谈。
如果 TWinControl 的继承类重载 WndProc 处鼠标消息,但不使用
inherited
把消息传递给父类处理,则会使从 TControl 继承下来的对象不能收到鼠标消息。现在我们来做个试验,下面 Form1 上的 TSpeedButton 等非窗口控件不会发生 OnClick 等鼠标事件。
procedure
TForm1
.
WndProc(
var
Message: TMessage); override;
begin
case
Message
.
Msg
of
WM_MOUSEFIRST
..
WM_MOUSELAST:
begin
DefWindowProc(Handle, Message
.
Msg, Message
.
WParam, Message
.
LParam);
Exit;
end
;
else
inherited
;
end
;
end
;
TWinControl
.
WndProc 的最后一行代码是:
inherited
WndProc(Message);
也就是调用 TControl
.
WndProc。让我们来看看 TControl
.
WndProc 做了些什么。
===============================================================================
⊙ TControl
.
WndProc
===============================================================================
TControl
.
WndProc 主要实现的操作是:
响应与 Form Designer 的交互(在设计期间)
在控件不支持双击的情况下把鼠标双击事件转换成单击
判断鼠标移动时是否需要显示提示窗口(HintWindow)
判断控件是否设置为 AutoDrag,如果是则执行控件的拖放处理
调用 TControl
.
MouseWheelHandler 实现鼠标滚轮消息
使用 TObject
.
Dispatch 调用 DMT 消息处理方法
TControl
.
WndProc 相对比较简单,在此只随便谈谈第二条。你是否有过这样的使用经验:在你快速双击某个软件的 Button 时,只形成一次 Click 事件。所以如果你需要设计一个不管用户用多快的速度点击,都能生成同样点击次数 Click 事件的按钮时,就需要参考 TControl
.
WndProc 处理鼠标消息的过程了。
TControl
.
WndProc 最后一行代码是 Dispatch(Message),也就是说如果某个消息没有被 TControl 以后的任何类处理,消息会被 Dispatch 处理。
TObject
.
Dispatch 是 Delphi VCL 消息体系中非常关键的方法。
===============================================================================
⊙ TObject
.
Dispatch
===============================================================================
TObject
.
Dispatch 是个虚函数,它的声明如下:
procedure
TObject
.
Dispatch(
var
Message); virtual;
请注意它的参数虽然与 MainWndProc 和 WndProc 的参数相似,但它没有规定参数的类型。这就是说,Dispatch 可以接受任何形式的参数。
Delphi 的文档指出:Message参数的前
2
个字节是 Message 的 ID(下文简称为 MsgID),通过 MsgID 搜索对象的消息处理方法。
这段话并没有为我们理解 Dispatch 方法提供更多的帮助,看来我们必须通过阅读源代码来分析这个函数的运作过程。
TObject
.
Dispatch 虽然是个虚方法,但却没有被 TPersistent、TComponent、TControl、TWinControl、TForm 等后续类重载( TCommonDialog 调用了 TObject
.
Dispatch,但对于整个 VCL 消息系统并不重要),并且只由 TControl
.
WndProc 调用过。所以可以简单地认为如果消息没有在 WndProc 中被处理,则被 TObject
.
Dispatch 处理。
我们很容易查觉到一个很重要的问题:MsgID 是
2
个字节,而 TMessage
.
Msg 是
4
个字节,如果 TControl
.
WndProc 把 TMessage 消息传递给 Dispatch 方法,是不是会形成错误的消息呢?
要解释这个问题,必须先了解 Windows 消息的规则。由于 Windows 操作系统的所有窗口都使用消息传递事件和信息,Microsoft 必须制定窗口消息的格式。如果每个程序员都随意定义消息 ID 值肯定会产生混乱。Microsoft 把窗口消息分为五个区段:
0
×
00000000
至 WM_USER –
1
标准视窗消息,以 WM_ 为前缀
WM_USER 至 WM_APP -
1
用户自定义窗口类的消息
WM_APP 至
0
×0000BFFF 应用程序级的消息
0
×0000C000 至
0
×0000FFFF RegisterWindowMessage 生成的消息范围
0
×
00010000
至 0xFFFFFFFF Microsoft 保留的消息,只由系统使用
( WM_USER =
0
×
00000400
, WM_APP =
0
×
00008000
)
发现问题的答案了吗?原来应用程序真正可用的消息只有
0
×
00000000
至
0
×0000FFFF,也就是消息 ID 只有低位
2
字节是有效的。(Borland 真是牛啊,连这也能想出来。)
由于 Intel CPU 的内存存放规则是高位字节存放在高地址,低位字节存放在低地址,所以 Dispatch 的 Message 参数的第一个内存字节就是 LoWord(Message
.
Msg)。下图是 Message参数的内存存放方式描述:
| | + Memory
|——–|
| HiWord |
|——–|
| LoWord | =
$C000
,调用 DefaultHandler (注意这里)
PUSH EAX ; 保存对象的指针
MOV EAX,[EAX] ; 找到对象的 VMT 指针
CALL GetDynaMethod ; 调用对象的动态方法; 如果找到了动态方法 ZF =
0
,
; 没找到 ZF =
1
; 注:GetDynaMethod 是 System
.
pas 中的获得动态方法地
; 址的汇编函数
POP EAX ; 恢复 EAX 为对象的指针
JE @@default ; 如果没找到相关的动态方法,调用 DefaultHandler
MOV ECX,ESI ; 把找到的动态方法指针存入 ECX
POP ESI ; 恢复 ESI
JMP ECX ; 调用对象的动态方法
@@default:
POP ESI ; 恢复 ESI
MOV ECX,[EAX] ; 把对象的 VMT 指针存入 ECX,以调用 DefaultHandler
JMP DWORD PTR [ECX] + VMTOFFSET TObject
.
DefaultHandler
end
;
TObject
.
Dispatch 的执行过程是:
把 MsgID 存入 SI,作为动态方法的索引值
如果 SI >=
$C000
,则调用 DefaultHandler(也就是所有 RegisterWindowMessage
生成的消息ID 会直接被发送到 DefaultHandler 中,后面会讲一个实例)
检查是否有相对应的动态方法
找到了动态方法,则执行该方法
没找到动态方法,则调用 DefaultHandler
原来以 message 关键字定义的对象方法就是动态方法,随便从 TWinControl 中抓几个消息处理函数出来:
procedure
WMSize(
var
Message: TWMSize); message WM_SIZE;
procedure
WMMove(
var
Message: TWMMove); message WM_MOVE;
到现在终于明白 WM_SIZE、WM_PAINT 方法的处理过程了吧。不但是 Windows 消息,连 Delphi 自己定义的消息也是以同样的方式处理的:
procedure
CMEnabledChanged(
var
Message: TMessage); message CM_ENABLEDCHANGED;
procedure
CMFontChanged(
var
Message: TMessage); message CM_FONTCHANGED;
所以如果你自己针对某个控件定义了一个消息,你也可以用 message 关键字定义处理该方法的函数,VCL 的消息系统会自动调用到你定义的函数。
由于 Dispatch 的参数只以最前
2
个字节为索引,并且自 MainWndProc 到 WndProc 到 Dispatch 都是以引用(传递地址)的方式来传递消息内容,你可以将消息的结构设置为任何结构,甚至可以只有 MsgID —— 只要你在处理消息的函数中正确地访问这些参数就行。
最关键的 Dispatch 方法告一段落,现在让我们看看 DefaultHandler 做了些什么?
===============================================================================
⊙ TWinControl
.
DefaultHandler
===============================================================================
DispatchHandler 是从 TObject 就开始存在的,它的声明如下:
procedure
TObject
.
DefaultHandler(
var
Message); virtual;
从名字也可以看出该函数的大概目的:最终的消息处理函数。在 TObject 的定义中 DefaultHandler 并没有代码,DefaultHandler 是在需要处理消息的类(TControl)之后被重载的。
从上面的讨论中已经知道 DefaultHandler 是由 TObject
.
Dispatch 调用的,所以 DefaultHandler 和 Dispatch 的参数类型一样都是无类型的
var
Message。
由于 DefaultHandler 是个虚方法,所以执行流程是从子类到父类。在 TWinControl 和 TControl 的 DefaultHandler 中,仍然遵从 WndProc 的执行规则,也就是 TWinControl 没处理的消息,再使用
inherited
调用 TControl
.
DefaultHandler 来处理。
在 TWinControl
.
DefaultHandler 中先是处理了一些不太重要的Windows 消息,如WM_CONTEXTMENU、WM_CTLCOLORMSGBOX等。然后做了两件比较重要的工作:
1
、处理 RM_GetObjectInstance 消息;
2
、对所有未处理的窗口消息调用 TWinControl
.
FDefWndProc。
下面分别讨论。
RM_GetObjectInstance 是应用程序启动时自动使用 RegisterWindowMessage API 注册的 Windows 系统级消息ID,也就是说这个消息到达 Dispatch 后会无条件地传递给 DefaultHandler(见 Dispatch 的分析)。TWinControl
.
DefaultHandler 发现这个消息就把 Self 指针设置为返回值。在 Controls
.
pas 中有个函数 ObjectFromHWnd 使用窗口句柄获得 TWinControl 的句柄,就是使用这个消息实现的。不过这个消息是由 Delphi 内部使用,不能被应用程序使用。(思考:每次应用程序启动都会调用 RegisterWindowMessage,如果电脑长期不停机,那么 0xC000 – 0xFFFF 之间的消息 ID 是否会被耗尽?)
另外,TWinControl
.
DefaultHandler 在 TWinControl
.
FHandle 不为
0
的情况下,使用 CallWindowProc API 调用 TWndControl
.
FDefWndProc 窗口过程。FDefWndProc 是个指针,它是从哪里初始化的呢?跟踪一下,发现它是在 TWinControl
.
CreateWnd 中被设置为如下值:
FDefWndProc := Params
.
WindowClass
.
lpfnWndProc;
还记得前面讨论的窗口创建过程吗?TWinControl
.
CreateWnd 函数首先调用 TWinControl
.
CreateParams 获得待创建的窗口类的参数。CreateParams 把 WndClass
.
lpfnWndProc 设置为 Windows 的默认回调函数 DefWindowProc API。但 CreateParams 是个虚函数,可以被 TWinControl 的继承类重载,因此程序员可以指定一个自己设计的窗口过程。
所以 TWinControl
.
DefaultHandler 中调用 FDefWndProc 的意图很明显,就是可以在 Win32 API 的层次上支持消息的处理(比如可以从 C 语言写的 DLL 中导入窗口过程给 VCL 控件),给程序员提供充足的弹性空间。
TWinControl
.
DefaultHandler 最后一行调用了
inherited
,把消息传递给 TControl 来处理。
TControl
.
DefaultHandler 只处理了三个消息 WM_GETTEXT、WM_GETTEXTLENGTH、WM_SETTEXT。为什么要处理这个几个看似不重要的消息呢?原因是:Windows 系统中每个窗口都有一个 WindowText 属性,而 VCL 的 TControl 为了模拟成窗口也存储了一份保存在 FText 成员中,所以 TControl 在此接管这几个消息。
TControl
.
DefaultHandler 并没有调用
inherited
,其实也没有必要调用,因为 TControl 的祖先类都没有实现 DefaultHandler 函数。可以认为 DefaultHandler 的执行到此为止。
VCL 的消息流程至此为止。
===============================================================================
⊙ TControl
.
Perform 和 TWinControl
.
Broadcast
===============================================================================
现在介绍 VCL 消息系统中两个十分简单但调用频率很高的函数。
TControl
.
Perform 用于直接把消息送往控件的消息处理函数 WndProc。Perform 方法不是虚方法,它把参数重新组装成一个 TMessage 类型,然后调用 WindowProc(还记得 WindowProc 的作用吗?),并返回 Message
.
Result 给用户。它的调用格式如下:
function
TControl
.
Perform(Msg:
Cardinal
; WParam, LParam:
Longint
):
Longint
;
Perform 经常用于通知控件某些事件发生,或得到消息处理的结果,如下例:
Perform(CM_ENABLEDCHANGED,
0
,
0
);
Text := Perform(WM_GETTEXTLENGTH,
0
,
0
);
TWinControl
.
Broadcast 用于把消息广播给每一个子控件。它调用 TWinControl
.
Controls[] 数组中的所有对象的 WindowsProc 过程。
procedure
TWinControl
.
Broadcast(
var
Message);
注意 Broadcast 的参数是无类型的。虽然如此,在 Broadcast 函数体中会把消息转换为 TMessage 类型,也就是说 Broadcast 的参数必须是 TMessage 类型。那么为什么要设计为无类型的消息呢?原因是 TMessage 有很多变体(Msg 和 Result 字段不会变,WParam 和 LParam 可设计为其它数据类型),将 Broadcast 设计为无类型参数可以使程序员不用在调用前强制转换参数,但调用时必须知道这一点。比如以下字符消息的变体,是和 TMessage 兼容的:
TWMKey =
packed
record
Msg:
Cardinal
;
CharCode:
Word
;
Unused:
Word
;
KeyData:
Longint
;
Result:
Longint
;
end
;
===============================================================================
⊙ TWinControl
.
WMPaint
===============================================================================
上面在讨论 TWinControl
.
WndProc 时提到,TControl 类控件的鼠标和重绘消息是从 Parent TWinControl 中产生的。但我们只发现了鼠标消息的产生,那么重绘消息是从哪里产生出来的呢?答案是TWinControl
.
WMPaint:
procedure
TWinControl
.
WMPaint(
var
Message: TWMPaint); message WM_PAINT;
在 TWinControl
.
WMPaint 中建立了双缓冲重绘机制,但我们目前不关心这个,只看最关键的代码:
if
not
(csCustomPaint
in
ControlState)
and
(ControlCount =
0
)
then
inherited
else
PaintHandler(Message);
这段代码的意思是,如果控件不支持自绘制并且不包含 TControl 就调用
inherited
。
inherited
是什么呢?由于 TWinControl
.
WMPaint 的父类 TControl 没有实现这个消息句柄,Delphi 生成的汇编代码竟然是:call Self
.
DefaultHandler。(TWinControl
.
DefaultHandler 只是简单地调用 TWinControl
.
FDefWndProc。)
如果条件为否,那么将调用 TWinControl
.
PaintHandler(不是虚函数)。PaintHandler 调用 BeginPaint API 获得窗口设备环境,再使用该设备环境句柄为参数调用 TWinControl
.
PaintWindow。在 TWinControl 中 PaintWindow 只是简单地把消息传递给 DefaultHandler。PaintWindow 是个虚函数,可以在继承类中被改写,以实现自己需要的绘制内容。PaintHandler 还调用了 TWinControl
.
PaintControls 方法。PaintControls 使用 Perform 发送 WM_PAINT 消息给 TWinControl 控件包含的所有 TControl 控件。
这样,TControl 控件才获得了重绘的消息。
让我们设计一个 TWinControl 的继承类作为练习:
TMyWinControl =
class
(TWinControl)
protected
procedure
PaintWindow(DC: HDC); override;
public
constructor
Create(AOwner: TComponent); override;
end
;
constructor
TMyWinControl
.
Create(AOwner: TComponent);
begin
inherited
Create(AOwner);
ControlState := ControlState + [csCustomPaint];
end
;
procedure
TMyWinControl
.
PaintWindow(DC: HDC);
var
Rect: TRect;
begin
Windows
.
GetClientRect(Handle, Rect);
FillRect(DC, Rect, COLOR_BTNSHADOW +
1
);
SetBkMode(DC, TRANSPARENT);
DrawText(DC, ‘Hello, TMyWinControl’, -
1
, Rect, DT_SINGLELINE
or
DT_VCENTER
or
DT_CENTER);
end
;
上面实现的 TMyWinControl 简单地重载 PaintWindow 消息,它可以包含 TControl 对象,并能正确地把它们画出来。如果你确定该控件不需要包含 TControl 对象,你也可以直接重载 WMPaint 消息,这就像用 C 语言写普通的 WM_PAINT 处理函数一样。
===============================================================================
⊙ 以 TWinControl 为例描述消息传递的路径
===============================================================================
下图描述一条消息到达后消息处理函数的调用路径,每一层表示函数被上层函数调用。
TWinControl
.
FObjectInstance
|-TWinControl
.
MainWndProc
|-TWinControl
.
WindowProc
|-TWinControl
.
WndProc
|-TControl
.
WndProc
|-TObject
.
Dispatch
|-Call DMT messages
|-TWinControl
.
DefaultHandler
|-TControl
.
DefaultHandler
注:
如前文所述,上图中的 WindowProc 是个指针,所以它在编译器级实际上等于 WndProc,而不是调用 WndProc,图中为了防止与消息分枝混淆特意区分成两层。
TObject
.
Dispatch 有两条通路,如果当前控件以 message 关键字实现了消息处理函数,则呼叫该函数,否则调用 DefaultHandler。
有些消息处理函数可能在中途就已经返回了,有些消息处理函数可能会被递归调用。
===============================================================================
结束语
VCL 的消息机制就讨论到这里。希望我们通过本文的讨论理清了 VCL 处理消息的框架,今后我们将使用这些最基础的知识开始探索 Delphi 程序设计的旅程。