簡體   English   中英

如何從另一個線程調用 UI 方法

[英]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 以獲取您可以從任何線程調用的方法列表,就像參考一樣,您始終可以調用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);
}

通過這幾行代碼,您的應用程序等待消息,然后將消息傳遞給窗口過程。 窗口過程是一個很大的 switch/case 語句,您可以在其中檢查您知道的消息 ( 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.DateTimeSystem.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM