Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Graph Tree Custom Layout Style for WPF TreeView Control

0.00/5 (No votes)
23 May 2011 2  
Shows how to use a WPF TreeView control to draw a graph style hierarchy with connecting lines.

Sample Image

Introduction

This article shows how to use a WPF Style and a little C# implementing a couple of Converters so the WPF TreeView control can be used to display a connected graph tree style diagram.

The interesting point is that whilst re-styling the TreeView and the contents it displays is relatively easy, trying to decorate the tree structure is harder.

Background

I had a data hierarchy that I wanted to display as a connected graph, i.e., a set of nodes with connecting lines between parent and child, along with nodes at the same depth being displayed adjacently.

The default TreeView control style does not provide this. In fact, it doesn’t provide any sort of decoration, i.e., connecting lines.

However, one important aspect of the TreeView control is that it works with the HierarchicalDataTemplate. This is effectively an adapter that describes to the TreeView control how a hierarchical data structure can be traversed. This means any such data structure can be visualized by a TreeView control.

My first attempt at trying to draw the hierarchy as desired was based around creating a custom panel of which the data structure was assigned to the panel’s ItemSource property, and the combination of MeasureOverride(), ArrangeOverride(), and OnRender() were used to perform the layout and draw the connecting lines. This worked pretty well, but a Panel can only work with the standard DataTemplate which meant an alternative mechanism was needed to describe the hierarchical data structure. This seemed like re-inventing the wheel though it worked.

My next approach was to extend this to become a custom control, and the implementation for binding to ItemSource required that the implementation support the HierarchicalDataTemplate taking into account both Path and XPath. However, whilst interesting to implement, again it also seemed like re-inventing the wheel especially as the Control Template mechanism is expressly designed to allow a completely different rendering of an existing control. This re-galvanized my attempt at tackling this problem the WPF way. In particular, a post showing how to add connecting lines to the default TreeView control style generated further impetus.

How it works

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:BlogSample">
    
  <local:HorzLineConv x:Key="horzLineConv"/>
  <local:VertLineConv x:Key="vertLineConv"/>

  <Style TargetType="TreeViewItem" x:Key="GraphStyle">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="TreeViewItem">

          <Grid> <!-- Main Grid-->
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto"/>
              <!-- Horizontal line-->
              <RowDefinition Height="Auto"/>
              <!--The top row contains the item's content.-->
              <RowDefinition Height="Auto" />
              <!-- Item presenter(children) -->
            </Grid.RowDefinitions>

            ...
            
          </Grid> <!-- End of Main grid -->
        </ControlTemplate>
      </Setter.Value>
    </Setter>

    <Setter Property="ItemsPanel">
      <Setter.Value>
        <ItemsPanelTemplate>
          <StackPanel
            HorizontalAlignment="Center" 
            IsItemsHost="True" 
            Orientation="Horizontal"/>
        </ItemsPanelTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

The code fragment above is taken from GraphStyle.xaml.

The style definition resides in a Resource Dictionary. This is done this way to partition the code. The first thing to note is the addition of two converters as static resources. The implementation couldn’t be done completely in XAML. We’ll come back to these later.

The control is composed of a main grid made up of 1 column and 3 rows. The rows are all of a Height set to Auto so they effectively size to content.

Row 0 contains the horizontal line that the parent and child node’s vertical connecting lines join to. Actually, this row contains a grid made of two columns and a single row and within them the line, but we’ll come to that later.

Row 1 contains another grid, this time a single column with three rows of which the top and bottom rows contain the vertical connecting lines and the middle row contains the actual node content.

Finally, row 2 which doesn't contain a grid. Instead, it’s the ItemPresenter control that contains the Panel that hosts the current node’s children.

The following diagram may help:

Sample Image

Drawing the item header

<Grid Grid.Row="1"> <!-- Header grid -->
  <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/><!-- Vert. line above node    -->
      <RowDefinition Height="*"/>   <!-- Header -->
      <RowDefinition Height="Auto"/><!-- Vert line below node    -->
  </Grid.RowDefinitions>
  <!-- Vertical line above node -->
  <Rectangle Grid.Row="0" 
              Height="10" Stroke="Black" SnapsToDevicePixels="true">
    <Rectangle.Width>
      <Binding Mode="OneWay" 
        Converter="{StaticResource vertLineConv}" 
        ConverterParameter="top" 
        RelativeSource=
          "{RelativeSource AncestorLevel=1, 
                            AncestorType={x:Type TreeViewItem}}"/>
    </Rectangle.Width>
  </Rectangle>

  <!-- Header -->
  <ContentPresenter Grid.Row="1" ContentSource="Header" 
    HorizontalAlignment="Center" VerticalAlignment="Center"/>

  <!-- Vertical line below node -->
  <Rectangle Grid.Row="2"  Height="10" Stroke="Black" 
    SnapsToDevicePixels="true">
    <Rectangle.Width>
      <Binding Mode="OneWay" 
        Converter="{StaticResource vertLineConv}" 
        ConverterParameter="bottom" 
        RelativeSource=
          "{RelativeSource  AncestorLevel=1, 
                            AncestorType={x:Type TreeViewItem}}"/>
    </Rectangle.Width>
  </Rectangle>
