dev-resources.site
for different kinds of informations.
[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
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": []
}
]
}
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...)
}
}
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);
}
}
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.
- 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.
- 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
- 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.
- 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.
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;
}
}
}
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.
Featured ones: