【wpf】子项容器模板 控件模板 数据模板 逻辑树 视觉树 之间的关系

举几个例子

ItemsPanelTemplate(子项容器模板)

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <UniformGrid Columns="3"/>
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

注意 ListView的ItemsPanel这个属性的类型为ItemsPanelTemplateItemsPanelTemplate用于改变子项默认的从上到下的布局。ListView虽然是类型名称,但是它点出来的东西是对象。而以单独以类型名作为标签的过程,都是实例化的过程。

2  DataTemplate(数据模板)

<ListView.ItemTemplate>
    <DataTemplate>
        <Grid Canvas.Left="{Binding Left}" Canvas.Top="{Binding Top}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding Name}"/>
            <TextBlock Text="{Binding Age}" Grid.Column="1"/>
        </Grid>
    </DataTemplate>
</ListView.ItemTemplate>

ListView的ItemTemplate这个属性的类型是DataTemplate

3  ControlTemplate(控件模板)

而Button的Template属性的类型是ControlTemplate,其实任何控制的Template属性的类型都是

ControlTemplate,如Button:

<Button DataContext="{StaticResource datas}" Height="50">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Grid DataContext="{Binding [0]}">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text="{Binding Age}" Grid.Column="1"/>
            </Grid>
        </ControlTemplate>

避开两个坑

要理解模板,首要避开的一个坑就是,模板属性名称和模板类型名称。不区分这个东西,你会误以为有很多模板类型,直接吓蒙,劝退。

        每个控件的模板属性和模板类型,是实例与类的关系,这里一定注意!

其次,要认识到,下面这种语法这并不是嵌套,而是属性的赋值,是一个实例化的过程:

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <!--布局的时候,子项是动态绑定-->
        <Canvas/>
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

ListView的ItemsPanel属性的类型为ItemsPanelTemplate,所以这里在里面写了一个ItemsPanelTemplate,这表示实例化了一个ItemsPanelTemplate对象并赋值给ListView的ItemsPanel属性

逻辑树和视觉树

谈论模板,避不开视觉树,首先看逻辑树,这个很简单,因为它很直观就是xaml中嵌套这些“业务逻辑”:

<Window x:Class="Zhaoxi.WPFLession.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Zhaoxi.WPFLession"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <UniformGrid Rows="2" Columns="2">
        <Button>
            <TextBox>asdfasdf</TextBox>
        </Button>
        <TextBox Width="100" Height="30">123456</TextBox>
        <TextBlock Width="100" Height="30" Background="AliceBlue">sdfad</TextBlock>
    </UniformGrid>
</Window>

现在上面这段代码的逻辑树,就是:Window -》UniformGrid-》Button-》TextBox (并列的关系我就不画了)

视觉树,就要看控件内部,比如Button这个控件,就是由更基础的元素构建的,只是被封装起来我们看不到细节而已,但是你能看到button中间有文字,背景是灰色的。

接下来,我们可以借助编辑模板这个功能,观察一下,Button的内部世界(视觉树):

选中Button,右键选择=》编辑模板

<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <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" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsDefaulted" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="true">
                                <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                                <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

这里通过Style设置了button的所有属性,但是这里我们重点关注的是Template这个属性!这里就是视觉树的呈现。那我们发现构成button的元素部分,异常的简单:

<Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
    <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>

一个 border里面放了一个ContentPresenter(数据模板相关,后面讲解) 。而这些被包含在了一个叫做ControlTemplate的标签里。

ControlTemplate

ControlTemplate控件模板就是给修改视觉树提供了一个接口!接下来我想看看Listbox的控件模板,方法一样:

<Style x:Key="ListBoxStyle1" 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" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="1" SnapsToDevicePixels="true">
                            <ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                            </ScrollViewer>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource ListBox.Disabled.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" 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>

元素构成部分也不多:

<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="1" SnapsToDevicePixels="true">
    <ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}">
        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
    </ScrollViewer>
</Border>

最后我想看看TextBlock的ControlTemplate,然而你会发现TextBlock根本没有ControlTemplate,因为他根本就不算是一个控件,他直接继承自FrameworkElement,它只是一个元素。(包括Border和ItemPresenter以及ContentPresenter都是继承FrameworkElement并没有直接继承自Control)

那么这里,我总结一下:

逻辑树中每个控件的内部其实包含了视觉树。视觉树也是有基本的元素和控件构成!因为视觉树是被封装起来的,所以微软为程序员提供了修改控件内部(视觉树)的机会,及控件模板。修改控件模板,可以轻易改变控件的外表!举个例子:

这两个都是checkbox,我们不需要重写控件,只需要通过控件模板修改一下逻辑树以及Trigger就能实现。(Trigger下一篇再说)

注意:这里指定TargetType是很重要的,不然IsChecked这个属性无法通过编译

