4

我们应用程序中的某个表单显示模型的图形视图。用户可以在大量其他内容中启动模型转换,这可能需要相当长的时间。这种转换有时会在没有任何用户交互的情况下进行,有时则需要频繁的用户输入。除非需要用户输入,否则应禁用 UI(仅显示进度对话框)。

可能的方法:

  1. 忽略这个问题,只需将转换代码放在一个过程中并调用它。不好,因为在转换需要一些时间但不需要用户输入的情况下,应用程序似乎挂起。
  2. 在代码中添加回调:这很突兀——你必须在转换代码中放置很多这样的调用——而且是不可预测的——你永远无法确定你找到了正确的位置。
  3. 用 Application.ProcessMessages 撒上代码:与回调相同的问题。此外,您还会遇到 ProcessMessages 的所有问题。
  4. 使用线程:这使我们摆脱了 2. 和 3 的“突兀和不可预测”部分。但是,由于用户输入需要“编组”,因此需要做很多工作 - 调用 Synchronize,将任何需要的参数放入量身定制的记录等。调试也是一场噩梦,而且容易出错。

//编辑:我们当前的解决方案是线程。然而,由于用户输入,a** 很痛苦。在很多例程中可能会有很多输入代码。这让我感觉线程不是正确的解决方案。

我要让自己难堪,并发布我生成的 GUI 和工作代码的邪恶组合的大纲:

type
  // Helper type to get the parameters into the Synchronize'd routine:
  PGetSomeUserInputInfo = ^TGetSomeUserInputInfo;
  TGetSomeUserInputInfo = record
    FMyModelForm: TMyModelForm;
    FModel: TMyModel;
    // lots of in- and output parameters
    FResult: Boolean;
  end;

{ TMyThread }

function TMyThread.GetSomeUserInput(AMyModelForm: TMyModelForm;
  AModel: TMyModel; (* the same parameters as in TGetSomeUserInputInfo *)): Boolean;
var
  GSUII: TGetSomeUserInputInfo;
begin
  GSUII.FMyModelForm := AMyModelForm;
  GSUII.FModel := AModel;
  // Set the input parameters in GSUII

  FpCallbackParams := @GSUII; // FpCallbackParams is a Pointer field in TMyThread
  Synchronize(DelegateGetSomeUserInput);
  // Read the output parameters from GSUII
  Result := GSUII.FResult;
end;

procedure TMyThread.DelegateGetSomeUserInput;
begin
  with PGetSomeUserInputInfo(FpCallbackParams)^ do
    FResult := FMyModelForm.DoGetSomeUserInput(FModel, (* the params go here *));
end;

{ TMyModelForm }

function TMyModelForm.DoGetSomeUserInput(Sender: TMyModel; (* and here *)): Boolean;
begin
  // Show the dialog
end;

function TMyModelForm.GetSomeUserInput(Sender: TMyModel; (* the params again *)): Boolean;
begin
  // The input can be necessary in different situations - some within a thread, some not.
  if Assigned(FMyThread) then
    Result := FMyThread.GetSomeUserInput(Self, Sender, (* the params *))
  else
    Result := DoGetSomeUserInput(Sender, (* the params *));
end;

你有什么意见吗?

4

10 回答 10

7

我认为只要您的长期转换需要用户交互,您就不会对得到的任何答案感到真正满意。因此,让我们回顾一下:为什么需要通过请求更多信息来中断转换?这些真的是您在开始转型之前无法预料的问题吗?当然,用户对中断也不太满意,对吧?他们不能只是开始转型,然后去喝杯咖啡;他们需要坐下来观看进度条,以防出现问题。啊。

也许转型遇到的问题是可以“保存”到最后的东西。转型是否需要立即知道答案,或者它是否可以完成其他所有事情,然后再做一些“修复”?

于 2009-01-12T19:46:40.003 回答
4

绝对选择线程选项(即使在您编辑之后,说您觉得它很复杂)。 在我看来, duffymo 建议的解决方案是非常糟糕的 UI 设计(尽管它没有明确地与外观有关,但它与用户与应用程序的交互方式有关)。这样做的程序很烦人,因为您不知道任务将花费多长时间,何时完成等。可以使这种方法变得更好的唯一方法是用生成日期/时间标记结果,但即使那么您需要用户记住他们何时开始该过程。

花点时间/精力,使应用程序对您的最终用户有用、信息丰富且不那么令人沮丧。

于 2009-01-12T18:51:50.863 回答
3

为了获得最佳解决方案,无论如何您都必须分析您的代码,并找到所有地方来检查用户是否想要取消长时间运行的操作。对于简单的过程和线程化的解决方案都是如此——您希望操作在十分之几秒后完成,以使您的程序看起来对用户有响应。

现在我首先要做的是使用以下方法创建一个接口(或抽象基类):

IModelTransformationGUIAdapter = interface
  function isCanceled: boolean;
  procedure setProgress(AStep: integer; AProgress, AProgressMax: integer);
  procedure getUserInput1(...);
  ....
end;

并将过程更改为具有此接口或类的参数:

procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);

现在您准备在后台线程或直接在主 GUI 线程中实现事物,一旦您添加了更新进度和检查取消请求的代码,转换代码本身就不需要更改。您只能以不同的方式实现接口。

我肯定会不使用工作线程,特别是如果您无论如何都想禁用 GUI。要利用多个处理器内核,您始终可以找到相对分离的转换过程部分,并在它们自己的工作线程中处理它们。这将为您提供比单个工作线程更好的吞吐量,并且使用AsyncCalls很容易完成。只要你有处理器内核就可以并行启动它们。

编辑:

IMO Rob Kennedy 的这个答案是迄今为止最有见地的,因为它不关注实施的细节,而是关注用户的最佳体验。这肯定是你的程序应该优化的地方。

如果真的无法在转换开始之前获取所有信息,或者稍后运行它并修补一些东西,那么您仍然有机会让计算机做更多的工作,以便用户获得更好的体验。我从您的各种评论中看到,转换过程有很多点,执行分支取决于用户输入。想到的一个例子是用户必须在两个备选方案(如水平或垂直方向)之间进行选择 - 您可以简单地使用 AsyncCalls 来启动这两个转换,并且用户选择他的备选方案的那一刻有可能结果已经计算出来,因此您可以简单地显示下一个输入对话框。这将更好地利用多核机器。也许是一个跟进的想法。

于 2009-01-12T19:31:22.483 回答
2

TThread 完美且易于使用。

开发和调试你的慢函数。

如果准备好了,将调用放入 tthread 执行方法。使用 onThreadTerminate 事件找出函数的结束。

对于用户反馈,请使用 syncronize!

于 2009-01-12T18:52:53.840 回答
2

我认为你的愚蠢是将转型视为一项单一任务。如果需要用户输入作为计算的一部分,并且要求的输入取决于到目前为止的计算,那么我会将单个任务重构为多个任务。

然后,您可以运行任务、请求用户输入、运行下一个任务、请求更多输入、运行下一个任务等。

如果您将流程建模为工作流,则应该清楚需要哪些任务、决策和用户输入。

我会在后台线程中运行每个任务以保持用户界面的交互性,但不会出现所有编组问题。

于 2009-01-13T08:40:24.700 回答
1

通过向队列发送消息并让侦听器进行处理来进行异步处理。控制器向用户发送一条 ACK 消息,上面写着“我们已收到您的处理请求。请稍后再回来查看结果。” 给用户一个邮箱或链接,以查看事情的进展情况。

于 2009-01-12T18:47:25.753 回答
1

虽然我不完全理解您要做什么,但我可以提供的是我对可能解决方案的看法。我的理解是,您有一系列 n 事情要做,并且在此过程中对一件事情的决定可能会导致一个或多个不同的事情被添加到“转换”中。如果是这种情况,那么我会尝试(尽可能地)将 GUI 和决策与需要完成的实际工作分开。当用户启动“转换”时,我会(还没有在线程中)循环遍历每个必要的决策,但不执行任何工作......只是询问完成工作所需的问题,然后将步骤与参数到列表中。

完成最后一个问题后,生成您的线程,将要运行的步骤列表与参数一起传递给它。这种方法的优点是您可以显示 n 个项目中的 1 个的进度条,让用户了解他们在喝完咖啡后回来可能需要多长时间。

于 2009-01-12T21:37:45.847 回答
1

我当然会选择线程。弄清楚线程如何与用户交互通常很困难,但对我来说效果很好的解决方案是不让线程与用户交互,而是让用户端 GUI 与线程交互。这解决了使用同步更新 GUI 的问题,并为用户提供更多响应活动。

因此,为此,我使用线程中的各种变量(由使用临界区的 Get/Set 例程访问)来包含状态信息。对于初学者,我有一个 GUI 的“已取消”属性来设置请求线程停止。然后是一个“状态”属性,指示线程是在等待、忙碌还是完成。您可能有一个“人类可读”状态来指示正在发生的事情,或者完成百分比。

要阅读所有这些信息,只需在表单上使用计时器并进行更新。我也倾向于有一个“statusChanged”属性,如果其他项目之一需要刷新,则设置该属性,这会阻止过多的阅读。

这在各种应用程序中对我来说效果很好,包括一个在带有进度条的列表框中显示多达 8 个线程的状态的应用程序。

于 2009-01-13T09:24:46.903 回答
1

如果您决定使用 Threads,我也发现它们在 Delphi 中实现的方式有些复杂,我会推荐 Primož Gabrijelčič 或 Gabr 的OmniThreadLibrary因为他在 Stack Overflow 上是众所周知的。

这是我所知道的最简单的使用线程库。Gabr 写了很棒的东西。

于 2009-01-13T21:45:43.070 回答
0

如果您可以将转换代码分成小块,那么您可以在处理器空闲时运行该代码。只需创建一个事件处理程序,将其连接到 Application.OnIdle 事件。只要您确保每个代码块都相当短(您希望应用程序无响应的时间量......比如说 1/2 秒。重要的是在最后将 done 标志设置为 false你的处理程序:

procedure TMyForm .IdleEventHandler(Sender: TObject;
  var Done: Boolean);
begin
  {Do a small bit of work here}
  Done := false;
end;

因此,例如,如果您有一个循环,而不是使用 for 循环,而是使用 while 循环,请确保循环变量的范围在表单级别。在设置 onIdle 事件之前将其设置为零,然后例如每次 onidle 命中执行 10 次循环,直到您到达循环的末尾。

Count := 0;
Application.OnIdle := IdleEventHandler;

...
...

procedure TMyForm .IdleEventHandler(Sender: TObject;
  var Done: Boolean);
var
  LocalCount : Integer;
begin
  LocalCount := 0;

  while (Count < MaxCount) and (Count < 10) do
  begin
    {Do a small bit of work here}
    Inc(Count);
    Inc(LocalCount);
  end;
  Done := false;
end;
于 2009-01-12T23:34:11.853 回答