重写TreeView模板来实现数据分层展示(一)

时间:2023-03-09 20:02:29
重写TreeView模板来实现数据分层展示(一)

  总想花些时间来好好总结一下TreeView这个WPF控件,今天来通过下面的这几个例子来好好总结一下这个控件,首先来看看一个常规的带虚线的TreeView控件吧,在介绍具体如何完成之前首先来看看最终实现的效果图吧!

重写TreeView模板来实现数据分层展示(一)

  然后我们来具体分析一下这个是怎样去实现的?

1 修改TreeView的模板层

其实TreeView中最重要的就是TreeViewItem项,这个决定了最终TreeView的展现方式,另外就是TreeView每展开子项时前面的ToggleButton的样式了,因为默认的TreeView样式前面是一个三角形,那么我们先从整个TreeView的样式开始说起吧!

 <Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Background" Value="Transparent"/>
<!--此处设置TreeViewItem的绑定样式-->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"></Setter>
<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="Padding" Value="1,0,0,0"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19" Width="Auto"/>
<ColumnDefinition Width="150" MinWidth="150"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="22"/>
<RowDefinition />
</Grid.RowDefinitions>
<!-- Connecting Lines -->
<!-- Horizontal line -->
<Rectangle x:Name="HorLn" Margin="9,0,0,0" Height="1" Stroke="Green" SnapsToDevicePixels="True"/>
<!-- Vertical line -->
<Rectangle x:Name="VerLn" Width="1" Stroke="Green" Margin="0,0,1,0" Grid.RowSpan="2" SnapsToDevicePixels="true" Fill="White"/>
<ToggleButton x:Name="Expander"
Grid.Column="0"
Grid.Row="0"
ClickMode="Press"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ExpandCollapseToggleStyle}"/>
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Grid.Column="1"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Width="250"/>
</Border>
<ItemsPresenter x:Name="ItemsHost"
Grid.ColumnSpan="2"
Grid.Column="1"
Grid.Row="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>

  这里面重点讲述一下TreeViewItem的模板,首先TreeViewItem是有一个两行三列的Grid构成的,首先在第0行0列有两部分构成的,首先是注释部分已经写清楚了是Horizontal line 这个是每一个TreeViewItem的水平横线,实际上是一个高度为1的Rectangle,<Rectangle x:Name="HorLn" Margin="9,0,0,0" Height="1" Stroke="Green" SnapsToDevicePixels="True"/>,另外一部分是很重要的ToggleButton,这里是通过一个Style来表示的,Style="{StaticResource ExpandCollapseToggleStyle}"那么我们再具体看看这个ExpandCollapseToggleStyle的样式。

 <Style x:Key="ExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="15" Height="13" SnapsToDevicePixels="True">
<!-- Rectangle 9x9 pixels -->
<Rectangle Width="15" Height="15" Stroke="#919191" SnapsToDevicePixels="true">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0.5,2" StartPoint="0.5,0">
<GradientStop Color="#00a3d9" Offset="0"/>
<GradientStop Color="#00a3d9" Offset="0.5"/>
<GradientStop Color="#00a3d9" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<!-- 画一个垂直方向的直线 -->
<Rectangle x:Name="ExpandPath" Width="2" Height="8" Stroke="Black" SnapsToDevicePixels="true"/>
<!-- 画一个水平方向的直线 -->
<Rectangle Width="8" Height="2" Stroke="Black" SnapsToDevicePixels="true"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Visibility" TargetName="ExpandPath" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  这个其实也比较简单就是在一个Grid里面放了两个Rectangle,并通过绑定IsChecked属性来改变垂直的那条直线的Visibility属性来完成最后的效果。介绍完了这一部分,我们再看看在TreeViewItem的模板中剩下的部分,在Grid的第0行第一列是用来显示每一个TreeViewItem的标题的内容,总体的结构是一个Border里面嵌套了一个ContentPresenter最终来呈现TreeViewItem的Header,这里面重点来介绍一下 ContentSource="Header" 这个属性非常重要,我们知道我们在给Header赋值时就会呈现该内容,当然赋值的内容可以是文字也可以是一个对象,如果是对象的话还需要通过写一个DataTemplate来呈现这个对象,我们来看看MSDN上的解释:获取或设置要在自动命名别名过程中使用的基名称。同时还给出了一个完整的例子,请参考下面的代码。

下面的示例演示一个用于样式 HeaderedContentControl 演示的用法 ContentSource 属性︰

<Style TargetType="HeaderedContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type HeaderedContentControl}">
<StackPanel>
<Grid>
<Rectangle Stroke="{TemplateBinding Background}"/>
<ContentPresenter ContentSource="Header"/>
</Grid>
<Grid>
<Rectangle Fill="{TemplateBinding Background}"/>
<ContentPresenter ContentSource="Content"/>
</Grid>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  这里使用这个例子再合适不过了,因为TreeViewItem就是直接从HeaderedContentControl继承过来的。

在介绍了上面这些外再看看Grid中第1行第2、3两列的模板内容吧! <ItemsPresenter x:Name="ItemsHost"  Grid.ColumnSpan="2"                                   Grid.Column="1"  Grid.Row="1" /> 这个ItemsPresenter 主要是来呈现当前TreeViewItem的子项的,这个也非常重要的,否则是不会显示子项内容的,按照这样去分析整个View层的模板就都已经到位了,另外再看这些代码中两个容易被忽视的小细节,一个是FocusVisualStyle它使用的是TreeViewItemFocusVisual,这个表示当前TreeView获得焦点时呈现的状态,MSDN的解释如下:获取或设置一个属性,该属性允许自定义此元素在捕获到键盘焦点时要应用于此元素的外观、效果或其他样式特征。(继承自 FrameworkElement。)我们再来看看具体的样式代码。

  <Style x:Key="TreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

  另外一个重要的部分就是TreeView的Style.Triggers属性,当TreeView的 VirtualizingStackPanel.IsVirtualizing设置为True时,为ItemsPanel的模板设置为VirtualizingStackPanel,至于VirtualizingStackPanel到底有哪些作用,这些又是一个很宏大的内容,我们来看看几篇介绍得比较好的博客及文章。

