Simple WPF 2D Graphics: DrawingVisual
And now for something completely different… Over the last little while I’ve been working on a side project just for fun which involves some simple 2D graphics using WPF. Because I’m calculating the image I want to draw rather than it being a static thing, one of my criteria for the basic approach is that it will all be code oriented. I’m not by any means opposed to xaml, and there are places in this program where I could use some xaml, but it would only be for the most basic top-level windows and such. So I decided to just do the whole thing in C# and avoid any xaml to keep all my code in one place and to force myself to learn how all the pieces go together.
For my first attempt I did the easiest thing (and the one most frequently shown in simple WPF graphics demos) which is to use a Canvas and create on it a series of Line objects. The result looked like this:
[STAThread]
public static void Main()
{
var app = new Application();
var window = new Window();
var canvas = new Canvas();
window.Content = canvas;
canvas.Children.Add(new Line
{
X1 = 0,
Y1 = 0,
X2 = 400,
Y2 = 400,
Stroke = Brushes.Black
});
canvas.Children.Add(new Line
{
X1 = 0,
Y1 = 400,
X2 = 400,
Y2 = 0,
Stroke = Brushes.Black
});
app.Run(window);
}
While this approach worked for my initial drawings and was quite easy, I became concerned when I wanted to introduce some animation because I had cases where I wanted to animate a figure made up of a large number of lines as though it was one thing, and it began to get a bit ugly to setup. In addition I started to worry a bit about perf, and even though I didn’t have the full program running yet so I wasn’t able to actually measure the perf, my reading on the net led me to think those worries were justified.
One fairly commonly recommended alternative approach is to use a DrawingVisual which as I understand it is basically a lower level construct which allows you to compose a number of drawing commands as though they were a single control in the visual hierarchy. Rather than each part of the drawing being full-fledged controls with their own events and everything, you create a series of drawing commands and put them inside a single object that is the only thing which has events and such. This is supposed to be faster, and it’s also supposed to allow you to use things like DrawingGroup which can cause all its contents to animate together as one thing—just what I wanted!
When I started to try this, though, everything I did seemed to just display a blank screen. My app would start, and the window would come up, but the graphics weren’t drawn into it. I looked at several samples—many of which were all in xaml—and you’d think that they would convert easily to code since xaml is just a straightforward mapping onto object creation after all, but I just couldn’t make it work. After much banging my head on the wall and searching on the net I eventually discovered that one of the main places I was looking, MSDN, had a sample with code shown on the help page that was incomplete. There was a small link to another page where you could go download the full sample, which worked fine. So a fair amount of scratching my head and trial and error later led me to the discovery of what was in that sample which was missing from my code. Hopefully this post will help some other poor wpf graphics programmer figure out their problem sooner.
It turns out that the key to my whole mess was creating the “visual host” control which would contain my drawing. This control inherits from FrameworkElement and provides the infrastructure for event handling and other things. It plays a role much like the canvas in the sample above except that with the canvas, each child is also something that inherits from FrameworkElement whereas when using a VisualDrawing this is the only thing that does that and all of its children are just simple drawing components. In addition to inheriting from FrameworkElement, the visual host needs to contain a VisualCollection object which is a container for its children. That much I had working. The part that had escaped me was that you are also required to override a property and a method from FrameworkElement. The property is VisualChildrenCount. It needs a getter and it should just return the count value from the VisualCollection. The method is GetVisualChild and it just returns the child with the specified index from the VisualCollection.
Unfortunately these aren’t abstract so the compiler won’t catch the issue and tell you that you must override them, but if you don’t, then the framework never becomes aware of your child objects and the result is that nothing is displayed.
Once over that hurdle, everything works great. The final program (equivalent to the one above) looks like this:
[STAThread]
public static void Main()
{
var app = new Application();
var window = new Window();
var host = new MyVisualHost();
window.Content = host;
app.Run(window);
}
public class MyVisualHost : FrameworkElement
{
private VisualCollection children;
public MyVisualHost()
{
children = new VisualCollection(this);
var visual = new DrawingVisual();
children.Add(visual);
using (var dc = visual.RenderOpen())
{
dc.DrawLine(new Pen(Brushes.Black, 1), new Point(0, 0), new Point(400, 400));
dc.DrawLine(new Pen(Brushes.Black, 1), new Point(0, 400), new Point(400, 0));
}
}
protected override int VisualChildrenCount
{
get { return children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= children.Count)
{
throw new ArgumentOutOfRangeException();
}
return children[index];
}
}
As you can see, the structure of the program is very similar to the first sample except that instead of creating a canvas and then adding child line objects to that canvas, I create a new “MyVisualHost” object which in its constructor creates a DrawingVisual, uses that object to get a DrawingContext and then calls methods on the DrawingContext to draw lines. Everything but the last two methods is fairly easy to find in samples on the net, but if you omit them, your program will compile—it just won’t work. Lovely.
- Danny
Comments
Anonymous
March 31, 2011
The comment has been removedAnonymous
June 07, 2014
Thank you SO MUCH !!! Like Jeff Shelby, I couldn't understand why it wasn't working. I've searched a lot on the web but in all the examples I've seen, they never mention these property and method to override.Anonymous
June 07, 2014
Thank you SO MUCH !!! Like Jeff Shelby, I couldn't understand why it wasn't working. I've searched a lot on the web but in all the examples I've seen, they never mention these property and method to override.