通过创建 ControlTemplate 自定义现有控件的外观

ControlTemplate 指定控件的可视结构和可视行为。 可以通过为控件指定新 ControlTemplate 自定义该控件的外观。 创建 ControlTemplate 后,可以在不更改现有控件的功能的情况下更改其外观。 例如,您可以将应用程序中的按钮设置为圆形,而不是默认的方形,但该按钮仍将引发 Click 事件。

本主题介绍 ControlTemplate 的各个部分,演示如何为 Button 创建简单的 ControlTemplate,并说明如何了解控件的控件协定以便可以自定义其外观。 由于您创建 ControlTemplate 时使用的是 XAML,因此无需编写任何代码即可更改控件的外观。 您还可以使用设计器(如 Microsoft Expression Blend)来创建自定义控件模板。 本主题演示 XAML 中自定义 Button 外观的示例,并在本主题的末尾列出了完整示例。 有关使用 Expression Blend 的更多信息,请参见设置支持模板的控件的样式

下图演示使用本主题中创建的 ControlTemplateButton

一个使用自定义控件模板的按钮

一个具有自定义控件模板的按钮。

一个使用自定义控件模板并将鼠标指针置于上的按钮

一个具有红色边框的按钮。

本主题包括下列各节。

  • 系统必备组件
  • 何时应创建 ControlTemplate
  • 更改控件的可视结构
  • 根据控件状态更改控件外观
  • 指定控件在状态间转换时的行为
  • 通过了解控件协定自定义其他控件
  • 完整的示例
  • 相关主题

系统必备组件

本主题假定您理解如何创建和使用控件和样式,如控件中所述。 本主题中讨论的概念适用于从 Control 类(UserControl 除外)继承的元素。 您不能将 ControlTemplate 应用于 UserControl

何时应创建 ControlTemplate

控件有很多属性(例如 BackgroundForegroundFontFamily),您可以设置这些属性以指定控件外观的不同方面,但通过设置这些属性可进行的更改是有限的。 例如,可以将 CheckBoxForeground 属性设置为蓝色,将 FontStyle 设置为倾斜。

如果不能为控件创建新的 ControlTemplate,则每个基于 WPF 的应用程序中的所有控件都有相同的一般外观,这限制了创建具有自定义外观的应用程序的能力。 默认情况下,每个 CheckBox 具有相似的特性。 例如,CheckBox 的内容始终位于选择指示符的右侧,复选标记总是用于表示选中了 CheckBox

如果您要自定义通过设置控件的其他属性无法生成的外观,可以创建 ControlTemplate。 在 CheckBox 的示例中,假设您希望复选框的内容位于选择指示符的上方,并希望用 X 表示选中了 CheckBox。 可以在 CheckBoxControlTemplate 中指定这些更改。

下图演示使用默认 ControlTemplateCheckBox

一个使用默认控件模板的 CheckBox

一个具有默认控件模板的复选框。

下图演示使用自定义 ControlTemplateCheckBoxCheckBox 的内容置于选择指示符的上方并在选中 CheckBox 时显示 X。

一个使用自定义控件模板的 CheckBox

一个具有自定义控件模板的复选框。

此示例中 CheckBoxControlTemplate 相对复杂,因此本主题使用较简单的示例为 Button 创建 ControlTemplate

更改控件的可视结构

在 WPF 中,控件通常是复合 FrameworkElement 对象。 当您创建 ControlTemplate 时,组合 FrameworkElement 对象以生成单一控件。 ControlTemplate 必须仅将一个 FrameworkElement 作为其根元素。 根元素通常包含其他 FrameworkElement 对象。 对象组合构成了控件的可视结构。

下面的示例为 Button 创建自定义 ControlTemplateControlTemplate 创建 Button 的可视结构。 当您将鼠标指针置于其上或单击它时,该示例不更改按钮的外观。 本主题后面将讨论按钮处于另一种状态时如何更改按钮的外观。

在该示例中,可视结构由下列部分组成:

<ControlTemplate TargetType="Button">
  <Border Name="RootElement">

    <!--Create the SolidColorBrush for the Background 
        as an object elemment and give it a name so 
        it can be referred to elsewhere in the
        control template.-->
    <Border.Background>
      <SolidColorBrush x:Name="BorderBrush" Color="Black"/>
    </Border.Background>

    <!--Create a border that has a different color
        by adding smaller grid. The background of 
        this grid is specificied by the button's 
        Background property.-->
    <Grid Margin="4" Background="{TemplateBinding Background}">

      <!--Use a ContentPresenter to display the Content of
          the Button.-->
      <ContentPresenter
        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
        Margin="4,5,4,4" />
    </Grid>

  </Border>
</ControlTemplate>

通过使用 TemplateBinding 保留控件属性的功能

