【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(, , 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
【WPF学习】第六十八章 自定义绘图元素的更多相关文章
- 【WPF学习】第十八章 多点触控输入
多点触控(multi-touch)是通过触摸屏幕与应用程序进行交互的一种方式.多点触控输入和更传统的基于笔(pen-based)的输入的区别是多点触控识别手势(gesture)——用户可移动多根手指以 ...
- 【WPF学习】第二十八章 程序集资源
WPF应用程序中的程序集资源与其他.NET应用程序中的程序集资源在本质上是相同的.基本概念是为项目添加文件,从而Visual studio可将其嵌入到编译过的应用程序的EXE或DLL文件中.WPF程序 ...
- 我的MYSQL学习心得(十) 自定义存储过程和函数
我的MYSQL学习心得(十) 自定义存储过程和函数 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心 ...
- “全栈2019”Java第六十八章:外部类访问内部类成员详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 【WPF学习】第二十六章 Application类——应用程序的生命周期
在WPF中,应用程序会经历简单的生命周期.在应用程序启动后,将立即创建应用程序对象,在应用程序运行时触发各种应用程序事件,你可以选择监视其中的某些事件.最后,当释放应用程序对象时,应用程序将结束. 一 ...
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- SpringBoot进阶教程(六十五)自定义注解
在上一篇文章<SpringBoot进阶教程(六十四)注解大全>中介绍了springboot的常用注解,springboot提供的注解非常的多,这些注解简化了我们的很多操作.今天主要介绍介绍 ...
- 【WPF学习】第十五章 WPF事件
前两章学习了WPF事件的工作原理,现在分析一下在代码中可以处理的各类事件.尽管每个元素都提供了许多事件,但最重要的事件通常包括以下5类: 生命周期事件:在元素被初始化.加载或卸载时发生这些事件. 鼠标 ...
- 【WPF学习】第二十九章 元素绑定——将元素绑定到一起
数据banding的最简单情形是,源对象时WPF元素而且源属性是依赖性属性.前面章节解释过,依赖项属性具有内置的更改通知支持.因此,当在源对象中改变依赖项属性的值时,会立即更新目标对象中的绑定属性.这 ...
随机推荐
- python的int float if...else
# 字符串 string 单引号 ‘ ’ 双引号 “ ”-包含的 app = 'dongt woory' 外面单引号里面可以双引号,外面双引号,里面也可以单引号 app ='你是真的“好看”吗' ...
- word多级列表应用
- 并查集---体会以及模板&&How Many Tables - HDU 1213
定义&&概念: 啥是并查集,就是将所有有相关性的元素放在一个集合里面,整体形成一个树型结构,它支持合并操作,但却不支持删除操作 实现步骤:(1)初始化,将所有节点的父亲节点都设置为自己 ...
- 漏洞复现环境集锦-Vulhub
0x01 Vulhub简介 Vulhub是一个面向大众的开源漏洞靶场,无需docker知识,简单执行两条命令即可编译.运行一个完整的漏洞靶场镜像. 0x02 安装 # 安装pip curl -s ht ...
- 1年左右的Java开发经验面试者的心得
面试,相信只要踏入这行业的人都会经历,不同的公司有不同的面试流程,但是综合起来,其实还是大体一致的!只有不断的总结自己的面试经历,得出自己的技术不足点,才能更好的去查缺补漏,从而更加自信的进行面试找到 ...
- AJ学IOS 之微博项目实战(1)微博主框架-子控制器的添加
AJ分享,必须精品 一:简单介绍 这是新浪微博的iOS端项目,来自于黑马的一个实战项目. 主要分成五大模块,本次全部运用纯代码实现,其中会用到很多前面学过得内容,如果有的地方有重复的知识点,说明这个知 ...
- 告诉你那里最受欢迎,python爬取全国13个城市旅游数据
前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取http ...
- 国产操作系统深度deepin V20体验
1. 安装系统 国产操作系统deepin V20 bata版本已经发布.本人第一时间安装和体验.在犹豫很久之后,因为受到最新内核,高版本的bash和Python的诱惑,字体更加和谐等因素,选择升级系统 ...
- linux CVE-2019-14287 Sudo提权漏洞
CVE-2019-14287 sudo介绍 sudo,也就是以超级管理员身份运行(superuser do)的意思.sudo 是 Linux 中最常使用的重要实用程序之一,它功能十分强大,几乎安装在每 ...
- 最全的 API 接口集合
对于程序员来说,为自己的程序选择一些合适的API并不是那么简单,有时候还会把你搞得够呛,今天猿妹要和大家分享一个开源项目,这个项目汇集了各种开发的api,涵盖了音乐.新闻.书籍.日历等,无论你是从事W ...