样式设置和模板化

更新:2007 年 11 月

Windows Presentation Foundation (WPF) 样式设置和模板化是指一套功能(样式、模板、触发器和演示图板),应用程序、文档或用户界面 (UI) 的设计人员使用这些功能可以创建更好的视觉效果,也可以对其产品的统一外观进行标准化。尽管作者或设计人员可以对应用程序的外观逐个进行大量自定义操作,他们还是需要一个功能强大的样式设置和模板化模型,以便在应用程序内部和应用程序之间维护和共享外观。Windows Presentation Foundation (WPF) 就提供了这样的模型。

WPF 样式设置模型的另一个功能是实现表示形式与逻辑的分离。这意味着,在开发人员使用 C# 或 Visual Basic 进行逻辑编程时,设计人员只需使用 XAML 即可设计程序的外观。

本概述将讨论介绍样式设置和模板化的示例应用程序,该应用程序具有两个 TextBlock 元素,以及一个绑定到图像列表的 ListBox 控件:

带样式的 ListView

本概述主要讨论该应用程序的样式设置和模板化方面,而不讨论任何数据绑定概念。有关数据绑定的信息,请参见数据绑定概述

另外,了解资源也很重要,正是资源使得样式和模板得以重用。有关资源的更多信息,请参见资源概述

本主题包括下列各节。

  • 样式基础知识
  • 数据模板
  • 控件模板
  • 触发器
  • 共享的资源和主题
  • 相关主题

样式基础知识

您可以将 Style 看作是将一组属性值应用到多个元素的捷径。例如,考虑下面的 TextBlock 元素及其默认外观:

<TextBlock>My Pictures</TextBlock>
<TextBlock>Check out my new pictures!</TextBlock>

样式示例屏幕快照

通过直接对每个 TextBlock 元素设置 FontSizeFontFamily 等属性可以更改默认外观。但是,如果希望 TextBlock 元素可以共享某些属性,则可以在 XAML 文件的 Resources 节中创建一个 Style,如下所示:

<Window.Resources>


...


<!--A Style that affects all TextBlocks-->
<Style TargetType="TextBlock">
  <Setter Property="HorizontalAlignment" Value="Center" />
  <Setter Property="FontFamily" Value="Comic Sans MS"/>
  <Setter Property="FontSize" Value="14"/>
</Style>


...


</Window.Resources>

将样式的 TargetType 设置为 TextBlock 类型时,该样式会应用于窗口中的所有 TextBlock 元素。

现在,TextBlock 元素的外观如下:

样式示例屏幕快照

本节包含下列子节。

  • 扩展样式
  • TargetType 属性 (Property) 和 x:Key 属性 (Attribute) 的关系
  • 样式和资源
  • 以编程方式设置样式
  • 绑定、动态资源和事件处理程序

扩展样式

您可能希望两个 TextBlock 元素共享某些属性值(如 FontFamily 和居中的 HorizontalAlignment),同时还希望文本“我的图片”具有某些其他属性。在第一个样式的基础上创建一个新样式可以达到这一目的,如下所示:

<Window.Resources>


...


<!--A Style that extends the previous TextBlock Style-->
<!--This is a "named style" with an x:Key of TitleText-->
<Style BasedOn="{StaticResource {x:Type TextBlock}}"
       TargetType="TextBlock"
       x:Key="TitleText">
  <Setter Property="FontSize" Value="26"/>
  <Setter Property="Foreground">
  <Setter.Value>
      <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
        <LinearGradientBrush.GradientStops>
          <GradientStop Offset="0.0" Color="#90DDDD" />
          <GradientStop Offset="1.0" Color="#5BFFFF" />
        </LinearGradientBrush.GradientStops>
      </LinearGradientBrush>
    </Setter.Value>
  </Setter>
</Style>


...


</Window.Resources>

请注意,已为上面的样式提供了 x:Key。若要应用该样式,请将 TextBlockStyle 属性设置为 x:Key 值,如下所示:

<TextBlock Style="{StaticResource TitleText}" Name="textblock1">My Pictures</TextBlock>
<TextBlock>Check out my new pictures!</TextBlock>

现在,此 TextBlock 样式的 HorizontalAlignment 值为 CenterFontFamily 值为 Comic Sans MS,FontSize 值为 26,Foreground 值设置为示例所示的 LinearGradientBrush。请注意,我们已重写了基础样式的 FontSize 值。如果有多个 SetterStyle 的同一属性进行设置,则最后声明的 Setter 优先。

下面演示 TextBlock 元素现在的外观:

带样式的 TextBlock

此 TitleText 样式扩展了为 TextBlock 类型创建的样式。此外,使用 x:Key 值也可以扩展具有 x:Key 的样式。有关示例,请参见为 BasedOn 属性提供的示例。

TargetType 属性 (Property) 和 x:Key 属性 (Attribute) 的关系

如第一个示例所示,如果将 TargetType 属性设置为 TextBlock 而不为样式分配 x:Key,样式就会应用于所有 TextBlock 元素。这种情况下,x:Key 隐式设置为 {x:Type TextBlock}。这意味着,如果将 x:Key 值显式设置为 {x:Type TextBlock} 之外的任何值,Style 就不会自动应用于所有 TextBlock 元素。此时,必须通过使用 x:Key 值,将样式显式应用于 TextBlock 元素。如果样式位于资源部分,并且未设置样式的 TargetType 属性,则必须提供 x:Key。

除了提供 x:Key 的默认值之外,TargetType 属性还指定要应用 setter 属性的类型。如果未指定 TargetType,则必须通过语法 Property="ClassName.Property",用类名限定 Setter 对象的属性。例如,必须将 Property 设置为 "TextBlock.FontSize" 或 "Control.FontSize",而不要设置 Property="FontSize"。

此外,还应注意,很多 WPF 控件都是由其他 WPF 控件组合而成的。如果创建要应用于某个类型的所有控件的样式,可能会得到意想不到的结果。例如,如果针对 Window 中的 TextBlock 类型创建一个样式,则该样式会应用于窗口中的所有 TextBlock 控件,即使 TextBlock 是另一个控件(如 ListBox)的组成部分也不例外。

样式和资源

任何派生自 FrameworkElementFrameworkContentElement 的元素都可以使用样式。声明样式的最常见方式是将样式作为 XAML 文件的 Resources 节中的资源,如前面的示例所示。由于样式是资源,因此同样遵循所有资源都适用的范围规则:样式的声明位置决定样式的应用范围。例如,如果在应用程序定义 XAML 文件的根元素中声明样式,则样式可在应用程序范围内使用。如果创建的是导航应用程序,并在该应用程序的某个 XAML 文件中声明样式,则该样式只能在该 XAML 文件中使用。有关资源范围规则的更多信息,请参见资源概述

另外,有关样式和资源的更多信息,请参见本概述后面部分的共享的资源和主题。

以编程方式设置样式

若要以编程方式向元素分配命名样式,请从资源集合中获取该样式,然后将其分配给元素的 Style 属性。请注意,资源集合中的项是 Object 类型,因此,将检索到的样式分配给 Style 属性之前,必须将该样式强制转换为 Style。例如,若要对名为 textblock1 的 TextBlock 设置定义的 TitleText 样式,请执行以下操作:

textblock1.Style = (Style)(this.Resources["TitleText"]);

请注意,样式一旦应用,便会密封并且无法更改。如果要动态更改已应用的样式,必须创建一个新样式来替换现有样式。有关更多信息,请参见 IsSealed 属性。

您可以创建一个根据自定义逻辑选择要应用的样式的对象。有关示例,请参见为 StyleSelector 类提供的示例。

绑定、动态资源和事件处理程序

请注意,使用 Setter.Value 属性可以指定 绑定标记扩展DynamicResource 标记扩展。有关更多信息,请参见为 Setter.Value 属性提供的示例。

到目前为止,我们只讨论了如何使用 setter 设置属性值。在样式中也可以指定事件处理程序。有关更多信息,请参见 EventSetter

数据模板

在本示例应用程序中,有一个绑定到照片列表的 ListBox 控件。

<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}"
         Background="Silver" Width="600" Margin="10" SelectedIndex="0"/>

ListBox 当前的外观如下所示:

应用模板之前的 ListBox

大多数控件都具有某种类型的内容,这些内容通常来自绑定到的数据。在本示例中,数据为照片列表。在 WPF 中,使用 DataTemplate 可以定义数据的可视表示形式。基本上,输入 DataTemplate 的内容决定了数据在呈现的应用程序中的外观。

在我们的示例应用程序中,每个自定义 Photo 对象都具有一个字符串类型的 Source 属性,该属性指定图像的文件路径。当前,照片对象显示为文件路径。

对于要显示为图像的照片,可以将 DataTemplate 作为资源创建:

<Window.Resources>


...


<!--DataTemplate to display Photos as images
    instead of text strings of Paths-->
<DataTemplate DataType="{x:Type local:Photo}">
  <Border Margin="3">
    <Image Source="{Binding Source}"/>
  </Border>
</DataTemplate>


...


</Window.Resources>

请注意,DataType 属性与 StyleTargetType 属性非常相似。如果 DataTemplate 位于资源部分,并且将 DataType 属性指定为某个类型,也不为其分配 x:Key,则只要该类型出现,便会应用 DataTemplate。任何时候都可以为 DataTemplate 分配 x:Key,然后将其设置为 DataTemplate 类型的属性(如 ItemTemplate 属性或 ContentTemplate 属性)的 StaticResource。

实质上,上面示例的 DataTemplate 确定只要存在 Photo 对象,该对象就应作为 Image 显示在 Border 中。通过此 DataTemplate,应用程序现在的外观如下:

照片图

数据模板化模型还提供其他功能。例如,如果要使用 HeaderedItemsControl 类型(如 MenuTreeView)显示包含其他集合的集合数据,则可以使用 HierarchicalDataTemplate。另一个数据模板化功能是 DataTemplateSelector,利用这一功能可以根据自定义逻辑选择要使用的 DataTemplate。有关更多信息,请参见数据模板化概述,该概述对不同的数据模板化功能进行了更加深入的讨论。

控件模板

本节包含下列子节。

  • 不使用 ControlTemplate
  • 什么是 ControlTemplate?
  • 创建 ControlTemplate
  • IsItemsHost 属性
  • ItemsPresenter 和 ContentPresenter
  • TemplateBinding

请注意,我们的照片显示为图片,我们要水平显示这些照片,而不是垂直显示;我们希望 ListBox 是水平的。

不使用 ControlTemplate

首先,要使 ListBox 水平,不一定要使用 ControlTemplate,明确这一点很重要。ListBox 具有 ItemsPanel 属性,利用该属性可以设置 ItemsPanelTemplate,即控制 ListBox 的项的布局的模板。一种方法是只创建 ListBox 样式,然后设置 ItemsPanel 属性,如下例所示:

<Style TargetType="ListBox">
  <Setter Property="ItemsPanel">
    <Setter.Value>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"/>
      </ItemsPanelTemplate>
    </Setter.Value>
  </Setter>
</Style>

这种方法很有效,为我们提供了一个水平的 ListBox。此示例表明,除了替换 ControlTemplate 之外,可能还有其他方法,这取决于具体的情况。在本示例中,如果希望获得具有其他属性(如圆角)的水平 ListBox,则需要使用 ListBoxControlTemplate

在提供示例来演示具体操作之前,首先需要说明 ControlTemplate 的概念。

什么是 ControlTemplate?

大多数控件都具有外观和行为。以按钮为例:外观是可以按下的凸起区域,行为是在响应单击时所引发的 Click 事件。

有时,控件可以提供所需行为,但不具有所需外观。到目前为止,我们已经演示了可以使用样式 setter 来设置属性值,从而影响控件的外观。但是,若要更改控件的结构,或对组成控件的组件设置属性值,就需要使用 ControlTemplate

在 WPF 中,控件的 ControlTemplate 定义控件的外观。通过为控件定义新的 ControlTemplate 可以更改控件的结构和外观。很多情况下,这种方法都足够灵活,您不需要自己编写自定义控件。如果没有为控件定义自己的 ControlTemplate,则可以获取与系统主题匹配的默认模板,该模板向 Button 控件提供默认外观。

请注意:一旦为控件创建 ControlTemplate,就会替换整个 ControlTemplate。例如,可以通过以下方式定义 ButtonControlTemplate

请注意,ContentPresenter 元素只标记 ButtonContent 应出现在何处。后面有一节专门展开详细讨论。

<Style TargetType="Button">
  <!--Set to true to not get any properties from the themes.-->
  <Setter Property="OverridesDefaultStyle" Value="True"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="Button">
        <Grid>
          <Ellipse Fill="{TemplateBinding Background}"/>
          <ContentPresenter HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

应用此模板之后,Button 显示为 Ellipse

按钮 ControlTemplate 示例

请记住,当 Button 具有焦点或按下时,其外观都是将替换的按钮的默认外观的组成部分。因此,您可能希望定义按钮按下时的外观,这取决于您的具体需要。有关完整示例,请参见Button ControlTemplate 示例

如果要创建 ControlTemplate,使用 ControlTemplate 示例 是最好的入门方法。如果确实需要查看控件的组成部分,可以查看位于主题的主题文件,也可以使用 XAMLPad(随 Windows 软件开发工具包 (SDK) 安装的应用程序)的 Show Visual Tree 功能。

创建 ControlTemplate

现在,继续演示示例,我们创建一个 ControlTemplate,它定义一个水平的圆角 ListBox。若要替换控件的 ControlTemplate,请将 Template 属性设置为新的 ControlTemplate

<Style TargetType="ListBox">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ListBox">
        <Border CornerRadius="5" Background="{TemplateBinding ListBox.Background}">
          <ScrollViewer HorizontalScrollBarVisibility="Auto">
            <StackPanel Orientation="Horizontal"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center"
                       IsItemsHost="True"/>
          </ScrollViewer>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

以这种方式设置 Template 属性,实际上与使用 Style 设置其他控件属性没有区别:您将 Style 用作一个工具来帮助设置 Template 属性。也就是说,设置 ControlTemplate 的另一种方法是直接设置控件的 Template 属性。如果以这种方式设置,则会在 Resources 节中创建一个 ControlTemplate,并为它提供 x:Key,然后将它作为静态资源使用。有关示例,请参见 Template 属性。

如上例所示,ControlTemplate 类具有 TargetType 属性,该属性类似于 Style 类的 TargetType 属性。但要注意,与 StyleDataTemplate 不同,ControlTemplate 对象没有隐式键的概念。换言之,如果有一个独立 ControlTemplate,其 TargetType 属性设置为某个类型,则 ControlTemplate 不会自动应用于该类型。另请注意,如果模板定义包含 ContentPresenter,则 ControlTemplate 需要 TargetType 属性。

尝试使用 ControlTemplate。例如,用 WrapPanel 替换 StackPanel,将 ScrollViewerHorizontalScrollBarVisibility 属性设置为 Disabled,然后将 ListBox 的宽度设置为 300。(只有第一行空间不足时,WrapPanel 才会将项放置到下一行。如果没有将 ScrollViewerHorizontalScrollBarVisibility 属性设置为 Disabled,由于可以滚动到末尾,则第一行不会空间不足。因此,WrapPanel 不会对项进行换行。)

IsItemsHost 属性

在此示例中,一个必需的重要属性是 IsItemsHost 属性。IsItemsHost 属性用于指示在 ItemsControl(如处理项列表的 ListBox 控件)的模板中,生成的元素应放在什么位置。如果将 StackPanel 的这一属性设置为 true,则添加到 ListBox 的所有项都将进入 StackPanel。请注意,此属性只对 Panel 类型有效。

ItemsPresenter 和 ContentPresenter

请注意,如果以这种方式在 ControlTemplate 中指定一个面板并将其标记为 IsItemsHost,控件的用户不使用 ControlTemplate 就无法替换 ItemsPanel。因此,除非您确信必须使用模板才能替换面板,否则不要采用这种方式。此外,您也可以使用 ItemsPresenter 元素来标记项的位置,然后通过设置 ItemsPanel 属性来指定 ItemsPanelTemplateItemsPanelTemplate 页提供了一个示例,为您演示如何操作。有关使用 ItemsPresenter 的其他示例,请参见 TreeView ControlTemplate 示例

如果要创建 ContentControl(如 Button)的模板,则对应元素为 ContentPresenter。同样,将此元素放置到 ContentControl 类型的 ControlTemplate 中,可以指示内容应在什么位置显示,如什么是 ControlTemplate?一节中的示例所示。有关其他示例,请参见 Label ControlTemplate 示例ListBoxItem ControlTemplate 示例

TemplateBinding

在上一示例中,需要注意的另一个重点是设置为 {TemplateBinding ListBox.Background} 的 Background 值。它只是指示 BorderBackground 应与 ListBox 上设置的 Background 值同步。TemplateBinding 与 Binding 类似。实际上,TemplateBinding 比 Binding 更有效,但功能更弱;使用 TemplateBinding 等效于使用 Binding 属性设置为 Source.RelativeSourceTemplatedParent

若要使控件用户能够控制某些属性的值,可以在 ControlTemplate 中使用 TemplateBinding。TemplateBinding 是一个由 TemplateBindingExtension 类表示的标记扩展。

您可能已经注意到,DataTemplateControlTemplate 的相似之处在于它们的内容变成了对象的外观。通过 ListBoxControlTemplate 定义,应用程序现在的外观如下:

样式示例屏幕快照

触发器

StyleControlTemplateDataTemplate 都具有 Triggers 属性,该属性可以包含一组触发器。某个属性值更改时,或某个事件引发时,触发器会相应地设置属性或启动操作(如动画操作)。

本节包含下列子节。

  • 属性触发器
  • EventTrigger 和 Storyboard
  • MultiTrigger、DataTrigger 和 MultiDataTrigger

属性触发器

为了演示如何使用触发器来设置属性,我们将每个 ListBoxItem 都设置为部分透明(除非它被选中)。

下面的样式将 ListBoxItemOpacity 值设置为 0.5。但是,当 IsSelected 属性为 true 时,Opacity 设置为 1.0:

<Style TargetType="ListBoxItem">
  <Setter Property="Opacity" Value="0.5" />
  <Setter Property="MaxHeight" Value="75" />
  <Style.Triggers>
    <Trigger Property="IsSelected" Value="True">
        <Setter Property="Opacity" Value="1.0" />
    </Trigger>


...


  </Style.Triggers>
</Style>

此示例使用 Trigger 来设置属性值,但请注意,Trigger 类还具有 EnterActionsExitActions 属性,通过这两个属性,触发器可以执行操作。

请注意,我们还将 ListBoxItemMaxHeight 属性设置为 75。在下面的屏幕快照中,选中的项是第三项:

带样式的 ListView

EventTrigger 和 Storyboard

我们刚刚演示了 Trigger 根据某个属性的值来设置属性值或启动操作。另一种类型的触发器是 EventTrigger,它根据事件的引发来启动一组操作。例如,下面的 EventTrigger 对象指定当鼠标指针进入 ListBoxItem 时,MaxHeight 属性在 0.2 秒时间内以动画方式增大为值 90。当鼠标离开该项时,该属性在 1 秒时间内还原为原始值。请注意为何无需为 MouseLeave 动画指定 To 值。这是因为动画能够跟踪原始值。

