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

绘制一棵树

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.07/5 (9投票s)

2020年12月24日

CPOL

3分钟阅读

viewsIcon

16805

一个树绘制算法,实现为C#/WinForms/.NET Core控件,用最少的努力产生出乎意料的好结果

引言

树可视化算法可以非常简单或非常复杂。 这完全取决于“足够好”的标准。 这是一个幼稚的树绘制算法,它以最小的努力产生出乎意料的好结果。

背景

在四个步骤中绘制树之前,让我们定义它。 我们的树有八个节点 {A,B,C,D,E,F,G,H} 和七条边 {A-B, A-C, A-D, D-E, A-F, A-G, G-H}。 我们将分四个步骤绘制它。

步骤 1: 让我们像标准的 Windows 树控件一样绘制树。 每个节点占据一行,节点缩进反映其级别。

步骤 2: 将所有父节点移动到其第一个子节点的行。

步骤 3: 步骤 2 已清空一些行。 让我们删除它们。

步骤 4: 在最后一步中,我们需要以递归方式将父节点置于其所有子节点的中心。

就是这样。 考虑到算法的简单性,它实际上看起来相当不错。

Using the Code

该算法实现为名为 Hierarchy 的 WinForms (.NET Core) 控件。 您可以使用它来可视化树。 它负责布局; 并期望您的代码绘制节点和边。 该控件可以使用相同的算法从左到右、从右到左、从上到下或从下到上绘制树。

控件属性

要控制流程,请设置属性 Direction。 该控件知道如何从左到右从右到左从上到下从下到上绘制树。

常规节点属性(在所有节点之间共享!)是 NodeWidthNodeHeight。 两个节点之间的最小像素空间由 NodeHorzSpacingNodeVertSpacing 属性确定。

传递数据源

您可以通过实现一个简单的 IHierarchyFeed 接口,然后通过 SetFeed() 方法将其传递给 Hierarchy 来将数据馈送到控件中。

这是接口

public interface IHierarchyFeed
{
    IEnumerable<string> Query(string key=null);
}

它只有一个函数,该函数返回节点键(节点标识符)的集合。

 

由于您的代码负责绘制节点和边,因此控件实际上不需要了解有关节点的更多信息。 当它需要绘制它时,它会在事件中传递节点键和矩形,并期望您的代码完成其余工作。

Query() 函数接受父键参数。 如果此参数为 null,则该函数返回所有根节点键(通常只有一个?),否则它返回提供的父节点的子节点。

以下示例代码实现了目录的简单文件系统馈送。

public class FileSystemHierarchyFeed : IHierarchyFeed
{
    private string _rootDir;

    public FileSystemHierarchyFeed(string rootDir) { _rootDir = rootDir; }

    public IEnumerable<string> Query(string key = null)
    {
        if (key == null) return new string[] { _rootDir };
        else return Directory.EnumerateDirectories(key + @"\");
    }
}

在上面的示例中,完整路径用作节点键。 如果您想绘制组织结构图,您可能会使用人的数据库标识符作为键。

 

免责声明: 让上面的文件馈送扫描您的 c: 驱动器是一个非常糟糕的主意。 只是说说而已。

实现绘图

您可以订阅两个事件:DrawEdge 事件用于绘制边,即连接两个节点的线。 DrawNode 事件用于绘制节点。 这两个事件都将传递给您节点键、节点矩形和用于绘图的 Graphics 实例。 此示例演示了在两个事件内部进行绘制。

private void _hierarchy_DrawEdge(object sender, DrawEdgeEventArgs e)
{
    // Calculate node centers.
    Point
        start = new Point(
            e.ParentRectangle.Left + e.ParentRectangle.Width / 2,
            e.ParentRectangle.Top + e.ParentRectangle.Height / 2),
        end = new Point(
            e.ChildRectangle.Left + e.ChildRectangle.Width / 2,
            e.ChildRectangle.Top + e.ChildRectangle.Height / 2);
    // And draw the line.
    using (Pen p = new Pen(ForeColor)) 
        e.Graphics.DrawLine(p,start,end);
}

private void _hierarchy_DrawNode(object sender, DrawNodeEventArgs e)
{
    // Extract directory name from the path.
    string dir= Path.GetFileName(Path.GetDirectoryName(e.Key+@"\"));

    // Draw the node.
    Graphics g = e.Graphics;
    using (Pen forePen = new Pen(ForeColor))
    using (Brush backBrush = new SolidBrush(BackColor),
        foreBrush = new SolidBrush(ForeColor))
    using(StringFormat sf=new StringFormat() { 
        LineAlignment=StringAlignment.Center, 
        Alignment=StringAlignment.Center})
    {
        g.FillRectangle(backBrush, e.Rectangle); // Border.
        g.DrawRectangle(forePen, e.Rectangle); // Rectangle.
        g.DrawString(dir, Font, foreBrush, e.Rectangle, sf); // Text.
    }
}

响应鼠标事件

您可以订阅标准鼠标事件(单击、移动等)并使用 NodeAt() 函数来查找单击了哪个节点。 例如,如果您想在单击时突出显示节点,请订阅 MouseUp 事件,找出单击了哪个节点,存储其键,然后调用 Refresh() 以重新绘制控件。

private string _highlightedNodeKey;
private void _hierarchy_MouseUp(object sender, MouseEventArgs e)
{
    _highlightedNodeKey = _hierarchy.NodeAt(e.Location);
    _hierarchy.Refresh();
}

然后,在 DrawNode 事件中,根据 _highlightedNodeKey 检查节点键并相应地绘制它。

黑客攻击边缘

由于 DrawEdge 事件为您提供了边的两端 - 父节点和子节点(及其坐标),因此您可以选择如何绘制边。 它可以是一条线、一条曲线等。 您也可以从父节点的末尾(而不是节点中心)开始绘制边,并将其绘制到另一个节点的开头。

private void _hierarchy_DrawEdge(object sender, DrawEdgeEventArgs e)
{
    // Change start and end location of an edge.
    Point
        start = new Point(
            e.ParentRectangle.Right,
            e.ParentRectangle.Top + e.ParentRectangle.Height / 2),
        end = new Point(
            e.ChildRectangle.Left,
            e.ChildRectangle.Top + e.ChildRectangle.Height / 2);
    using (Pen p = new Pen(ForeColor))
        e.Graphics.DrawLine(p, start, end);
}

结果如下

历史

  • 2020 年 12 月 24 日:初始版本
© . All rights reserved.