cnpack-group,您好!
今日在QQ群上聊到了一个话题,就是关于MouseEnter和MouseLeave事件的产生原理,
以及为啥不在VCL的普通控件中实现。讨论的过程有点繁复,这里总结一下讨论的结论:
一、MouseEnter和MouseLeave的来源
大伙儿都知道,Windows的WM开头的消息里,是不提供什么WM_MOUSEENTER的消息的。
而自己写控件时,偏偏又可以写message CM_MOUSEENTER等消息处理器。那么这个CM开头
的消息是从哪儿来的?答案在Application.Idle的DoMouseIdle中。
function TApplication.DoMouseIdle: TControl;
var
CaptureControl: TControl;
P: TPoint;
begin
GetCursorPos(P);
Result := FindDragTarget(P, True);
if (Result <> nil) and (csDesigning in Result.ComponentState) then
Result := nil;
CaptureControl := GetCaptureControl;
if FMouseControl <> Result then
begin
if ((FMouseControl <> nil) and (CaptureControl = nil)) or
((CaptureControl <> nil) and (FMouseControl = CaptureControl)) then
FMouseControl.Perform(CM_MOUSELEAVE, 0, 0);
FMouseControl := Result;
if ((FMouseControl <> nil) and (CaptureControl = nil)) or
((CaptureControl <> nil) and (FMouseControl = CaptureControl)) then
FMouseControl.Perform(CM_MOUSEENTER, 0, 0);
end;
end;
这里,最重要的就是一句Result := FindDragTarget(P, True),这句找到鼠标位置所在的
TControl类,然后和旧的MouseControl比较,如果有变化,则对旧的发个CM_MOUSELEAVE消
息,对新的发个CM_MOUSEENTER消息,然后将新的记录下来,供下次Idle的时候作为旧的来对
比。这应该是容易理解的。但需要注意的是,如果是俩Control嵌套的情形,那么当鼠标移入
子控件时,父控件也会收到CM_MOUSELEAVE消息,而此时鼠标处在子控件内,由于子控件的
Parent是父控件,所以鼠标也在父控件内,此时的CM_MOUSELEAVE消息就有点不符合常规了。
不过这应该是规定,所以也没办法,只要我们能看懂就行。
当初我们还犯了个错误,认为FindDragTarget(P, True)只能找到WinControl不能找到
TGraphicControl,结果后来看代码发现ControlAtPos是能找到TControl类的,而不光光是
TWinControl,所以CM_MOUSEENTER/CM_MOUSELEAVE消息是直接发给了鼠标作用的TControl。
二、CM_MOUSEENTER/CM_MOUSELEAVE消息和其参数的含义
CM_MOUSELEAVE和CM_MOUSEENTER俩消息的含义还随LParam的值不同而不同。上面的过程
中,LPARAM和WPARAM都是0,这代表,鼠标进入/离开了“收到此消息的Control”。另外当
LParam不为0的时候,那么它代表一个Control,这个Control应该是“收到此消息的Control”
的子Control,鼠标移入/移出是发生在它的身上。——这样的消息是从哪儿来的?看看这里:
procedure TControl.CMMouseEnter(var Message: TMessage);
begin
if FParent <> nil then
FParent.Perform(CM_MOUSEENTER, 0, Longint(Self));
end;
procedure TControl.CMMouseLeave(var Message: TMessage);
begin
if FParent <> nil then
FParent.Perform(CM_MOUSELEAVE, 0, Longint(Self));
end;
原来,TControl在收到CM_MOUSEENTER/CM_MOUSELEAVE消息的时候,默认会朝其Parent发送
同样的消息,只是把自己附在LParam参数中。所以当一个Control收到CM_MOUSEENTER/
CM_MOUSELEAVE消息的时候,不一定是由DoMouseIdle直接传来的,而可能是由其子控件发来
的。只有当LParam参数为0的时候,才代表鼠标移入/移出操作是发生在自身。
三、CM_MOUSEENTER/CM_MOUSELEAVE机制的不足
由于CM_MOUSEENTER/CM_MOUSELEAVE机制是来自Application.Idle,所以只在空闲的时候
产生,所以忙的时候,很可能该产生的CM_MOUSEENTER/CM_MOUSELEAVE消息会被吞掉而不发生。
这也是ActionMenuBar之类的控件经常产生时效滞后或痕迹不消失的原因。而ToolBar上的按钮
不产生此类问题则似乎因为它是Windows控件,大概有些内部的东西我们还没研究到。
大概正因为有这个不足,VCL的标准控件中才没实现OnMouseEnter和OnMouseLeave事件,
同时也保持了和Windows标准控件的一致。
四、TLabel控件的OnMouseEnter/和OnMouseLeave事件和一个有趣的现象
从D6起,TLabel控件增加了OnMouseEnter和OnMouseLeave事件,可以用来方便的处理鼠标
移入移出。它也是通过响应CM_MOUSEENTER/CM_MOUSELEAVE消息来实现这一点的。
做个有趣的试验,Form上放一Panel,Panel里放一Label。Form上写两个message处理函数
响应CM_MOUSEENTER/CM_MOUSELEAVE,并且在Label1的OnMouseEnter和OnMouseLeave里头写
处理事件,目的就是记录这些事件的前后顺序。我们自己的例子里,用了个MEMO,朝上输出字符
串来记录顺序。
当把鼠标从Panel外移动到Panel里,再移动到Label上,再移出Panel的时候,记录事件如下:
Form CMMouseEnter:Panel1
Label CMMouseEnter
Form CMMouseLEAVE:Panel1
Label CMMouseLeave
意思是,Form先收到CM_MouseEnter消息,告诉说Panel1发生了变化,然后Label1触发
OnMouseEnter事件;离开的时候,Form先收到MOUSELEAVE消息,告诉说Panel1发生了变化,然
后才触发Label的MouseLeave事件。这里,父类先遇到MOUSEENTER/MOUSELEAVE消息,然后再是
子类。为什么父类比子类更先触发呢?因为Label1从DoMouseIdle中收到消息后,先通知Panel1
,再进行OnMouseEnter处理,Panel1收到这个消息后,才通知Form说Panel1自己变了。
代码如下:
procedure TCustomLabel.CMMouseEnter(var Message: TMessage);
begin
inherited;
if Assigned(FOnMouseEnter) then
FOnMouseEnter(Self);
end;
因为它是先inherite的,而inherited会调用TControl的CMMouseEnter,从而先通知的父控件。
每一级子Control都调用父Control的Perform,但是都把lParam改成了自己,层层上报,每层都
说是自己的“功劳”,有点意思。
如果大家写控件的时候继承自TWinControl的类要处理CM_MOUSEENTER/CM_MOUSELEAVE消息,
记得判断一下LParam,看看究竟是鼠标移入/移出了自己的子控件还是移入/移出了自身,这点是
需要注意的。
五、BringToFront和SendToBack的实现以及和ZOrder的关系。
大家应该记得,TControl有个Controls列表,记录了Parent是自己的子控件。它除了记录
子控件实例外,还记录了子控件的摆放前后顺序,也就是ZOrder。当某子控件被BringToFront
的时候,就把它移动到父控件的Controls的列表的第一项然后重画,SendToBack则是放到最后
一项(或许说反了)。具体可以看看BringToFront和SendToBack的实现。
今天就总结到这里,以后有什么话题,欢迎一块儿讨论。
致
礼!
Passion passion@cnvcl.org
CnPack 开发组管理员
http://www.cnvcl.org
2004-06-04
|