c#子线程更新ui




我如何从另一个线程更新GUI? (20)

处理长时间工作

.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. Task而不是Thread
  3. async关键字,它允许使用await ,从而阻止事件处理程序达到完成状态,直到任务完成并且同时不阻止UI线程。
  4. 支持分离关注(SoC)设计原则的Progress类(参见IProgress接口 ),不需要显式调度器和调用。 它从创建位置(这里是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...");
    }
}

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

我在thread1上有一个Form ,然后从另一个线程( thread2 )开始。 虽然thread2正在处理一些文件,但我想用thread2的当前状态更新Form上的Label

我怎样才能做到这一点?


Salvete! 找到这个问题后,我发现FrankGOregon Ghost的答案对我来说是最简单最有用的。 现在,我在Visual Basic中编写代码并通过转换器运行此代码段; 所以我不确定事情结果如何。

我有一个名为form_Diagnostics,的对话框form_Diagnostics,它有一个名为updateDiagWindow, form_Diagnostics,框,我用它作为一种日志显示。 我需要能够从所有线程更新其文本。 额外的行允许窗口自动滚动到最新的行。

因此,我现在可以在整个程序中的任何位置以一行代码更新显示,并且您认为它可以在没有任何线程的情况下运行:

  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

最简单的方法是传递给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阻止执行直到它完成 - 这是同步代码。 这个问题没有询问关于异步代码的问题,但是当你想了解它时,上有很多关于编写异步代码的内容。


Create a class variable:

SynchronizationContext _context;

Set it in the constructor that creates your UI:

var _context = SynchronizationContext.Current;

When you want to update the label:

_context.Send(status =>{
    // UPDATE LABEL
}, null);

For example, access a control other than in the current thread:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

There the lblThreshold is a Label and Speed_Threshold is a global variable.


I just read the answers and this appears to be a very hot topic. I'm currently using .NET 3.5 SP1 and Windows Forms.

The well-known formula greatly described in the previous answers that makes use of the InvokeRequired property covers most of the cases, but not the entire pool.

What if the Handle has not been created yet?

The InvokeRequired property, as described here (Control.InvokeRequired Property reference to MSDN) returns true if the call was made from a thread that is not the GUI thread, false either if the call was made from the GUI thread, or if the Handle was not created yet.

You can come across an exception if you want to have a modal form shown and updated by another thread. Because you want that form shown modally, you could do the following:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

And the delegate can update a Label on the 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!";
}

This can cause an InvalidOperationException if the operations before the label's update "take less time" (read it and interpret it as a simplification) than the time it takes for the GUI thread to create the Form 's Handle . This happens within the ShowDialog() method.

You should also check for the Handle like this:

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!";
}

You can handle the operation to perform if the Handle has not been created yet: You can just ignore the GUI update (like shown in the code above) or you can wait (more risky). This should answer the question.

Optional stuff: Personally I came up coding the following:

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

I feed my forms that get updated by another thread with an instance of this ThreadSafeGuiCommand , and I define methods that update the GUI (in my Form) like this:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

In this way I'm quite sure that I will have my GUI updated whatever thread will make the call, optionally waiting for a well-defined amount of time (the timeout).


My version is to insert one line of recursive "mantra":

For no arguments:

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

        // Your code!
    }

For a function that has arguments:

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

THAT is IT .

Some argumentation : Usually it is bad for code readability to put {} after an if () statement in one line. But in this case it is routine all-the-same "mantra". It doesn't break code readability if this method is consistent over the project. And it saves your code from littering (one line of code instead of five).

As you see if(InvokeRequired) {something long} you just know "this function is safe to call from another thread".


Simply use something like this:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

Try to refresh the label using this

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}

You may use the already-existing delegate Action :

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

你必须确保更新发生在正确的线程上; 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以及所有其他平台一起工作,您可以查看AsyncOperationAsyncOperationManagerSynchronizationContext类。

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

MyEvent.Raise(this, EventArgs.Empty);

当然,你也可以使用BackGroundWorker类,它会为你抽象这个问题。


在Ian Kemp解决方案的C#3.0变体中,

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

你这样称呼它:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. 它将“作为MemberExpression”的结果添加空检查。
  2. 它提高了静态型安全性。

否则,原来是一个非常好的解决方案。


对于.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);

UPDATE 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 ||
      [email protected]().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版本好很多。

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


对于很多目的来说,它就像这样简单:

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

“serviceGUI()”是窗体(this)中的GUI级别的方法,可以根据需要更改任意数量的控件。 从另一个线程调用“updateGUI()”。 可以添加参数来传递值,或者(可能更快)使用类作用域变量,并根据需要锁定它们,如果访问它们的线程之间有可能导致冲突而导致不稳定。 如果非GUI线程时间关键(使用Brian Gideon的警告),请使用BeginInvoke而不是Invoke。


您需要在GUI线程上调用该方法。 你可以通过调用Control.Invoke来做到这一点。

例如:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}

火,忘了.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");

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


这与使用.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");

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

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中,您需要通过线程来更改实际控件......如标签或进度条。





user-interface