3

I use Delphi 7 and I have a TFrame (hosted by a TForm) with three panels that span over the whole surface, in a "upside down T" layout. The panels should be resizeable, so I could use 2 splitters, but I want to give a better user experience: I'd like to have a single "size grip" in the T junction. This "handle" should appear only when the user hovers the junction area.

So here is my question: what is the best way to have a control show on top of any other on mouse move? TFrame.OnMouseMove don't get called (obviously) because there are the panels inside and possibly any sort of other controls inside them. I also strongly want to keep all the code inside the frame.

I see 2 solutions:

  1. Install a local Mouse Hook and go with it. But there could be some performance issues (or not?)
  2. Handle TApplication.OnMessage inside the frame, but adding some other code in order to simulate a "chain" of event handlers. This is because other parts of the application could need to handle TApplication.OnMessage for their own purposes.

Any other idea?

Thank you

Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
yankee
  • 509
  • 2
  • 9
  • 18
  • I'm not sure if I get your problem right, but can't you just handle `OnMouseMove` for each inner panel you have ? It can be just one common event handler. – TLama Jan 23 '14 at 10:27
  • @TLama I got your point, but those panels will be full of other inner controls, some of which will be created or hosted at runtime and I don't know them in advance. – yankee Jan 23 '14 at 10:38
  • Ah, sure, of course. Who would need empty panels. Silly me :-) – TLama Jan 23 '14 at 11:54

2 Answers2

2

To make a mouse move event notifier for the whole frame, no matter which child control is hovered, you can write a handler for the WM_SETCURSOR message as I've learnt from TOndrej in this post. From such event handler you can then determine which control is hovered and bring it to front.

Please note, I have done quite commonly used mistake here. The GetMessagePos result must not be read this way. It's even explicitly mentioned in docs. I don't have Windows SDK to see the MAKEPOINTS macro, so I'll fix this later:

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;
Community
  • 1
  • 1
TLama
  • 75,147
  • 17
  • 214
  • 392
  • Brilliant! I never investigated WM_SETCURSOR. Before reading your answer I found a way to successfully implement solution 2) of my first question, but This is of course a better solution! Thank you. – yankee Jan 23 '14 at 13:50
  • You're welcome! Well, I didn't know about `WM_SETCURSOR` before TOndrej's answer. It's pretty well hidden with its name. – TLama Jan 23 '14 at 13:51
1

I'll post this self-answer just because it works and it could be useful in some cases, but I marked TLama's as the best answer.
This is the solution 2) of the question:

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;
yankee
  • 509
  • 2
  • 9
  • 18
  • This way you'll be stealing the `OnMessage` event handler from the only global `TApplication` instance. Consider what happens if you'll have more than one such frame. Each new instance of this frame will steal it from the previously created one (not talking about other controls which may want to get it). – TLama Jan 23 '14 at 17:53
  • That's why I saved any pre-existent handler in the constructor, called it at the end of my own handler and restored it in the destructor. this way I'm creating a chain of handlers. – yankee Jan 24 '14 at 11:12
  • I see, but only one handler can exist at a time, so you can't have more than one frame of this kind. If you were having more than one, only the latest created one will process those `WM_MOUSEMOVE` messages. – TLama Jan 24 '14 at 11:23
  • I don't get your point: Application.OnMessage hooks all messages in the whole application, so there's no such thing as "only one frame will process the message". Whatever object handles "directly" this event can see any message, thus passing them to the next handler in the chain (the object that previously was the official handler). So, yes, there is only one official handler, but it will call all other handlers, and the nature of this event makes it possible because it's application-wide. – yankee Jan 24 '14 at 14:08
  • I'm sorry. I've totally overlooked that you're dispatching the event to the original `OnMessage` handler. My mistake. – TLama Jan 24 '14 at 14:11