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

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

我想看看如何System.Timers.Timer工作,所以我没有使用Forms计时器。 我明白,表单和myTimer现在将运行在不同的线程。 是否有一种简单的方法来表示以下forms的lblValue的stream逝时间?

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

我想你的代码只是一个testing,所以我不会讨论你如何处理你的计时器。 这里的问题是如何在定时器callback中使用用户界面控件做些事情。

Control的大多数方法和属性只能从UI线程访问(实际上只能从创build它们的线程访问,但这是另一回事)。 这是因为每个线程必须有自己的消息循环( GetMessage()按线程过滤掉消息),然后使用Control来做一些事情,必须从线程向线程发送一条消息。 在.NET中很容易,因为每个Control为此inheritance了几个方法: 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线程可能很忙或挂一点点)。 如果你不需要方法的返回值,你可以简单的用BeginInvokereplaceInvoke ,对于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等应用程序)。

现在想象你有一个工作线程,你怎么能打电话给你的主线程? 最简单的方法就是使用这个底层结构去做这个伎俩。 我简单地说明了这个任务,但是这些步骤是:

  • 创build一个(线程安全的)函数队列来调用( 这里的一些例子)。
  • 将自定义消息发送到窗口过程。 如果您将此队列设置为优先级队列,那么您甚至可以为这些调用确定优先级(例如,来自工作线程的进度通知可能具有比警报通知更低的优先级)。
  • 在窗口过程中(在你的switch / case语句中)你理解那个消息,那么你可以偷看函数从队列中调用并调用它。

WPF和WinForms都使用此方法将消息从线程传递(分发)到UI线程。 看一下这篇关于multithreading和用户界面的更多细节的MSDN文章 ,WinForms隐藏了很多这些细节,你不必去关心它们,但是你可以看看它是如何工作的。

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

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

当我在一个不同的线程asynchronous调用,我总是可以callback使用。

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

简单而干净。

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

所以this.lblElapsedTime.Text = ...在你的callback中显然是错误的。 看看Control.BeginInvoke 。

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

未经testing:

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

如问,这是我的答案,检查跨线程调用,同步variables更新,不要停止和启动计时器,并没有使用计时器来计算经过的时间。

编辑修复了BeginInvoke调用。 我已经使用通用的Action来完成跨线程调用,这允许传递sender和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; } } } } 

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

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