在 WPF 里面,渲染可以从架构上划分为两层。上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令。上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GFX 层,作用是根据收到的渲染的命令绘制出界面。本文所聊的是渲染上层部分,在 WPF 框架是如何做到界面刷新渲染,包括此调用的顺序以及框架逻辑

阅读本文之前,我期望读者有一定的 WPF 渲染基础,以及了解 WPF 的大架构。本文不会涉及到任何底层渲染相关的知识。阅读本文,你将了解到依赖属性和 WPF 渲染层之间的关系

在开始之前,必须明确一点的是,不是所有的 WPF 应用行为,如依赖属性变更,都会触发渲染变更。有渲染变更不代表立刻将会触发界面刷新,从触发渲染变更到界面刷新,还有以下步骤: 触发渲染,渲染上层收集应用层的绘制渲染的命令,触发渲染线程接收绘制渲染的命令,渲染的下层根据绘制渲染的命令进入 DirectX 渲染管线,由 DirectX 完成后续渲染步骤

本文所聊到的仅仅只是以上的触发渲染,渲染上层收集应用层的绘制渲染的命令这两个步骤。关于 WPF 渲染部分的大框架还请参阅 WPF 渲染原理

本篇博客基于 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客进行更深入 WPF 框架源代码探讨

为了能更好说明 WPF 框架的行为,本文开始先介绍一个测试代码用来测试 WPF 的行为

在本文实际开始之前,还请大家思考一个问题,在 WPF 中,调用 DrawingVisual 的 RenderOpen 方法返回的 DrawingContext 对象里面,传入的参数的属性值影响渲染结果,是一次性的,还是持续的?什么是一次性的,什么是持续的?换个问法是如果传入的值在 DrawingContext 关闭之后,变更属性,此时是否还会影响到渲染结果。答案的是或否就决定了 WPF 底层的实现行为,是否在 DrawingContext 关闭的时候,就直接触发渲染模块,或者就取出了传入的值的数据,断开和传入值之间的影响。如下面最简单的代码

            var drawingVisual = new DrawingVisual();var translateTransform = new TranslateTransform();using (var drawingContext = drawingVisual.RenderOpen()){drawingContext.PushTransform(translateTransform);var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 10, 10));drawingContext.DrawGeometry(Brushes.Red, null, rectangleGeometry);drawingContext.Pop();}SetTranslateTransform(translateTransform);private async void SetTranslateTransform(TranslateTransform translateTransform){while (true){translateTransform.X++;await Task.Delay(TimeSpan.FromMilliseconds(10));}}

以上代码在 SetTranslateTransform 函数里面,不断修改 TranslateTransform 的属性,是否还会影响到 DrawingVisual 的渲染效果?带着这个问题,进入到本文的开始

众所周知,只有在渲染收集触发的时候,才会收集应用层的渲染数据。以 TranslateTransform 为例,在更改 TranslateTransform 的 X 或 Y 属性的值的时候,如果没有给此 TranslateTransform 对象建立直接渲染关系,也就是 Freezable 的 AddSingletonContext 方法没有被传入渲染的直接元素联系的时候,对属性值的更改只是和更改 CLR 自动属性一样,不会有任何的通知和变更。也就是说在 TranslateTransform 对象想要影响到最终界面渲染,需要被动在渲染收集时,才会更新数据

class Freezable
{private void AddSingletonContext(DependencyObject context, DependencyProperty property){// 忽略建立联系代码,这里面比较绕。核心逻辑就是取出 context 里面的 SingletonHandler 委托// 以上的 context 是 RenderData 类型。此 SingletonHandler 委托将会在继承 Freezable 的类型的依赖属性变更的时候,支持被调用// 对于建立直接联系的对象,如存放在 UIElement 上的 TranslateTransform 属性,此时的 SingletonHandler 对象就是由 UIElement 发起的订阅}
}

如在 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客所聊到的实现方式,通过在 DrawingVisual 里面设置一个 TranslateTransform 对象,再将 DrawingVisual 放入到 UIElement 里面。如此行为将让 TranslateTransform 无法和 UIElement 建立直接的联系。以上进入 AddSingletonContext 函数将传入的是属于 DrawingVisual 的 RenderData 对象,这就意味着当 TranslateTransform 的属性变更时,仅仅只能通知到 DrawingVisual 对象,而不能通知到更上层的 UIElement 对象

这完全取决于此应用代码的实现,为了让大家不需要在两篇博客之间来回跳,以下给出用到 WPF 更改 DrawingVisual 的 RenderOpen 用到的对象的内容将持续影响渲染效果 博客的核心代码

以下是一个继承 UIElement 的 Foo 类

