You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Delphi 10.3 WinControl消息路由与自定义控件鼠标状态感知问题

问题描述

组件层级结构:

TForm
  |-- TMyCustomScrollBox
        |-- TMyCustomButton
        |-- TMyCustomButton
        |-- TMyCustomButton
        ...

其中TMyCustomButton设置为alTop对齐。

当前已做处理:

  • 在TMyCustomButton中捕获了CMMouseEnter、CMMouseLeave和WMMouseMove事件;
  • 在TMyCustomScrollBox中同样捕获了上述三类事件。

需求:

  1. 让TMyCustomScrollBox能够知晓鼠标正处于内部的TMyCustomButton上;
  2. 当鼠标离开TMyCustomButton后,能判断鼠标是否仍在TMyCustomScrollBox范围内;
  3. 鼠标完全离开TMyCustomScrollBox时,触发其CMMouseLeave事件。

当前TMyCustomButton的事件处理代码:

procedure TUItemButton.CMMouseEnter(var Msg: TMessage);
begin
  if Enabled then begin
    ButtonState := csHover;
  end;
  inherited;
end;

procedure TUItemButton.CMMouseLeave(var Msg: TMessage);
begin
  if Enabled then begin
    ButtonState := csNone;
  end;
  inherited;
end;

procedure TUItemButton.WMMouseMove(var Msg: TWMMouseMove);
begin
  if Enabled then
    Repaint;
  inherited;
end;

疑问:

  • 调用inherited是否等同于把事件消息推送给父WinControl?
  • 该如何正确实现消息从TMyCustomButton到TMyCustomScrollBox的传递?
解决方案

关于inherited的作用

inherited在这里不是把消息推送给父控件,而是调用当前类继承链中父类的同名方法。比如如果TUItemButton继承自TButton或自定义基类,inherited会执行父类里的CMMouseEnter等方法,完成控件自身的基础消息处理,不会自动把消息转发给容器控件(TMyCustomScrollBox)。

实现消息传递的可行方案

方案1:主动向父控件发送自定义消息

在TMyCustomButton的鼠标事件中,主动告知父控件自身的鼠标状态变化:

  1. 先在公共单元定义自定义消息:
    const
      UM_BUTTON_MOUSE_ENTER = WM_USER + 100; // 自定义按钮鼠标进入消息
      UM_BUTTON_MOUSE_LEAVE = WM_USER + 101; // 自定义按钮鼠标离开消息
    
  2. 修改TMyCustomButton的事件处理逻辑:
    procedure TUItemButton.CMMouseEnter(var Msg: TMessage);
    begin
      if Enabled then begin
        ButtonState := csHover;
      end;
      // 向父控件发送鼠标进入通知
      if Parent is TMyCustomScrollBox then
        PostMessage(TMyCustomScrollBox(Parent).Handle, UM_BUTTON_MOUSE_ENTER, 0, 0);
      inherited;
    end;
    
    procedure TUItemButton.CMMouseLeave(var Msg: TMessage);
    begin
      if Enabled then begin
        ButtonState := csNone;
      end;
      // 向父控件发送鼠标离开通知
      if Parent is TMyCustomScrollBox then
        PostMessage(TMyCustomScrollBox(Parent).Handle, UM_BUTTON_MOUSE_LEAVE, 0, 0);
      inherited;
    end;
    
  3. 在TMyCustomScrollBox中处理自定义消息:
    type
      TMyCustomScrollBox = class(TScrollBox)
      private
        FButtonHoverCount: Integer; // 记录处于悬停状态的子按钮数量
        procedure UMButtonMouseEnter(var Msg: TMessage); message UM_BUTTON_MOUSE_ENTER;
        procedure UMButtonMouseLeave(var Msg: TMessage); message UM_BUTTON_MOUSE_LEAVE;
        procedure CheckMouseInScrollBox; // 检查鼠标是否仍在ScrollBox内
      protected
        procedure CMMouseEnter(var Msg: TMessage); override;
        procedure CMMouseLeave(var Msg: TMessage); override;
      public
        constructor Create(AOwner: TComponent); override;
      end;
    
    constructor TMyCustomScrollBox.Create(AOwner: TComponent);
    begin
      inherited;
      FButtonHoverCount := 0;
    end;
    
    procedure TMyCustomScrollBox.UMButtonMouseEnter(var Msg: TMessage);
    begin
      Inc(FButtonHoverCount);
      // 此处可处理ScrollBox的悬停状态,比如更新UI
    end;
    
    procedure TMyCustomScrollBox.UMButtonMouseLeave(var Msg: TMessage);
    begin
      Dec(FButtonHoverCount);
      if FButtonHoverCount <= 0 then
        CheckMouseInScrollBox; // 无按钮悬停时,检查鼠标位置
    end;
    
    procedure TMyCustomScrollBox.CheckMouseInScrollBox;
    var
      Pt: TPoint;
      ClientRect: TRect;
    begin
      GetCursorPos(Pt);
      Pt := ScreenToClient(Pt);
      ClientRect := Self.ClientRect;
      // 鼠标不在ScrollBox客户端区域时,触发自身CMMouseLeave
      if not PtInRect(ClientRect, Pt) then
        Perform(CMMouseLeave, 0, 0);
    end;
    

方案2:绑定子按钮的鼠标事件

如果TMyCustomButton暴露了OnMouseEnter和OnMouseLeave事件,可在TMyCustomScrollBox创建子按钮时直接绑定处理逻辑:

// TMyCustomScrollBox中添加子按钮的代码
var
  btn: TMyCustomButton;
begin
  btn := TMyCustomButton.Create(Self);
  btn.Parent := Self;
  btn.Align := alTop;
  btn.OnMouseEnter := ButtonMouseEnter;
  btn.OnMouseLeave := ButtonMouseLeave;
end;

// TMyCustomScrollBox的事件处理方法
procedure TMyCustomScrollBox.ButtonMouseEnter(Sender: TObject);
begin
  Inc(FButtonHoverCount);
  // 处理ScrollBox悬停状态
end;

procedure TMyCustomScrollBox.ButtonMouseLeave(Sender: TObject);
begin
  Dec(FButtonHoverCount);
  if FButtonHoverCount <= 0 then
    CheckMouseInScrollBox;
end;

关键注意事项

  • 维护FButtonHoverCount计数器:避免鼠标在多个子按钮间切换时,重复触发ScrollBox的状态变化;
  • 用GetCursorPos判断鼠标位置:鼠标离开按钮时,消息自带的坐标可能不是最新的,必须获取当前屏幕坐标再转换为客户端坐标;
  • 确保ScrollBox自身的鼠标事件正常工作:当鼠标直接进入ScrollBox空白区域时,也要正确标记悬停状态。

内容的提问来源于stack exchange,提问作者NevTon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 08:41:09