How do I catch certain events of a form from outside the form?
Asked Answered
Q

5

7

I'm working on something which will require monitoring of many forms. From outside the form, and without putting any code inside the form, I need to somehow capture events from these forms, most likely in the form of windows messages. But how would you capture windows messages from outside the class it's related to?

My project has an object which wraps each form it is monitoring, and I presume this handling will go in this object. Essentially, when I create a form I want to monitor, I create a corresponding object which in turn gets added to a list of all created forms. Most importantly, when that form is closed, I have to know so I can remove this form's wrapper object from the list.

These events include:

  • Minimize
  • Maximize
  • Restore
  • Close
  • Focus in/out

What I DON'T want:

  • Any code inside any forms or form units for this handling
  • Inheriting the forms from any custom base form
  • Using the form's events such as OnClose because they will be used for other purposes

What I DO want:

  • Handling of windows messages for these events
  • Any tips on how to get windows messages from outside the class
  • Which windows messages I need to listen for

Question re-written with same information but different approach

Quenby answered 5/1, 2012 at 14:10 Comment(3)
I'm not so sure but I think You may also consider code injection as it was done by some AOP framework.Zahavi
You are aware that you can replace the form events with your own handler but keep around the old value, and then invoke the old handler, from your replacement handler, right? That's simpler than true "code injection" or true "hooking". This is very much like how "interrupt handlers" work in most operating systems. We call it "vector replacement".Cacophony
@WarrenP I do know this, and would probably do it if David hadn't mentioned a cleaner method. But this strategy (at least in my opinion) is probably 90-95% effective (I can foresee some issues that would mess this situation up). David's solution is 100% effective.Quenby
H
7

Here's a more complete example of the solution that David Provided:

private
  { Private declarations }
  SaveProc : TWndMethod;
  procedure CommonWindowProc(var Message: TMessage);

...

procedure TForm1.Button1Click(Sender: TObject);
var
  f : tForm2;
begin
  f := tForm2.Create(nil);
  SaveProc := f.WindowProc;
  f.WindowProc := CommonWindowProc;
  f.Show;
end;

procedure TForm1.CommonWindowProc(var Message: TMessage);
begin
  case Message.Msg of
    WM_SIZE : Memo1.Lines.Add('Resizing');
    WM_CLOSE : Memo1.Lines.Add('Closing');
    CM_MOUSEENTER : Memo1.Lines.Add('Mouse enter form');
    CM_MOUSELEAVE : Memo1.Lines.Add('Mouse leaving form');
    // all other messages will be available as needed
  end;
  SaveProc(Message); // Call the original handler for the other form
end;
Hyponitrite answered 6/1, 2012 at 0:58 Comment(0)
K
9

You need to listen for particular windows messages being delivered to the form. The easiest way to do this is to assign the WindowProc property of the form. Remember to keep a hold of the previous value of WindowProc and call it from your replacement.

In your wrapper object declare a field like this:

FOriginalWindowProc: TWndMethod;

Then in the wrapper's constructor do this:

FOriginalWindowProc := Form.WindowProc;
Form.WindowProc := NewWindowProc;

Finally, implement the replacement window procedure:

procedure TFormWrapper.NewWindowProc(var Message: TMessage);
begin
  //test for and respond to the messages of interest
  FOriginalWindowProc(Message);
end;
Kilowatt answered 5/1, 2012 at 14:18 Comment(5)
That looks promising, but can't say for sure yet until I get home and give it a run.Quenby
I was about to ask you how to handle the messages in NewWindowProc but Mike beat me to it - I'll have to accept his answer now :( still +1 for original answerQuenby
Oh I thought that was the easy bit so I just did the hard bit. I only tackled message interception since the entire focus of the question and comments was that part.Kilowatt
FWIW you didn't ask about mouse enter/leave. You did ask about focus and for that you could listen for WM_ACTIVATE.Kilowatt
I never knew there was a way to catch any and all messages from the same handler.Quenby
H
7

Here's a more complete example of the solution that David Provided:

private
  { Private declarations }
  SaveProc : TWndMethod;
  procedure CommonWindowProc(var Message: TMessage);

...

procedure TForm1.Button1Click(Sender: TObject);
var
  f : tForm2;
begin
  f := tForm2.Create(nil);
  SaveProc := f.WindowProc;
  f.WindowProc := CommonWindowProc;
  f.Show;
end;

procedure TForm1.CommonWindowProc(var Message: TMessage);
begin
  case Message.Msg of
    WM_SIZE : Memo1.Lines.Add('Resizing');
    WM_CLOSE : Memo1.Lines.Add('Closing');
    CM_MOUSEENTER : Memo1.Lines.Add('Mouse enter form');
    CM_MOUSELEAVE : Memo1.Lines.Add('Mouse leaving form');
    // all other messages will be available as needed
  end;
  SaveProc(Message); // Call the original handler for the other form
end;
Hyponitrite answered 6/1, 2012 at 0:58 Comment(0)
H
1

A better solution than trying to work outside of the form would be to make every form descend from a common base form that implements the functionality. The form event handlers are exactly the right place to add this code but you'd write it all in the ancestor form. Any descendant form could still use the form events and as long as they always call inherited somewhere in the event handler the ancestor code would still execute.

Hyponitrite answered 5/1, 2012 at 14:17 Comment(8)
I agree with this but sometimes you are including 3rd party code in your project which can make this hard to achieveKilowatt
This was my original plan, but the key reason this won't work is because - like mentioned in my question - I cannot put any code inside any form for this. Making a base form is no different than putting the code inside the form.Quenby
@Jerry - understood but you also indicated that you'd eventually add code to individual form event handlers which is also putting code inside the forms.Hyponitrite
This is a different story. That code has nothing to do with my question, and obviously every form will have some sort of code in it. My point was that since I'm using the form's event handlers for other purposes I need to find something else to catch these events.Quenby
Also, you can put an event handler on the form's events, whether you put the code inside or outside the form.Quenby
What I'm saying is that you can do both. The ancestor form can have code in OnClose/OnShow/OnResize, etc. and the descendant forms can have their own code for those handlers.Hyponitrite
I understand, but like I mention in my question, and like David also explains, sometimes you have forms which you cannot change, and have to capture these things from the outside. Well that's exactly my case, I have absolutely no ability to modify these forms, and especially not making it inherit from another base form.Quenby
Actually, the whole reason I'm asking this question is because I cannot handle this from inside the forms. Otherwise I would have never even asked.Quenby
U
1

Another option is create TApplicationEvents and assign a handler to OnMessage event. Once if it fired, use the FindControl function and Msg.hWnd to check if it is the tform type and do what ever you want without hookin

Uncommonly answered 7/1, 2012 at 19:55 Comment(3)
Notice however that TApplication.OnMessage event (and the TApplicationEvents is just event handler multiplier here) does NOT handle WM_CLOSE command that is used to hide/free the window due to user's actions. You can intercept Release of the form using TComponent.Notification mechanism, but for the caHide type forms it won't work! The WM_CLOSE is generated internally and is pumped into Window's message handler bypassing Application's main queue (SendMessage vs PostMessage). Practically feasible seems to use OnMessage to intercept precursor messagesAnthocyanin
namely WM_SYSCOMMAND with wParam=SC_CLOSE and WM_NCLBUTTONDOWN with wParam=HTCLOSE. But if future Windows versions would add more ways to close forms, that list would have to be extended!!! Also, one ideally has to account somehow for monitored form occasional handle changes (AKA RecreateWnd) stackoverflow.com/questions/27400973Anthocyanin
Oh, one more "niche case" is when closing is denied using OnClose or OnCloseQuery events, this will be a hard case too. Maybe just to a PostMessage or TThread.Queue a lazy after-all-done handler that would check the monitored form's .Showing - If that form as TObject would not be already destroyed at that time.Anthocyanin
Z
0

Using Windows Messages can really attain a fine granularity (Yes, its part of your requirements!) but in some user cases where relying just on the VCL Event Framework suffices, a similar solution can be suggested:

unit Host;

interface

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

type
  THostForm = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    FFormResize: TNotifyEvent;
    FFormActivate: TNotifyEvent;
    FFormDeactivate: TNotifyEvent;
    FFormDestroy: TNotifyEvent;

    procedure _FormResize(Sender: TObject);
    procedure _FormActivate(Sender: TObject);
    procedure _FormDeactivate(Sender: TObject);

    procedure InternalEventHandlerInit(const AForm:TForm);
  public
    procedure Log(const Msg:string);
    procedure Logln(const Msg:string);
  end;

var
  HostForm: THostForm;

implementation

{$R *.dfm}

procedure THostForm.Button1Click(Sender: TObject);
var
  frm: TForm;
begin
  frm := TForm.Create(nil);
  frm.Name := 'EmbeddedForm';
  frm.Caption := 'Embedded Form';
  //
  InternalEventHandlerInit(frm);
  //
  Logln('<'+frm.Caption+'> created.');
  //
  frm.Show;
end;


procedure THostForm.InternalEventHandlerInit(const AForm: TForm);
begin
  FFormResize := AForm.OnResize;
  AForm.OnResize := _FormResize;
  //
  FFormActivate :=  AForm.OnActivate;
  AForm.OnActivate := _FormActivate;
  //
  FFormDeactivate :=  AForm.OnDeactivate;
  AForm.OnDeactivate := _FormDeactivate;
end;

procedure THostForm.Log(const Msg: string);
begin
  Memo1.Lines.Add(Msg);
end;

procedure THostForm.Logln(const Msg: string);
begin
  Memo1.Lines.Add(Msg);
  Memo1.Lines.Add('');
end;

procedure THostForm._FormActivate(Sender: TObject);
begin
  Log('Before OnActivate <'+(Sender as TCustomForm).Caption+'>');
  //
  if Assigned(FFormActivate) then
    FFormActivate(Sender) // <<<
  else
    Log('No OnActivate Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
  //
  Logln('After OnActivate <'+(Sender as TCustomForm).Caption+'>');
end;

procedure THostForm._FormDeactivate(Sender: TObject);
begin
  Log('Before OnDeactivate <'+(Sender as TCustomForm).Caption+'>');
  //
  if Assigned(FFormDeactivate) then
    FFormDeactivate(Sender)
  else
    Log('No OnDeActivate Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
  //
  Logln('After OnDeactivate <'+(Sender as TCustomForm).Caption+'>');
end;

procedure THostForm._FormResize(Sender: TObject);
begin
  Log('Before OnResize <'+(Sender as TCustomForm).Caption+'>');
  //
  if Assigned(FFormResize) then
    FFormResize(Sender)
  else
    Log('No OnResize Event Handler attached in <'+(Sender as TCustomForm).Caption+'>');
  //
  Logln('After OnResize <'+(Sender as TCustomForm).Caption+'>');
end;

end.
Zahavi answered 6/1, 2012 at 12:10 Comment(2)
This is fine until the victim form decides to modify its own event handlers and then its game over.Kilowatt
@David Heffernan: You are right. Now I see they are not protected at all. I'm afraid the only way to go this way in this case is to do some code injection to the "victim" form (speculation).Zahavi

© 2022 - 2024 — McMap. All rights reserved.