How to detect that a form is being destroyed across the Application?
Asked Answered
S

7

11

We have many forms in our application, and I need a global event handler to detect when one of the forms is being destroyed (and then take some action).

p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.

I was thinking about maybe use a global TApplicationEvents.

What is the best approach for this?

Scope answered 7/12, 2013 at 19:53 Comment(13)
Define closed/destroyed. Define global event handler. Do you have a common base class for all your forms? If you don't, then you are about to regret not having done so.Dunaville
create one OnFormClosedEvent in your main form class and assign this handler when you create a new form? You can assign the same event to multiple forms...Oyster
@DavidHeffernan, No I don't have base class. I was thinking about maybe use TApplicationEventsScope
@whosrdaddy, Im trying to avoid adding code to the existing forms.Scope
@Scope So, which event are you going to use? And are you going to answer my questions? Or do we have to find out the hard way what you mean?Dunaville
@DavidHeffernan, I made an edit. hope now it's clear.Scope
Yes, that's better. Now, which event in TApplicationEvents did you have in mind?Dunaville
@Scope It might help if you were more specific about what action you want to take when a form is destroyed. Some actions might be better suited to certain solutions than others.Inelegancy
@CraigYoung I need to log the form destructors. But I don't want to go all over my framework and add extra code. bummi's answer looks very promissing so far. You got a +1 from me as well for a very good solution. But that will mean I need to change all my code where I call TMyForm.Create.... creating a common class seems like a solid solution, but I need a fast solution for now.Scope
@Vald Would I be correct in assuming you want this logging for debugging purposes? Do you also want to log destruction of short-lived modal dialogs? (I.e. created and destroyed within one method.) The question of whether you need to change all your code where you create forms actually depends on how you're creating them. E.g. If you use a factory method such as Application.CreateForm or your own to create all forms; or if you have a common owner for all your forms: Both of these would give you a suitable entry point to register for notification without changing all calls to TMyForm.Create.Inelegancy
@CraigYoung, I must admit I don't have a solid framework with this particular huge project. Only the main form is created with Application.CreateForm. most of the forms are modal, and created like: frm := TMyForm.Create(Application); ... frm.Free; but other forms are used as children inside other forms where the owner is not TApplication, but the hosting Form itself.Scope
... If I have had a common base class, I would not had any issue at all. I could however, use your excellent answer in other parts of our projects. How would I use your suggestion if all the form owners was TApplication without changing each frm := TMyForm.Create(Application); by hand?Scope
@Scope Certainly, I'll add a second answer for this special case. (Though perhaps it should rather be a separate question.)Inelegancy
M
6

A constraint on modifying code in existing forms, or creation of forms, as can be seen from other answers and comments, leaves hacks and hooks. A local CBT hook, f.i., would be a little work but probably work fine. Below is one of the simpler hacky solutions.

Screen global object holds a list of forms at all times via a regular TList. TList has a virtual Notify procedure which is called every time an item is added/removed. The idea is to employ a TList derivative that overrides this method and use it in the Screen object.

type
  TNotifyList = class(TList)
  protected
    procedure Notify(Ptr: Pointer; Action: TListNotification); override;
  end;

procedure TNotifyList.Notify(Ptr: Pointer; Action: TListNotification);
begin
  inherited;
  if (Action = lnDeleted) and (csDestroying in TForm(Ptr).ComponentState) and
      (TForm(Ptr) <> Application.MainForm) then
    // do not use ShowMessage or any 'TForm' based dialog here
    MessageBox(0,
        PChar(Format('%s [%s]', [TForm(Ptr).ClassName, TForm(Ptr).Name])), '', 0);
end;

Testing for csDestroying is required because the Screen adds/removes forms to its list not only when forms are created/destroyed but also when they are activated etc..

Then make the Screen use this list. This requires an "accessing private fields" hack, as the FForms list is private. You can read about this hack on Hallvard Vassbotn's blog. It also requires "changing the class of an object at run time" hack. You can read about this hack on Hallvard Vassbotn's blog.

