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

WPF的图形树绘图控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (31投票s)

2008 年 9 月 20 日

CPOL

10分钟阅读

viewsIcon

305985

downloadIcon

9031

本文描述并实现了一个用于在 WPF 面板中绘制树形结构的图形绘制控件。

Sample Image - maximum width is 600 pixels

引言

本文包含两个截然不同的基本程序集和几个用于 WPF 的测试程序集。第一个基本程序集是 GraphLayout,它实现了 Reingold-Tilford 算法(此处有描述),用于确定树结构中节点的位置。这棵树的结构是自上而下的,根在顶部,叶子在底部。这个算法完全独立于节点的实际绘制,因此可以用于任何需要这种定位的场合。它包括节点垂直对齐、节点折叠、兄弟节点之间距离和非兄弟节点之间距离的选项。这个程序集并非 WPF 特有,可以很容易地用于生成 GDI+ 树形绘制器(尽管我在这里没有包含任何)。它还包括一个可以作为节点之间连接器绘制的线段端点列表。目前,这些线段仅仅是父节点底部中心到子节点顶部中心的直线,但很容易修改它们以绘制不同类型的连接器。

第二个程序集是一个从 Panel 派生的 WPF 控件,它使用 GraphLayout 来确定其子控件在树中的位置。每个非根节点都有一个用于其父节点名称的依赖属性,这就是图的连接方式。由于这是一个派生面板,所以各个节点可以是任何所需类型的 WPF 控件。这允许在 XAML 中指定树,如 TreeContainerTest 测试应用程序所示,或者在代码中指定,如 VisLogTree 所示。

它还包含一个执行相同功能的 Silverlight 控件。Silverlight 控件的主要区别在于,连接必须作为 Path 子项添加到树容器中,而不是直接绘制到控件上。Silverlight 不支持控件的 OnRender 函数,该函数允许在控件上进行绘制。我更喜欢 WPF 控件中使用的绘制方法,这样就不会有这个“悬空”的 Path 控件,但在 Silverlight 中这似乎并不是一个选项。我几天前刚开始接触 Silverlight,所以如果有人更了解,我很乐意听取意见。GraphLayout DLL 必须重新编译,以便与 Silverlight 库链接,但除此之外,它保持不变。在弄清楚所有 Silverlight 的小怪癖之后,新的 Silverlight 控件在我第一次尝试时就成功运行,这在一定程度上证明了 GraphLayout 的通用性。

背景

我可以找到一些 WPF 控件,它们在 WPF 中做出了一些绘制树的努力,但据我所知,目前还没有免费的程序集/源代码能够以标准 WPF 方式(即,可以在 XAML 中实现的派生面板)正确实现 Reingold-Tilford 算法,这似乎是一件显而易见且有价值的事情,所以我已经做到了。我可以想到很多可以进行扩展的地方,但它目前看起来已经非常有价值,所以我将其发布出来,暂时转向其他事情。

Reingold-Tilford 算法最简单的描述是“递归地绘制所有子图,然后尽可能地将它们全部向左挤压,在节点之间留下固定距离,最后清理那些被挤压到左侧但可以向右移动以进行居中的内部节点”。我知道这并不那么直观——尤其是最后一部分——但是阅读上面引用的文章可以提供所有细节(尽管在我看来,它描述得并不十分清晰,也与我未阅读该文章之前,仅凭此处给出的简略描述实现的我的代码不直接对应——仔细研读演练是我真正理解它的唯一方式,即使我了解该算法的基础知识)。

我尝试测试所有情况,但我可能遗漏了一些——树的种类繁多,所以如果你发现任何错误,请给我发邮件。在大多数情况下,如果我在 WPF 控件中发现与父级不一致的情况,我就根本不定位相应的控件,因此它们最终会出现在 TreeContainer 控件的左上角。这是为了避免 XAML 由于抛出异常而不断崩溃并什么都不显示。如果你在 TreeContainer 控件的左上角看到节点,请检查 Parent 属性的一致性。

使用 GraphLayout

GraphLayout(它可能更恰当地称为 TreeLayout,尽管将来我可能会向其中添加其他图类型布局)提供了一个 ITreeNode 接口,它代表(你猜对了)树中的节点。GraphLayout.LayeredTreeDraw 是计算所有节点位置的类。它的构造函数接受一个表示树根的 ITreeNode 和一些影响定位的全局参数(节点之间的最小距离、垂直对齐等),并设置所有节点的属性,给出它们在最终图形树中的正确位置。ITreeNode 接口相当简单。它包括 TreeWidthTreeHeight 属性,它们返回节点的宽度和高度。它还包括布尔型 Collapsed 属性,这是一个只读属性,指示节点是否应该折叠。这些信息如何在节点上保持和设置取决于实现。最重要的属性是 TreeChildren,它以 TreeNodeGroup 集合的形式返回此节点的子节点。TreeNodeGroup 是一个足够简单的类,具有一个 Add 函数,用于将其集合中添加 ITreeNode 对象。最后,LayeredTreeDraw 需要一个地方将自己的私有信息注入到每个节点中。您必须实现 PrivateNodeInfo,它接受一个对象,将其存储到节点中并在以后检索回来。

与树关联的有三种缓冲距离。垂直缓冲距离是树的分层行之间的最小距离。水平缓冲距离是图中兄弟节点之间的最小距离。水平子树距离是非直接兄弟节点之间的最小距离。这使得某些图通过将子树分离得比单个兄弟节点更远而看起来更好。

构建 LayeredTreeDraw 对象后,只需在其上调用 LayoutTree(),然后从中检索信息。给定节点的 X 和 Y 坐标是从名为 ltdLayeredTreeDraw 对象中通过 ltd.X(treeNode)ltd.Y(treeNode) 检索的,其中“treeNode”是所讨论的 ITreeNodeltd.PxOverallWidth()ltd.PxOverallHeight() 返回结果树的宽度和高度。ltd.Connections() 返回一个 TreeConnection 对象列表。每个对象都有一个父 ITreeNode 和一个子 ITreeNode,以及一个 DPoint 列表,该列表给出了从父节点到子节点的线路径。由于 WPF 和 GDI+ 对“Point”使用不同的定义,我决定实现一个非常轻量级的 DPoint 结构,它始终使用双精度浮点数作为坐标,并且可以在 WPF 或 GDI+(或任何其他地方)中使用。目前,这些连接器在父节点和子节点之间提供一条单线,但这不难改变。实际上,我打算提供一组不同类型的连接器,但这可能是在未来。就目前而言,由于垂直节点大小不同,短节点的连接器可能会与较高的兄弟节点重叠。我考虑过的一件事是提供折线,以避免这种可能性。

GraphLayout 的功能大致如此。它使用起来足够简单,但绝对是幕后最复杂的部分。

使用 TreeContainer

TreeContainer 是一个从 Panel 派生的 WPF 控件,它使用 GraphLayout 来布局其子控件。为了正确使用,我们必须知道子控件的树结构。这通过在 TreeContainer 上设置一个名为“Root”的依赖属性来处理,该属性具有作为树根的节点名称的 string 值。TreeContainer 的子控件必须都是 TreeNodes,它们是内容控件,可以包含您喜欢的任何控件。TreeNodes 具有 TreeParent 属性,该属性告诉哪个节点是它们在树中的父节点——同样,是一个带有父节点名称的 string 值。TreeContainer 中的每个 TreeNode 都必须具有父属性,除了 root 节点。每个 TreeNode 上还有 CollapsibleCollapsed 属性。如果节点的 Collapsible 属性设置为 false,则它不能折叠。如果为 true,则 Collapsed 的值决定节点是否折叠。在 VisLogTree 示例中,节点是按钮,按下时会切换其下方树的折叠状态。这些属性都可以在 XAML 中设置。

TreeContainer 还包含一些用于以编程方式创建 TreeNodes 的实用程序例程。这包括 Clear(),它清除 TreeContainer 中的所有节点,以及用于添加根节点或内部节点的例程。在最简单的情况下,这些例程无需直接处理名称,在这种情况下,代码会生成内部名称。例如,AddNode(Object, TreeNode) 将对象包装在一个新的 TreeNode 中,给它一个名称并将其父级设置为传入的 TreeNode。返回用于包装对象的 TreeNode。请参阅 VisLogTree,了解如何在运行时使用这些例程轻松地在 TreeContainer 中生成树的示例。

TreeNode 容器实现了 ITreeNode 接口,因此它们本身就是 GraphLayout 计算所需的 ITreeNode

关注点

可以很容易地为此控件添加许多功能。我已经在文中提到了几个——用于绘制连接的任意画笔、不同类型的连接、图的不同方向(即从左到右或从下到上)、节点折叠或展开时路由事件等。然而,目前它似乎是一个有价值的控件,老实说,我渴望探索新的领域,所以我将其按原样发布。

事实证明,该算法比我最初想象的要复杂得多。正如我最初阅读的那样,该算法存在相当多的例外和陷阱。盲目遵循上面超链接中 Dr. Dobb 文章中描述的算法可能会更容易,但我当时没有那篇文章,而且我对文章中描述的算法的某些方面并不喜欢。

有两个示例。一个是上述论文中使用的图形的 XAML 版本,仅用于验证它是否如论文中描述的那样正确。另一个示例是基于 Adam Nathan 的优秀书籍《WPF Unleashed》中一个对话框的视觉树和逻辑树。这是 Nathan 用来演示逻辑树和视觉树的对话框,因此您可以检查它是否提供了正确的信息。第二个示例中的节点是按钮,按下时可以切换其子图是否折叠。还有一个按钮可以删除或重新添加对话框顶部的标签,我将其用于调试并保留在那里。

在开发此控件之前,我不知道 FrameworkElements 上的 RegisterName/UnregisterName 方法。当我第一次尝试以编程方式创建树控件时,我天真地在 TreeNodes 上设置了名称,然后使用 FindName() 来定位它们。错了!如果你也这样做,你会收到一个错误,提示不存在这样的控件名称。你必须使用 RegisterName() 方法向父级注册名称才能使其工作。同样,如果你移除一个控件,你确实应该执行 UnregisterName()。我读了很多关于 WPF 的资料,这是我第一次看到这个。这似乎应该在文档中更明显一些。TreeContainer 上的实用程序可以轻松生成 TreeNodes,它会自动处理所有这些问题。

历史

  • 2008年9月20日 第一个版本
  • 2009年2月21日 添加 Silverlight 控件
© . All rights reserved.