DEV Community

A A
A A

Posted on

[WPF] Draw Tree Topology

How to draw tree topology in WPF

When expressing data structures with parent-child relationships, we use a tree topology as shown below, but other than TreeView, WPF has no components or libraries for graphical display.
In addition, TreeView becomes more difficult to view as the number of nodes increases and the depth of the tree increases.

In this article, we show how to graphically render the tree topology as follows.
The application displaying the topology is also available on Github below.

https://github.com/wjtj52twa/WpfDrawTreeTopology

Image description

Tree Structure Data

The application reads the json file containing the tree structure data and draws the tree topology in WPF's Canvas, and the json file is “data.json” directly under the executable file.

The json data for the topology displayed in this article uses the following

{
  "Text": "Root",
  "Children": [
    {
      "Text": "Node1",
      "Children": [
        {
          "Text": "Node1-1",
          "Children": []
        },
        {
          "Text": "Node1-2",
          "Children": []
        },
        {
          "Text": "Node1-2",
          "Children": []
        }
      ]
    },
    {
      "Text": "Node2",
      "Children": [
        {
          "Text": "Node2-1",
          "Children": []
        },
        {
          "Text": "Node2-2",
          "Children": [
            {
              "Text": "Node2-2-1",
              "Children": []
            },
            {
              "Text": "Node2-2-2",
              "Children": []
            }
          ]
        },
        {
          "Text": "Node2-2",
          "Children": []
        }
      ]
    },
    {
      "Text": "Node3",
      "Children": []
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The above tree structure is expanded into the “TreeNode” class and referenced in the program.
Parent” is set to the parent node and ‘Children’ to the child nodes, and the structure is such that the nodes are connected to the tree structure.

The “X” and “Y” properties are used to set the coordinates for drawing the tree structure, and the calculation method is described below. There are also other properties and functions for calculation, but the details are omitted.

namespace TreeTopology
{
    public class TreeNode
    {
        public TreeNode? Parent { get; set; } = null;

        public List<TreeNode> Children { get; set; } = new List<TreeNode>();

        public string Text { get; set; } = string.Empty;

        public float X { get; set; }

        public float Y { get; set; }

        public float Mod { get; set; }

        public void SetParentReferences()
        {
            foreach (var child in Children)
            {
                child.Parent = this;
                child.SetParentReferences();
            }
        }

        (Omitted...)
    }
}

Enter fullscreen mode Exit fullscreen mode

Overall processing until the tree structure is drawn

The following is an overview of the process up to drawing the tree topology and the program.
The program is processed when the “Read topology json” button is pressed. The “data.json” file is read and parsed into the TreeNode class using “JsonSerializer”.

  • Reading json file with tree structure defined.
  • Parse json data to create TreeNode class, property sets for parent and child nodes
  • Calculate coordinates of the tree topology
  • Drawing the tree topology
private void ReadTopologyJsonButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // Get the path directly under the executable file
        string filePath = AppDomain.CurrentDomain.BaseDirectory + "data.json";

        // Check file exsited
        if (!File.Exists(filePath))
        {
            Console.WriteLine("File not found: data.json.");
            return;
        }

        // Read file contents
        string jsonData = File.ReadAllText(filePath);

        // Deserialize JSON to TreeNode object
        TreeNode root = JsonSerializer.Deserialize<TreeNode>(jsonData, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        }) ?? throw new InvalidOperationException("Failed to deserialize JSON.");


        bool isHorizontal = HorizontalRadioButton.IsChecked ?? false;
        if (root != null)
        {
            // Set Parent property
            root.SetParentReferences();

            // Calculate coordinates of tree topology
            TreeHelper.CalculateNodePositions(root, isHorizontal);

            // Draw Topology
            DrawTopology.Draw(TopologyCanvas, root, isHorizontal);
        }

    }
    catch (Exception ex)
    {
        // Error
        Console.WriteLine("Exception Error: " + ex.Message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Calculate coordinates of topology

To draw the tree structure, we calculate the coordinate position of each node. The coordinates are calculated according to the following rules to ensure a “clean” display of the tree structure.

  • Tree edges do not overlap with other edges.
  • Nodes at the same depth are placed on the same horizontal line.
  • Trees are drawn as narrow as possible.
  • the parent node is drawn in the middle of its children nodes
  • Sub-trees of the same structure are drawn in the same way, no matter where the sub-trees are located.

According to the above rules, the following processing steps are used to calculate the coordinates. The basic idea is to construct the coordinates and subtrees in order from the terminal leaf node, and then proceed with the coordinate calculation in the direction of the root (parent) while adjusting the position among the subtrees.

  1. process leaf nodes using tree depth-first search (DFS)
    • Start at the lowest level of the tree (leaf node).
    • Initially initialize the leaf nodes to the appropriate coordinates.
    • Y-coordinate is set by the depth of the node.
  2. placement of child nodes
    • For the first child node in a subtree at a leaf node, set the X coordinate to 0.
    • For the second and subsequent child nodes, the X-coordinate shall be the X-coordinate of the neighboring child node + 1
    • Once the placement of a child node in a subtree is complete, the parent node is placed in the middle of the child nodes
  3. eliminate overlap between subtrees
    • Perform a shift operation to ensure proper spacing between each subtree.
    • At this time, the entire subtree, including the child nodes, is shifted in parallel to eliminate overlap.
  4. determination of absolute position
    • Calculate steps 2~3 sequentially starting from the leaf node, and finally calculate the coordinates up to the root node

The following tree structure is created for the final enemy. The coordinates are stored in the “X” and “Y” properties of the TreeNode class. The coordinates can be changed to those of a horizontal tree structure by swapping the “X” and “Y” coordinates.

Image description

Drawing the topology

According to the calculated coordinates of each node, the following code draws a tree topology in Canvas. Draw the nodes as Rectangle and connect the nodes with Lines.

public static class DrawTopology
{
    // Node dimensions
    private static double nodeWidth = 140.0d;
    private static double nodeHeight = 40.0d;

    // Line colors
    private static SolidColorBrush green = Brushes.Green;
    private static SolidColorBrush black = Brushes.Black;

    // Canvas size
    private static double canvasHeight = 0;
    private static double canvasWidth = 0;

    /// <summary>
    /// Draws the tree topology on the canvas.
    /// </summary>
    /// <param name="canvas">The Canvas to draw on.</param>
    /// <param name="root">The root node of the tree.</param>
    /// <param name="isHorizontal">True for horizontal layout; false for vertical layout.</param>
    public static void Draw(this Canvas canvas, TreeNode root, bool isHorizontal = false)
    {
        canvas.Children.Clear();
        canvasHeight = 0;
        canvasWidth = 0;

        if (isHorizontal) canvas.drawHorizontal(root);
        else canvas.drawVertical(root);
    }

    /// <summary>
    /// Creates a line connecting two points.
    /// </summary>
    /// <param name="x1">Start point X-coordinate.</param>
    /// <param name="y1">Start point Y-coordinate.</param>
    /// <param name="x2">End point X-coordinate.</param>
    /// <param name="y2">End point Y-coordinate.</param>
    /// <param name="brush">Color of the line.</param>
    /// <param name="thickness">Thickness of the line.</param>
    /// <returns>A Line object.</returns>
    private static Line createLine(double x1, double y1, double x2, double y2, Brush brush, double thickness)
    {
        Line line = new Line();
        line.X1 = x1;
        line.Y1 = y1;
        line.X2 = x2;
        line.Y2 = y2;
        line.Stroke = brush;
        line.StrokeThickness = thickness;
        return line;
    }

    /// <summary>
    /// Creates a rectangle representing a node.
    /// </summary>
    /// <param name="x">X-coordinate of the rectangle.</param>
    /// <param name="y">Y-coordinate of the rectangle.</param>
    /// <param name="width">Width of the rectangle.</param>
    /// <param name="height">Height of the rectangle.</param>
    /// <param name="stroke">Border color of the rectangle.</param>
    /// <param name="thickness">Border thickness of the rectangle.</param>
    /// <param name="fill">Fill color of the rectangle.</param>
    /// <returns>A Rectangle object.</returns>
    private static Rectangle createRect(double x, double y, double width, double height, Brush stroke, double thickness, Brush fill)
    {
        Rectangle rect = new Rectangle();
        rect.RadiusX = 8d;
        rect.RadiusY = 8d;
        Canvas.SetLeft(rect, x);
        Canvas.SetTop(rect, y);
        rect.Width = width;
        rect.Height = height;
        rect.Stroke = stroke;
        rect.StrokeThickness = thickness;
        rect.Fill = fill;
        return rect;
    }

    /// <summary>
    /// Creates text content to display within a node.
    /// </summary>
    /// <param name="text">The text content.</param>
    /// <param name="fontSize">Font size of the text.</param>
    /// <param name="brush">Text color.</param>
    /// <param name="x">X-coordinate of the text.</param>
    /// <param name="y">Y-coordinate of the text.</param>
    /// <param name="widh">Width of the text container.</param>
    /// <param name="height">Height of the text container.</param>
    /// <param name="hAlign">Horizontal alignment of the text.</param>
    /// <param name="vAlign">Vertical alignment of the text.</param>
    /// <returns>A ContentControl containing the text.</returns>
    private static ContentControl createText(string text, double fontSize, Brush brush, double x, double y, double widh, double height, HorizontalAlignment hAlign, VerticalAlignment vAlign)
    {
        ContentControl content = new ContentControl();
        Canvas.SetLeft(content, x);
        Canvas.SetTop(content, y);
        content.Width = widh;
        content.Height = height;

        TextBlock tb = new TextBlock();
        tb.Text = text;
        tb.FontSize = fontSize;
        tb.Foreground = brush;
        tb.HorizontalAlignment = hAlign;
        tb.VerticalAlignment = vAlign;
        content.Content = tb;

        return content;
    }

    /// <summary>
    /// Draws the tree in a horizontal layout.
    /// </summary>
    /// <param name="canvas">The Canvas to draw on.</param>
    /// <param name="node">The current node to draw.</param>
    private static void drawHorizontal(this Canvas canvas, TreeNode node)
    {
        // Spacing between nodes
        double spaceX = 120;
        double spaceY = 50;

        // Draw the current node
        var x = node.X * spaceX + node.X * nodeWidth;
        var y = node.Y * spaceY + node.Y * nodeHeight;
        canvas.Children.Add(createRect(x, y, nodeWidth, nodeHeight, green, 2.0d, Brushes.White));
        canvas.Children.Add(createText(node.Text, 16.0d, black, x, y - 1, nodeWidth, nodeHeight, HorizontalAlignment.Center, VerticalAlignment.Center));

        // Update canvas size
        canvas.updateCanvasSize(x + nodeWidth, y + nodeHeight);

        foreach (var child in node.Children)
        {
            // Draw connecting line
            var line_x = child.X * spaceX + child.X * nodeWidth;
            var line_y = child.Y * spaceY + child.Y * nodeHeight + nodeHeight / 2;
            canvas.Children.Add(createLine(x + nodeWidth, y + nodeHeight / 2, line_x, line_y, green, 2.5d));

            // Draw child nodes
            canvas.drawHorizontal(child);
        }
    }

    /// <summary>
    /// Draws the tree in a vertical layout.
    /// </summary>
    /// <param name="canvas">The Canvas to draw on.</param>
    /// <param name="node">The current node to draw.</param>
    private static void drawVertical(this Canvas canvas, TreeNode node)
    {
        // Spacing between nodes
        double spaceX = 50;
        double spaceY = 120;

        // Draw the current node
        var x = node.X * spaceX + node.X * nodeWidth;
        var y = node.Y * spaceY + node.Y * nodeHeight;
        canvas.Children.Add(createRect(x, y, nodeWidth, nodeHeight, green, 2.0d, Brushes.White));
        canvas.Children.Add(createText(node.Text, 16.0d, black, x, y - 1, nodeWidth, nodeHeight, HorizontalAlignment.Center, VerticalAlignment.Center));

        // Update canvas size
        canvas.updateCanvasSize(x + nodeWidth, y + nodeHeight);

        foreach (var child in node.Children)
        {
            // Draw connecting line
            var line_x = child.X * spaceX + child.X * nodeWidth + nodeWidth / 2;
            var line_y = child.Y * spaceY + child.Y * nodeHeight;
            canvas.Children.Add(createLine(x + nodeWidth / 2, y + nodeHeight, line_x, line_y, green, 2.5d));

            // Draw child nodes
            canvas.drawVertical(child);
        }
    }

    /// <summary>
    /// Updates the canvas size based on the drawn elements.
    /// </summary>
    /// <param name="canvas">The Canvas to update.</param>
    /// <param name="widht">The new width of the canvas.</param>
    /// <param name="height">The new height of the canvas.</param>
    private static void updateCanvasSize(this Canvas canvas, double widht, double height)
    {
        if (widht > canvasWidth)
        {
            canvasWidth = widht;
            canvas.Width = widht;
        }

        if (height > canvasHeight)
        {
            canvasHeight = height;
            canvas.Height = height;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Source Code

The project files for the created application are available for sale at the following site. If you would like to review the entire source code, please feel free to purchase it.

https://aktwjtj.gumroad.com/l/WpfDrawTreeTopology

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay