从另一个线程更新标签的最简单方法是什么?
我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。
我怎么能这样做?
从另一个线程更新标签的最简单方法是什么?
我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。
我怎么能这样做?
当前回答
基本上,无论框架版本或GUI底层库类型如何,解决此问题的方法都是保存控件,为工作线程创建线程的同步上下文,从而将控件的相关交互从工作线程编组到GUI的线程消息队列。
例子:
SynchronizationContext ctx = SynchronizationContext.Current; // From control
ctx.Send\Post... // From worker thread
其他回答
当我遇到同样的问题时,我向谷歌寻求帮助,但它并没有给我一个简单的解决方案,而是举了MethodInvoker和blah blah的例子,让我更加困惑。所以我决定自己解决。以下是我的解决方案:
制作一个这样的代理:
Public delegate void LabelDelegate(string s);
void Updatelabel(string text)
{
if (label.InvokeRequired)
{
LabelDelegate LDEL = new LabelDelegate(Updatelabel);
label.Invoke(LDEL, text);
}
else
label.Text = text
}
您可以像这样在新线程中调用此函数
Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();
不要与线程(()=>…..)混淆。在处理线程时,我使用匿名函数或lambda表达式。为了减少代码行,您也可以使用ThreadStart(..)方法,我不打算在这里解释。
这一解决方案类似于上面使用.NET Framework 3.0的解决方案,但它解决了编译时安全支持的问题。
public static class ControlExtension
{
delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);
public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
{
if (source.InvokeRequired)
{
var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
source.Invoke(del, new object[]{ source, selector, value});
}
else
{
var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
propInfo.SetValue(source, value, null);
}
}
}
要使用:
this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);
如果用户传递错误的数据类型,编译器将失败。
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
致敬!在搜索了这个问题之后,我发现FrankG和Oregon Ghost的答案对我来说是最简单、最有用的;所以我不太确定结果如何。
我有一个名为form_Diagnostics的对话框,它有一个称为updateDiagWindow的富文本框,我正在使用它作为一种日志显示。我需要能够从所有线程更新它的文本。额外的行允许窗口自动滚动到最新的行。
因此,我现在可以从整个程序的任何地方,以您认为无需任何线程的方式,用一行代码更新显示:
form_Diagnostics.updateDiagWindow(whatmessage);
主代码(将其放入表单的类代码中):
#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
var _with1 = diagwindow;
if (_with1.InvokeRequired) {
_with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
} else {
UpdateDiag(whatmessage);
}
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
var _with2 = diagwindow;
_with2.appendtext(whatmessage);
_with2.SelectionStart = _with2.Text.Length;
_with2.ScrollToCaret();
}
#endregion
绝大多数答案使用Control.Invoke,这是等待发生的竞争条件。例如,考虑接受的答案:
string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate {
someLabel.Text = newText; // runs on UI thread
});
如果用户在调用this.Invoke之前关闭表单(请记住,这是form对象),则可能会触发ObjectDisposedException。
解决方案是使用SynchronizationContext,特别是hamilton.danielb建议的SynchronizaionContext.Current(其他答案依赖于特定的Synchronization Context实现,这是完全不必要的)。我会稍微修改他的代码,使用SynchronizationContext.Post而不是SynchronizazationContext.Send(因为通常不需要工作线程等待):
public partial class MyForm : Form
{
private readonly SynchronizationContext _context;
public MyForm()
{
_context = SynchronizationContext.Current
...
}
private MethodOnOtherThread()
{
...
_context.Post(status => someLabel.Text = newText,null);
}
}
请注意,在.NET4.0及更高版本上,您应该真正将任务用于异步操作。有关等效的基于任务的方法(使用TaskScheduler.FromCurrentSynchronizationContext),请参见n-san的答案。
最后,在.NET 4.5及更高版本上,您还可以使用Progress<T>(它基本上在创建时捕获SynchronizationContext.Current),正如Ryszard Dżegan所演示的,用于长时间运行的操作需要在运行时运行UI代码的情况。
Marc Gravell的.NET 4最简单解决方案的变体:
control.Invoke((MethodInvoker) (() => control.Text = "new text"));
或者改用Action委托:
control.Invoke(new Action(() => control.Text = "new text"));
请参阅此处了解两者的比较:MethodInvoker与Action for Control.BeginInvoke