65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF:入门指南 - 第 6 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (88投票s)

2008 年 3 月 11 日

CPOL

23分钟阅读

viewsIcon

339702

downloadIcon

5258

WPF 样式和模板介绍。

序言与致谢

我是一名 .NET 程序员,而且非常忙碌,我涉猎 VB .NET 和 C#、ASP.NET / WinForms / WPF / WCF Flash Silverlight 等所有技术。基本上,我都会尝试。但是当我开始撰写这个系列文章时,我自然选择了自己最喜欢的语言(碰巧是 C#)。后来我收到了一封邮件,有人要求我发布这个系列文章,并提供 VB.NET 和 C# 两种语言的源代码。我简单地表示我没有时间。所以这个人(Robert Ranck)主动提出帮助,根据我原来的 C# 项目将它们翻译成 VB.NET。

因此,对于这个以及你将在这里找到的后续 VB.NET 项目,我请你感谢 Robert Ranck。干杯,Robert,你的贡献一定会让这个系列对所有 .NET 开发者更加开放。

还要感谢 Karl Shifflett(又名博客/文章机器,也被称为 Molenator),他回答了我那些愚蠢的 VB.NET 问题。我还想提一下,Karl 刚刚开始了一个更高级的 WPF 系列文章(目前将以 VB.NET 编写,但希望也会出现 C# 版本)。Karl 的新系列会非常出色,我强烈建议大家鼓励 Karl 继续这个系列。用一种语言写完整个系列就已经不容易了,更何况是两种。Karl 的第一篇文章位于此处,他现在还发布了第 2 部分;大家可以自己去看看。我个人非常喜欢它们。

引言

本文是我的 WPF 初学者系列文章中的第六篇。在本文中,我们将讨论样式/模板。本系列文章的计划日程如下:

在本文中,我旨在简要介绍以下内容:

我将**不**涵盖样式/模板中动画的使用。Josh Smith 在这篇文章中很好地使用了样式中的动画。如果您想了解这方面的内容,MSDN 也有一篇不错的文章,就在这里

本文内容概要

如果您正在阅读本文,并且曾经尝试过创建所有者绘制的选项卡/自定义按钮(您知道,重写 `OnPaint()` 和 `OnPaintBackGround()`),那么您可能会知道,创建与标准控件外观不同的自定义控件是可行的,但乐趣不大。

我做过不少 WinForms 自定义/用户控件,我对那些方法重写和鼠标处理毫无兴趣,并且经常认为一定有更好的方法。

WPF 通过创建两个 UI 设计支柱来解决所有这些问题,一个称为**样式**,一个称为**模板**。本文涵盖了 WPF 环境中的这两个方面。

什么是样式

概述

简单来说,样式允许 WPF 开发者在一个方便的地方维护一个公共属性值列表,以存储所有这些属性值。这有点类似于 CSS 在 Web 开发中的工作方式。通常,样式将维护在资源部分或单独的资源字典中。WPF 也正是通过使用样式来迎合主题感知控件。在Chaz 的博客中有一篇关于如何做到这一点的优秀文章。

在这篇文章中,我不想过于深入地探讨如何创建主题,我只想涵盖基础知识,所以我将向你展示样式中可用的一些内容,但之后我将专注于你最常使用的样式主要区域。

对于 `Style`,以下属性可用:

名称 描述
BasedOn 获取或设置作为当前样式基础的已定义样式。
Dispatcher 获取此 `DispatcherObject` 所关联的 `Dispatcher`。(继承自 `DispatcherObject`。)
IsSealed 获取一个值,该值指示样式是否为只读且无法更改。
资源 获取或设置可在此样式范围内使用的资源集合。
TargetType 获取或设置此样式适用的类型。
Setters 获取 `Setter` 和 `EventSetter` 对象的集合。
触发器 获取 `TriggerBase` 对象的集合,这些对象根据指定条件应用属性值。

其中,最重要的属性是:

  • BasedOn
  • TargetType
  • Setters
  • 触发器

所以我认为值得快速了解一下这些语法片段。

BasedOn

这就像继承,一个 `Style` 从另一个 `Style` 继承公共属性。每个 `Style` 只支持一个 `BasedOn` 值。下面是一个小例子:

<Style x:Key="Style1">
...
</Style>

<Style x:Key="Style2" BasedOn="{StaticResource Style1}">
...
</Style>

TargetType

目标类型属性用于限制哪些控件可以使用特定样式。例如,如果我们有一个 `Style`,其 `TargetType` 属性设置为 `Button`,则此 `Style` 不能用于 `TextBox` 类型的控件。

设置一个有效的 `TargetType` 属性就像下面这样简单:

<Style TargetType="{x:Type Button}">
....
</Style>

Setters

`Setter` 们其实很简单。它们只是将事件或属性设置为某个值。在设置事件的情况下,它们会连接事件。在设置属性的情况下,它们会将属性设置为一个值。

事件的 `EventSetter` 可能如下所示,其中 `Style`d `Button` 的 `Click` 事件正在被连接。

<Style TargetType="{x:Type Button}">
    <EventSetter Event="Click" Handler="b1SetColor"/>
</Style>

然而,通常 `Setter` 仅用于将属性设置为一个值。可能像这样:

<Style TargetType="{x:Type Button}">
    <Setter Property="BackGround" Value="Yellow"/>
</Style>

属性元素语法

有时你可能不希望值是单一值,而是一大段复杂的 XAML,其中包含许多元素。为了实现这一点,XAML 允许开发者使用属性元素语法。在 `Style` 中最可能看到这种情况的地方是在 Template `Setter` 中。例如以下内容:

<!-- Tab Item Style -->
<Style x:Key="TabItemStyle1" TargetType="{x:Type TabItem}">
<Setter Property="FocusVisualStyle" Value="{StaticResource TabItemFocusVisual}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Padding" Value="6,1,6,1"/>
<Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}"/>
<Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="{x:Type TabItem}">
            <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">
                .....
                .....
                .....
            </Grid>
        </ControlTemplate>
    </Setter.Value>
</Setter>
</Style>

这里重要的部分是 `Setter` 通过使用属性值语法分成几行。

<Setter Property="Template">
      <Setter.Value>
       .....
       .....
       .....
    </Setter.Value>
</Setter>

触发器

WPF 样式和模板模型允许您在 `Style` 中指定 `Trigger`。本质上,`Trigger` 是允许您在满足特定条件(例如当某个属性值变为 true 或当事件发生时)时应用更改的对象。

以下示例展示了一个可用于 `Button` 控件的命名 `Style`。该 `Style` 定义了一个 `Trigger` 元素,当 `IsPressed` 属性为 true 时,该元素会改变 `Button` 的 `Foreground` 属性。

<Style x:Key="Triggers" TargetType="Button">
    <Style.Triggers>
        <Trigger Property="IsPressed" Value="true">
        <Setter Property = "Foreground" Value="Green"/>
        </Trigger>
    </Style.Triggers>
