在控件模板和为其提供支持的代码之间又一个隐含约定。如果使用自定义控件模板替代控件的标准模板,就需要确保新模板能够满足控件的实现代码的所有需要。
在简单控件中,这个过程比较容易,因为对模板几乎没有(或完全没有)什么真正的需求。对于复杂控件,问题就显得有些微妙了,因为控件的外观和实现不可能完全相互独立的。对于这种情况,控件需要对其可视化显示做出一些假设,而不管曾经被设计的多好。
在前面已经看到了控件模板的这种需求的两个例子,占位元素(如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元素指定背景,所以保持原有的透明背景。这样,列表框的轻微阴影渐变可透过滚动条显示。下图是最终的列表框: