Vlad
Vlad

Reputation: 1433

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

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?

Upvotes: 11

Views: 4992

Answers (7)

NGLN
NGLN

Reputation: 43649

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.

Upvotes: 0

Disillusioned
Disillusioned

Reputation: 14832

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;

Upvotes: 1

Sertac Akyuz
Sertac Akyuz

Reputation: 54802

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..

Upvotes: 6

bummi
bummi

Reputation: 27377

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://stackoverflow.com/a/8978266/1699210
//   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.

Upvotes: 3

David Heffernan
David Heffernan

Reputation: 612954

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.

Upvotes: 7

Disillusioned
Disillusioned

Reputation: 14832

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.

Upvotes: 7

Sir Rufo
Sir Rufo

Reputation: 19106

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.

Upvotes: 6

Related Questions