Introduction
This article describes how Graphviz can be incorporated in your project to show graphs describing your state machine / process for example.
Background
The input for Graphviz is a dot file. this simple text file describes the graph layout in a textual manner with it's own grammar: https://en.wikipedia.org/wiki/DOT_(graph_description_language)
Using the code
Basically there are two ways to incorporate the graphviz functionality:
- Calling the external program dot.exe supplied by Graphviz and perform data transfer over the file system
- Wrapping the libraries of Graphviz and perform in memory data transfer
I will show both methods based on Graphviz 2.38.
Download and extract the Graphviz windows release
Download the release here: file
This file contains the libraries and dot.exe a command line program to generate from a dot graph files an image file.
After download extract the folder: /release/bin. This folder contains dot.exe and related libraries running on windows. This is important because these libraries are build against the right font directories which is of importance when you want to have an aligned and nice looking graph in the end.
Examples of problems that occur when using not the right libraries which can be found in the folder: /release/lib/release/dll. These libraries are build not against the right font folders.
From the /release/bin folder extract the following files:
These are needed to run dot.exe and generate the images from dot graphs.
Testing graph vizualisation
Now we are going to test is everything is working fine
First open a command window in the folder and run from the command line: dot.exe –c
This will cause dot.exe to update the config6 file which contains the administration of available libraries which can be used by GraphViz. If this file is not available of not up to date, then GraphViz (gvc.dll) will malfunction and cause exceptions.
Now we can test dot.exe on the command line by creating a dot file in notepad with the following contents:
digraph g{
A->B
label ="Graph";
labelloc = top;
labeljust = left;
}
And save it as testgraph.dt (use .dt instead of calling it .dot since it will cause windows to recognize it as an office template).
Now run from the command line: dot.exe –Tjpg –O testgraph.dt
When everything works fine a jpg file is created: testgraph.dt.jpg (so .dt is not really needed)
If this works fine than we can proceed.
Adding the files to your visual studio project
In both cases we have to add the needed external (Graphviz files) to the visual studio project.
To do this, do the following:
- Create a map in you visual studio project and call it ‘external’
- Then via file explorer copy all files mentioned above including dot.exe and config6
- Next in visual studio go to the map and right click to add existing items
- Go to the folder external and change the file type from ‘Visual C# files’ to all files (*.*)
Now the files will be shown, select all (including dot.exe and config6) and select add.
The needed external files are now added to your project and we can continue to work out both methods.
Option 1, using the external dot.exe program
Visual studio has added the needed external files to the project, but they won’t be copied during built. Therefore select them all including dot.exe and set the properties to ‘copy always’
Now with each built the support files including dot.exe are copies to the output folder under /external.
(dot.exe can be run from the command line)
Basically we have an input:
String graphVizString = @" digraph g{ label=""Graph""; labelloc=top;labeljust=left;}";
And we want to end with a bitmap. So we have to make a class called ‘FileDotEngine
’ which looks as:
public static class FileDotEngine
{
public static Bitmap Run(string dot)
{
string executable = @".\external\dot.exe";
string output = @".\external\tempgraph";
File.WriteAllText(output, dot);
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.StartInfo.FileName = executable;
process.StartInfo.Arguments = string.Format(@"{0} -Tjpg -O", output);
process.Start();
process.WaitForExit();
Bitmap bitmap = null; ;
using (Stream bmpStream = System.IO.File.Open(output + ".jpg", System.IO.FileMode.Open))
{
Image image = Image.FromStream(bmpStream);
bitmap = new Bitmap(image);
}
File.Delete(output);
File.Delete(output + ".jpg");
return bitmap;
}
}
On the calling side only the following code is needed to use it as a bitmap:
Bitmap bm = FileDotEngine.Run(graphVizString);
When calling FileDotEngine.Run your application will start an external process (dot.exe) pass the string as input file to it. Then dot.exe will create the image on disk which is read afterwards by your application again and imported as bitmap.
Option 2, using wrapper and call the libraries directly
Visual studio has added the needed external files to the project, but they won’t be copied during built. Therefore select them all except for dot.exe and set the properties to ‘copy always’
Now with each built the support files excluding dot.exe are copies to the output folder under /external.
(dot.exe can't be run from the command line)
Now create a new class and call it: ‘Graphviz
’ with the following content:
public static class Graphviz
{
public const string LIB_GVC = @".\external\gvc.dll";
public const string LIB_GRAPH = @".\external\cgraph.dll";
public const int SUCCESS = 0;
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gvContext();
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvFreeContext(IntPtr gvc);
[DllImport(LIB_GRAPH, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr agmemread(string data);
[DllImport(LIB_GRAPH, CallingConvention = CallingConvention.Cdecl)]
public static extern void agclose(IntPtr g);
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvLayout(IntPtr gvc, IntPtr g, string engine);
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvFreeLayout(IntPtr gvc, IntPtr g);
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvRenderFilename(IntPtr gvc, IntPtr g,
string format, string fileName);
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvRenderData(IntPtr gvc, IntPtr g,
string format, out IntPtr result, out int length);
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvFreeRenderData(IntPtr result);
public static Image RenderImage(string source, string format)
{
IntPtr gvc = gvContext();
if (gvc == IntPtr.Zero)
throw new Exception("Failed to create Graphviz context.");
IntPtr g = agmemread(source);
if (g == IntPtr.Zero)
throw new Exception("Failed to create graph from source. Check for syntax errors.");
if (gvLayout(gvc, g, "dot") != SUCCESS)
throw new Exception("Layout failed.");
IntPtr result;
int length;
if (gvRenderData(gvc, g, format, out result, out length) != SUCCESS)
throw new Exception("Render failed.");
byte[] bytes = new byte[length];
Marshal.Copy(result, bytes, 0, length);
gvFreeLayout(gvc, g);
agclose(g);
gvFreeContext(gvc);
gvFreeRenderData(result);
using (MemoryStream stream = new MemoryStream(bytes))
{
return Image.FromStream(stream);
}
}
}
This content has been found (so i'm not the author of this piece of code) on the internet and completed/corrected to do what is needed. It is a wrapper around the graphviz libraries (gvc and cgraph) which do the work.
On the calling side only the following code is needed to use it as a bitmap:
Bitmap bm = new Bitmap(Graphviz.RenderImage(graphVizString, "jpg"));
To my opinion the second option is a better solution since the file system is not being used, but debugging is worse since dot.exe (option 1) can give you more insights.
Using this code gives you the following result for an example:
>
Points of Interest
The issue with the fonts (misalignment of the text) were a nightmare to find out the issue. Nothing is/has been written on the internet so far.