我们的应用程序中的某个表单显示模型的图形视图。用户可以在其他东西的负载中启动模型的转换,这可能需要相当长的时间。有时在没有任何用户交互的情况下进行该转换,在其他时候需要频繁的用户输入。除非需要用户输入,否则应该禁用UI(仅显示进度对话框)。
可能的方法:
//编辑:我们当前的解决方案是一个线程。然而,由于用户输入,这是一个痛苦的**。在很多例程中可能会有很多输入代码。这让我感觉线程不正确的解决方案。
我将自己进行骚扰并发布我生成的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;
你有什么意见吗?
答案 0 :(得分:7)
我认为只要您长时间运行的转换需要用户互动,您就不会对任何答案感到满意。所以让我们回顾一下:为什么你需要通过请求获取更多信息来中断转换?在开始转型之前,这些问题真的是你无法预料到的吗?当然,用户对中断也不太满意,对吧?他们不仅可以设定转型,还可以获得一杯咖啡;如果出现问题,他们需要坐下来观看进度条。啊。
也许转型遇到的问题是可以“保存”到最后的事情。变换是否需要立即知道答案,还是可以完成其他所有事情,然后再做一些“修复”?
答案 1 :(得分:4)
在编辑后肯定选择一个线程选项( 甚至 ,说你发现它很复杂)。在我看来,The solution that duffymo suggests是非常差的UI设计(即使它没有明确地说明外观,它是关于用户如何与您的应用程序连接)。执行此操作的程序很烦人,因为您不知道任务需要多长时间,何时完成等等。这种方法可以做得更好的唯一方法是使用生成日期/时间标记结果,但即使是那么你需要用户记住他们何时开始这个过程。
花费时间/精力,使应用程序变得有用,信息丰富,让您的最终用户不那么沮丧。
答案 2 :(得分:3)
对于最佳解决方案,您无论如何都必须分析代码,并找到所有位置以检查用户是否要取消长时间运行的操作。这对于简单的过程和线程解决方案都是如此 - 您希望在几分之几秒后完成操作,以使您的程序看起来对用户有响应。
现在我要做的是用以下方法创建一个接口(或抽象基类):
IModelTransformationGUIAdapter = interface
function isCanceled: boolean;
procedure setProgress(AStep: integer; AProgress, AProgressMax: integer);
procedure getUserInput1(...);
....
end;
并将过程更改为具有此接口或类的参数:
procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);
现在您已准备好在后台线程中或直接在主GUI线程中实现内容,一旦添加了代码以更新进度并检查取消请求,就不需要更改转换代码本身。您只能以不同的方式实现接口。
我肯定会没有工作线程,特别是如果你想要禁用GUI。要使用多个处理器核心,您始终可以找到相对分离的转换过程的一部分,并在它们自己的工作线程中处理它们。这将为您提供比单个工作线程更好的吞吐量,并且使用AsyncCalls很容易实现。只要你有多个处理器内核就可以并行启动。
修改强>
Rob Kennedy的IMO this answer是最具洞察力的,因为它不关注实施细节,而是关注用户的最佳体验。这肯定是你的程序应该优化的东西。
如果确实无法在转换开始之前获取所有信息,或者运行它并稍后修补一些内容,那么您仍然有机会让计算机做更多工作,以便用户拥有更好的体验。我从各种评论中看到,转换过程有很多要点,执行根据用户输入进行分支。想到的一个例子是用户必须在两个选择之间进行选择(如水平或垂直方向) - 你可以简单地使用AsyncCalls来启动两个转换,并且有可能在用户选择他的替代方案时结果已经计算过,因此您只需显示下一个输入对话框即可。这将更好地利用多核机器。也许是一个跟进的想法。
答案 3 :(得分:2)
TThread非常完美且易于使用。
开发和调试慢速函数。
如果准备就绪,将调用放入tthread执行方法。 使用onThreadTerminate事件找出你的功能结束。
用户反馈使用syncronize!
答案 4 :(得分:2)
我认为你的愚蠢是将转变视为一项单一任务。如果计算中需要用户输入,并且要求的输入取决于到那时的计算,那么我会将单个任务重构为许多任务。
然后,您可以运行任务,询问用户输入,运行下一个任务,请求更多输入,运行下一个任务等。
如果您将流程建模为工作流程,则应明确需要哪些任务,决策和用户输入。
我会在后台线程中运行每个任务,以保持用户界面的交互性,但没有所有的编组问题。
答案 5 :(得分:1)
通过向队列发送消息并让侦听器执行处理来异步处理。控制器向用户发送一条ACK消息,说明“我们已收到您的处理请求。请稍后再查看结果。”为用户提供邮箱或链接以进行检查并查看事情的进展情况。
答案 6 :(得分:1)
虽然我不完全明白你想做什么,但我能提供的是我对可能的解决方案的看法。我的理解是你有一系列的事情要做,而且一方面的决定可能会导致一个或多个不同的事情被添加到“转变”。如果是这种情况,那么我会尝试将GUI和决策与需要完成的实际工作分开(尽可能多)。当用户启动“转换”时,我会(不是在一个线程中)循环执行每个必要的决定但不执行任何工作...只是询问完成工作所需的问题,然后推动步骤与参数列表。
当完成最后一个问题时,产生你的线程,传递与参数一起运行的步骤列表。这种方法的优点是你可以显示n个项目中的1个的进度条,让用户知道他们喝咖啡后回来可能需要多长时间。
答案 7 :(得分:1)
我肯定会选择线程。弄清楚线程如何与用户交互通常很困难,但对我来说效果很好的解决方案是不让线程与用户交互,而是让用户端GUI与线程交互。这解决了使用同步更新GUI的问题,并为用户提供了更具响应性的活动。
因此,为此,我在线程中使用各种变量,由使用关键部分的Get / Set例程访问,以包含状态信息。对于初学者,我有一个“已取消”属性,GUI设置为要求线程停止请。然后是“Status”属性,指示线程是在等待,忙碌还是完成。您可能具有“人类可读”状态,以指示正在发生的事情或完成百分比。
要阅读所有这些信息,只需在表单上使用计时器并进行更新。我倾向于拥有一个“statusChanged”属性,如果其他一个项目需要刷新,则会设置该属性,这会停止进行过多的阅读。
这在我的各种应用程序中运行良好,包括在带有进度条的列表框中显示最多8个线程的状态。
答案 8 :(得分:1)
如果您决定使用Threads,我发现它在Delphi中的实现方式有些复杂,我建议使用PrimožGabrijelčič的OmniThreadLibrary或Gabr,因为他在Stack Overflow中已知
这是我所知道的最简单的使用线程库。加布尔写了很多东西。
答案 9 :(得分: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;