富客戶端 wpf, Winform 多線程更新UI控件


前言  

在富客戶端的app中,如果在主線程中運行一些長時間的任務,那么應用程序的UI就不能正常相應。因為主線程要負責消息循環,相應鼠標等事件還有展現UI。

因此我們可以開啟一個線程來格外處理需要長時間的任務,但在富客戶端中只有主線程才能更新UI的控件。

解決方法

簡單的來說,我們需要從其他的線程來更新UI線程的控件,需要將這個操作轉交給UI線程(線程marshal)。

方法1:

在底層的操作中,可以有以下的方法:

  • WPF中,在element的Dispatcher類中調用BeginInvoke或者Invoke方法
  • Metro中,在Dispatcher類中調用RunAsync或者Invoke方法
  • Winform中,在控件中直接調用BeginInvoke或者Invoke方法

以上所有的方法的參數都是一個Delegate,用此Delegate來代表需要處理的任務:

public IAsyncResult BeginInvoke(Delegate method);

BeginInvoke/RunAsync方法是將這個 Delegate推送到UI線程的消息隊列中,這個消息隊列也就是前面提到的鼠標,鍵盤事件等隊列。

Invoke方法也是推送delegate到消息隊列,但還會一直阻塞到此delegate被UI線程處理為止。所以一般來說我們還是用BeginInvoke/RunAsync方法。

對應app來說,我們可以將其想象為一下的偽代碼:

while (!thisApplication.Ended)
{
wait for something to appear in message queue
Got something: what kind of message is it?
Keyboard/mouse message -> fire an event handler
User BeginInvoke message -> execute delegate
User Invoke message -> execute delegate & post result
}

那接下來我們用winform來demo一下:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        new Thread(work).Start();
    }

    void work()
    {
        Thread.Sleep(5000);
        UpdateMessage("july Luo thread Test");
    }

    void UpdateMessage(string message)
    {
        Action action = () => lblJulyLuo.Text = message;
        this.BeginInvoke(action);
    }
}

方法2

在 System.ComponentModel命名空間中,有 SynchronizationContext抽象類,此類也可以處理線程marshal。

在wpf,metro, winform中都定義了此類的子類,而且可以用SynchronizationContext.Current獲取,然后調用Post方法,可以理解為將其他線程的任務post到UI線程中。

一下為demo:

public partial class Form1 : Form
{
    SynchronizationContext _uiSyncContext;

    public Form1()
    {
        InitializeComponent();
        new Thread(() => work()).Start();
        _uiSyncContext = SynchronizationContext.Current;
    }

    void work()
    {
        Thread.Sleep(5000);
        UpdateMessage("july Luo thread Test");
    }

    void UpdateMessage(string message)
    {
        _uiSyncContext.Post(_ => lblJulyLuo.Text = message, null);
    }
}

 

SynchronizationContext類還有一個Send方法,和我們上面提到的Invoke方法的作用一致。

當然了還有BackgroundWorker類,此類在內部用了SynchronizationContext,所以其也可在其他線程中更新UI線程。

方法3

在.net 4.0之后,已經有了TPL供我們方便的操作多線程,這里試用Task類也可以完成類似操作,demo如下:

public partial class Form1 : Form
{
    Task<string> t;

    public Form1()
    {
        InitializeComponent();
        t = Task.Run(() => work());
        var awaiter = t.GetAwaiter();
        awaiter.OnCompleted(() =>
        {
            string message = awaiter.GetResult();
            lblJulyLuo.Text = message;
        });
    }

    string work()
    {
        Thread.Sleep(5000);
        return "july Luo thread Test";
    }
}

 這里和上面有點不同,我們使用Task來代替多線程,而且其返回要更新的字符串,如何調用GetAwaiter方法返回需要的awaiter,最后在awaiter中的OnCompleted方法中直接更新控件。

原理就是awaiter中的OnCompleted方法會自動獲取synchronizationContext,也會將其推送到UI線程的消息隊列中。

 方法4

使用TaskScheduler來marshal線程,TaskScheduler是抽象類,其負責分配管理Task對象。

Framework中有兩個實現:一個就是 default scheduler 其負責串聯CLR中的線程池,還有一個就是synchronization context scheduler,其負責解決其他線程需要更新UI控件的。

所以思路就是用Task實現多線程,然后調用其ContinueWith方法,並傳入對應的TaskScheduler:

public partial class Form1 : Form
{
    TaskScheduler _uiScheduler;
    Task<string> t;

    public Form1()
    {
        InitializeComponent();
        _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        t = Task.Run(() => work()).ContinueWith(t => lblJulyLuo.Text = t.Result, _uiScheduler);
    }

    string work()
    {
        Thread.Sleep(5000);
        return "july Luo thread Test";
    }
}

總結

富客戶端中UI線程一直會處理着消息循環,無論使用那種方法都是將其推送到消息隊列中以便UI線程處理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM