锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 开源技术 / WPF / 深度解析WPF布局和渲染
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft

锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。点名“简易百科”和闲暇巴盗用锐英源原创内容。

深度解析WPF布局和渲染


最近开发企业信息管理软件,使用了WPF,对WPF的强大功能有了充分了解。本来翻译自codeproject,对理解WPF界面机制非常有好处,codeproject看不懂,找锐英源软件。

 

WPF控件继承

在我们讨论布局的细节(即,测量控件需要多少屏幕空间并将其放置在屏幕上)之前,我们需要简要讨论布局的对象。在本文中,我将它们称为控件,它可以是从FrameworkElement继承的任何内容,如控件或TextBox或。。。

 

对象->DispatcherObject->DependencyObject->Visual->UIElement->FrameworkElement->Control->TextBoxBase->TextBox

 

以下课程对布局讨论很重要:

 

DispatcherObject:非常简单,只有Dispatcher属性,它控制WPF UI线程的执行。Dispatcher还管理以何种顺序测量、排列和呈现控件。

DependencyObject:可以拥有WPF依赖属性的对象

Visual:是一个抽象类,包含将控件呈现(绘制)到屏幕所需的属性

UIElement:包含决定此控件是否需要布局和渲染的属性。一些与布局相关的属性:DesiredSize、IsArrangeValid、IsMeasureValid、RenderSize

FrameworkElement:包含逻辑树(Parent,LogicalChildren)和视觉树信息(VisualChildrenCount,GetVisualChild())以及一些大小信息(ActualHeight,Height,VerticalAlignment,Margin,Padding)。

 

WPF布局流程概述

WPF使用两个线程,一个用于渲染,另一个用于管理UI。您编写的WPF代码在UI线程上运行。渲染线程在场景后面运行,用渲染指令填充GPU(图形处理单元)。几乎没有关于渲染线程的任何文档,但幸运的是,我们不需要知道关于它的任何细节。

 

WPF对所有代码只使用一个线程具有巨大的优势,即没有多线程问题,即两个线程试图同时更改相同的数据。

 

UI代码的执行不是线性的,而是分三个阶段运行,这三个阶段会反复进行:

 

第1阶段

在Window生命周期的开始,代码调用其构造函数或执行XAML会创建一个由Visuals组成的树,其中Window可能是用户在窗口中看到的所有其他内容的根。施工完成后,第2阶段开始。稍后,由于属性更改、鼠标单击、窗口大小更改以及许多其他原因而引发的某些事件,阶段1可能会再次运行。在第2阶段开始之前完成所有第1阶段代码的好处是,如果不同的代码部分要求再次布局控件,那么每次请求时,布局只会立即执行一次。

 

第2阶段

一旦WPF完成了想要在阶段1中运行的所有操作,它就通过遍历树并要求树中的每个孩子来测量自己来测量所有控件。

 

第3阶段

一旦孩子们被测量好,他们就会被安排好,也就是说,每一位家长都会告诉他们的孩子应该在哪里画画,以及他们能得到多少空间。

如有必要,一旦排列了子对象,它就会得到一个DrawingContext,可以用来编写绘图指令(=渲染)。

 

UI线程由Dispatcher控制。Dispatcher根据活动的优先级选择活动。第1阶段的活动具有最高优先级(正常)。即使在阶段1中运行的方法将Visual标记为布局,这也不会立即发生。相反,该Visual被分配给ContextLayoutManager中的MeasureQueue。一旦Dispatcher处理了所有具有正常优先级的活动,它就告诉ContextLayoutManager处理MeasureQueue中的可视化(第2阶段)。一旦处理完所有的视觉效果,Dispatcher就会通知其ContextLayoutManager处理ArrangeQueue中的所有视觉效果(阶段3,DispatterPriority.Render)。

 

从自己的代码开始布局

WPF通常知道控件何时需要再次布局,例如,如果用户更改了控件所在窗口的大小。但有时,只有您的代码知道您的控件需要再次布局,例如,因为用户单击了按钮或计时器。您的控件可能仍然具有相同的大小,因此严格来说,不需要新的测量和排列,只需要渲染,唉,WPF不允许您只请求渲染。

 

您的代码可以调用以强制布局和渲染的UIElement方法列表:

 

InvalidateMeasure():将Visual添加到MeasurementQueue并设置其MeasureDirty标志。稍后执行测量(阶段2)。如果该Visual的DesiredSize发生变化,则在测量所有Visual后将调用该Visual的Arrange()。

InvalidateArrange():将Visual添加到ArrangeQueue并设置其ArrangeDirty标志。安排在稍后的时间执行(阶段3)。如果该Visual的RenderSize发生更改,则UIElement。Arrange()作为其代码的一部分立即调用OnRender()。请注意,在使用InvalidateArrange()时,如果控件的大小没有改变,甚至控件的内容可能已经改变,则不会执行渲染。

InvalidateVisual():人们可能希望您的代码可以告诉WPF只需要呈现Visual,而不需要布局。唉,WPF不允许你这么做。OnRender()只能从UIElement.Arrange()中调用。因此,InvalidateVisual()必须将Visual添加到ArrangeQueue,并设置RenderingInvalidated标志。

 

DependencyProperty和布局

WPF使用自己的属性系统。定义WPF属性时,可以指示更改该属性的值是否需要对该FrameworkElement进行新的布局。例如,FrameworkElement的Width属性定义如下:

 

public static readonly DependencyProperty WidthProperty =
DependencyProperty.Register(
"Width",
typeof(double),
typeof(FrameworkElement),
new FrameworkPropertyMetadata(
Double.NaN,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnTransformDirty)),
new ValidateValueCallback(IsWidthHeightValid));

对于我们的讨论,有趣的是FrameworkPropertyMetadataOptions。AffectsMeasure,表示如果属性值发生变化,则需要测量FrameworkElement。该FrameworkElement会自动添加到MeasureQueue及其MeasureDirty标志集。

 

实际上有五种不同的FrameworkPropertyMetadataOptions。影响Xxx标志:

 

  1. AffectsArrange
  2. AffectsMeasure
  3. AffectsParentArrange
  4. AffectsParentMeasure
  5. AffectsRender

 

有趣的是,属性值更改不仅可以强制其所属控件的新布局,还可以强制控件的父级的新布局。另一个有趣的点是,属性值的改变可以表明只需要渲染,这在InvalidateXxx()方法中是不可能的。

 

谁调用UIElement。Measure()和UIElement.Arrange()?

像Window或Grid这样的WPF容器是其他WPF控件的父级。是父级调用其一个子级或多个子级的Measure()和Arrange(),因为只有父级知道子级有多少屏幕空间可用。因此,在父母面前衡量或安排孩子是没有意义的。当需要布局时,WPF必须从树根开始,例如,使用Window并从那里遍历整个树,或者,如果只需要布局树的一部分,则从该部分树的根开始。为布局找到正确的控件是ContextLayoutManager的工作。

 

上下文布局管理器

我只是在写这篇文章时发现了ContextLayoutManager,这意味着我下面的描述可能不是100%准确。另一方面,ContextLayoutManager在幕后完成其工作,我们不需要了解所有细节。

 

Dispatcher有一个UI线程可能需要执行的所有活动的队列,例如对鼠标单击或计时器滴答声作出反应、打开窗口或进行布局……这些活动按优先级排序,其中一个原因是确保在布局开始前完成所有正常的UI活动。Dispatcher还拥有一个ContextLayoutManager,它基本上为所有需要测量的可视化对象维护一个队列,为所有需要安排的可视化对象保持一个队列。一旦测量时间到了(=阶段2,所有更高优先级的操作都已完成,树的至少一部分需要布局),ContextLayoutManager将搜索最接近需要测量的根的Visual,并调用其Measure(),然后调用其子级的所有Measure),这些子级对所有子级执行相同的操作,以此类推。每个被测量的视觉都会从MeasureQueue中移除。一旦子树被完全测量,一些可视化可能会留在MeasureQueue中,因为它们属于另一个子树,ContextLayoutManager开始处理该树,直到Measure队列为空。在此之后,ArrangeQueue(=Phase3)再次发生相同的事情。

 

如果没有改变大小,并且通过设置ArrangeDirty标志明确要求不进行排列,则可能只执行测量,而不进行排列。

 

如果之前已经执行了测量,并且不需要新的测量,则可能只执行排列,而不执行测量。

 

第2阶段:测量

YourControl.Measure()
UIElement.Measure()

  FrameworkElement.MeasurementCore()

    virtual FrameworkElement.MeasureOverride()
    override YourControl.MeasureOverride()

