如何从另一个线程更新 GUI?

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

我在thread1上有一个Formthread1开始,我开始了另一个线程( thread2 )。当thread2处理某些文件时,我想使用thread2的当前状态更新Form上的Label

我怎样才能做到这一点?

答案

最简单的方法是将匿名方法传递给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 表达式允许更简洁,更简单和更安全的语法:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

现在不仅在编译时检查属性名称,而且属性的类型也是如此,因此不可能(例如)为布尔型属性分配字符串值,从而导致运行时异常。

不幸的是,这不会阻止任何人做一些愚蠢的事情,例如传递另一个Control的属性和值,因此可以很高兴地编译以下内容:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

因此,我添加了运行时检查,以确保传递的属性确实属于调用该方法的Control 。虽然不完美,但仍比. NET 2.0 版本好很多。

如果有人对如何提高此代码的编译时安全性有任何进一步的建议,请发表评论!

处理长时间的工作

.NET 4.5 和 C#5.0 开始,您应该在所有区域 (包括 GUI)中使用基于任务的异步模式(TAP)以及async - await关键字:

TAP 是新开发的推荐异步设计模式

而不是异步编程模型(APM)基于事件的异步模式(EAP) (后者包括BackgroundWorker 类 )。

然后,针对新开发的推荐解决方案是:

  1. 事件处理程序的异步实现(是的,仅此而已):

    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";
    }
  2. 通知 UI 线程的第二个线程的实现:

    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());
            }
        }
    }

请注意以下几点:

  1. 以顺序方式编写的简洁代码,没有回调和显式线程。
  2. 任务而不是Thread
  3. async关键字,该关键字允许使用await ,从而防止事件处理程序到达完成状态,直到任务完成为止,同时不会阻塞 UI 线程。
  4. Progress 类(请参阅IProgress 接口 ),它支持关注点分离(SoC)设计原理,并且不需要显式的调度程序和调用。它从创建位置(此处为 UI 线程)使用当前的SynchronizationContext
  5. TaskCreationOptions.LongRunning提示不要将任务排队到ThreadPool 中

有关更详细的示例,请参见: 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...");
    }
}

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

.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");

这是您应该执行的经典方式:

using 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;
        }
    }
}

您的工作线程中有一个事件。您的 UI 线程从另一个线程开始进行工作,并连接该工作程序事件,以便您可以显示工作程序线程的状态。

然后,在用户界面中,您需要跨线程来更改实际控件…… 例如标签或进度条。

简单的解决方案是使用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方法将被完全忽略。

绝大多数答案使用Control.Invoke ,这是一种等待发生竞赛条件 。例如,考虑接受的答案:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

如果用户在此之前关闭窗体。调用this.Invoke (请记住, thisForm对象),则可能会引发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 )。

最后,在. NET 4.5 及更高版本上,您也可以使用Progress<T> (基本上捕获SynchronizationContext.Current在创建时),这由RyszardDżegan演示,用于长时间运行的操作需要在运行时仍运行 UI 代码的情况。

您必须确保更新发生在正确的线程上。 UI 线程。

为此,您必须调用事件处理程序,而不是直接调用它。

您可以通过引发以下事件来做到这一点:

(这里输入的代码不费吹灰之力,因此我没有检查语法是否正确,等等,但是它应该可以助您一臂之力。)

if( 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 控件未实现ISynchronizeInvoke接口,因此上面的代码不适用于 WPF 项目。

为了确保上面的代码可与 Windows Forms 和 WPF 以及所有其他平台一起使用,可以查看AsyncOperationAsyncOperationManagerSynchronizationContext类。

为了以这种方式轻松引发事件,我创建了一个扩展方法,该方法允许我通过调用以下方法简化引发事件:

MyEvent.Raise(this, EventArgs.Empty);

当然,您也可以使用 BackGroundWorker 类,该类将为您抽象此问题。