</Style>

在 `Style` 中还可以使用其他类型的 `Trigger`。

DataTriggers

表示一个 `Trigger`,当绑定的数据满足指定条件时,它会应用属性值或执行操作。

`DataTrigger` 的指定方式是,如果 Place 数据项的 State 为“WA”,则相应 `ListBoxItem` 的前景色设置为红色。

<Style TargetType="ListBoxItem">
    <Style.Triggers>
      <DataTrigger Binding="{Binding Path=State}" Value="WA">
        <Setter Property="Foreground" Value="Red" />
      </DataTrigger>    
    </Style.Triggers>
</Style>

还有一种特殊类型的 `Trigger`,它使用多个值进行条件测试。这被称为多触发器。而它所做的就是在一个 `MultiDataTrigger` 中使用多个条件。这里有一个例子:

<Style TargetType="ListBoxItem">
    <Style.Triggers>
      <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
          <Condition Binding="{Binding Path=Name}" Value="Portland" />
          <Condition Binding="{Binding Path=State}" Value="OR" />
        </MultiDataTrigger.Conditions>
        <Setter Property="Background" Value="Cyan" />
      </MultiDataTrigger>
    </Style.Triggers>
</Style>

在此示例中,绑定的对象必须具有 Name="Portland" 和 State="OR",然后相应 `ListBoxItem` 的前景色将设置为红色。

EventTriggers

是特殊的 `Trigger`,它表示一个 `Trigger`,用于响应事件应用一组动作。这些 `EventTrigger` 的奇特之处在于它们**只**允许触发动画。它们不允许根据普通属性进行设置,这是普通 `Trigger` 的作用。一个 `EventTrigger` 示例可能如下所示:

<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>

演示应用中的样式示例

附加的演示应用程序使用了相当多的 `Style`。通常,这些样式与 `Template` 的使用混合在一起,因此很难分离出单个示例。

这是一个针对 `TabItem` 的 `Style` 示例;这是一个完整的 `Style`,所以你可以看到这里面也有 `Template`,以及我们刚刚讨论过的东西(你知道的,`Setter` / `TargetType` 和属性元素语法...这个里面没有触发器)

<!-- Tab Item Style -->
<Style x:Key="TabItemStyle1" TargetType="{x:Type TabItem}">
    <Setter Property="FocusVisualStyle" 
       Value="{StaticResource TabItemFocusVisual}"/>
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="Padding" Value="6,1,6,1"/>
    <Setter Property="BorderBrush" 
       Value="{StaticResource TabControlNormalBorderBrush}"/>
    <Setter Property="Background" 
       Value="{StaticResource ButtonNormalBackground}"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding}"/>
                        <Button x:Name="btnClose" Margin="10,3,3,3" 
                            Template="{StaticResource closeButtonTemplate}" 
                            Background="{StaticResource buttonNormalBrush}" 
                            IsEnabled="True"/>
                </StackPanel>
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">
                    <Border x:Name="Bd" 
                            Background="{TemplateBinding Background}" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="1,1,1,0" 
                            CornerRadius="10,10,0,0" 
                            Padding="{TemplateBinding Padding}">
                        <ContentPresenter SnapsToDevicePixels=
                            "{TemplateBinding SnapsToDevicePixels}" 
                            HorizontalAlignment="{Binding 
                            Path=HorizontalContentAlignment, 
                            RelativeSource={RelativeSource 
                            AncestorType={x:Type ItemsControl}}}" 
                            x:Name="Content" VerticalAlignment="
                            {Binding Path=VerticalContentAlignment, 
                            RelativeSource={RelativeSource 
                            AncestorType={x:Type ItemsControl}}}" 
                            ContentSource="Header" 
                            RecognizesAccessKey="True"/>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="true">
                        <Setter Property="Background" TargetName="Bd" 
                            Value="{StaticResource TabItemHotBackground}"/>
                    </Trigger>
                    <Trigger Property="IsSelected" Value="true">
                        <Setter Property="Panel.ZIndex" Value="1"/>
                        <Setter Property="Background" TargetName="Bd" 
                            Value="{StaticResource TabItemSelectedBackground}"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="false"/>
                            <Condition Property="IsMouseOver" Value="true"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="BorderBrush" TargetName="Bd" 
                                Value="{StaticResource TabItemHotBorderBrush}"/>
                    </MultiTrigger>
                    <Trigger Property="TabStripPlacement" Value="Bottom">
                        <Setter Property="BorderThickness" 
                             TargetName="Bd" Value="1,0,1,1"/>
                    </Trigger>
                    <Trigger Property="TabStripPlacement" Value="Left">
                        <Setter Property="BorderThickness" 
                             TargetName="Bd" Value="1,1,0,1"/>
                    </Trigger>
                    <Trigger Property="TabStripPlacement" Value="Right">
                        <Setter Property="BorderThickness" 
                             TargetName="Bd" Value="0,1,1,1"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Top"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-2,-2,-1"/>
                        <Setter Property="Margin" 
                             TargetName="Content" Value="0,0,0,1"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Bottom"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-1,-2,-2"/>
                        <Setter Property="Margin" 
                             TargetName="Content" Value="0,1,0,0"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Left"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-2,-2,-1,-2"/>
                        <Setter Property="Margin" 
                             TargetName="Content" Value="0,0,1,0"/>
                    </MultiTrigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="TabStripPlacement" Value="Right"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Margin" Value="-1,-2,-2,-2"/>
                        <Setter Property="Margin" 
                            TargetName="Content" Value="1,0,0,0"/>
                    </MultiTrigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Background" TargetName="Bd" 
                                Value="{StaticResource TabItemDisabledBackground}"/>
                        <Setter Property="BorderBrush" TargetName="Bd" 
                                Value="{StaticResource TabItemDisabledBorderBrush}"/>
                        <Setter Property="Foreground" 
                                Value="{DynamicResource 
                            {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

这个 `Style` 及其关联的 `Template`(这里没有全部展示)足以将 `TabItem` 从标准视觉表示更改为带有圆角和关闭按钮的样式。顶部的选项卡是标准的未样式化选项卡,其他选项卡是我自己样式化的。

演示应用程序还包含 `Style` 和相关的 `Template`,用于创建全新的 `ScrollBar` 和 `ScrollViewer` 控件表示,如下图所示。`ScrollBar` 在左侧,`ScrollViewer` 在右侧。

为了实现这一点,需要相当多的 `Style` 和相关联的 `Template`。看看它需要多少 XAML 代码。我已删除了 `Template` 的核心部分,因为我还没有讨论它们的工作原理。我只是想向你展示完全重新 `Style` 一个标准控件需要什么。显然,有些比其他更复杂。例如,一个 `Button` 控件是微不足道的。

<!-- Brushses-->
<LinearGradientBrush x:Key="VerticalScrollBarBackground" 
                     EndPoint="1,0" StartPoint="0,0">
    <GradientStop Color="#E1E1E1" Offset="0"/>
    <GradientStop Color="#EDEDED" Offset="0.20"/>
    <GradientStop Color="#EDEDED" Offset="0.80"/>
    <GradientStop Color="#E3E3E3" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalScrollBarBackground" 
                     EndPoint="0,1" StartPoint="0,0">
    <GradientStop Color="#E1E1E1" Offset="0"/>
    <GradientStop Color="#EDEDED" Offset="0.20"/>
    <GradientStop Color="#EDEDED" Offset="0.80"/>
    <GradientStop Color="#E3E3E3" Offset="1"/>
</LinearGradientBrush>

<LinearGradientBrush x:Key="ListBoxBackgroundBrush"
StartPoint="0,0" EndPoint="1,0.001">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="White" Offset="0.0" />
            <GradientStop Color="White" Offset="0.6" />
            <GradientStop Color="#DDDDDD" Offset="1.2"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="StandardBrush"
StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#FFF" Offset="0.0"/>
            <GradientStop Color="#CCC" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBrush"
StartPoint="0,0" EndPoint="0,1">
    <GradientBrush.GradientStops>
        <GradientStopCollection>
            <GradientStop Color="#BBB" Offset="0.0"/>
            <GradientStop Color="#EEE" Offset="0.1"/>
            <GradientStop Color="#EEE" Offset="0.9"/>
            <GradientStop Color="#FFF" Offset="1.0"/>
        </GradientStopCollection>
    </GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="ScrollBarDisabledBackground" Color="#F4F4F4"/>
<SolidColorBrush x:Key="StandardBorderBrush" Color="#888" />
<SolidColorBrush x:Key="StandardBackgroundBrush" Color="#FFF" />
<SolidColorBrush x:Key="HoverBorderBrush" Color="#DDD" />
<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="Gray" />
<SolidColorBrush x:Key="SelectedForegroundBrush" Color="White" />
<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />
<SolidColorBrush x:Key="NormalBrush" Color="#888" />
<SolidColorBrush x:Key="NormalBorderBrush" Color="#888" />
<SolidColorBrush x:Key="HorizontalNormalBrush" Color="#888" />
<SolidColorBrush x:Key="HorizontalNormalBorderBrush" Color="#888" />
<SolidColorBrush x:Key="GlyphBrush" Color="#444" />

<!-- ScrollBarButton Vertical -->
<Style x:Key="VerticalScrollBarPageButton" TargetType="{x:Type RepeatButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- ScrollBarButton Horizontal -->
<Style x:Key="HorizontalScrollBarPageButton" TargetType="{x:Type RepeatButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                ...
                ...
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- Scroll Buttons Down-->
<Style x:Key="RepeatButtonStyleDown" TargetType="{x:Type RepeatButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                ...
                ...
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- Scroll Buttons Up-->
<Style x:Key="RepeatButtonStyleUp" TargetType="{x:Type RepeatButton}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- Scroll Thumb Style-->
<Style x:Key="ThumbStyle1" TargetType="{x:Type Thumb}">
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
                ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- ScrollBar  Style-->
<Style x:Key="ScrollBarStyle1" TargetType="{x:Type ScrollBar}">
    <Setter Property="Background" 
            Value="{StaticResource VerticalScrollBarBackground}"/>
    <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="false"/>
    <Setter Property="Foreground" 
            Value="{DynamicResource 
        {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="Width" 
            Value="{DynamicResource 
        {x:Static SystemParameters.VerticalScrollBarWidthKey}}"/>
    <Setter Property="MinWidth" 
            Value="{DynamicResource 
        {x:Static SystemParameters.VerticalScrollBarWidthKey}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ScrollBar}">
                ...
                ... 
              </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Orientation" Value="Horizontal">
            <Setter Property="Width" Value="Auto"/>
            <Setter Property="MinWidth" Value="0"/>
            <Setter Property="Height" Value="{DynamicResource 
                {x:Static SystemParameters.HorizontalScrollBarHeightKey}}"/>
            <Setter Property="MinHeight" 
                    Value="{DynamicResource 
                {x:Static SystemParameters.HorizontalScrollBarHeightKey}}"/>
            <Setter Property="Background" 
                    Value="{StaticResource HorizontalScrollBarBackground}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ScrollBar}">
                         ...
                        ...
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Trigger>
    </Style.Triggers>
</Style>

<!-- ScrollBar  Repeat Button-->
<Style x:Key="ScrollBarLineButton" TargetType="{x:Type RepeatButton}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                 ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- ScrollBar  Repeat Button-->
<Style x:Key="ScrollBarPageButton" TargetType="{x:Type RepeatButton}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type RepeatButton}">
                ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- Scroll Thumb Style-->
<Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
                ...
                ...
              </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- Vertical ScrollBar Template -->
<ControlTemplate x:Key="VerticalScrollBar" 
                       TargetType="{x:Type ScrollBar}">
                ...
                ... 
</ControlTemplate>

<!-- ScrollBar Style -->
<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Style.Triggers>
        <Trigger Property="Orientation" Value="Horizontal">
            <Setter Property="Width" Value="Auto"/>
            <Setter Property="Height" Value="18" />
            <Setter Property="Template"
        Value="{StaticResource HorizontalScrollBar}" />
        </Trigger>
        <Trigger Property="Orientation" Value="Vertical">
            <Setter Property="Width" Value="18"/>
            <Setter Property="Height" Value="Auto" />
            <Setter Property="Template"
        Value="{StaticResource VerticalScrollBar}" />
        </Trigger>
    </Style.Triggers>
</Style>

<!-- ScrollViewer Template -->
<ControlTemplate x:Key="ScrollViewerControlTemplate" 
                        TargetType="{x:Type ScrollViewer}">
        ...
        ...
</ControlTemplate>

现在,只有真正、真正疯狂的人(Josh/Karl,我们知道你在哪里)才会手动尝试这样做。我没有,也不会。我通常不喜欢工具,但有时你需要它们。手动做这种事情会让你发疯。最好的方法是启动 Expression Blend 并用它来修改你的 `Template`。你仍然需要了解 XAML,但 Expression Blend 肯定会在这个领域提供帮助。

当然,要做到这一点需要相当多的标记。但原理始终相同。在 `Style` 中,只有 `Style Setter` 和 `Trigger`,我们已经讨论过了,所以现在应该更清楚了。

演示应用程序组织

