Delphi中如何在鼠标移动时显示叠加控件

3
我使用Delphi 7,有一个TFrame(托管在TForm上),其中包含三个跨越整个表面的面板,呈“倒T”形状。这些面板应该是可调整大小的,所以我可以使用2个分隔条,但我想提供更好的用户体验:我想在T型交汇处只有一个单独的“大小手柄”。当用户悬停在交汇区域时,此“手柄”应该出现。
那么我的问题是:如何在鼠标移动时使控件显示在其他任何控件之上?TFrame.OnMouseMove不会被调用(显然),因为里面有面板和可能是任何类型的其他控件。我还强烈希望将所有代码保留在框架内部。
我看到有两种解决方案:
  1. 安装本地鼠标钩子并使用它。但可能存在一些性能问题(或者没有?)
  2. 在框架内处理TApplication.OnMessage,但添加一些其他代码以模拟事件处理程序的“链”。这是因为应用程序的其他部分可能需要处理TApplication.OnMessage以满足其自身目的。
还有其他想法吗?
谢谢

我不确定我是否正确理解了你的问题,但是你不能为你拥有的每个内部面板处理 OnMouseMove 吗?它可以只是一个共同的事件处理程序。 - TLama
@TLama,我明白你的意思,但是这些面板将充满其他内部控件,其中一些将在运行时创建或托管,并且我事先不知道它们。 - yankee
啊,当然。谁需要空面板呢?我真傻 :-) - TLama
2个回答

2
为了创建一个鼠标移动事件通知器,使其适用于整个框架,无论哪个子控件被悬停,您可以编写一个处理程序来处理 WM_SETCURSOR 消息,就像我从 TOndrej 在 this post 中学到的一样。通过这种事件处理程序,您可以确定哪个控件正在被悬停并将其置于前台。
请注意,我在这里犯了一个相当常见的错误。不应该以这种方式读取 GetMessagePos 的结果。这甚至在文档中明确提到。我没有 Windows SDK 来查看 MAKEPOINTS 宏,所以稍后我会进行修复。
type
  TFrame1 = class(TFrame)
    // there are many controls here; just pretend :-)
  private
    procedure WMSetCursor(var Msg: TWMSetCursor); message WM_SETCURSOR;
  end;

implementation

procedure TFrame1.WMSetCursor(var Msg: TWMSetCursor);
var
  MsgPos: DWORD;
  Control: TWinControl;
begin
  inherited;
  MsgPos := GetMessagePos;
  Control := FindVCLWindow(Point(LoWord(MsgPos), HiWord(MsgPos)));
  if Assigned(Control) then
    Control.BringToFront;
end;

太棒了!我从未研究过WM_SETCURSOR。在阅读您的答案之前,我找到了一种成功实现我的第一个问题解决方案2)的方法,但这当然是更好的解决方案!谢谢。 - yankee
不客气!嗯,我在TOndrej的回答之前并不知道WM_SETCURSOR。它的名称相当隐蔽。 - TLama

1
我会提供这个自我回答,因为它可行并且在某些情况下可能会有用,但我把 TLama 的回答标为最佳答案。
这是问题的解决方案2):
TMyFrame = class(TFrame)
  // ...design time stuff...
private
  FMouseHovering: Boolean;
  FPreviousOnAppMessage: TMessageEvent;
  procedure DoOnAppMessage(var Msg: TMsg; var Handled: Boolean);
protected
  procedure CMMouseEnter(var Message: TMessage); message CM_MOUSEENTER;
  procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
end;


implementation

constructor TMyFrame.Create(AOwner: TComponent);
begin
  inherited;
  FMouseHovering := False;
  FPreviousOnAppMessage := Application.OnMessage;
  Application.OnMessage := DoOnAppMessage;
end;

destructor TMyFrame.Destroy;
begin
  Application.OnMessage := FPreviousOnAppMessage;
  inherited;
end;

procedure TRiascoFrame.CMMouseEnter(var Message: TMessage);
begin
  FMouseHovering := True;
end;

procedure TRiascoFrame.CMMouseLeave(var Message: TMessage);
begin
  FMouseHovering := False;
end;

procedure TMyFrame.DoOnAppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  if (Msg.message = WM_MOUSEMOVE) and FMouseHovering then
    DoHandleMouseMove(Msg.hwnd, Integer(LoWord(Msg.lParam)), Integer(HiWord(Msg.lParam)));
  if Assigned(FPreviousOnAppMessage) then
    FPreviousOnAppMessage(Msg, Handled);
end;

procedure TMyFrame.DoHandleMouseMove(hWnd: HWND; X, Y: Integer);
var
  ClientPoint: TPoint;
begin
  ClientPoint := Point(X, Y);
  Windows.ClientToScreen(hwnd, ClientPoint);
  Windows.ScreenToClient(Self.Handle, ClientPoint);
  if PtInRect(ClientRect, ClientPoint) then
  begin
    // ...do something...
  end;
end;

这样做会从唯一的全局TApplication实例中窃取OnMessage事件处理程序。考虑一下如果您有多个这样的帧会发生什么。每个此类帧的新实例都将从先前创建的实例中窃取它(不谈其他可能想要获取它的控件)。 - TLama
这就是为什么我在构造函数中保存了任何预先存在的处理程序,在我的处理程序末尾调用它,并在析构函数中恢复它。这样,我就创建了一个处理程序链。 - yankee
我明白了,但是一次只能存在一个处理程序,所以你不能有多个这种类型的框架。如果你有多个,只有最新创建的一个会处理那些 WM_MOUSEMOVE 消息。 - TLama
我不明白你的观点:Application.OnMessage钩子捕获整个应用程序中的所有消息,因此不存在“只有一个框架将处理该消息”的情况。任何直接处理此事件的对象都可以看到任何消息,因此将它们传递给链中的下一个处理程序(先前是官方处理程序的对象)。因此,确实只有一个官方处理程序,但它将调用所有其他处理程序,并且此事件的性质使其成为可能,因为它是应用程序范围内的。 - yankee
抱歉,我完全忽略了你正在将事件发送到原始的 OnMessage 处理程序。是我的错误。 - TLama

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接