在控件模板和为其提供支持的代码之间又一个隐含约定。如果使用自定义控件模板替代控件的标准模板,就需要确保新模板能够满足控件的实现代码的所有需要。

  在简单控件中,这个过程比较容易,因为对模板几乎没有(或完全没有)什么真正的需求。对于复杂控件,问题就显得有些微妙了,因为控件的外观和实现不可能完全相互独立的。对于这种情况,控件需要对其可视化显示做出一些假设,而不管曾经被设计的多好。

  在前面已经看到了控件模板的这种需求的两个例子,占位元素(如ContentPresenter和ItemsPresenter)和模板绑定。接下来的将例举另外两个例子:具有特定名称(以PART_开头)的元素和专门设计的用于控件模板的元素(如ScrollBar控件中的Track元素)。为成功地创建控件模板,需要仔细查看相关控件的标准模板,并注意分析这4种技术的用法,然后将他们复制到自己的模板中。

一、嵌套的模板

  按钮控件的模板可分解成几个较简单的部分。然而,许多模板并非如此简单。在某些情况下,控件模板将包含每个自定义模板也需要的大量元素。而在有些情况下,改变控件的外观涉及创建多个模板。

  例如,假设计划修改熟悉的ListBox控件。创建这个示例的第一步是为ListBox控件设计模板,并酌情添加自动应用模板的样式。下面的标记将这两个要素合并到一起:

<Style TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border Name="Border"
Background="{StaticResource ListBoxBackgroundBrush}"
BorderBrush="{StaticResource StandardBorderBrush}"
BorderThickness="1"
CornerRadius="3"
>
<ScrollViewer Focusable="False">
<ItemsPresenter Margin="2"></ItemsPresenter>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  该样式使用两个画刷绘制边框和背景。实际模板是标准模板ListBox的简化版本,但没有使用ListBoxChrome类,而使用了较简单的Border元素。在Border元素内部是为列表提供滚动功能的ScrollViewer元素以及容纳所有列表项的ItemsPresenter元素。

  对于该模板,最值的注意之处是它未提供的功能——配置列表中各项的外观。没有该功能,呗选择的元素总是使用熟悉的蓝色背景突出显示。为改变这种行为,需要为ListBoxItem控件添加控件模板,ListBoxItem控件是封装列表中每个单独元素内容的内容控件。

  与ListBox模板一样,可使用元素类型样式应用ListBoxItem模板。下面的基本模板在一个不可见的边框中封装了每个项。因此ListBoxItem是内容控件,所以需要使用ContentPresenter的触发器:

<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border Name="Border"
BorderThickness="2" CornerRadius="3" Padding="1">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="ListBoxItem.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="FontSize" To="20" Duration="0:0:1"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="ListBoxItem.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="FontSize" BeginTime="0:0:0.5" Duration="0:0:0.2"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource HoverBorderBrush}"></Setter>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource SelectedBackgroundBrush}"/>
<Setter TargetName="Border" Property="TextBlock.Foreground" Value="{StaticResource SelectedForegroundBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  总之,可以使用这两个模板创建当将鼠标移动到当前定位的项上时使用动画放大项的列表框。因为每个ListBoxItem可具有自己的动画,所以当用户在列表框中上下移动鼠标时,将看到几个项开始增大,然后再次收缩,创建了动人的“鱼眼”效果(当将鼠标悬停在项上时,使用具有动画的变换,可实现更夸张的鱼眼效果,放大项并使项变形)。

  尽管不可能在一幅图像中捕获这种效果,下图显示了将鼠标快速移过几个项之后该列表的快照。

  在此不会重新分析整个ListBoxItem模板示例,因为它由许多不同部分构建,包括用于设置ListBox控件、ListBoxItem控件以及ListBox控件的各种组成元素(如滚动条)样式的部分,其中重要的部分是改变ListBoxItem模板的样式。

  在这个示例中,ListBoxItem对象较缓慢地扩大(经过1秒),然后更快地进行缩小(经过0.2s)。然而,在开始缩小动画之前有0.5秒得延迟。

  需要注意,缩小动画省略了From和To属性。通过这种方式,缩小动画总将文本从当前尺寸缩小到它原来的尺寸。如果将鼠标移到ListBoxItem上然后移开,就会得到所期望的效果——当鼠标停留在项上时,项会不断地扩张,当移走鼠标时项会不断地缩小。

