精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。点名“简易百科”和闲暇巴盗用锐英源原创内容。
最近开发企业信息管理软件,使用了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标志:
有趣的是,属性值更改不仅可以强制其所属控件的新布局,还可以强制控件的父级的新布局。另一个有趣的点是,属性值的改变可以表明只需要渲染,这在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(()时。
您可以在此处找到实际的源代码:
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