绘制一棵树






4.07/5 (9投票s)
一个树绘制算法,实现为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
。 该控件知道如何从左到右、从右到左、从上到下和从下到上绘制树。
常规节点属性(在所有节点之间共享!)是 NodeWidth
和 NodeHeight
。 两个节点之间的最小像素空间由 NodeHorzSpacing
和 NodeVertSpacing
属性确定。
传递数据源
您可以通过实现一个简单的 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 日:初始版本