如何在长时间运行的函数期间更新UI(文本字段)?

3

我知道这个问题可能没有意义,我很难想象一个解释方式,所以我会展示一小段代码来帮助理解。我在使用Visual Studio Express 2010上的Winforms:

private void button1(object sender, EventArgs e)
    {
        txtOutput.Text += "Auto-collecting variables. This may take several minutes";
        string v = foo();
        txtOutput.Text += "\n" + v;
        string b = bar();
        txtOutput.Text += "\n" + b;

        txtOutput.SelectionStart = txtOutput.Text.Length;
        txtOutput.ScrollToCaret(); //scrolls to the bottom of textbox
    }

基本上,当用户点击button1时,我希望“自动收集变量…”显示在文本框中,然后执行foo()函数,并将其显示出来,然后执行bar()函数,然后再将其显示出来。
目前的情况是,在foo()和bar()函数执行后一次性显示所有内容(这些函数需要几分钟的时间才能执行)。有没有办法解决这个问题,或者有没有解决方法?
编辑:C#的版本是4.0。如果我更新到4.5或5.0,那么没有安装.NET 4.5/5.0的计算机能否运行.exe文件?

1
我已经更新了标题 - 如有需要可以还原。请注意,“output”通常用于“console output”或各种顺序日志/文件的上下文中。在UI情况下,以下单词更常见:“update”、“refresh”、“show”。 - Alexei Levenkov
@AlexeiLevenkov 啊!这个标题好多了。谢谢! - echolocation
5个回答

6

C# 5.0使这变得轻而易举。

使用Task.Run在后台线程中执行长时间运行的任务,并使用await将方法的其余部分作为UI线程中的继续执行,而不会在异步任务持续时间内阻塞UI线程。

private async void button1(object sender, EventArgs e)
{
    txtOutput.Text += "Auto-collecting variables. This may take several minutes";
    string v = await Task.Run(() => foo());
    txtOutput.Text += "\n" + v;
    string b = await Task.Run(() => bar());
    txtOutput.Text += "\n" + b;

    txtOutput.SelectionStart = txtOutput.Text.Length;
    txtOutput.ScrollToCaret(); //scrolls to the bottom of textbox
}

你可以在C# 4.0中使用类似的方法进行操作:(第一个解决方案将被编译器转换成类似的代码)。
private  void button1(object sender, EventArgs e)
{
    txtOutput.Text += "Auto-collecting variables. This may take several minutes";
    Task.Factory.StartNew(() => foo())
        .ContinueWith(t => txtOutput.Text += "\n" + t.Result
            , TaskScheduler.FromCurrentSynchronizationContext())
        .ContinueWith(t => bar())
        .ContinueWith(t =>
        {
            txtOutput.Text += "\n" + t.Result;
            txtOutput.SelectionStart = txtOutput.Text.Length;
            txtOutput.ScrollToCaret(); //scrolls to the bottom of textbox
        }
            , TaskScheduler.FromCurrentSynchronizationContext());
}

好的,我已经尝试了一段时间并升级到了.NET 5.0,但是不知为何,Task.Run没有被定义。错误1:'System.Threading.Tasks.Task'中不包含对'Run'的定义。我是否使用了错误的命名空间? - echolocation
1
@BrianR 没有 .NET 5。有 C# 4.0 和 .NET 4.5。如果您没有 Task.Run 的定义,则意味着您正在使用的是 .NET 4.0 而不是 4.5。 - Servy
@Servy 很抱歉,我对C#和.NET一点也不熟悉。在将项目属性更改为使用.NET 4.5后,您的解决方案完美地运行了。谢谢。 - echolocation
@BrianR 不用担心;C#、.NET、Visual Studio等的版本控制都非常混乱。被它搞糊涂是完全可以理解的。 - Servy

2
根据 .NET 的版本,您可以使用 BackgroundWorker(4.0 以前)或 Tasks(4.0 后 - 3.5 需要插件)等,这里只是其中几个例子。
BackgroundWorker 伪代码:
var backgroundWorker = new BackgroundWorker()

method
{
    //Update UI
    backgroundWorker.RunWorkAsync()
}

asyncworkmethod
{
    //do main logic
}

asynccompletemethod
{
    //Update UI to say done
}

任务伪代码:
method
{
     //Update UI
     TaskFactory.StartNew(()=>DoWork).ContinueWith((previousTask)=>UpdateUIToSayDone)
}

如果您正在使用4.5版本,则可以使用async/await关键字,但这只是任务周围的语法糖(大多数情况下...)。 Servy已经有一个不错的例子,尽管如果您采用这种方法。

1
  1. 澄清问题应该在评论中提出,而不是在帖子中发布。
  2. 如果您没有完整的答案,请不要发布一个回答说“更多细节即将到来”。当您有完整的答案时再发布一个回答。就目前而言,这充其量只是一条评论。
- Servy
没有人点踩,我刚刚检查了(更新:现在有一个)。此外,我认为这更好,因为它提到了“任务”,这比BGWorkers好得多。 - It'sNotALie.
谁在4分钟前删除了我的评论?我之前的评论是“也许那只是一些错误”? - King King
@Servy 我会看看 Meta 上是否有相关内容,但我认为这是胡说八道。我经常看到这种回答方式。你不能否认 SO 在某种程度上是一个“快速应对”的地方。 - Justin Pihony
@JustinPihony 如果你要发布一个答案,它应该回答问题。这是SO的基本概念,我可以在meta上找到很多例子,解释为什么将评论、澄清问题或其他非答案作为答案发布是不合适的。即使你打算将其编辑成适当的答案,最初的帖子也不合适。你似乎把这个问题和发布一个有效的回答混淆了,这个回答回答了问题,但没有深入探讨,深度是在编辑中添加的。这不违反规则。 - Servy
显示剩余2条评论

2
使用BackgroundWorker类来处理你的任务,不会阻塞UI更新。它有事件可以用来将进度信息传输到UI线程。

1

使用后台进程(请参阅其他答案)是正确的方法,但如果您正在寻找一个非常快速的解决方法,您可以在更新文本框后调用Application.DoEvents()。在大多数情况下,此调用将导致您的表单更新以反映您所做的更改。


在大多数情况下,除非您滥用消息循环,否则不会出现整个应用程序崩溃(或更糟的情况)。 - Servy
@Servy:在哪些情况下应用程序会崩溃? - Douglas
这怎么会滥用消息循环呢(你有一些崩溃应用的例子吗)?只是短暂地将 UI 线程交还给处理消息。这不是推荐的方法,但在某些情况下,与其切换到后台进程,这可能更合适实现。 - Knaģis
1
@Knaģis 在使用函数时,正确使用非常重要,而这不是一个适当的用法。特别是,在使用 DoEvents 时,重入是不合适的;您需要禁用窗体上可能会向消息队列添加新事件的控件。如果您在整个表单上禁用它,则可以工作,但这只是非常糟糕的做法。从根本上讲,实际上很难使其正常工作。比起其他替代方案,它要困难得多。如果您不了解其工作原理,则最好不要使用它。 - Servy
应该注意到 DoEvents 的作用。如果您没有禁用所有输入,那么用户可能会单击另一个按钮或更改其他状态。一般来说,在启动长时间处理之前禁用所有输入,完成后再启用它们是更好的做法。例外情况包括取消按钮或长时间运行的操作,其有效地作为单独的批处理进行,不会受到用户继续使用应用程序的影响。 - HABO

0

txtOutput.Update()应该可以满足您的需求,但是您应该考虑使用后台线程来完成长时间运行的任务,以避免阻塞UI线程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接