type
  THackScreenFForms = class
{$IF CompilerVersion = 15}
    Filler: array [1..72] of Byte;
{$ELSE}
    {$MESSAGE ERROR 'verify/modify field position before compiling'}
{$IFEND}
    Forms: TList;
  end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  PPointer(THackScreenFForms(Screen).Forms)^ := TNotifyList;
end;

Note that the notification will be called for every form destruction. This also includes forms created through MessageDlg, ShowMessage etc..

Manicdepressive answered 10/12, 2013 at 1:7 Comment(8)
I ended up using your solution. but I believe David's answer deserves to be the accepted one because I strongly think it's the right way to go. I hope a made the right desicion by accepting the best answer rather than accepting the one I actually used. Thank you so much for this great answer!Scope
@Scope - You're welcome! I see your point, I don't think there's anything wrong with an answer you haven't chosen to implement being more helpful to you, in the long run for instance.Manicdepressive
BTW, How did calculate that Filler size is 72?Scope
@Scope - The debugger is able to read private fields. Pause the debugger in a VCL forms application and f.i. add this to a watch Integer(@Screen.FForms)-Integer(Screen). This gives 76 for a D7 application. If you exclude SizeOf(Screen.FForms) from it, you'll have the space that fields before FForms occupy.Manicdepressive
I love your method. I will make this the accepted answer after all. I saw this technique also in TNT Unicode controls, so I'm pretty convinced that this method is solid enough.Scope
It's hackier than it needs to be. Crack the private with a helper, and do runtime subclassing with a virtual method interceptor. But it's essentially the method that I would use.Dunaville
@David - Class helpers and virtual method interceptors are not available in D7. Still there may be better ways which I'm not aware of..Manicdepressive
Ah, forgot about that. In D7 I think what you did is the only way.Dunaville
D
7

What you are wanting is for the framework to fire an event when a form is destroyed. When a form is destroyed, its destructor is run. So, in order for the framework to fire such an event, it would need to be implemented from within the form's destructor. If you take a look inside TCustomForm.Destroy, you will find that there is not such event.

From this we can conclude that there can be no application wide event fired whenever a form is destroyed. This means that you will have to implement a solution yourself. One obvious way to make this happen is to introduce a common base class for all your forms. Ensure that every form in your program derives ultimately from this common base class. Then arrange for the base class to surface an event that is fired whenever an instance is destroyed.


There seems to be some mis-understanding about what I am saying above. Craig demonstrates how to subscribe to notification of a single form's destruction. The ability to do that does not contradict what I am saying. My point is that there is no mechanism in place that allows you to subscribe to receive notification when any form is destroyed.

Dunaville answered 7/12, 2013 at 20:50 Comment(1)
What about the OnDestroy event? You could use the TScreen.OnActiveFormChange event to assign an OnDestroy handler to every available Form.Intolerable
I
7

Contrary to David's answer, there is a suitable framework. It's built in higher up in the class hierarchy at TComponent. Sir Rufo is on the right track, but you don't need to force your forms to be owned by this object.

You're welcome to write any number of classes that can take specialised action when a form (or any other component for that matter) is destroyed. E.g.

TDestroyedFormLogger = class(TComponent)
protected
  { Write to log file when forms are destroyed. }
  procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;

TMenuManager = class(TComponent)
protected
  { Remove/hide a menu item corresponding to the form that has been destroyed. }
  procedure Notification(AComponent: TComponent; Operation: TOperation); override;
end;

Now whenever you create a form, simply set a notification as follows (assuming you have given yourself access to suitable instances of the above objects):

LForm := TMyForm.Create(Application);
LForm.FreeNotification(DestroyedFormLogger);
LForm.FreeNotification(MenuManager);

This approach is better than using the OnDestroy event because that permits only 1 observer, whereas FreeNotification permits any number of observers.

NOTE: As with any useful technique, don't force-fit a problem to the technique. There may be a more appropriate technique to your specific problem. E.g. The MenuManager idea might be better solved by using the global Screen object to iterate forms OnPopup.


