WPF中的ControlTemplate(控件模板)
周银辉
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
>
我们为模板定义了一个动画资源,此后在模板的触发器中我们就可以调用该资源来实现一个动画效果了:
<
EventTrigger
RoutedEvent
="Mouse.MouseDown"
SourceName
="faceEllipse"
>
<
EventTrigger
.Actions
>
<
BeginStoryboard
Storyboard
="{StaticResource MouseClickButtonStoryboard}"
/>
</
EventTrigger.Actions
>
</
EventTrigger
>
你可以粘贴以下代码到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
.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的默认模板,请调用以下方法:
周银辉
WPF包含数据模板和控件模板,其中控件模板又包括ControlTemplate和ItemsPanelTemplate,这里讨论一下ControlTemplate。
其实WPF的每一个控件都有一个默认的模板,该模板描述了控件的外观以及外观对外界刺激所做出的反应。我们可以自定义一个模板来替换掉控件的默认模板以便打造个性化的控件。
与Style不同,Style只能改变控件的已有属性值(比如颜色字体)来定制控件,但控件模板可以改变控件的内部结构( VisualTree ,视觉树)来完成更为复杂的定制,比如我们可以定制这样的按钮:在它的左办部分显示一个小图标而它的右半部分显示文本。
要替换控件的模板,我们只需要声明一个ControlTemplate对象,并对该ControlTemplate对象做相应的配置,然后将该ControlTemplate对象赋值给控件的Template属性就可以了。
ControlTemplate包含两个重要的属性:
1,VisualTree,该模板的视觉树,其实我们就是使用这个属性来描述控件的外观的
2,Triggers,触发器列表,里面包含一些触发器Trigger,我们可以定制这个触发器列表来使控件对外界的刺激发生反应,比如鼠标经过时文本变成粗体等。
参考以下代码












很容易联想到一个问题:控件(Button)的一些属性,比如高度、宽度、文本等如何在新定义的外观中表现出来呢?
我们使用TemplateBinding 将控件的属性与新外观中的元素的属性关联起来Width="{TemplateBinding Button.Width}" ,这样我们就使得椭圆的宽度与按钮的宽度绑定在一起而保持一致,同理我们使用Text="{TemplateBinding Button.Content}"将TextBlock的文本与按钮的Content属性绑定在一起。
除了定义控件的默认外观外,也许我们想还定义当外界刺激我们的控件时,控件外观做出相应的变化,这是我们需要触发器。参考以下代码:



















你可以粘贴以下代码到XamlPad查看效果:









































接下来的一个问题是:如果我要重用我的模板,应该怎么办呢?
你需要将模板定义为资源,其实大多数情况下,我们也是这样做的
参考以下代码:



















你可以粘贴以下代码到XamlPad查看效果:














































额外提一下的是,我们也可以在触发器中,调用一个故事板来达到对事件响应时的动画效果
参考以下代码






















































































最好的模板示例:我们知道每个控件都有自己默认的模板,这是MS编写的,如果我们能够得到这些模板的XAML代码,那么它将是学习模板的最好的示例,
要想获得某个控件ctrl的默认模板,请调用以下方法: