从UI线程强制进行GUI更新


71

在WinForms中,如何从UI线程强制立即进行UI更新?

我正在做的大致是:

label.Text = "Please Wait..."
try 
{
    SomewhatLongRunningOperation(); 
}
catch(Exception e)
{
    label.Text = "Error: " + e.Message;
    return;
}
label.Text = "Success!";

在操作之前,标签文本未设置为“ Please Wait ...”。

我使用另一个线程解决了这个问题,但是它变得繁琐,我想简化代码。


5
SomewhatLongRunningOperation()在另一个线程中运行是正确的答案。您不应将UI线程用于任何不会直接影响UI的事情。至于简化代码,很有可能可以简化其他线程的使用。
cHao

Answers:


103

最初,我想知道为什么OP尚未将其中一个答复标记为答案,但是在我自己尝试并且仍然无法正常工作之后,我更深入地挖掘了一下,发现这个问题还有很多,然后我首先应该。

通过阅读类似的问题可以获得更好的理解:为什么不控制中间过程的更新/刷新

最后,作为记录,我能够通过执行以下操作来更新标签:

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

尽管从我的理解来看,这远远不是一种优雅而正确的方法。根据线程的繁忙程度,这是一种可能有效或无效的技巧。


1
谢谢,它实际上有效。我重新构建了代码以解决此问题,但是您的解决方案(可能有些hacky)更加优雅。
dbkk'4

我当时正要疯了!菲!以为无效调用会做...从来没有想过打包所有这些调用只是为了更新Label。朦胧,但可以。
Mehdi LAMRANI

31
Refresh()等于Invalidate()后跟Update(),因此实际上您在这里做了两次。
克里斯·摩根

11
甚至.Refresh()没有用,请参阅下面的答案。Application.DoEvents()尽管只应在非常简单的程序上使用,但它是唯一需要的命令。在其他方面,最好使用真实线程(例如,通过使用BackgroundWorker)。
Tobias Knauss'3

这不再起作用了。您是在表单对象(在我的情况下为richtextbox1)还是仅Application.DoEvents();上使用所有这些命令?都不起作用。我有richtextbox1.Text =“ test”,除了键入一个显然不符合此目的的键外,没有一个导致单词test在框中显示。请更新此答案。如果知道的话我会告诉修复程序,但我也无法解决。
codehelp4

16

调用label.Invalidate然后label.Update()-通常仅在退出当前函数之后才进行更新,但是调用Update会强制其在代码中的特定位置进行更新。从MSDN

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


3
因此,如您所见,label.Invalidate()(不带参数)后跟label.Update()等效于label.Refresh()
克里斯·摩根

15

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


2
+1,但是对于简单的应用程序,通过不使用后台工作线程来使事情简单是完全有效的。只需将光标设置为沙漏调用Application.DoEvents。
灰烬

4
不,不是,因为SomeWhatLongRunningOperation阻止了整个应用程序。你喜欢无响应的程序吗?我不!
Scoregraphic

1
不,我只是不喜欢过于复杂的简单应用程序,这些应用程序根本不需要多个线程的开销。Windows是一个多任务操作系统,只需启动任务并执行其他应用程序中完成的其他工作即可。
灰烬

2
我认为简单的应用程序将为出色的培训奠定基础,以使正确的线程处理成为可能,这样您就不必尝试使用更关键的代码。此外,Application.DoEvents可能会引入有趣的问题(线程也会发生),例如,如果用户单击再次触发该操作的按钮(该按钮仍在运行)会发生什么?
弗雷德里克·莫尔克(FredrikMörk)2009年

2
@Frederik,我在这里工作,如果您要通过向简单的应用程序引入更复杂的代码来进行“培训”,您将很快被“踢倒”。
灰烬

4

我偶然发现了同样的问题,发现了一些有趣的信息,我想放两分钱在这里加进去。