现在我想在我们继续讨论 `Template` 之前,先简单谈谈演示应用程序的组织结构。演示应用程序的组织方式如下:

  • VariousControlTemplatesWindow.xaml
  • 包含如何重新设置按钮/选项卡/滚动条样式的示例。这是您尚未见过的最后一张屏幕截图:

  • HierarchicalDataTemplateWindow.xaml
  • 包含如何重新设置层次结构数据样式的示例;以下是屏幕截图:

  • DemoLauncherWindow.xaml
  • 包含基于项目的数据样式示例。

  • Beatriz Costa 星球列表框/PlanetsListBoxWindow.xaml
  • 包含 `Style` 和 `Template` 巧妙使用的示例。这经 Beatriz Costa (Microsoft) 许可使用。我本可以自己做,但 Bea 做得非常出色,并且很好地说明了 `Style` 和 `Template` 的强大之处,所以我不得不在这里也使用它。所以感谢 Bea 的许可,我欠你一次。

别担心,当我们谈到 `Template` 时,我将更详细地讨论这些部分。我只是想让您知道演示应用程序实际上做了什么。

什么是模板

每个控件都有一个标准的观感……你能猜到那是什么吗?是的,没错,它是一个 `Template`。但它长什么样?在 Expression Blend 中,当您编辑控件的 `Template` 时,您会看到它,如下图所示,我正在编辑一个 `ScrollViewer` 控件。

现在,如果你看一下这个,再回过头看看我之前包含的那大段 XAML 代码,可能会变得更清楚一些。我们可以看到,一个 `ScrollViewer` 控件实际上是由几个元素组成的。这构成了它的 VisualTree。现在,我们要改变它,要么改变这棵树的一部分,要么完全替换它。信不信由你,我们几乎可以替换掉一个控件原本应该看起来的所有东西,只要遵守一些规则,但稍后会详细介绍。我只是想让你能够理解为什么创建控件的 `Style` 或 `Template` 需要所有这些 XAML。

前段时间,我在我的博客上对 `ScrollViewer` 控件做了一个相当彻底的讲解(我们**总能**做得更好);你可以在这里查看;另外值得注意的是 标准 ControlTemplate 示例 MSDN 链接,你可以在那里看到每个默认 WPF 控件的外观。

概述

正如我刚才提到的,我们可以使用 `Template` 来改变控件的外观。你经常会在 `Style` 中看到 `Template` 的使用,但这没关系,我们已经看过这样的例子,它并没有吓到我们。事实上,我们说,来吧,`Template`,那些 `Style` 真是太简单了!

要精确地确定 `Template` 中会包含什么并不那么容易,因为它可能取决于我们正在处理的 `Template` 类型;市面上有相当多的不同 `Template`。

我想我会介绍一些基本语法,然后讨论一些你可能会遇到的不同 `Template`。然后,我将解释演示应用程序的 `Template`。

如果我提到 `DataTempate` / `HierarchicalDataTemplate` 之类的东西,而你一无所知,也别太担心。我会在 `Template` 的基本语法之后涵盖这些内容。在完全迷失在 WPF 之前,你得先学会爬行,然后打包所有东西,剃掉头发,成为一名僧侣。至少他们有酒喝。好了,我走了。

触发器

`Style`、`ControlTemplate` 和 `DataTemplate` 都具有可以包含一组触发器的 `Triggers` 属性。当属性值更改或事件引发时,触发器会设置属性或启动动画等操作。我们已经在 `Style` 中看到了触发器,但 `Template` 呢?嗯,在 `Template` 中,它们几乎相同。触发器有几种不同的类型。我们来看看吧,好吗?

Property Triggers

用于在特定条件发生时将属性设置为某个值。此示例在 `Button.IsEnabled` 属性为 false 时将 `Border.opacity` 属性设置为 0.4。

<!-- Simple Button with some Mouse events/Property Hi-Jacking-->
<ControlTemplate 
         x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" 
         TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" 
            Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding Foreground}" 
            BorderThickness="2" Width="auto" 
            Visibility="Visible">
        ....
        ....
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter TargetName="border" Property="Opacity" Value="0.4"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Event Triggers

它们的工作方式与 `Style` 的工作方式完全相同,我们上面已经概述过了。

MultiTriggers、DataTriggers 和 MultiDataTriggers

除了 `Trigger` 和 `EventTrigger`,还有其他类型的触发器。`MultiTrigger` 允许您根据多个条件设置属性值。当您的条件属性是数据绑定时,您可以使用 `DataTrigger` 和 `MultiDataTrigger`。

<!-- Listbox DemoListItem Type Template -->
<DataTemplate x:Key="demoItemTemplate" DataType="x:Type local:DemoListItem">
    <StackPanel Orientation="Horizontal" Margin="10">
        <Path Name="pathSelected" Fill="White" 
            Stretch="Fill" Stroke="White" Width="15" 
            Height="20" Data="M0,0 L 0,15 L 7.5,7.5" 
            Visibility="Hidden"/>
        <Border BorderBrush="White" 
                  BorderThickness="4" Margin="5">
            <Image Source="Images/DataLogo.png" Width="45" Height="45"/>
        </Border>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center">
            <TextBlock FontFamily="Arial Black" FontSize="20" 
                   FontWeight="Bold"
                   Width="auto" Height="auto"
                   Text="{Binding Path=DemoName}"    />
            <TextBlock FontFamily="Arial" FontSize="10" 
                   FontWeight="Normal"
                   Width="auto" Height="auto"
                   Text="{Binding Path=WindowName}" />  
        </StackPanel>
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger 
          Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                    AncestorType={x:Type ListBoxItem}, AncestorLevel=1}, 
                    Path=IsSelected}" 
          Value="True">
            <Setter TargetName="pathSelected" 
                       Property="Visibility" Value="Visible"  />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>

由于这是一个 `DataTemplate`,它是用于某些绑定数据的模板,因此我们可以在这里使用 `DataTrigger`。因此,这个 `DataTrigger` 检查绑定的对象的 `IsSelected` 属性是否为 true,如果是,则设置 `DataTemplate` 中另一个元素的 `Visibility`。

这是一个 `MultiTrigger` 的例子,它基本上在触发器运行之前,在条件评估中使用了不止一个属性。

<ControlTemplate TargetType="{x:Type TabItem}">
    <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">
        <Border x:Name="Bd" Background="{TemplateBinding Background}" 
                BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1,1,1,0" 
                CornerRadius="10,10,0,0" Padding="{TemplateBinding Padding}">
            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                HorizontalAlignment="{Binding Path=HorizontalContentAlignment, 
                RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" 
                x:Name="Content" 
                VerticalAlignment="{Binding Path=VerticalContentAlignment, 
                RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" 
                ContentSource="Header" RecognizesAccessKey="True"/>
        </Border>
    </Grid>
    <ControlTemplate.Triggers>
            <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsSelected" Value="false"/>
                <Condition Property="IsMouseOver" Value="true"/>
            </MultiTrigger.Conditions>
            <Setter Property="BorderBrush" TargetName="Bd" 
                    Value="Black"/>
        </MultiTrigger>
        </ControlTemplate.Triggers>
</ControlTemplate>

在此示例中,`TabItem` 必须将其 `IsSelected` 属性设为 false,并且其 IsMous`e`Over 为 true,触发器才会运行并将名为 `Bd` 的元素的 `BorderBrush` 设置为 Black Brush。

TemplateBinding

现在我们已经了解了数据绑定(还记得第 5 部分吗?),我们知道如何将事物相互绑定。`TemplateBinding` 只是一种不同的绑定方式,它将控件模板中属性的值链接到模板化控件上其他公开属性的值。

看看 MSDN 页面,它非常简单。我们想要做的是确保我们的控件能够响应用户的需求。例如,如果用户将控件的 `BackGround` 设置为蓝色,而我们提供了一个将 `BackGround` 设置为绿色的控件 `Template`,那不是用户想要的。肯定有更好的方法。是的,有,我们只需使用 `{TemplateBinding }` 标记扩展来告诉控件 `Template` 从模板化父控件获取其值。例如:

<!-- Simple Button with some Mouse events/Property Hi-Jacking-->
<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" 
                 TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" 
          Background="{TemplateBinding Background}" 
        .....
    </Border>
    <ControlTemplate.Triggers>
    ....
    ....
    </ControlTemplate.Triggers>
</ControlTemplate>

重要的部分是 `Background="{TemplateBinding Background}"`;这足以确保控件 `Template` 使用与模板化父控件相同的值。

劫持属性

有时您可能会遇到这样的情况:您想从正在模板化的原始源控件中使用多个属性,但是您尝试使用的项目没有相应的属性。例如,假设您想创建一个包含文本和图像的 Button `Template`。文本很简单,您可以使用 `Button.Content` 属性,但如果它用于文本,我们还能从哪里获取图像的值呢?当然,我们可以使用我之前关于依赖属性的文章中描述的附加属性,但我们也可以寻找任何未使用的属性并劫持它们以用于控件 `Template`。例如,所有控件的 `Tag` 属性都接受 `object` 作为值,这使得它非常方便。

然后我们可以在绑定中使用它;例如,看看这个:

<!-- Simple Button with some Mouse events/Property Hi-Jacking-->
<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" 
                 TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding Foreground}" 
            BorderThickness="2" Width="auto" Visibility="Visible">
        <StackPanel Orientation="Horizontal">
            <Image Source="{Binding RelativeSource={RelativeSource TemplatedParent},
                 Path=Tag}" Width="20" Height="20" HorizontalAlignment="Left"
                 Margin="{TemplateBinding Padding}" />
            <ContentPresenter  
                Margin="{TemplateBinding Padding}" 
                Content="{TemplateBinding Content}" 
                Width="auto" Height="auto"/>
        </StackPanel>
    </Border>
    <ControlTemplate.Triggers>
    ....
    ....
    </ControlTemplate.Triggers>
</ControlTemplate>

请注意,我实际上同时使用了原始控件(`Button`)的内容作为文本(它将显示在 `ContentPresenter` 中,`ContentPresenter` 能够显示任何单个内容,在本例中是原始 `Button.Context` 文本)以及原始控件(`Button`)的 `Tag` 作为 `Template` 的图像。

如果您有兴趣了解如何在 `Template` 中使用附加属性而不是劫持未使用的属性,请查看 Josh Smith 这篇出色的博客文章

模板示例

好的,我们做得很好,再多一点点。我只想描述几种不同的 `Template` 类型,最后我还会介绍一个相当酷的例子。

ControlTemplate

`ControlTemplate` 是最常见的 `Template` 类型,它通过指定控件的视觉结构和行为方面来控制控件的呈现方式和行为。

这是一个非常简单的例子,它只是用一个 `Ellipse` 和一个 `ContentPresenter` 来呈现 `Button.Content`,替换了标准 `Button` 控件的 `Template`。

<ControlTemplate TargetType="Button">
    <Grid>
        <Ellipse Fill="{TemplateBinding Background}"/>
        <ContentPresenter HorizontalAlignment="Center"
                          VerticalAlignment="Center"/>
    </Grid>
</ControlTemplate>

这里还有另外两个(同样是针对 `Button` 的)例子,它们使用了我们讨论过的大部分内容:属性触发器、属性劫持(我的术语,非官方术语,所以不要去查)、以及模板绑定。

<!-- Simple Button with simple properties-->
<ControlTemplate x:Key="bordereredButtonTemplate" TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" Background="Transparent" 
            BorderBrush="{TemplateBinding Foreground}" BorderThickness="2" 
            Width="auto" Visibility="Visible">
        <ContentPresenter  Margin="3" 
            Content="{TemplateBinding Content}" 
            Width="auto" Height="auto"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter TargetName="border" 
                Property="Opacity" Value="0.4"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<!-- Simple Button with some Mouse events-->
<ControlTemplate x:Key="bordereredButtonTemplateWithMouseEvents" 
                             TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" 
            Background="Transparent" 
            BorderBrush="{TemplateBinding Foreground}" 
            BorderThickness="2" Width="auto" 
            Visibility="Visible">
        <ContentPresenter  Margin="3" 
            Content="{TemplateBinding Content}" 
            Width="auto" Height="auto"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter TargetName="border" 
                  Property="Opacity" Value="0.4"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter TargetName="border" 
                 Property="Background" Value="Orange"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>


<!-- Simple Button with some Mouse events/Property Hi-Jacking-->
<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" 
            TargetType="{x:Type Button}">
    <Border x:Name="border" CornerRadius="3" 
            Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding Foreground}" 
            BorderThickness="2" Width="auto" 
            Visibility="Visible">
        <StackPanel Orientation="Horizontal">
            <Image Source="{Binding RelativeSource={RelativeSource TemplatedParent},
                 Path=Tag}" Width="20" 
                 Height="20" HorizontalAlignment="Left"
                 Margin="{TemplateBinding Padding}" />
            <ContentPresenter  
                Margin="{TemplateBinding Padding}" 
                Content="{TemplateBinding Content}" 
                Width="auto" Height="auto"/>
        </StackPanel>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter TargetName="border" 
                Property="Opacity" Value="0.4"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter TargetName="border" 
                Property="Background" Value="Orange"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

这些控件模板可在演示应用程序的 *VariousControlTemplatesWindow.xaml* 文件中找到,并如下所示:

你可以在这里阅读更多关于 `ControlTemplate` 的信息。

数据模板

您可以使用 `DataTemplate` 来指定数据对象的可视化。`DataTemplate` 对象在您将 `ItemsControl`(例如 `ListBox`)绑定到整个集合时特别有用。如果没有特定说明,`ListBox` 会显示集合中对象的字符串表示。在这种情况下,您可以使用 `DataTemplate` 来定义数据对象的外观。`DataTemplate` 的内容成为数据对象的视觉结构。

在演示应用程序的 *DemoLauncherWindow.xaml* 文件中,我向 `ListBox` 添加了许多 `DemoListItem` 类型的自定义对象。

`DemoListItem` 对象看起来像这样:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;

