Logo

dev-resources.site

for different kinds of informations.

[WPF] Draw Tree Topology

Published at
1/11/2025
Categories
wpf
csharp
Author
ren482
Categories
2 categories in total
wpf
open
csharp
open
Author
6 person written this
ren482
open
[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

wpf Article's
30 articles in total
Favicon
[WPF] Draw Tree Topology
Favicon
Whatโ€™s New in WPF Diagram: 2024 Volume 4
Favicon
Implementing Full Transparency No Frame and Acrylic Effects in Avalonia UI
Favicon
๐—š๐—ฒ๐˜ ๐——๐—ฒ๐˜ƒ๐—˜๐˜…๐—ฝ๐—ฟ๐—ฒ๐˜€๐˜€ .๐—ก๐—˜๐—ง ๐— ๐—”๐—จ๐—œ ๐—–๐—ผ๐—ป๐˜๐—ฟ๐—ผ๐—น๐˜€ ๐—ณ๐—ผ๐—ฟ ๐—™๐—ฟ๐—ฒ๐—ฒ ๐—•๐—ฒ๐—ณ๐—ผ๐—ฟ๐—ฒ ๐——๐—ฒ๐—ฐ๐—ฒ๐—บ๐—ฏ๐—ฒ๐—ฟ ๐Ÿฏ๐Ÿญ, ๐Ÿฎ๐Ÿฌ๐Ÿฎ๐Ÿฐ!
Favicon
Por onde anda o WPF?
Favicon
Streamlining .NET Development with Practical Aspects
Favicon
Creating a Dynamic WPF Chart Dashboard to Showcase 2024 Womenโ€™s T20 World Cup Statistics
Favicon
In-Depth Technical Analysis of XAML-Based Frameworks and Cross-Platform Project Architecture Design
Favicon
Semantic Searching using Embedding in WPF DataGrid
Favicon
Integrating AI-Powered Smart Location Search in WPF Maps
Favicon
Create a WPF Multi-Bar Chart to Visualize U.S. vs. China Superpower Status
Favicon
AI-Powered Smart Paste in WPF Text Input Layout for Effortless Data Entry
Favicon
Latest LightningChart .NET Release: v.12.1.1 is out now!
Favicon
Create a WPF FastLine Chart to Analyze Global Population Trends by Age Group
Favicon
Introducing AI-Powered Smart Components & Features in Syncfusion Desktop Platforms
Favicon
Chart of the Week: Clean and Preprocess E-Commerce Website Traffic Data Using an AI-Powered Smart WPF Chart
Favicon
Everything You Need to Know About WPF Gantt Control
Favicon
Chart of the Week: Creating a WPF Chart Dashboard to Analyze 2024 T20 World Cup Statistics
Favicon
Whatโ€™s New in WPF Gantt Chart: 2024 Volume 2
Favicon
Chart of the Week: Creating a WPF Doughnut Chart to Visualize the New European Parliamentโ€™s Composition in 2024
Favicon
Chart of the Week: Creating a WPF Pie Chart to Visualize the Percentage of Global Forest Area for Each Country
Favicon
Chart of the Week: Creating a WPF Range Bar Chart to Visualize the Hearing Range of Living Beings
Favicon
Chart of the Week: Creating a WPF Sunburst Chart to Visualize the Syncfusion Chart of the Week Blog Series
Favicon
Chart of the Week: Creating a WPF Stacked Area Chart to Visualize Wealth Distribution in the U.S.
Favicon
Elevating Automation: Mastering PowerShell and C# Integration with Dynamic Paths and Parameters
Favicon
Navigate PDF Annotations in a TreeView Using WPF PDF Viewer
Favicon
Chart of the week: Creating a WPF 3D Column Chart to Visualize the Panama Canalโ€™s Shipment Transit Data
Favicon
Improve Real-Time WPF Visualization of ECG Signals With SciChart
Favicon
Race Strategy Analysis using SciChart WPF
Favicon
Chart of the Week: Creating a WPF Chart Dashboard to Visualize the 2023 World Billionaires List

Featured ones: