C# 如何从另一个线程更新 GUI?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/661561/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 12:20:04  来源:igfitidea点击:

How do I update the GUI from another thread?

c#.netmultithreadingwinformsuser-interface

提问by CruelIO

Which is the simplest way to update a Labelfrom another Thread?

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

  • I have a Formrunning on thread1, and from that I'm starting another thread (thread2).

  • While thread2is processing some files I would like to update a Labelon the Formwith the current status of thread2's work.

  • 我有一个Form运行thread1,然后我开始另一个线程(thread2)。

  • 虽然thread2在处理一些文件,我想更新LabelForm用的当前状态thread2的工作。

How could I do that?

我怎么能那样做?

采纳答案by Ian Kemp

For .NET 2.0, here's a nice bit of code I wrote that does exactly what you want, and works for any property on a Control:

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

Call it like this:

像这样调用它:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

If you're using .NET 3.0 or above, you could rewrite the above method as an extension method of the Controlclass, which would then simplify the call to:

如果您使用的是 .NET 3.0 或更高版本,则可以将上述方法重写为类的扩展方法,Control然后将调用简化为:

myLabel.SetPropertyThreadSafe("Text", status);

UPDATE 05/10/2010:

2010 年 5 月 10 日更新:

For .NET 3.0 you should use this code:

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

which uses LINQ and lambda expressions to allow much cleaner, simpler and safer syntax:

它使用 LINQ 和 lambda 表达式来允许更清晰、更简单和更安全的语法:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

Not only is the property name now checked at compile time, the property's type is as well, so it's impossible to (for example) assign a string value to a boolean property, and hence cause a runtime exception.

现在不仅在编译时检查属性名称,属性的类型也是如此,因此不可能(例如)将字符串值分配给布尔属性,从而导致运行时异常。

Unfortunately this doesn't stop anyone from doing stupid things such as passing in another Control's property and value, so the following will happily compile:

不幸的是,这并不能阻止任何人做一些愚蠢的事情,例如传递 anotherControl的属性和值,因此以下内容将很容易编译:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

Hence I added the runtime checks to ensure that the passed-in property does actually belong to the Controlthat the method's being called on. Not perfect, but still a lot better than the .NET 2.0 version.

因此,我添加了运行时检查以确保传入的属性确实属于Control调用该方法的属性。不完美,但仍然比 .NET 2.0 版本好很多。

If anyone has any further suggestions on how to improve this code for compile-time safety, please comment!

如果有人对如何改进此代码以确保编译时安全有任何进一步的建议,请发表评论!

回答by Frederik Gheysels

You'll have to make sure that the update happens on the correct thread; the UI thread.

您必须确保更新发生在正确的线程上;用户界面线程。

In order to do this, you'll have to Invoke the event-handler instead of calling it directly.

为此,您必须调用事件处理程序,而不是直接调用它。

You can do this by raising your event like this:

你可以通过像这样引发你的事件来做到这一点:

(The code is typed here out of my head, so I haven't checked for correct syntax, etc., but it should get you going.)

(代码是在我脑子里打出来的,所以我没有检查正确的语法等,但它应该能让你继续前进。)

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

Note that the code above will not work on WPF projects, since WPF controls do not implement the ISynchronizeInvokeinterface.

请注意,上面的代码不适用于 WPF 项目,因为 WPF 控件不实现该ISynchronizeInvoke接口。

In order to make sure that the code above works with Windows Forms and WPF, and all other platforms, you can have a look at the AsyncOperation, AsyncOperationManagerand SynchronizationContextclasses.

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

In order to easily raise events this way, I've created an extension method, which allows me to simplify raising an event by just calling:

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

MyEvent.Raise(this, EventArgs.Empty);

Of course, you can also make use of the BackGroundWorker class, which will abstract this matter for you.

当然,您也可以使用 BackGroundWorker 类,它会为您抽象出这件事。

回答by OregonGhost

The simple solution is to use Control.Invoke.

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

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

void updateGUI() {
    // update gui here
}

回答by Kieron

You'll need to Invoke the method on the GUI thread. You can do that by calling Control.Invoke.

您需要在 GUI 线程上调用该方法。您可以通过调用 Control.Invoke 来实现。

For example:

例如:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

回答by Marc Gravell

The simplestway is an anonymous method passed into Label.Invoke:

最简单的方法是匿名方法传递到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

Notice that Invokeblocks execution until it completes--this is synchronous code. The question doesn't ask about asynchronous code, but there is lots of content on Stack Overflowabout writing asynchronous code when you want to learn about it.

请注意,Invoke阻塞执行直到它完成——这是同步代码。该问题不询问异步代码,但是当您想了解异步代码时,Stack Overflow 上有很多关于编写异步代码的内容。

回答by Hath

This is the classic way you should do this:

这是您应该执行此操作的经典方法:

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

Your worker thread has an event. Your UI thread starts off another thread to do the work and hooks up that worker event so you can display the state of the worker thread.

您的工作线程有一个事件。您的 UI 线程从另一个线程开始执行工作并连接该工作线程事件,以便您可以显示工作线程的状态。

Then in the UI you need to cross threads to change the actual control... like a label or a progress bar.

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

回答by Frankg

For many purposes it's as simple as this:

出于许多目的,它就像这样简单:

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

"serviceGUI()" is a GUI level method within the form (this) that can change as many controls as you want. Call "updateGUI()" from the other thread. Parameters can be added to pass values, or (probably faster) use class scope variables with locks on them as required if there is any possibility of a clash between threads accessing them that could cause instability. Use BeginInvoke instead of Invoke if the non-GUI thread is time critical (keeping Brian Gideon's warning in mind).

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

回答by Brian Gideon

Because of the triviality of the scenario I would actually have the UI thread poll for the status. I think you will find that it can be quite elegant.

由于场景的琐碎,我实际上会让 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...";
    }
  }
}

