WPF进阶技巧和实战06-控件模板

系列文章链接

逻辑树和可视化树

System.Windows.LogicalTreeHelper

System.Windows.Media.VisualTreeHelper

逻辑树类(LogicalTreeHelper)的方法

名称 说明
FindLogicalNode 根据名称查找特定元素,从指定的元素开始并向下查找逻辑树
BringIntoView 如果元素在可滚动的容器中,并且当前不可见,就将元素滚动到视图中
GetParent 获取指定元素的父元素
GetChildren 获取指定元素的子元素

理解模板

每个控件都有一个内置的方法,用于确定如何渲染控件,该方法称为控件模板,是用XAML标记块来定义。WPF实际有3种模板:

ControlTemplate:控件的显示外衣,可以定制

ItemsPanelTemplate:集合控件的子项排列方式,可以定制

DataTemplate:控件中,数据的展示形式,可以定制,一般用于集合控件子项展示数据的形式

数据模板用于从对象中提取数据,并在内容控件或者列表控件的各项中显示数据。在数据绑定中,数据模板非常有效。在一定程度上,控件模板和数据模板是重叠的。这两种类型的模板都允许插入附加元素和应用格式化,然而,数据模板用于在已有控件的内部添加元素。预先构建好的控件内容不能改变。另一方面,控件模板是一种更加激进的方法,允许完全重写控件的内容模型。

面板模板用于控制列表控件中各项的布局,例如,可以使用面板模板创建列表框,从右向左然后向下平铺各项。

创建控件模板

简单按钮的模板:

<Style x:Key="ButtonBaseStyle" BasedOn="{StaticResource BaseStyle}" TargetType="{x:Type Button}">
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="Background" Value="{StaticResource Button.Static.Background}" />
        <Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}" />
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="HorizontalContentAlignment" Value="Center" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="Padding" Value="1" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                        <ContentPresenter
                            x:Name="contentPresenter"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            Focusable="False"
                            RecognizesAccessKey="True"
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsDefaulted" Value="true">
                            <Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="true">
                            <Setter TargetName="border" Property="Background" Value="{StaticResource Button.MouseOver.Background}" />
                            <Setter TargetName="border" Property="BorderBrush" Value="{StaticResource Button.MouseOver.Border}" />
                        </Trigger>
                        <Trigger Property="IsPressed" Value="true">
                            <Setter TargetName="border" Property="Background" Value="{StaticResource Button.Pressed.Background}" />
                            <Setter TargetName="border" Property="BorderBrush" Value="{StaticResource Button.Pressed.Border}" />
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter TargetName="border" Property="Background" Value="{StaticResource Button.Disabled.Background}" />
                            <Setter TargetName="border" Property="BorderBrush" Value="{StaticResource Button.Disabled.Border}" />
                            <Setter TargetName="contentPresenter" Property="TextElement.Foreground" Value="{StaticResource Button.Disabled.Foreground}" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

为应用自定义控件模板,只需要设置控件的Template属性。可以定义内联模板(上述代码实现方式),也可以将模板作为资源使用。

  • 控件模板的主要属性:

TargetType:明确指示该模板是为按钮设计

ContentPresenter:所有内容控件都有的元素,标识“在此插入内容”的标记器

x:Key:模板资源的名称,同一个资源文件中,键值必须唯一。不同资源文件中,后面的会覆盖前面的

  • 模板绑定TemplateBinding

模板绑定和普通绑定类似,但模板绑定只有用于控件模板使用,只支持单向绑定(从控件向模板传递信息),并且不能从Freezable类的派生类的属性中提取信息。

  • 改变属性的触发器

可以根据某些属性的改变,来修改控件模板的样式,

  • 样式和模板

样式和模板有类似之处,通常,在应用程序中,两者都可以改变元素的外观。然而,样式被限制在一个很小的范围内。他们都可以调整控件的属性,但是不能使用全新的由不同元素组成的可视化树替代控件原有的外观。

如果在样式和控件模板中都设置了同样触发器,那么样式触发器会优先于控件模板触发器。

如果在样式或者控件模板等资源上,不加入键名,就意味着改变元素类型对应的所有控件。也可以通过设置Style="{x:Null}"来退出默认样式。

构建更复杂的模板

在控件模板和为其提供支持的代码之间有一个隐含约定。如果使用自定义控件模板替代控件的标准模板,就需要确保新模板能够满足控件的实现代码的所有需求。

在简单控件中,这个过程比较容易,因为对模板几乎没有真正的要求。对于复杂控件,因为控件的外观和实现不可能是完全相互独立的,所以,控件要求对其可视化显示做出了一些假设。

前面看到的控件模板这种需求的例子,使用占位元素(ContentPresenter和ItemsPresenter)和模板绑定。后面会出现具有特定名称(PART_开头)的元素和专门设计的用于特定控件模板的元素(比如ScrollBar控件中的Track元素)。

嵌套的模板

假设计划修改熟悉的ListBox控件,为ListBox控件设计模板:

<Style x:Key="ListBoxBaseStyle" TargetType="{x:Type ListBox}">
        <Setter Property="Background" Value="{StaticResource ListBox.Static.Background}" />
        <Setter Property="BorderBrush" Value="{StaticResource ListBox.Static.Border}" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
        <Setter Property="ScrollViewer.CanContentScroll" Value="true" />
        <Setter Property="ScrollViewer.PanningMode" Value="Both" />
        <Setter Property="Stylus.IsFlicksEnabled" Value="False" />
        <Setter Property="VerticalContentAlignment" Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBox}">
                    <Border x:Name="Bd" Padding="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                        <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
                            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        </ScrollViewer>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter TargetName="Bd" Property="Background" Value="{StaticResource ListBox.Disabled.Background}" />
                            <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource ListBox.Disabled.Border}" />
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsGrouping" Value="true" />
                                <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" />
                            </MultiTrigger.Conditions>
                            <Setter Property="ScrollViewer.CanContentScroll" Value="false" />
                        </MultiTrigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

对于该模板,最值得注意的地方是它未提供的功能(配置列表项中各项的外观)。没有该功能,被选中的元素总是使用默认的状态显示。为了改变这种行为,需要为ListBoxItem控件增加控件模板,ListBoxItem控件是封装列表中每个单独元素内容的内容控件。

<Style x:Key="ListBoxItemContainerStyle1" TargetType="{x:Type ListBoxItem}">
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="Padding" Value="4,1" />
        <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
        <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="BorderBrush" Value="Transparent" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Border x:Name="Bd" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </Border>
                    <ControlTemplate.Triggers>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsMouseOver" Value="True" />
                            </MultiTrigger.Conditions>
                            <Setter TargetName="Bd" Property="Background" Value="{StaticResource Item.MouseOver.Background}" />
                            <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Item.MouseOver.Border}" />
                        </MultiTrigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="Selector.IsSelectionActive" Value="False" />
                                <Condition Property="IsSelected" Value="True" />
                            </MultiTrigger.Conditions>
                            <Setter TargetName="Bd" Property="Background" Value="{StaticResource Item.SelectedInactive.Background}" />
                            <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Item.SelectedInactive.Border}" />
                        </MultiTrigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="Selector.IsSelectionActive" Value="True" />
                                <Condition Property="IsSelected" Value="True" />
                            </MultiTrigger.Conditions>
                            <Setter TargetName="Bd" Property="Background" Value="{StaticResource Item.SelectedActive.Background}" />
                            <Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Item.SelectedActive.Border}" />
                        </MultiTrigger>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter TargetName="Bd" Property="TextElement.Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

修改滚动条

ScrollBar控件是出奇的复杂。

图片

滚动条的背景是由Track类表示(实际上是一个具有阴影并且被拉伸占满整个滚动条长度的矩形)。滚动条首尾处是按钮,通过这些按钮可以向上或者向下滚动一个步长,这些按钮是RepeatButton类的实例。滚动条中间是代表滚动内容当前位置的Thumb元素。这个滑块两侧也是透明的RepeatButton组成。当单击这两个按钮任意一个时,滚动条就会滚动一页。

