How to redirect mouse events to another control?
Asked Answered
E

3

8

I have a situation where I have a TImage and on top of it a TPanel covering it partially and they share the same parent:

------------------
|  Image1        |
|  ------------  |
|  |  Panel1  |  |
|  ------------  |
|                |
------------------

Panel1 is receiving mouse down/move/up events and processing it (so does Image1), but in some situation I would like to "redirect" the mouse down message to Image1 as if to simulate that Image1 was clicked rather than Panel1.

Here is what I did:

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if (ssLeft in Shift) then
    Beep;
end;

procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; 
  X, Y: Integer);
begin
  //...
end;

procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  ShowMessage('boo!');
end;

procedure TForm1.Panel1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  P: TPoint;
begin
  if FRedirectToImage then begin
    ReleaseCapture; // do I need to send a WM_LBUTTONUP as well to the panel?        
    GetCursorPos(P);
    P := ScreenToClient(P);
    Image1.Perform(WM_LBUTTONDOWN, MK_LBUTTON, Longint(PointToSmallPoint(P)));
    Exit;
  end;

  // Normal handling
  if (ssLeft in Shift) then begin
    // ...
  end;
end;

It works as expected but I'm not sure It's the right way.
My question is, am I doing it right? is there a better or recommended way of doing it?


Update (1) : Handling WM_NCHITTEST as suggested is a valid answer and I thought about it also. even setting Panel1.Enabled to False will route the mouse messages to the underlying Image1 control.

But (!) consider this situation where I click the x location on the Panel and still need to route the message to Image1:

------------------
|  Image1        |
|          --------------
|          |  Panel1  x |
|          --------------
|                |
------------------

My method works, but WM_NCHITTEST is not applicable in the described scenario. I still didn't get an answer if my method is valid or not. (or maybe I should ask another question with the above scenario?)

Ezzell answered 13/12, 2012 at 20:26 Comment(6)
I'd say that your best bet is to do this at the message loop level. Implement an OnMessage handler for TApplication. Or perhaps do the same with a TApplicationEvents object. In there you can change the target window handle of the messages of interest.Paxton
@David, it's not visible on a first view from this question, but OP really wants to redirect messages. So, that's the way to go.Selfsame
@Selfsame I've no thirst to write an answer here. Please don't feel inhibited from doing so yourself!Paxton
@DavidHeffernan, "I've no thirst to write an answer here". why is that?Ezzell
@Ezzell I want to watch television right now. I've written enough answers today.Paxton
RE (update1): The original question was not clear on that you wanted mouse messages even when the mouse is out of image area, hence the answers you've got. Also, outside the image area the image shouldn't have been clicked, is it not? The answers are better provided they suit your needs. Disabling the panel sure is fine, provided you don't need an enabled panel. I haven't tested your code so I cannot comment on main functionality, but it doesn't look quite right - you're not even converting the point to image's coordinates. And you already have a point in (X, Y), no need for GetCursorPos.Bio
S
6