namespace Styles_And_Templates
{
    /// <summary>
    /// Is used within the <see cref="DemoLauncherWindow">
    /// demo launcher window</see> as individual listBox
    /// items
    /// </summary>
    public class DemoListItem
    {
        #region Public Properties
        public string WindowName { get; set; }
        public string DemoName { get; set; }
        #endregion
    }
}

以及 VB.NET

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Text
Imports System.Windows

''' <summary> 
''' Is used within the <see cref="DemoLauncherWindow"> 
''' demo launcher window</see> as individual listBox 
''' items 
''' </summary> 
Public Class DemoListItem
#Region "Public Properties"
    Private m_WindowName As String
    Public Property WindowName() As String
        Get
            Return m_WindowName
        End Get
        Set(ByVal value As String)
            m_WindowName = value
        End Set

    End Property

    Private m_DemoName As String
    Public Property DemoName() As String
        Get
            Return m_DemoName
        End Get
        Set(ByVal value As String)
            m_DemoName = value
        End Set
    End Property
#End Region
End Class

有了这些知识,我们可以在承载这些对象的 Window 中创建一个 `DataTemplate`。我们来看看吧:

<!-- Listbox DemoListItem Type Template -->
<DataTemplate x:Key="demoItemTemplate" DataType="x:Type local:DemoListItem">
    <StackPanel Orientation="Horizontal" Margin="10">
        <Path Name="pathSelected" Fill="White" 
            Stretch="Fill" Stroke="White" Width="15" 
            Height="20" Data="M0,0 L 0,15 L 7.5,7.5" 
            Visibility="Hidden"/>
        <Border BorderBrush="White" BorderThickness="4" Margin="5">
            <Image Source="Images/DataLogo.png" Width="45" Height="45"/>
        </Border>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center">
            <TextBlock FontFamily="Arial Black" FontSize="20" 
                   FontWeight="Bold"
                   Width="auto" Height="auto"
                   Text="{Binding Path=DemoName}"    />
            <TextBlock FontFamily="Arial" FontSize="10" 
                   FontWeight="Normal"
                   Width="auto" Height="auto"
                   Text="{Binding Path=WindowName}" />  
        </StackPanel>
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger 
          Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                  AncestorType={x:Type ListBoxItem}, AncestorLevel=1}, Path=IsSelected}" 
          Value="True">
            <Setter TargetName="pathSelected" 
                 Property="Visibility" Value="Visible"  />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

您会注意到,这利用了 `{Binding }` 标记扩展,以便在 `DataTemplate` 中选择底层绑定数据对象的属性。另请注意,由于这是绑定数据对象的 `Template`,我们需要使用 `DataTrigger`。

这将导致 `ListBox` 项目如下所示:

您可以在这里找到更多关于数据模板化的概述。

HierarchicalDataTemplate

`HierarchicalDataTemplate` 实际上只是一个 `DataTemplate`,它可以用在 `TreeView` 或 `Menu` 等分层结构上。

我从 MSDN 文档中盗用了演示应用程序 *HierarchicalDataTemplateWindow.xaml* 文件中的示例。基本思想是,你有一个层次列表,你将其用作 `TreeView` 和 `Menu` 的数据源。然后你应用一些 `HierarchicalDataTemplate`,使绑定的层次列表数据正确呈现。

这是 C# 中的源列表:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Collections.Generic;

namespace Styles_And_Templates
{
    #region Inner data classes

    public class League
    {
        public string Name { get; private set; }
        public List<Division> Divisions { get; private set; }

        public League(string name)
        {
            Name = name;
            Divisions = new List<Division>();
        }
    }
    
    public class Division
    {

        public string Name { get; private set; }
        public List<Team> Teams { get; private set; }

        public Division(string name)
        {
            Name = name;
            Teams = new List<Team>();

        }
    }

    public class Team
    {
        public string Name { get; private set; }

        public Team(string name)
        {
            Name = name;
        }
    }
    #endregion

    #region LeagueList
    
    /// <summary>
    /// Provides a simple LeagueList, holding dummy
    /// data to demonstrate binding to Hierarchical
    /// data structures
    /// </summary>
    public class LeagueList : List<League>
    {
        public LeagueList()
        {
            League l;
            Division d;

            Add(l = new League("League A"));
            l.Divisions.Add((d = new Division("Division A")));
            d.Teams.Add(new Team("Team I"));
            d.Teams.Add(new Team("Team II"));
            d.Teams.Add(new Team("Team III"));
            d.Teams.Add(new Team("Team IV"));
            d.Teams.Add(new Team("Team V"));
            l.Divisions.Add((d = new Division("Division B")));
            d.Teams.Add(new Team("Team Blue"));
            d.Teams.Add(new Team("Team Red"));
            d.Teams.Add(new Team("Team Yellow"));
            d.Teams.Add(new Team("Team Green"));
            d.Teams.Add(new Team("Team Orange"));
            l.Divisions.Add((d = new Division("Division C")));
            d.Teams.Add(new Team("Team East"));
            d.Teams.Add(new Team("Team West"));
            d.Teams.Add(new Team("Team North"));
            d.Teams.Add(new Team("Team South"));
            Add(l = new League("League B"));
            l.Divisions.Add((d = new Division("Division A")));
            d.Teams.Add(new Team("Team 1"));
            d.Teams.Add(new Team("Team 2"));
            d.Teams.Add(new Team("Team 3"));
            d.Teams.Add(new Team("Team 4"));
            d.Teams.Add(new Team("Team 5"));
            l.Divisions.Add((d = new Division("Division B")));
            d.Teams.Add(new Team("Team Diamond"));
            d.Teams.Add(new Team("Team Heart"));
            d.Teams.Add(new Team("Team Club"));
            d.Teams.Add(new Team("Team Spade"));
            l.Divisions.Add((d = new Division("Division C")));
            d.Teams.Add(new Team("Team Alpha"));
            d.Teams.Add(new Team("Team Beta"));
            d.Teams.Add(new Team("Team Gamma"));
            d.Teams.Add(new Team("Team Delta"));
            d.Teams.Add(new Team("Team Epsilon"));
        }

        public League this[string name]
        {
            get
            {
                foreach (League l in this)
                    if (l.Name == name)
                        return l;

                return null;
            }
        }
    }
    #endregion
}

以及 VB.NET

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Documents
Imports System.Windows.Media
Imports System.Windows.Shapes
Imports System.Collections.ObjectModel
Imports System.Collections.Generic

#Region "Inner data classes"
Public Class League
    Private m_Name As String
    Public Property Name() As String
        Get
            Return m_Name
        End Get
        Private Set(ByVal value As String)
            m_Name = value
        End Set
    End Property

    Private m_Divisions As List(Of Division)
    Public Property Divisions() As List(Of Division)
        Get
            Return m_Divisions
        End Get
        Private Set(ByVal value As List(Of Division))
            m_Divisions = value
        End Set
    End Property

    Public Sub New(ByVal newname As String)
        Name = newname
        Divisions = New List(Of Division)()
    End Sub
End Class

Public Class Division
    Private m_Name As String
    Public Property Name() As String
        Get
            Return m_Name
        End Get
        Private Set(ByVal value As String)
            m_Name = value
        End Set
    End Property
    Private m_Teams As List(Of Team)
    Public Property Teams() As List(Of Team)
        Get
            Return m_Teams
        End Get
        Private Set(ByVal value As List(Of Team))
            m_Teams = value
        End Set
    End Property

    Public Sub New(ByVal newname As String)
        Name = newname
        Teams = New List(Of Team)()
    End Sub
End Class

Public Class Team
    Private m_Name As String
    Public Property Name() As String
        Get
            Return m_Name
        End Get
        Private Set(ByVal value As String)
            m_Name = value
        End Set
    End Property

    Public Sub New(ByVal newname As String)
        Name = newname
    End Sub
End Class
#End Region

#Region "LeagueList"

''' <summary> 
''' Provides a simple LeagueList, holding dummy 
''' data to demonstrate binding to Hierarchical 
''' data structures 
''' </summary> 
Public Class LeagueList
    Inherits List(Of League)
    Public Sub New()
        Dim l As League
        Dim d As Division
        l = New League("League A")
        Add(l)
        d = New Division("Division A")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team I"))
        d.Teams.Add(New Team("Team II"))
        d.Teams.Add(New Team("Team III"))
        d.Teams.Add(New Team("Team IV"))
        d.Teams.Add(New Team("Team V"))
        d = New Division("Division B")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team Blue"))
        d.Teams.Add(New Team("Team Red"))
        d.Teams.Add(New Team("Team Yellow"))
        d.Teams.Add(New Team("Team Green"))
        d.Teams.Add(New Team("Team Orange"))
        d = New Division("Division C")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team East"))
        d.Teams.Add(New Team("Team West"))
        d.Teams.Add(New Team("Team North"))
        d.Teams.Add(New Team("Team South"))
        l = New League("League B")
        Add(l)
        d = New Division("Division A")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team 1"))
        d.Teams.Add(New Team("Team 2"))
        d.Teams.Add(New Team("Team 3"))
        d.Teams.Add(New Team("Team 4"))
        d.Teams.Add(New Team("Team 5"))
        d = New Division("Division B")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team Diamond"))
        d.Teams.Add(New Team("Team Heart"))
        d.Teams.Add(New Team("Team Club"))
        d.Teams.Add(New Team("Team Spade"))
        d = New Division("Division C")
        l.Divisions.Add(d)
        d.Teams.Add(New Team("Team Alpha"))
        d.Teams.Add(New Team("Team Beta"))
        d.Teams.Add(New Team("Team Gamma"))
        d.Teams.Add(New Team("Team Delta"))
        d.Teams.Add(New Team("Team Epsilon"))
    End Sub

    Default Public Overloads ReadOnly Property Item(ByVal name As String) As League
        Get
            For Each l As League In Me
                If l.Name = name Then
                    Return l
                End If
            Next

            Return Nothing
        End Get
    End Property
