c# - update - winform invoke ui thread




我如何從另一個線程更新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。


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

由於場景的微不足道,我實際上會對UI狀態進行UI線程輪詢。 我想你會發現它可以很優雅。

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

該方法避免了使用ISynchronizeInvoke.InvokeISynchronizeInvoke.BeginInvoke方法時所需的封送處理操作。 使用編組技術沒有任何問題,但有一些注意事項需要注意。

  • 確保你不會過於頻繁地調用BeginInvoke或者它可能會溢出消息泵。
  • 在工作線程上調用Invoke是一個阻塞調用。 它會暫時停止正在該線程中完成的工作。

我在這個答案中提出的策略顛倒了線程的溝通角色。 而不是工作線程推送數據的UI線程輪詢它。 這是許多情況下使用的常見模式。 既然你想要做的就是顯示來自工作線程的進度信息,那麼我認為你會發現這個解決方案是編組解決方案的一個很好的選擇。 它具有以下優點。

  • UI和工作線程保持鬆散耦合,而不是緊密耦合它們的Control.InvokeControl.BeginInvoke方法。
  • UI線程不會妨礙工作線程的進度。
  • 工作線程不能支配UI線程花費更新的時間。
  • UI和工作線程執行操作的間隔可以保持獨立。
  • 工作線程不能溢出UI線程的消息泵。
  • UI線程可以指定UI何時更新以及更新的頻率。

簡單的解決方案是使用Control.Invoke

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

void updateGUI() {
    // update gui here
}

絕大多數答案使用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 ,特別是SynchronizationContext ,正如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及更高版本中,您應該真正使用異步操作的任務。 請參閱n-san's針對基於等效任務的方法n-san's回答(使用TaskScheduler.FromCurrentSynchronizationContext )。

最後,在.NET 4.5及更高版本中,您還可以使用Progress<T> (基本上在創建SynchronizationContext.Current時捕獲它),如RyszardDżegan所示 ,其中長期運行的操作需要在運行時繼續運行UI代碼。


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

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


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




user-interface