从UI线程强制更新GUI

81
在WinForms中,我如何从UI线程强制进行立即的UI更新?
我正在做的大概是:
label.Text = "Please Wait..."
try 
{
    SomewhatLongRunningOperation(); 
}
catch(Exception e)
{
    label.Text = "Error: " + e.Message;
    return;
}
label.Text = "Success!";

操作前标签文本未设置为“请稍等...”。

我使用另一个线程解决了这个问题,但代码变得复杂了,我希望简化代码。


5
在这里,将“SomewhatLongRunningOperation()”运行在另一个线程中是正确的答案。您不应该为任何与UI无关的事情占用UI线程。至于简化代码,您很可能可以简化对该其他线程的使用。 - cHao
13个回答

113

一开始我想知道为什么楼主还没有将其中一个回答标记为答案,但在尝试后仍然不起作用后,我深入挖掘发现这个问题比我最初想象的要复杂得多。

通过阅读类似问题的内容可以更好地理解:为什么控件无法在中途更新/刷新

最后,为了记录,我成功更新标签的方法如下:

private void SetStatus(string status) 
{
    lblStatus.Text = status;
    lblStatus.Invalidate();
    lblStatus.Update();
    lblStatus.Refresh();
    Application.DoEvents();
}

尽管据我所知,这远非一种优雅且正确的处理方法。这是一种可能有效或无效取决于线程忙碌程度的黑客方式。


1
谢谢,它确实有效。我重新组织了代码来解决这个问题,但你的解决方案(虽然有些hacky)更加优雅。 - dbkk
36
Refresh() 相当于先调用 Invalidate() 再调用 Update(),因此在这里您实际上进行了两次操作。 - Chris Morgan
14
即使是.Refresh()也相当无用,见下面我的回答。Application.DoEvents() 是你唯一需要的命令,虽然它只应该在非常简单的程序中使用。对于其他情况,最好使用真正的线程(例如使用BackgroundWorker)。 - Tobias Knauss
这个不再起作用了。无论你是在表单对象上使用所有这些命令(在我的情况下是richtextbox1),还是只是Application.DoEvents();都不起作用。我有richtextbox1.Text = "test",但除了键入一个键以外,这些都没有导致单词“test”显示在框中,显然这违背了本意。请更新此答案。如果我知道修复方法,我会告诉你的,但我也无法弄清楚。 - codehelp4
@codehelp4 - 自今年(2018年)四月以来,我已经看到了这个答案的10个赞成票和0个反对票,所以我倾向于相信它仍然有效。 - Jagd
显示剩余4条评论

18

在设置标签之后,调用 Application.DoEvents(),但是应该在单独的线程中完成所有工作,以便用户可以关闭窗口。


4
+1,但对于简单的应用程序来说,保持简单,不使用后台工作线程是完全有效的。只需将光标设置为沙漏并调用Application.DoEvents即可。 - Ash
4
不是,因为SomeWhatLongRunningOperation会阻塞整个应用程序。你喜欢无响应的程序吗?我不喜欢! - Scoregraphic
1
不,我只是不喜欢让简单的应用程序变得过于复杂,这些应用程序根本不需要多线程的开销。Windows 是一个多任务操作系统,只需启动任务,然后在另一个应用程序中完成其他工作。 - Ash
2
我认为简单的应用程序非常适合作为训练场地,以便正确处理像线程之类的问题,这样你就不必在更关键的代码中进行实验。此外,Application.DoEvents可能会引入有趣的问题(线程也会出现),例如如果用户点击一个按钮再次触发操作时,它仍在运行会发生什么? - Fredrik Mörk
2
@Frederik,在我工作的地方,如果你在简单应用程序中引入更复杂的代码来进行“训练练习”,你很快就会被“踢出去”。 - Ash
1
@Ash:你有没有在启动一个简单的.NET表单应用程序后查看线程计数?如果不使用简单的练习,你会如何介绍自己进入多线程开发?我很好奇。 - Scoregraphic

17

调用label.Invalidate然后调用label.Update() - 通常情况下,更新只会在退出当前函数后发生,但是调用Update会强制在代码的特定位置进行更新。 来自MSDN

Invalidate方法控制什么被绘制或重新绘制。Update方法控制何时进行绘制或重新绘制。如果您使用Invalidate和Update方法而不是调用Refresh,则重新绘制取决于您使用哪个Invalidate重载。Update方法只是强制立即绘制控件,但是当您调用Update方法时,Invalidate方法控制要绘制的内容。


6
因此,正如您所看到的,label.Invalidate()(没有参数)后跟label.Update()等效于label.Refresh() - Chris Morgan

6

如果您只需要更新一些控件,使用.update()就足够了。

btnMyButton.BackColor=Color.Green; // it eventually turned green, after a delay
btnMyButton.Update(); // after I added this, it turned green quickly

4

我刚刚遇到了同样的问题,并找到了一些有趣的信息。在这里,我想要发表我的看法并添加进来。

首先,正如其他人已经提到的,长时间运行的操作应该由一个线程来处理,它可以是一个后台工作者、一个显式线程、一个线程池中的线程或者(自 .Net 4.0) 一个任务: Stackoverflow 570537: update-label-while-processing-in-windows-forms, 这样UI就会保持响应。

但是对于短时间的任务,实际上没有必要使用多线程,虽然当然也不会有什么坏处。

我创建了一个包含一个按钮和一个标签的winform来分析这个问题:

System::Void button1_Click(System::Object^  sender, System::EventArgs^  e)
{
  label1->Text = "Start 1";
  label1->Update();
  System::Threading::Thread::Sleep(5000); // do other work
}

我的分析是通过使用F10逐步执行代码并观察发生了什么。在阅读了这篇文章之后WinForms中的多线程,我发现了一些有趣的事情。该文章在第一页底部指出,UI线程不能重新绘制UI直到当前执行的函数完成,并且窗口在一段时间后被Windows标记为“无响应”。我也注意到,在上面的测试应用程序中,在逐步执行过程中,只有在某些情况下才会出现这种情况。
(对于以下测试,重要的是不要将Visual Studio设置为全屏模式,您必须能够同时看到您的小应用程序窗口和VS窗口,您不必在调试期间在VS窗口和应用程序窗口之间切换以查看发生了什么。启动应用程序,在label1->Text ...处设置断点,将应用程序窗口放在VS窗口旁边,并将鼠标光标放在VS窗口上。)
- 当我在应用程序启动后单击一次VS(将焦点置于那里并启用逐步执行),然后在不移动鼠标的情况下逐步执行,新的文本被设置且标签在update()函数中更新。这意味着,UI显然已经重绘。 - 当我逐步执行第一行,然后大量移动鼠标并单击某个位置,然后继续逐步执行,新的文本很可能被设置并调用update()函数,但是UI不会更新/重绘,旧文本仍然存在,直到button1_click()函数完成。窗口被标记为“无响应”而不是重新绘制!添加this->Update();也没有帮助更新整个表单。 - 添加Application::DoEvents();给UI一个机会来更新/重绘。无论如何,您必须确保用户不能按下按钮或在UI上执行其他不允许的操作!!因此:尽量避免使用DoEvents()!最好使用线程(我认为在.Net中非常简单)。但是(@Jagd,Apr 2 '10 at 19:25),您可以省略.refresh().invalidate()
我的解释如下:据我所知,winform仍然使用WINAPI函数。此外,关于System.Windows.Forms Control.Update方法的MSDN文章引用了WINAPI函数WM_PAINT。在WM_PAINT的MSDN文章中第一句话指出,当消息队列为空时,系统才会发送WM_PAINT命令。但由于在第二种情况下消息队列已经满了,因此该命令不会被发送,从而标签和应用程序窗体都没有被重新绘制。

<>玩笑<结论:所以你只需要阻止用户使用鼠标;-) </joke>


3
您可以尝试这个。
using System.Windows.Forms; // u need this to include.

MethodInvoker updateIt = delegate
                {
                    this.label1.Text = "Started...";
                };
this.label1.BeginInvoke(updateIt);

看看它是否可行。


3

更新UI后,启动一个任务来执行长时间运行的操作:

label.Text = "Please Wait...";

Task<string> task = Task<string>.Factory.StartNew(() =>
{
    try
    {
        SomewhatLongRunningOperation();
        return "Success!";
    }
    catch (Exception e)
    {
        return "Error: " + e.Message;
    }
});
Task UITask = task.ContinueWith((ret) =>
{
    label.Text = ret.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());

这适用于.NET 3.5及更高版本。


1

我想我有答案了,从上面和一些实验中提炼出来。

progressBar.Value = progressBar.Maximum - 1;
progressBar.Maximum = progressBar.Value;

我尝试减少值并在调试模式下更新了屏幕,但对于将 progressBar.Value 设置为 progressBar.Maximum,这种方式行不通,因为您不能将进度条的值设置为高于最大值,所以我首先将 progressBar.Value 设置为 progressBar.Maximum -1,然后将 progressBar.Maximum 设置为等于 progressBar.Value。有人说杀死猫有多种方法。有时候我想杀死比尔·盖茨或者现在是谁就好了 :o)。
通过这个结果,我似乎甚至不需要 Invalidate()Refresh()Update() 或者做任何关于进度条或其面板容器或父窗体的操作。

1

很有诱惑力想要“修复”它并强制更新UI,但最好的解决方案是在后台线程上执行此操作,而不是占用UI线程,以便它仍然可以响应事件。


对于简单的应用程序来说,过度设计是完全可以接受的,强制进行UI更新也是可以的。最小化/最大化按钮仍然有效,您可以同时处理其他事情。为什么要引入线程间通信问题呢? - Ash
我不会认为对于一个简单的应用程序来说这是过度设计,只有在操作非常短暂时才算过度设计。然而,他称之为“相当长时间运行的操作”。由于人们通常只需要在 UI 被占用了相当长的时间时才需要这样做,那么为什么要锁定 UI 呢?这就是 BackgroundWorker 类的作用!没有比这更容易的了。 - Mike Hall


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