不知道如何解决这个冲突

时间:2015-02-07 00:29:44

标签: delphi user-interface

我的Delphi应用程序有2个活动(貌似)必须同时出现在UI线程中。在大多数方面,它是一个单线程应用程序。我在这里部分遇到问题,因为我使用过Application.ProcessMessages,我希望尽量减少这个因为它可能导致的问题。

我将首先描述成分然后问题。

该应用程序可以打开多个"文档"形式,但为简单起见,我只会提到一份文件表格。每个表格都可以与外部设备通信。

有些情况可能出现,程序构建为循环,直到设备的某种情况允许我们退出循环用户取消操作。设备接口没有线程,必须进行轮询。

问题在于:

当用户想要关闭文档时,我们可能处于其中一个循环中。这是一个问题,因为所有这些都在UI线程中...我没有一个好方法来打破文档表单中的循环,所以我可以关闭文档。

虽然没有(DeviceReady或CancelKeyPressed)   Application.ProcessMessages;

原始解决方案:

我的原始解决方案是在用户尝试关闭文档时执行以下操作:

  1. 将用户Windows消息发布到文档表单,告诉我们要关闭。
  2. 文档表单接收消息并中断任何活动循环并将消息发送回主表单,表示我们已准备好关闭。
  3. 主窗体循环,处理消息和民意调查以查看文档是否已准备好关闭。
  4. 当文档准备好关闭时,它将完成文档的关闭。
  5. 这大部分时间都有效,但它很复杂 - 特别是因为我们一次打开多个文档。

    其他可能的解决方案

    • 因为在我看来,所有这一切都必须发生在主线程中,将它移动到一个线程并不像解决方案。

    • 曾想过让文档异步关闭但我们需要知道文档关闭何时完成,所以我们可以让用户做其他事情,比如打开另一个文档。

    • 循环非常快; 1)发送"停止"消息2)等待___ ms并处理消息以使其工作3)关闭它。我认为这将是一种低质量的设计,会很弱。

    • 将此循环转换为状态机。 (传统)设备接口没有线程,必须进行轮询。我需要创建一个执行轮询的线程并将消息发布到表单以引发事件。然后,如果发生这样的事件或按下某个键,将发生将导致下一个状态发生的事件。这样,如果要关闭表单,它就不会成为问题。遗憾的是,还需要努力将周围的代码转换为参与状态机。 (这并不全是坏事。)

    结论

    Application.ProcessMessages看起来像一种药......开始使用它&很快你就需要更多地使用它了!

    任何人都有任何建议如何更好地处理这类问题?

    您的问题的答案:

    (我发布了这个问题是为了得到一些关于我应该采用哪种方式的意见;我和开发人员一起工作,这是让社区提供一些圣人建议的意义......)< / p>

    感谢您的评论!

      

    这些&#34;文件&#34;管理/引用?

    文档表单是对象列表拥有的标准delphi TForm后代。

      

    多线程规则是,如果你需要一个可能导致主线程不响应的连续循环,它应该是一个线程的形式。为什么你不允许把它放在别的地方?为什么必须在主线程中?

    问题是整个应用程序需要等到操作完成或被用户取消。我想我可以将所有传入的密钥传递给线程。当线程看到满足循环退出条件时,它可以建议UI线程。

      

    我喜欢状态机方法。

    我也是......它具有一定的优雅和简洁。它还可以简化其他一些相关系统。

      

    为什么你不能和&#34;设备&#34;来自线程?我会从UI中打破这一部分。

    可能这就是我应该做的。我需要的唯一电话是线程安全的。

      

    有很多时候人们可能会告诉你,为了提高性能,将内容转移到线程中是一个常见的错误。但这是你应该做的一个主要的例子。我无法想象任何严格要求主UI线程的界面。您是否正在使用嵌入到表单设计器中的组件?如果这是您的挫折,您是否尝试在不同的线程中动态创建这些组件?

    组件包含一个功能区,是的,所有都是从UI线程创建的。假设我无法在另一个线程中安全地创建组件并将它们放在用户将与之交互的表单上。当然,这实际上不会正常运作吗?

    我对线程有足够的经验,我实际上可以做到这一点,但我想更确定没有明显的东西我不知道。

2 个答案:

答案 0 :(得分:0)

这个想法非常简单:

为文档构建基类并将基本函数(在后台处理一些操作)放在那里

unit BaseDocument;

interface

uses
  System.Classes,
  System.SysUtils,
  System.Threading;

type
  TBaseDocument = class
  private
    FIsReady: Boolean;
    FIsReadyChanged: TNotifyEvent;
    procedure SetIsReady( const Value: Boolean );
    procedure SetIsReadyChanged( const Value: TNotifyEvent );
  protected
    procedure PerformBackgroundAction( AProc: TProc );
  public
    property IsReady: Boolean read FIsReady;
    property IsReadyChanged: TNotifyEvent read FIsReadyChanged write SetIsReadyChanged;
  end;

implementation

{ TBaseDocument }

procedure TBaseDocument.PerformBackgroundAction( AProc: TProc );
begin
  SetIsReady( False );
  TTask.Run( nil,
      procedure
    begin
      try
        AProc( );
      finally
        TThread.Synchronize( nil,
            procedure
          begin
            SetIsReady( True );
          end );
      end;
    end );
end;

procedure TBaseDocument.SetIsReady( const Value: Boolean );
begin
  if FIsReady <> Value
  then
    begin
      FIsReady := Value;
      if Assigned( FIsReadyChanged )
      then
        FIsReadyChanged( Self );
    end;
end;

procedure TBaseDocument.SetIsReadyChanged( const Value: TNotifyEvent );
begin
  FIsReadyChanged := Value;
end;

end.

