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

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

我怎么能这样做?


当前回答

只需使用以下内容:

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

其他回答

Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

请注意,BeginInvoke()比Invoke(()更受欢迎,因为它不太可能导致死锁(然而,在这里,将文本分配给标签时,这不是问题):

使用Invoke()时,您正在等待方法返回。现在,可能是您在调用的代码中执行了一些需要等待线程的操作,如果它隐藏在您正在调用的某些函数中,这可能不会立即显现出来,而这本身可能会通过事件处理程序间接发生。因此,您将等待线程,线程将等待您,并且您处于死锁状态。

这实际上导致了我们发布的一些软件挂起。用BeginInvoke()替换Invoke(。除非需要同步操作(如果需要返回值,则可能是这种情况),否则请使用BeginInvoke()。

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

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

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

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

我无法理解这一丑陋实现背后的微软逻辑,但你必须有两个功能:

void setEnableLoginButton()
{
  if (InvokeRequired)
  {
    // btn_login can be any conroller, (label, button textbox ..etc.)

    btn_login.Invoke(new MethodInvoker(setEnable));

    // OR
    //Invoke(new MethodInvoker(setEnable));
  }
  else {
    setEnable();
  }
}

void setEnable()
{
  btn_login.Enabled = isLoginBtnEnabled;
}

这些片段对我有用,所以我可以在另一个线程上执行一些操作,然后更新GUI:

Task.Factory.StartNew(()=>
{
    // THIS IS NOT GUI
    Thread.Sleep(5000);
    // HERE IS INVOKING GUI
    btn_login.Invoke(new Action(() => DoSomethingOnGUI()));
});

private void DoSomethingOnGUI()
{
   // GUI
   MessageBox.Show("message", "title", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}

更简单的是:

btn_login.Invoke(new Action(()=>{ /* HERE YOU ARE ON GUI */ }));

这是您应该采用的经典方法:

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