End Class
#End Region

这是 `HierarchicalDataTemplate`:

<!-- League matched template-->
<HierarchicalDataTemplate DataType="{x:Type local:League}"
                    ItemsSource = "{Binding Path=Divisions}">
    <TextBlock Text="{Binding Path=Name}" Background="Red"/>
</HierarchicalDataTemplate>

<!-- Division matched template-->
<HierarchicalDataTemplate DataType="{x:Type local:Division}"
                    ItemsSource = "{Binding Path=Teams}">
    <TextBlock Text="{Binding Path=Name}" Background="Green"/>
</HierarchicalDataTemplate>

<!-- Division matched Team-->
<DataTemplate DataType="{x:Type local:Team}">
    <TextBlock Text="{Binding Path=Name}" 
               Background="CornflowerBlue"/>
</DataTemplate>

这会产生类似这样的结果,其中每个底层绑定数据对象都有自己的 `Template`:

您可以在这里阅读更多相关信息,如果您想深入了解 `HierarchicalDataTemplate` 的强大功能,请查看 Codeproject MVP Karl Shifflett 的优秀博客文章这里,其中 Karl 使用 `HierarchicalDataTemplate` 创建了一个简单的资源管理器类型树视图(这实际上是对 Josh 对我最初的简单资源管理器类型树视图文章的反应的反应)。

一个小小的演示讨论(因为它很酷)

很长一段时间以来,我一直在阅读关于 WPF 的博客,而在数据绑定方面,没有人比微软的 Beatriz Costa 更出色。她真是太棒了。她的一些示例给我留下了深刻的印象,我特别被其中一个样本所震撼,它让我真正体会到了 WPF 中绑定和模板的强大之处。

她的原始帖子是 WPF 中样式和模板的力量;我问 Bea 是否可以在本文中使用它。她欣然同意(她告诉我我得请她喝一杯啤酒……对我来说听起来不错)。所以谢谢 Bea。

首先是截图。

现在,如果我告诉你这是一个 ListBox,你会怎么说?很酷吧?要了解它是如何工作的,让我们稍作剖析。

我不会在这里包含所有代码;您可以查看我的代码,或者直接访问源并查看 Bea 的博客。

但我只想向你展示你可以用一个巧妙的 `Style` 和一两个 `Template` 做些什么。让我们看看重要的部分,XAML:

