从另一个线程更新标签的最简单方法是什么?

我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。

我怎么能这样做?


当前回答

我认为最简单的方法是:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

其他回答

我的版本是插入一行递归的“咒语”:

对于无参数:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

对于具有参数的函数:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

就是这样。


一些论证:通常在if()语句后一行放置{}对代码可读性不利。但在这种情况下,这是例行的“咒语”。如果此方法在项目中保持一致,则不会破坏代码的可读性。它还可以避免代码乱丢(一行代码而不是五行代码)。

当您看到(InvokeRequired){some long}时,您只知道“从另一个线程调用此函数是安全的”。

绝大多数答案使用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代码的情况。

出于许多目的,它非常简单:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

“serviceGUI()”是表单(this)中的GUI级方法,可以根据需要更改任意数量的控件。从另一个线程调用“updateGUI()”。可以添加参数来传递值,或者(可能更快)根据需要使用带有锁的类范围变量,如果访问这些变量的线程之间可能发生冲突,从而导致不稳定。如果非GUI线程是时间关键的(请记住BrianGideon的警告),请使用BeginInvoke而不是Invoke。

当我遇到同样的问题时,我向谷歌寻求帮助,但它并没有给我一个简单的解决方案,而是举了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(..)方法,我不打算在这里解释。

线程代码经常有bug,而且总是很难测试。您不需要编写线程代码来从后台任务更新用户界面。只需使用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方法将被完全忽略。