创建新的 ControlTemplate 后,您仍可能想要使用公共属性更改控件的外观。 TemplateBinding 标记扩展将 ControlTemplate 中元素的属性绑定到由控件定义的公共属性。 使用 TemplateBinding 时,可让控件上的属性充当模板的参数。 也就是说,在设置控件上的属性时,该值将传递给具有 TemplateBinding 的元素。

下面的示例重复上面的示例中使用 TemplateBinding 标记扩展将 ControlTemplate 中元素的属性绑定到由按钮定义的公共属性部分。

<Grid Margin="4" Background="{TemplateBinding Background}">

  <!--Use a ContentPresenter to display the Content of
      the Button.-->
  <ContentPresenter
    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
    Margin="4,5,4,4" />
</Grid>

在此示例中,Grid 将其 Panel.Background 属性模板绑定到 Control.Background。 由于 Panel.Background 是通过模板绑定的,您可以创建使用同一 ControlTemplate 的多个按钮,并将每个按钮的 Control.Background 设置为不同的值。 如果在 ControlTemplate 中未通过模板绑定将 Control.Background 绑定到元素的属性,则设置按钮的 Control.Background 对按钮的外观没有任何影响。

请注意,两个属性的名称不必相同。 在上面的示例中,ButtonControl.HorizontalContentAlignment 属性通过模板绑定到 ContentPresenterFrameworkElement.HorizontalAlignment 属性。 这样就可以水平放置按钮的内容。 ContentPresenter 没有名为 HorizontalContentAlignment 的属性,但 Control.HorizontalContentAlignment 可以绑定到 FrameworkElement.HorizontalAlignment。 通过模板绑定属性时,请确保目标属性和源属性是同一类型。

Control 类定义多个必须由控件模板使用的属性,以便可以通过设置这些属性来对控件产生一定的作用。 ControlTemplate 如何使用这些属性视具体的属性而定。 ControlTemplate 必须以下列方式之一使用属性:

下表列出了 Control 类的控件所继承的可视属性。 它还指示控件的默认控件模板是使用继承的属性值还是必须是通过模板绑定。

Property

使用方法

Background

模板绑定

BorderThickness

模板绑定

BorderBrush

模板绑定

FontFamily

属性继承或模板绑定

FontSize

属性继承或模板绑定

FontStretch

属性继承或模板绑定

FontWeight

属性继承或模板绑定

Foreground

属性继承或模板绑定

HorizontalContentAlignment

模板绑定

Padding

模板绑定

VerticalContentAlignment

模板绑定

此表只列出从 Control 类继承的可视属性。 除了表中列出的属性之外,控件还可以继承父框架元素的 DataContextLanguageTextDecorations 属性。

另外,如果 ContentPresenter 位于 ContentControlControlTemplate 中,则 ContentPresenter 将自动绑定到 ContentTemplateContent 属性。 同样,位于 ItemsControlControlTemplate 中的 ItemsPresenter 将自动绑定到 ItemsItemsPresenter 属性。

下面的示例创建两个使用上一示例中定义的 ControlTemplate 的按钮。 该示例设置每个按钮的 BackgroundForegroundFontSize 属性。 设置 Background 属性也有作用,因为它在 ControlTemplate 中是通过模板绑定的。 即使 ForegroundFontSize 属性不是通过模板绑定的,设置它们也有作用,因为它们的值是通过继承而来的。

<StackPanel>
  <Button Style="{StaticResource newTemplate}" 
          Background="Navy" Foreground="White" FontSize="14"
          Content="Button1"/>

  <Button Style="{StaticResource newTemplate}" 
          Background="Purple" Foreground="White" FontSize="14"
          Content="Button2" HorizontalContentAlignment="Left"/>
</StackPanel>

前面示例生成的输出结果与下图相似。

具有不同背景色的两个按钮

两个按钮,其中一个蓝色,一个紫色。

根据控件状态更改控件外观

具有默认外观的按钮和上一示例中的按钮之间的区别是默认按钮在处于不同状态时会进行相应变化。 例如,当按下默认按钮或将鼠标指针悬停在默认按钮上方时,按钮外观会产生变化。 虽然 ControlTemplate 不更改控件的功能,但它更改控件的可视行为。 可视行为描述控件处于特定状态时的控件外观。 若要了解控件的功能与可视行为之间的区别,请考虑按钮示例。 按钮的功能是在被单击时引发 Click 事件,而按钮的可视行为是在指向它或按下它时更改按钮的外观。

可以使用 VisualState 对象指定控件在处于特定状态时的外观。 VisualState 包含 Storyboard,用于更改 ControlTemplate 中的元素的外观。 您无需编写任何代码即可实现此目的,这是因为控件的逻辑可通过使用 VisualStateManager 来更改状态。 控件进入 VisualState.Name 属性指定的状态时,Storyboard 开始。 控件退出该状态时,Storyboard 停止。

