大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移。如有两个放入到 StackPanel 的自定义 UIElement 控件,这两个控件都在 OnRender 方法里面,画出一条从 0 到 100 的线段,此时两个控件画出的直线在窗口里面没有重叠。也就是说在 OnRender 里面绘制的内容将会叠加上元素被布局控件布局的偏移的值

阅读本文,你将了解布局控件是如何影响到里层控件的渲染,以及渲染收集过程中将会如何受到元素坐标的影响

如本文开始的问题,如有两个自定义的 UIElement 控件放到 StackPanel 里面,尽管这两个自定义的 UIElement 使用相同的代码绘制线段,然而在界面呈现的效果不相同。接下来本文将告诉大家在 WPF 框架是如何在布局时影响元素渲染坐标

在 WPF 里面,最底层的界面元素是 Visual 类,在此类型上包含了一个 protected internal 访问权限的 VisualOffset 属性,大概定义如下

  1. protected internal Vector VisualOffset { set; get; }

当然了,在 WPF 框架里面,在 VisualOffset 属性的 set 方法上是有很多代码的,不过这里面代码不是本文的主角,还请大家忽略

此 VisualOffset 属性就是容器控件布局的时候,将会设置元素的偏移的关键属性。尽管此属性是没有公开的,但是咱可以通过 VisualTreeHelper 的 GetOffset 方法获取到此属性的值,因为 GetOffset 方法的代码如下

  1. public static class VisualTreeHelper
  2. {
  3. /// <summary>
  4. /// Returns the offset of the Visual.
  5. /// </summary>
  6. public static Vector GetOffset(Visual reference)
  7. {
  8. return reference.VisualOffset;
  9. }
  10. }

在 UIElement 的 Arrange 方法里面,大家都知道此方法就是用来布局当前控件的。传入的参数就是 Rect 包含了坐标和尺寸,而传入的坐标将会在 UIElement 上被设置到 VisualOffset 属性里面,从而实现在布局时修改元素的偏移量

大概代码如下

  1. public partial class UIElement : Visual, IInputElement, IAnimatable
  2. {
  3. public void Arrange(Rect finalRect)
  4. {
  5. // 忽略很多代码
  6. ArrangeCore(finalRect);
  7. }
  8. protected virtual void ArrangeCore(Rect finalRect)
  9. {
  10. VisualOffset = new Vector(finalRect.X, finalRect.Y);
  11. }
  12. }

通过以上代码可以了解到,实际上的元素的偏移量仅仅只是相对于上层的元素而已,也就是说 VisualOffset 存放的值是相对于上层容器的偏移量,而不是相对于窗口的偏移量

那么此属性是如何影响到元素的渲染的?在 Visual 类型里面,包含了 Render 方法,这就是 Visual 在渲染收集时进入的方法。需要知道的是,调用 Visual 的 Render 方法和 UIElement 的 OnRender 方法是没有直接联系的哦

在开始之前,先来聊聊 Visual 的 Render 方法和 UIElement 的 OnRender 方法。在 UIElement 里面,将会在 Arrange 里面,调用 OnRender 方法收集渲染的指令

  1. public partial class UIElement : Visual, IInputElement, IAnimatable
  2. {
  3. public void Arrange(Rect finalRect)
  4. {
  5. // 忽略很多代码
  6. DrawingContext dc = RenderOpen();
  7. OnRender(dc);
  8. }
  9. protected virtual void OnRender(DrawingContext drawingContext)
  10. {
  11. }
  12. internal DrawingContext RenderOpen()
  13. {
  14. return new VisualDrawingContext(this);
  15. }
  16. }

而 Visual 的 Render 方法的调用堆栈是大概如下

  1. PresentationCore.dll!System.Windows.Media.Visual.Render(System.Windows.Media.RenderContext ctx = {System.Windows.Media.RenderContext}, uint childIndex = 0) 1169 C#
  2. PresentationCore.dll!System.Windows.Media.CompositionTarget.Compile(System.Windows.Media.Composition.DUCE.Channel channel) 465 C#
  3. PresentationCore.dll!System.Windows.Media.CompositionTarget.System.Windows.Media.ICompositionTarget.Render(bool inResize, System.Windows.Media.Composition.DUCE.Channel channel) 346 C#
  4. PresentationCore.dll!System.Windows.Media.MediaContext.Render(System.Windows.Media.ICompositionTarget resizedCompositionTarget = null) 2077 C#

依然入口在 MediaContext 的 Render 方法里面,在这里面将会调用到 Visual 的 Render 方法,此时的 Visual 的第一层就是 RootVisual 然后由 Visual 的 RenderRecursive 方法进行递归调用,让可视化树上的所有 Visual 进行收集渲染

