【问题标题】:Delphi Push Notifications To Update FormsDelphi 推送通知以更新表单
【发布时间】:2023-03-28 08:15:01
【问题描述】:

我正在尝试找出将状态更改分配到构成应用程序的多个表单的最佳方法。

在我的场景中,我有许多由我的应用程序监控的硬件设备。作为示例,设备之一是GPS设备。来自这些设备的数据进入,然后信息存储在一个大状态对象中。例如,输入的 GPS 位置与应用程序可用的当前位置和历史位置一起存储。

该应用程序有许多不同的表单和窗口可供用户使用。例如,GPS 有一个显示头顶可见卫星的表格,还有一个显示信噪比的表格和另一个显示 GPS 轨迹随时间变化的表格。

我考虑过的一个解决方案是在我的状态对象上使用类似于观察者模式的东西。新表单订阅状态对象,然后在状态对象更改时接收推送通知(新的 GPS 位置强制推送到表单,然后表单重新绘制并重新更新自己的状态)。

IObserver = interface
    procedure Update;
end;

IObservable = interface
    procedure Subscribe(Observer : IObserver);
end;

TObserverForm = class(TForm, IObserver)
    // ....
    procedure Update;
    begin
        // State has changed, update
    end;
end;

TApplicationState = class(IObservable);
    private
        FObservers : TList<IObserver>;
        FPosition : TPoint;
    public
        procedure Subscribe(Observer : IObserver);
        begin
            FObservers.Add(Observer);
        end;

        procedure PushUpdate;
        begin
            foreach Observer in Observers
                Observer.Update;
        end;

        property Position : TPoint read GetPosition write SetPosition;

        procedure SetPosition(Pos : TPoint);
        begin
            FPosition : Pos;
            // Notify all observers
            Self.PushUpdate;
        end;

end;

以上是我正在考虑的粗略模型。这依赖于在应用程序生命周期期间创建的每个 TObserverForm 上调用 Subscribe 的状态(并在它们被销毁时取消订阅)。

我真的看不出这有什么缺点。此解决方案可以进行一些调整,以便 TObserverForms 仅更新并对某些类型的推送更新做出反应。

这里是否缺少一些基本的东西?这个常见问题是否有更合乎逻辑/更简单的解决方案?

【问题讨论】:

  • 观察者模式非常简单。如果它符合您的需求(听起来确实如此),请使用它。我认为这个问题是基于意见的,因为对于大多数问题有不止一种可能的方法。我投票决定关闭,但如果您在实施过程中偶然发现一些具体问题,请随时提出单独的问题。
  • @RenéHoffmann 感谢您的回复。它似乎确实符合我的需求,但我主要想知道其他人是如何复制这个的,以及是否有比这更好的方法。不过我明白你在说什么,谢谢。
  • FWIW - 我喜欢它。我可能会产生订阅特定数据类型而不是每种数据类型的想法。此外,我已经通过订阅完成了一段时间 - 如果观察者想要继续接收数据,则需要重新订阅。这避免了订阅者在关闭之前没有取消订阅的问题,只要状态正在运行,就不会发送数据(我的是 UDP 传输,所以无连接)。
  • 也许你的问题更适合programmers.stackexchange.com,在那里可以提出更抽象的问题。
  • @RenéHoffmann 在引用其他网站时,指出cross-posting is frowned upon 通常会有所帮助

标签: forms delphi observer-pattern


【解决方案1】:

我认为你应该首先确定你想要得到什么。

事件接收和传播应该是:

  • 单线程或多线程
  • 同步或异步
  • 跨平台或仅限 Windows/VCL

对于简单的单线程同步 VCL 应用程序,您可以只使用来自 http://www.Spring4D.org 库的多事件。所有感兴趣的表单只需声明事件处理程序并在 Tracker 对象中注册/注销它。

type iTrackerUpdated = iEvent<TNotifyEvent>;

type TMyGPSTracker = class.....
     public
       property OnUpdate: iTrackerUpdated read FOnUpdate;

       procedure AfterCreation; override;

       property Coords: TMyGPSCoords read FCoords write SetCoords;
     end;

var GPSTracker: TMyGPSTracker;

procedure TMyGPSTracker.AfterCreation;
begin
  inherited;
  FOnUpdate := TEvent<TNotifyEvent>.Create; // auto-freed on destruction
end;

procedure TMyGPSTracker.SetCoords(const NewValue: FCoords);
begin
  if NewValue = FCoords then exit;
  FCoords := NewValue;
  OnUpdate.Invoke(Self);
end;

然后在表格中类似的东西

type TMyForm = class(TForm)
.....

 private
    procedure CoordsUpdated(Sender: TObject);
 public
    procedure AfterCreation; override;
    procedure BeforeDestruction; override;
 end;

procedure TMyForm.CoordsUpdated(Sender: TObject);
begin
  Caption := (Sender as TMyGPSTracker).Coords.ToString();
end;

procedure TMyForm.AfterCreation;
begin
  inherited;
  GPSTracker.OnUpdate.Add( CoordsUpdated );
end;

procedure TMyForm.BeforeDestruction;
begin
  inherited;
  GPSTracker.OnUpdate.RemoveAll( Self );
end;

这就是这种情况所需要的一切。不过……

如果某些表单可能需要长时间处理更改 - 那么它将阻止其他表单更新和从 GPS 驱动程序获取的新坐标。

在这种情况下你最好

  1. 将 GPS 对象提取到一个单独的空闲优先级线程中,即使主 VCL 线程很忙,它也会继续更新数据。
  2. 切换到异步事件传播,对于 VCL,最简单的方法是使用 PostMessage API。那么您应该知道,繁忙的表单可能会在准备好处理它们之前“收集”少量警报 - 并且只需要处理其中一个。

http://docwiki.embarcadero.com/RADStudio/Berlin/en/Understanding_the_Message-Handling_System

const WM_GPS_UPDATE = WM_USER + 10;

type TMyGPSTracker = class.....
     public
       property OnUpdateAlertForms: TThreadList<TForm> read FOnUpdate;
       property UpdateCounter: Cardinal read FUpdateCounter;     

       property Coords: TMyGPSCoords read FCoords write SetCoords;
     end;

var GPSTracker: TMyGPSTracker;

procedure TMyGPSTracker.SetCoords(const NewValue: FCoords);
var Form: TForm;
begin
  if NewValue = FCoords then exit;

  Inc(FUpdateCounter);
  FCoords := NewValue;

  for Form in FOnUpdate do
      if Form.HandleAllocated and Form.Showing then
         PostMessage( Form.WindowHandle, WM_GPS_UPDATE,
                       FUpdateCounter, LPARAM(Pointer(Self)) );
  // in some rare cases sometimes this might post messages to nowhere
  // but the only consequence would be a single non-update
end;

然后

type TMyForm = class(TForm)
.....

 private
    procedure CoordsUpdated(var Message: TMessage); message WM_GPS_UPDATE;
    var LastCoordUpdateProcessed: Cardinal; 
 end;

procedure TMyForm.CoordsUpdated(var Message: TMessage);
begin
  if LastCoordUpdateProcessed >= UpdateCounter then exit;

  Caption := GPSTracker.Coords.ToString();
  LastCoordUpdateProcessed := GPSTracker.UpdateCounter;
end;

procedure TMyForm.FormShow(Sender: TObject);
begin
  GPSTracker.OnUpdateAlertForms.Add( Self );
end;

procedure TMyForm.FormHide(Sender: TObject);
begin
  GPSTracker.OnUpdateAlertForms.Remove( Self );
end;

对于非 VCL 跨平台目标,您必须找到类似的异步消息传递工具,并将 PostMessage 和 Windows GDI 句柄替换为相应的方法。

【讨论】:

  • 感谢您的全面回复。该应用程序是一个具有异步数据接收和事件传播的 VCL 应用程序 - 可以同时连接 5 个或更多硬件设备,每个设备异步发送数据。表单会很快更新,因此在更新期间不会有任何较长的处理周期。
  • 整体回复不错,但将句柄存储到任何 Windows 控件(包括窗体)是一个非常糟糕的主意,因为控件可能随时重新创建。在您的示例中,您可以简单地存储 TForm 引用,因为访问 Handle 属性总是返回正确的句柄。在必须存储句柄的情况下,您可以使用 TWinControl.RecreateWnd 事件 (docwiki.embarcadero.com/Libraries/Seattle/en/…) 在重新创建窗口时收到通知,以便您可以更新存储的句柄。
  • 对不起,错误信息,我花了太长时间来编辑。如果您需要在重新创建窗口时收到通知,您可以为 CM_RECREATEWND 实现消息处理程序。但尽可能避免存储窗口句柄。见stackoverflow.com/questions/3474227/…
  • @JonRobertson 是的,存储TForm 引用是可能的,尽管它可能会跳过悬空指针问题是注销被跳过(程序员错误或注销前表单对象的破坏序列中的一些异常) .在我的表单重新创建 hwnd 的实践中,这种情况非常罕见......但大多数情况下,在考虑 PostMessage 时,我是在“内部”windows API 并且没有考虑像 TForm 这样的高级类型 :-)
  • 修复了一个细微的错误 - 需要 TThreadList 而不是 TList 或者当 GPS 跟踪器处于 for 循环中时可能会 [取消] 注册
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 1970-01-01
  • 2023-03-29
  • 1970-01-01
  • 2021-05-18
  • 2018-03-01
相关资源
最近更新 更多