Donnerstag, 16. Februar 2012

Custom TreeView Style

In this Post I would like to show how the basic Style of a TreeView (or better: TeeViewItem) can be changed to get an unique Style like this one:


When you like to design something like this in a Style, you'll need to modify the Template of TreeViewItem via Style.

The following shows the basic TreeViewItem-Style you will get when you want to change it using Expression Blend:


  <Style x:Key="TreeViewItemFocusVisual">
      <Setter Property="Control.Template">
        <Setter.Value>
          <ControlTemplate>            <Rectangle/>          </ControlTemplate>        </Setter.Value>      </Setter>    </Style> 
    <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>

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

The marked section shows us, how a basic TreeViewItem is designed:


We will change it to this layout:


The red section will look like this:


The Style then looks like this:

    <Style x:Key="StretchedTreeViewItemWithLinesStyle" TargetType="{x:Type TreeViewItem}">
      <Setter Property="AlternationCount" Value="{Binding RelativeSource={RelativeSource Self}, Path=Items.Count}"/>
      <Setter Property="Background" Value="Transparent"/>
      <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="0,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="15" Width="Auto"/>
                <ColumnDefinition MinWidth="19" Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
              </Grid.ColumnDefinitions>
              <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
              </Grid.RowDefinitions>
              <Grid Grid.Column="0" Grid.ColumnSpan="2">
                <Grid.RowDefinitions>
                  <RowDefinition Height="*"/>
                  <RowDefinition Height="Auto"/>
                  <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <Rectangle Grid.Row="0"
                           Width="3"
                           Fill="Green"
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Stretch" HorizontalAlignment="Left"
                           Margin="5,0,0,0"/>
                <Rectangle Grid.Row="1"
                           Height="3"
                           Fill="Green"
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="Left">
                  <Rectangle.Style>
                    <Style TargetType="Rectangle">
                      <Setter Property="Fill" Value="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=LinienFarbe}"/>
                      <Setter Property="Margin" Value="5,0,15,0"/>
                      <Setter Property="Width" Value="13"/>
                      <Style.Triggers>
                        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=HasItems}" Value="False">
                          <Setter Property="Margin" Value="5,0,0,0"></Setter>
                          <Setter Property="Width" Value="25"/>
                        </DataTrigger>
                      </Style.Triggers>
                    </Style>
                  </Rectangle.Style>
                </Rectangle>
              </Grid>
              <Border Grid.Column="0"
                      Grid.ColumnSpan="2"
                      Grid.RowSpan="2">
                <Rectangle Grid.Row="2"
                           Width="3"
                           SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                           VerticalAlignment="Stretch"
                           HorizontalAlignment="Left"
                           Margin="5,0,0,0"
                           Fill="Green">
                  <Rectangle.Style>
                    <Style TargetType="Rectangle">
                      <Style.Triggers>
                        <DataTrigger Value="True">
                          <DataTrigger.Binding>
                            <MultiBinding Converter="{StaticResource MyAlternationEqualityConverter}">
                              <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}, AncestorLevel=2}" Path="Items.Count" />
                              <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}" Path="(ItemsControl.AlternationIndex)" />
                            </MultiBinding>
                          </DataTrigger.Binding>
                          <Setter Property="Visibility" Value="Collapsed"/>
                        </DataTrigger>
                      </Style.Triggers>
                    </Style>
                  </Rectangle.Style>
                </Rectangle>
              </Border>
              <ToggleButton Grid.Column="1" x:Name="Expander" Style="{StaticResource ExpandCollapseToggleStyle}" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"/>
              <Border x:Name="Bd" SnapsToDevicePixels="true" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" Grid.Column="2">
                <ContentPresenter x:Name="PART_Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" ContentSource="Header"/>
              </Border>
              <ItemsPresenter x:Name="ItemsHost" HorizontalAlignment="Stretch" Grid.Column="2" Grid.ColumnSpan="2" 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 HorizontalAlignment="Stretch"/>
              </ItemsPanelTemplate>
            </Setter.Value>
          </Setter>
        </Trigger>
      </Style.Triggers>
    </Style>

The important parts are highlighted.

One note to the changes:

The last item has to know, that it is the last one. I found a solution based on this link: http://stackoverflow.com/questions/7834987/distinct-item-template-for-first-and-last-item-in-a-listview

By setting

AlternationCount
and implementing this

  class MyAlternationEqualityConverter : IMultiValueConverter
  {
    #region Implementation of IMultiValueConverter
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
      if (values != null && values.Length == 2 &&
          values[0] is int && values[1] is int)
      {
        bool retval = Equals((int)values[0], (int)values[1] + 1);
        return retval;
      }
      return DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
      throw new NotSupportedException();
    }
    #endregion
  }
<DataTrigger Value="True">
  <DataTrigger.Binding>
    <MultiBinding Converter="{StaticResource MyAlternationEqualityConverter}">
      <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}, AncestorLevel=2}" Path="Items.Count" />
      <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}" Path="(ItemsControl.AlternationIndex)" />
    </MultiBinding>
  </DataTrigger.Binding>
  <Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>

we can show or hide the down-going rectangle.


To get the sample shown on top this code does the work:


<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication3"
        Title="MainWindow" Height="200" Width="1900">
  <Window.Resources>
    <local:MyAlternationEqualityConverter x:Key="MyAlternationEqualityConverter" />

    <Style x:Key="TreeViewItemFocusVisual">
...
    </Style>

    <PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>

    <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
...
    </Style>

    <Style x:Key="StretchedTreeViewItemWithLinesStyle" TargetType="{x:Type TreeViewItem}">
...
    </Style>
  </Window.Resources>

  <TreeView AlternationCount="{Binding RelativeSource={RelativeSource Self}, Path=Items.Count}">
    <TreeViewItem Header="Item2" Style="{StaticResource StretchedTreeViewItemWithLinesStyle}">
      <TreeViewItem Header="SubItem1" Style="{StaticResource StretchedTreeViewItemWithLinesStyle}"/>
      <TreeViewItem Header="SubItem2" Style="{StaticResource StretchedTreeViewItemWithLinesStyle}"/>
    </TreeViewItem>
    <TreeViewItem Header="Item3" Style="{StaticResource StretchedTreeViewItemWithLinesStyle}"/>
  </TreeView>
</Window>

And that's it :-)

Have fun,

Matthias