二、修改滚动条

  列表框还有一个方向没有改变:右边的滚动条。它是ScrollViewer元素的一部分,ScrollViewer元素是ListBox模板的一部分。尽管该例重新定义了ListBox模板,但没有替换ScrollBar的ScrollViewer。

  为自定义该细节,可为ListBox控件创建一个新的ScrollViewer模板。然后可将ScrollViewer模板指向自定义的ScrollBar模板。然而,还有更简单的选择。可创建一个改变所有ScrollBar控件模板的特定于元素类型的样式。这样就避免了创建ScrollViewer模板所需的额外的工作。

  当然,还需要考虑这种设计会对应用程序的其他部分造成什么影响。如果创建元素类型样式ScrollBar,并将其添加到窗口的Resources集合中,对于窗口中的所有控件,无论何时使用ScrollBar控件,都会有新样式的滚动条,这可能正是你所希望的效果。另一方面,如果希望只改变ListBox控件中的滚动条,就必须为ListBox控件本身的资源集合添加元素类型样式ScrollBar。

  滚动条的背景由Track类表示——实际上时一个具有阴影并且被拉伸占满整个滚动条长度的矩形。滚动条的末尾处是按钮,通过这些按钮可以向上或向下(或向左或向右)滚动一个步长。这些按钮是RepeatButton类的实例,该类继承自ButtonBase类。RepeatButton类和普遍Button类之间的重要区别在于,如果在RepeatButton按钮上保持鼠标为按下状态,就会反复触发Click事件(对于滚动条这是非常方便的)。

  在滚动条的中间是代表滚动内容中当前位置的Thumb元素。并且最有趣的是,滑块两侧的空白实际上由另外两个RepeatButton对象构成,它们都是透明的。当单击这两个按钮中的一个时,滚动条会滚动一整页(一页是滚动内容所在的可见窗口中的内部容量)。通过单击滑块两侧的条形区域,可快速浏览滚动内容,这一功能是大家所熟悉的。

  下面是用于垂直滚动条的模板:

<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18"/>
<RowDefinition Height="*"/>
<RowDefinition MaxHeight="18"/>
</Grid.RowDefinitions> <RepeatButton Grid.Row="0" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineUpCommand" >
<Path
Fill="{StaticResource GlyphBrush}"
Data="M 0 4 L 8 4 L 4 0 Z"></Path>
</RepeatButton>
<Track Name="PART_Track" Grid.Row="1"
IsDirectionReversed="True" ViewportSize="0">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumbStyle}">
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton
Grid.Row="3" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineDownCommand">
<Path
Fill="{StaticResource GlyphBrush}"
Data="M 0 0 L 4 4 L 8 0 Z"></Path>
</RepeatButton>
</Grid>
</ControlTemplate>

  一旦理解滚动条的多部分结构,上面的模板就非常直观了。下面列出需要注意的几个要点:

  垂直滚动条由一个包含三行的网格构成。顶行和底行容纳两端的按钮(并显示为箭头),它们固定占用18个单位。中间部分容纳Track元素,占用剩余空间。

  两端的RepeatButton元素使用相同的样式。唯一的区别是Content属性,该属性包含了一个用于绘制箭头的Path对象,因为顶部的按钮具有上箭头而底部的按钮具有下箭头。

  两个按钮都连接到ScrollBar类中的命令(LineUpCommand和LineDownCommand)。这正是其工作原理。只要提供链接到这个命令的按钮即可,不必考虑按钮的名称是什么,也不必考虑其他外观像什么或使用哪个特定的类。

  Track元素名为PART_Track。为使ScrollBar类能够成功地关联到它的代码,必须使用这个名称。如果查看ScrollBar类的默认模板(类似与上面的模板,但更长一些),也会看到该元素。

  Track.ViewportSize属性被设置为0,。这是该模板特有的实现细节,可确保Thumb元素总有相同的尺寸(通常,滑块根据内容按比例地改变尺寸,因此如果滚动的内容在窗口中基本上能够显示,这是滑块会变得较长)。

  Track元素封装了两个RepeatButton对象(它们的样式单独定义)和Thumb元素。同样,这些按钮通过命令连接到适当的功能。

  通过上面代码,发现模板使用了键名,明确指定它作为垂直滚动条。当为样式设置键名时,可确保它不能被自动应用,即使同时设置了TargetType属性也是如此。该例使用这种方法的原因是,该模板只适用于垂直方向的滚动条,而且如果ScrollBar.Orientation属性被设置为Vertical,元素类型样式会使用触发器自动应用控件模板:

<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="18"/>
<Setter Property="Height" Value="Auto" />
<Setter Property="Template" Value="{StaticResource VerticalScrollBar}" />
</Trigger>
</Style.Triggers>
</Style>

  尽管可以使用相同的基本部分很容易地创建水平滚动条,但该例没有采用该步骤(从而保留了正常样式的水平滚动条)。

  最后一项任务是填充格式化各个RepeatButton对象和Thumb元素的样式。这些样式比较简单,但它们确实改变了滚动条的标准外观。首先,Thumb元素的形状被设置成类似椭圆的形状:

<Style x:Key="ScrollBarThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Margin" Value="1,0,1,0" />
<Setter Property="Background" Value="{StaticResource StandardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource StandardBorderBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Ellipse Stroke="{StaticResource StandardBorderBrush}"
Fill="{StaticResource StandardBrush}"></Ellipse>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  接下来,在美观的圆圈中绘制两端的箭头。这些圆圈是在控件模板中定义的,而箭头由RepeatButton对象的内容提供,并使用ContentPresenter元素插入到控件模板中:

 <Style x:Key="ScrollBarLineButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Grid Margin="1">
<Ellipse Name="Border" StrokeThickness="1" Stroke="{StaticResource StandardBorderBrush}"
Fill="{StaticResource StandardBrush}"></Ellipse>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="Border" Property="Fill" Value="{StaticResource PressedBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  显示在Track元素上面的RepeatButton对象没有发生改变。它们只使用透明背景,使Track元素可透过它们显示:

