Дерево с несколькими колонками


Стандартный TreeView в WPF, казалось бы, не позволяет сделать многоколоночное дерево. Но, как оказалось на практике, немного пошаманив с DataTemplate и Binding, нужного результата достигнуть не сложно.
Дерево с несколькими колонками

Сначала я создал модель элемента дерева содержащую три текстовых поля и коллекцию потомков:
public class TreeItemModel
{
    #region Properties

    /// <summary>
    /// Gets or sets Property1.
    /// </summary>
    public string Property1 { get; set; }

    /// <summary>
    /// Gets or sets Property2.
    /// </summary>
    public string Property2 { get; set; }

    /// <summary>
    /// Gets or sets Property3.
    /// </summary>
    public string Property3 { get; set; }

    /// <summary>
    /// TreeItem children collection.
    /// </summary>
    private ObservableCollection<TreeItemModel> children;

    /// <summary>
    /// Gets TreeItem children collection.
    /// </summary>
    public ObservableCollection<TreeItemModel> Children
    {
        get
        {
            if (children == null) children = new ObservableCollection<TreeItemModel>();
            return children;
        }
    }

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="TreeItemModel"/> class.
    /// </summary>
    /// <param name="property1">
    /// The property 1.
    /// </param>
    /// <param name="property2">
    /// The property 2.
    /// </param>
    /// <param name="property3">
    /// The property 3.
    /// </param>
    public TreeItemModel(string property1, string property2, string property3)
    {
        Property1 = property1;
        Property2 = property2;
        Property3 = property3;
    }

    #endregion
}

Затем создал основную ViewModel для дерева:

public class SampleViewModel
{
    #region Properties

    /// <summary>
    /// Children collection.
    /// </summary>
    private ObservableCollection<TreeItemModel> children = new ObservableCollection<TreeItemModel>();

    /// <summary>
    /// Gets children collection.
    /// </summary>
    public ObservableCollection<TreeItemModel> Children
    {
        get
        {
            return children;
        }
    }

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="SampleViewModel"/> class.
    /// </summary>
    public SampleViewModel()
    {
        // Set sample data
        TreeItemModel item1 = new TreeItemModel("Item 1", "565", "6486");
        Children.Add(item1);
        TreeItemModel item11 = new TreeItemModel("Item 1-1", "321", "15");
        item1.Children.Add(item11);
        TreeItemModel item12 = new TreeItemModel("Item 1-2", "652", "456");
        item1.Children.Add(item12);
        TreeItemModel item121 = new TreeItemModel("Item 1-2-1", "1210", "457");
        item12.Children.Add(item121);
        TreeItemModel item122 = new TreeItemModel("Item 1-2-2", "4576", "06");
        item12.Children.Add(item122);
        TreeItemModel item13 = new TreeItemModel("Item 1-3", "547", "975");
        item1.Children.Add(item13);
        TreeItemModel item2 = new TreeItemModel("Item 1", "565", "6486");
        Children.Add(item2);
        TreeItemModel item21 = new TreeItemModel("Item 2-1", "255", "454");
        item2.Children.Add(item21);
        TreeItemModel item211 = new TreeItemModel("Item 2-1-1", "245", "4534");
        item21.Children.Add(item211);
        TreeItemModel item212 = new TreeItemModel("Item 2-1-2", "464", "624362");
        item21.Children.Add(item212);
        TreeItemModel item22 = new TreeItemModel("Item 2-2", "545", "453");
        item2.Children.Add(item22);
    }

    #endregion
}

Потом создал окно, в котором будет располагаться трёх-колоночный Grid - он будет играть роль заголовка нашего дерева и собственно сам TreeView:

<Grid Height="20" VerticalAlignment="Top">
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="50" MinWidth="20" MaxWidth="150"/>
    <ColumnDefinition Width="50" MinWidth="20" MaxWidth="150"/>
  </Grid.ColumnDefinitions>
  <Grid.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
      <GradientStop Color="White" Offset="0"/>
      <GradientStop Color="White" Offset="0.5"/>
      <GradientStop Color="#FFDDDDEE" Offset="0.5"/>
      <GradientStop Color="#FFBBBBCC" Offset="0.95"/>
      <GradientStop Color="#FF000055" Offset="1"/>
    </LinearGradientBrush>
  </Grid.Background>
  <TextBlock Grid.Column="0" Margin="3" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" x:Name="column1">Column 1</TextBlock>
  <TextBlock Grid.Column="1" Margin="3" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" x:Name="column2">Column 2</TextBlock>
  <TextBlock Grid.Column="2" Margin="3" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" x:Name="column3">Column 3</TextBlock>

  <GridSplitter Grid.Column="0" HorizontalAlignment="Right" ResizeDirection="Columns" Width="3" Background="Black" Opacity="0.1"/>
  <GridSplitter Grid.Column="1" HorizontalAlignment="Right" ResizeDirection="Columns" Width="3" Background="Black" Opacity="0.1" ResizeBehavior="PreviousAndNext"/>
</Grid>
<TreeView Margin="0,20,0,18" BorderThickness="0" ItemsSource="{Binding Children}"/>

Потом я задал ItemTemplate у дерева, так чтобы в нём было три TextBlock-а, привязанных к колонкам:

<HierarchicalDataTemplate ItemsSource="{Binding Children}">
  <DockPanel LastChildFill="True">
    <TextBlock Text="{Binding Property3}" Width="{Binding ActualWidth, ElementName=column3}" DockPanel.Dock="Right" Margin="3"/>
    <TextBlock Text="{Binding Property2}" Width="{Binding ActualWidth, ElementName=column2}" DockPanel.Dock="Right" Margin="3"/>
    <TextBlock Text="{Binding Property1}" Width="Auto" HorizontalAlignment="Stretch" Margin="3"/>
  </DockPanel>
</HierarchicalDataTemplate>

К сожалению, стандартный шаблон TreeViewItem не позволяет ему растягиваться на всю ширину TreeView (по крайне мере я не нашёл способа сделать это). Пришлось скопировать стиль и слегка его подправить:

<Style TargetType="TreeViewItem">
  <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type TreeViewItem}">
        <Grid>
          <Grid.Resources>
            <PathGeometry x:Key="TreeArrow">
              <PathGeometry.Figures>
                <PathFigureCollection>
                  <PathFigure IsFilled="True" StartPoint="0 0" IsClosed="True">
                    <PathFigure.Segments>
                      <PathSegmentCollection>
                        <LineSegment Point="0 6"/>
                        <LineSegment Point="6 0"/>
                      </PathSegmentCollection>
                    </PathFigure.Segments>
                  </PathFigure>
                </PathFigureCollection>
              </PathGeometry.Figures>
            </PathGeometry>
            <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
              <Setter Property="Focusable" Value="False"/>
              <Setter Property="Width" Value="16"/>
              <Setter Property="Height" Value="16"/>
              <Setter Property="Template">
                <Setter.Value>
                  <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <Border Width="16" Height="16" Background="Transparent" Padding="5,5,5,5">
                      <Path x:Name="ExpandPath" Fill="Transparent" Stroke="#FF989898" Data="{StaticResource TreeArrow}">
                        <Path.RenderTransform>
                          <RotateTransform Angle="135" CenterX="3" CenterY="3"/>
                        </Path.RenderTransform>
                      </Path>
                    </Border>
                    <ControlTemplate.Triggers>
                      <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="ExpandPath" Property="Stroke" Value="#FF1BBBFA"/>
                        <Setter TargetName="ExpandPath" Property="Fill" Value="Transparent"/>
                      </Trigger>

                      <Trigger Property="IsChecked" Value="True">
                        <Setter TargetName="ExpandPath" Property="RenderTransform">
                          <Setter.Value>
                            <RotateTransform Angle="180" CenterX="3" CenterY="3"/>
                          </Setter.Value>
                        </Setter>
                        <Setter TargetName="ExpandPath" Property="Fill" Value="#FF595959"/>
                        <Setter TargetName="ExpandPath" Property="Stroke" Value="#FF262626"/>
                      </Trigger>
                    </ControlTemplate.Triggers>
                  </ControlTemplate>
                </Setter.Value>
              </Setter>
            </Style>
          </Grid.Resources>
          <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="19" Width="Auto"/>
            <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <ToggleButton x:Name="Expander" Style="{StaticResource ExpandCollapseToggleStyle}" IsChecked="{Binding Path=IsExpanded,RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press"/>
          <Border Name="Bd" Grid.Column="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
            <ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
          </Border>
          <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"/>
        </Grid> 
        <ControlTemplate.Triggers>
          <Trigger Property="IsExpanded" Value="false">
            <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
          </Trigger>
          <Trigger Property="HasItems" Value="false">
            <Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
          </Trigger>
          <Trigger Property="IsSelected" Value="true">
            <Setter TargetName="Bd" Property="Background" 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 TargetName="Bd" Property="Background" 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>

Скачать исходники:


PS. В данном примере есть проблема: при появлении вертикальной полосы прокрутки колонки в дереве сдвигаются относительно заголовка. Поправить это можно несколькими способами. Например, сделать полосу прокрутки всегда видимой и изначально выделить для неё место. Либо добавить последнюю колонку, по ширине равной полосе прокрутки, и привязать её видимость к видимости полосы прокрутки.

КОММЕНТАРИИ


НОВЫЙ КОММЕНТАРИЙ


*жирный*
_курсив_
+подчеркнутый+
! заголовок 1
!! заголовок 2
* список
** список 2
# нумерованый список
## нумерованый список 2
[url:http://www.example.com]
{"без форматирования"}
Полное описание синтаксиса