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

Vla*_*lad 8 delphi delphi-7

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

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

我在考虑使用全球TApplicationEvents.

对此最好的方法是什么?

Dis*_*ned 7

大卫的答案相反,有一个合适的框架.它建立在类层次结构的较高位置TComponent.Rufo爵士走在正确的轨道上,但您不需要强制您的表单归该对象所有.

欢迎您编写任意数量的类,当表单(或任何其他组件)被销毁时,这些类可以采取专门的操作.例如

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;
Run Code Online (Sandbox Code Playgroud)

现在,无论何时创建表单,只需按如下方式设置通知(假设您已自己访问上述对象的合适实例):

LForm := TMyForm.Create(Application);
LForm.FreeNotification(DestroyedFormLogger);
LForm.FreeNotification(MenuManager);
Run Code Online (Sandbox Code Playgroud)

这种方法比使用OnDestroy事件更好,因为它只允许1个观察者,而FreeNotification允许任意数量的观察者.

注意:与任何有用的技术一样,不要强迫技术问题.对于您的具体问题,可能有更合适的技术.例如,MenuManager通过使用全局Screen对象来迭代表单可以更好地解决这个想法OnPopup.


编辑:观察者模式的说明

TComponent通知机制是内置实施的观察者模式当一个组件被破坏了.FreeNotification(也许不是理想的名字)相当于registerObserverRemoveNotification等价的unregisterObserver.

观察者模式的重点在于被观察的主体(有时称为发布者)没有观察它的对象(有时称为订阅者)的类型特定知识.发布者只知道他们能够在每个注册用户(观察者)上调用通用通知方法.这允许对象与正在观看它的对象松散耦合.事实上,甚至根本不需要观察出版商.显然,注册方法需要从订户本身或从第三方调用 - 否则解耦目标就会失败.

观察者可以以不同程度的复杂性实施.最简单的是事件或回调.最复杂的是一个调度员,负责管理中间注册并独立于发布者和订阅者.调度程序甚至可以实现线程切换,以便发布者甚至不会受到慢速订阅者的性能副作用的影响.

TComponent观察者实现有一个限制,发布者和订阅者都必须继承TComponent.基本上任何组件都可以向另一个组件注册以通知其销毁.

也许Delphi中这个特性最常见的用途是:当组件A引用组件B时; 如果组件B被销毁,则通知组件A,以便它可以将其引用设置为nil.

  • PS:关于你的[Remy的评论](http://stackoverflow.com/questions/20445859/how-to-detect-that-a-form-is-being-destroyed-across-the-application/20451630#comment30553360_20446480)回答,`TScreen.OnActiveFormChange`是一个可用于为FreeNotification分配最少额外代码的选项.(虽然我个人更喜欢工厂方法中更明确的赋值.) (2认同)
  • 如果表单从未激活怎么办? (2认同)
  • 令人失望的是你没有回答我提出的观点.我不认为你在浪费你的时间. (2认同)

Dav*_*nan 6

你想要的是框架在表单被销毁时触发事件.当一个表单被销毁时,它的析构函数就会运行.因此,为了让框架触发这样的事件,需要在表单的析构函数中实现它.如果你看看里面TCustomForm.Destroy,你会发现没有这样的事件.

由此我们可以得出结论,只要表单被销毁,就不会触发应用程序范围的事件.这意味着您必须自己实施解决方案.实现这一目标的一个显而易见的方法是为所有表单引入一个公共基类.确保程序中的每个表单最终都来自此公共基类.然后安排基类表示每当实例被销毁时触发的事件.


似乎对我上面所说的内容有些误解.Craig演示了如何订阅单个表单的销毁通知.这样做的能力与我所说的并不矛盾.我的观点是,没有任何机制允许您在销毁任何表单时订阅接收通知.

  • @RemyLebeau如果表格已经使用OnDestroy怎么办? (5认同)
  • 那么`OnDestroy`事件呢?您可以使用`TScreen.OnActiveFormChange`事件为每个可用的表单分配一个`OnDestroy`处理程序. (2认同)

Sir*_*ufo 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.
Run Code Online (Sandbox Code Playgroud)

主表单所有者总是,Application但没有必要跟踪这个.


Ser*_*yuz 5

从其他答案和评论中可以看出,修改现有表单中的代码或创建表单的约束留下了黑客和钩子.一个本地CBT钩子,fi,将是一个小工作,但可能工作正常.下面是一个更简单的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;
Run Code Online (Sandbox Code Playgroud)

测试csDestroying是必需的,因为Screen不仅在创建/销毁表单时,而且在激活表单时,还将表单添加/删除到其列表中.

然后Screen使用此列表.这需要"访问私有字段"黑客,因为FForms列表是私有的.您可以在Hallvard Vassbotn的博客上阅读有关此黑客的信息.它还需要"在运行时更改对象的类" hack.您可以在Hallvard Vassbotn的博客上阅读有关此黑客的信息.

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;
Run Code Online (Sandbox Code Playgroud)

请注意,将为每个表单销毁调用通知.这也包括通过创建形式MessageDlg,ShowMessage等等.

  • +1 Sertac,我喜欢你的风格.TScreen,做我说的!! (2认同)
  • @Vlad - 调试器能够读取私有字段.在VCL表单应用程序中暂停调试器并将其添加到监视"整数(@ Screen.FForms) - 整数(屏幕)"中.这为D7应用程序提供了76.如果你从它中排除`SizeOf(Screen.FForms)`,你将拥有FForms占用之前的字段空间. (2认同)