<Style x:Key="ScrollBarPageButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  与正常的滚动不同,在该模板中没有为Track元素指定背景,所以保持原有的透明背景。这样,列表框的轻微阴影渐变可透过滚动条显示。下图是最终的列表框:

【WPF学习】第六十二章 构建更复杂的模板的更多相关文章

  1. 【WPF学习】第二十二章 文本控件

    WPF提供了三个用于输入文本的控件:TextBox.RichTextBox和PasswordBox.PasswordBox控件直接继承自Control类.TextBox和RichTextBox控件间接 ...

  2. “全栈2019”Java第六十二章:接口与常量详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  3. o'Reill的SVG精髓(第二版)学习笔记——第十二章

    第十二章 SVG动画 12.1动画基础 SVG的动画特性基于万维网联盟的“同步多媒体集成语言”(SMIL)规范(http://www.w3.org/TR/SMIL3). 在这个动画系统中,我们可以指定 ...

  4. java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

    java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...

  5. 【WPF学习】第十四章 事件路由

    由上一章可知,WPF中的许多控件都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容.例如,可构建包含图形的按钮,创建混合了文本和图片内容的标签,或者为了实现滚动或折叠的显示效果而在特定容器中放置 ...

  6. 【WPF学习】第六十四章 构建基本的用户控件

    创建一个简单用户控件是开始自定义控件的好方法.本章主要介绍创建一个基本的颜色拾取器.接下来分析如何将这个控件分解成功能更强大的基于模板的控件. 创建基本的颜色拾取器很容易.然而,创建自定义颜色拾取器仍 ...

  7. 【WPF学习】第二十九章 元素绑定——将元素绑定到一起

    数据banding的最简单情形是,源对象时WPF元素而且源属性是依赖性属性.前面章节解释过,依赖项属性具有内置的更改通知支持.因此,当在源对象中改变依赖项属性的值时,会立即更新目标对象中的绑定属性.这 ...

  8. 【WPF学习】第十五章 WPF事件

    前两章学习了WPF事件的工作原理,现在分析一下在代码中可以处理的各类事件.尽管每个元素都提供了许多事件,但最重要的事件通常包括以下5类: 生命周期事件:在元素被初始化.加载或卸载时发生这些事件. 鼠标 ...

  9. 《机器学习实战》学习笔记第十二章 —— FP-growth算法

    主要内容: 一.  FP-growth算法简介 二.构建FP树 三.从一颗FP树中挖掘频繁项集 一.  FP-growth算法简介 1.上次提到可以用Apriori算法来提取频繁项集,但是Aprior ...

随机推荐

  1. js笔记系列之--时间及时间戳

    js入门系列之 时间及时间戳 时间及时间戳 时间及时间戳是js里面很常见的一个概念,在我们写前端页面的时候,经常会遇到需要获取当前时间的情况,所以,了解js中的时间概念非常重要.而时间戳是指格林威治时 ...

  2. 初窥构建之法——记2020BUAA软工个人博客作业

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标是 完成一次完整的软件开发经历并以博客的方式记录开发过程的心得掌握 ...

  3. 一份简明的 Base64 原理解析

    书接上回,在 记一个 Base64 有关的 Bug 一文里,我们说到了 Base64 的编解码器有不同实现,交叉使用它们可能引发的问题等等. 这一回,我们来对 Base64 这一常用编解码技术的原理一 ...

  4. 第六章、Vue项目预热

    6-5. (1). (2).引入reset.css及border.css (3).解决手机点击延迟300ms的问题 a.安装 b.引入fastclick 6-2.项目的整体架构 src--->整 ...

  5. Spark RDD Tutorial

    Spark RDD教程 这个教程将会帮助你理解和使用Apache Spark RDD.所有的在这个教程中使用的RDD例子将会提供在github上,供大家快速的浏览. 什么是RDD(Rssilient ...

  6. win10安装docker 和 splash

    参考链接1:https://www.cnblogs.com/321lxl/p/9536616.html 参考链接2:https://blog.csdn.net/qq_18831501/article/ ...

  7. go 广度搜索案例(迷宫)

    package main import ( "fmt" "os" ) /* *将文档结构读入到切片中(二维数组) *row, col 行数 列数 (文档第一行数 ...

  8. 【简说Python WEB】视图函数操作数据库

    目录 [简说Python WEB]视图函数操作数据库 系统环境:Ubuntu 18.04.1 LTS Python使用的是虚拟环境:virutalenv Python的版本:Python 3.6.9 ...

  9. 配置GitLab或Git环境之教程

    配置GitLab或Git环境之教程 1.安装好Git后,首先打开开始菜单的所有程序里面的git文件夹,打开Git Bash/ ​ 2.弹出的命令行里面输入ssh-keygen 输入y,一直Enter ...

  10. Asp.Net Core AuthorizeAttribute 和AuthorizeFilter 跟进及源码解读

    一.前言 IdentityServer4已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对IdentityServer4掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持 ...