EDIT: Explanation of Observer Pattern

The TComponent notification mechanism is a built-in implementation of the Observer Pattern for when a component is destroyed. FreeNotification (perhaps not ideally named) is the equivalent of registerObserver and RemoveNotification the equivalent of unregisterObserver.

The whole point of the observer pattern is that the subject being observed (sometimes called publisher) has no type-specific knowledge of the objects that are observing it (sometimes called subscribers). Publishers only know that they are able to call a generic notification method on each registered subscriber (observer). This allows objects to be loosely coupled from those that are watching it. In fact the publisher doesn't even need to be observed at all. Obviously the registration method needs to be called either from the subscribers themselves or from a third-party - otherwise the decoupling objective is defeated.

Observers can be implemented at varying degrees of complexity. The simplest being an event or callback. The most complex being a dispatcher that manages registrations in-between and independent of both publishers and subscribers. The dispatcher might even implement thread switching so that publishers don't even get impacted by performance side-effects of slow subscribers.

TComponent's observer implementation has a limitation that both the publisher and subscriber must inherit from TComponent. Basically any component can register with another component to be notified of its destruction.

Perhaps the most common use of this feature in Delphi is: When component A has a reference to component B; If component B is destroyed, component A is notified so that it can set its reference to nil.

Inelegancy answered 8/12, 2013 at 9:5 Comment(22)
That doesn't contradict me at all. The calls to FreeNotification are not built in. How are you going to make sure they are made for every form? The framework doesn't fire an event when every form is destroyed. You have to make that happen. Unless you add a common base class, or similar, your approach is going to involve duplication on an epic scale.Dunaville
@David You gave the impression that there is no built in functionality to achieve this - which is patently not true. Yes, there's no event (the most trivial form of observer) in TCustomForm's destructor, but there's a more flexible observer in TComponent's destructor. As for where to set up the notifications, that's up to @Vlad. He can control exactly which forms to configure. If he has appropriate factory classes for his dynamically created forms it will be easy. This solution has absolutely no need to change ownership or form hierarchies of existing forms.Inelegancy
There's no built in functionality that will exposes an event you can subscribe too. You have to change all the forms. Either the type as I suggested, or at every single constructor as you suggest. Which of those options would you choose? Subscribing to free notifications is better than Remy's OnDestroy because free notifications are multi-cast. I grant you that. But I really don't think you are contradicting me. The asker it looking for an application wide event. There is none.Dunaville
PS: Taking Remy's comment on your answer, TScreen.OnActiveFormChange is an option that could be used to assign the FreeNotification's with a minimum of extra code. (Though personally I would prefer the more explicit assignment in a factory method.)Inelegancy
What if the form was never activated?Dunaville
@David Your suggestion is to change the form's class, mine is to call a method on the form. (And doesn't have to be within the constructor.)Inelegancy
Yes I know that. I don't see what point you make. Which would you choose? You'd choose the option that forced you to remember to call a method every time you called a constructor?Dunaville
@David I prefer the option that doesn't bloat my forms with extra functionality that isn't needed for the (currently working) form to do its job. I don't know what "some action" Vlad wishes to "take". But introducing a new level to the class hierarchy simply binds more dependencies to the class which might be undesirable if you wish to reuse the form in a different application. Yes I prefer to explicitly code "I want my form to do optional X". Would you choose the option that forced you to remember to do something extra to disable optional functionality in cases you don't want it?Inelegancy
So you prefer the option that forces the functionality to be managed at each and every place where you instantiate the object? If each and every object has to do the exact same thing, doesn't that make that thing part of the type?Dunaville
@David For your answer, it might help to reread my previous comment. From the question it seems currently all forms work correctly. So the requirement to "take some action when one of the forms is being destroyed" belongs somewhere else (it's not a form requirement). Hence the responsibility also belongs elsewhere (perhaps the application, a form manager, ...). The forms would only need to change if they did not already implement a suitable notification mechanism. Responsibility is perhaps the most import OO design consideration: Wrong objects doing wrong things leads to tight coupling.Inelegancy
All solutions, including what you outline in your answer, are driven from the form's destructor.Dunaville
@David Yes. And your point is? You said the existing destructor didn't support the necessary functionality, I said it did.Inelegancy
It doesn't. Because you have to graft on the subscribing mechanism outside. Your idea is not that much better than adding something to every place where the form is destroyed. Now, free notifications would work just great with a common base class. The common base class could register a free notification and it would be all good.Dunaville
@David I think you're missing the point of a publish-subscribe model (observer pattern). The publisher (observable) provides an interface for subscribers (observers) to subscribe (register) to be notified of certain events. Publishers have zero knowledge about what is observing them. It simply sends out notifications whenever appropriate to whatever is looking. This decouples publisher and subscriber. If the publisher (form) also does the registration, then it becomes tightly coupled to the subscriber. Attempting to make registration generic just reinvents the FreeNotification wheel.Inelegancy
You think I don't understand the observer pattern? Really? I think I understand it. What the asker wants is to have a single observer listening to an event that occurs when any form is destroyed. That's not what you have. The key difference is that there should be a single act the subscribes to the event. The framework does not support that. That's what I said. I find it somewhat galling that your start your answer with "Contrary to David's answer" when you have contradicted nothing I said.Dunaville
@David Yes, really! And only because you said: "The common base class could register a free notification and it would be all good." If the publisher registers on behalf of the subscriber, then it already has a direct reference to the subscriber. So why bother registering at all? Also considering I did contradict you, I believe you're actually galled by the fact that I corrected you. Personally, I find it disappointing that a brilliant expert seems to consider himself incapable of error; and so becomes argumentative just to defend his position. I'm done wasting time here.Inelegancy
You don't seem to understand my point that it is inconvenient to have to subscribe every single time you instantiate an object. Yes, everytime you instantiate it. Every single time you call a constructor you need to add this subscription. How do you address that? You've not done so yet. That's my point. I'd like to hear your views on that. What the asker really wants is an event in TApplication. For that to work, code in TCustomForm.Destroy needs to trigger the event. It really needs framework support that is lacking.Dunaville
And only because you said: "The common base class could register a free notification and it would be all good." If the publisher registers on behalf of the subscriber, then it already has a direct reference to the subscriber. So why bother registering at all? Fair enough. So you would instead override the destructor and trigger the event there.Dunaville
Personally, I find it disappointing that a brilliant expert seems to consider himself incapable of error; and so becomes argumentative just to defend his position. I'm done wasting time here. I admit I am wrong plenty often. I'm sure you've seen me do that in the past. I just don't think I got this one wrong. Are you really denying my right to argue. If your argument is better, it will prevail. Now, I'd still like to see your explanation of why you prefer to force every instantiation of a form object in the program to subscribe individually. Can you justify that viewpoint?Dunaville
It's disappointing that you didn't answer the points I raised. I don't think you were wasting your time.Dunaville
@David It's disappointing that you didn't answer the points I raised. Please reread my answer and all my comments. I have answered the points you raised. (1) I've explained that depending on how the forms are constructed, the registration can be centralised. (I've recently added an answer demonstrating one case.) (2) I've answered your question about repeated registration - reasons included! Just because you're incredulous of my answer doesn't mean I didn't answer. So, for me to repeat my answers would be a waste of time!Inelegancy
I think it's unlikely that there will be a single call to construct forms. Even with factories there are likely to be many. Which is the weakness of what you suggest.Dunaville
L
6

