WPF 包含数据模板和控件模板,其中控件模板又包括 ControlTemplate 和 ItemsPanelTemplate ,这里讨论一下 ControlTemplate 。
其实 WPF 的每一个控件都有一个默认的模板,该模板描述了控件的外观以及外观对外界刺激所做出的反应。我们可以自定义一个模板来替换掉控件的默认模板以便打造个性化的控件。
与 Style 不同, Style 只能改变控件的已有属性值(比如颜色字体)来定制控件,但控件模板可以改变控件的内部结构( VisualTree ,视觉树) 来完成更为复杂的定制,比如我们可以定制这样的按钮:在它的左办部分显示一个小图标而它的右半部分显示文本。
要替换控件的模板,我们只需要声明一个 ControlTemplate 对象,并对该 ControlTemplate 对象做相应的配置,然后将该 ControlTemplate 对象赋值给控件的 Template 属性就可以了。
ControlTemplate 包含两个重要的属性:
1 , VisualTree ,该模板的视觉树,其实我们就是使用这个属性来描述控件的外观的
2 , Triggers ,触发器列表,里面包含一些触发器 Trigger ,我们可以定制这个触发器列表来使控件对外界的刺激发生反应,比如鼠标经过时文本变成粗体等。
参考以下代码
<Button> <Button.Template> <ControlTemplate> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> </ControlTemplate> </Button.Template>
</Button> 在上面的代码中,我们修改了 Button 的 Template 属性,我们定义了一个 ControlTemplate ,在 <ControlTemplate> ... </ControlTemplate> 之间包含的是模板的视觉树,也就是如何显示控件的外观,我们这里使用了一个 Ellipse (椭圆)和一 个 TextBlock (文本块)来定义控件的外观。
很容易联想到一个问题:控件( Button )的一些属性,比如高度、宽度、文本等如何在新定义的外观中表现出来呢?
我 们使用 TemplateBinding 将控件的属性与新外观中的元素的属性关联起来 Width="{TemplateBinding Button.Width}" ,这样我们就使得椭圆的宽度与按钮的宽度绑定在一起而保持一致,同理我们使用 Text="{TemplateBinding Button.Content}" 将 TextBlock 的文本与按钮的 Content 属性绑定在一起。
除了定义控件的默认外观外,也许我们想还定义当外界刺激我们的控件时,控件外观做出相应的变化,这是我们需要触发器。参考以下代码:
<Button Content="test btn" Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" Grid.RowSpan="1" > <Button.Template> <ControlTemplate> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> <!--定义触发器--> <ControlTemplate.Triggers> <Trigger Property="Button.IsMouseOver" Value="True"> <Setter Property="Button.Foreground" Value="Red" /> </Trigger> </ControlTemplate.Triggers> <!--定义触发器_End--> </ControlTemplate> </Button.Template>
</Button> 在上面的代码中注意到 <ControlTemplate.Triggers>... </ControlTemplate.Triggers> 之间的部分,我们定义了触发器 <Trigger Property="Button.IsMouseOver" Value="True"> ,其表示当我们 Button 的 IsMouseIOver 属性变成 True 时,将使用设置器 <Setter Property="Button.Foreground" Value="Red" /> 来将 Button 的 Foreground 属性设置为 Red 。这里有一个隐含的意思是:当 Button 的 IsMouseIOver 属性变成 False 时,设 置器中设置的属性将回复原值。
你可以粘贴以下代码到 XamlPad 查看效果:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ControlTemplateTest" Height="300" Width="300" > <Grid ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.2*"/> <ColumnDefinition Width="0.6*"/> <ColumnDefinition Width="0.2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.4*"/> </Grid.RowDefinitions> <Button Content="test btn" Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" Grid.RowSpan="1" > <Button.Template> <ControlTemplate> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> <!--定义触发器--> <ControlTemplate.Triggers> <Trigger Property="Button.IsMouseOver" Value="True"> <Setter Property="Button.Foreground" Value="Red" /> </Trigger> </ControlTemplate.Triggers> <!--定义触发器_End--> </ControlTemplate> </Button.Template> </Button> </Grid> </Window>
接下来的一个问题是:如果我要重用我的模板,应该怎么办呢?
你需要将模板定义为资源,其实大多数情况下,我们也是这样做的
参考以下代码:
<Window.Resources> <ControlTemplate TargetType="Button" x:Key="ButtonTemplate"> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> <!--定义触发器--> <ControlTemplate.Triggers> <Trigger Property="Button.IsMouseOver" Value="True"> <Setter Property="Button.Foreground" Value="Red" /> </Trigger> </ControlTemplate.Triggers> <!--定义触发器_End--> </ControlTemplate> </Window.Resources>
上面的代码将我们原来的模板定义为窗体范围内的资源,其中 TargetType="Button" 指示我们的模板作用对象为 Button ,这样在整个窗体范围内的按钮都可以使用这个模板了,模板的使用方法也很简单:
<Button Content="test btn" Template="{StaticResource ButtonTemplate}" /> 其中的 ButtonTemplate 是我们定义的模板的 x:Key
你可以粘贴以下代码到 XamlPad 查看效果:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ControlTemplateTest" Height="300" Width="300" > <Window.Resources> <ControlTemplate TargetType="Button" x:Key="ButtonTemplate"> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> <!--定义触发器--> <ControlTemplate.Triggers> <Trigger Property="Button.IsMouseOver" Value="True"> <Setter Property="Button.Foreground" Value="Red" /> </Trigger> </ControlTemplate.Triggers> <!--定义触发器_End--> </ControlTemplate> </Window.Resources> <Grid ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.2*"/> <ColumnDefinition Width="0.6*"/> <ColumnDefinition Width="0.2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.4*"/> </Grid.RowDefinitions> <Button Content="test btn1" Grid.Column="0" Grid.ColumnSpan="1" Grid.Row="0" Grid.RowSpan="1" /> <Button Content="test btn2" Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" Grid.RowSpan="1" Template="{StaticResource ButtonTemplate}" /> <Button Content="test btn2" Grid.Column="2" Grid.ColumnSpan="1" Grid.Row="2" Grid.RowSpan="1" Template="{StaticResource ButtonTemplate}" /> </Grid> </Window>
额外提一下的是,我们也可以在触发器中,调用一个故事板来达到对事件响应时的动画效果
参考以下代码 <!-- 定义动画资源 -->
<ControlTemplate.Resources> <Storyboard x:Key="MouseClickButtonStoryboard"> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="faceEllipse" Storyboard.TargetProperty="Width" BeginTime="00:00:00"> <SplineDoubleKeyFrame KeyTime="00:00:00" Value="50"/> <SplineDoubleKeyFrame KeyTime="00:00:00.3" Value="100"/> </DoubleAnimationUsingKeyFrames> </Storyboard>
</ControlTemplate.Resources> 我们为模板定义了一个动画资源,此后在模板的触发器中我们就可以调用该资源来实现一个动画效果了:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ControlTemplateTest" Height="300" Width="300" > <Window.Resources> <ControlTemplate TargetType="Button" x:Key="ButtonTemplate"> <!--定义视觉树--> <Grid> <Ellipse Name="faceEllipse" Width="{TemplateBinding Button.Width}" Height="{TemplateBinding Control.Height}" Fill="{TemplateBinding Button.Background}"/> <TextBlock Name="txtBlock" Margin="{TemplateBinding Button.Padding}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{TemplateBinding Button.Content}" /> </Grid> <!--定义视觉树_end--> <!--定义动画资源--> <ControlTemplate.Resources> <Storyboard x:Key="MouseClickButtonStoryboard"> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="faceEllipse" Storyboard.TargetProperty="Width" BeginTime="00:00:00"> <SplineDoubleKeyFrame KeyTime="00:00:00" Value="50"/> <SplineDoubleKeyFrame KeyTime="00:00:00.3" Value="100"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </ControlTemplate.Resources> <!--定义动画资源_end--> <!--定义触发器--> <ControlTemplate.Triggers> <Trigger Property="Button.IsMouseOver" Value="True"> <Setter Property="Button.Foreground" Value="Red" /> </Trigger> <EventTrigger RoutedEvent="Mouse.MouseDown" SourceName="faceEllipse"> <EventTrigger.Actions> <BeginStoryboard Storyboard="{StaticResource MouseClickButtonStoryboard}"/> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseDown" SourceName="txtBlock"> <EventTrigger.Actions> <BeginStoryboard Storyboard="{StaticResource MouseClickButtonStoryboard}"/> </EventTrigger.Actions> </EventTrigger> </ControlTemplate.Triggers> <!--定义触发器_End--> </ControlTemplate> </Window.Resources> <Grid ShowGridLines="True"> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.2*"/> <ColumnDefinition Width="0.6*"/> <ColumnDefinition Width="0.2*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.3*"/> <RowDefinition Height="0.4*"/> </Grid.RowDefinitions> <Button Content="test btn1" Grid.Column="0" Grid.ColumnSpan="1" Grid.Row="0" Grid.RowSpan="1" /> <Button Content="test btn2" Grid.Column="1" Grid.ColumnSpan="1" Grid.Row="1" Grid.RowSpan="1" Template="{StaticResource ButtonTemplate}" /> <Button Content="test btn2" Grid.Column="2" Grid.ColumnSpan="1" Grid.Row="2" Grid.RowSpan="1" Template="{StaticResource ButtonTemplate}" /> </Grid> </Window>
最好的模板示例:我们知道每个控件都有自己默认的模板,这是 MS 编写的,如果我们能够得到这些模板的 XAML 代码,那么它将是学习模板的最好的示例,
要想获得某个控件 ctrl 的默认模板,请调用以下方法:
string GetTemplateXamlCode(Control ctrl) { FrameworkTemplate template = ctrl.Template; string xaml = ""; if (template != null) { XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = new string(' ', 4); settings.NewLineOnAttributes = true; StringBuilder strbuild = new StringBuilder(); XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings); try { XamlWriter.Save(template, xmlwrite); xaml = strbuild.ToString(); } catch (Exception exc) { xaml = exc.Message; } } else { xaml = "no template"; } return xaml; }