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

WPF 中的高级自定义 TreeView 布局

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (68投票s)

2007年1月28日

CPOL

4分钟阅读

viewsIcon

484098

downloadIcon

9620

回顾 WPF TreeView 的高级布局自定义。

引言

在 CodeProject 上的一篇先前文章中,我演示了一种重定义 WPF TreeView 显示方式的方法。在那篇文章中,我们研究了如何使用一些 XAML 魔术来让 TreeView 看起来像一个组织结构图。在本文中,我们将探讨一种更复杂、更具交互性的自定义,它使 TreeView 将其项目呈现为一组“嵌套的存储桶”。

让我看看实际效果

让我们来看看自定义的 TreeView 的样子。稍后我们将了解这种自定义布局是如何实现的。下面看到的截图是一个演示应用程序,可以通过页面顶部的链接下载。

The TreeView with a layout customization in effect.

用户界面的顶部是自定义的 TreeView。最里面的项目提供了指向维基百科页面的链接,这些页面包含有关这些项目中命名的城市的信息。UI 的底部是一个 Frame 元素,它加载并显示在 TreeView 中选择的维基百科页面。

如果我们不对上面看到的 TreeView 应用布局自定义,它看起来会是这样的

Plain Jane

什么是布局自定义?

再次快速浏览一下上面未自定义的 TreeView 截图。请注意,即使没有应用布局自定义,叶子项目也呈现为超链接。TreeViewItem 内容的渲染方式受数据模板的影响。“布局自定义”我指的是不处理项目内容的渲染,而是解释如何渲染包含项目内容的容器以及这些容器如何相互定位。项目内容对我们来说无关紧要。

TreeViewTreeViewItem 都派生自 ItemsControlItemsControl 包含项目容器,可以将其视为容纳任意内容的“盒子”。这些盒子的内容是用户消费的数据(即用户关心并最关注的东西)。在 TreeView 中,这些盒子由 TreeViewItem 对象表示。布局自定义会组织 TreeViewItem——解释它们应该如何定位、如何渲染、是否应该显示或隐藏等。

工作原理

数据格式

在深入研究实现我们自定义布局的代码之前,让我们花点时间回顾一下正在显示的数据。在演示应用程序中,TreeView 绑定到 XML 数据,其格式非常简单

<?xml version="1.0" encoding="utf-8" ?>
<Countries>
  <Country CountryName="USA">
    <Region RegionName="California">
      <City 
        CityName="Los Angeles" 
        Uri="http://en.wikipedia.org/wiki/Los_Angeles" />
      
      <!-- More City elements... -->
      
    </Region>

    <!-- More Region elements... -->

  </Country>

  <!-- More Country elements... -->

</Countries>

TreeView 将 Country、Region 和 City 元素显示为 TreeViewItem。它将 Country 和 Region 项目渲染为可折叠组,其标题是 CountryName 或 RegionName 属性值,内部项目列表来自元素中的嵌套子元素集合(一个国家包含地区,一个地区包含城市)。

TreeViewItem 样式

下面是包含大部分自定义布局实现的 Style 的节选版本

<Style TargetType="TreeViewItem">
  <Style.Resources>
    <!-- Resources omitted for clarity... -->
  </Style.Resources>

  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="TreeViewItem">
        <Grid Margin="8,4">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
          </Grid.RowDefinitions>

          <!-- This Border contains elements which display 
               the content and child items of the TreeViewItem. -->
          <Border Name="Bd" 
            Background="{StaticResource ItemAreaBrush}"
            BorderBrush="{StaticResource ItemBorderBrush}" 
            BorderThickness="0.6" 
            CornerRadius="8"              
            Padding="6"     
            SnapsToDevicePixels="True"
            >
            <Grid>
              <!-- Items with children are shown in an Expander. -->
              <Expander Name="Exp" 
                IsExpanded="{TemplateBinding TreeViewItem.IsExpanded}">
                <Expander.Header>
                  <!-- Displays the item's header in the Expander. -->
                  <ContentPresenter ContentSource="Header" />
                </Expander.Header>
                <!-- Displays the item's children. -->
                <ItemsPresenter />
              </Expander>

              <!--Items without children are shown in a ContentPresenter.-->
              <ContentPresenter Name="CntPres"
                ContentSource="Header"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Visibility="Collapsed" 
                />
            </Grid>
          </Border>
        </Grid>

        <ControlTemplate.Triggers>
          <!-- If the TreeViewItem has child items,
               show it in an Expander.  Otherwise
               hide the Expander and show the hidden
               ContentPresenter. -->
          <Trigger Property="TreeViewItem.HasItems" Value="false">
            <Setter 
              TargetName="Exp" 
              Property="Visibility" 
              Value="Collapsed" />
            <Setter 
              TargetName="CntPres" 
              Property="Visibility" 
              Value="Visible" />
          </Trigger>

          <!--When the item is selected in the TreeView, use the 
              "selected" colors and give it a drop shadow. -->
          <Trigger Property="IsSelected" Value="true">
            <!-- Setters omitted for clarity... -->
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>

  <!-- Make each TreeViewItem show it's children 
       in a StackPanel. If it is a root item then
       the Orientation will be 'Horizontal', else
       'Vertical'. -->
  <Setter Property="ItemsPanel">
    <Setter.Value>
      <ItemsPanelTemplate>
        <ItemsPanelTemplate.Resources>
          <local:ItemsPanelOrientationConverter x:Key="conv" />
        </ItemsPanelTemplate.Resources>
        <StackPanel 
          IsItemsHost="True" 
          Orientation="{Binding 
            RelativeSource={x:Static RelativeSource.TemplatedParent}, 
            Converter={StaticResource conv}}" 
          />
      </ItemsPanelTemplate>
    </Setter.Value>
  </Setter>
