从另一个线程更新标签的最简单方法是什么?
我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。
我怎么能这样做?
从另一个线程更新标签的最简单方法是什么?
我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。
我怎么能这样做?
当前回答
处理长工作
由于.NET 4.5和C#5.0,您应该在所有领域(包括GUI)使用基于任务的异步模式(TAP)和异步等待关键字:
TAP是新开发推荐的异步设计模式
而不是异步编程模型(APM)和基于事件的异步模式(EAP)(后者包括BackgroundWorker类)。
那么,新开发的建议解决方案是:
事件处理程序的异步实现(是的,仅此而已):私有异步void Button_Clicked(对象发送方,EventArgs e){var progress=new progress<string>(s=>label.Text=s);await Task.Factory.StartNew(()=>SecondThreadConcern.LongWork(进度),TaskCreationOptions.LongRunning);label.Text=“已完成”;}通知UI线程的第二个线程的实现:类SecondThreadConcern{公共静态void LongWork(IProgress<string>进度){//进行长时间的工作。。。对于(变量i=0;i<10;i++){任务.延迟(500).等待();progress.Report(i.ToString());}}}
请注意以下事项:
以顺序方式编写的简短而干净的代码,没有回调和显式线程。任务而不是线程。async关键字,它允许使用await,从而防止事件处理程序在任务完成之前达到完成状态,同时不会阻塞UI线程。进程类(参见IProgress接口),支持关注点分离(SoC)设计原则,不需要显式调度程序和调用。它使用创建位置(此处为UI线程)的当前SynchronizationContext。TaskCreationOptions.LongRunning,提示不要将任务排入ThreadPool。
有关更详细的示例,请参阅约瑟夫·阿尔巴哈里(Joseph Albahari)的《C#的未来:美好的事物降临在那些“等待”的人身上》。
另请参见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...");
}
}
其他回答
必须使用invoke和delegate
private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
一般方法如下:
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
int clickCount = 0;
public Form1()
{
InitializeComponent();
label1.SetText("0");
}
private void button1_Click(object sender, EventArgs e)
{
new Thread(() => label1.SetText((++clickCount).ToString())).Start();
}
}
public static class ControlExtensions
{
public static void SetText(this Control control, string text)
{
if (control.InvokeRequired)
control.Invoke(setText, control, text);
else
control.Text = text;
}
private static readonly Action<Control, string> setText =
(control, text) => control.Text = text;
}
}
说明:
答案很像这个。但使用了更整洁(对我来说)和更新的语法。点是控件的InvokeRequired属性。它获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用调用方法,因为调用方所在的线程与创建控件的线程不同。因此,如果我们在创建控件的同一线程上调用control.SetText(“一些文本”),则只需将text设置为此控件即可。text=text。但在任何其他线程上,它都会导致System.InvalidOperationException,因此必须通过控件调用方法。Invoke(…)在创建的线程控件上设置Text。
也许有点过量,但这是我通常解决问题的方式:
由于同步,此处不需要调用。BasicClassThreadExample对我来说只是一种布局,因此请根据您的实际需要进行更改。
这很简单,因为您不需要处理UI线程中的内容!
public partial class Form1 : Form
{
BasicClassThreadExample _example;
public Form1()
{
InitializeComponent();
_example = new BasicClassThreadExample();
_example.MessageReceivedEvent += _example_MessageReceivedEvent;
}
void _example_MessageReceivedEvent(string command)
{
listBox1.Items.Add(command);
}
private void button1_Click(object sender, EventArgs e)
{
listBox1.Items.Clear();
_example.Start();
}
}
public class BasicClassThreadExample : IDisposable
{
public delegate void MessageReceivedHandler(string msg);
public event MessageReceivedHandler MessageReceivedEvent;
protected virtual void OnMessageReceivedEvent(string msg)
{
MessageReceivedHandler handler = MessageReceivedEvent;
if (handler != null)
{
handler(msg);
}
}
private System.Threading.SynchronizationContext _SynchronizationContext;
private System.Threading.Thread _doWorkThread;
private bool disposed = false;
public BasicClassThreadExample()
{
_SynchronizationContext = System.ComponentModel.AsyncOperationManager.SynchronizationContext;
}
public void Start()
{
_doWorkThread = _doWorkThread ?? new System.Threading.Thread(dowork);
if (!(_doWorkThread.IsAlive))
{
_doWorkThread = new System.Threading.Thread(dowork);
_doWorkThread.IsBackground = true;
_doWorkThread.Start();
}
}
public void dowork()
{
string[] retval = System.IO.Directory.GetFiles(@"C:\Windows\System32", "*.*", System.IO.SearchOption.TopDirectoryOnly);
foreach (var item in retval)
{
System.Threading.Thread.Sleep(25);
_SynchronizationContext.Post(new System.Threading.SendOrPostCallback(delegate(object obj)
{
OnMessageReceivedEvent(item);
}), null);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
_doWorkThread.Abort();
}
disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~BasicClassThreadExample() { Dispose(false); }
}
我刚读了答案,这似乎是一个非常热门的话题。我目前正在使用.NET 3.5 SP1和Windows窗体。
前面的答案中大量描述的使用InvokeRequired属性的著名公式涵盖了大多数情况,但不包括整个池。
如果尚未创建句柄怎么办?
InvokeRequired属性,如这里所述(Control.InvokeRequired属性对MSDN的引用),如果调用是从非GUI线程的线程进行的,则返回true;如果调用是由GUI线程进行的或尚未创建Handle,则返回false。
如果希望由另一个线程显示和更新模态表单,则可能会遇到异常。因为您希望以模态方式显示该表单,所以可以执行以下操作:
private MyForm _gui;
public void StartToDoThings()
{
_gui = new MyForm();
Thread thread = new Thread(SomeDelegate);
thread.Start();
_gui.ShowDialog();
}
代理可以更新GUI上的标签:
private void SomeDelegate()
{
// Operations that can take a variable amount of time, even no time
//... then you update the GUI
if(_gui.InvokeRequired)
_gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
else
_gui.Label1.Text = "Done!";
}
如果标签更新之前的操作“花费的时间”(阅读并将其解释为简化)少于GUI线程创建表单句柄所花费的时间,则这可能会导致InvalidOperationException。这发生在ShowDialog()方法中。
您还应检查手柄,如下所示:
private void SomeDelegate()
{
// Operations that can take a variable amount of time, even no time
//... then you update the GUI
if(_gui.IsHandleCreated) // <---- ADDED
if(_gui.InvokeRequired)
_gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
else
_gui.Label1.Text = "Done!";
}
如果尚未创建handle,您可以处理要执行的操作:您可以忽略GUI更新(如上面的代码所示),也可以等待(风险更大)。这应该能回答这个问题。
可选材料:就我个人而言,我编写了以下代码:
public class ThreadSafeGuiCommand
{
private const int SLEEPING_STEP = 100;
private readonly int _totalTimeout;
private int _timeout;
public ThreadSafeGuiCommand(int totalTimeout)
{
_totalTimeout = totalTimeout;
}
public void Execute(Form form, Action guiCommand)
{
_timeout = _totalTimeout;
while (!form.IsHandleCreated)
{
if (_timeout <= 0) return;
Thread.Sleep(SLEEPING_STEP);
_timeout -= SLEEPING_STEP;
}
if (form.InvokeRequired)
form.Invoke(guiCommand);
else
guiCommand();
}
}
我用这个ThreadSafeGuidCommand的一个实例为另一个线程更新的表单提供数据,并定义如下更新GUI(在我的表单中)的方法:
public void SetLabeTextTo(string value)
{
_threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}
通过这种方式,我很确定无论什么线程进行调用,我都会更新GUI,可以选择等待一段明确定义的时间(超时)。
这是一个古老问题的新视角,使用了更实用的风格。如果您在所有项目中都保留TaskXM类,那么只有一行代码不再担心跨线程更新。
public class Example
{
/// <summary>
/// No more delegates, background workers, etc. Just one line of code as shown below.
/// Note it is dependent on the Task Extension method shown next.
/// </summary>
public async void Method1()
{
// Still on the GUI thread here if the method was called from the GUI thread
// This code below calls the extension method which spins up a new task and calls back.
await TaskXM.RunCodeAsync(() =>
{
// Running an asynchronous task here
// Cannot update the GUI thread here, but can do lots of work
});
// Can update GUI on this line
}
}
/// <summary>
/// A class containing extension methods for the Task class
/// </summary>
public static class TaskXM
{
/// <summary>
/// RunCodeAsyc is an extension method that encapsulates the Task.run using a callback
/// </summary>
/// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
/// <returns></returns>
public async static Task RunCodeAsync(Action Code)
{
await Task.Run(() =>
{
Code();
});
return;
}
}