在 WinForms 中,如何强制从 UI 线程立即更新 UI?
我正在做的大致是:
label.Text = "Please Wait..."
try
{
SomewhatLongRunningOperation();
}
catch(Exception e)
{
label.Text = "Error: " + e.Message;
return;
}
label.Text = "Success!";
操作前标签文本未设置为“请稍候...”。
我使用另一个线程进行操作解决了这个问题,但它变得很麻烦,我想简化代码。
SomewhatLongRunningOperation()
是正确的答案。你不应该为任何不直接影响 UI 的东西占用 UI 线程。至于简化代码,您很可能可以简化其他线程的使用。
起初我想知道为什么 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();
}
尽管据我了解,这远非一种优雅而正确的方法。根据线程的繁忙程度,它可能会或可能不会起作用。
设置标签后调用 Application.DoEvents()
,但您应该在单独的线程中完成所有工作,以便用户可以关闭窗口。
Application.DoEvents
可能会引入有趣的问题(线程也会发生),例如,如果用户单击按钮再次触发操作,该操作仍在运行,会发生什么情况?
调用 label.Invalidate
然后调用 label.Update()
- 通常更新仅在您退出当前函数后发生,但调用 Update 会强制它在代码中的特定位置更新。从 MSDN:
Invalidate 方法控制绘制或重新绘制的内容。 Update 方法控制何时进行绘制或重新绘制。如果您同时使用 Invalidate 和 Update 方法而不是调用 Refresh,那么重绘的内容取决于您使用的 Invalidate 重载。 Update 方法只是强制立即绘制控件,但 Invalidate 方法控制调用 Update 方法时绘制的内容。
label.Invalidate()
(不带参数)后跟 label.Update()
等价于 label.Refresh()
。
如果您只需要更新几个控件, .update() 就足够了。
btnMyButton.BackColor=Color.Green; // it eventually turned green, after a delay
btnMyButton.Update(); // after I added this, it turned green quickly
我刚刚偶然发现了同样的问题并发现了一些有趣的信息,我想投入两分钱并在此处添加。
首先,正如其他人已经提到的,长时间运行的操作应该由一个线程来完成,该线程可以是后台工作者、显式线程、线程池中的线程或(从 .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> 结论:所以你只需要阻止用户使用鼠标;-)
你可以试试这个
using System.Windows.Forms; // u need this to include.
MethodInvoker updateIt = delegate
{
this.label1.Text = "Started...";
};
this.label1.BeginInvoke(updateIt);
看看它是否有效。
更新 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 及更高版本。
想要“修复”此问题并强制更新 UI 是很诱人的,但最好的解决方法是在后台线程上执行此操作,而不是占用 UI 线程,以便它仍然可以响应事件。
尝试调用 label.Invalidate()
http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invalidate(VS.80).aspx
想我有答案,从上面和一些实验中提炼出来的。
progressBar.Value = progressBar.Maximum - 1;
progressBar.Maximum = progressBar.Value;
我尝试减小值并且即使在调试模式下也会更新屏幕,但这不适用于将 progressBar.Value
设置为 progressBar.Maximum
,因为您无法将进度条值设置为最大值以上,所以我首先将 progressBar.Value
设置为progressBar.Maximum -
1,然后将 progressBar.Maxiumum
设置为等于 progressBar.Valu
e。他们说杀死猫的方法不止一种。有时我想杀死比尔盖茨或现在的任何人:o)。
有了这个结果,我什至不需要 Invalidate()
、Refresh()
、Update()
或对进度条或其面板容器或父窗体执行任何操作。
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 方法时绘制的内容。
我在属性 Enabled
上遇到了同样的问题,我发现引发了一个 first chance exception
,因为它不是线程安全的。我找到了有关“如何从 C# 中的另一个线程更新 GUI?”的解决方案。这里https://stackoverflow.com/a/661706/1529139而且它有效!
当我想“实时”更新 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;
});
}
}
Refresh()
等价于Invalidate()
后跟Update()
,因此您实际上在此处执行了两次。.Refresh()
也没什么用,请参阅下面的答案。Application.DoEvents()
是您唯一需要的命令,尽管它只能用于非常简单的程序。其余的,最好使用真正的线程(例如通过使用BackgroundWorker)。