How to determine that the form is destroyed in the application?

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

ps: I want to avoid adding code to each form, which will have to send a message to the main form when it is about to destroy. most forms are also created and destroyed dynamically at runtime.

I was thinking about the possibilities of using global TApplicationEvents.

What is the best approach for this?

+7
delphi delphi-7
source share
7 answers

The restriction on changing code in existing forms or creating forms, as can be seen from other answers and comments, leaves hacks and intercepts. The local CBT hook, fi, will be a little work, but it probably works fine. Below is one of the simple hacking solutions.

Screen global object always saves a list of forms using a regular TList . TList has a virtual Notify procedure that is called every time an item is added / removed. The idea is to use a derivative TList that overrides this method and uses 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 Screen adds / removes forms to its list not only when creating / destroying forms, but also when they are activated, etc.

Then make Screen use this list. This requires "access to private fields" because the FForms list is private. You can read about this hack on the Hallvard Vassbotn blog . It also requires "enumerating an object class at runtime." You can read about this hack on the Hallvard Vassbotn 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; 

Please note that a notification will be triggered for each destruction of the form. It also includes forms created through MessageDlg , ShowMessage , etc.

+5
source share

Unlike David's answer , there is a suitable structure. It is built higher in the class hierarchy in TComponent . Sir Rufo is on the right track, but you do not need to force your forms to belong to this object.

You can write any number of classes that can take specialized actions when the form (or any other component, for that matter) is destroyed. For example.

 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 that you are creating the form, simply set up a notification as follows (assuming that you have granted yourself access to the appropriate 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 it only allows 1 observer, while FreeNotification allows any number of observers.

NOTE As with any useful technique, do not apply this technique to force. There may be a more appropriate method for your specific problem. For example. The idea of MenuManager could be better solved using the global Screen object to iterate OnPopup forms.


EDIT: Observer Pattern Explanation

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

The whole point of the observer pattern is that the observed object (sometimes called the publisher) does not have type-specific knowledge about the objects that observe it (sometimes called subscribers). Publishers know that they can call a common notification method for each registered subscriber (observer). This allows objects to be loosely connected to those who observe it. In fact, the publisher does not need to be watched at all. Obviously, the registration method must be called either from the subscribers themselves or from a third party, otherwise the goal of decoupling will be defeated.

Observers can be implemented in varying degrees of complexity. The simplest event or callback. The most difficult of these is the dispatcher, which manages registrations between independents and publishers and subscribers. A dispatcher can even implement thread switching so that publishers do not even suffer from the side effects of slow subscribers.

TComponent observer implementation has a constraint that both the publisher and the subscriber must inherit from TComponent . In principle, any component can register with another component to notify of its destruction.

Perhaps the most common use of this function 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 link to nil.

+7
source share

What you want is a framework for triggering an event when a form is destroyed. When a form is destroyed, its destructor starts. So, in order for the framework to trigger such an event, it would have to be implemented from the form destructor. If you look inside TCustomForm.Destroy , you will find that there is no such event.

From this we can conclude that there can be no widespread application event when the entire form is destroyed. This means that you have to implement the solution yourself. One obvious way to do this is to introduce a common base class for all your forms. Make sure that every form of your program is ultimately derived from this common base class. Then arrange for the base class to display an event that fires whenever an instance is destroyed.


It seems that there is an incorrect understanding of what I am saying above. Craig demonstrates how to sign up for a single form notification notice. The ability of this does not contradict what I say. I want to say that there is no mechanism that allows you to subscribe to receive notifications when any form is destroyed.

+6
source share

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


Since each form can have an owner (type TComponent ), and this owner receives a notification if a child component is destroyed, simply create a global form owner and pass it on as the owner of each created form that you want to receive a notification for destruction.

You need to override the TComponent.Notification method and do what you need (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 primary owner of the form is always Application , but there is no need to track this.

+6
source share

Personally, I would prefer David Heffernan's solution, since all my forms are always based on a template, and this would be the simplest and easiest to implement. But the requirements that come from you are ps: I want to avoid adding code to each form that will need to send a message to the main form when it about to destroy. also most of the forms are created and destroyed dynamicaly at run-time. ps: I want to avoid adding code to each form that will need to send a message to the main form when it about to destroy. also most of the forms are created and destroyed dynamicaly at run-time.
you could change Destroy to your own method.
I would take the last named destructor in the chain and set TObject.Destroy to TMyClass.Destroy. Place for implementation should be a project.
The correction code is taken from David Heffernan โ€™s answer on a regular Patch call in delphi and is included only to save the answer in its entirety, and credits for 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 // /questions/304473/patch-routine-call-in-delphi/1489286#1489286 // 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. 
+3
source share

According to Vladโ€™s request , this expanded to my initial answer , explaining how to register all forms belonging to Application without any changes to the design of each form. That is, forms created using TMyForm.Create(Application); , and also implies Application.CreateForm(TMyForm, MyForm); .

The original answer did not indicate any special registration methods for FreeNotification , since the parameters depend on how the forms are created. Since there were no restrictions on how forms are created in the answer to the question, the original answer is more suitable in the general case.

If we could make sure that Application belongs to a custom subclass of TApplication , the problem could be easily resolved by overriding TApplication.Notification; . This is not possible, therefore, in this special case, the fact is used that the ownership structure of the component notifies all components belonging to it when adding or removing another component. Thus, basically all we need is a component tracker, also owned by Application , and we can respond to 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; 

A WARNING

You can implement everything that you have selected in the OnComponentNotification event OnComponentNotification . This would include registering that the form is โ€œdestroyedโ€. However, such a simplified approach would be erroneous because TComponent.InsertComponent allows TComponent.InsertComponent to change the ownership of a component without destroying it.

Therefore, in order to accurately report the destruction, you would have to combine this using FreeNotification , as in the first

.

This is fairly easy to do by setting LComponentTracker.OnComponentNotification := FDestructionLogger.RegisterFreeNotification; where RegisterFreeNotification is done 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; 
+1
source share

A very simple approach can track the score of a form. When it falls, then the form is destroyed. Check 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 needs to be taken, you can go through Screen.CustomForms to determine which form has been destroyed.

0
source share

All Articles