2  TreeView控件绑定数据

  前面的一部分内容只是单纯去修改TreeView的模板,从而决定最终展示的形式,但是最终展示哪些内容,如何展示就涉及到另外的一部分内容即:TreeView控件的数据绑定。在我们的DEMO中,我们使用级联的数据模板来展示最终的内容,关于级联的数据模板,这里也不再赘述,直接来看代码,然后做进一步的分析。  

    <TreeView x:Name="trvMenu" Background="LightGray">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type self:MyTreeViewItem}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<Image Source="/TestTreeView;component/Images/connect.png"
Width="24"
Height="24"
Stretch="Fill">
</Image>
<TextBlock Text="{Binding TreeViewItemName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>

  每一个TreeViewItem的最终呈现的方式是通过TreeView.ItemTemplate的模板来表现的,这里通过一个StackPanel来展示每个节点的内容,包括一个图片和一个TextBlock内容,如果有需要还可以为每个TreeViewItem前面加上CheckBox等控件,从而完成更加复杂的内容。

  最后面就是通过代码来给该TreeView绑定内容了,这里也贴出代码。这里直接在MainWindow.cs中添加相关的内容。

 private void InitDataSource()
{
MyTreeViewItem root = new MyTreeViewItem() { TreeViewItemName="Root"};
MyTreeViewItem childLevel1 = new MyTreeViewItem() { TreeViewItemName = "childLevel1" };
MyTreeViewItem childLevel1_1 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1" };
MyTreeViewItem childLevel1_1_1 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1_1" };
MyTreeViewItem childLevel1_1_1_1 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1_1_1" };
MyTreeViewItem childLevel1_1_1_2 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1_1_2" };
MyTreeViewItem childLevel1_1_1_3 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1_1_3" };
MyTreeViewItem childLevel1_1_1_4 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_1_1_4" };
childLevel1_1_1.Children.Add(childLevel1_1_1_1);
childLevel1_1_1.Children.Add(childLevel1_1_1_2);
childLevel1_1_1.Children.Add(childLevel1_1_1_3);
childLevel1_1_1.Children.Add(childLevel1_1_1_4);
childLevel1_1.Children.Add(childLevel1_1_1); MyTreeViewItem childLevel1_2 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_2" };
MyTreeViewItem childLevel1_2_1 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_2_1" };
MyTreeViewItem childLevel1_2_2 = new MyTreeViewItem() { TreeViewItemName = "childLevel1_2_2" };
childLevel1_2.Children.Add(childLevel1_2_1);
childLevel1_2.Children.Add(childLevel1_2_2); childLevel1.Children.Add(childLevel1_1);
childLevel1.Children.Add(childLevel1_2); MyTreeViewItem childLevel2 = new MyTreeViewItem() { TreeViewItemName = "childLevel2" };
MyTreeViewItem childLevel2_1 = new MyTreeViewItem() { TreeViewItemName = "childLevel2_1" };
MyTreeViewItem childLevel2_2 = new MyTreeViewItem() { TreeViewItemName = "childLevel2_2" };
MyTreeViewItem childLevel2_3 = new MyTreeViewItem() { TreeViewItemName = "childLevel2_3" };
MyTreeViewItem childLevel2_4 = new MyTreeViewItem() { TreeViewItemName = "childLevel2_4" };
childLevel2.Children.Add(childLevel2_1);
childLevel2.Children.Add(childLevel2_2);
childLevel2.Children.Add(childLevel2_3);
childLevel2.Children.Add(childLevel2_4); root.Children.Add(childLevel1);
root.Children.Add(childLevel2); trvMenu.Items.Add(root); }

  这里面最重要的就是MyTreeViewItem对象了,这个是绑定到TreeViewItem中的最直接的数据源,对应 DataType="{x:Type self:MyTreeViewItem}" 这里面的内容,这里也贴出相关的代码。

public class MyTreeViewItem
{
private string _treeViewItemName;
public string TreeViewItemName
{
get { return _treeViewItemName; }
set { _treeViewItemName = value; }
}
private ObservableCollection<MyTreeViewItem> _children = new ObservableCollection<MyTreeViewItem>();
public ObservableCollection<MyTreeViewItem> Children
{
get
{
return _children;
}
set
{
if (value != _children)
{
_children = value;
}
} }
}

  这里需要说明一下,这种写法有些不规范,单由于仅仅是一个简单的DEMO,规范的写法是每一个属性都要带通知,及当前MyTreeViewItem类要实现INotifyPropertyChanged接口,并且在属性值改变时发出通知,从而通过View层去更新相应的内容。

另外需要注意的内容是 HierarchicalDataTemplate 的ItemsSource绑定的内容ItemsSource="{Binding Children}",这个Children代表的是当前节点的下一级别节点的数据源,第一级节点的数据源已经通过后台代码绑定好了,这里需要特别注意。

下一节我们将继续如何巧用TreeView的模板来实现更加多样的数据呈现方式,从而真正地去体现WPF的强大之处......