65.9K
CodeProject 正在变化。 阅读更多。
Home

Tree-Painter

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (21投票s)

2011 年 5 月 1 日

GPL3

10分钟阅读

viewsIcon

79167

downloadIcon

5466

以垂直方式绘制一组树节点。允许导出SVG图像。

旧版本

运行示例

001_Application_Running-1.png

导出的图像

exported_image.svg.png

引言

图形用户界面可以提供对同一概念的多种理解方式,而“树”是计算机科学课程中学习的基本数据结构之一。

树形图、树状图或树形绘制可以快速轻松地理解层次结构、搜索地图、演化与继承、短语结构、组织图或思维导图,以及由此衍生出的各种应用。

这是绘图算法的基本描述,只是从个人角度解释一种布局。我希望它能帮助您或作为未来项目、扩展功能或简单“帮助”的基础。

也许过去有、现在有、将来还会有更多的解释、其他方法、不同的想法来实现相同的目标,但我们每个人的理解方式都不同,没有普遍的法律或普遍可理解的描述方式,所以,这是一种贡献,希望能为某些人带来好处,或为共同利益而扩展它。

背景

在我主修期间,当我学习“自动机理论语言”课程时,我们被要求开发一个程序,其主要任务是验证“上下文无关文法”,并且有一个次要目标是能够绘制“解析树”。

在那几年,Java版本是1.4,我的团队选择了它来编程作业,由于绘图不是项目的主要部分(理论逻辑才是),我们把它留到了最后,而我们的绘图并不美观……

在同一学期,我的朋友 Tomás Ortíz S. 更聪明,他使用了VB的一个树控件……完成了老师要求的任务,节点按顺序绘制并分组良好。换句话说,他很有效率,节省了时间。

几年后我毕业了,那个项目又回到我脑海中,我记得我们当时(那时)奇怪为什么不存在一个绘制树形或图形的控件……最近,我对此进行了一些搜索,发现没有太多项目(至少在描述方法方面)。我知道树很简单,但考虑到这一点,我想到这个结构有很多用途,我在介绍中已经提到。当然,我们现在有各种绘图工具……但在我看来,我们直接从原始需求跳到了最终应用,而没有记录过程。本文旨在填补这一空白。

我必须说……我写最后一段是为了说明我写这篇文章的第一个也是主要动机,但随后我不得不承认,在试图让我相信信息不多的时候,我错了,我最初考虑的4或5个选项……变成了10多个不同的方法,我想在最后以链接的形式总结和列出。我一开始并没有考虑所有这些,但在尝试了不同的搜索词“tree viewer”、“tree drawing code”、“tree painter”、“graph”、“visualization”……之后,我发现了很多“派生分支”。我希望保持这个列表,并随着社区的建议不断增长。

Using the Code

本文以两个控件为基础:TreeView Panel 。它们都已标准化,文档齐全,并且有大量的示例。公开的代码演化了3个“主要”版本,第三个版本中,添加了 Label 控件来标识每个节点。

对于初学者和中级开发者,本文将继续保持其“初步方法”的目标。制作用户控件或组件,或将代码公开为Web服务可能是此任务的第二个发布版本。

对于输入,我决定使用 TreeView 。对于输出,如果我只想绘制矩形或椭圆形,我可以采用 Canvas ……但经过一番分析,您会发现 Panel 提供了 Canvas 图形对象,并作为我们“节点”(版本3中的Label)的容器。
在本文中,我将主要介绍源代码的最后一个版本。注释已放在代码中,并且与第一个版本应用了相同的原则。

分析

让我们开始吧。假设我们有一些具有不同节点数量的树。

01_examples_1-2-3.png

02_examples_4-5-6.png

图1、2和3是简单的例子。在每个例子中,复杂性都在增加。这些是“手工”布局。

而主要问题是:如何以“美观”的方式定位每个节点?嗯……这是一个关键点……应该花几个小时来解决。如果您继续阅读……您将剥夺自己解决这个谜题的机会,并且可能在脑海中发展出一些东西。但是,您也可以从本文开始,开发一种新的替代方案。

话虽如此……让我们假设我们计算了每个分支的最后一层……
对于第一个树形图,所有“子节点”的层级都相同……
对于图4、5和6,每个分支都有不同的“深度”。

关键点(获得节点排列)是“平衡”(如果没有更好的词)树。换句话说,填充每个分支,使每个分支包含相同数量的层级(深度)。每个分支不需要有相同数量的子节点,我们只需要相同的层级数量。图5可以近似说明如何想象较短分支中的“缺失”节点(您将如何平衡该树?)。在开始分析之后,相同的原理可以应用于任何类型的分支或树。只需“填补空白”。图5碰巧显示了每个分支下相同数量的子节点,图6显示了不必有相同数量的子节点。图5和6显示了灰色的缺失节点。

03_examples_5-6_extended.png

现在,这是完整的布局,下一个问题可能是从哪里开始绘制?。我们如何知道每个“父节点”的中心位置?这是另一个好问题(当您一无所知时)。

假设我们混合了图3和图4。

04_examples_add_3_and_4.png

我们可以有一个“第一个美观的布局”。

05_examples_result_3_and_4.png

但是如果我们添加了想象中的缺失节点,那么那些节点可能会“重叠”……

06_examples_result_3_and_4_expanded.png

在观察了图5和图6的布局之后……如果我们寻找最小的空间或“连续节点”,很容易发现最深的层级可以确定节点的第一位置,而不会超出上层。

从一个简单的例子开始,序列将是以下情况:

07_order_of_drawing.png

因此,另一个关键点将是从底部开始(最深的层级),然后确定父节点的位置。我假设一个好的树或节点结构(与TreeView不同或替代)应该能够轻松确定节点的父节点。

下面解释的代码将利用前面暴露的分析。请检查注释。

代码

主函数在此处粘贴,但我建议您检查源代码中的代码并进行调试。

我认为locateNodes是实现了分析中解释的算法的主要函数。

/// <summary>
/// Function that obtains children for each node in the <c>TreeNodeCollection</c>.
/// The full 'family' will be contained in the <c>ArrayList</c>.
/// </summary>
/// <param name="nodes"><c>TreeNodeCollection</c> to read.</param>
/// <param name="array">Destination of data.</param>
/// <returns>A integer with the counter of all nodes.</returns> 
private int getAllChildNodes(TreeNodeCollection nodes, ArrayList array)
...
/// <summary>
/// Function to iteratively add 'auxiliar' nodes to the current <c>t</c> node.
/// </summary>
/// <param name="t">A <c>TreeNode</c> contained in a TreeView.</param>
/// <param name="untilLevel">Maximum depth. If the <c>t</c> node is not in this level, 
/// this function will add one 'auxiliar' node to it, as child.
/// </param>
private void addBlankNode(TreeNode t, int untilLevel)
...  
/// <summary>
/// Main Function to Locate and Draw Nodes based on a TreeView.
/// V2 Named "drawNodes"
/// V3 Renames to "locateNodes"
/// </summary>
/// <param name="drawingPanel">Panel that will be used as Drawing Board.</param>
/// <param name="originalTree">TreeView containing the Nodes to draw.</param>
/// <param name="tempTree">Temporary (Work) variable.</param>
private void locateNodes(Panel drawingPanel,  TreeView originalTree, 
 TreeView tempTree, out ArrayList labeledNodes)

{
tempTree.Nodes.Clear();
// Version 3 Feature
// ArrayList labeledNodes;      // ArrayList which will contain Label objects.
Label labelAux;                 // Temporary Label which will be added to 
                                // the labeledNodes arrayList.

labeledNodes = new ArrayList();

if (originalTree.Nodes.Count == 0)
    return;

#region drawing variables

Graphics board = drawingPanel.CreateGraphics();

int x = 20;
int y = 20;

int maxDepht = 0;

ArrayList list;
ArrayList[] listByLevel;

int xInitial = int.Parse(xStart.Value.ToString());
int yInitial = int.Parse(yStart.Value.ToString());            

// this.drawNodeFont = new Font(System.Windows.Forms.TextBox.DefaultFont.FontFamily, 
this.drawNodeFont = new Font("Arial", 
                             float.Parse(fontSize.Value.ToString()),FontStyle.Bold);

#endregion

/*
 * 1. Obtain Nodes per each Level.
 *    'x' position will identify the 'index' of the node in each level.
 *    'y' position will identify the 'level'.
 * 2. Draw nodes, starting from the left (first node) of the deepest level.
 *    The posterior nodes (from the same level) should have an 'x' position 
 *    equal to the current 'x' node position plus the node width.
 * 3. When each 'level' is fully drawn (printed), or located, then, 
 *    the superior level should continue. ('y' is decreased).
 * 4. The 'father' should know which is its own center, based on 
 *    the 'x' position of the 'first' and 'last' child.
 * 5. Each node should know its 'own' scope in 'x' and 'y' positions, 
 *    based on its children. In other words, each time that a child
 *    is drawn (or printed), then, the parent should be 'updated' in
 *    its 'scope'.
 *    The 'Tag' property of each TreeNode can be used to store this info.
 * A. Idea: Auxiliar (temporary) Nodes can be added to (fill) those 
 *    TreeNodes that do not contain 'offspring' (any child). In this way, 
 *    the drawing can 'start' with the 'deepest' level.
 *    Obviously, this 'temporary' Nodes will have a 'Tag' value 
 *    indicating that the node is 'Non-printable'.
 * 
 */

list = new ArrayList();
int totalNodes = getAllChildNodes(originalTree.Nodes, list);

// Obtaining the deepest node.
foreach (TreeNode n in list)
    if (n.Level > maxDepht)
        maxDepht = n.Level; // Maximum Depth

// ArrayList to group nodes by level.
listByLevel = new ArrayList[maxDepht + 1];

// TreeNodes from originalTree are *copied* to tempTree (work TreeView)            
foreach (TreeNode n in list)
    if (n.Level == 0)
        tempTree.Nodes.Add((TreeNode)n.Clone());

// Temporary adjusts will be made only on the tempTree (work TreeView)
list = new ArrayList();
totalNodes = getAllChildNodes(tempTree.Nodes, list);

// The 'branches' of tempTree will be checked; each branch should have the same 'depth'.
foreach (TreeNode n in list)
    if (n.Nodes.Count == 0 && n.Level < maxDepht)
        addBlankNode(n, maxDepht);  // Recursive function

// Work Process is made only on the tempTree (work TreeView)
// At this point, each branch have the same 'depth'. Containing 'auxiliary' nodes.
list = new ArrayList();
totalNodes = getAllChildNodes(tempTree.Nodes, list);

// Nodes are grouped by its Level.
foreach (TreeNode n in list)
{
    if (listByLevel[n.Level] == null)
        listByLevel[n.Level] = new ArrayList();
    listByLevel[n.Level].Add(n);
}

// Initial position for the bottom left node (last level, first node)
x = xInitial;
y = yInitial;

// Assigning Location of the nodes of the second tree, in hierarchical order.
for (int z = maxDepht; z >= 0; z--)
{
    for (int index = 0; index < listByLevel[z].Count; index++)
    {
        TreeNode nodeToPaint = (TreeNode)(listByLevel[z][index]);

        // Version 3 Feature
        labelAux = new Label();                    
        labelAux.Name = nodeToPaint.Name;                    
        labelAux.Font = drawNodeFont;
        labelAux.Text = nodeToPaint.Text;
        labelAux.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
        labelAux.AutoSize = true;
        labelAux.BorderStyle = BorderStyle.FixedSingle;
        labelAux.MouseLeave += new System.EventHandler(this.lblTest_MouseLeave);
        labelAux.MouseEnter += new System.EventHandler(this.lblTest_MouseEnter);
        
        labelAux.Tag = nodeToPaint;

        // Drawing Style
        if (nodeToPaint.Text == txbBlankValue.Text)
        {   // Current node is auxiliar.
            labelAux.BackColor = this.disabledNodeBackColor;
            labelAux.ForeColor = this.disabledNodeForeColor;
        }
        else
        {   // Current node contains valid data.
            labelAux.BackColor = this.enabledNodeBackColor;
            labelAux.ForeColor = this.enabledNodeForeColor;
        }
        unionNodeLinesPen = new Pen(labelAux.BackColor);
        
        // Calculating Node Position.
        labelAux.Location = new Point(x, y);

        // nodeToPaint.Tag = labelAux.ClientRectangle;
        nodeToPaint.Tag = new Rectangle(labelAux.Left,
                                        labelAux.Top,
                                        labelAux.PreferredWidth,
                                        labelAux.PreferredHeight);

        // If the current node is not in the last level, then, 
        // its position should be calculated based on its child nodes.
        if (z < maxDepht)
        {
            Point posFirstChild = 
            	getRectangleCenter((Rectangle)(nodeToPaint.FirstNode.Tag));
            
            Point posLastChild =
            	getRectangleCenter((Rectangle)(nodeToPaint.LastNode.Tag));
            
            Point relocatedPoint = labelAux.Location;
            relocatedPoint.X = (posFirstChild.X + posLastChild.X) / 2 - 
            	labelAux.PreferredWidth / 2;
            System.Console.WriteLine(nodeToPaint.Text + " x= " + relocatedPoint.X 
                + "\n  ->1: " + nodeToPaint.FirstNode.Text + " (" + posFirstChild.X + ");"
                + "\n  ->2: " + nodeToPaint.LastNode.Text + " (" + posLastChild.X + ");");
			
            labelAux.Location = relocatedPoint;
            // nodeToPaint.Tag = labelAux.ClientRectangle;
            nodeToPaint.Tag = new Rectangle(labelAux.Left,
                    labelAux.Top,
                    labelAux.PreferredWidth,
                    labelAux.PreferredHeight);            
        }

        // Union Lines
        foreach (TreeNode t in nodeToPaint.Nodes)
        {
        	Rectangle r = new Rectangle(labelAux.Left,
                                        labelAux.Top,
                                        labelAux.PreferredWidth,
                                        labelAux.PreferredHeight);
            // Parent Center
            Point parentCenterPos = getRectangleCenter(r);

            // Child Center
            Point childCenterPos = getRectangleCenter((Rectangle)t.Tag);
            childCenterPos.Y = ((Rectangle)t.Tag).Top;

            board.DrawLine(unionNodeLinesPen, parentCenterPos, childCenterPos);
        }
        
        // Return Located Labels
        labeledNodes.Add(labelAux);

        // The next sibling node will be to the right of the current one.
        // Where this node finishes plus Margin.
        
        // Note: Label.Right != Label.Left + labelAux.PreferredWidth
        x = labelAux.Left + labelAux.PreferredWidth +
        	int.Parse(xPadding.Value.ToString());
        System.Console.WriteLine("Calculated X:" + x.ToString());
    }
    // The total nodes of the current level had been drawn.
    // The previous (superior) level should be located above the current level.
    y -= int.Parse(yPadding.Value.ToString());

}

// Drawing Root
//Point rootPos = new Point();
//Point posFirstRootChild = new Point();
//Point posLastRootChild = new Point();
//posFirstRootChild = (Point)((TreeNode)(listByLevel[0][0])).FirstNode.Tag;
//posLastRootChild = 
(Point)((TreeNode)(listByLevel[0][listByLevel[0].Count - 1])).LastNode.Tag;
//rootPos.X = (posFirstRootChild.X + posLastRootChild.X) / 2;
//rootPos.Y = y;
//board.DrawString("Root", drawFont, drawBrush, rootPos.X, rootPos.Y);

//// Drawing Root Lines To First Level Nodes
//TreeNode[] tArr = (TreeNode[])listByLevel[0].ToArray(typeof(TreeNode));
//foreach (TreeNode t in tArr)
//{
//    Point pChild = (Point)t.Tag;

//    pChild.X += nodeWidth / 2;
//    pChild.Y -= nodeHeight / 2 - 5;

//    board.DrawLine(p, rootPos, pChild);
//}

//Last node (located at the bottom right position) of the last level.
//TreeNode rightNode = (TreeNode)(listByLevel[maxDepht][listByLevel[maxDepht].Count-1]);
//drawingPanel.AutoScrollMinSize = new Size(((Rectangle)(rightNode.Tag)).Right, 600);

}

关注点

起初,我决定解决这个问题作为个人挑战,但在看到我对代码的注释后,我想知道这是否可以被认为是我的第一个开源贡献,几天后,我决定迈出第一步,发布它,并可能从社区获得学习反馈。我认为我没有什么可失去的(这就是CodeProject的精神)。

我决定检查是否有人之前解决过这个问题,并且很高兴了解到不同的需求如何可以利用相同的原理。总结那些引起我注意的不同项目也很有趣。有C#、Java、Python、WPF的部署,以及Google Charts目前实验性的功能(我认为这些人现在有很多选择)。

我的谜题和个人挑战变成了一个对其他解决方案的研究,以及对不同在线工具、理论和符号的学习。我最初想象的是“少数”或“许多”应用程序,但事实上,如果您考虑到所有选项,它涵盖了“巨大的”范围。

搜索

历史

版本简要总结

  • V1. 一个窗体。一个树状视图。一个面板。节点大小有限,在Panel 控件的 OnPaint 事件中绘制。绘制文本,但不计算节点之间的边距,也不计算文本长度。
  • V2. 重新组织的测试GUI。
    节点大小动态。内部矩形指定位置(“x,y”坐标)和边界(文本的宽度和高度)。
    文本宽度根据Font对象计算。
    字体大小动态。
    添加了动态“起始点”(起始点是指第一个最深子节点所在处的左下角位置)。
    水平和垂直边距动态。
    错误。Panel不显示超出可见区域的节点。
  • V3. 添加了标签。内部矩形被移除。
    大小无需计算,因为Label.Autosize 属性可以完成这项工作。
    Panel显示所有节点,超出可见区域的节点通过滚动条显示。
  • V3.1 启用了导出到SVG文件的选项(内部和基本的SVG格式生成,没有使用特定库)。

未来

公开为 WebService:
想法/建议

提出圆形(径向)布局
如果我们有两个第一级节点,它们将分开180°。
如果我们有三个第一级节点,它们将分开120°。
因此……360° / 节点是排列它们的对称公式,但当分支数量增加时,应考虑最深层级的位置,以避免重叠……

这是我在CodeProject上的第一篇文章,欢迎提出宝贵的意见,并且任何建议都将得到尊重和考虑。

© . All rights reserved.