<Window x:Class="PlanetsListBox.PlanetsListBoxWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:local="clr-namespace:PlanetsListBox" 
       Title="PlanetsListBox" Height="700" Width="700"
    >
    <Window.Resources>
        <local:SolarSystem x:Key="solarSystem" />
        <local:ConvertOrbit x:Key="convertOrbit" />

        <DataTemplate DataType="{x:Type local:SolarSystemObject}">
            <Canvas Width="20" Height="20" >
                <Ellipse 
                    Canvas.Left="{Binding Path=Orbit, 
                        Converter={StaticResource convertOrbit}, 
                        ConverterParameter=-1.707}" 
                    Canvas.Top="{Binding Path=Orbit, 
                        Converter={StaticResource convertOrbit}, 
                        ConverterParameter=-0.293}" 
                    Width="{Binding Path=Orbit, 
                        Converter={StaticResource convertOrbit}, 
                        ConverterParameter=2}" 
                    Height="{Binding Path=Orbit, 
                        Converter={StaticResource convertOrbit}, 
                        ConverterParameter=2}" 
                    Stroke="White" 
                    StrokeThickness="1"/>
                <Image Source="{Binding Path=Image}" 
                               Width="20" Height="20">
                    <Image.ToolTip>
                        <StackPanel Width="250" 
                                TextBlock.FontSize="12">
                            <TextBlock FontWeight="Bold" 
                                    Text="{Binding Path=Name}" />
                            <StackPanel Orientation="Horizontal">
                                <TextBlock Text="Orbit: " />
                                <TextBlock Text="{Binding Path=Orbit}" />
                                <TextBlock Text=" AU" />
                            </StackPanel>
                            <TextBlock Text="{Binding Path=Details}" 
                                TextWrapping="Wrap"/>
                        </StackPanel>
                    </Image.ToolTip>
                </Image>
            </Canvas>
        </DataTemplate>

        <Style TargetType="ListBoxItem">
            <Setter Property="Canvas.Left" 
              Value="{Binding Path=Orbit, Converter={StaticResource 
                     convertOrbit}, ConverterParameter=0.707}"/>
            <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, 
                    Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Grid>
                            <Ellipse x:Name="selectedPlanet" 
                                Margin="-10" StrokeThickness="2"/>
                            <ContentPresenter 
                                SnapsToDevicePixels=
                                  "{TemplateBinding SnapsToDevicePixels}" 
                                HorizontalAlignment=
                                  "{TemplateBinding HorizontalContentAlignment}" 
                                VerticalAlignment=
                                  "{TemplateBinding VerticalContentAlignment}"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter Property="Stroke" 
                                  TargetName="selectedPlanet" 
                                  Value="Yellow"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="ListBox">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas Width="590" Height="590" 
                                   Background="Black" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid HorizontalAlignment="Center" 
           VerticalAlignment="Center" ClipToBounds="True">
        <ListBox ItemsSource="{Binding Source={StaticResource solarSystem}, 
            Path=SolarSystemObjects}" Focusable="False" />
    </Grid>
</Window>

这里面的内容,要么是我们在本文中讨论过的,要么是在与本文配套的其他五篇文章中讨论过的。我认为这非常令人印象深刻。

干得好,Bea。谢谢。

无外观控件

现在你已经差不多读完了这篇文章,我想向你提出一个问题。我们现在知道,我们可以使用 `Style` 和 `Template` 完全改变控件的外观。我们甚至可以想象这样一种情况:控件设计者创建了一个控件,比如说一个图片选择器,它只有一个按钮,但我们可以随心所欲地对这个控件进行 `Style` 和 `Template`。这很酷,但不能保证设计 `Style` 和 `Template` 的人知道控件的功能和工作原理。微软对此的看法是,这是两个角色,设计师和开发者。他们都不应该关心对方做什么。

他们应该能够编写可正常工作的控件,设计师可以根据自己的喜好重新设计样式。这就是所谓的无外观控件。

嗯,这很好,但在实践中,当然,两者都需要对彼此的工作有所了解。

因此,为了让开发者帮助设计师,开发者至少可以公开某些元数据,以表达开发者对控件的意图。设计师当然可以忽略这一点,但样式化和模板化的控件可能无法正常工作。但理论上,如果设计师公平行事并遵循以下建议,控件应该会正常工作(前提是开发者知道自己在做什么并实际完成了他们的工作)。

所以想象一下这个。

开发人员创建一个简单的控件,例如:

我们有一个图像和一个按钮来分配新图像。现在设计师对 WPF 和 Expression Blend 有一些了解,所以他决定控件应该看起来像这样:

现在从 `Style` 和 `Template` 的角度来看,这并没有什么问题。请记住,我们可以为控件创建 `Template` 并使其看起来像我们喜欢的那样。所以这没问题。**不**对的是,控件将不再按照开发人员编码的方式工作。

为什么不呢……嗯,没有按钮,那么如何选择新图片呢?答案在于代码;开发人员代码和设计人员代码都扮演着各自的角色。

推荐的程序如下:

首先,开发人员必须通过使用一些属性(你可以根据需要添加任意数量的属性)来表达他们对控件的意图,其中 `TemplatePartAttribute` 告诉设计人员在控件的 `Template` 中需要包含什么。开发人员**必须**做的另一件事是,要么重写 `OnApplyTemplate()` 并查找他们期望设计人员提供的 Template **部分**,并在后台代码中连接这些事件,要么使用路由命令。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace UntitledProject1
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    /// 

    [TemplatePart(Name="PART_PickNew",Type=typeof(Button))]
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            Button PART_PickNew = 
              this.Template.FindName("PART_PickNew") as Button;
            if (PART_PickNew != null)
            {
                PART_PickNew.Click += new RoutedEventHandler(PART_PickNew_Click);
            }
        }

        void PART_PickNew_Click(object sender, RoutedEventArgs e)
        {
            //do the stuff here that handles the PART_PickNew logic
        }
    }
}

所以从开发者的角度来看,一切都已考虑周全。接下来是设计师的部分。只要他们确实使用了 `Button` 并给它一个与开发者编码所用的名称匹配的名称(线索在 `TemplatePartAttribute` 中),一切都应该正常,并且按预期工作。这里有一个很好的例子:

<Image Source="C:\Users\sacha\Pictures\BLACK_OR_WHITE.jpg" 
    Stretch="UniformToFill" Width="50" Height="50"/>
<Button x:Name="PART_PickNew" Content="Pick New Image"/>
ALWAYS 

当 `Template` 应用时,代码隐藏可以愉快地链接到 `Button` 的事件,即使控件外观完全不同,它也能正常工作,这要归功于应用了一个很酷的 `Template`。太棒了。

这就是无外观控件的全部意义所在。赋予设计师按照他们想要的方式设计外观的能力,但始终确保它仍然按预期工作。

这也可以通过路由 UI 命令来完成;事实上,我过去曾专门写过一篇关于这个概念的完整文章,如果您感兴趣,可以在这里找到该文章。

等等,还会有一篇文章!!!

你们当中眼尖的人可能已经注意到,这实际上是我建议的初学者系列中的最后一篇文章。

但是……

在这之后还会有一篇文章,这将是本系列的最后一集。这篇文章将涵盖我们在整个初学者系列中学习到的所有内容。我已经完成了代码,并且非常非常自豪,迫不及待地想与大家分享。但现在,我们必须专注于手头的任务,学习这部分内容。

我还计划在这最后一篇之后再写一篇 WPF 文章。所以总共有两篇;然后我真的必须转到其他内容了。模式/线程/WF/PLINQ/动态查询/Entity Framework,谁知道呢。拭目以待。

参考资料

  1. Beatriz “绑定女王” Costa:博客
  2. Bea Costa 的:WPF 中样式和模板的力量(`PLanetListBox`),经她许可使用。
  3. 干杯 Bea,我找个时间请你喝杯啤酒。

其他好资源

  1. Josh Smith:WPF 指导之旅 - 第 4 部分(数据模板和触发器)
  2. Josh Smith:WPF 指导之旅 - 第 5 部分(样式)
  3. Chaz:WPF 中的主题
  4. MSDN:Style 类
  5. MSDN:样式和模板
  6. MSDN:如何:在数据更改时触发动画

历史记录

  • 08/03/09:首次发布。
© . All rights reserved.