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

时间:2012-04-16 07:54:48

标签: c# .net multithreading winforms timer

与计时器一起玩。 上下文:带有两个标签的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(); }

    }
  }
}

5 个答案:

答案 0 :(得分:35)

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

大多数Control的方法和属性只能从UI线程访问(实际上,只能从创建它们的线程访问它们,但这是另一个故事)。这是因为每个线程都必须有自己的消息循环(GetMessage()按线程筛选出消息)然后用Control做一些事情,你必须从你的线程向 main发送消息线程。在.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线程可能忙或暂停一点)。如果您不需要方法的返回值,则只需将Invoke替换为BeginInvoke,对于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);
}

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

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

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

WPF和WinForms都使用此方法将消息从线程传递(分派)到UI线程。看看this article on MSDN有关多线程和用户界面的更多详细信息,WinForms隐藏了很多这些细节而你不需要处理它们,但是你可以看看它是如何在引擎盖下工作的

答案 1 :(得分:7)

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

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

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

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

简单干净。

答案 2 :(得分:2)

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

编辑已修复BeginInvoke来电。我使用泛型Action完成了跨线程调用,这允许传递发送方和eventargs。如果它们未被使用(因为它们在这里),使用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;
            }
        }
    }
}

答案 3 :(得分:1)

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

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

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

未测试:

DateTime startTime = DateTime.Now;

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

答案 4 :(得分:0)

使用以下内容结束。这是给出的建议的组合:

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