Click here to Skip to main content
Click here to Skip to main content

A Spider type control tree thingy for WPF

, , 20 Sep 2008
Rate this:
Please Sign up or sign in to vote.
A Spider type control tree thingy for WPF.

Introduction

I recently started a new job where I am employed as a WPF developer. When I arrived, the guys there gave me a brief that was to make a cool app, and they really liked the look and feel of the FamilyShow exemplar by Vertigo. Which I also love, that and Tangerine by Infragistics are my favourite WPF demos.

What I liked in both where the fluid movements, and the diagramming approach used in the FamilyShow exemplar particularly. The guys where I just started working asked me how hard it would be to create something like the diagramming component seen in the FamilyShow exemplar. So without further ado, I contacted my favourite partner in weird WPF briefs, Mr. Fredrik Bornander, who I love working with on these stranger ideas. We seem to manage to do a reasonable job together, at least I think anyway.

This article will describe a tree like diagram component that we have nicknamed the "SpiderControl".

Here is a screenshot just to wet your appetite:

The rest of this article will describe how we went about building this little control:

What Does it Do

The following is a list of what the control actually does:

  • Uses a specialized ScrollViewer which allows the user to use the mouse to create a friction enabled drag operation (this is pretty cool actually)
  • Only shows three layers of the tree maximum to keep it clean
  • Current node selected is centered within the available area
  • Node collapse/expand buttons are automatically enabled dependant on the number of children the current node has

How is it Made

So now onto the nitty gritty, which is the part you are probably wanting to read anyhow.

So first, let's just have a quick look at the basic structure.

I'll split this into two diagrams for no other reason than I couldn't figure out how to get Word's SmartArt to add more levels to its standard SmartArt diagrams, curse technology.

It can be seen from the above diagram that the HostWindow holds an instance of a DragViewer. So the window's code is simply the following:

<Window x:Class="SpiderTreeControl.HostWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram"  
      WindowStartupLocation="CenterScreen"
      Title="HostWindow" Height="400" Width="400">
    <Grid>
        <diagram:DragViewer x:Name="dragViewer" 
                            Width="auto" Height="auto" 
                            Margin="0"/>
    </Grid>
</Window>

And then, we focus our attention to the actual DragViewer, where we wrap a DiagramViewer within a FrictionScrollViewer.

The FrictionScrollViewer is a specialized ScrollViewer that acts using friction to create nice fluid drag operations. I talk more about this on an older blog entry of mine, which you can read here: http://sachabarber.net/?p=225.

<UserControl x:Class="SpiderTreeControl.Diagram.DragViewer"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram;assembly="                
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Height="auto" Width="auto">
    <diagram:FrictionScrollViewer x:Name="sv" 
            Style="{StaticResource ScrollViewerStyle}">
        <diagram:DiagramViewer x:Name="diagramViewer" 
            Margin="0" Width="2000" Height="2000"/>
    </diagram:FrictionScrollViewer>
</UserControl>

The Style you see for the scrollbar is achieved using some Styles which are located within the AppStyles.xaml ResourceDictionary. This is what gives the ScrollViewer its appearance as shown below:

But all of that is simple eye candy, we need to get to the nuts and bolts.

DragViewer Class

Going back to the DragViewer, which holds an instance of the DiagramViewer. In the code-behind, the DragViewer is responsible for setting up the nodes collection that is used for its embedded DiagramViewer.

