上一章分析了WPF元素的内部工做元素——容许每一个元素插入到WPF布局系统的MeasureOverride()和ArrangeOverride()方法中。本章将进一步深刻分析和研究元素如何渲染自身。ide
大多数WPF元素经过组合方式建立可视化外观。换句话说,典型的元素经过其余更基础的元素进行构建。例如,使用标记定义用户控件的组合元素,处理标记的方式与自定义窗口中的XAML相同。使用控件模板为自定义控件定义可视化树。而且当建立自定义面板时,根本没必要定义任何可视化细节。组合元素由克难攻坚使用者提供,并添加到Children集合。 布局
固然,知道如今才能使用组合。最终,一些类须要负责绘制内容。在WPF中,这些类位于元素树的底层。在典型窗口中,是经过单独的文本、形状以及位图执行渲染的,而不是经过高级元素。this
1、OnRender()方法编码
为了执行自定义渲染,元素必须重写OnRender()方法,该方法继承自UIElement基类。OnRender()方法未必不须要替换组合——一些控件使用OnRender()方法绘制可视化细节并使用组合在其上叠加其余元素。Border和Panel类是两个例子,Border类在OnRender()方法中绘制边框,Panel类在OnRender()方法中绘制背景。Border和Panel类都支持子内容,而且这些子内容在自定义的绘图细节之上进行渲染。spa
OnRender()方法接受一个DrawingContext对象,该对象为绘制内容提供了了一套颇有用的方法。在OnRender()方法中执行绘图的主要区别是不能显示地建立和关闭DrawingContext对象。这是由于几个不一样的OnRender()方法可能使用相同的DrawingContext对象。例如,派生的元素能够执行一些自定义绘图操做并调用基类中的OnRender()方法来绘制其余内容。这种方法是可行的,由于当开始这一过程时,WPF会自动建立DrawingContext对象,而且当再也不须要时关闭对象。设计
关于WPF渲染,最使人惊奇的细节是实际上只须要使用不多的类。大多数类是经过其余更简单的类构建的,而且对于典型的控件,为了找到实际重写OnRender()方法的类,须要进入到控件元素树中很是深的层次。下面是一些重写OnRender()方法的类:code
一般,OnRender()方法的实现看起来很简单。例如,下面是继承自Shape类的全部渲染代码:视频
protected override void OnRender(DrawingContext drawingContext) { this.EnsureRenderedGeometry(); if(this._renderedGeometry!=Geometry.Empty) { drawingContext.DrawingGeometry(this.Fill,this.GetPen(),this._renderedGeometry); } }
请记住,重写OnRender()方法不是渲染内容而且将其添加到用户界面的惟一方法。也能够建立DrawingVisual对象,并是哟AddVisualChild()方法为UIElement对象添加该可视化对象。而后能够调用DrawingVisual.RenderOpen()方法为DrawingVisual对象检索DrawingContext对象,并使用返回的DrawingContext对象渲染DrawingVisual对象的内容。对象
在WPF中,一些元素使用这种策略在其余元素内容之上显示一些图形细节。例如,在拖放指示器、错误指示器以及焦点框中能够看到这种状况。在全部这些状况中,DrawingVisual类容许元素在其余内容之上绘制内容,而不是在其余内容之下绘制内容。但对于大部分状况,是在专门的OnRender()方法中进行渲染。blog
2、评估自定义绘图
当建立自定义元素时,可能会选择重写OnRender()方法来绘制自定义内容。可在包含内容的元素(最多见的状况是继承自Decorator的类)中重写OnRender()方法,从而能够在内容周围添加图形装饰。也能够在没有任何嵌套内容的元素中重写OnRender()方法,从而能够绘制元素的整个可视化外观。例如,能够建立绘制一些小的图形细节的自定义元素,而后能够经过组合,在其余类中使用自定义元素。WPF中的这方面示例是TickBar元素,该元素为Slider控件绘制刻度标记。TickBar元素经过Slider控件的默认控件模板(该模板还包括一个Border和一个Track元素,Track元素又包含了两个RepeatButton控件和一个Thumb元素)嵌入到Slider控件的可视化树中。
一个明显的问题是须要肯定什么时候使用较低级的OnRender()方法,以及什么时候使用其余类(l例如,继承自Shape类的元素)的组合来绘制所需的内容。为了作出决定,须要评估所需图形的复杂程度以及但愿提供的交互能力。
例如,分析一下ButtonChrome类。在ButtonChrome类的WPF实现中,自定义的渲染代码考虑了各类属性,包括RenderDefaulted、RenderMouseOver以及RenderPressed。Button类的默认控件模板在适当的时机使用触发器设置这些属性。例如,当将鼠标移动到按钮上时,Button类使用触发器将ButtonChrome.RenderMouseOver属性设置为true。
不管什么时候改变RenderDefaulted、RenderMouseOver或RenderPressed属性,ButtonChrome类都会调用基本的InvalidateVisual()方法来指示当前外观不在有效。WPF而后调用ButtonChrome.OnRender()方法来获取新的图形表示。
若是ButtonChrome类使用组合,这种行为就更难实现。使用合适的元素为ButtonChrome类建立标准外观很容易,可是当按钮的状态发生变化是,须要作更多的工做来修改外观。须要动态改变构成ButtonChrome类的嵌套元素,若是外观变化很大的话,就必须隐藏一个元素并在合适的位置显示另外一个元素。
大多数自定义元素不须要自定义渲染。可是当属性发生变化或执行特定操做是,须要渲染复杂的变化很大的可视化外观,此时使用自定义的渲染方法可能更加简单而且更便捷。
3、自定义绘图元素
经过前面对OnRender()方法的介绍,理解其工做原理。下面使用OnRender()方法建立自定义控件。
下面建立了一个名为CustomDrawnElement的元素,演示了一种简单的效果。该元素使用RadialGradientBrush画刷绘制阴影背景,技巧是动态设置强调显示的渐变起点,使用其跟随鼠标。从而当用户在控件上移动鼠标时,白色的发光中心点跟随鼠标移动。
CustomDrawnElement元素不须要包含任何子内容,因此它直接继承自FrameworkElement类。该元素只提供了一个能够设置的属性——渐变的背景色。
public class CustomDrawnElement:FrameworkElement { public static DependencyProperty BackgroundColorProperty; static CustomDrawnElement() { FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(Colors.Yellow); metadata.AffectsRender = true; BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElement), metadata); } public Color BackgroundColor { get { return (Color)GetValue(BackgroundColorProperty); } set { SetValue(BackgroundColorProperty, value); } } ... }
BackgroundColor依赖性属性使用FrameworkPropertyMetadata.AffectRender标志明确进行了标识。所以,不管什么时候改变了背景色,WPF都自动调用OnRender()方法。然而,当鼠标移动到新的位置时,也须要确保调用OnRender()方法。这是经过在合适的时间调用InvalidateVisual()方法实现的。
. . . protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); this.InvalidateVisual(); } protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); this.InvalidateVisual(); } . . .
剩下的惟一细节是渲染代码。渲染代码使用DrawingContext.DrawRectangle()方法绘制元素的背景。ActualWidth和ActualHeight属性只是控件最终的渲染尺寸。
. . . protected override void OnRender(DrawingContext dc) { base.OnRender(dc); Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight); dc.DrawRectangle(GetForegroundBrush(), null, bounds); } . . .
最后,名为GetForegroundBrush()的私有辅助方法根据鼠标的当前位置构造正确的RadialGradientBrush画刷。为了计算中心点,须要将鼠标在元素上悬停的当前位置转换成从0到1的相对位置,这正是RadialGradientBrush画刷指望的结果。
. . . private Brush GetForegroundBrush() { if (!IsMouseOver) { return new SolidColorBrush(BackgroundColor); } else { RadialGradientBrush brush = new RadialGradientBrush(Colors.White, BackgroundColor); Point absoluteGradientOrigin = Mouse.GetPosition(this); Point relativeGradientOrigin = new Point( absoluteGradientOrigin.X / base.ActualWidth, absoluteGradientOrigin.Y / base.ActualHeight); brush.GradientOrigin = relativeGradientOrigin; brush.Center = relativeGradientOrigin; brush.Freeze(); return brush; } } . . .
4、建立自定义装饰元素
做为一条通用规则,切勿在控件中使用自定义绘图。若是在控件中使用自定义绘图,就违反了WPF无外观空间的承诺。问题是一旦硬编码一些绘图逻辑,就会使控件可视化外观的一部分不能经过控件模板进行定制。更好的方法是设计单独的绘制自定义内容的元素(如上面示例中的CustomDrawnElement类),而后在控件的默认模板内部使用自定义元素。
有必要快速分析一下如何修改上面示例,使其可以成为控件模板的一部分。在控件模板中,自定义绘图元素一般扮演两个角色:
第二种方法须要自定义装饰元素,能够经过两个轻微的改动将CustomDrawnElement类转换成自定义绘图元素。首先,使该类继承自Decorator类:
public class CustomDrawnDecorator:Decorator
而后重写OnMeasure()方法,指定须要的尺寸,全部装饰元素都会考虑它们的子元素,增长装饰所须要的额外空间,而后返回组合以后的尺寸。CustomDrawnDecorator类不须要任何额外的空间来绘制边框,相反,使用下面的代码简单地使其自定和其内容具备相同的尺寸:
protected override Size MeasureOverride(Size constraint) { UIElement child = this.Child; if (child != null) { child.Measure(constraint); return child.DesiredSize; } else { return new Size(); } }
一旦建立自定义装饰元素,就能够在自定义控件模板中使用它们。例如,下面的按钮模板在按钮内容的后面放置了跟随鼠标踪影的渐变背景。使用模板绑定确保使用对齐属性和内边距属性。
<ControlTemplate x:Key="ButtonWithCustomChrome"> <lib:CustomDrawnDecorator BackgroundColor="LightGreen"> <ContentPresenter Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" Content="{TemplateBinding ContentControl.Content}" RecognizesAccessKey="True" /> </lib:CustomDrawnDecorator> </ControlTemplate>
如今可使用这个模板从新样式化按钮,使其具备新的外观。固然,为了使自定义装饰元素更加实用,当单击鼠标按钮时可能更但愿改变它的外观。使用修改装饰类属性的触发器能够完成该工做。
本章示例源码:CustomDrawnElement.zip