2021-01-25
關鍵字:子線程調用主線程資源、子線程更新UI
WPF中想在子線程中操作在主線程中創建的控件其實很簡單,使用 Dispatcher 類對象即可實現需求。
下面直接上一個最簡單的實例。
假設我們有一個Window,裏面包含了一個TextBlock控件,其界面及xaml代碼如下所示:
<Window x:Name="hello__net_core" x:Class="Wpf_dotnetonly.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Wpf_dotnetonly" mc:Ignorable="d" Title="MainWindow" Height="150" Width="310"> <StackPanel> <TextBlock x:Name="TB" Width="100" Height="50" Background="AliceBlue" /> </StackPanel> </Window>
這個程序想實現的功能是:有一個間隔一秒永久循環運行的子線程,記錄這個子線程的循環次數並使其顯示在TextBlock上。
常見的做法是將對控件更新的操作封裝到一個方法中,然後使用Dispatcher類對象來從子線程中更新主線程中的控件,關鍵代碼如下所示:
public partial class MainWindow : Window { private delegate void MyDelegate(int value); private int GlobalCounter; public MainWindow() { InitializeComponent(); new Thread(SubThreadProc).Start(); } private void SubThreadProc() { int counter = 0; while(true) { counter++; GlobalCounter++; Thread.Sleep(1000); Dispatcher.Invoke(new MyDelegate(SetTB), counter); //Dispatcher.Invoke(SetTB2);
//TB.Dispatcher.Invoke(SetTB2); } }
//方式一 private void SetTB(int val) { TB.Text = val.ToString(); }
//方式二 private void SetTB2() { TB.Text = GlobalCounter.ToString(); } }
對於無參的封裝方法,直接通過Invoke()傳入方法名即可。有參的封裝方法,則需要通過一個Delegate來額外包裝一下。
知曉了術以後,接下來簡單分析一下它的法。
Dispatcher類位於 System.Windows.Threading 名稱空間中,它的功能是爲線程提供一個作業隊列。在這裏我們簡單地將它理解成是將要執行的作業先提交到一個隊列中,然後再由這個Dispatcher對象的所有者來執行這些作業即可。通常情況下,WPF中是由主線程--即UI線程負責創建Dispatcher對象的,再加上Dispatcher對象全局共享的特性,我們就可以在子線程中將作業提交到它的隊列裏,待Dispatcher對象的所有者--即主線程來最終執行從而達到在子線程中更新控件內容的目的。
由於筆者沒有閱讀.NET CORE的源碼,所以並不知道Dispatcher對象的創建流程。不過我們可以從Window以及TextBlock的繼承關係來大概猜測一下,以下猜測很可能是錯的,僅供娛樂,切勿當真。
Window的繼承關係如下所示:
DispatcherObject ^ DependencyObject
^ Visual
^ UIElement
^ FrameworkElement
^ Control
^ ContentControl
^ Window
TextBlock的繼承關係如下所示:
DispatcherObject ^ DependencyObject ^ Visual ^ UIElement ^ FrameworkElement ^ TextBlock
Dispacthcer類就是DispactherObejct類中的屬性。由此可以猜想,Dispatcher類是在控件或窗口實例化的時候就創建了的。同時又因爲Dispacther類對象是全局唯一且共享的,進一步提出是在WPF程序啓動時創建的。猜測結束!
一般來說,Dispatcher對象我們只管用即可,不需要去理會它的生命週期,它不需要我們去操心創建、管理或銷燬。事實上,一個線程有且只能有一個Dispatcher對象,且一旦一個Dispatcher對象被銷燬以後就不能再恢復了。換句話說,如果我們不小心將主線程的Dispatcher對象註銷掉了,那就只有重啓程序才能重新創建了。
言歸正傳,我們還是重點來看看Dispatcher關於在子線程中操作主線程控件的方法。
Dispatcher提供了很多用於此目的的方法,但概括下來主要有兩種:
1、同步式的;
2、異步式的。
1、同步式的
以Invoke爲方法名的各種重載方法。本文介紹以下幾種典型方法,更詳盡的請參閱官方文檔:
https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher.invoke?view=net-5.0#System_Windows_Threading_Dispatcher_Invoke_System_Action_
a), public void Invoke (Action callback);
最簡單的。在子線程中調用該方法後會阻塞直至主線程將Action代理中的方法執行完成爲止。Action的原型爲一個無參代理函數:public delegate void Action(); 一般直接傳方法名稱即可。
b), public object Invoke (Delegate method, params object[] args);
有參的封裝方法。參數 method 是一個開放的Delegate類型,你可以任意定義Delegate並將其封裝後傳入,參數args則是要傳遞給method的參數列表。這個方法的使用正如本文開頭的例子所示。
c), public object Invoke (Delegate method, System.Windows.Threading.DispatcherPriority priority, params object[] args);
與b)類似,不同的是允許爲要執行的作業設置優先級。優先級爲一個枚舉類,該枚舉類數值越大優先級越高。具體請參閱官方文檔:https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcherpriority?view=net-5.0
沒有特殊的要求都不需要去設置優先級。
2、異步式的
以BeginInvoke爲方法名的重載方法。該方法調用後會立即返回,提交的作業會在恰當的時機被Dispatcher的持有線程執行。
a), public System.Windows.Threading.DispatcherOperation BeginInvoke (Delegate method, params object[] args);
帶參數的封裝方法。
b), public DispatcherOperation BeginInvoke (Delegate method, DispatcherPriority priority, params object[] args);
帶參數且可以指定優先級的封裝方法。
到此,本文所介紹的知識基本夠用了,如果想更進一步瞭解Dispacther,請參閱官方文檔:
https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher?view=net-5.0