下面的示例演示当鼠标指针悬停在 Button 上方时更改其外观的 VisualStateStoryboard 通过更改 BorderBrush 的颜色来更改按钮的边框颜色。 如果您查阅本主题开始处的 ControlTemplate 示例,就会记得 BorderBrush 是分配给 BorderBackgroundSolidColorBrush 的名称。

<!--Change the border of the button to red when the
    mouse is over the button.-->
<VisualState x:Name="MouseOver">
  <Storyboard>
    <ColorAnimation Storyboard.TargetName="BorderBrush"     
                    Storyboard.TargetProperty="Color"
                    To="Red" />

  </Storyboard>
</VisualState>

按照控件协定,该控件负责定义状态,本主题后面的通过了解控件协定自定义其他控件将对此进行详细讨论。 下表列出了为 Button 指定的状态。

VisualState 名称

VisualStateGroup 名称

说明

正常

CommonStates

默认状态。

MouseOver

CommonStates

鼠标指针悬停在控件上。

Pressed(已按下)

CommonStates

控件已按下。

Disabled

CommonStates

控件被禁用。

Focused

FocusStates

控件具有焦点。

Unfocused

FocusStates

控件不具有焦点。

Button 定义两个状态组:CommonStates 组包含 Normal、MouseOver、Pressed 和 Disabled 状态。 FocusStates 组包含 Focused 和 Unfocused 状态。 同一状态组中的状态是互斥的。 控件始终只能处于每组状态中的一种。 例如,Button 甚至在鼠标指针未悬停在其上方时都可以具有焦点,因此处于 Focused 状态的 Button 可以处于 MouseOver、Pressed 或 Normal 状态。

可以将 VisualState 对象添加到 VisualStateGroup 对象中。 可以将 VisualStateGroup 对象添加到 VisualStateManager.VisualStateGroups 附加属性。 下面的示例为 Normal、MouseOver 和 Pressed 等状态定义 VisualState 对象,这些状态全部包含在 CommonStates 组中。 每个 VisualStateName 与上表中的名称匹配。 省略了 Disabled 状态和 FocusStates 组中的状态以使该示例简短,但本主题结尾的完整示例中包含了这些状态。

注意注意

请务必设置 ControlTemplate 的根 FrameworkElement 上的 VisualStateManager.VisualStateGroups 附加属性。

<ControlTemplate TargetType="Button">
  <Border Name="RootElement">

    <VisualStateManager.VisualStateGroups>

      <!--Define the states and transitions for the common states.
          The states in the VisualStateGroup are mutually exclusive to
          each other.-->
      <VisualStateGroup Name="CommonStates">

        <!--The Normal state is the state the button is in
            when it is not in another state from this VisualStateGroup.-->
        <VisualState Name="Normal" />

        <!--Change the SolidColorBrush, BorderBrush, to red when the
            mouse is over the button.-->
        <VisualState Name="MouseOver">
          <Storyboard>
            <ColorAnimation Storyboard.TargetName="BorderBrush" 
                            Storyboard.TargetProperty="Color" 
                            To="Red" />
          </Storyboard>
        </VisualState>

        <!--Change the SolidColorBrush, BorderBrush, to Transparent when the
            button is pressed.-->
        <VisualState Name="Pressed">
          <Storyboard>
            <ColorAnimation Storyboard.TargetName="BorderBrush" 
                            Storyboard.TargetProperty="Color"
                            To="Transparent"/>
          </Storyboard>
        </VisualState>

        <!--The Disabled state is omitted for brevity.-->
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

    <Border.Background>
      <SolidColorBrush x:Name="BorderBrush" Color="Black"/>
    </Border.Background>

    <Grid Background="{TemplateBinding Background}" Margin="4">
      <ContentPresenter
        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
        Margin="4,5,4,4" />
    </Grid>
  </Border>
</ControlTemplate>

前面示例生成的输出结果与下图相似。

一个处于正常状态的使用自定义控件模板的按钮

一个具有自定义控件模板的按钮。

一个处于鼠标悬停状态的使用自定义控件模板的按钮

一个具有红色边框的按钮。

一个处于按下状态的使用自定义控件模板的按钮

按下的按钮上的边框是透明的。

若要查找 WPF 附带的控件的可视状态,请参见Control 样式和模板

指定控件在状态间转换时的行为

在前面的示例中,用户单击按钮时,按钮的外观也会更改,但是用户只有将按钮按下整整 1 秒时才会看到更改效果。 默认情况下,动画要 1 秒后才产生动作。 因为用户可能在不到 1 秒的时间内单击并松开按钮,如果将 ControlTemplate 保持为默认状态,在视觉上将看不到什么变化。

通过将 VisualTransition 对象添加到 ControlTemplate,可以指定动画产生动作的延迟时间,以使控件从一种状态平滑地转换为另一种状态。 创建 VisualTransition 时,指定以下一项或多项内容:

  • 状态转换所耗费的时间。

  • 转换时发生的控件外观的其他更改。

  • VisualTransition 所应用于的状态。

指定转换的持续时间

可以通过设置 GeneratedDuration 属性来指定转换所需的时间。 上面的示例中有一个 VisualState,用于指定按下按钮时按钮边框变为透明,但如果按钮被快速按下后又松开,该动画将来不及产生动作。 您可以使用 VisualTransition 指定控件转换为按下状态所耗费的时间。 下面的示例指定控件进入按下状态需要百分之一秒的时间。

<!--Take one hundredth of a second to transition to the
    Pressed state.-->
<VisualTransition To="Pressed" 
                  GeneratedDuration="0:0:0.01" />

指定转换期间控件外观的更改

VisualTransition 包含一个当控件转换状态时开始工作的 Storyboard。 例如,您可以指定当控件从 MouseOver 状态转换为 Normal 状态时某动画产生动作。 下面的示例创建一个 VisualTransition,它指定当用户将鼠标指针从按钮上移开时,按钮的边框在 1.5 秒内先变为蓝色,然后变为黄色,最后变为黑色。

<!--Take one and a half seconds to transition from the
    MouseOver state to the Normal state. 
    Have the SolidColorBrush, BorderBrush, fade to blue, 
    then to yellow, and then to black in that time.-->
<VisualTransition From="MouseOver" To="Normal" 
                      GeneratedDuration="0:0:1.5">
  <Storyboard>
    <ColorAnimationUsingKeyFrames
      Storyboard.TargetProperty="Color"
      Storyboard.TargetName="BorderBrush"
      FillBehavior="HoldEnd" >

      <ColorAnimationUsingKeyFrames.KeyFrames>

        <LinearColorKeyFrame Value="Blue" 
          KeyTime="0:0:0.5" />
        <LinearColorKeyFrame Value="Yellow" 
          KeyTime="0:0:1" />
        <LinearColorKeyFrame Value="Black" 
          KeyTime="0:0:1.5" />

      </ColorAnimationUsingKeyFrames.KeyFrames>
    </ColorAnimationUsingKeyFrames>
  </Storyboard>
</VisualTransition>

指定何时应用 VisualTransition

可以将 VisualTransition 限制为仅应用于某些状态,或在控件进行状态转换的任意时间均可应用。 在上面的示例中,当控件从 MouseOver 状态进入 Normal 状态时应用 VisualTransition;在该示例前面的示例中,当控件进入 Pressed 状态时应用 VisualTransition。 通过设置 ToFrom 属性,限制应用 VisualTransition 的时间。 下表说明从最高限制到最低限制的各个限制级别。

限制类型

过渡开始值

过渡结束值

从一个指定状态到另一个指定状态

VisualState 的名称

VisualState 的名称

从任意状态到指定状态

未设置

VisualState 的名称

从指定状态到任意状态

VisualState 的名称

未设置

从任意状态到任意其他状态

未设置

未设置

您可以具有 VisualStateGroup 中引用相同状态的多个 VisualTransition 对象,但是将按上表中指定的顺序使用它们。 在下面的示例中,有两个 VisualTransition 对象。 当控件从 Pressed 状态转换为 MouseOver 状态时,使用设置了 FromToVisualTransition。 当控件从非 Pressed 的状态转换为 MouseOver 状态时,使用其他状态。

<!--Take one half second to trasition to the MouseOver state.-->
<VisualTransition To="MouseOver" 
                  GeneratedDuration="0:0:0.5" />

<!--Take one hundredth of a second to transition from the
    Pressed state to the MouseOver state.-->
<VisualTransition From="Pressed" To="MouseOver" 
                  GeneratedDuration="0:0:0.01" />

VisualStateGroup 具有 Transitions 属性,该属性包含 VisualTransition 对象,这些对象应用于 VisualStateGroup 中的 VisualState 对象。 作为 ControlTemplate 作者,您可以随意包括任何想要的 VisualTransition。 不过,如果 ToFrom 属性设置为 VisualStateGroup 中不存在的状态名称,将忽略 VisualTransition

下面的示例演示 CommonStates 的 VisualStateGroup。 该示例定义每个按钮的下列转换的 VisualTransition

  • 进入 Pressed 状态。

  • 进入 MouseOver 状态。

  • 从 Pressed 状态进入 MouseOver 状态。

  • 从 MouseOver 状态进入 Normal 状态。

<VisualStateGroup Name="CommonStates">

  <!--Define the VisualTransitions that
      can be used when the control transitions 
      between VisualStates that are defined in the
      VisualStatGroup.-->
  <VisualStateGroup.Transitions>

    <!--Take one hundredth of a second to 
        transition to the Pressed state.-->
    <VisualTransition To="Pressed" 
                      GeneratedDuration="0:0:0.01" />

    <!--Take one half second to trasition 
        to the MouseOver state.-->
    <VisualTransition To="MouseOver" 
                      GeneratedDuration="0:0:0.5" />

    <!--Take one hundredth of a second to transition from the
        Pressed state to the MouseOver state.-->
    <VisualTransition From="Pressed" To="MouseOver" 
                      GeneratedDuration="0:0:0.01" />

    <!--Take one and a half seconds to transition from the
        MouseOver state to the Normal state. 
        Have the SolidColorBrush, BorderBrush, fade to blue, 
        then to yellow, and then to black in that time.-->
    <VisualTransition From="MouseOver" To="Normal" 
                      GeneratedDuration="0:0:1.5">
      <Storyboard>
        <ColorAnimationUsingKeyFrames
          Storyboard.TargetProperty="Color"
          Storyboard.TargetName="BorderBrush"
          FillBehavior="HoldEnd" >

          <ColorAnimationUsingKeyFrames.KeyFrames>
            <LinearColorKeyFrame Value="Blue" 
              KeyTime="0:0:0.5" />
            <LinearColorKeyFrame Value="Yellow" 
              KeyTime="0:0:1" />
            <LinearColorKeyFrame Value="Black" 
              KeyTime="0:0:1.5" />

          </ColorAnimationUsingKeyFrames.KeyFrames>
        </ColorAnimationUsingKeyFrames>
      </Storyboard>
    </VisualTransition>
  </VisualStateGroup.Transitions>

  <!--The remainder of the VisualStateGroup is the
      same as the previous example.-->

  <VisualState Name="Normal" />

  <VisualState Name="MouseOver">
    <Storyboard>
      <ColorAnimation 
        Storyboard.TargetName="BorderBrush" 
        Storyboard.TargetProperty="Color" 
        To="Red" />

    </Storyboard>
  </VisualState>

  <VisualState Name="Pressed">
    <Storyboard>
      <ColorAnimation 
        Storyboard.TargetName="BorderBrush" 
        Storyboard.TargetProperty="Color" 
        To="Transparent"/>
    </Storyboard>
  </VisualState>

  <!--The Disabled state is omitted for brevity.-->

</VisualStateGroup>

通过了解控件协定自定义其他控件

使用 ControlTemplate 指定其可视结构(通过使用 FrameworkElement 对象)和可视行为(通过使用 VisualState 对象)的控件使用了部件控件模型。 WPF 4 附带的许多控件都使用此模型。 ControlTemplate 作者需要了解的部件是通过控件协定传递的。 了解控件协定的组成部分之后,您可以自定义使用部件控件模型的任何控件的外观。

控件协定具有三个元素:

  • 控件的逻辑使用的可视元素。

  • 控件的状态以及每个状态所属的组。

  • 以可视方式影响控件的公共属性。

控件协定中的可视元素

有时,控件的逻辑与 ControlTemplate 中的 FrameworkElement 进行交互。 例如,控件可能处理其元素之一的一个事件。 如果控件期望在 ControlTemplate 中找到特定 FrameworkElement,它必须将该信息传递给 ControlTemplate 作者。 控件使用 TemplatePartAttribute 传递期望使用的元素类型,以及元素应具有的名称。 Button 在其控件协定中没有 FrameworkElement 部件,但是其他控件(例如 ComboBox)在其控件协定中有该部件。

下面的示例演示 ComboBox 类上指定的 TemplatePartAttribute 对象。 ComboBox 的逻辑期望在其 ControlTemplate 中找到名为 PART_EditableTextBox 的 TextBox 和名为 PART_Popup 的 Popup

<TemplatePartAttribute(Name:="PART_EditableTextBox", Type:=GetType(TextBox))> _
<TemplatePartAttribute(Name:="Part_Popup", Type:=GetType(Popup))> _
Public Class ComboBox
    Inherits ItemsControl

End Class
[TemplatePartAttribute(Name = "PART_EditableTextBox", Type = typeof(TextBox))]
[TemplatePartAttribute(Name = "PART_Popup", Type = typeof(Popup))]
public class ComboBox : ItemsControl
{
}

下面的示例演示一个简单的 ControlTemplate,该对象用于 ComboBox,其中包括由 ComboBox 类上的 TemplatePartAttribute 对象指定的元素。