首先,正如其他人已经提到的那样,长时间运行的操作应该由线程完成,该线程可以是后台工作程序,显式线程,线程池中的线程或(自.Net 4.0起)任务: 池中 Stackoverflow 570537:在Windows窗体中处理时更新标签,以便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中的多线程》之后,我发现了一些有趣的东西。文章在第一页底部说,直到当前执行的功能完成并且一段时间后Windows将窗口标记为“无响应”,UI线程才能重绘UI。我还注意到,在我的测试应用程序中,它是从上面逐步执行的,但仅在某些情况下如此。

(对于以下测试,重要的是不要将Visual Studio设置为全屏,必须同时看到小应用程序窗口,并且不必在调试的Visual Studio窗口和应用程序窗口,看看会发生什么。启动应用程序,在以下位置设置断点 label1->Text ...,将应用程序窗口置于VS窗口旁边,然后将鼠标光标放在VS窗口上。)

  1. 当我在应用程序启动后在VS上单击一次(将焦点放在此处并启用步进)并在不移动鼠标的情况下逐步执行时,将设置新文本,并在update()函数中更新标签。这意味着,UI显然已重新粉刷。

  2. 当我跨过第一行时,将鼠标移至很多位置并单击某个位置,然后再往前走,可能会设置新文本并调用update()函数,但UI不会更新/重新绘制,而旧文本保留在那里,直到button1_click()函数完成。而不是重新粉刷窗口被标记为“无响应”!这也无助于添加this->Update();以更新整个表单。

  3. 添加Application::DoEvents();使UI有机会更新/重新绘制。无论如何,您必须注意用户不能按下按钮或在UI上执行不允许的其他操作!因此:尝试避免DoEvents()!,最好使用线程(我认为在.Net中这很简单)。
    但是(@ Jagd,2010年4月2日,19:25)您可以省略.refresh().invalidate()

我的解释如下:AFAIK winform仍使用WINAPI函数。MSDN上有关System.Windows.Forms Control.Update方法的文章也引用WINAPI函数WM_PAINT。有关WM_PAINTMSDN文章在其第一句话中指出,仅当消息队列为空时,系统才发送WM_PAINT命令。但是由于在第二种情况下消息队列已被填充,因此不会发送消息,因此不会重新绘制标签和申请表。

<>笑话>结论:因此,您只需要防止用户使用鼠标;-) <> / joke>


3

你可以试试这个

using System.Windows.Forms; // u need this to include.

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

看看是否可行。


2

更新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及更高版本。


2

如果只需要更新几个控件,则.update()就足够了。

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

1

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


矫kill过正,对于一个简单的应用程序,强制更新UI完全有效。最小化/最大化按钮仍然有效,仅在其他方面起作用。为什么引入线程间通信问题?
灰烬

对于一个简单的应用程序,我不会称其为矫kill过正,只有在操作非常短的情况下才算矫over过正。但是,他称其为“ SomewhatLongRunningOperation”。而且由于人们通常只需要在将UI捆绑很长时间的情况下执行此操作,为什么要完全锁定UI?那就是BackgroundWorker类的用途!再简单不过了。
麦克·霍尔


1

认为我有答案,从上面总结出来并做了一些实验。

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

我尝试减小该值,即使在调试模式下也更新了屏幕,但是对于设置progressBar.ValueprogressBar.Maximum,这将不起作用,因为您无法将进度条的值设置为大于最大值,因此我首先将其设置progressBar.ValueprogressBar.Maximum -1,然后设置progressBar.Maxiumum为等于progressBar.Value。他们说,杀死猫的方法不止一种。有时我想杀死比尔·盖茨或其他人:o)。

有了这个结果,我甚至没有出现需要Invalidate()Refresh()Update(),或做任何事情的进度条或它的面板容器或父窗体。


0

我在属性上Enabled也遇到了同样的问题,并且发现了一个first chance exception凸起的,因为它不是线程安全的。我找到了有关“如何从C#中的另一个线程更新GUI的解决方案?”的解决方案。在这里https://stackoverflow.com/a/661706/1529139而且有效!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.