正如您所看到的,我们有一个IsReady作为UI部分的指示符,如果文档已准备就绪,还有一个事件在IsReady正在改变其状态时触发。

现在我们需要一个基础文档表单来处理这个问题。这也很容易,因为我们只想让用户远离控件上的点击。我们只是在所有表单内容上添加了一些内容(此处为ActivityLayout : TLayoutTLayout.Hittest设置为True)。

ActivityLayout包含一个黑色矩形,其中不透明度设置为30%以使表单内容变暗,并包含TAiniIndicator以向用户提供一些信息,表明仍有一些操作正在进行中。

文档更改IsReady状态后,表单会获得通知并显示或隐藏ActivityLayout

unit Form.BaseDocument;

interface

uses
  BaseDocument,

  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
  FMX.Objects, FMX.Layouts;

type
  TBaseDocumentForm = class( TForm )
    ActivityLayout: TLayout; { container for ActivityIndicators }
    ActivityCurtain: TRectangle; { darken the form content }
    ActivityIndicator: TAniIndicator; { shows an animation while activity is in progress }
    procedure FormCloseQuery( Sender: TObject; var CanClose: Boolean );
  private
    FDocument: TBaseDocument;
    function CanFormClose: Boolean;
  protected
    procedure SetDocument( ADocument: TBaseDocument );
    procedure DoShowActivity( const AVisible: Boolean );
    procedure DocumentIsReadyChanged( Sender: TObject );
    procedure DoReloadDocument; virtual;
  public

  end;

var
  BaseDocumentForm: TBaseDocumentForm;

implementation

{$R *.fmx}
{ TBaseDocumentForm }

function TBaseDocumentForm.CanFormClose: Boolean;
begin
  Result := not Assigned( FDocument ) or Assigned( FDocument ) and FDocument.IsReady;
end;

procedure TBaseDocumentForm.DocumentIsReadyChanged( Sender: TObject );
begin
  DoShowActivity( not FDocument.IsReady );
  if FDocument.IsReady
  then
    DoReloadDocument;
end;

procedure TBaseDocumentForm.DoReloadDocument;
begin
  // override this to load the document after being ready
end;

procedure TBaseDocumentForm.FormCloseQuery( Sender: TObject; var CanClose: Boolean );
begin
  CanClose := CanFormClose;
end;

procedure TBaseDocumentForm.SetDocument( ADocument: TBaseDocument );
begin
  if FDocument <> ADocument
  then
    begin
      if Assigned( FDocument ) and ( FDocument.IsReadyChanged = DocumentIsReadyChanged )
      then
        FDocument.IsReadyChanged := nil;

      FDocument := ADocument;

      if Assigned( FDocument )
      then
        begin
          FDocument.IsReadyChanged := DocumentIsReadyChanged;
          DocumentIsReadyChanged( FDocument );
        end
      else
        DoShowActivity( False );
    end;
end;

procedure TBaseDocumentForm.DoShowActivity( const AVisible: Boolean );
begin
  ActivityLayout.Visible := AVisible;
  if AVisible
  then
    begin
      { just to ensure the right order }
      ActivityLayout.BringToFront;
      ActivityCurtain.BringToFront;
      ActivityIndicator.BringToFront;
    end;
end;

end.

这可以很容易地扩展到使用可中断的后台操作以及ActivityLayout上的取消按钮。


<强> PS

仅需要

线程安全,如果正在执行多个线程,则同时访问该线程。如果你能确保它只能在给定的时间跨度内被一个线程访问,那么就没有必要让它具有线程安全性。

答案 1 :(得分:-1)

你说你的循环很快但看起来它们还不够快。现在我认为你正试着让它们更快,所以我不会建议这个。

但你可能忘记的是,使用Break命令可以在循环中间打破循环。所以也许你应该检查循环周期中是否有可用的位置,你可以检查CancelKeyPressed的值,然后调用Break命令,你将过早地结束你的循环。

while not (DeviceReady or CancelKeyPressed) do 
begin
  //Do some work
  ...
  //Check if premature exit condition is set
  if CancelKeyPressed then
  begin
    //Do some clean up if needed
    ...
    Break;
  end;
  //Do some more work
  ...
  //Check again if premature exit condition is set
  if CancelKeyPressed then
  begin
    //Do some clean up if needed
    ...
    Break;
  end;
  Application.ProcessMessages;
end;

现在你应该在使用Break命令时特别注意,特别是如果你在循环中动态创建和释放一些对象。为什么?

将break命令放在错误的位置可能会阻止某些对象被正确释放,从而导致内存泄漏。

在这种情况下,您需要确保在释放所有这些对象之后调用Break命令之前。因此要么确保在已经存在的执行释放的代码之后放置break,要么添加仅在设置循环过早退出条件时才执行释放的aditional代码。

说到控制循环,您可能需要检查“继续”命令,该命令允许您打破当前循环周期并继续下一步。

现在这个命令并不常用,因为你可以通过简单地将各种循环代码块放在if stamtents中来实现类似的功能。因此,如果满足某些条件,那么代码就会被执行,否则它就不会,并且你可能已经处于循环周期的末尾。

Anywhay我推荐你检查过早结束循环的能力的主要原因是,即使你实现了多线程,你仍然需要等待你的循环结束。特别是如果在每个循环周期结束时,您可以借助Syncronize命令更新一些UI控件。

您不想破坏表单,然后让您的其他线程尝试更新现在不存在的UI组件吗?我不认为这会导致大量的访问冲突。

所以我建议首先检查一下是否有办法提前结束你的循环。然后我还建议您尝试将该代码移动到单独的线程中。

主要原因是你的Application.ProcessMessages的常见用法可能是降低循环性能的主要原因,因为循环不会继续直到Application.ProcessMessages完成其工作。