<EventTrigger RoutedEvent="Mouse.MouseEnter">
  <EventTrigger.Actions>
    <BeginStoryboard>
      <Storyboard>
        <DoubleAnimation
          Duration="0:0:0.2"
          Storyboard.TargetProperty="MaxHeight"
          To="90"  />
      </Storyboard>
    </BeginStoryboard>
  </EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
  <EventTrigger.Actions>
    <BeginStoryboard>
      <Storyboard>
        <DoubleAnimation
          Duration="0:0:1"
          Storyboard.TargetProperty="MaxHeight"  />
      </Storyboard>
    </BeginStoryboard>
  </EventTrigger.Actions>
</EventTrigger>

有关更多信息,请参见演示图板概述

在下面的屏幕快照中,鼠标指向第三项:

样式示例屏幕快照

MultiTrigger、DataTrigger 和 MultiDataTrigger

除了 TriggerEventTrigger 之外,还有其他类型的触发器。通过 MultiTrigger,可以根据多个条件来设置属性值。如果条件的属性是经过数据绑定的,则可以使用 DataTriggerMultiDataTrigger

有关本概述中讨论的完整示例,请参见介绍样式设置和模板化的示例

共享的资源和主题

典型 Windows Presentation Foundation (WPF) 应用程序可能具有多个在整个应用程序范围内应用的用户界面 (UI) 资源。概括地说,这组资源可视为应用程序的主题。通过使用封装为 ResourceDictionary 类的资源字典,Windows Presentation Foundation (WPF) 支持将用户界面 (UI) 资源打包为主题。

Windows Presentation Foundation (WPF) 主题是使用样式设置和模板化机制定义的,Windows Presentation Foundation (WPF) 公开该机制,用于自定义任何元素的可视对象。

Windows Presentation Foundation (WPF) 主题资源存储在嵌入式资源字典中。这些资源字典必须嵌入到已签名的程序集中,它们既可以嵌入到代码自身所在的程序集中,也可以嵌入到并行程序集中。对于包含 Windows Presentation Foundation (WPF) 控件的程序集 PresentationFramework.dll,主题资源在一系列并行程序集中。

搜索元素样式时,主题是最后查找的位置。通常,搜索首先沿元素树向上查找相应资源,然后在应用程序资源集合中查找,最后查询系统。这为应用程序的作者提供了机会,让他们可以在到达主题之前,在树或应用程序级重新定义任何对象的样式。

您可以将各资源字典分别定义为单个文件,这样就可以在多个应用程序中重用某个主题。通过定义多个提供相同类型资源、但具有不同值的资源字典,也可以创建可交换的主题。在设计应用程序外观时,建议在应用程序级重新定义这些样式或其他资源。

若要在多个应用程序中共享一组资源(包括样式和模板),可以创建一个 XAML 文件并定义一个 ResourceDictionary。例如,请看下面的屏幕快照,它显示了使用 ControlTemplates 设置样式的示例的一部分:

控件模板示例

如果查看示例中的 XAML 文件,您会注意到所有文件都包含以下内容:

<ResourceDictionary.MergedDictionaries>
  <ResourceDictionary Source="Shared.xaml" />
</ResourceDictionary.MergedDictionaries>

这是共享的 shared.xaml,该文件定义一个 ResourceDictionary,该资源字典包含一组样式和画笔资源,使示例中的控件具有了一致的外观。

有关更多信息,请参见合并资源字典

如果要为自定义控件创建主题,请参见控件创作概述中的“外部控件库”一节。

请参见

任务

如何:查找由 ControlTemplate 生成的元素

如何:查找由 DataTemplate 生成的元素

照片存储区演示

概念

Windows Presentation Foundation 中的 Pack URI

其他资源

主题