<Style x:Key="ScrollBarBaseStyle" TargetType="ScrollBar">
        <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
        <Setter Property="Stylus.IsFlicksEnabled" Value="false" />
        <Setter Property="Foreground" Value="{DynamicResource ThirdlyTextBrush}" />
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Focusable" Value="False" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="Width" Value="8" />
        <Setter Property="MinWidth" Value="8" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ScrollBar">
                    <hc:SimplePanel x:Name="Bg" SnapsToDevicePixels="true">
                        <Border Background="{TemplateBinding Background}" />
                        <Track x:Name="PART_Track" IsDirectionReversed="true" IsEnabled="{TemplateBinding IsMouseOver}">
                            <Track.DecreaseRepeatButton>
                                <RepeatButton Command="{x:Static ScrollBar.PageUpCommand}" Style="{StaticResource ScrollBarBaseRepeatButton}" />
                            </Track.DecreaseRepeatButton>
                            <Track.IncreaseRepeatButton>
                                <RepeatButton Command="{x:Static ScrollBar.PageDownCommand}" Style="{StaticResource ScrollBarBaseRepeatButton}" />
                            </Track.IncreaseRepeatButton>
                            <Track.Thumb>
                                <Thumb Background="{TemplateBinding Foreground}" Style="{StaticResource ScrollBarBaseThumbVertical}" />
                            </Track.Thumb>
                        </Track>
                    </hc:SimplePanel>
                </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="8" />
                <Setter Property="MinHeight" Value="8" />
                <Setter Property="BorderThickness" Value="0,1" />
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="ScrollBar">
                            <hc:SimplePanel x:Name="Bg" SnapsToDevicePixels="true">
                                <Border Background="{TemplateBinding Background}" />
                                <Track x:Name="PART_Track" IsEnabled="{TemplateBinding IsMouseOver}">
                                    <Track.DecreaseRepeatButton>
                                        <RepeatButton Command="{x:Static ScrollBar.PageLeftCommand}" Style="{StaticResource ScrollBarBaseRepeatButton}" />
                                    </Track.DecreaseRepeatButton>
                                    <Track.IncreaseRepeatButton>
                                        <RepeatButton Command="{x:Static ScrollBar.PageRightCommand}" Style="{StaticResource ScrollBarBaseRepeatButton}" />
                                    </Track.IncreaseRepeatButton>
                                    <Track.Thumb>
                                        <Thumb Background="{TemplateBinding Foreground}" Style="{StaticResource ScrollBarBaseThumbHorizontal}" />
                                    </Track.Thumb>
                                </Track>
                            </hc:SimplePanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

下面列出需要注意的几个要点:

  • 滚动条(垂直)由一个包含三行的网格构成。顶部和底部容纳两个按钮(箭头),固定占用18个像素,剩余部分是Track部分
  • 两端的RepeatButton样式相同,分别是向上和向下的箭头
  • 两个按钮链接到ScrollBar;类中的命令(LineUpCommand和LineDownCommand),这正是其工作原理,只要链接到这两个命令即可,无需关心按钮的名字、外观
  • Track元素的名为PART_Track。
  • Track.ViewportSize属性被设置为0,这是该模板特有的细节,可确保Thumb元素总有相同的尺寸(通常,滑块根据内容按照比例地改变尺寸,因此如果滚动的内容在窗口中基本上能够显示,这时滑块就会变长)
  • Track元素封装了两个RepeatButton对象和Thumb元素,这些按钮也是通过命令连接到适当的功能。

可视化状态

最直接编写控件模板的方法:混合使用元素、绑定表达式以及触发器。使用元素创建控件的整个可视化结构。绑定用于从控件类的属性提取信息并将其应用于元素内部。而触发器创建交互功能,当控件的状态发生改变时,允许控件改变其外观。

使用具有特定名称的部件和可视化状态,控件能提供标准化的可视化协定。控件可使用TemplatePart特性指示控件模板应包含具有特定名称的元素,可使用TemplateVisualStatus特性指示他们支持的可视化状态。(在自定义元素中会有详细的例子出现)

posted @ 2021-08-16 14:39  蜗牛的希望  阅读(523)  评论(0编辑  收藏  举报