This is not the best practice (have a look at David's answer) but a way to go.


Since every form can have an owner (type TComponent) and this owner gets notified, if a child component is destroyed, just create a global form owner and pass this as the owner of every created form you want to get notified on destroy.

You have to override the TComponent.Notification method and do what you have to (e.g. fire an event)

unit GlobalViewHolder;

interface

  uses
    Forms,
    Classes;

  type
    TComponentNotificationEvent = procedure( Sender : TObject; AComponent : TComponent; Operation : TOperation ) of object;

    TGlobalViewHolder = class( TComponent )
    private
      FOnNotification : TComponentNotificationEvent;
    protected
      procedure Notification( AComponent : TComponent; Operation : TOperation ); override;
    public
      property OnNotification : TComponentNotificationEvent read FOnNotification write FOnNotification;
    end;

  // small and simple singleton :o) 

  function ViewHolder : TGlobalViewHolder;

implementation

  var
    _ViewHolder : TGlobalViewHolder;

  function ViewHolder : TGlobalViewHolder;
    begin
      if not Assigned( _ViewHolder )
      then
        _ViewHolder := TGlobalViewHolder.Create( Application );

      Result := _ViewHolder;
    end;

  { TGlobalViewHolder }

  procedure TGlobalViewHolder.Notification( AComponent : TComponent; Operation : TOperation );
    begin
      inherited;
      if Assigned( OnNotification )
      then
        OnNotification( Self, AComponent, Operation );
    end;

end.

The main form owner is always Application but there is no need to track this.

Longueur answered 8/12, 2013 at 0:30 Comment(0)
M
6

A constraint on modifying code in existing forms, or creation of forms, as can be seen from other answers and comments, leaves hacks and hooks. A local CBT hook, f.i., would be a little work but probably work fine. Below is one of the simpler hacky solutions.

Screen global object holds a list of forms at all times via a regular TList. TList has a virtual Notify procedure which is called every time an item is added/removed. The idea is to employ a TList derivative that overrides this method and use it in the Screen object.

type
  TNotifyList = class(TList)
  protected
    procedure Notify(Ptr: Pointer; Action: TListNotification); override;
  end;

procedure TNotifyList.Notify(Ptr: Pointer; Action: TListNotification);
begin
  inherited;
  if (Action = lnDeleted) and (csDestroying in TForm(Ptr).ComponentState) and
      (TForm(Ptr) <> Application.MainForm) then
    // do not use ShowMessage or any 'TForm' based dialog here
    MessageBox(0,
        PChar(Format('%s [%s]', [TForm(Ptr).ClassName, TForm(Ptr).Name])), '', 0);
end;

Testing for csDestroying is required because the Screen adds/removes forms to its list not only when forms are created/destroyed but also when they are activated etc..

Then make the Screen use this list. This requires an "accessing private fields" hack, as the FForms list is private. You can read about this hack on Hallvard Vassbotn's blog. It also requires "changing the class of an object at run time" hack. You can read about this hack on Hallvard Vassbotn's blog.

type
  THackScreenFForms = class
{$IF CompilerVersion = 15}
    Filler: array [1..72] of Byte;
{$ELSE}
    {$MESSAGE ERROR 'verify/modify field position before compiling'}
{$IFEND}
    Forms: TList;
  end;


procedure TForm1.FormCreate(Sender: TObject);
begin
  PPointer(THackScreenFForms(Screen).Forms)^ := TNotifyList;
end;

Note that the notification will be called for every form destruction. This also includes forms created through MessageDlg, ShowMessage etc..

Manicdepressive answered 10/12, 2013 at 1:7 Comment(8)
I ended up using your solution. but I believe David's answer deserves to be the accepted one because I strongly think it's the right way to go. I hope a made the right desicion by accepting the best answer rather than accepting the one I actually used. Thank you so much for this great answer!Scope
@Scope - You're welcome! I see your point, I don't think there's anything wrong with an answer you haven't chosen to implement being more helpful to you, in the long run for instance.Manicdepressive
BTW, How did calculate that Filler size is 72?Scope
@Scope - The debugger is able to read private fields. Pause the debugger in a VCL forms application and f.i. add this to a watch Integer(@Screen.FForms)-Integer(Screen). This gives 76 for a D7 application. If you exclude SizeOf(Screen.FForms) from it, you'll have the space that fields before FForms occupy.Manicdepressive
I love your method. I will make this the accepted answer after all. I saw this technique also in TNT Unicode controls, so I'm pretty convinced that this method is solid enough.Scope
It's hackier than it needs to be. Crack the private with a helper, and do runtime subclassing with a virtual method interceptor. But it's essentially the method that I would use.Dunaville
@David - Class helpers and virtual method interceptors are not available in D7. Still there may be better ways which I'm not aware of..Manicdepressive
Ah, forgot about that. In D7 I think what you did is the only way.Dunaville
C
3

Personally I'd prefer David Heffernan's solution since all my forms are allways based on a template and it would be the cleanest and easiest to implement way.
But coming from you demand
p.s: I want to avoid adding code to each form that will need to send a message to the main form when it's about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
you would be able to patch Destroy to an own method.
I'd take the latest called destructor in the chain and patch TObject.Destroy to TMyClass.Destroy. The place to implement should be the project.
The code for patching is taken from David Heffernan 's answer on Patch routine call in delphi and only included to keep the answer complete, credits regarding this code go there.

program AInformOnCloseForms;

uses
  Forms,
  Classes,
  Windows,
  Dialogs,
  Unit3 in 'Unit3.pas' {Mainform},
  Unit4 in 'Unit4.pas' {Form2};

{$R *.res}

//   PatchCode and RedirectProcedure are taken from David Heffernans answer
//   https://mcmap.net/q/672047/-patch-routine-call-in-delphi
//   on "Patch routine call in delphi" , credits regarding this code go there
procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
  begin
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

type
  PInstruction = ^TInstruction;
  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;//jump relative
  NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

type

TMyClass=Class(TObject) // Dummy to handle "events"
  public
  Destructor Destroy;override;
End;


destructor TMyClass.Destroy;
begin
                                          // pervent recursion from call to Showmessage
 if (Self.InheritsFrom(TCustomForm)) and (Self.ClassName<>'TTaskMessageDialog') then
      Showmessage(Self.ClassName);
end;



begin
  RedirectProcedure(@TObject.Destroy,@TMyClass.Destroy);

  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TMainform, Mainform);
  Application.CreateForm(TForm2, Form2);
  Application.Run;

