C# 如何从另一个线程调用 UI 方法

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10170448/
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-09 12:43:37  来源:igfitidea点击:

How to invoke a UI method from another thread

c#.netmultithreadingwinformstimer

提问by whytheq

Playing round with Timers. Context: a winforms with two labels.

玩定时器。上下文:一个带有两个标签的 winforms。

I would like to see how System.Timers.Timerworks so I've not used the Forms timer. I understand that the form and myTimer will now be running in different threads. Is there an easy way to represent the elapsed time on lblValuein the following form?

我想看看它是如何System.Timers.Timer工作的,所以我没有使用 Forms 计时器。我知道表单和 myTimer 现在将在不同的线程中运行。有没有一种简单的方法可以用lblValue以下形式表示经过的时间?

I've looked here on MSDNbut is there an easier way !

我在MSDN上看过这里,但有没有更简单的方法!

Here's the winforms code:

这是winforms代码:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {
    //instance variables of the form
    System.Timers.Timer myTimer;
    int ElapsedCounter = 0;

    int MaxTime = 5000;
    int elapsedTime = 0;
    static int tickLength = 100;

    public AirportParking()
    {
        InitializeComponent();
        keepingTime();
        lblValue.Text = "hello";
    }

    //method for keeping time
    public void keepingTime() {

        myTimer = new System.Timers.Timer(tickLength); 
        myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);

        myTimer.AutoReset = true;
        myTimer.Enabled = true;

        myTimer.Start();
    }


    void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        myTimer.Stop();
        ElapsedCounter += 1;
        elapsedTime += tickLength; 

        if (elapsedTime < MaxTime)
        {
            this.lblElapsedTime.Text = elapsedTime.ToString();

            if (ElapsedCounter % 2 == 0)
                this.lblValue.Text = "hello world";
            else
                this.lblValue.Text = "hello";

            myTimer.Start(); 

        }
        else
        { myTimer.Start(); }

    }
  }
}

采纳答案by Adriano Repetti

I guess your code is just a test so I won't discuss about what you do with your timer. The problem here is how to do something with an user interface control inside your timer callback.

我猜你的代码只是一个测试,所以我不会讨论你用定时器做什么。这里的问题是如何在计时器回调中使用用户界面控件执行某些操作。

Most of Control's methods and properties can be accessed only from the UI thread (in reality they can be accessed only from the thread where you created them but this is another story). This is because each thread has to have its own message loop (GetMessage()filters out messages by thread) then to do something with a Controlyou have to dispatch a message from your thread to the mainthread. In .NET it is easy because every Controlinherits a couple of methods for this purpose: Invoke/BeginInvoke/EndInvoke. To know if executing thread must call those methods you have the property InvokeRequired. Just change your code with this to make it works:

的大多数Control方法和属性只能从 UI 线程访问(实际上它们只能从您创建它们的线程访问,但这是另一回事)。这是因为每个线程都必须有自己的消息循环(GetMessage()按线程过滤消息),然后要对 a 执行某些操作,Control您必须将消息从您的线程发送到线程。在 .NET 中很容易,因为每个都Control为此目的继承了几个方法:Invoke/BeginInvoke/EndInvoke. 要知道执行线程是否必须调用您拥有属性的那些方法InvokeRequired。只需使用此更改您的代码即可使其正常工作:

if (elapsedTime < MaxTime)
{
    this.BeginInvoke(new MethodInvoker(delegate 
    {
        this.lblElapsedTime.Text = elapsedTime.ToString();

        if (ElapsedCounter % 2 == 0)
            this.lblValue.Text = "hello world";
        else
            this.lblValue.Text = "hello";
    }));
}

Please check MSDN for the list of methods you can call from any thread, just as reference you can always call Invalidate, BeginInvoke, EndInvoke, Invokemethods and to read InvokeRequiredproperty. In general this is a common usage pattern (assuming thisis an object derived from Control):

请查看 MSDN 以获取您可以从任何线程调用的方法列表,就像您始终可以调用Invalidate, BeginInvoke, EndInvoke,Invoke方法和读取 InvokeRequired属性一样。一般来说,这是一种常见的使用模式(假设this是从 派生的对象Control):

void DoStuff() {
    // Has been called from a "wrong" thread?
    if (InvokeRequired) {
        // Dispatch to correct thread, use BeginInvoke if you don't need
        // caller thread until operation completes
        Invoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Note that current thread will block until UI thread completed method execution. This may be an issue if thread's timing is important (do not forget that UI thread may be busy or hung for a little). If you don't need method's return value you may simply replace Invokewith BeginInvoke, for WinForms you don't even need subsequent call to EndInvoke:

请注意,当前线程将阻塞,直到 UI 线程完成方法执行。如果线程的时间很重要,这可能是一个问题(不要忘记 UI 线程可能很忙或挂了一点)。如果您不需要方法的返回值,您可以简单地替换InvokeBeginInvoke,对于 WinForms,您甚至不需要后续调用EndInvoke

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

If you need return value then you have to deal with usual IAsyncResultinterface.

如果您需要返回值,那么您必须处理通常的IAsyncResult接口。

How it works?

这个怎么运作?

A GUI Windows application is based on the window procedure with its message loops. If you write an application in plain C you have something like this:

GUI Windows 应用程序基于带有消息循环的窗口过程。如果你用普通的 C 编写一个应用程序,你会有这样的东西:

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}

With these few lines of code your application wait for a message and then delivers the message to the window procedure. The window procedure is a big switch/case statement where you check the messages (WM_) you know and you process them somehow (you paint the window for WM_PAINT, you quit your application for WM_QUITand so on).

通过这几行代码,您的应用程序等待消息,然后将消息传递给窗口过程。窗口过程是一个很大的 switch/case 语句,您可以在其中检查WM_您知道的消息 ( ) 并以某种方式处理它们(您为 绘制窗口WM_PAINT,退出应用程序WM_QUIT等等)。

Now imagine you have a working thread, how can you callyour main thread? Simplest way is using this underlying structure to do the trick. I oversimplify the task but these are the steps:

现在想象你有一个工作线程,你怎么能调用你的主线程?最简单的方法是使用这个底层结构来解决这个问题。我过度简化了任务,但这些是步骤:

  • Create a (thread-safe) queue of functions to invoke (some examples here on SO).
  • Post a custom message to the window procedure. If you make this queue a priority queue then you can even decide priority for these calls (for example a progress notification from a working thread may have a lower priority than an alarm notification).
  • In the window procedure (inside your switch/case statement) you understandthat message then you can peek the function to call from the queue and to invoke it.
  • 创建要调用的(线程安全的)函数队列(SO 上的一些示例)。
  • 将自定义消息发布到窗口过程。如果您将此队列设为优先级队列,那么您甚至可以决定这些调用的优先级(例如,来自工作线程的进度通知的优先级可能低于警报通知)。
  • 在窗口过程中(在您的 switch/case 语句中),您了解该消息,然后您可以查看从队列中调用并调用它的函数。

Both WPF and WinForms use this method to deliver (dispatch) a message from a thread to the UI thread. Take a look to this article on MSDNfor more details about multiple threads and user interface, WinForms hides a lot of these details and you do not have to take care of them but you may take a look to understand how it works under the hood.

WPF 和 WinForms 都使用此方法将消息从线程传递(调度)到 UI 线程。有关多线程和用户界面的更多详细信息,请查看MSDN上的这篇文章,WinForms 隐藏了许多这些细节,您不必处理它们,但您可以查看一下以了解它在底层是如何工作的。

回答by Nicolas Repiquet

First, in Windows Forms (and most frameworks), a control can only be accessed (unless documented as "thread safe") by the UI thread.

首先,在 Windows 窗体(和大多数框架)中,控件只能由 UI 线程访问(除非记录为“线程安全”)。

So this.lblElapsedTime.Text = ...in your callback is plain wrong. Take a look at Control.BeginInvoke.

所以this.lblElapsedTime.Text = ...在你的回调中是完全错误的。看看Control.BeginInvoke

Second, You should use System.DateTimeand System.TimeSpanfor your time computations.

其次,您应该使用System.DateTimeSystem.TimeSpan进行时间计算。

Untested:

未经测试:

DateTime startTime = DateTime.Now;

void myTimer_Elapsed(...) {
  TimeSpan elapsed = DateTime.Now - startTime;
  this.lblElapsedTime.BeginInvoke(delegate() {
    this.lblElapsedTime.Text = elapsed.ToString();
  });
}

回答by Jodrell

As asked, here is my answer that checks for cross thread calls, synchronises variable updates, doesen't stop and start the timer and doesn't use the timer for counting elapsed time.

正如所问,这是我的答案,它检查跨线程调用,同步变量更新,不停止和启动计时器,也不使用计时器来计算经过的时间。

EDITfixed BeginInvokecall. I've done the cross thread invoke using a generic Action, This allows the sender and eventargs to be passed. If these are unused (as they are here) it is more efficient to use MethodInvokerbut I suspect the handling would need to be moved into a parameterless method.

编辑固定BeginInvoke电话。我已经使用泛型完成了跨线程调用Action,这允许传递发送者和事件参数。如果这些未使用(因为它们在此处),则使用效率更高,MethodInvoker但我怀疑需要将处理移至无参数方法中。

public partial class AirportParking : Form
{
    private Timer myTimer = new Timer(100);
    private int elapsedCounter = 0;
    private readonly DateTime startTime = DateTime.Now;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    public AirportParking()
    {
        lblValue.Text = EvenText;
        myTimer.Elapsed += MyTimerElapsed;
        myTimer.AutoReset = true;
        myTimer.Enabled = true;
        myTimer.Start();
    }

    private void MyTimerElapsed(object sender,EventArgs myEventArgs)
    {
        If (lblValue.InvokeRequired)
        {
            var self = new Action<object, EventArgs>(MyTimerElapsed);
            this.BeginInvoke(self, new [] {sender, myEventArgs});
            return;   
        }

        lock (this)
        {
            lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
            elapesedCounter++;
            if(elapsedCounter % 2 == 0)
            {
                lblValue.Text = EvenText;
            }
            else
            {
                lblValue.Text = OddText;
            }
        }
    }
}

回答by whytheq

Ended up using the following. It's a combination of the suggestions given:

最终使用了以下内容。这是给出的建议的组合:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    //instance variables of the form
    System.Timers.Timer myTimer;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    static int tickLength = 100; 
    static int elapsedCounter;
    private int MaxTime = 5000;
    private TimeSpan elapsedTime; 
    private readonly DateTime startTime = DateTime.Now; 
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    public AirportParking()
    {
        InitializeComponent();
        lblValue.Text = EvenText;
        keepingTime();
    }

    //method for keeping time
    public void keepingTime() {

    using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
    {  
           myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
           myTimer.AutoReset = true;
           myTimer.Enabled = true;
           myTimer.Start(); 
    }  

    }

    private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        elapsedCounter++;
        elapsedTime = DateTime.Now.Subtract(startTime);

        if (elapsedTime.TotalMilliseconds < MaxTime) 
        {
            this.BeginInvoke(new MethodInvoker(delegate
            {
                this.lblElapsedTime.Text = elapsedTime.ToString();

                if (elapsedCounter % 2 == 0)
                    this.lblValue.Text = EvenText;
                else
                    this.lblValue.Text = OddText;
            })); 
        } 
        else {myTimer.Stop();}
      }
  }
}

回答by Theodor Solbj?rg

Personally when I work in a application that works with threads out of the UI one, I usually write this little snippet.

就我个人而言,当我在使用 UI 之外的线程的应用程序中工作时,我通常会编写这个小片段。

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}

When i do a async call in a differnt thread I can always callback using.

当我在不同的线程中进行异步调用时,我总是可以使用回调。

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});

Simple and clean.

简单干净。