CnPack 开源软件项目 - 6月4日关于MOUSEENTER和MOUSELEAVE的讨论
  网站首页 下载中心 每日构建 文档中心 捐助我们 开发论坛 关于我们 致谢名单 English


 Google 搜索

内容: 
 最新下载


 
CnWizards 1.5.1.1219
[2024-11-03]

 
CnVCL 组件包 20241103
[2024-11-03]

 
CnPack 密码算法库 20241103
[2024-11-03]
  每日构建版下载
  专家包时间线
 项目相关链接


 
CnPack GitHub 首页
GIT 使用说明
申请加入 CnPack
CnPack 成员名单
 网站访问量

今日首页访问: 69
今日页面流量: 295
全部首页访问: 5296676
全部页面流量: 21342279
建站日期: 2003-09-01

 
6月4日关于MOUSEENTER和MOUSELEAVE的讨论
 
CnPack 开源软件项目 2004-06-05 21:55:51

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



本文已阅读 10347 次
来自: CnPack 开源软件项目

上一主题 | 返回上级下一主题

相关主题:


版权所有(C) 2001-2024 CnPack 开发组 网站编写:Zhou Jinyu