</Grid> <!-- End of Header grid -->

The code fragment above shows the second row of the main grid. This needs to display the actual TreeViewItem Header, i.e., the content of the node as this level, along with the vertical connecting lines above and below the content. These need to be horizontally centered to the content.

This naturally splits into a grid with a single column containing three rows. The middle row’s Height is set to ‘*’ so it uses all the available space after the vertical connecting lines have been drawn. The rows for the vertical lines occupy a space governed by the height of the line which is hard-coded. The actual content is rendered by using a ContentPresenter in the middle row.

For the lines, a rectangle with a height of 10 is specified though a line is not always required. If the node is the root node, then it should have no top connecting line and/or if the node has no children, then it should have no bottom connecting line. This is what the binding takes care of. It essentially binds the Width property of the rectangle to a converter that determines if the specified connecting line is needed. If it is, a value of 1 is returned, otherwise 0. A width of 0 causes the rectangle to be of no size and thus is not drawn.

Drawing the horizontal line

<Grid Grid.Row="0"> <!-- Horizontal line grid -->
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <!-- Horizontal line to the left -->
    <Rectangle  Grid.Column="0" HorizontalAlignment="Stretch"
                Stroke="Black" SnapsToDevicePixels="true">
    <Rectangle.Height>
        <Binding  Mode="OneWay"
                Converter="{StaticResource horzLineConv}"
                ConverterParameter="left" 
                RelativeSource="{RelativeSource
                    AncestorLevel=1,
                    AncestorType={x:Type TreeViewItem}}"/>
    </Rectangle.Height>
    </Rectangle>

    <!-- Horizontal line to the right -->
    <Rectangle Grid.Column="1" HorizontalAlignment="Stretch" 
                Stroke="Black"
                SnapsToDevicePixels="true">
    <Rectangle.Height>
        <Binding Mode="OneWay" 
        Converter="{StaticResource horzLineConv}" 
        ConverterParameter="right" 
        RelativeSource="{RelativeSource 
            AncestorLevel=1, 
            AncestorType={x:Type TreeViewItem}}"/>
    </Rectangle.Height>
    </Rectangle>
</Grid> <!-- End of Horizontal line grid -->

Drawing a horizontal line the width of the container that holds the children is relatively easy. The required width of the line is dictated by the width of the panel containing the children. In fact, the width of the column (that contains the content grid) shown in yellow is determined by the width of the panel shown in red. This is because the column in red contains the panel that holds the children. As such, the horizontal line could have been drawn by adding another row and drawing a line within it.

However, this line would span the width of all the children and there are three cases where this is undesirable. Firstly, if a node has only a single child, in which case no horizontal line is required at all. Secondly, the left most node only requires a horizontal line above its right half running from its centre to the right edge. Conversely, the right most node has the same issue but needs a line running from its centre to the left edge.

The solution is to not have the parent node draw the horizontal line. Instead, a child node draws its portion of the line above itself which joins to form a continuous line.

Rather than a single line, the row contains a grid consisting of a single row with two columns. This is shown in light blue. The two columns divide the space equally, which means each column represents half the line centered on the item, just what's required! If the node has siblings both sides, then a line is drawn in both columns, creating a single line spanning the entire node.

For each node, a converter is used to determine if the node is left-most or right-most. If so, then either the left or right portion of the line is not drawn. The same technique for not drawing the top and bottom connecting lines is used here, i.e., a Rectangle, but instead of setting its Width to 0, this time the Height is set to 0. It is important to not interfere with the Width calculation as the Rectangle has its Width set to that of the column which is taken from the size of the item it's above, and so forth.

The Rectangle Shape is really a very useful Shape for rendering optional lines tied to the implicit dynamic size of a control.

Why the converters?

public class HorzLineConv : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, 
                        System.Globalization.CultureInfo culture)
  {
    TreeViewItem item = (TreeViewItem)value;
    ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
    int index = ic.ItemContainerGenerator.IndexFromContainer(item);

    if ((string)parameter == "left")
    {
      // Either left most or single item
      if (index == 0)    
        return (int)0;
      else
        return (int)1;
    }
    else // assume "right"
    {
      // Either right most or single item
      if (index == ic.Items.Count - 1)    
        return (int)0;
      else
        return (int)1;
    }
  }
}

public class VertLineConv : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, 
                            System.Globalization.CultureInfo culture)
  {
    TreeViewItem item = (TreeViewItem)value;
    ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
    int index = ic.ItemContainerGenerator.IndexFromContainer(item);

    if ((string)parameter == "top")
    {
      if (ic is TreeView)
        return 0;
      else
      return 1;
    }
    else // assume "bottom"
    {
      if (item.HasItems == false)
        return 0;
      else
        return 1;
    }
  }
}

Unfortunately, the implementation was not XAML alone. In order to determine whether the top and bottom connecting lines should be drawn, and also whether a node was the left-most or right-most, the knowledge of the tree structure was needed. This required recourse to a Converter. These are defined in GraphStyle.xaml.cs.

The basis for this approach owes much to this post which shows how to obtain the TreeViewItem being displayed.

There are two Converter classes: HorzLineConv which determines whether each half segment of a node's horizontal line should be drawn, and VertLineConv which determines whether the top or bottom connecting lines should be drawn.

These work and are used in much the same way. A binding is established between a Rectangle’s Height and the Converter for the horizontal lines and a Rectangle’s Width for the vertical lines. Each binding passes a string as the ConverterParamater indicating which connecting line it’s interested in, i.e., top, bottom, left, or right. The source of the binding is the TreeViewItem being displayed.

There is an interesting case for VertLineConv in determining whether the top connecting line should be shown. This is true for all but the root node. The easiest way to do this was check the current item’s parent control as only the root item is owned by the TreeView as opposed to another TreeViewItem.

Putting it all together

<Window x:Class="BlogSample.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="GraphStyle.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <XmlDataProvider x:Key="nodes" Source=".\nodes.xml" XPath="Node"/>

    <HierarchicalDataTemplate DataType="Node" 
      ItemsSource="{Binding XPath=Children/Node}">

      <Border Name="bdr" CornerRadius="360" BorderThickness="3" 
        BorderBrush="Blue" Width="Auto" Height="50" MinWidth="50">
        <TextBlock Text="{Binding XPath=@Name}" 
          HorizontalAlignment="Center" 
          VerticalAlignment="Center"/>
      </Border>
    </HierarchicalDataTemplate>
  </ResourceDictionary>
</Window.Resources>
    
<TreeView ItemContainerStyle="{StaticResource GraphStyle}" 
  ItemsSource="{Binding Source={StaticResource nodes}}"/>
</Window>

The program to demonstrate the style is straightforward. It just merges the ResourceDicitionary that contains the style into the Window's resources. In addition, it turns a simple hierarchical XML structure into source that can be consumed by a TreeView control. A HierarchicalDataTemplate is also defined that describes how to expand it and also provides a basic rendering as an ellipse for each node, with the node's name centered within it.

Finally, the Window has a single child of type TreeView control whose source is bound to the XML data provider. Its style is set to the one we've been looking at. This needs to be specified as the style itself was given a key, which means it will not be applied automatically based on the type of the control it's a style for.

When run, it displays the window pictured at the top.

References

This is most definitely not all my work. In particular, the following articles and postings were instrumental in allowing me to get this far.

Firstly, those from Josh Smith provided the examples of how to re-style the TreeView control.

Secondly, this post was where the idea of using a Rectangle for the connecting lines and using a grid in order to cause the Rectangle to be drawn to the correct dimensions came from. This is also where the mechanism of binding to the actual TreeViewItem came from.

What next?

There are at least a couple of issues with this style that I haven't investigated yet. I think a TreeView can handle multiple root nodes whereas the style really only accommodates one. Sometimes the vertical line of the left-most or right-most node doesn't meet perfectly with the horizontal line. This can change for the same graph depending on the size of the window, so I suspect it's something to do with SnapsToDevicePixels.

I've changed the HierarchicalDataTemplate so it renders the nodes as buttons, and also changed the data source to traverse the file-system, and this all works.

Another interesting enhancement would be to place each node in a Expander so traversal of the hierarchy could be controlled as per a conventionally styled TreeView control.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here