我们的应用程序中有很多表单,我需要一个全局事件处理程序来检测其中一个表单何时被销毁(然后采取一些操作)。
p.s:我想避免在每个表单中添加代码,这些表单需要在主表单即将销毁时向主表单发送消息。此外,大多数表单都是在运行时动态创建和销毁的。
我在考虑使用全球TApplicationEvents。
最佳方法是什么?
答案 0 :(得分:7)
与David's answer相反,有一个合适的框架。它位于TComponent
的类层次结构中的较高位置。 Sir Rufo位于正确的轨道上,但您无需强制您的表单归该对象所有。
欢迎您编写任意数量的类,当表单(或任何其他组件)被销毁时,这些类可以采取专门的操作。 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;
现在无论何时创建表单,只需按如下方式设置通知(假设您已经自己访问了上述对象的合适实例):
LForm := TMyForm.Create(Application);
LForm.FreeNotification(DestroyedFormLogger);
LForm.FreeNotification(MenuManager);
这种方法比使用OnDestroy
事件更好,因为它只允许1个观察者,而FreeNotification
允许任意数量的观察者。
注意:与任何有用的技术一样,不要强迫技术问题。对于您的具体问题,可能有更合适的技术。例如。通过使用全局MenuManager
对象迭代表单Screen
,可以更好地解决OnPopup
想法。
TComponent
通知机制是组件被销毁时Observer Pattern的内置实现。 FreeNotification
(可能没有理想的名称)相当于registerObserver
和RemoveNotification
相当于unregisterObserver
。
观察者模式的重点在于观察对象(有时称为发布者)没有 类型特定 对观察对象的知识(有时候)叫订户)。发布者只知道他们能够在每个注册用户(观察者)上调用通用通知方法。这允许对象与正在观看它的对象松散耦合。 事实上,甚至根本不需要观察出版商。显然,注册方法需要从订户本身或从第三方调用 - 否则解耦目标就会失败。
观察者可以以不同程度的复杂性实施。最简单的是事件或回调。最复杂的是一个调度员,负责管理中间注册并独立于发布者和订阅者。调度程序甚至可以实现线程切换,以便发布者甚至不会受到慢速订阅者的性能副作用的影响。
TComponent
的观察者实现有一个限制,即发布者和订阅者都必须从TComponent
继承。基本上任何组件都可以向另一个组件注册,以通知其被销毁。
Delphi中这个特性最常见的用法可能是:当组件A引用组件B时;如果组件B被销毁,则通知组件A,以便它可以将其引用设置为nil。
答案 1 :(得分:6)
您想要的是框架在销毁表单时触发事件。当一个表单被销毁时,它的析构函数就会运行。因此,为了使框架触发此类事件,需要在表单的析构函数中实现。如果您查看TCustomForm.Destroy
内部,您会发现没有此类事件。
由此我们可以得出结论,每当表单被销毁时都不会触发应用程序范围的事件。这意味着您必须自己实施解决方案。实现这一目标的一个显而易见的方法是为所有表单引入一个公共基类。确保程序中的每个表单最终都来自此公共基类。然后安排基类来表示每当实例被销毁时触发的事件。
似乎对我上面所说的内容有些误解。 Craig演示了如何订阅单个表单的销毁通知。这样做的能力与我所说的并不矛盾。我的观点是,当任何表单被销毁时,没有适当的机制允许您订阅接收通知。
答案 2 :(得分:6)
这不是最好的做法(看看大卫的答案),但这是一种方法。
由于每个表单都可以拥有所有者(类型TComponent
)并且此所有者会收到通知,如果子组件被销毁,只需创建一个全局表单所有者并将其作为您想要的每个创建表单的所有者传递得到破坏通知。
您必须覆盖TComponent.Notification方法并执行必要的操作(例如,举办活动)
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.
主表单所有者始终为Application
,但无需跟踪此内容。
答案 3 :(得分:5)
从其他答案和评论中可以看出,修改现有表单中的代码或创建表单的约束留下了黑客和钩子。一个本地CBT钩子,f.i.,将是一个小工作,但可能工作正常。下面是一个更简单的hacky解决方案。
Screen
全局对象始终通过常规TList
保存表单列表。 TList
具有虚拟Notify
过程,每次添加/删除项目时都会调用该过程。我们的想法是使用TList
衍生物来覆盖此方法并在Screen
对象中使用它。
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;
需要对csDestroying
进行测试,因为Screen
不仅在创建/销毁表单时添加/删除表单,而且还在激活等表单时添加/删除表单。
然后让Screen
使用此列表。这需要“访问私有字段” hack,因为FForms
列表是私有的。你可以在Hallvard Vassbotn的blog上读到这个黑客。它还需要“在运行时更改对象的类” hack。你可以在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;
请注意,将调用每个表单销毁的通知。这还包括通过MessageDlg
,ShowMessage
等创建的表单。
答案 4 :(得分:3)
就我个人而言,我更喜欢David Heffernan的解决方案,因为我的所有表单都是基于模板的,并且它将是最干净,最容易实现的方式。
但是来自你的要求
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.
你可以将Destroy修补为自己的方法
我将链中的最新调用析构函数和TObject.Destroy修补为TMyClass.Destroy。实施的地方应该是项目
修补代码取自David Heffernan 's answer on Patch routine call in delphi,仅包含以保持答案完整,有关此代码的信用额。
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.
答案 5 :(得分:1)
根据Vlad's request,通过解释如何注册Application
所拥有的所有表单而不对每个表单的构造进行任何更改来扩展my original answer。即使用TMyForm.Create(Application);
创建的表单以及Application.CreateForm(TMyForm, MyForm);
的含义。
原始答案没有指定注册FreeNotification
的任何特定方法,因为选项因创建表单的方式而异。由于回答的问题没有对表格的创建方式施加任何限制,因此原始答案在一般情况下更合适。
如果我们可以确保Application
引用TApplication
的自定义子类,那么通过覆盖TApplication.Notification;
可以很容易地解决问题。这是不可能的,因此这种特殊情况利用了组件所有权框架在添加或删除另一个组件时通知所有自有组件的事实。基本上我们所需要的只是一个由Application
拥有的组件跟踪器,我们可以对其“兄弟”通知作出反应。
以下测试用例将证明新通知有效。
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
的实施方式如下:
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;
您可以在OnComponentNotification
事件处理程序中实现您选择的任何内容。这将包括记录表单“销毁”。但是,这种简单的方法实际上会有缺陷,因为TComponent.InsertComponent
允许更改组件的所有者而不会破坏它。
因此,要准确报告销毁情况,您必须将其与使用FreeNotification
中的LComponentTracker.OnComponentNotification := FDestructionLogger.RegisterFreeNotification;
相结合。
通过设置RegisterFreeNotification
实现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}}
答案 6 :(得分:0)
一种非常简单的方法可能是跟踪表格计数。当它降低时,就会有一个Form被破坏。签入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;
根据应采取的操作,您可以遍历Screen.CustomForms
以确定哪个表单已被销毁。