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

why*_*heq 21 .net c# multithreading timer winforms

与计时器一起玩.上下文:带有两个标签的winforms.

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

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

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

    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Adr*_*tti 35

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

大多数Control的方法和属性只能从UI线程访问(实际上它们只能从你创建它们的线程访问,但这是另一个故事).这是因为每个线程必须有自己的消息循环(GetMessage()按线程过滤掉消息)然后用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";
    }));
}
Run Code Online (Sandbox Code Playgroud)

请检查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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}
Run Code Online (Sandbox Code Playgroud)

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

这个怎么运作?

GUI Windows应用程序基于窗口过程及其消息循环.如果你用普通的C编写一个应用程序,你会有这样的事情:

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}
Run Code Online (Sandbox Code Playgroud)

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

现在假设你有一个工作线程,你怎么能打电话给你的主线程?最简单的方法是使用这个底层结构来完成这个技巧.我过度简化了任务,但这些步骤是:

  • 创建一个(线程安全的)函数队列来调用(这里的一些例子在SO上).
  • 将自定义消息发布到窗口过程.如果将此队列设为优先级队列,则甚至可以确定这些调用的优先级(例如,来自工作线程的进度通知的优先级可能低于警报通知).
  • 在窗口过程中(在switch/case语句内),您可以了解该消息,然后您可以查看从队列调用的函数并调用它.

WPF和WinForms都使用此方法将消息从线程传递(分派)到UI线程.有关多线程和用户界面的更多详细信息,请查看MSDN上的这篇文章,WinForms隐藏了很多这些细节,您不必处理它们,但您可能需要了解它是如何工作的.


The*_*ørg 8

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

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}
Run Code Online (Sandbox Code Playgroud)

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

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});
Run Code Online (Sandbox Code Playgroud)

简单干净.