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

本文关键字:调用 UI 方法 线程 另一个 | 更新日期: 2023-09-27 18:31:10

使用计时器进行回合。上下文:具有两个标签的Winforms。

我想看看System.Timers.Timer是如何工作的,所以我没有使用表单计时器。我知道表单和 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(); }
    }
  }
}

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

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

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

请检查 MSDN 以获取可以从任何线程调用的方法列表,就像您始终可以调用InvalidateBeginInvokeEndInvokeInvoke方法并读取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
    }
}

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

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

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

它是如何工作的?

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

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

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

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

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

WPF 和 WinForms 都使用此方法将消息从线程传递(调度)到 UI 线程。有关多线程和用户界面的更多详细信息,请查看MSDN上的这篇文章,WinForms隐藏了很多这些细节,您不必照顾它们,但您可以查看以了解它在后台的工作方式。

就我个人而言,当我在一个与 UI 线程一起使用的应用程序时,我通常会写这个小片段:

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

当我在不同的线程中进行异步调用时,我始终可以使用以下内容进行回调:

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

简单干净。

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

编辑固定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;
            }
        }
    }
}

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

因此,回调中的this.lblElapsedTime.Text = ...是完全错误的。看看 Control.BeginInvoke。

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

未经测试:

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

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

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