</Style>

选择正确的视觉效果

上面 XAML 中有几个方面值得指出。StyleTreeViewItemTemplate 属性设置为 ControlTemplate。该模板负责解释 TreeViewItem 实例应如何渲染。代表 Country 或 Region XML 元素的项目必须渲染为可折叠组,但 City 元素则不需要。让我们仔细看看如何实现这一点。我省略了一些不重要的设置,以便我们能专注于关键信息

<Border>
  <Grid>
    <!-- Items with children are shown in an Expander. -->
    <Expander Name="Exp">
      <Expander.Header>
        <!-- Displays the item's header in the Expander. -->
        <ContentPresenter ContentSource="Header" />
      </Expander.Header>
      <!-- Displays the item's children. -->
      <ItemsPresenter />
    </Expander>

    <!-- Items without children are shown in a ContentPresenter. -->
    <ContentPresenter Name="CntPres"
      ContentSource="Header"      
      Visibility="Collapsed" />
  </Grid>
</Border>

上面的 XAML 创建了一个 Border 元素,其中包含一个 Grid 面板。该 Grid 有一行一列(即一个“单元格”)。该单元格包含一个 Expander 和一个 ContentPresenter,但在这两个元素中的任何一个在任何给定时刻都只会可见。如果 TreeViewItem 具有子项,则会显示 Expander。如果项目没有任何子项(在这种情况下,如果它代表 XML 数据中的 City 元素),则会显示 ContentPresenter

控件模板有一个 Trigger 来确定应使用哪个元素来渲染 TreeViewItem。下面的代码显示了该 Trigger

<Trigger Property="TreeViewItem.HasItems" Value="false">
  <Setter 
    TargetName="Exp" 
    Property="Visibility" 
    Value="Collapsed" />
  <Setter 
    TargetName="CntPres" 
    Property="Visibility" 
    Value="Visible" />
</Trigger>

项目布局方向

本文开头截图中看到的布局的另一个棘手方面与 TreeViewItem 的排列方向有关。代表 Country 和 Region 元素的项目水平排列,但 City 项目垂直排列。

要将根项目(Country 项目)排列成水平行,需要将 TreeViewItemsPanel 属性设置为具有水平方向的 StackPanel。以下是一些来自演示应用程序主 Window 的 XAML,用于配置 TreeView

<TreeView Name="tree" 
  DataContext="{StaticResource countriesXml}" 
  ItemsSource="{Binding}"
  >
  <!-- Import the resource file with the 
       new TreeViewItem style. -->
  <TreeView.Resources>
    <ResourceDictionary 
      Source="GroupedTreeViewItemStyle.xaml" />
  </TreeView.Resources>

  <!-- Arrange the root items horizontally. -->
  <TreeView.ItemsPanel>
    <ItemsPanelTemplate>
      <StackPanel 
        IsItemsHost="True" 
        Orientation="Horizontal" />
    </ItemsPanelTemplate>
  </TreeView.ItemsPanel>
</TreeView>

下一块拼图需要比仅仅设置一个属性更多的技巧。由于代表 Region 元素的 TreeViewItem 必须水平排列,而 City 项目必须垂直列出,因此我们需要使用值转换器在运行时确定 TreeViewItemItemsPanel 应使用何种方向。以下是来自前面看到的 Style 的 XAML,用于设置 TreeViewItemItemsPanel

<Setter Property="ItemsPanel">
  <Setter.Value>
    <ItemsPanelTemplate>
      <ItemsPanelTemplate.Resources>
        <local:ItemsPanelOrientationConverter x:Key="conv" />
      </ItemsPanelTemplate.Resources>
      <StackPanel 
        IsItemsHost="True" 
        Orientation="{Binding 
            RelativeSource={x:Static RelativeSource.TemplatedParent}, 
            Converter={StaticResource conv}}" 
          />
    </ItemsPanelTemplate>
  </Setter.Value>
</Setter>

用作 ItemsPanel 模板的 StackPanelOrientation 属性已绑定。该绑定使用值转换器来确定 StackPanel 应具有水平还是垂直方向。以下是值转换器的代码

[ValueConversion( typeof( ItemsPresenter ), typeof( Orientation ) )]
public class ItemsPanelOrientationConverter : IValueConverter
{  
 // Returns 'Horizontal' for root TreeViewItems 
 // and 'Vertical' for all other items.
 public object Convert( 
  object value, Type targetType, object parameter, CultureInfo culture )
 {
  // The 'value' argument should reference 
  // an ItemsPresenter.
  ItemsPresenter itemsPresenter = value as ItemsPresenter;
  if( itemsPresenter == null )
   return Binding.DoNothing;

  // The ItemsPresenter's templated parent 
  // should be a TreeViewItem.
  TreeViewItem item = itemsPresenter.TemplatedParent as TreeViewItem; 
  if( item == null )
   return Binding.DoNothing;

  // If the item is contained in a TreeView then it is
  // a root item.  Otherwise it is contained in another
  // TreeViewItem, in which case it is not a root.
  bool isRoot = 
   ItemsControl.ItemsControlFromItemContainer( item ) is TreeView;

  // The children of root items are layed out
  // in a horizontal row.  The grandchild items 
  // (i.e. cities) are layed out vertically.
  return
   isRoot ?
   Orientation.Horizontal :
   Orientation.Vertical;
 }

 public object ConvertBack( 
  object value, Type targetType, object parameter, CultureInfo culture )
 {
  throw new NotSupportedException( "Cannot convert back." );
 }
}
© . All rights reserved.