This article shows you how easy it is to do "owner drawn graphics" in WPF. The viewer of this sample also has some advantages over existing viewers.
I am presenting a WPF control for viewing graphs rendered by
GraphViz (Dot). The
DotViewer control has the usual navigation -- i.e., zoom, drag, scroll -- and supports hit testing on nodes, which is used for displaying tooltips. In the first place, this article should show you how easy it is to do "owner drawn graphics" in WPF. As it turns out, the viewer of this sample also has some advantages over existing viewers:
- It is several times faster, probably because WPF is hardware accelerated.
- Because WPF works vector-oriented, zooming is fast and produces smooth, good looking pictures.
- Nodes can be found by mouse position, which makes user interactions possible (tooltips, selections).
- You can easily integrate it into your own .NET applications.
- It works with huge graphs; for example 350 nodes with 2600 edges -- i.e., more than 53000 Bezier points -- can be displayed and zoomed nearly delay-free.
I suggest that you now download the sample and play with it a little before continuing.
Currently, I am working on a project that is packaged in over 500 assemblies. To understand this packaging better, I wrote a little Python script that analyses the assembly dependencies and generates a graph description for
GraphViz. It uses Dot to render a GIF image and displays it with the standard windows image viewer. This works well for small graphs. The first time I tried to view the graph of the complete system, however, I got images so big -- more than 80000 pixels wide -- that it took minutes to load, display, zoom and scroll them. I tried other viewers but wasn't really satisfied due to their speed, usability and the possibility of using them in my application. Also, the printing capabilities are very limited. I found no easy way to print large graphs on several pages. So for a while, I viewed only subgraphs, arranged for my current needs and never seeing the big picture. Then I remembered TechEd 2006 in Barcelona, where I saw some really impressive WPF demos. Instantly, I knew: this was the right problem to try WPF with. If it is really so cool, as Microsoft emphasized, it should be no problem to make my own lightning-fast viewer. And lo and behold: Microsoft was right!
- GraphViz Homepage - a very good and free tool for graph visualization, used as the layout engine for this project
- MSDN DrawingVisual sample - this gave me a quick start into using visuals for high performance drawing
- QuickGraph - A graph library with additional GDI+ based
- Glee - a Microsoft Research project that does the layout and rendering of graphs, also based on GDI+
The solution contains two projects. The Visualizing project contains the
DotViewer control. The
Dot2Wpf project is just a simple wrapper application that hosts the
DotViewer control. It allows you to open files in the .plain format produced by Dot.
Dot2Wpf contains several samples, so you don't need to install the
GraphViz package if you just want to play around a bit. If you have
GraphViz installed, you can create the .plain output from .dot files with this command:
dot -Tplain -o "graph.plain" "graph.dot"
You can open a file by pressing Ctrl+O or by clicking the button in the upper left. Use the mouse wheel to zoom the graph. If the graph gets too big, move it with the scrollbars or drag it with the right mouse button. You can select a node by clicking it. If you hover the mouse over a node, it will display a tooltip. Just in case you are wondering about the meaning of my graph samples:
- The node color indicates the area from and to which the assembly is assigned.
- The size of the node text is proportional to the code size of the assembly.
- Orange edges indicate dependencies that are defined at compile time, but not used at runtime.
The DotViewer Control
DotViewer control is contained in the Visualizing project. It is a simple
UserControl composed of a standard
ScrollViewer and some floating
ScrollViewer itself contains the
GraphElement, which is derived from
FrameworkElement and is my host for the visuals that draw the entire graph. You can use the
DotViewer in your own applications by simply adding it to a panel. If you are using XAML, you probably want to define a custom namespace that allows you to write something like the following:
After the control has been loaded -- wait for the
Loaded event of the hosting window -- you can call
LoadPlain to load a .plain graph file. If you want to supply tooltips for nodes, you have to subscribe the
NodeTipEventArgs has a
Tag attribute that identifies the node. It is the
nodeID from the .dot file. Assign the
Content attribute your tooltip content. It can be arbitrary WPF content, but most probably, you will use a
GraphElement uses the
GraphLoader class to read the .plain output of Dot and create the visuals displaying the graph. Because shapes are not very efficient when there are many of them, I am using visuals. Visuals don't support high-end stuff like data binding triggers, but they are very performant and still have the ability to do hit testing via
VisualTreeHelper. The graph is represented by a
DrawingVisual with children. It directly contains all edges. Every node is a child
DrawingVisual, tagged with the
nodeID from the original .dot file. This is necessary to distinguish between the nodes when hit testing.
Printing and Paginating
Graphs are frequently huge and if you print them on one page, the text is often unreadably small. With GraphViz, I had real problems with printing big graphs. I had to render into the PS format and used Adobe Distiller to manually distribute the output over several pages. This was a very time-consuming process. WPF uses a
DocumentPaginator in its
PrintDocument method and I hoped I could use this class to do my own paginating.
DocumentPaginator is just an
abstract class that does nothing. But by overriding the
GetPage method and the
PageCount property, I was able to print my graph visual on several pages. The constructor of my
GraphPaginator class gets the visual and the size of one printed page. The first problem I needed to solve was getting a copy of the original visual. I found no way to do this, so I created a new visual and used the
DrawDrawing to draw the
Drawing properties of each visual. My new visual could now be transformed and clipped as I wanted, without changing the original visual. All that the
GetPage method now had to do was translate to the proper page position and clip everything that didn't belong to this page. Because I wanted to draw glue marks on every page, I used the same trick as before and created a new visual. I drew the glue marks on it and then the clipped part of the graph that I needed.
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);
new TranslateTransform(margin - view.X, margin - view.Y));
return new DocumentPage(v, PageSize, frameRect, frameRect);
Points of Interest
- I had to implement my own ToolTip service because the standard service allows only one tooltip per
UIElement. Because only one
GraphElement) is responsible for rendering the complete graph, the standard
ToolTipService was not suitable.
- With WPF Bezier methods, it was extremely easy to render the Dot output, just a few dozen lines of code. In fact, the most difficult part was to draw the arrowheads. I had to normalize a vector, rotate it and then scale it a bit to get the desired effect. Thank God WPF has a
Vector class at last!
- Most WPF books tell you that you need a
HostElement to display a
DrawingVisual. This is true, but you need to do your own layout code in
ArrangeOverride. Without doing so, WPF doesn't know how big your element is and your element probably won't behave as you expect!
- I tried to rotate the visual before printing at 90 degrees to do my own landscape orientation. However, I got some very ugly text output as a result. Printing to the XPS printer was fine and as expected, but printing to a real (PCL) printer made garbage of my text. So I override the Page orientation, regardless of what the user selects. This also works on real printers.
DotViewer supports only the .plain output format of Dot. This means that:
- Every node is rendered as an ellipse.
- All edges are interpreted as arrows.
- There are no edge labels.
- The font is hard-coded as
Verdana, so if you want another, you will have to change this in the code.
I want to relax the limits of the .plain format and switch to annotated Dot. I hope that this will allow me to render any valid .dot graph.
- 21st May, 2007 -- 0.2.0.0, First public release
- 13th June, 2007 -- 0.3.0.0, Added print support