模板和样式是相互补充的,允许用户对控件进行重新设计。想要重新设计控件,我们就要先了解控件的逻辑树和可视化树。WPF提供了用于浏览逻辑树和可视化树的两个类:
- System.Windows.LogicalTreeHelper
- System.Windows.Media.VisualTreeHelper
LogicalTreeHelper类提供了几个方法:
方法名 | 说明 |
---|---|
FindLogicalNode() | 根据名称查找特定元素,从指定的元素开始并向下查找逻辑树 |
BringIntoView() | 将元素滚动到视图中 |
GetParent() | 获取指定元素的父元素 |
GetChildren() | 获取指定元素的子元素 |
与此类似,VisualTreeHelper 类还提供了 GetChildrenCount() , GetChild() , GetParent() .可视化树是逻辑树的扩展版本,将元素分成更小的部分,这些部分都是由 FrameworkElement 类的派生类表示的。
每个控件都有一个内置的方法,用于确定如何渲染控件(作为一组更基础的元素)。这个方法称为 控件模板(control template) ,是用XAML标记块定义的。
控件模板定义构成控件的元素。在WPF中有三种类型的模板,都继承自 FrameworkTemplate 基类。控件模板(由ControlTemplate类表示),数据模板(由DataTemplate和HierachicalDataTemplate类表示),面板模板(由ItemsPanelTemplate类表示)。
下面是普通 Button 类的模板的简化版本:
<ControlTemplate>
<!--ButtonChrome元素绘制按钮的标准化可视外观,用于在按钮内容的周围添加图形装饰-->
<mwt:ButtonChrome Name="Chrome" ...>
<!--ContentPresenter包含了按钮的内容-->
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" .../>
</nwt:ButtonChrome>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsKeyboardFocuse">
<!--TargetName属性表示作用于控件模板的特定部分-->
<Setter Property="mwt:ButtonChrome.RenderDefaulted" TargetName="Chrome">
<Setter.Value>
<s:Boolean>True</s:Boolean>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="ToggleButton.IsChecked">
<Setter Property="mwt:ButtonChrome.RenderPressed" TargetName="Chrome">
<Setter.Value>
<s:Boolean>True</s:Boolean>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
可以使用 Snoop 工具深入研究程序的可视化树。或者通过一个简单的工具 WPFControl
创建控件模板
<Window.Resources>
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<!--Rectange和Border用来绘制边框-->
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2">
<!--所有内容控件都需要ContentPresenter元素,表示“在此插入内容”-->
<ContentPresenter RecognizesAccessKey="True"/>
</Border>
<!--add something-->
</ControlTemplate>
</Window.Resources>
<!--为了应用自定义模板,只需要设置控件的 Template属性-->
<Button Template="{StaticResource ButtonTemplate}">Custom Template Button</Button>
模板和样式有类似之处,都能改变元素的外观。然而,样式被限制在一个小得多的范围,可调整控件的属性但不能使用全新的由不同元素组成的可视化树代替控件原来的外观。
控件模板应用于多个窗口,也可以用于整个程序。为避免多次定义,可在 Application 类的 Resources 集合中定义模板资源。为每个控件创建单独的资源字典是推荐的方式,并添加到特定窗口或应用程序(更常见)的Resources 集合中。可使用 MergedDictionaries 集合完成该工作。
<Application >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\Button.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
如果希望重用这些具有不同格式细节(通常是颜色和字体)元素,可从模板中将这些细节提取出来,并将它们放到样式中。
<!--不使用硬编码的颜色,使用模板绑定从控件属性中提取信息-->
<ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type Button}">
<Border Name="Border" BorderThickness="2" CornerRadius="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}">
<Grid>
<Rectangle Name="FocuseCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
</Rectangle>
<ContentPresenter Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility" Value="Visible">
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
关联的样式应用这个控件模板,设置边框和背景色颜色,并添加触发器以便根据按钮的状态改变背景色:
<Style x:Key="CustomButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Control.Template" Value="{StaticResource CustomButtonTemplate}"></Setter>
<Setter Property="BorderBrush" Value="{StaticResource Border}"></Setter>
<Setter Property="Background" Value="{StaticResource DefaultBackground}"></Setter>
<Setter Property="TextBlock.Foreground" Value="White"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource HighlightBackground}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource PressedBackground}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{StaticResource DisabledBackground}"/>
</Trigger>
</Style.Triggers>
</Style>
为使用这个模板,需要设置按钮的 Style 属性而不是 Template 属性:
<Button Margin="10" Style="{StaticResource CustomButtonStyle}">Custom Template</Button>
理想情况,能在控件模板中保留所有触发器,因为它们代表控件的行为,并使用简单设置基本属性。但如果希望样式能够设置颜色方案,是不可能实现的。如果在控件模板和样式中都设置了触发器,那么样式触发器具有优先权。
包含基于类型的样式的组合的资源字典通常被称为 主题(theme) 。通过主题可以为已有程序的所有控件重新应用皮肤,而不需要更改用户界面标记。需要为项目添加资源字典,并将其合并到 App.xaml 文件的 Application.Resources 集合中。
<Application>
<Application.Resources>
<ResourceDictionary Source="ExpressionDark.xaml"/>
</Application.Resources>
</Application>
当允许用户选择皮肤时,在于检索 ResourceDictionary 对象:
ResourceDictionary newDictionary = new ResourceDictionary();
newDictionary.Source = new Uri("Resources/GradientButtonVariant.xaml", UriKind.Relative);
// Window
this.Resources.MergedDictionaries[0] = newDictionary;
// Application
Application.Current.Resources.MergedDictionaries[0] = newDictionary;