题 如何从其他线程更新GUI?


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

我有一个 Form 上 thread1从那我开始另一个线程(thread2)。而 thread2 正在处理我想要更新的一些文件 Label 在...上 Form 以当前的状态 thread2的工作。

我怎样才能做到这一点?


1143
2018-03-19 09:37


起源


.net 2.0+不具备BackgroundWorker类。它是UI线程感知。 1.创建BackgroundWorker 2.添加两个代理(一个用于处理,一个用于完成) - Preet Sangha
也许有点晚了: codeproject.com/KB/cs/Threadsafe_formupdating.aspx - MichaelD
请参阅.NET 4.5和C#5.0的答案: stackoverflow.com/a/18033198/2042090 - Ryszard Dżegan
此问题不适用于Gtk#GUI。对于Gtk#,请参阅 这个 和 这个 回答。 - hlovdal
注意:这个问题的答案现在是混乱的OT(“这是我为我的WPF应用程序所做的”)和历史的.NET 2.0工件。 - Marc L.


答案:


对于.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 class,这将简化调用:

myLabel.SetPropertyThreadSafe("Text", status);

更新05/10/2010:

对于.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版本好很多。

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


687
2018-03-19 10:37



有些情况下this.GetType()的计算结果与propertyInfo.ReflectedType相同(例如WinForms上的LinkLabel)。我没有大的C#经验,但我认为异常的条件应该是:if(propertyInfo == null ||(!@ this.GetType()。IsSubclassOf(propertyInfo.ReflectedType)&& @ this.GetType( )!= propertyInfo.ReflectedType)|| @ this.GetType()。GetProperty(propertyInfo.Name,propertyInfo.PropertyType)== null) - Corvin
@lan可以这样 SetControlPropertyThreadSafe(myLabel, "Text", status) 从另一个模块或类或形式调用 - Smith
提供的解决方案不必要地复杂。如果您重视简洁,请参阅Marc Gravell的解决方案或Zaid Masud的解决方案。 - Frank Hileman
如果您更新多个属性,则此解决方案会浪费大量资源,因为每个Invoke都需要花费大量资源。我不认为这是线程安全的特性无论如何都是如此。对您的UI更新操作进行封装并将其调用ONCE(而不是每个属性) - Console
为什么你会在BackgroundWorker组件上使用这个代码呢? - Andy


简单 way是传入的匿名方法 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上的内容 关于在想要了解它时编写异步代码。


938
2018-03-19 10:17



看到OP没有提到任何类/实例 除 形式,这不是一个错误的默认... - Marc Gravell♦
不要忘记“this”关键字引用了“Control”类。 - AZ.
@codecompleting它是安全的,我们已经知道我们是一个工人,所以为什么要检查一下我们知道的东西? - Marc Gravell♦
@Dragouf并不是真的 - 使用这种方法的一个原因是你已经知道哪些部分在worker上运行,哪些部分在UI线程上运行。无需检查。 - Marc Gravell♦
@ Joan.bdm我没有足够的背景来评论这个问题 - Marc Gravell♦


处理长期工作

以来 .NET 4.5和C#5.0 你应该使用 基于任务的异步模式(TAP) 随着 异步 - 等待 关键字 在所有领域 (包括GUI):

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. 任务 代替 线
  3. 异步 关键字,允许使用 等待 这反过来阻止事件处理程序达到完成状态,直到任务完成,同时不阻止UI线程。
  4. 进步课(见 IProgress接口)支持 关注点分离(SoC) 设计原则,不需要显式调度和调用。它使用当前 的SynchronizationContext 从它的创建地点(这里是UI线程)。
  5. TaskCreationOptions.LongRunning 提示不将任务排入队列 线程池

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

334
2017-08-03 13:09



如果 SecondThreadConcern.LongWork() 抛出一个异常,是否可以被UI线程捕获?这是一个很棒的帖子,顺便说一下。 - kdbanman
我在答案中添加了一个额外的部分以满足您的要求。问候。 - Ryszard Dżegan
该 ExceptionDispatchInfo类 负责在async-await模式中重新抛出UI线程上的后台异常的奇迹。 - Ryszard Dżegan
难道只是我在想这种做法比仅仅调用Invoke / Begin更加冗长吗?! - Marco
Task.Delay(500).Wait()?创建一个只阻止当前线程的Task有什么意义?你永远不应该阻止一个线程池线程! - Yarik


变异 Marc Gravell的 简单 解 对于.NET 4:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

或者使用Action委托代替:

control.Invoke(new Action(() => control.Text = "new text"));

请看这里比较两者: MethodInvoker与Control.BeginInvoke的Action


195
2018-05-29 18:51



这个例子中的“控制”是什么?我的UI控件?试图在标签控件上的WPF中实现它,并且Invoke不是我的标签的成员。 - Dbloom
怎么样? 扩展方法 喜欢@styxriver stackoverflow.com/a/3588137/206730 ? - Kiquenet
宣布'行动y;'在类或方法中更改text属性并使用这段代码更新文本'yourcontrol.Invoke(y =()=> yourcontrol.Text =“new text”);' - Antonio Leite
@Dbloom它不是会员,因为它只适用于WinForms。对于WPF,您可以使用Dispatcher.Invoke - sLw
我正在关注此解决方案,但有时我的UI没有得到更新。我发现我需要 this.refresh() 强制使GUI无效并重新绘制..如果它有用.. - Rakibul Haq


.NET 3.5+的Fire and forget扩展方法

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

115
2017-08-27 21:10



这个用法有什么意义? “控制”不等同吗? @this有什么好处吗? - jeromeyers
@jeromeyers - The @this 只是变量名,在这种情况下是对调用扩展名的当前控件的引用。您可以将其重命名为source,或者将您的船浮起来。我用 @this,因为它指的是调用扩展名的'this Control',并且在普通(非扩展)代码中使用'this'关键字是一致的(至少在我脑海中)。 - StyxRiver
这很棒,很容易,对我来说是最好的解决方案。您可以在ui线程中包含您必须完成的所有工作。示例:this.UIThread(()=> {txtMessage.Text = message; listBox1.Items.Add(message);}); - Auto
我非常喜欢这个解决方案。小尼特:我会说出这种方法 OnUIThread 而不是 UIThread。 - ToolmakerSteve
@ToolmakerSteve - 我选择的唯一原因 不 使用 OnXXX 风格是因为这通常表示事件超载。 OnLoad, OnInitialize等等也许不是最好的决定,但这是我当时的想法。哇,差不多6年前到现在。时光飞逝。 - StyxRiver


这是你应该这样做的经典方式:

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线程从另一个线程开始执行工作并挂接该工作事件,以便您可以显示工作线程的状态。

然后在UI中,您需要跨线程来更改实际控件...如标签或进度条。


57
2018-03-19 10:31





简单的解决方案是使用 Control.Invoke

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}

51
2018-03-19 09:46



这并不完美。更新任何控件时,另一个控件不响应.. - Ahosan Karim Asik
为简单而做得很好!不仅简单,而且效果很好!我真的不明白为什么微软不能让它更简单,因为它本来就是!为了在主线程上调用1行,我们应该编写几个函数! - MBH
@MBH同意。顺便说一句,你注意到了吗? stackoverflow.com/a/3588137/199364 回答上面,它定义了一个扩展方法?在自定义实用程序类中执行一次,然后不必再关心Microsoft没有为我们执行此操作:) - ToolmakerSteve
@ToolmakerSteve这就是它的意思!你是对的,我们可以找到一种方法,但我的意思是从DRY(不要重复自己)的观点来看,这个有共同解决方案的问题,可以用微软最小的努力来解决,这样可以节省大量的时间。程序员:) - MBH


线程代码经常出错并且总是难以测试。您无需编写线程代码来从后台任务更新用户界面。只需使用 的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 方法将被完全忽略。


41
2017-08-27 20:42



在处理结束时,还可以通过更新用户界面 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特别是 SynchronizationContext.Current 如 hamilton.danielb 建议(其他答案依赖于具体的 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及更高版本中,您应该使用异步操作的任务。看到 正桑的 回答基于任务的等效方法(使用 TaskScheduler.FromCurrentSynchronizationContext)。

最后,在.NET 4.5及更高版本上,您也可以使用 Progress<T> (基本上捕获 SynchronizationContext.Current 正如其所表明的那样 RyszardDżegan的 对于长时间运行的操作需要在仍然工作时运行UI代码的情况。


31
2018-05-24 08:45





您必须确保更新发生在正确的线程上; 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项目,因为WPF控件不实现 ISynchronizeInvoke 接口。

为了确保上面的代码适用于Windows窗体和WPF以及所有其他平台,您可以查看 AsyncOperationAsyncOperationManager 和 SynchronizationContext 类。

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

MyEvent.Raise(this, EventArgs.Empty);

当然,您也可以使用BackGroundWorker类,它将为您抽象出这个问题。


30
2018-03-19 09:45



确实,但我不喜欢用这个问题“混乱”我的GUI代码。我的GUI不应该关心它是否需要调用。换句话说:我认为执行context-swithc不是GUI的责任。 - Frederik Gheysels
打破代表等等似乎有点过分 - 为什么不只是:SynchronizationContext.Current.Send(委托{MyEvent(...);},null); - Marc Gravell♦
您是否始终可以访问SynchronizationContext?即使你的班级是在一个类lib? - Frederik Gheysels