如何用最简单的方式从另一个线程更新Label
?
我有一个在
thread1
上运行的Form
,从那里我启动了另一个线程(thread2
)。当
thread2
处理一些文件时,我想通过Form
将Label
的当前状态更新为thread2
的工作状态。
我该怎么做呢?
如何用最简单的方式从另一个线程更新Label
?
我有一个在thread1
上运行的Form
,从那里我启动了另一个线程(thread2
)。
当thread2
处理一些文件时,我想通过Form
将Label
的当前状态更新为thread2
的工作状态。
我该怎么做呢?
最简单的方法是将一个匿名方法传递给Label.Invoke
:
// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
// Running on the UI thread
form.Label.Text = newText;
});
// Back on the worker thread
请注意,Invoke
方法会阻塞执行,直到完成--这是同步代码。虽然该问题不涉及异步代码,但在Stack Overflow上有很多关于编写异步代码的内容可供学习。
针对 .NET 2.0 版本,我写了一段很好的代码,可以准确地实现你所需要的功能,并且适用于 Control
上的任何属性:
private delegate void SetControlPropertyThreadSafeDelegate(
Control control,
string propertyName,
object propertyValue);
public static void SetControlPropertyThreadSafe(
Control control,
string propertyName,
object propertyValue)
{
if (control.InvokeRequired)
{
control.Invoke(new SetControlPropertyThreadSafeDelegate
(SetControlPropertyThreadSafe),
new object[] { control, propertyName, propertyValue });
}
else
{
control.GetType().InvokeMember(
propertyName,
BindingFlags.SetProperty,
null,
control,
new object[] { propertyValue });
}
}
像这样调用:
// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);
如果你使用的是 .NET 3.0 或以上版本,你可以将上面的方法重写为 Control
类的扩展方法,这样就可以简化调用方式:myLabel.SetPropertyThreadSafe("Text", status);
更新于 2010 年 5 月 10 日:
对于 .NET 3.0,您应该使用此代码:
private delegate void SetPropertyThreadSafeDelegate<TResult>(
Control @this,
Expression<Func<TResult>> property,
TResult value);
public static void SetPropertyThreadSafe<TResult>(
this Control @this,
Expression<Func<TResult>> property,
TResult value)
{
var propertyInfo = (property.Body as MemberExpression).Member
as PropertyInfo;
if (propertyInfo == null ||
!@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
@this.GetType().GetProperty(
propertyInfo.Name,
propertyInfo.PropertyType) == null)
{
throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
}
if (@this.InvokeRequired)
{
@this.Invoke(new SetPropertyThreadSafeDelegate<TResult>
(SetPropertyThreadSafe),
new object[] { @this, property, value });
}
else
{
@this.GetType().InvokeMember(
propertyInfo.Name,
BindingFlags.SetProperty,
null,
@this,
new object[] { value });
}
}
使用 LINQ 和 lambda 表达式,可以实现更加清晰、简洁和安全的语法:
// status has to be of type string or this will fail to compile
myLabel.SetPropertyThreadSafe(() => myLabel.Text, status);
现在属性名称不仅在编译时进行检查,属性的类型也会被检查,因此无法(例如)将字符串值分配给布尔属性,从而导致运行时异常。
不幸的是,这并不能阻止任何人做一些愚蠢的事情,比如传递另一个Control
的属性和值,所以以下代码可以成功编译:
myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);
因此,我添加了运行时检查,以确保传入的属性确实属于调用该方法的Control
。虽然不完美,但比.NET 2.0版本要好得多。如果有人有进一步建议来提高编译时安全性,请留言!
SetControlPropertyThreadSafe(myLabel, "Text", status)
可以从另一个模块、类或窗体中调用吗? - Smith自.NET 4.5和C# 5.0以来,您应该在所有领域中(包括GUI)使用基于任务的异步模式(TAP)以及async-await关键字:
TAP是新开发的推荐异步设计模式
建议在新开发中,使用以下方法代替 异步编程模型(APM) 和 基于事件的异步模式(EAP)(后者包括 BackgroundWorker Class)。
因此,建议的解决方案是:
Asynchronous implementation of an event handler (Yes, that's all):
private async void Button_Clicked(object sender, EventArgs e)
{
var progress = new Progress<string>(s => label.Text = s);
await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
TaskCreationOptions.LongRunning);
label.Text = "completed";
}
Implementation of the second thread that notifies the UI thread:
class SecondThreadConcern
{
public static void LongWork(IProgress<string> progress)
{
// Perform a long running work...
for (var i = 0; i < 10; i++)
{
Task.Delay(500).Wait();
progress.Report(i.ToString());
}
}
}
请注意以下内容:
如果需要更加详细的示例,请参阅C#的未来:等待者皆有好处,作者为Joseph Albahari。
另请查看UI线程模型概念。
以下代码段是如何处理异常并切换按钮的Enabled
属性以防止在后台执行期间出现多次点击的示例。
private async void Button_Click(object sender, EventArgs e)
{
button.Enabled = false;
try
{
var progress = new Progress<string>(s => button.Text = s);
await Task.Run(() => SecondThreadConcern.FailingWork(progress));
button.Text = "Completed";
}
catch(Exception exception)
{
button.Text = "Failed: " + exception.Message;
}
button.Enabled = true;
}
class SecondThreadConcern
{
public static void FailingWork(IProgress<string> progress)
{
progress.Report("I will fail in...");
Task.Delay(500).Wait();
for (var i = 0; i < 3; i++)
{
progress.Report((3 - i).ToString());
Task.Delay(500).Wait();
}
throw new Exception("Oops...");
}
}
SecondThreadConcern.LongWork()
抛出异常,能否被 UI 线程捕获?顺便说一句,这是一篇很棒的文章。 - kdbanmancontrol.Invoke((MethodInvoker) (() => control.Text = "new text"));
或者使用 Action 委托:
control.Invoke(new Action(() => control.Text = "new text"));
请参考以下内容,比较两者的差异:MethodInvoker与Action对于Control.BeginInvoke的使用
this.refresh()
来强制使GUI无效并重新绘制它。如果有帮助的话。 - Rakibul Haq针对 .NET 3.5+ 的一种“快速执行,不等待返回结果”的扩展方法
using System;
using System.Windows.Forms;
public static class ControlExtensions
{
/// <summary>
/// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
/// </summary>
/// <param name="control"></param>
/// <param name="code"></param>
public static void UIThread(this Control @this, Action code)
{
if (@this.InvokeRequired)
{
@this.BeginInvoke(code);
}
else
{
code.Invoke();
}
}
}
您可以使用以下代码调用此函数:
this.UIThread(() => this.myLabel.Text = "Text Goes Here");
@this
只是变量名,在这种情况下它引用当前调用扩展的控件的引用。你可以将其重命名为source或者其他任何名称。我使用@this
,因为它指的是调用该扩展的"此控件"并且与在正常(非扩展)代码中使用关键字"this"是一致的(至少在我的头脑中)。 - StyxRiverOnUIThread
而不是UIThread
。 - ToolmakerSteveusing System;
using System.Windows.Forms;
using System.Threading;
namespace Test
{
public partial class UIThread : Form
{
Worker worker;
Thread workerThread;
public UIThread()
{
InitializeComponent();
worker = new Worker();
worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
workerThread = new Thread(new ThreadStart(worker.StartWork));
workerThread.Start();
}
private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
{
// Cross thread - so you don't get the cross-threading exception
if (this.InvokeRequired)
{
this.BeginInvoke((MethodInvoker)delegate
{
OnWorkerProgressChanged(sender, e);
});
return;
}
// Change control
this.label1.Text = e.Progress;
}
}
public class Worker
{
public event EventHandler<ProgressChangedArgs> ProgressChanged;
protected void OnProgressChanged(ProgressChangedArgs e)
{
if(ProgressChanged!=null)
{
ProgressChanged(this,e);
}
}
public void StartWork()
{
Thread.Sleep(100);
OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
Thread.Sleep(100);
}
}
public class ProgressChangedArgs : EventArgs
{
public string Progress {get;private set;}
public ProgressChangedArgs(string progress)
{
Progress = progress;
}
}
}
Control.Invoke
。void DoSomething()
{
if (InvokeRequired) {
Invoke(new MethodInvoker(updateGUI));
} else {
// Do Something
updateGUI();
}
}
void updateGUI() {
// update gui here
}
编写线程代码经常会出现错误,而且很难进行测试。你不需要编写线程代码来从后台任务更新用户界面。只需要使用BackgroundWorker类来运行任务和它的ReportProgress方法来更新用户界面。通常只需报告完成百分比,但还有另一个包括状态对象的重载。下面是一个仅报告字符串对象的示例:
private void button1_Click(object sender, EventArgs e)
{
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(5000);
backgroundWorker1.ReportProgress(0, "A");
Thread.Sleep(5000);
backgroundWorker1.ReportProgress(0, "B");
Thread.Sleep(5000);
backgroundWorker1.ReportProgress(0, "C");
}
private void backgroundWorker1_ProgressChanged(
object sender,
ProgressChangedEventArgs e)
{
label1.Text = e.UserState.ToString();
}
如果您只想更新同一个字段,这样做是可以的。如果您需要进行更复杂的更新操作,您可以定义一个类来表示UI状态并将其传递给ReportProgress方法。
最后,请确保设置WorkerReportsProgress标志,否则ReportProgress方法将被完全忽略。
backgroundWorker1_RunWorkerCompleted
更新用户界面。 - DavidRR绝大多数答案使用 Control.Invoke
,这是一个等待发生竞争条件的情况。例如,请考虑接受的答案:
string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate {
someLabel.Text = newText; // runs on UI thread
});
如果用户在调用this.Invoke
之前关闭了表单(记住,this
是Form
对象),那么很可能会触发一个ObjectDisposedException
异常。
解决方案是使用SynchronizationContext
,特别是像hamilton.danielb建议的SynchronizationContext.Current
(其他答案依赖于特定的SynchronizationContext
实现,这是完全不必要的)。我会稍微修改他的代码来使用SynchronizationContext.Post
而不是SynchronizationContext.Send
(因为通常不需要让工作线程等待):
public partial class MyForm : Form
{
private readonly SynchronizationContext _context;
public MyForm()
{
_context = SynchronizationContext.Current
...
}
private MethodOnOtherThread()
{
...
_context.Post(status => someLabel.Text = newText,null);
}
}
请注意,在.NET 4.0及以上版本中,您应该使用任务来执行异步操作。请参阅n-san的答案,了解等效的基于任务的方法(使用TaskScheduler.FromCurrentSynchronizationContext
)。Progress<T>
(基本上在创建时捕获SynchronizationContext.Current
),如Ryszard Dżegan's所演示的那样,用于需要同时运行UI代码和长时间运行操作的情况。Myform
中的变量,如果它被处理掉了,你的解决方案也会失败,对吗? - AaAMyForm
对 _context
没有任何影响,它仍然是 SynchronizationContext.Current
(只要 WinForms 应用程序在运行,它始终有效)。 - Ohad Schneiderif (someLabel.IsDisposed) { someLabel.Text = newText }
(安全,因为此时您正在UI线程上运行,具体地说是阻止消息泵释放标签)。 - Ohad Schneiderif( MyEvent != null )
{
Delegate[] eventHandlers = MyEvent.GetInvocationList();
foreach( Delegate d in eventHandlers )
{
// Check whether the target of the delegate implements
// ISynchronizeInvoke (Winforms controls do), and see
// if a context-switch is required.
ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;
if( target != null && target.InvokeRequired )
{
target.Invoke (d, ... );
}
else
{
d.DynamicInvoke ( ... );
}
}
}
请注意,上述代码无法在WPF项目上运行,因为WPF控件没有实现ISynchronizeInvoke
接口。
为了确保上述代码适用于Windows Forms和WPF以及所有其他平台,您可以查看AsyncOperation
、AsyncOperationManager
和SynchronizationContext
类。
为了以这种方式轻松地触发事件,我创建了一个扩展方法,使我可以通过简单地调用来简化引发事件:
MyEvent.Raise(this, EventArgs.Empty);
当然,你也可以使用 BackGroundWorker 类,它将为你抽象这个问题。