WPF 同一窗口內的多線程 UI(VisualTarget)

 

WPF 的 UI 邏輯只在同一個線程中,這是學習 WPF 開發中你們幾乎都會學習到的經驗。若是但願作不一樣線程的 UI,你們也會想到使用另外一個窗口來實現,讓每一個窗口擁有本身的 UI 線程。然而,就不能讓同一個窗口內部使用多個 UI 線程嗎?git

答案實際上是——能夠的!使用 VisualTarget 便可。github

閱讀本文將收穫一份對 VisualTarget 的解讀以及一份我封裝好的跨線程 UI 控件 DispatcherContainer.cs編程


幾個必備的組件

微軟給 VisualTarget 提供的註釋是:多線程

提供跨線程邊界將一個可視化樹鏈接到另外一個可視化樹的功能。異步

註釋中說 VisualTarget 就是用來鏈接可視化樹(VisualTree)的,並且能夠跨線程邊界。也就是說,這是一個專門用來使同一個窗口內部包含多個不一樣 UI 線程的類型。ide

因此,咱們的目標是使用 VisualTarget 顯示跨線程邊界的 UI。函數

VisualTarget 自己繼承自 CompositionTarget,而不是 Visual;其自己並非可視化樹的一部分。可是它的構造函數中能夠傳入一個 HostVisual 對象,這個對象是一個 Visual,若是將此 HostVisual 放入原 UI 線程的可視化樹上,那麼 VisualTarget 就與主 UI 線程鏈接起來了。學習

另一半,VisualTarget 須要鏈接另外一個異步線程的可視化樹。然而,VisualTarget 提供了 RootVisual 屬性,直接給此屬性賦一個後臺 UI 控件做爲其值,即鏈接了另外一個 UI 線程的可視化樹。測試

總結起來,其實咱們只須要 new 一個 VisualTarget 的新實例,構造函數傳入一個 UI 線程的可視化樹中的 HostVisual 實例,RootVisual 屬性設置爲另外一個 UI 線程中的控件,便可完成跨線程可視化樹的鏈接。優化

事實上通過嘗試,咱們真的只須要這樣作就可讓另外一個線程上的 UI 呈現到當前的窗口上,同一個窗口。讀者能夠自行編寫測試代碼驗證這一點,我並不打算在這裏貼上試驗代碼,由於後面會給出完整可用的所有代碼。

完善基本功能

雖然說 VisualTarget 的基本使用已經能夠顯示一個跨線程的 UI 了,可是其實功能仍是欠缺的。

一個典型的狀況是,後臺線程的這部分 UI 沒有鏈接到 PresentationSource;而 Visual.PointFromScreenVisual.PointFromScreen 這樣的方法明確須要鏈接到 PresentationSource 才行。參見這裏:In WPF, under what circumstances does Visual.PointFromScreen throw InvalidOperationException? - Stack Overflow

但是,應該如何將 RootVisual 鏈接到 PresentationSource 呢?我從 Microsoft.DwayneNeed 項目中找到了方法。這是源碼地址:Microsoft.DwayneNeed - Home

作法是重寫屬性和方法:

public override Visual RootVisual
{
    get => _visualTarget.RootVisual;
    set
    {
        // 此處省略大量代碼。
    }
}
protected override CompositionTarget GetCompositionTargetCore()
{
    return _visualTarget;
}

Microsoft.DwayneNeed 中有 VisualTargetPresentationSource 的完整代碼,我本身只爲這個類添加了 IDisposable 接口,用於 DisposeVisualTarget 的實例。我須要這麼作是由於我即將提供可修改後臺 UI 線程控件的方法。

讓方法變得好用

爲了讓整個多線程 UI 線程的使用行雲流水,我準備寫一個 DispatcherContainer 類來優化多線程 UI 的使用體驗。指望的使用方法是給這個控件的實例設置 Child 屬性,這個 Child 是後臺線程建立的 UI。而後一切線程同步相關的工做所有交給此類來完成。

在我整理後,使用此控件只需如此簡單:

<Grid Background="#FFEEEEEE">
    <local:DispatcherContainer x:Name="Host"/>
</Grid>
await Host.SetChildAsync<MyUserControl>();

其中,MyUserControl 是控件的類型,能夠是你寫的某個 XAML 用戶控件,也能夠是其餘任何控件類型。用這個方法建立的控件,直接就是後臺 UI 線程的。

固然,若是你須要本身控制初始化邏輯,可使用委託建立控件。

await Host.SetChildAsync(() => { var box = new TextBox { Text = "呂毅 - walterlv", Margin = new Thickness(16), }; return box; });

下圖便是用以上代碼建立的後臺線程文本框。

後臺線程的文本框

甚至,你已經有線程的後臺 UI 控件了,或者你但願本身來建立後臺的 UI 控件,則能夠這樣:

// 建立一個後臺線程的 Dispatcher。
// 其中,UIDispatcher 是我本身封裝的方法,在 GitHub 上以 MIT 協議開源。
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.WPF/Utils/Threading/UIDispatcher.cs
var dispatcher = await UIDispatcher.RunNewAsync("walterlv's testing thread");

// 使用這個後臺線程的 Dispatcher 建立一個本身的用戶控件。
var control = await dispatcher.InvokeAsync(() => new MyUserControl());

// 將這個用戶控件放入封裝好的 DispatcherContainer 中。
// DispatcherContainer 是我本身封裝的方法,在 GitHub 上以 MIT 協議開源。
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.WPF/Utils/Threading/DispatcherContainer.cs
await Host.SetChildAsync(control);

注意到咱們本身建立的控件已經運行在後臺線程中了:

運行在後臺線程中

完整的代碼

如下全部代碼都可點擊進入 GitHub 查看。

核心的代碼包含兩個類:

  • VisualTargetPresentationSource 這是實現異步 UI 的關鍵核心,用於鏈接兩個跨線程邊界的可視化樹,並同時提供鏈接到 PresentationSource 的功能。(因爲我對 PresentationSource 的瞭解有限,此類絕大多數代碼都直接來源於 Microsoft.DwayneNeed - Home。)
  • DispatcherContainer 當使用我封裝好的多線程 UI 方案時(其實就是把這幾個類本身帶走啦),這個類纔是你們編程開發中主要面向的 API 類啊!

其餘輔助型代碼:

  • UIDispatcher 這並非重點,此類型只是爲了方便地建立後臺 Dispatcher
  • DispatcherAsyncOperation 此類型只是爲了讓 UIDispatcher 中的方法更好寫一些。
  • AwaiterInterfaces 這是一組無關緊要的接口;給 DispatcherAsyncOperation 繼承的接口,可是不繼承也沒事,同樣能跑。

這些輔助型代碼的含義能夠查看個人另外一篇博客:如何實現一個能夠用 await 異步等待的 Awaiter - walterlv的專欄 - CSDN博客


參考資料

相關文章
相關標籤/搜索