【WPF】ツリートポロジーを描画する方法

# WPFでツリートポロジーを描画する方法

親子関係があるデータ構造を表現する時には以下のようにツリートポロジーで表現しますが、TreeView以外にWPFにはグラフィカルに表示するためのコンポーネントやライブラリがありませんでした。
また、TreeViewはノードが多く・深くなっていくにつれて見づらくなります。

本記事では、以下のようにツリートポロジーをグラフィカルに描画する方法を紹介します。
また、トポロジーを表示しているアプリケーションは以下のGithubで公開しています。

https://github.com/wjtj52twa/WpfDrawTreeTopology

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1425498/95f87854-7da1-c0b3-108c-ac73b6ffc6f3.png)

## ツリー構造のデータ

アプリケーションではツリー構造のデータが格納されたjsonファイルを読み込んで、WPFのCanvasにツリートポロジーを描画しており、jsonファイルは実行ファイル直下の「data.json」を読み込んでいます。

本記事で表示しているトポロジーのjsonデータは以下を使用しています。
```json
{
  "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": []
    }
  ]
}
```

また、上記のツリー構造は「TreeNode」クラスに展開してプログラム上で参照します。
「Parent」に親ノード、「Children」に子ノードが設定され、ツリー構造にノードが繋がるような構造となります。

プロパティに「X」、「Y」がありますが、これはツリー構造を描画するときの座標で、後述する座標計算の結果（座標）を格納する用のプロパティです。また、他に計算用のプロパティや関数がありますが詳細は省略します。

```c#
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();
            }
        }

        (省略)
    }
}

```

## ツリー構造を描画するまでの全体処理

ツリートポロジーを描画するまでの処理概要と、プログラムを以下に示します。
プログラムは「Read topology json」ボタンを押した時に処理となります。「data.json」ファイルを読み込んで、「JsonSerializer」を使用してTreeNodeクラスにパースします。

* ツリー構造が定義されたjsonファイル読み込み
* jsonデータをパースしてTreeNodeクラスを作成、親子ノードのプロパティセット
* ツリートポロジーの座標計算
* ツリートポロジーの描画

```cs
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);
    }
}
```

## トポロジーの座標計算

ツリー構造を描画するために、各ノードの座標位置を計算します。この時、ツリー構造を"きれい"に表示するために、以下のルールに従い座標を計算します。

* 木のエッジがその他のエッジと重ならない
* 同じ深さにあるノードが同じ水平のライン上に配置される
* 木が出来る限り狭く描かれる
* 親のノードがその子のノードの中央に描かれる
* サブツリーがどこに存在しても、同じ構造のサブツリーが同じように描かれる

上記のルールに従い、以下の処理手順で座標を計算します。基本の考え方は、末端の葉ノードから順に座標及びサブツリーを構成し、サブツリー間で位置調整しながらルート（親）方向に座標計算を進めていきます。

1. 木の深さ優先探索 (DFS) を用いて葉ノードを処理

    * 木の最下層（葉ノード）からスタートします。
    * 葉ノードは初期的に適当な座標に初期化します。
    * Y座標はノードの深さで設定する。

2. 子ノードの配置

    * 葉ノードでサブツリー内で最初の子ノードの場合は、X座標を0にする
    * 2個目以降は、隣の子ノードのX座標+1とする
    * サブツリー内で子ノードの配置が完了すれば、親ノードを子ノードの中間に配置する

3. サブツリー間の重なりを解消

    * 各サブツリー間の間隔が適切に確保されるように、シフト操作を行います。
    * この時、子ノードを含むサブツリー全体を平行移動させて重なりを解消する。

4. 絶対位置の決定

    * 2~3の手順を葉ノードから順に計算し、最終的にルートノードまでの座標を計算する

最終敵には以下のようなツリー構造が作成される。座標はTreeNodeクラスの「X」、「Y」プロパティに格納される。また、「X」と「Y」の座標を入れ替えることで横方向のツリー構造の座標に変更することが出来ます。


![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1425498/575ceb41-1b52-bbb2-32e6-868431440fca.png)



## トポロジーの描画

計算した各ノードの座標に従い、以下のコードでCanvasにツリートポロジーを描画します。ノードをRectangleで描画して、ノード間をLineで繋げます。

```cs
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;
        }
    }
}
```

## ソースコード

作成したアプリケーションのプロジェクトファイルは以下のサイトで販売しています。ソースコード全体を確認したい方は、ご購入の程よろしくお願いいたします。

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