从 UI 线程强制 GUI 更新

Posted

技术标签:

【中文标题】从 UI 线程强制 GUI 更新【英文标题】:Force GUI update from UI Thread 【发布时间】:2010-11-24 13:27:02 【问题描述】:

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

我在做的大致是:

label.Text = "Please Wait..."
try 

    SomewhatLongRunningOperation(); 

catch(Exception e)

    label.Text = "Error: " + e.Message;
    return;

label.Text = "Success!";

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

我使用另一个线程进行操作解决了这个问题,但它很麻烦,我想简化代码。

【问题讨论】:

在另一个线程中运行SomewhatLongRunningOperation() 是正确的答案。你不应该为任何不直接影响 UI 的东西占用 UI 线程。至于简化代码,您很有可能可以简化其他线程的使用。 相关帖子 - How do I update the GUI from another thread? & Why won't control update/refresh mid-process 【参考方案1】:

起初我想知道为什么 OP 还没有将其中一个响应标记为答案,但是在我自己尝试之后仍然无法正常工作,我挖得更深一点,发现这个问题还有很多'首先假设。

阅读类似的问题可以更好地理解:Why won't control update/refresh mid-process

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

private void SetStatus(string status) 

    lblStatus.Text = status;
    lblStatus.Invalidate();
    lblStatus.Update();
    lblStatus.Refresh();
    Application.DoEvents();

尽管据我了解,这远非一种优雅而正确的方法。根据线程的繁忙程度,它可能会起作用,也可能不会起作用。

【讨论】:

谢谢,它确实有效。我重新构造了代码来解决这个问题,但你的解决方案(尽管它可能很老套)更优雅。 我当时就疯了!噗!!!认为无效调用会做......从来没有想过打包所有这些调用只是为了更新标签。哈基,但它有效。 Refresh() 等价于Invalidate() 后跟Update(),所以实际上你在那里做了两次。 即使.Refresh() 也没什么用,请看下面我的回答。 Application.DoEvents() 是您唯一需要的命令,尽管它应该只用于非常简单的程序。其余的,最好使用真正的线程(例如通过使用 BackgroundWorker)。 这不再起作用了。无论您是在表单对象上使用所有这些命令(在我的情况下为richtextbox1)还是只是 Application.DoEvents();两者都不起作用。我有richtextbox1.Text = "test",除了键入一个明显违背此处目的的键之外,这些都不会导致单词 test 显示在框中。请更新此答案。如果我知道,我会告诉修复,但我也想不通。【参考方案2】:

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

【讨论】:

+1,但对于简单的应用程序,不使用后台工作线程来保持简单是完全有效的。只需将光标设置为沙漏调用 Application.DoEvents。 不,不是,因为 SomeWhatLongRunningOperation 阻塞了整个应用程序。你喜欢无响应的程序吗?我没有! 不,我只是不喜欢过于复杂的简单应用程序,这些应用程序根本不需要多线程的开销。 Windows 是一个多任务操作系统,只需启动任务并在另一个应用程序中完成一些其他工作。 我认为这个简单的应用程序为获得正确的线程等事情提供了极好的训练场,因此您不必在更关键的代码中进行试验。此外,Application.DoEvents 可能会引入有趣的问题(线程也会发生),例如,如果用户再次单击按钮触发操作,该操作仍在运行,会发生什么情况? @Frederik,我工作的地方,如果你通过向简单的应用程序引入更复杂的代码来做“训练练习”,你很快就会得到“踢后腿”。【参考方案3】:

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

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

【讨论】:

因此,如您所见,label.Invalidate()(不带参数)后跟label.Update() 等价于label.Refresh()【参考方案4】:

我刚刚偶然发现了同样的问题并发现了一些有趣的信息,我想投入两分钱并在此处添加。

首先,正如其他人已经提到的,长时间运行的操作应该由一个线程来完成,它可以是后台工作者、显式线程、线程池中的线程或(从 .Net 4.0 开始)任务: *** 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)并查看发生了什么。在阅读了这篇文章Multithreading in WinForms 之后,我发现了一些有趣的东西。文章在第一页的底部说,UI 线程在当前执行的函数完成之前无法重新绘制 UI,并且窗口在一段时间后被 Windows 标记为“无响应”。我还注意到,在我的测试应用程序中,在单步执行时,但仅在某些情况下。

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

    当我在应用程序启动后单击 VS(将焦点放在那里并启用步进)并在不移动鼠标的情况下单步执行时,会设置新文本并在更新中更新标签() 函数。 这意味着,UI 明显被重新绘制。

    当我越过第一行,然后将鼠标移动很多并单击某处,然后再进一步,可能会设置新文本并调用 update() 函数,但 UI 未更新/repainted 并且旧文本保留在那里,直到 button1_click() 函数完成。窗口被标记为“不响应”,而不是重新绘制!添加this->Update(); 来更新整个表单也无济于事。

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

我的解释如下:AFAIK winform 仍然使用 WINAPI 函数。 MSDN article about System.Windows.Forms Control.Update method 也指 WINAPI 函数 WM_PAINT。 MSDN article about WM_PAINT 在其第一句中声明 WM_PAINT 命令仅在消息队列为空时由系统发送。但是由于第二种情况下消息队列已经填满,所以没有发送,因此标签和申请表也没有重绘。

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

【讨论】:

【参考方案5】:

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

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

【讨论】:

【参考方案6】:

你可以试试这个

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

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

看看它是否有效。

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

【讨论】:

矫枉过正,对于一个简单的应用程序,强制更新 UI 是完全有效的。最小化/最大化按钮仍然有效,只是处理其他事情。为什么要引入线程间通信问题? 对于一个简单的应用程序,我不会称其为矫枉过正,仅当操作非常短时才矫枉过正。然而,他称之为“SomewhatLongRunningOperation”。而且由于人们通常只需要在 UI 被捆绑很长时间的情况下才需要这样做,为什么要锁定 UI?这就是 BackgroundWorker 类的用途!再简单不过了。【参考方案9】:

尝试调用 label.Invalidate()

http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invalidate(VS.80).aspx

【讨论】:

【参考方案10】:

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

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

我尝试减小值并且即使在调试模式下也会更新屏幕,但这不适用于将progressBar.Value 设置为progressBar.Maximum,因为您无法将进度条值设置为高于最大值,所以我首先设置了@ 987654324@ 到 progressBar.Maximum -1,然后将 progressBar.Maxiumum 设置为等于 progressBar.Value。他们说杀死猫的方法不止一种。有时我想杀死比尔盖茨或现在的任何人:o)。

有了这个结果,我什至不需要Invalidate()Refresh()Update(),或者对进度条或其面板容器或父窗体做任何事情。

【讨论】:

【参考方案11】:

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

【讨论】:

以上是关于从 UI 线程强制 GUI 更新的主要内容,如果未能解决你的问题,请参考以下文章

来自不同 vb 文件中的线程的 VB 网络更新 GUI

强制 Java 刷新 Java Swing GUI

在主线程的密集功能期间 UI 未更新(在 Swift 中)

关于使用多线程定期强制检查软件更新的问题

多线程更新UI的常用方法

如何从另一个线程更新 GUI?