关于 MediaContext 的 Render 方法的调用,请看 dotnet 读 WPF 源代码笔记 渲染收集是如何触发

在 Visual 的 RenderRecursive 方法里面将会更新当前 Visual 层的偏移量,如下面代码

  1. internal void Render(RenderContext ctx, UInt32 childIndex)
  2. {
  3. DUCE.Channel channel = ctx.Channel;
  4. // 在 WPF 里面,不是所有的 Visual 都需要刷新,只有在 Visual 存在变更的时候,影响到渲染才会重新收集
  5. if (CheckFlagsAnd(channel, VisualProxyFlags.IsSubtreeDirtyForRender)
  6. || !IsOnChannel(channel))
  7. {
  8. RenderRecursive(ctx);
  9. }
  10. // 忽略代码
  11. }
  12. internal virtual void RenderRecursive(
  13. RenderContext ctx)
  14. {
  15. DUCE.Channel channel = ctx.Channel;
  16. DUCE.ResourceHandle handle = DUCE.ResourceHandle.Null;
  17. VisualProxyFlags flags = VisualProxyFlags.None;
  18. bool isOnChannel = IsOnChannel(channel);
  19. UpdateCacheMode(channel, handle, flags, isOnChannel);
  20. UpdateTransform(channel, handle, flags, isOnChannel);
  21. UpdateClip(channel, handle, flags, isOnChannel);
  22. UpdateOffset(channel, handle, flags, isOnChannel);
  23. UpdateEffect(channel, handle, flags, isOnChannel);
  24. UpdateGuidelines(channel, handle, flags, isOnChannel);
  25. UpdateContent(ctx, flags, isOnChannel);
  26. UpdateOpacity(channel, handle, flags, isOnChannel);
  27. UpdateOpacityMask(channel, handle, flags, isOnChannel);
  28. UpdateRenderOptions(channel, handle, flags, isOnChannel);
  29. UpdateChildren(ctx, handle);
  30. UpdateScrollableAreaClip(channel, handle, flags, isOnChannel);
  31. }
  32. private void UpdateChildren(RenderContext ctx,
  33. DUCE.ResourceHandle handle)
  34. {
  35. // 递归渲染所有元素
  36. for (int i = 0; i < childCount; i++)
  37. {
  38. Visual child = GetVisualChild(i);
  39. if (child != null)
  40. {
  41. //
  42. // Recurse if the child visual is dirty
  43. // or it has not been marshalled yet.
  44. //
  45. if (child.CheckFlagsAnd(channel, VisualProxyFlags.IsSubtreeDirtyForRender)
  46. || !(child.IsOnChannel(channel)))
  47. {
  48. child.RenderRecursive(ctx);
  49. }
  50. }
  51. }
  52. }
  53. private void UpdateOffset(DUCE.Channel channel,
  54. DUCE.ResourceHandle handle,
  55. VisualProxyFlags flags,
  56. bool isOnChannel)
  57. {
  58. if ((flags & VisualProxyFlags.IsOffsetDirty) != 0)
  59. {
  60. if (isOnChannel || _offset != new Vector())
  61. {
  62. //
  63. // Offset is (0, 0) by default so do not update it for new visuals.
  64. //
  65. DUCE.CompositionNode.SetOffset(
  66. handle,
  67. _offset.X,
  68. _offset.Y,
  69. channel);
  70. }
  71. SetFlags(channel, false, VisualProxyFlags.IsOffsetDirty);
  72. }
  73. }

通过上面代码可以看到,在 WPF 里面,不是所有的 Visual 都会在每次更新界面时,需要重新收集渲染信息。只有被标记了 IsSubtreeDirtyForRender 的 Visual 才会重新收集渲染信息。在 UpdateChildren 方法里面将会递归刷新所有的元素

在 UpdateOffset 方法将会用上 _offset 字段,也就是 VisualOffset 属性的字段,相当于就在这里获取 VisualOffset 的值。通过上面逻辑了解到元素的偏移量影响到元素的渲染核心就是通过在 Visual 的 UpdateOffset 方法将元素的偏移量通过 DUCE.CompositionNode.SetOffset 方法传入到 WPF_GFX 层,也就是实际的渲染控制层

这里面的 CompositionNode 的 SetOffset 方法代码如下

  1. internal static void SetOffset(
  2. DUCE.ResourceHandle hCompositionNode,
  3. double offsetX,
  4. double offsetY,
  5. Channel channel)
  6. {
  7. DUCE.MILCMD_VISUAL_SETOFFSET command;
  8. command.Type = MILCMD.MilCmdVisualSetOffset;
  9. command.Handle = hCompositionNode;
  10. command.offsetX = offsetX;
  11. command.offsetY = offsetY;
  12. unsafe
  13. {
  14. channel.SendCommand(
  15. (byte*)&command,
  16. sizeof(DUCE.MILCMD_VISUAL_SETOFFSET)
  17. );
  18. }
  19. }

实际是调用到 MIL 层的逻辑,以上代码的 hCompositionNode 表示的是在 MIL 层代表此 Visual 的指针。对应的参数将会在 MIL 层进行读取使用,也就是说在 MIL 层将会记录当前元素的偏移量,从而在渲染收集过程,自动给收集到的绘制指令叠加元素偏移量

在 MIL 层将会根据 command.Type = MILCMD.MilCmdVisualSetOffset; 通过一个很大的 switch 语句,进入到大概如下代码

  1. case MilCmdVisualSetOffset:
  2. {
  3. #ifdef DEBUG
  4. if (cbSize != sizeof(MILCMD_VISUAL_SETOFFSET))
  5. {
  6. IFC(WGXERR_UCE_MALFORMEDPACKET);
  7. }
  8. #endif
  9. const MILCMD_VISUAL_SETOFFSET* pCmd =
  10. reinterpret_cast<const MILCMD_VISUAL_SETOFFSET*>(pcvData);
  11. CMilVisual* pResource =
  12. static_cast<CMilVisual*>(pHandleTable->GetResource(
  13. pCmd->Handle,
  14. TYPE_VISUAL
  15. ));
  16. if (pResource == NULL)
  17. {
  18. RIP("Invalid resource handle.");
  19. IFC(WGXERR_UCE_MALFORMEDPACKET);
  20. }
  21. IFC(pResource->ProcessSetOffset(pHandleTable, pCmd));
  22. }
  23. break;

以上代码的核心是调用 pResource->ProcessSetOffset(pHandleTable, pCmd) 方法,而 IFC 只是一个宏而已,用来判断方法返回值的 HResult 是否成功

这里的 ProcessSetOffset 方法的实现代码大概如下

  1. HRESULT
  2. CMilVisual::ProcessSetOffset(
  3. __in_ecount(1) CMilSlaveHandleTable* pHandleTable,
  4. __in_ecount(1) const MILCMD_VISUAL_SETOFFSET* pCmd
  5. )
  6. {
  7. // The packet contains doubles. Should they be floats? Why are we using doubles in managed
  8. // but run the compositor in floats?
  9. float offsetX = (float)pCmd->offsetX;
  10. float offsetY = (float)pCmd->offsetY;
  11. SetOffset(offsetX, offsetY);
  12. return S_OK;
  13. }
  14. void
  15. CMilVisual::SetOffset(
  16. float offsetX,
  17. float offsetY
  18. )
  19. {
  20. // 忽略代码
  21. m_offsetX = offsetX;
  22. m_offsetY = offsetY;
  23. }
  24. float m_offsetX;
  25. float m_offsetY;

以上代码也提了一个问题,为什么在托管层使用的是 double 类型,而在这里使用的 float 类型。我在 GitHub 上尝试去问问大佬们,这个是否有特别的原因,请看 Why the Visual.VisualOffset is double type but run the compositor in floats? · Issue #5389 · dotnet/wpf

太子爷: 为什么在托管层使用的是 double 而在 MIL 层使用的是 float 类型?原因是在托管层将会用到大量的计算,此时如果使用 float 将会因为精度问题而偏差较大,如叠加很多层的布局。但是在 MIL 层面,这是在做最终的渲染,此时使用 float 可以更好的利用显卡的计算资源,因为显卡层面对 float 的计算效率将会更高,而在这一层是最终渲染,不怕丢失精度

在 WPF 框架,将会在元素布局的时候,也就是 UIElement 的 Arrange 方法里面,设置 Visual 的 VisualOffset 属性用于设置元素的偏移量,此元素偏移量是元素相对于上层容器的偏移量。此偏移量将会影响元素渲染收集过程中的绘制坐标。渲染收集里面,在 UIElement 的 OnRender 方法和 Visual 的 Render 方法之间不是顺序调用关系,而是两段不同的调用关系

将会在 UIElement 的布局的时候,从 Arrange 调用到 OnRender 方法,此方法是给开发者进行重写的,绘制开发者业务上的界面使用。此过程将是作为开发者绘制内容的渲染收集,此过程可以不在 WPF 渲染消息触发时被触发,可以由开发者端发起。在 WPF 的渲染消息进入时,将会到达 MediaContext 的 Render 方法,此方法将会层层调用进入 Visual 的 Render 方法,在此 Render 方法将会递归可视化树的元素进行收集渲染指令,这是应用的渲染收集过程

在 Visual 的 Render 方法里面,将会传输 VisualOffset 的数据到 MIL 层,由底层控制渲染的 MIL 层使用此属性决定渲染命令的偏移量

当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建

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

dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标的更多相关文章

  1. dotnet 读 WPF 源代码笔记 渲染收集是如何触发

    在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...

  2. 布局时margin会影响父元素

    布局时margin会影响父元素.md 在布局使用margin时 <div class="login-bg"> <div class="login&quo ...

  3. WPF学习笔记(8):DataGrid单元格数字为空时避免验证问题的解决

    原文:WPF学习笔记(8):DataGrid单元格数字为空时避免验证问题的解决 如下图,在凭证编辑窗体中,有的单元格不需要数字,但如果录入数字后再删除,会触发数字验证,单元格显示红色框线,导致不能执行 ...

  4. 《深入浅出WPF》笔记——绘画与动画

    <深入浅出WPF>笔记——绘画与动画   本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...

  5. WPF源代码分析系列一:剖析WPF模板机制的内部实现(一)

    众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElement.FrameworkElment.Control等)都直接或间接继承自Vi ...

  6. 读Flask源代码学习Python--config原理

    读Flask源代码学习Python--config原理 个人学习笔记,水平有限.如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!. 起因   莫名其妙在第一份工作中使用了从来没有接 ...

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

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

  8. 《深入浅出WPF》笔记——资源篇

    原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...

  9. 《深入浅出WPF》笔记——模板篇

    原文:<深入浅出WPF>笔记--模板篇 我们通常说的模板是用来参照的,同样在WPF中,模板是用来作为制作控件的参照. 一.认识模板 1.1WPF菜鸟看模板 前面的记录有提过,控件主要是算法 ...

随机推荐

  1. Windows10 + Chrome 触发蓝屏

    姿势一: 打开Chrome,输入路径:\\.\globalroot\device\condrv\kernelconnect 姿势二: 将代码 <iframe src="\\.\glob ...

  2. Flutter 与 Swift - 在创建 iOS 应用程序时应该押注什么技术?

    Swift 和 Flutter 是考虑创建 iOS 应用程序的公司最想要的两种技术.开发者能用原生技术取胜吗?如何选择,哪种更适合您的应用?让我们一探究竟吧! 根据 Statista 的数据, 201 ...

  3. NLP与深度学习(一)NLP任务流程

    1. 自然语言处理简介 根据工业界的估计,仅有21% 的数据是以结构化的形式展现的[1].在日常生活中,大量的数据是以文本.语音的方式产生(例如短信.微博.录音.聊天记录等等),这种方式是高度无结构化 ...

  4. redis的过期策略和淘汰策略

    过期键删除策略 1.定时删除:在设置键的过期时间的同时,创建一个定时器timer,让定时器在键过期时间来临时,立即执行对键的删除操作. 2.惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查 ...

  5. "image watch" for QtCreator

    Image Watch Image Watch 是Visual Studio的一个插件,用来在C++ 调试时显示内存中的位图图像.可以直观的看到图像的变化而不用添加额外的显示代码.其内建了对OpenC ...

  6. .net core 微服务参考文章

    网址: https://www.cnblogs.com/edisonchou/p/9124985.html Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.Consul基础介绍 Co ...

  7. COM笔记-包容与聚合

    COM不支持实现继承的原因在于这种继承方式将使得一个对象的实现同另外一个对象的实现紧紧地关联起来.在这种情况下,当基类的实现被修改后,派生类将无法正常运行而必须被修改.这就是为什么一些用C++编写大型 ...

  8. .NET WebApi 实战第五讲之EntityFramework事务

    在<.NET WebApi 实战第二讲>中我们有提到过事务的概念!任何数据库的读操作可以没有事务,但是写事件必须有事务,如果一个后端工程师在数据库写入时未添加事务,那就不是一个合格的工程师 ...

  9. SSM整合二

    总结 <!-- 批量删除 --> <delete id="deleteAll"> delete from tbl_emp where emp_id in & ...

  10. 实现Comparable接口

    1 import java.util.TreeSet; 2 3 4 /** 5 * PriorityQueue, TreeSet是排序集合,存储的对象必须实现Comparable接口. 6 * 原因是 ...