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

Jerry Dodge
  • 25,720
  • 28
  • 139
  • 301
  • I'm not so sure but I think You may also consider code injection as it was done by some AOP framework. – menjaraz Jan 05 '12 at 17:00
  • 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". – Warren P Jan 05 '12 at 17:46
  • @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. – Jerry Dodge Jan 05 '12 at 17:53

5 Answers5

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;
David Heffernan
  • 572,264
  • 40
  • 974
  • 1,389
  • That looks promising, but can't say for sure yet until I get home and give it a run. – Jerry Dodge Jan 05 '12 at 17:54
  • 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 answer – Jerry Dodge Jan 06 '12 at 01:54
  • 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. – David Heffernan Jan 06 '12 at 07:22
  • FWIW you didn't ask about mouse enter/leave. You did ask about focus and for that you could listen for [`WM_ACTIVATE`](http://msdn.microsoft.com/en-us/library/windows/desktop/ms646274(v=vs.85).aspx). – David Heffernan Jan 06 '12 at 08:05
  • I never knew there was a way to catch any and all messages from the same handler. – Jerry Dodge Jan 06 '12 at 13:34
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;
Mike W
  • 1,278
  • 8
  • 10
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.

Mike W
  • 1,278
  • 8
  • 10
  • 1
    I agree with this but sometimes you are including 3rd party code in your project which can make this hard to achieve – David Heffernan Jan 05 '12 at 14:19
  • 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. – Jerry Dodge Jan 05 '12 at 14:51
  • @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. – Mike W Jan 05 '12 at 14:57
  • 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. – Jerry Dodge Jan 05 '12 at 14:59
  • Also, you can put an event handler on the form's events, whether you put the code inside or outside the form. – Jerry Dodge Jan 05 '12 at 15:00
  • 1
    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. – Mike W Jan 05 '12 at 15:10
  • 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. – Jerry Dodge Jan 05 '12 at 15:17
  • 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. – Jerry Dodge Jan 06 '12 at 00:30
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

APZ28
  • 987
  • 5
  • 4
  • 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 messages – Arioch 'The Oct 31 '19 at 11:06
  • 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`) https://stackoverflow.com/questions/27400973 – Arioch 'The Oct 31 '19 at 11:09
  • 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. – Arioch 'The Oct 31 '19 at 11:25
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.
menjaraz
  • 7,447
  • 4
  • 38
  • 79
  • 3
    This is fine until the victim form decides to modify its own event handlers and then its game over. – David Heffernan Jan 06 '12 at 20:59
  • @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). – menjaraz Jan 07 '12 at 05:41