【WPF学习】第六十八章 自定义绘图元素
上一章分析了wpf元素的内部工作元素——允许每个元素插入到wpf布局系统的measureoverride()和arrangeoverride()方法中。本章将进一步深入分析和研究元素如何渲染自身。
大多数wpf元素通过组合方式创建可视化外观。换句话说,典型的元素通过其他更基础的元素进行构建。例如,使用标记定义用户控件的组合元素,处理标记的方式与自定义窗口中的xaml相同。使用控件模板为自定义控件定义可视化树。并且当创建自定义面板时,根本不必定义任何可视化细节。组合元素由克难攻坚使用者提供,并添加到children集合。
当然,知道现在才能使用组合。最终,一些类需要负责绘制内容。在wpf中,这些类位于元素树的底层。在典型窗口中,是通过单独的文本、形状以及位图执行渲染的,而不是通过高级元素。
一、onrender()方法
为了执行自定义渲染,元素必须重写onrender()方法,该方法继承自uielement基类。onrender()方法未必不需要替换组合——一些控件使用onrender()方法绘制可视化细节并使用组合在其上叠加其他元素。border和panel类是两个例子,border类在onrender()方法中绘制边框,panel类在onrender()方法中绘制背景。border和panel类都支持子内容,并且这些子内容在自定义的绘图细节之上进行渲染。
onrender()方法接受一个drawingcontext对象,该对象为绘制内容提供了了一套很有用的方法。在onrender()方法中执行绘图的主要区别是不能显示地创建和关闭drawingcontext对象。这是因为几个不同的onrender()方法可能使用相同的drawingcontext对象。例如,派生的元素可以执行一些自定义绘图操作并调用基类中的onrender()方法来绘制其他内容。这种方法是可行的,因为当开始这一过程时,wpf会自动创建drawingcontext对象,并且当不再需要时关闭对象。
关于wpf渲染,最令人惊奇的细节是实际上只需要使用很少的类。大多数类是通过其他更简单的类构建的,并且对于典型的控件,为了找到实际重写onrender()方法的类,需要进入到控件元素树中非常深的层次。下面是一些重写onrender()方法的类:
- textblock类。无论在何处放置文本,都有textblock对象使用onrender()方法绘制文本。
- image类。image类重写onrender()方法,使用drawingcontext.drawimage()方法绘制图形内容。
- mediaelement类。如果正在使用该类播放视频文件,该类会重写onrender()方法以绘制视频帧。
- 各种形状类。shape基类重写了onrender()方法,通过使用drawingcontext.drawgeometry()方法,绘制在其内部存储的geometry对象。根据shape类的特定派生类,geometry对象可以表示椭圆、矩形或更复杂的由直线和曲线构成的路径。许多元素使用形状绘制小的可视化细节。
- 各种修饰类。这些类(如buttonchrome和listboxchrome)绘制通用控件的外侧外观,并在具*定的内部放置内容。其他许多继承自decorator的类,如border类,都重写了onrender()方法。
- 各种面板类。尽管面板的内容是由其子元素提供的,但是onrender()方法绘制具有背景色(假设设置了background属性)的矩形。
通常,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()方法中进行渲染。
二、评估自定义绘图
当创建自定义元素时,可能会选择重写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类的嵌套元素,如果外观变化很大的话,就必须隐藏一个元素并在合适的位置显示另一个元素。
大多数自定义元素不需要自定义渲染。但是当属性发生变化或执行特定操作是,需要渲染复杂的变化很大的可视化外观,此时使用自定义的渲染方法可能更加简单并且更便捷。
三、自定义绘图元素
通过前面对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; } } . . .
四、创建自定义装饰元素
作为一条通用规则,切勿在控件中使用自定义绘图。如果在控件中使用自定义绘图,就违反了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
上一篇: 推荐算法的“前世今生”