    class Foo : UIElement{public Foo(){var drawingVisual = new DrawingVisual();var translateTransform = new TranslateTransform();using (var drawingContext = drawingVisual.RenderOpen()){var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 10, 10));drawingContext.PushTransform(translateTransform);drawingContext.DrawGeometry(Brushes.Red, null, rectangleGeometry);drawingContext.Pop();}Visual = drawingVisual;SetTranslateTransform(translateTransform);}private async void SetTranslateTransform(TranslateTransform translateTransform){while (true){translateTransform.X++;if (translateTransform.X > 700){translateTransform.X = 0;}await Task.Delay(TimeSpan.FromMilliseconds(10));}}protected override Visual GetVisualChild(int index) => Visual;protected override int VisualChildrenCount => 1;private Visual Visual { get; }}

以下是使用此 Foo 的 xaml 代码

    <Grid><local:Foo x:Name="Foo"></local:Foo></Grid>

以上就是本文所有用到的测试辅助的代码

为了更好了解 WPF 框架的底层行为,以上代码被我放入到我私有的 WPF 仓库中,作为 WPF 仓库里面的 demo 的代码。可以从 github 获取本文以上测试代码,获取代码之后,请将 WPFDemo 作为启动项目

以上就是本文构建的测试逻辑。下面将回到主题部分

从 TranslateTransform 属性影响界面逻辑渲染入手,在变更 TranslateTransform 属性时,将因为没有和 Foo 此 UIElement 建立直接的逻辑关系,同时所在的 DrawingVisual 也没有在 Foo 里面被调用 AddVisualChild 方法而加入到可视化树(视觉树)上,因此 TranslateTransform 属性的变更无法通知到 WPF 布局层

更好利用此特性来测试 WPF 框架层的行为。在此先回答一个问题,为什么不通过静态代码阅读了解框架的行为?原因是 WPF 框架太过庞大,我在静态代码阅读过程将受限于记忆而无法从全局把握 WPF 框架逻辑。因此更多的是需要靠测试代码来了解 WPF 框架的逻辑

在 Dispatcher 对象里面,从 VisualStudio 的调试窗口可以看到有没有开放的几个 Reserved 属性,其中一项就是专门给 MediaContext 所使用。如命名,此 MediaContext 类型就是 WPF 渲染上层的渲染上下文,依靠此渲染上下文可以用来控制 WPF 的多媒体(渲染)层的行为

在 WPF 框架里面可以随处见到从 Dispatcher 里面获取 MediaContext 对象的代码

MediaContext mctx = MediaContext.From(dispatcher);

