Delphi 10.3 WinControl消息路由与自定义控件鼠标状态感知问题
问题描述
组件层级结构:
TForm |-- TMyCustomScrollBox |-- TMyCustomButton |-- TMyCustomButton |-- TMyCustomButton ...
其中TMyCustomButton设置为alTop对齐。
当前已做处理:
- 在
TMyCustomButton中捕获了CMMouseEnter、CMMouseLeave和WMMouseMove事件; - 在
TMyCustomScrollBox中同样捕获了上述三类事件。
需求:
- 让
TMyCustomScrollBox能够知晓鼠标正处于内部的TMyCustomButton上; - 当鼠标离开
TMyCustomButton后,能判断鼠标是否仍在TMyCustomScrollBox范围内; - 鼠标完全离开
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的鼠标事件中,主动告知父控件自身的鼠标状态变化:
- 先在公共单元定义自定义消息:
const UM_BUTTON_MOUSE_ENTER = WM_USER + 100; // 自定义按钮鼠标进入消息 UM_BUTTON_MOUSE_LEAVE = WM_USER + 101; // 自定义按钮鼠标离开消息 - 修改
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; - 在
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
相关产品推荐
相关产品推荐

