ChatGPT解决这个技术问题 Extra ChatGPT

从 UI 线程强制 GUI 更新

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

我正在做的大致是:

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

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

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

在另一个线程中运行 SomewhatLongRunningOperation() 是正确的答案。你不应该为任何不直接影响 UI 的东西占用 UI 线程。至于简化代码,您很可能可以简化其他线程的使用。

C
Community

起初我想知道为什么 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 显示在框中。请更新此答案。如果我知道,我会告诉修复,但我也无法弄清楚。
S
Scoregraphic

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


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

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

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


因此,如您所见,label.Invalidate()(不带参数)后跟 label.Update() 等价于 label.Refresh()
u
user3029478

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

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

C
Community

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

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

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

当我在应用程序启动后单击 VS 一次(将焦点放在那里并启用步进)并在不移动鼠标的情况下单步执行它时,会设置新文本并在 update() 函数中更新标签。这意味着,UI 明显被重新绘制。当我越过第一行,然后将鼠标移动很多并单击某处,然后再进一步,新文本可能已设置并调用 update() 函数,但 UI 未更新/重绘且旧文本保持在那里,直到 button1_click() 函数完成。窗口被标记为“不响应”,而不是重新绘制!添加 this->Update(); 也无济于事。更新整个表格。添加应用程序::DoEvents();使 UI 有机会更新/重绘。无论如何,您必须注意用户不能在 UI 上按下按钮或执行其他不允许的操作!因此:尽量避免 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> 结论:所以你只需要阻止用户使用鼠标;-)


R
Rick2047

你可以试试这个

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

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

看看它是否有效。


p
pixelgrease

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


M
Mike Hall

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


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

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

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

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

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


R
Robert Martin

myControlName.Refresh() 是一个简单的解决方案,用于在移动到“SomewhatLongRunningOperation”之前更新控件。来自:https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.update?view=windowsdesktop-6.0 有两种方法可以重绘表单及其内容:

您可以将 Invalidate 方法的重载之一与 Update 方法一起使用。您可以调用 Refresh 方法,该方法强制控件重绘自身及其所有子项。这相当于将 Invalidate 方法设置为 true 并将其与 Update 一起使用。

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


C
Community

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


I
Ibrennan208

当我想“实时”更新 UI(或基于数据更新或长时间运行的操作)时,我使用辅助函数来“简化”代码(这里看起来可能很复杂,但它向上扩展非常很好)。下面是我用来更新 UI 的代码示例:

    // a utility class that provides helper functions
    // related to Windows Forms and related elements
    public static class FormsHelperFunctions {

        // This method takes a control and an action
        // The action can simply be a method name, some form of delegate, or it could be a lambda function (see: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions)
        public static void InvokeIfNeeded(this Control control, Action action)
        {

            // control.InvokeRequired checks to see if the current thread is the UI thread,
            // if the current thread is not the UI thread it returns True - as in Invoke IS required
            if(control.InvokeRequired)
            {
                // we then ask the control to Invoke the action in the UI thread
                control.Invoke(action);
            }
            // Otherwise, we don't need to Invoke
            else
            {
                // so we can just call the action by adding the parenthesis and semicolon, just like how a method would be called.
                action();
            }
        }

    }

    // An example user control
    public class ExampleUserControl : UserControl {

        /*
            //
            //*****
            // declarations of label and other class variables, etc.
            //*****
            //




            ...
        */

        // This method updates a label, 
        // executes a long-running operation, 
        // and finally updates the label with the resulting message.
        public void ExampleUpdateLabel() {

            // Update our label with the initial text
            UpdateLabelText("Please Wait...");

            // result will be what the label gets set to at the end of this method
            // we set it to Success here to initialize it, knowing that we will only need to change it if an exception actually occurs.
            string result = "Success";

            try {
                // run the long operation
                SomewhatLongRunningOperation(); 
            }
            catch(Exception e)
            {
                // if an exception was caught, we want to update result accordingly
                result = "Error: " + e.Message;
            }

            // Update our label with the result text
            UpdateLabelText(result);
        }

        // This method takes a string and sets our label's text to that value
        // (This could also be turned into a method that updates multiple labels based on variables, rather than one input string affecting one label)
        private void UpdateLabelText(string value) {

            // call our helper funtion on the current control
            // here we use a lambda function (an anonymous method) to create an Action to pass into our method
            // * The lambda function is like a Method that has no name, here our's just updates the label, but it could do anything else we needed
            this.InvokeIfNeeded(() => {

                // set the text of our label to the value
                // (this is where we could set multiple other UI elements (labels, button text, etc) at the same time if we wanted to)
                label.Text = value;
            });
        }

    }