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

Dot2WPF - 用于查看 Dot 图的 WPF 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (32投票s)

2007年5月21日

CPOL

8分钟阅读

viewsIcon

215065

downloadIcon

6201

一个用于查看 GraphViz (Dot) 布局的快速平滑的 WPF 查看器

Screenshot - dot2wpf_small.png

Screenshot - dot2wpf_zoomed.png

引言

我将介绍一个用于查看 GraphViz (Dot) 渲染的图表的 WPF 控件。DotViewer 控件具有常用的导航功能——即缩放、拖动、滚动——并支持节点上的命中测试,这用于显示工具提示。首先,本文应该向您展示在 WPF 中进行“所有者绘制图形”有多么容易。事实证明,此示例的查看器也比现有查看器有一些优势。

  • 它的速度快了好几倍,这可能是因为 WPF 是硬件加速的。
  • 由于 WPF 是矢量导向的,缩放速度快,并产生流畅、美观的图像。
  • 可以通过鼠标位置找到节点,这使得用户交互成为可能(工具提示、选择)。
  • 您可以轻松地将其集成到您自己的 .NET 应用程序中。
  • 它可以处理大型图表;例如,具有 2600 条边的 350 个节点——即超过 53000 个贝塞尔点——可以几乎无延迟地显示和缩放。

我建议您现在就下载示例并稍加尝试,然后再继续阅读。

动机

目前,我正在开发一个包含 500 多个程序集的项目。为了更好地理解这种打包方式,我编写了一个小小的 Python 脚本来分析程序集的依赖关系并为 GraphViz 生成图表描述。它使用 Dot 渲染 GIF 图像,并使用标准的 Windows 图像查看器显示它。对于小型图表,这效果很好。然而,当我第一次尝试查看整个系统的图表时,我得到的图像非常大——宽度超过 80000 像素——加载、显示、缩放和滚动都需要花费几分钟时间。我尝试了其他查看器,但由于它们的性能、可用性以及在我应用程序中使用它们的能力,我并不太满意。此外,打印功能非常有限。我没有找到一种简单的方法将大型图表打印在多页上。因此,一段时间以来,我只查看了根据我当前需求安排的子图,从未看到整体。然后我回忆起 2006 年在巴塞罗那的 TechEd 大会,在那里我看到了一些令人印象深刻的 WPF 演示。我立刻意识到:这是尝试 WPF 的绝佳问题。如果它真的像微软强调的那么酷,那么制作我自己的闪电般快速的查看器应该不是问题。果然,微软是对的!

背景

替代方案

  • QuickGraph - 一个带有额外 GDI+ 基于 GraphViz 支持的图表库
  • Glee - 一个微软研究院项目,负责图表的布局和渲染,同样基于 GDI+

示例应用

该解决方案包含两个项目。Visualizing 项目包含 DotViewer 控件。Dot2Wpf 项目只是一个简单的包装应用程序,用于托管 DotViewer 控件。它允许您打开 Dot 生成的 .plain 格式文件。Dot2Wpf 包含多个示例,因此如果您只是想随便玩玩,则无需安装 GraphViz 包。如果您已安装 GraphViz,您可以使用此命令从 .dot 文件创建 .plain 输出。

dot -Tplain -o "graph.plain" "graph.dot"

您可以通过按 Ctrl+O 或单击左上角的按钮来打开文件。使用鼠标滚轮缩放图表。如果图表太大,请使用滚动条移动它,或用鼠标右键拖动它。您可以通过单击节点来选择节点。将鼠标悬停在节点上时,会显示一个工具提示。万一您想知道我的图表示例的含义。

  • 节点颜色指示程序集被分配到的区域。
  • 节点文本的大小与其程序集的代码大小成正比。
  • 橙色边表示在编译时定义但在运行时未使用的依赖项。

DotViewer 控件

DotViewer 控件包含在 Visualizing 项目中。它是一个简单的 UserControl,由一个标准的 ScrollViewer 和一些浮动的 TextBlocks 组成。ScrollViewer 本身包含 GraphElement,它派生自 FrameworkElement,是我用于绘制整个图表的 Visual 的宿主。您可以通过将其添加到面板中来在自己的应用程序中使用 DotViewer。如果您使用的是 XAML,您可能希望定义一个自定义命名空间,允许您编写如下内容:

<Window xmlns:r=
    "clr-namespace:Rodemeyer.Visualizing;assembly=Rodemeyer.Visualizing"
[...]
<Grid>
    <r:DotViewer x:Name="MyDotViewer"></r:DotViewer>
</Grid>

