[C#] 我如何從另一個線程更新GUI?


Answers

最簡單的方法是傳遞給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上有很多關於編寫異步代碼的內容。

Question

從另一個線程更新Label的最簡單方法是什麼?

我在thread1上有一個Form ,然後從另一個線程( thread2 )開始。 雖然thread2正在處理一些文件,但我想用thread2的當前狀態更新Form上的Label

我怎樣才能做到這一點?




The easiest way I think:

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



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




你必須確保更新發生在正確的線程上; 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類,它會為你抽像這個問題。




這是你應該這樣做的經典方式:

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中,您需要通過線程來更改實際控件......如標籤或進度條。




線程代碼通常很麻煩並且總是很難測試。 您無需編寫線程代碼即可從後台任務更新用戶界面。 只需使用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方法將被完全忽略。




You must use invoke and delegate

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });



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




Simply use something like this:

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



在以前的答案中沒有一個Invoke的東西是必要的。

你需要看看WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}



Even if the operation is time-consuming (thread.sleep in my example) - This code will NOT lock your UI:

 private void button1_Click(object sender, EventArgs e)
 {

      Thread t = new Thread(new ThreadStart(ThreadJob));
      t.IsBackground = true;
      t.Start();         
 }

 private void ThreadJob()
 {
     string newValue= "Hi";
     Thread.Sleep(2000); 

     this.Invoke((MethodInvoker)delegate
     {
         label1.Text = newValue; 
     });
 }



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



這與使用.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");






對於很多目的來說,它就像這樣簡單:

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

“serviceGUI()”是表單(this)中的GUI級別的方法,可以根據需要更改任意數量的控件。 從另一個線程調用“updateGUI()”。 可以添加參數來傳遞值,或者(可能更快)使用類作用域變量,並根據需要鎖定它們,如果訪問它們的線程之間有可能導致衝突而導致不穩定。 如果非GUI線程時間關鍵(使用Brian Gideon的警告),請使用BeginInvoke而不是Invoke。