如何检测整个应用程序中的表单是否被销毁?

时间:2013-12-07 19:53:53

标签: delphi delphi-7

我们的应用程序中有很多表单,我需要一个全局事件处理程序来检测其中一个表单何时被销毁(然后采取一些操作)。

p.s:我想避免在每个表单中添加代码,这些表单需要在主表单即将销毁时向主表单发送消息。此外,大多数表单都是在运行时动态创建和销毁的。

我在考虑使用全球TApplicationEvents。

最佳方法是什么?

7 个答案:

答案 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(可能没有理想的名称)相当于registerObserverRemoveNotification相当于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;

请注意,将调用每个表单销毁的通知。这还包括通过MessageDlgShowMessage等创建的表单。

答案 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以确定哪个表单已被销毁。