Reputation: 1433
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
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
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;
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
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
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
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
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
.
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
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