This is done in code as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace SpiderTreeControl.Diagram
{
    /// <summary>
    /// Interaction logic for DragViewer.xaml
    /// </summary>
    public partial class DragViewer : UserControl
    {

        public DragViewer()
        {
            InitializeComponent();
            this.Loaded+=delegate
            {
                LoadDiagramNodes();
            };
        }

        public void LoadDiagramNodes()
        {

            DiagramNode root = new DiagramNode("Root", null, 
              "../Images/DiagramRootNode.png", "Dummy1View", 
              "this is the root node");

            DiagramNode a = new DiagramNode("A", root, 
              "../Images/DiagramNode.png", "Dummy1View", 
              "this is node A");
            DiagramNode b = new DiagramNode("B", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node B");
            DiagramNode c = new DiagramNode("C", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node C");
            DiagramNode d = new DiagramNode("D", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node D");
            DiagramNode e = new DiagramNode("E", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node E");
            DiagramNode f = new DiagramNode("F", root, 
              "../Images/DiagramNode.png", "Dummy1View", "this is node F");

            diagramViewer.RootNode = root;
            diagramViewer.FrictionScrollViewer = this.sv;
        }
    }
}

Where individual DiagramNode objects are created, and the relationship between them are established by passing the relevant DiagramNode in as a constructor parameter to another DiagramNode. Obviously, in the case of the root DiagramNode, this value is null. The last thing that is done is that the embedded DiagramViewer has its RootNode property set to the root DiagramNode. There is also a requirement to set the embedded DiagramViewer's FrictionScrollViewer property, such that the DiagramViewers layout algorithm can react to new ScrollViewer positions should the user move the ScrollViewer or drag the diagram.

DiagramViewer Class

Is where all the layout of the contained DiagramNodes occurs. This is a simple user control that contains a single TreeCanvas that holds the actual collection of DiagramNodes, and draws the lines between them, which is discussed below. The DiagramViewer also listens to events from the DiagramNodes such as Selected/Collapsed/Expanded, where it will perform the layout based on the node selection.

The DiagramViewer uses a radial algorithm to lay out the collection of child nodes around a parent node. This is done using standard trigonometry math. The basic idea is that each DiagramNode is given a bounding circle to ensure that all nodes have a uniform size, and then an angle between the nodes is calculated.

This process is done in NodeExpanded(), as shown below:

private void NodeExpanded(DiagramNode sender, RoutedEventArgs eventArguments)
{
    rootNode.Location = new Point(
        (double)GetValue(Canvas.ActualWidthProperty) / 2.0,
        (double)GetValue(Canvas.ActualHeightProperty) / 2.0);


    MakeChildrenVisible(sender);

    if (sender.DiagramParent != null)
    {
        sender.DiagramParent.Visibility = Visibility.Visible;
        foreach (DiagramNode sibling in sender.DiagramParent.DiagramChildren)
        {
            if (sibling != sender)
                sibling.Visibility = Visibility.Collapsed;
        }
        if (sender.DiagramParent.DiagramParent != null)
            sender.DiagramParent.DiagramParent.Visibility = Visibility.Collapsed;
    }

    if (sender.DiagramChildren.Count > 0)
    {
        double startAngle = CalculateStartAngle(sender);
        double angleBetweenChildren = (sender == rootNode ? Math.PI * 2.0 : Math.PI) / 
                    ((double)sender.DiagramChildren.Count - 0);

        double legDistance = CalculateLegDistance(sender, angleBetweenChildren);

        for (int i = 0; i < sender.DiagramChildren.Count; ++i)
        {
            DiagramNode child = sender.DiagramChildren[i];
            child.Selected += new NodeStateChangedHandler(NodeSelected);
            child.Expanded += new NodeStateChangedHandler(NodeExpanded);
            child.Collapsed += new NodeStateChangedHandler(NodeCollapsed);

            Point parentLocation = sender.Location;

            child.Location = new Point(
                parentLocation.X + Math.Cos(startAngle + 
                   angleBetweenChildren * (double)i) * legDistance,
                parentLocation.Y + Math.Sin(startAngle + 
                  angleBetweenChildren * (double)i) * legDistance);

            foreach (DiagramNode childsChild in child.DiagramChildren)
            {
                childsChild.Visibility = Visibility.Collapsed;
            }
        }
    }

    BaseCanvas.InvalidateArrange();
    BaseCanvas.UpdateLayout();
    BaseCanvas.InvalidateVisual();
}

The above process also relies on another process, which is the process which works out the leg distances (the length of the line to draw from one node to its child) between nodes.

This is as shown below:

private static double CalculateLegDistance(DiagramNode sender, double angleBetweenChildren)
{
    double legDistance = 1.0;
    double childToChildMinDistance = 1.0;
    foreach (DiagramNode child in sender.DiagramChildren)
    {
        legDistance = Math.Max(legDistance, 
            sender.BoundingCircle + child.BoundingCircle);
        foreach (DiagramNode otherChild in sender.DiagramChildren)
        {
            if (otherChild != child)
            {
                childToChildMinDistance = 
                Math.Max(childToChildMinDistance, 
                child.BoundingCircle + otherChild.BoundingCircle);
            }
        }
    }

    legDistance = Math.Max(
        legDistance,
        (childToChildMinDistance / 2.0) / Math.Sin(angleBetweenChildren / 2.0));
    return legDistance;
}

TreeCanvas Class

This is a specialized Canvas control that simply draws the lines between all the DiagramNodes, currently shown within the DiagramViewer. This is done by overriding the OnRender() method of the Canvas control. This is shown below:

protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    base.OnRender(dc);
    foreach (UIElement uiElement in Children)
    {
        if (uiElement is DiagramNode)
        {
            DiagramNode node = (DiagramNode)uiElement;

            if (node.Visibility == Visibility.Visible)
            {
                if (node.DiagramParent != null && 
                    node.DiagramParent.Visibility == Visibility.Visible)
                {
                    dc.DrawLine(new Pen(Brushes.Black, 2.0), 
                        node.Location, node.DiagramParent.Location);
                }
            }
        }
    }
}

DiagramNode Class

This is a pretty standard WPF UserControl that represents a single DiagramNode within the DiagramViewer. It's pretty standard stuff really, a few buttons for the expanded/collapsed and selected states. Let's see the XAML for it, shall we?

<UserControl x:Class="SpiderTreeControl.Diagram."
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram"  
        Height="80" Width="90" 
        Background="Transparent"
        BorderBrush="Transparent">

    <UserControl.CommandBindings>
        <CommandBinding Command="{x:Static diagram:DiagramNode.expandCommand}" 
            CanExecute="ExpandCommand_CanExecute"
            Executed="ExpandCommand_Executed"/>

        <CommandBinding Command="{x:Static diagram:DiagramNode.collapseCommand}" 
            CanExecute="CollapseCommand_CanExecute"
            Executed="CollapseCommand_Executed"/>

    </UserControl.CommandBindings>

    <UserControl.Resources>

        <ControlTemplate x:Key="expandCollapseButton" TargetType="{x:Type Button}">
            <Grid Width="20" Height="20" Background="Transparent">
                <Ellipse Width="20" Height="20" Fill="DarkGray"/>
                <Ellipse Width="16" Height="16" Fill="WhiteSmoke"/>
                
                <Label x:Name="lbl" Content="{TemplateBinding Content}" 
                       HorizontalAlignment="Center" VerticalAlignment="Center"
                       Background="Transparent"
                       FontFamily="Arial Black" FontSize="10"/>
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="IsEnabled" Value="false">
                    <Setter TargetName="lbl" 
                      Property="Foreground" Value="DarkGray"/>
                </Trigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="BitmapEffect">
                        <Setter.Value>
                            <DropShadowBitmapEffect />
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

    </UserControl.Resources>
    
    
    <Canvas Background="Transparent" 
                   Width="90" Height="80" >
        <Label x:Name="NodeName" 
           Content="Name" FontFamily="Arial Black" 
           FontSize="14" Canvas.ZIndex="1"/>
        
        <Button x:Name="btnNavigate" 
                Template="{StaticResource simpleImageButtonTemplate}"
                Click="btnNavigate_Click" Width="60" Height="60" 
                Canvas.Left="20" Canvas.Top="20"
                Canvas.ZIndex="0"/>
        
        <Button x:Name="ExpandButton" Content="+" 
                Canvas.Top="30" Canvas.Left="0"
                Template="{StaticResource expandCollapseButton}"
                Command="{x:Static diagram:DiagramNode.expandCommand}" />


        <Button x:Name="CollapseButton" Content="-" 
                Canvas.Top="55" Canvas.Left="0"
                Template="{StaticResource expandCollapseButton}"
                Command="{x:Static diagram:DiagramNode.collapseCommand}" />
    </Canvas>
</UserControl>

Each DiagramNode looks like the following:

Known Issues

There is only one known issue, but this is actually the case with pretty much every diagramming solution I have seen within WPF, including the FamilyShow exemplar by Vertigo, and lots of others which I had to evaluate in my last job. Basically, in order to dynamically position diagram nodes, you have to use a container panel that supports X/Y positioning, which means using a Canvas.

Which is fine, but this means you must ensure that the Canvas is big enough to accommodate the largest layout positions that your algorithm demands. Not a huge problem, but it is one that you need to be aware of.

To this end, you will find that within the embedded DiagramViewer within the DragViewer control within the demo application, the DiagramViewer has a fixed size of 2000 by 2000, which is declared as follows:

<UserControl x:Class="SpiderTreeControl.Diagram.DragViewer"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:diagram="clr-namespace:SpiderTreeControl.Diagram;assembly="                
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Height="auto" Width="auto">
    <diagram:FrictionScrollViewer x:Name="sv" 
           Style="{StaticResource ScrollViewerStyle}">
        <diagram:DiagramViewer x:Name="diagramViewer" 
           Margin="0" Width="2000" Height="2000"/>
    </diagram:FrictionScrollViewer>
</UserControl>

Other than this single issue, which as I say you will probably see in practically all diagramming components for WPF, there are no known issues.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Authors

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Fredrik Bornander
Software Developer (Senior)
Sweden Sweden
Article videos
Oakmead Apps Android Games
 
21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
18 Oct 2013: Best VB.NET article of September 2013
23 Jun 2012: Best C++ article of May 2012
20 Apr 2012: Best VB.NET article of March 2012
22 Feb 2010: Best overall article of January 2010
22 Feb 2010: Best C# article of January 2010

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberaaroncampf19-May-11 6:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 21 Sep 2008
Article Copyright 2008 by Sacha Barber, Fredrik Bornander
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid