[英]How to invoke a UI method from another thread
玩定時器。 上下文:一個帶有兩個標簽的 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(); }
}
}
}
我猜你的代碼只是一個測試,所以我不會討論你用定時器做什么。 這里的問題是如何在計時器回調中使用用戶界面控件執行某些操作。
大多數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 以獲取您可以從任何線程調用的方法列表,就像參考一樣,您始終可以調用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
}
}
請注意,當前線程將阻塞,直到 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);
}
通過這幾行代碼,您的應用程序等待消息,然后將消息傳遞給窗口過程。 窗口過程是一個很大的 switch/case 語句,您可以在其中檢查您知道的消息 ( WM_
) 並以某種方式處理它們(您為WM_PAINT
繪制窗口,為WM_QUIT
退出應用程序等等)。
現在想象你有一個工作線程,你怎么能調用你的主線程? 最簡單的方法是使用這個底層結構來解決這個問題。 我過度簡化了任務,但這些是步驟:
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();}
}
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.