在控件加载后——等待宿主窗口的 Loaded 事件——您可以调用 LoadPlain 来加载 .plain 图表文件。如果您想为节点提供工具提示,您必须订阅 ShowNodeTip 事件。NodeTipEventArgs 有一个 Tag 属性,用于标识节点。它是 .dot 文件中的 nodeID。将 Content 属性分配给您的工具提示内容。它可以是任意 WPF 内容,但最有可能的是,您将使用一个 TextBlock 元素。

GraphElement 使用 GraphLoader 类读取 Dot 的 .plain 输出并创建显示图表的 Visual。由于形状效率不高,当数量很多时,我正在使用 Visual。Visual 不支持数据绑定触发器等高端功能,但它们的性能非常高,并且仍然能够通过 VisualTreeHelper 进行命中测试。图表由一个带有子项的 DrawingVisual 表示。它直接包含所有边。每个节点都是一个子 DrawingVisual,带有原始 .dot 文件中的 nodeID 标签。这对于在命中测试时区分节点是必需的。

打印和分页

图表通常非常大,如果将其打印在一页上,文本通常会小得难以辨认。使用 GraphViz,我在打印大型图表时遇到了实际问题。我必须渲染成 PS 格式,并使用 Adobe Distiller 手动将输出分配到多个页面。这是一个非常耗时的过程。WPF 在其 PrintDocument 方法中使用 DocumentPaginator,我希望我可以使用这个类来进行自己的分页。

实际上,DocumentPaginator 只是一个什么都不做的抽象类。但通过重写 GetPage 方法和 PageCount 属性,我能够将我的图表 Visual 打印到多个页面。我的 GraphPaginator 类的构造函数获取 Visual 和一个打印页面的大小。我需要解决的第一个问题是获取原始 Visual 的副本。我找不到这样做的方法,所以我创建了一个新的 Visual,并使用 DrawDrawing 来绘制每个 Visual 的 Drawing 属性。现在,我的新 Visual 可以根据需要进行转换和裁剪,而不会改变原始 Visual。GetPage 方法现在只需要翻译到正确的页面位置并将所有不属于该页面的内容裁剪掉。因为我想在每个页面上绘制胶印标记,所以我使用了与之前相同的技巧,创建了一个新的 Visual。我在上面绘制了胶印标记,然后是所需的图表的裁剪部分。

public override DocumentPage GetPage(int pageNumber)
{
    int x = pageNumber % pageCountX;
    int y = pageNumber / pageCountX;

    Rect view = new Rect();
    view.X = x * contentSize.Width;
    view.Y = y * contentSize.Height;
    view.Size = contentSize;

    DrawingVisual v = new DrawingVisual();
    using (DrawingContext dc = v.RenderOpen())
    {
        dc.DrawRectangle(null, framePen, frameRect);
        dc.PushTransform(
            new TranslateTransform(margin - view.X, margin - view.Y));
        dc.PushClip(new RectangleGeometry(view));
        dc.DrawDrawing(graph);
    }
    return new DocumentPage(v, PageSize, frameRect, frameRect); 
}

关注点

  • 我不得不实现我自己的 ToolTip 服务,因为标准服务只允许每个 UIElement 有一个工具提示。因为只有一个 UIElement (GraphElement) 负责渲染整个图表,所以标准的 ToolTipService 不适用。
  • 使用 WPF 的贝塞尔方法,渲染 Dot 输出非常简单,只需几十行代码。事实上,最困难的部分是绘制箭头。我必须对一个向量进行归一化,然后旋转它,然后再稍微缩放一下以获得期望的效果。感谢 WPF 终于有了一个 Vector 类!
  • 大多数 WPF 书籍会告诉你,你需要一个 HostElement 来显示一个 DrawingVisual。这是真的,但你需要在 MeasureOverride 和 ArrangeOverride 中自己编写布局代码。如果不这样做,WPF 就不知道你的元素有多大,你的元素很可能不会按预期工作!
  • 我尝试在打印前将 Visual 旋转 90 度以实现自己的横向打印。然而,结果是文本输出非常糟糕。打印到 XPS 打印机效果很好,符合预期,但打印到真实的 (PCL) 打印机时,我的文本就变成了乱码。因此,我覆盖了页面方向,忽略用户选择。这在真实打印机上也有效。

限制

目前,DotViewer 只支持 Dot 的 .plain 输出格式。这意味着:

  • 每个节点都渲染成一个椭圆。
  • 所有边都被解释为箭头。
  • 没有边标签。
  • 字体被硬编码为 Verdana,因此如果您想要其他字体,则需要在代码中进行更改。

未来

我想放宽 .plain 格式的限制,切换到带注释的 Dot。我希望这将允许我渲染任何有效的 .dot 图。

历史

  • 2007 年 5 月 21 日 -- 0.2.0.0,首次公开发布
  • 2007 年 6 月 13 日 -- 0.3.0.0,添加了打印支持
© . All rights reserved.