WPF 同一窗口内的多线程 UI(VisualTarget)

摘要:
阅读本文将收获一份对VisualTarget的解读以及一份我封装好的跨线程UI控件DispatcherContainer.cs。所以,我们的目标是使用VisualTarget显示跨线程边界的UI。VisualTarget本身继承自CompositionTarget,而不是Visual;其本身并不是可视化树的一部分。另外一半,VisualTarget需要连接另一个异步线程的可视化树。一个典型的情况是,后台线程的这部分UI没有连接到PresentationSource;而Visual.PointFromScreen、Visual.PointFromScreen这样的方法明确需要连接到PresentationSource才行。

WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验。如果希望做不同线程的 UI,大家也会想到使用另一个窗口来实现,让每个窗口拥有自己的 UI 线程。然而,就不能让同一个窗口内部使用多个 UI 线程吗?

答案其实是——可以的!使用 VisualTarget 即可。

阅读本文将收获一份对 VisualTarget 的解读以及一份我封装好的跨线程 UI 控件 DispatcherContainer.cs


几个必备的组件

微软给 VisualTarget 提供的注释是:

提供跨线程边界将一个可视化树连接到另一个可视化树的功能。

注释中说 VisualTarget 就是用来连接可视化树(VisualTree)的,而且可以跨线程边界。也就是说,这是一个专门用来使同一个窗口内部包含多个不同 UI 线程的类型。

所以,我们的目标是使用 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博客


参考资料

免责声明:文章转载自《WPF 同一窗口内的多线程 UI(VisualTarget)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇架构语言ArchiMate - 开篇:企业架构语言ArchiMate介绍Myeclipse下安装和使用SVN(一)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

15分钟让你了解如何实现并发中的Barrier

说到Barrier,很多语言中已经是标准库中自带的概念,一般情况下,只需要直接使用就行了。而最近一些机缘巧合的机会,我需要在c++中使用这么个玩意儿。但是c++标准库里还没有这个概念,只有boost里面有这样现成的东西,而我又不想为了这么一个小东西引入个boost。所以,我借着这个机会研究了下,发现其实这些多线程/并发中的东西还是蛮有意思的。 阅读本文你可...

WPF模式思考 (zt)

Introduction Since XAML things have become a bit complicated in trying to conceptualize MVC architectures for Windows applications. The gap between web and win is narrowing and th...

Linux线程同步之读写锁

1. 特性:     一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性, 当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞. 当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须阻塞知...

十、future其他成员函数、shared_future、atomic(原子操作)

一、 1 int mythread(){ 2 cout<<"thread"<<endl; 3 std::chrono::milliseconds dura(5000);//5秒钟 4 std::this_thread::sleep_for(dura);//休息5秒钟 5 return 5;...

学习WPF——WPF布局——了解布局容器

WPF布局工作内部原理 WPF渲染布局时主要执行了两个工作:测量和排列 测量阶段,容器遍历所有子元素,并询问子元素所期望的尺寸 排列阶段,容器在合适的位置放置子元素,并设置元素的最终尺寸 这是一个递归的过程,界面中任何一个容器元素都会被遍历到 WPF布局容器的继承机制 DispatcherObject WPF应用程序使用单线程亲和模型(STA...

Spring boot中最大连接数、最大线程数与最大等待数在生产中的异常场景

在上周三下午时,客户、业务和测试人员同时反溃生产环境登录进入不了系统,我亲自测试时,第一次登录进去了,待退出后再登录时,复现了客户的问题,场景像是请求连接被拒绝了,分析后判断是spring boot的连接数使用完了,于是重启了服务,客户访问都恢复正常。虽然问题暂时解决了,但实质原因还无法确定。根据分析,判断是spring boot服务连接被拒绝,查看配置的...