The approach avoids the marshaling operation required when using the ISynchronizeInvoke.Invokeand ISynchronizeInvoke.BeginInvokemethods. There is nothing wrong with using the marshaling technique, but there are a couple of caveats you need to be aware of.

该方法避免了使用ISynchronizeInvoke.InvokeISynchronizeInvoke.BeginInvoke方法时所需的编组操作。使用封送处理技术并没有错,但是您需要注意一些注意事项。

  • Make sure you do not call BeginInvoketoo frequently or it could overrun the message pump.
  • Calling Invokeon the worker thread is a blocking call. It will temporarily halt the work being done in that thread.
  • 确保您不要BeginInvoke太频繁地打电话,否则可能会超过消息泵。
  • 调用Invoke工作线程是一个阻塞调用。它将暂时停止在该线程中完成的工作。

The strategy I propose in this answer reverses the communication roles of the threads. Instead of the worker thread pushing the data the UI thread polls for it. This a common pattern used in many scenarios. Since all you are wanting to do is display progress information from the worker thread then I think you will find that this solution is a great alternative to the marshaling solution. It has the following advantages.

我在这个答案中提出的策略颠倒了线程的通信角色。UI 线程轮询数据而不是工作线程推送数据。这是许多场景中使用的常见模式。由于您想要做的只是显示来自工作线程的进度信息,因此我认为您会发现此解决方案是封送处理解决方案的绝佳替代方案。它具有以下优点。

  • The UI and worker threads remain loosely coupled as opposed to the Control.Invokeor Control.BeginInvokeapproach which tightly couples them.
  • The UI thread will not impede the progress of the worker thread.
  • The worker thread cannot dominate the time the UI thread spends updating.
  • The intervals at which the UI and worker threads perform operations can remain independent.
  • The worker thread cannot overrun the UI thread's message pump.
  • The UI thread gets to dictate when and how often the UI gets updated.
  • UI 和工作线程保持松散耦合,而不是将它们紧密耦合的Control.InvokeorControl.BeginInvoke方法。
  • UI 线程不会阻碍工作线程的进度。
  • 工作线程不能支配 UI 线程花费更新的时间。
  • UI 和工作线程执行操作的时间间隔可以保持独立。
  • 工作线程不能溢出 UI 线程的消息泵。
  • UI 线程可以决定 UI 更新的时间和频率。

回答by Don Kirkby

Threading code is often buggy and always hard to test. You don't need to write threading code to update the user interface from a background task. Just use the BackgroundWorkerclass to run the task and its ReportProgressmethod to update the user interface. Usually, you just report a percentage complete, but there's another overload that includes a state object. Here's an example that just reports a string object:

线程代码通常有问题并且总是难以测试。您不需要编写线程代码来从后台任务更新用户界面。只需使用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();
    }

That's fine if you always want to update the same field. If you've got more complicated updates to make, you could define a class to represent the UI state and pass it to the ReportProgress method.

如果您总是想更新同一个字段,那很好。如果您要进行更复杂的更新,您可以定义一个类来表示 UI 状态并将其传递给 ReportProgress 方法。

One final thing, be sure to set the WorkerReportsProgressflag, or the ReportProgressmethod will be completely ignored.

最后一件事,一定要设置WorkerReportsProgress标志,否则该ReportProgress方法将被完全忽略。

回答by StyxRiver

Fire and forget extension method for .NET 3.5+

.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 can be called using the following line of code:

这可以使用以下代码行调用:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");