<CheckBox Width="100" Height="50" >
    <CheckBox.Template>
        <ControlTemplate TargetType="CheckBox">
            <Border Background="AliceBlue">
                <Canvas>
                    <Rectangle x:Name="rt" Fill="Orange" Width="50" Height="50"/>
                </Canvas>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsChecked" Value="true">
                    <Setter TargetName="rt" Property="Canvas.Left" Value="50"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </CheckBox.Template>
</CheckBox>

这里的改造,其实有个小问题,他把原有的 ContentPresenter 去掉了。这样改造了控件之后,那么这个控件就无法在支持数据模板了! 接下来,请继续往下看。

DataTemplate (数据模板)

接下来是数据模板,他也要看这个课视觉树,前面的视觉树中,我们发现了一个不认识的东西,但是视觉树里基本都有他:ItemPresenter以及ContentPresenter(以Presenter结尾的东西)

Presenter 主要含义时主持人,但它还有一个含义是,呈献者!

这些以Presenter结尾的元素,统称为“内容占位”。为啥wpf中控件之间可以任意嵌套?奥秘就在这里,如果你给button内部嵌套个啥的,这个东西都会扔给ContentPresenter,ContentPresenter会将其包裹起来。而Presenter结尾的元素也就是数据模板作用的对象

举个例子:

<Window x:Class="Zhaoxi.WPFLession.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Zhaoxi.WPFLession"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <Window.Resources>
        <x:Array Type="local:Person" x:Key="datas">
            <local:Person Name="啊啊啊" Age="20" Gender="1"/>
            <local:Person Name="呃呃呃" Age="21" Gender="2"/>
            <local:Person Name="哦哦哦" Age="21" Gender="2"/>
            <local:Person Name="呵呵呵" Age="21" Gender="2"/>
        </x:Array>
    </Window.Resources>
    <UniformGrid Rows="2" Columns="2">
        <ListBox ItemsSource="{StaticResource datas}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <DockPanel LastChildFill="False">
                        <TextBlock DockPanel.Dock="Right" Text="{Binding Name}"/>
                        <CheckBox/>
                    </DockPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </UniformGrid>
</Window>

 效果如下:

 

 用snoop观察一下我们这个程序的内部结构:

 你发现了三个Presenter结尾的元素,

一个ScrollContentPresenter是属于ListBox的视觉树种的ScrollView的,ScrollContentPresenter就包含了ItemsPresenter,就包含了一系列的ListBoxItem,而ListBoxItem就包含了我们在数据模板中定义的DataTemplate的内容:

小结:数据模板,其实也能改变视觉树,只不过他并不是重新修改控件的整个视觉树,而是向Presenter结尾的元素中添加内容。而这些内容需要绑定的数据数据才会被实例化。

数据模板和控件模板的具体关系

接下来的例子可以很好的看清,数据模板和控件模板的关系:

我想给ItemsControl添加一个滚动条,通过观察ItemsControl的控件模板,可以发现ItemsControl没有滚动条的:

 你在回头看 Listbox 的控件模板,其实就是比 ItemsControl 多了个 ScrollViewer,

那我,我们就可以手动改造ItemsControl的控件模板,让他升级为 Listbox!

ItemsPresenter,就是 Items的呈献者,它包含了所有的Item条目,

而数据模板(ItemTemplate),就是 规划 ItemsPresenter中每个Item的 具体样子!

这里就能清楚的看到,其实数据模板是存在于控件模板内部的

额外的一个小例子

再来一个例子:(它实现了,Items颜色变换规律,通过数据模板实现)

<ItemsControl ItemsSource="{StaticResource datas}" AlternationCount="2">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid Background="Transparent" Name="root">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding Name}"/>
                        <TextBlock Text="{Binding Age}" Grid.Column="1"/>
                    </Grid>
                    <DataTemplate.Triggers>
                        <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                            <Setter Property="Background" Value="Orange" TargetName="root"/>
                        </Trigger>
                    </DataTemplate.Triggers>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

这里注意,这种触发器是设置Background属性时要指定TargetName。

ItemsPanelTemplate

ItemsPanelTemplate用于改变子项默认的从上到下的布局。

Items控件默认的布局方式是从上到下,如果你像改变这种布局方式,比如你想布局为从左往右。

你可以这么写:

<ItemsControl Grid.Row="1" ItemsSource="{Binding config_infos.list_seriaInfo}" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Columns="3"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ctl:SerialControl/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

最终得到这样的效果:(SerialControl,是我自定义的用户控件,我希望他们横向排列而非纵向)

总结:

1  我们需要避开两个坑。

2  理解什么是视觉树。

3  数据模板和控件模板都是微软提供的接口,用于修改视觉树。

4 子项容器模板,用于改变默认布局方式。

posted @ 2022-07-30 14:28  宋桓公  阅读(117)  评论(0编辑  收藏  举报