<ControlTemplate TargetType="ComboBox">
  <Grid>
    <ToggleButton x:Name="DropDownToggle"
      HorizontalAlignment="Stretch" VerticalAlignment="Stretch"  
      Margin="-1" HorizontalContentAlignment="Right"
      IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,
                  RelativeSource={RelativeSource TemplatedParent}}">
      <Path x:Name="BtnArrow" Height="4" Width="8" 
        Stretch="Uniform" Margin="0,0,6,0"  Fill="Black"
        Data="F1 M 300,-190L 310,-190L 305,-183L 301,-190 Z " />
    </ToggleButton>
    <ContentPresenter x:Name="ContentPresenter" Margin="6,2,25,2"
      Content="{TemplateBinding SelectionBoxItem}"
      ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
      ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}">
    </ContentPresenter>
    <TextBox x:Name="PART_EditableTextBox"
      Style="{x:Null}"
      Focusable="False"
      Background="{TemplateBinding Background}"
      HorizontalAlignment="Left" 
      VerticalAlignment="Center" 
      Margin="3,3,23,3"
      Visibility="Hidden"
      IsReadOnly="{TemplateBinding IsReadOnly}"/>

    <Popup x:Name="PART_Popup"
      IsOpen="{TemplateBinding IsDropDownOpen}">
      <Border x:Name="PopupBorder" 
        HorizontalAlignment="Stretch" Height="Auto" 
        MinWidth="{TemplateBinding ActualWidth}"
        MaxHeight="{TemplateBinding MaxDropDownHeight}"
        BorderThickness="{TemplateBinding BorderThickness}" 
        BorderBrush="Black" Background="White" CornerRadius="3">
        <ScrollViewer x:Name="ScrollViewer" BorderThickness="0" Padding="1">
          <ItemsPresenter/>
        </ScrollViewer>
      </Border>
    </Popup>

  </Grid>
</ControlTemplate>

控件协定中的状态

控件状态也是控件协定的组成部分。 为 Button 创建 ControlTemplate 的示例演示了如何根据 Button 的状态指定其外观。 可以为每个指定的状态创建一个 VisualState,并将所有共享 GroupNameVisualState 对象放在一个 VisualStateGroup 中,如本主题前面的根据控件状态更改控件外观中所述。 第三方控件应使用 TemplateVisualStateAttribute 指定状态,这样可使设计器工具(如 Expression Blend)为创作控件模板来公开控件的状态。

若要查找 WPF 附带的控件的控件协定,请参见Control 样式和模板

控件协定中的属性

控件协定中还包含了以可视方式影响控件的公共属性。 可以设置这些属性来更改控件的外观,无需创建新的 ControlTemplate。 您还可以使用 TemplateBinding 标记扩展将 ControlTemplate 中元素的属性绑定到由 Button 定义的公共属性。

下面的示例演示按钮的控件协定。

    <TemplateVisualState(Name:="Normal", GroupName:="CommonStates")> _
    <TemplateVisualState(Name:="MouseOver", GroupName:="CommonStates")> _
    <TemplateVisualState(Name:="Pressed", GroupName:="CommonStates")> _
    <TemplateVisualState(Name:="Disabled", GroupName:="CommonStates")> _
    <TemplateVisualState(Name:="Unfocused", GroupName:="FocusStates")> _
    <TemplateVisualState(Name:="Focused", GroupName:="FocusStates")> _
    Public Class Button
        Inherits ButtonBase

        Public Shared ReadOnly BackgroundProperty As DependencyProperty
        Public Shared ReadOnly BorderBrushProperty As DependencyProperty
        Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
        Public Shared ReadOnly ContentProperty As DependencyProperty
        Public Shared ReadOnly ContentTemplateProperty As DependencyProperty
        Public Shared ReadOnly FontFamilyProperty As DependencyProperty
        Public Shared ReadOnly FontSizeProperty As DependencyProperty
        Public Shared ReadOnly FontStretchProperty As DependencyProperty
        Public Shared ReadOnly FontStyleProperty As DependencyProperty
        Public Shared ReadOnly FontWeightProperty As DependencyProperty
        Public Shared ReadOnly ForegroundProperty As DependencyProperty
        Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
        Public Shared ReadOnly PaddingProperty As DependencyProperty
        Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
        Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
        Public Shared ReadOnly TextWrappingProperty As DependencyProperty
        Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty

        Public Background As Brush
        Public BorderBrush As Brush
        Public BorderThickness As Thickness
        Public Content As Object
        Public ContentTemplate As DataTemplate
        Public FontFamily As FontFamily
        Public FontSize As Double
        Public FontStretch As FontStretch
        Public FontStyle As FontStyle
        Public FontWeight As FontWeight
        Public Foreground As Brush
        Public HorizontalContentAlignment As HorizontalAlignment
        Public Padding As Thickness
        Public TextAlignment As TextAlignment
        Public TextDecorations As TextDecorationCollection
        Public TextWrapping As TextWrapping
        Public VerticalContentAlignment As VerticalAlignment
    End Class

[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "MouseOver", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Pressed", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusStates")]
public class Button : ButtonBase
{
    public static readonly DependencyProperty BackgroundProperty;
    public static readonly DependencyProperty BorderBrushProperty;
    public static readonly DependencyProperty BorderThicknessProperty;
    public static readonly DependencyProperty ContentProperty;
    public static readonly DependencyProperty ContentTemplateProperty;
    public static readonly DependencyProperty FontFamilyProperty;
    public static readonly DependencyProperty FontSizeProperty;
    public static readonly DependencyProperty FontStretchProperty;
    public static readonly DependencyProperty FontStyleProperty;
    public static readonly DependencyProperty FontWeightProperty;
    public static readonly DependencyProperty ForegroundProperty;
    public static readonly DependencyProperty HorizontalContentAlignmentProperty;
    public static readonly DependencyProperty PaddingProperty;
    public static readonly DependencyProperty TextAlignmentProperty;
    public static readonly DependencyProperty TextDecorationsProperty;
    public static readonly DependencyProperty TextWrappingProperty;
    public static readonly DependencyProperty VerticalContentAlignmentProperty;

    public Brush Background { get; set; }
    public Brush BorderBrush { get; set; }
    public Thickness BorderThickness { get; set; }
    public object Content { get; set; }
    public DataTemplate ContentTemplate { get; set; }
    public FontFamily FontFamily { get; set; }
    public double FontSize { get; set; }
    public FontStretch FontStretch { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontWeight FontWeight { get; set; }
    public Brush Foreground { get; set; }
    public HorizontalAlignment HorizontalContentAlignment { get; set; }
    public Thickness Padding { get; set; }
    public TextAlignment TextAlignment { get; set; }
    public TextDecorationCollection TextDecorations { get; set; }
    public TextWrapping TextWrapping { get; set; }
    public VerticalAlignment VerticalContentAlignment { get; set; }
}

创建 ControlTemplate 时,最方便的方法通常是打开现有 ControlTemplate,然后对其进行更改。 可以执行下列操作之一来更改现有 ControlTemplate

完整的示例

下面的示例演示本主题讨论的完整 Button ControlTemplate

<StackPanel>
  <StackPanel.Resources>
    <Style TargetType="Button" x:Key="newTemplate">
      <!--Set the Background, Foreground, FontSize, Width, 
                  Height, Margin, and Template properties for
                  the Button.-->
      <Setter Property="Background" Value="Navy"/>
      <Setter Property="Foreground" Value="White"/>
      <Setter Property="FontSize" Value="14"/>
      <Setter Property="Width" Value="100"/>
      <Setter Property="Height" Value="40"/>
      <Setter Property="Margin" Value="10"/>
      <Setter Property="HorizontalContentAlignment" Value="Center"/>
      <Setter Property="VerticalContentAlignment" Value="Center"/>
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Button">
            <Border x:Name="RootElement">
              <VisualStateManager.VisualStateGroups>

                <!--Define the states and transitions for the common states.
                    The states in the VisualStateGroup are mutually exclusive to
                    each other.-->
                <VisualStateGroup Name="CommonStates">

                  <!--Define the VisualTransitions that can be used when the control
                      transitions between VisualStates that are defined in the
                      VisualStatGroup.-->
                  <VisualStateGroup.Transitions>

                    <!--Take one hundredth of a second to transition to the
                        Pressed state.-->
                    <VisualTransition To="Pressed" 
                                      GeneratedDuration="0:0:0.01" />

                    <!--Take one half second to trasition to the MouseOver state.-->
                    <VisualTransition To="MouseOver" 
                                      GeneratedDuration="0:0:0.5" />

                    <!--Take one hundredth of a second to transition from the
                        Pressed state to the MouseOver state.-->
                    <VisualTransition From="Pressed" To="MouseOver" 
                                      GeneratedDuration="0:0:0.01" />

                    <!--Take one and a half seconds to transition from the
                        MouseOver state to the Normal state. 
                        Have the SolidColorBrush, BorderBrush, fade to blue, 
                        then to yellow, and then to black in that time.-->
                    <VisualTransition From="MouseOver" To="Normal" 
                                          GeneratedDuration="0:0:1.5">
                      <Storyboard>
                        <ColorAnimationUsingKeyFrames
                          Storyboard.TargetProperty="Color"
                          Storyboard.TargetName="BorderBrush"
                          FillBehavior="HoldEnd" >

                          <ColorAnimationUsingKeyFrames.KeyFrames>