end.
Caerleon answered 8/12, 2013 at 14:51 Comment(6)
very nice indeed. but why did you declare TMyClass=Class(TObject) and not TMyClass=Class(TCustomForm)? (which works just fine)Scope
Also, how do you revers the patch? something like UnPatchCode?Scope
@Scope You would need to modify Patch to return the original memory before the call to Move. And then UnpatchCode would undo the original change and restore the original. Why would you need to do that though?Dunaville
@DavidHeffernan, I prefer to be on the safe side. In this answer: https://mcmap.net/q/672840/-how-to-change-the-implementation-detour-of-an-externally-declared-function it shows you need to UnHook the patch. I still don't understand why bummi declare TMyClass as TObject rather than TCustomForm. is that important?Scope
Sometimes you need to unhook, often you don't. I use quite a few hooks in my program to fix RTL/VCL problems and never unhook.Dunaville
@Vlad, responding to the second part of your question, try to add ReportMemoryLeaksOnShutDown := true; before RedirectProcedure if you are using an other ancestor than TObject. Since you are replacing the procedure you would have to reimplement anything done in the destructor if this class.Caerleon
I
1

As per Vlad's request this expands on my original answer by explaining how to register all forms owned by Application without any changes to the construction of each form. I.e. forms created using TMyForm.Create(Application); and by implication also Application.CreateForm(TMyForm, MyForm);.

The original answer doesn't specify any particular means of registering for FreeNotification because the options vary according to how forms are created. Since the question answered did not put any constraints on how the forms are created, the original answer is more appropriate in the general case.

If we could ensure that Application referred to a custom subclass of TApplication, the problem would be fairly easy to solve by overriding TApplication.Notification;. That's not possible, so this special case leverages the fact that the component ownership framework notifies all owned components when another component is added or removed. So basically all we need is a component tracker also owned by Application and we can react on its "sibling" notifications.

The following test case will demonstrate that new notifications work.

procedure TComponentTrackerTests.TestNewNotifications;
var
  LComponentTracker: TComponentTracker;
  LInitialFormCount: Integer;
  LForm: TObject;
begin
  LComponentTracker := TComponentTracker.Create(Application);
  try
    LComponentTracker.OnComponentNotification := CountOwnedForms;
    LInitialFormCount := FOwnedFormCount;
    LForm := TForm.Create(Application);
    CheckEquals(LInitialFormCount + 1, FOwnedFormCount, 'Form added');
    LForm.Free;

    CheckEquals(LInitialFormCount, FOwnedFormCount, 'Form removed');
  finally
    LComponentTracker.Free;
  end;
end;

procedure TComponentTrackerTests.CountOwnedForms(AComponent: TComponent; AOperation: TOperation);
begin
  if (AComponent is TCustomForm) then
  begin
    case AOperation of
      opInsert: Inc(FOwnedFormCount);
      opRemove: Dec(FOwnedFormCount);
    end;
  end;
end;

TComponentTracker is implemented as follows:

TComponentNotificationEvent = procedure (AComponent: TComponent; AOperation: TOperation) of object;

TComponentTracker = class(TComponent)
private
  FOnComponentNotification: TComponentNotificationEvent;
  procedure SetOnComponentNotification(const Value: TComponentNotificationEvent);
  procedure DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
protected
  procedure Notification(AComponent: TComponent; AOperation: TOperation); override;
public
  property OnComponentNotification: TComponentNotificationEvent read FOnComponentNotification write SetOnComponentNotification;
end;

procedure TComponentTracker.DoComponentNotification(AComponent: TComponent; AOperation: TOperation);
begin
  if Assigned(FOnComponentNotification) then
  begin
    FOnComponentNotification(AComponent, AOperation);
  end;
end;

procedure TComponentTracker.Notification(AComponent: TComponent; AOperation: TOperation);
begin
  inherited Notification(AComponent, AOperation);
  DoComponentNotification(AComponent, AOperation);
end;

procedure TComponentTracker.SetOnComponentNotification(const Value: TComponentNotificationEvent);
var
  LComponent: TComponent;
begin
  FOnComponentNotification := Value;
  if Assigned(Value) then
  begin
    { Report all currently owned components }
    for LComponent in Owner do
    begin
      DoComponentNotification(LComponent, opInsert);
    end;
  end;
end;

WARNING

You could implement anything you choose in the OnComponentNotification event handler. This would include logging that the form is "destroyed". However, such a simplistic approach would actually be flawed because TComponent.InsertComponent allows a component's owner to be changed without destroying it.

Therefore to accurately report destruction, you would have to combine this with using FreeNotification as in my first answer.

This is quite easily done by setting LComponentTracker.OnComponentNotification := FDestructionLogger.RegisterFreeNotification; where RegisterFreeNotification is implemented as follows:

procedure TDestructionLogger.RegisterFreeNotification(AComponent: TComponent; AOperation: TOperation);
begin
  if (AComponent is TCustomForm) then
  begin
    case AOperation of
      opInsert: AComponent.FreeNotification(Self);
    end;
  end;
end;
Inelegancy answered 11/12, 2013 at 22:3 Comment(1)
This is a bit complicated for me, but never the less, I have much to learn from your answers! :)Scope
A
0

A very simple approach could be keeping track of the Form count. When it lowers, then there is a Form destroyed. Check in Application.OnIdle:

procedure TMainForm.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
  if Screen.CustomFormCount < FFormCount then
    FormDestroyed;
  if FFormCount <> Screen.CustomFormCount then
    FFormCount := Screen.CustomFormCount;
end;

Depending on what action should be taken, you can loop through Screen.CustomForms to determine which Form was destroyed.

Ainslee answered 13/12, 2013 at 12:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.