父控件调用YourControl.Measure()实际上是UIElement对Measure()的调用,稍后调用FrameworkElement.MeasureCore(),稍后调用FrameworkElement.MeasureOverride(),它被YourControlMeasureOverride()覆盖,其中控件具有测量自身的代码。

 

注意:以下是伪代码,它试图只显示基本内容。实际的代码要复杂得多。伪代码包括来自不同类的代码,比如当UI代码调用被FrameworkElement.MeasurementCore()覆盖的MeasureCore(()时。

 

您可以在此处找到实际的源代码:

UIElement

FrameworkElement

              

void UIElement.Measure(avialableSize){
//1)
if (IsNaN(availableSize) throw Exception
if (Visibility==Collapsed) return;
if (avialableSize==previousAvialableSize && !isMeasureDirty) return;

//2)
ArrangeDirty = true;
desiredSize = MeasureCore(availableSize);

virtual Size UIElement.MeasureCore(Size availableSize) {}
override Size FrameworkElement.MeasureCore(availableSize){
//3)
frameworkAvailableSize = availableSize - Margin;
if (frameworkAvailableSize>UIElement.MaxSize)
frameworkAvailableSize = UIElement.MaxSize;
if (frameworkAvailableSize<UIElement.MinSize)
frameworkAvailableSize = UIElement.MinSize;
desiredSize = MeasureOverride(frameworkAvailableSize);

virtual Size FrameworkElement.MeasureOverride(Size availableSize){}
override Size YourControl.MeasureOverride(Size constraint){
//4) here you write the code measuring the control
return desiredSize;
}

//5)
desiredSize += Margin
if (desiredSize<UIElement.MinSize)
desiredSize = UIElement.MinSize;
if (desiredSize>UIElement.MaxSize)
desiredSize = UIElement.MaxSize;
return desiredSize;
}

//6)
if (IsNaN(desiredSize) throw Exception
if (IsPositiveInfinity(desiredSize) throw Exception
MeasureDirty = false;
}

伪代码可能看起来有点混乱。重要的是:

 

如果控件被折叠,或者自上次Measure()调用以来可用空间没有改变,代码将立即返回。如果可用空间没有改变,Measure()将不会强制稍后运行Arrange()。

如果可用大小已更改,则稍后将执行Arranged(),即使desiredSize未更改!

FrameworkElement从可用大小中减去边距。FrameworkElement确保availableSize小于控件的MaxSize。MaxSize实际上不是WPF属性。我用它作为MaxWidth和MaxHeight的缩写。FrameworkElement确保availableSize大于控件的MinSize。

要对控件进行自己的测量,请重写控件中的MeasureOverride()。

一旦您的代码返回了desiredSize,FrameworkElement就会将Margin添加到desiredSize。FrameworkElement确保desiredSize大于等于MinSize,小于等于MaxSize。如果定义了“宽度”或“高度”,则它们将否决“最小尺寸”,并且所需的“尺寸”将成为“宽度”和“高度”所指定的任何尺寸。

最后,当desiredSize不是数字或无穷大时,UIELement抛出异常。

有趣的是,输入availableSize可以是无限的,但输出desiredSize不能是无限的。例如,当父级是ScrollViewer时,无限大小作为输入是有意义的。在ScrollViewer中,每个孩子都可以使用任意多的空间。基本上,当父母给孩子无限的空间时,它会问孩子想要多少空间,没有任何限制。

 

如果子对象不知道应该使用多少空间,它可以只返回约束,但也可以返回新的大小(0,0)。在安排过程中,家长可能不关心孩子需要多少空间,甚至给孩子更多的空间。例如,当子对象的“对齐”(Alignment)设置为“拉伸”(Stretch)时,父对象会提供所有可用空间,即使子对象要求的空间更少。

 

请注意,FrameworkElement负责Margin、MaxSize和MinSize,但不处理Border和Padding,这意味着您必须在代码中处理Padding(如果您的控件支持)。

 

第3阶段:布置(包括渲染)

YourControl.Arrange()
UIElement.Arrange()

  FrameworkElement.ArrangeCore()

    virtual FrameworkElement.ArrangeOverride()
    override YourControl.ArrangeOverride()

  virtual UIElement.OnRender()
  override YourControl.OnRender ()

父控件调用YourControl.Arrange()实际上是UIElement的Arrange()调用。稍后调用FrameworkElement.ArrangeCore(),稍后调用FrameworkElement.ArrangeOverride(),它被YourControl.ArrangeOverride()覆盖,其中您的控件具有度量自身的代码。 如果控件的RenderSize已更改或其RenderingInvalidated标志已设置,UIElement也会调用UIElement.OnRender(),由YourControl.OnRender()覆盖,其中控件包含创建渲染指令的代码。

void UIElement.Arrange(Rect finalRect) {
  //1)
  if (IsNaN(finalRect) throw Exception
  if (IsPositiveInfinity(finalRect) throw Exception
  if (Visibility==Collapsed) return;
  //2)
    if (MeasureDirty){
    UIElement.Measure(PreviousConstraint)
  }
  //3)
  if (finalRect==previousFinalRect && !isArrangeDirty) return;
  UIElement.ArrangeCore(finalRect);

    virtual void UIElement.ArrangeCore(Rect finalRect){}
    override void FrameworkElement.ArrangeCore(Rect finalRect){
      //4)
      Size arrangeSize = finalRect.Size;
      arrangeSize = Math.Max(arrangeSize - Margin, 0);
      if (Alignment!= Stretch) arrangeSize = desiredSize;
      if (arrangeSize>MaxSize) arrangeSize = MaxSize;
      RenderSize = ArrangeOverride(arrangeSize);

        virtual Size UIElement.ArrangeOverride(Size finalSize){}
        override Size YourControl.ArrangeOverride(Size arrangeBounds) {
           //5) here, you write the code measuring the control
         }

      //6) this is followed by some complicated code doing clipping and LayoutTransform
    }
   
  ArrangeDirty = false;
  //7)
  if ((sizeChanged || RenderingInvalidated || firstArrange){
    DrawingContext dc = RenderOpen();
     OnRender(dc);

    virtual void UIElement.OnRender(DrawingContext drawingContext)
    override void YourControl.OnRender(DrawingContext drawingContext) {
      //9) here you write the code rendering the control
    }
  }

对于测量,如果父级提供无限空间,则可以。但如果父级传递无限空间进行排列,则会抛出异常。如果控件被折叠,Arrange()将返回。

理论上,在布置之前,应始终测量控件。但是,当Arrange()注意到Measure()之前没有被正确调用,即MeasureDirty仍然被设置时,Arrange()会立即调用Measure()。

如果可用空间未更改且未设置isArrangeDirty,Arrange()将返回。

arrangeSize也可能会因为剪裁而调整,这在伪代码中没有显示。由于LayoutTransform(伪代码中未显示),arrangeSize也可能会被调整。

Arrange(Rect finalRect)接收包含坐标X、Y和大小的Rect,而ArrangeOverride(Size finalSize)仅接收finalRect.Size。当子对象自行排列时,它不知道其父对象内自己的X和Y坐标。要对控件进行自己的排列和渲染,请重写控件中的ArrangeOverride()。

控件完成排列和编写呈现指令后,UIElement。Arrange()继续进行一些裁剪和布局转换计算。

如果渲染大小已更改或设置了RenderingInvalidated标志,则UIElement。Arrange()调用UIElement。在控件中重写的OnRender()。在那里,您可以放置渲染说明。

请注意,如果对齐设置为拉伸,则父对象将为子对象提供所有可用空间,而不仅仅是子对象所需的所需大小。

 

不同尺寸特性的含义

当我开始使用WPF时,我经常错误地假设控制。Width将告诉我屏幕上控件的宽度是多少,这一点都不正确:

 

宽度:可以在XAML中设置一个属性,该属性可用于建议控件的宽度,默认值为double。Nan(即未使用)。

高度:可以在XAML中设置一个属性,该属性可用于建议控件的高度,默认值为double。Nan(即未使用)。

DesiredSize:您的控件在MeasureOverride()结束时请求的大小,加上添加了边距,并强制执行宽度/高度、最小宽度/最小高度和最大宽度/最大高度(如果已定义)。DesiredSize由父控件用于安排。

RenderSize:父控件为渲染提供的大小。RenderSize与DesiredSize不同,因为a)DesiredSize有边距,而RenderSize没有边距,并且b)父级可能决定给子级一个不同于请求的大小。

ActualWidth:实际上是RenderSize.Width

ActualHeight:实际上是RenderSize.Height

友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州大学北校区院(文化路97号院)内