                            <LinearColorKeyFrame Value="Blue" 
                              KeyTime="0:0:0.5" />
                            <LinearColorKeyFrame Value="Yellow" 
                              KeyTime="0:0:1" />
                            <LinearColorKeyFrame Value="Black" 
                              KeyTime="0:0:1.5" />

                          </ColorAnimationUsingKeyFrames.KeyFrames>
                        </ColorAnimationUsingKeyFrames>
                      </Storyboard>
                    </VisualTransition>
                  </VisualStateGroup.Transitions>

                  <!--The Normal state is the state the button is in
                      when it is not in another state from this VisualStateGroup.
                      There is no special visual behavior for this state, but
                      the VisualState must be defined in order for the button
                      to return to its initial state.-->
                  <VisualState x:Name="Normal" />

                  <!--Change the border of the button to red when the
                      mouse is over the button.-->
                  <VisualState x:Name="MouseOver">
                    <Storyboard>
                      <ColorAnimation Storyboard.TargetName="BorderBrush"     
                                      Storyboard.TargetProperty="Color"
                                      To="Red" />

                    </Storyboard>
                  </VisualState>

                  <!--Change the border of the button to Transparent when the
                      button is pressed.-->
                  <VisualState x:Name="Pressed">
                    <Storyboard >
                      <ColorAnimation Storyboard.TargetName="BorderBrush" 
                                      Storyboard.TargetProperty="Color" 
                                      To="Transparent" 
                                      />
                    </Storyboard>
                  </VisualState>

                  <!--Show the DisabledRect when the IsEnabled property on
                      the button is false.-->
                  <VisualState x:Name="Disabled">
                    <Storyboard>
                      <DoubleAnimation Storyboard.TargetName="DisabledRect" 
                                       Storyboard.TargetProperty="Opacity"
                                       To="1" Duration="0" />
                    </Storyboard>
                  </VisualState>
                </VisualStateGroup>

                <!--Define the states and transitions for the focus states.
                    The states in the VisualStateGroup are mutually exclusive to
                    each other.-->
                <VisualStateGroup x:Name="FocusStates">

                  <!--Define the VisualStates in this VistualStateGroup.-->
                  <VisualState x:Name="Focused">
                    <Storyboard>
                      <ObjectAnimationUsingKeyFrames 
                        Storyboard.TargetName="FocusVisual" 
                        Storyboard.TargetProperty="Visibility" Duration
                        ="0">

                        <DiscreteObjectKeyFrame KeyTime="0">
                          <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                          </DiscreteObjectKeyFrame.Value>
                        </DiscreteObjectKeyFrame>
                      </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                  </VisualState>
                  <VisualState x:Name="Unfocused">
                    <Storyboard>
                      <ObjectAnimationUsingKeyFrames 
                        Storyboard.TargetName="FocusVisual" 
                        Storyboard.TargetProperty="Visibility"
                        Duration="0">

                        <DiscreteObjectKeyFrame KeyTime="0">
                          <DiscreteObjectKeyFrame.Value>
                            <Visibility>Collapsed</Visibility>
                          </DiscreteObjectKeyFrame.Value>
                        </DiscreteObjectKeyFrame>
                      </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                  </VisualState>
                </VisualStateGroup>
              </VisualStateManager.VisualStateGroups>

              <!--Create the SolidColorBrush for the Background 
                  as an object elemment and give it a name so 
                  it can be referred to elsewhere in the control template.-->
              <Border.Background>
                <SolidColorBrush x:Name="BorderBrush" Color="Black"/>
              </Border.Background>

              <!--Create a border that has a different color by adding smaller grid.
                  The background of this grid is specified by the button's Background
                  property.-->
              <Grid Background="{TemplateBinding Background}" Margin="4">

                <!--Create a Rectangle that indicates that the
                    Button has focus.-->
                <Rectangle Name="FocusVisual" 
                           Visibility="Collapsed" Margin="2" 
                           Stroke="{TemplateBinding Foreground}" 
                           StrokeThickness="1" 
                           StrokeDashArray="1.5 1.5"/>

                <!--Use a ContentPresenter to display the Content of
                    the Button.-->
                <ContentPresenter
                  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                  Margin="4,5,4,4" />

                <!--Create a rectangle that causes the button to appear
                    grayed out when it is disabled.-->
                <Rectangle x:Name="DisabledRect" 
                         Fill="#A5FFFFFF"
                         Opacity="0" IsHitTestVisible="false" />
              </Grid>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

  </StackPanel.Resources>

  <Button Style="{StaticResource newTemplate}" 
          Content="Button1"/>

  <Button Style="{StaticResource newTemplate}"
          Background="Purple" 
          Content="Button2" />
</StackPanel>

请参见

概念

样式设置和模板化