In case when the control from which you want to redirect mouse events will not be in its whole client area inside the control to which those events should be redirected (as you've shown in your question update), then the WM_NCHITTEST message might be send to another control. Then the only one way remains to use IMHO, redirect all mouse messages.

As @David mentioned in his comment, you can do this message redirection in a global way by writing an event handler for the OnMessage event for TApplication. Or use a TApplicationEvents object.

In the following example, you can define the range of messages, that will be redirected as well as specify the list of source and target controls for that redirection. For redirecting is used the OnMessage event of the TApplication object, but since your target is in this case TGraphicControl descendant, you can't only change the recipient of the incoming message, but you have to eat this message and perform the message on the target control through the Perform method by yourself.

Here is the code showing how to redirect all mouse messages from Panel1 to Image1. You can get the whole testing project from here if you want:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TMsgRange = record
    MsgFrom: UINT;
    MsgTo: UINT;
  end;
  TRedirect = record
    Source: HWND;
    Target: TControl;
  end;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Panel1: TPanel;
    Image1: TImage;
    procedure FormCreate(Sender: TObject);
    procedure Image1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Panel1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Image1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Panel1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
  private
    FRedirectList: array of TRedirect;
    FRedirectEnabled: Boolean;
    FRedirectMsgRange: TMsgRange;
    procedure ApplicationMessage(var AMessage: TMsg; var Handled: Boolean);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ApplicationMessage(var AMessage: TMsg; var Handled: Boolean);
var
  I: Integer;
begin
  if FRedirectEnabled and (AMessage.message >= FRedirectMsgRange.MsgFrom) and
    (AMessage.message <= FRedirectMsgRange.MsgTo) then
  begin
    for I := 0 to High(FRedirectList) do
      if (AMessage.hwnd = FRedirectList[I].Source) and
        Assigned(FRedirectList[I].Target) then
      begin
        Handled := True;
        FRedirectList[I].Target.Perform(AMessage.message,
          AMessage.wParam, AMessage.lParam);
        Break;
      end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FRedirectEnabled := True;
  FRedirectMsgRange.MsgFrom := WM_MOUSEFIRST;
  FRedirectMsgRange.MsgTo := WM_MOUSELAST;
  SetLength(FRedirectList, 1);
  FRedirectList[0].Source := Panel1.Handle;
  FRedirectList[0].Target := Image1;
  Application.OnMessage := ApplicationMessage;
end;

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Image1MouseDown')
end;

procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Image1MouseUp')
end;

procedure TForm1.Panel1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Panel1MouseDown')
end;

procedure TForm1.Panel1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Panel1MouseUp')
end;

end.
Selfsame answered 14/12, 2012 at 0:15 Comment(1)
Forgot to notice, due to the Break used in the source control search loop (when the control is found in the redirection list), only one redirection can be specified for a source control. All the others will be ignored. If you remove that Break, you'll be able to broadcast the message to more than one target control.Selfsame
S
8

Handle wm_NCHitTest messages sent to the panel and return htTransparent. The OS will send the mouse message to the next control down without any further processing required from your program. (From the OS perspective, the "next control down" is the parent control of both the panel and the image; the VCL takes care of routing the mouse message back to the image control, as it does with all TGraphicControl descendants, since they aren't real windowed controls.)

Something like this:

procedure TParentForm.PanelWindowProc(var Msg: TMessage);
begin
  FPrevPanelWindowProc(Msg);
  if (Msg.Msg = wm_NCHitTest) and FRedirectToImage then
    Msg.Result := htTransparent;
end;

Assign that method to the panel's WindowProc method. Store the previous value of the property in a field of the form.

var
  FPrevPanelWindowProc: TWndMethod;

FPrevPanelWindowProc := Panel.WindowProc;
Panel.WindowProc := Self.PanelWindowProc;
Sharilyn answered 13/12, 2012 at 22:14 Comment(2)
Nice! One correction: Msg.Message should be Msg.Msg. Perhaps that's changed between 2012 and 2024!Ashe
Of course you're right, @Gwideman. TMessage has a Msg field, and TMsg has a Message field.Sharilyn
S
6

In case when the control from which you want to redirect mouse events will not be in its whole client area inside the control to which those events should be redirected (as you've shown in your question update), then the WM_NCHITTEST message might be send to another control. Then the only one way remains to use IMHO, redirect all mouse messages.

As @David mentioned in his comment, you can do this message redirection in a global way by writing an event handler for the OnMessage event for TApplication. Or use a TApplicationEvents object.

In the following example, you can define the range of messages, that will be redirected as well as specify the list of source and target controls for that redirection. For redirecting is used the OnMessage event of the TApplication object, but since your target is in this case TGraphicControl descendant, you can't only change the recipient of the incoming message, but you have to eat this message and perform the message on the target control through the Perform method by yourself.

Here is the code showing how to redirect all mouse messages from Panel1 to Image1. You can get the whole testing project from here if you want:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TMsgRange = record
    MsgFrom: UINT;
    MsgTo: UINT;
  end;
  TRedirect = record
    Source: HWND;
    Target: TControl;
  end;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    Panel1: TPanel;
    Image1: TImage;
    procedure FormCreate(Sender: TObject);
    procedure Image1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Panel1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Image1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Panel1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
  private
    FRedirectList: array of TRedirect;
    FRedirectEnabled: Boolean;
    FRedirectMsgRange: TMsgRange;
    procedure ApplicationMessage(var AMessage: TMsg; var Handled: Boolean);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ApplicationMessage(var AMessage: TMsg; var Handled: Boolean);
var
  I: Integer;
begin
  if FRedirectEnabled and (AMessage.message >= FRedirectMsgRange.MsgFrom) and
    (AMessage.message <= FRedirectMsgRange.MsgTo) then
  begin
    for I := 0 to High(FRedirectList) do
      if (AMessage.hwnd = FRedirectList[I].Source) and
        Assigned(FRedirectList[I].Target) then
      begin
        Handled := True;
        FRedirectList[I].Target.Perform(AMessage.message,
          AMessage.wParam, AMessage.lParam);
        Break;
      end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FRedirectEnabled := True;
  FRedirectMsgRange.MsgFrom := WM_MOUSEFIRST;
  FRedirectMsgRange.MsgTo := WM_MOUSELAST;
  SetLength(FRedirectList, 1);
  FRedirectList[0].Source := Panel1.Handle;
  FRedirectList[0].Target := Image1;
  Application.OnMessage := ApplicationMessage;
end;

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Image1MouseDown')
end;

procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Image1MouseUp')
end;

procedure TForm1.Panel1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Panel1MouseDown')
end;

procedure TForm1.Panel1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  Memo1.Lines.Add('Panel1MouseUp')
end;

end.
Selfsame answered 14/12, 2012 at 0:15 Comment(1)
Forgot to notice, due to the Break used in the source control search loop (when the control is found in the redirection list), only one redirection can be specified for a source control. All the others will be ignored. If you remove that Break, you'll be able to broadcast the message to more than one target control.Selfsame
B
5

You can derive your panel class to handle WM_NCHITTEST messages to return HTTRANSPARENT for the region you want the control beneath the panel receive mouse messages. E.g.:

procedure TMyPanel.WMNCHitTest(var Message: TWMNCHitTest);
var
  Pt: TPoint;
begin
  Pt := ScreenToClient(SmallPointToPoint(Message.Pos));
  if (Pt.X < 80) and (Pt.Y < 60) then // devise your logic here...
    Message.Result := HTTRANSPARENT
  else
    inherited;
end;

Obviously this is just a test, you can publish a field in your component for it to resolve where that control resides etc..

Bio answered 13/12, 2012 at 22:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.