从众多的(不包括动画)触发渲染进入之后,都会汇总到 MediaContext 的 PostRender 方法。此方法是给 Dispatcher 传递一个渲染消息,也就是优先级为 Render 的 RenderMessage 任务。以下是有删减的 PostRender 方法代码

        internal void PostRender(){// 如果当前没有在进入渲染状态,那么开始触发渲染消息if (!_isRendering){if (_currentRenderOp != null){// 如果已有渲染消息在消息队列里,那么更改优先级确保是 Render 优先级。此渲染消息将会很快被调度// If we already have a render operation in the queue, we should// change its priority to render priority so it happens sooner._currentRenderOp.Priority = DispatcherPriority.Render;}else{// 如果还没有渲染消息,那么给 Dispatcher 传入优先级为 Render 的渲染消息// If we don't have a render operation in the queue, add one at// render priority._currentRenderOp = Dispatcher.BeginInvoke(DispatcherPriority.Render, _renderMessage, null);}}}

以上代码的 _renderMessage 就是具体的执行渲染消息,定义如下

        internal MediaContext(Dispatcher dispatcher){_renderMessage = new DispatcherOperationCallback(RenderMessageHandler);}private DispatcherOperationCallback _renderMessage;

歪楼一下,在 WPF 里面,通用的调度使用的委托都是 DispatcherOperationCallback 类型,使用此类型是为了性能考虑。在 Dispatcher 的 WrappedInvoke 方法里面,将会通过 as 判断当前传入的 Delegate 委托类型。使用框架内置的 Action 和 DispatcherOperationCallback 等类型,可以使用明确类型的委托调用,而不需要使用 DynamicInvoke 调用委托来提升性能。详细请看 github 上大佬的更改 内容

通过以上代码可以了解到渲染消息的在于 MediaContext 的 RenderMessageHandler 方法里面。此方法将会被 Dispatcher 使用 Render 优先级进行调用,也会被各个模块触发渲染时加入 Dispatcher 队列

            private object RenderMessageHandler(object resizedCompositionTarget /* can be null if we are not resizing*/){// 忽略调试用的逻辑 RenderMessageHandlerCore(resizedCompositionTarget);}

接着在 RenderMessageHandlerCore 里面将会层层调用,调用到 Render 方法。此方法实现以下功能

  • 渲染每个注册的 ICompositionTarget 以完成批处理。 渲染都是一批批处理的
  • 更新收集的渲染数据
  • 将收集到的数据提交给下层渲染

核心的步骤就是在 更新收集的渲染数据 这一步。这里也就能解答 WPF 的渲染收集是如何触发的

在 更新收集的渲染数据 里面的实现代码如下

        private void RaiseResourcesUpdated(){if (_resourcesUpdatedHandlers != null){DUCE.ChannelSet channelSet = GetChannels();_resourcesUpdatedHandlers(channelSet.Channel, false /* do not skip the "on channel" check */);_resourcesUpdatedHandlers = null;}}

这里的 _resourcesUpdatedHandlers 是委托,在各个资源,如 TranslateTransform 都会注册到 MediaContext 里,也就是在这一层可以让资源可以收到渲染更新的消息

如在 TranslateTransform 的基类 Animatable 里面,就在 RegisterForAsyncUpdateResource 方法注册,代码如下

        internal void RegisterForAsyncUpdateResource(){MediaContext mediaContext = MediaContext.From(Dispatcher);if (!resource.GetHandle(mediaContext.Channel).IsNull){mediaContext.ResourcesUpdated += new MediaContext.ResourcesUpdatedHandler(UpdateResource);}}

如上文,在 WPF 框架里面,可以非常方便从 Dispatcher 拿到 MediaContext 对象,从而也很方便加上 ResourcesUpdated 委托

在此 ResourcesUpdated 事件触发的时候,就需要各个资源向 DUCE.Channel 写入资源的数据,让下层渲染使用。如 TranslateTransform 的实现代码

        internal override void UpdateResource(DUCE.Channel channel, bool skipOnChannelCheck){if (skipOnChannelCheck || _duceResource.IsOnChannel(channel)){base.UpdateResource(channel, skipOnChannelCheck);DUCE.MILCMD_TRANSLATETRANSFORM data;unsafe{data.Type = MILCMD.MilCmdTranslateTransform;data.Handle = _duceResource.GetHandle(channel);data.X = X;data.Y = Y;channel.SendCommand((byte*)&data,sizeof(DUCE.MILCMD_TRANSLATETRANSFORM));}}}

回到本文开始的问题,在 WPF 调用 DrawingContext 的关闭时,此时不会立刻执行界面渲染逻辑。此时离实际的界面渲染还很远,需要先通知到 MediaContext 将渲染消息加入到 Dispatcher 队列。等待 Dispatcher 的调度,接着进入 MediaContext 的层层 Render 方法,再由 Render 方法触发资源收集更新的事件,依靠监听事件让各个资源向 Channel 写入资源的当前状态信息。最后告诉下层渲染,批量收集渲染数据完成,可以开始执行下层渲染逻辑

更多渲染相关博客请看 渲染相关

我搭建了自己的博客 林德熙 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流


本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:lindexi_lindexi_gd_CSDN博客 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

dotnet 读 WPF 源代码笔记 渲染收集是如何触发相关推荐

  1. dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现

    在 WPF 框架提供方便进行像素读写的 WriteableBitmap 类,本文来告诉大家在咱写下像素到 WriteableBitmap 渲染,底层的逻辑 之前我使用 WriteableBitmap ...

  2. dotnet 读 WPF 源代码笔记 AppDomainShutdownMonitor 的设计

    本文是我在读 WPF 源代码做的笔记.在 WPF 中的 AppDomainShutdownMonitor 类是一个不开放的类,这个类当前只是给 D3DImage 类使用.在 AppDomainShut ...

  3. dotnet 读 WPF 源代码笔记 创建 SolidColorBrush 性能没有想象那么差

    在 WPF 中,常用的画刷里面有纯色画刷 SolidColorBrush 类.因为画刷会对应到 DirectX 的资源,因此之前我以为纯色画刷其实会比 Color 会占用更多的资源.在 WPF 中 C ...

  4. dotnet 读 WPF 源代码笔记 了解 WPF 已知问题 用户设备上不存在 Arial 字体将导致应用闪退...

    本文来告诉大家 WPF 已知问题,在用户的设备上,如果不存在 Arial 字体,同时安装了一些诡异的字体,那么也许就会让应用在使用到诡异的字体的时候,软件闪退 在 WPF 的 FontFamily.c ...

  5. dotnet 读 WPF 源代码笔记 提升调试效率的 NamedObject 类型

    本文来聊聊 WPF 那些值得称赞的设计中的 NamedObject 类型.在 WPF 中,有很多值得我学习的设计开发思想,其中就包括本文将要介绍的 NamedObject 类型.此类型的定义仅仅只是为 ...

  6. dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息

    在 WPF 触摸应用中,插入触摸设备,即可在应用里面使用上插入的触摸设备.在 WPF 使用触摸设备的触摸时,需要获取到触摸设备的信息,才能实现触摸 获取触摸设备插入 在 WPF 中,通过 Window ...

  7. 《深入浅出WPF》笔记——绑定篇(一)

    上一节,有记录写到:在WPF里,数据驱动UI,数据占核心地位,UI次之.怎么恢复数据的核心地位,那就要先了解一下Binding. 一.Binding 基础 1.1WPF中Data Binding的带来 ...

  8. 《深入浅出WPF》笔记——事件篇

    如果对事件一点都不了解或者是模棱两可的话,建议先去看张子阳的委托与事件的文章(比较长,或许看完了,也忘记看这一篇了,没事,我会原谅你的)http://www.cnblogs.com/JimmyZhan ...

  9. 在Linux上编译dotnet cli的源代码生成.NET Core SDK的安装包

    .NET 的开源,有了更多的DIY乐趣.这篇博文记录一下在新安装的 Linux Ubuntu 14.04 上通过自己动手编译 dotnet cli 的源代码生成 .net core sdk 的 deb ...

最新文章

  1. 自定义Django的admin界面
  2. sql 与linq的转换
  3. Linux服务器安装NodeJs简易方法
  4. Android通过使用系统广播监听网络状态的改变
  5. matlab球心投影,球心投影
  6. 逻辑回归 - sklearn (LR、LRCV、MLP、RLR)- Python代码实现
  7. python常用函数import_python 常用函数集合
  8. [leetcode]5355. T 秒后青蛙的位置
  9. 程序员工作交接文档怎么写_程序员如何优雅的做好离职交接工作?
  10. 2021-06-22Oracle常用函数
  11. 下面哪个linux内核是稳定版本,哪个 Linux 内核版本是 “稳定的”?
  12. TaiShan服务器介绍
  13. 【英文】Node.js Streams: Everything you need to know //转载
  14. 文件服务器角色提供多种服务 其中,Win2008实战:配置双节点打印服务器故障转移群集...
  15. 将iTunes降级到12.6版本
  16. HC-02蓝牙串口模块的配置和使用
  17. Code Embedding研究系列11-ContraFlow
  18. Flutter,SharedPreferences的同步处理,如Android原生般的
  19. RNA核糖核酸修饰荧光染料|HiLyte Fluor 488/555/594/647/680/750标记RNA核糖核酸
  20. 野狗API的初步使用

热门文章

  1. 二十、D3D12学习笔记——环境光遮蔽
  2. 截至2015年工作中接触集成过的硬件设备整理记录
  3. 基于数据驱动的交叉口精细化管理——以北京门头沟双峪路口为例
  4. Andersen Global在巴西增设法律服务
  5. 节点操作之创建和添加节点
  6. ERROR:ORA-28009: connection to sys should be as sy - [oracle10g]
  7. 『腾讯后台开发』实习生技能要求
  8. 关于TTMS影院系统的总结
  9. Google Earth Engine(GEE)实例代码学习十六——绘制经纬网(Pixel Lon Lat)
  10. ALSA声卡笔记4-----体验声卡