Дерево с несколькими колонками
17 января 2012 - 21:03
Стандартный TreeView в WPF, казалось бы, не позволяет сделать многоколоночное дерево. Но, как оказалось на практике, немного пошаманив с DataTemplate и Binding, нужного результата достигнуть не сложно.
Сначала я создал модель элемента дерева содержащую три текстовых поля и коллекцию потомков:
Затем создал основную ViewModel для дерева:
Потом создал окно, в котором будет располагаться трёх-колоночный Grid - он будет играть роль заголовка нашего дерева и собственно сам TreeView:
Потом я задал ItemTemplate у дерева, так чтобы в нём было три TextBlock-а, привязанных к колонкам:
К сожалению, стандартный шаблон TreeViewItem не позволяет ему растягиваться на всю ширину TreeView (по крайне мере я не нашёл способа сделать это). Пришлось скопировать стиль и слегка его подправить:
Скачать исходники:
PS. В данном примере есть проблема: при появлении вертикальной полосы прокрутки колонки в дереве сдвигаются относительно заголовка. Поправить это можно несколькими способами. Например, сделать полосу прокрутки всегда видимой и изначально выделить для неё место. Либо добавить последнюю колонку, по ширине равной полосе прокрутки, и привязать её видимость к видимости полосы прокрутки.
Сначала я создал модель элемента дерева содержащую три текстовых поля и коллекцию потомков:
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. В данном примере есть проблема: при появлении вертикальной полосы прокрутки колонки в дереве сдвигаются относительно заголовка. Поправить это можно несколькими способами. Например, сделать полосу прокрутки всегда видимой и изначально выделить для неё место. Либо добавить последнюю колонку, по ширине равной полосе прокрутки, и привязать её видимость к видимости полосы прокрутки.