20.9.08

Manual Double-Buffering in C#

I believe double buffering is enabled by default on most .NET controls, which is convenient for the majority of simple graphics. I intend to draw once and use repeatedly, which makes manual buffering a requisite.

This program plots mathematical equations (i.e. y=sin(x)) on the surface of a custom Control. The actual rendering of a plot could take from 14ms to 200ms or even more, depending on the quality and complexity. However, I want to draw tangent lines to follow the curve in real-time--I cannot afford 14ms between MouseMove events.



The process is simple:
  1. Render the graph to a buffer stored in memory

  2. Copy the buffer to the screen

  3. When the mouse is clicked and moved, re-copy the buffer to the screen, then draw a new tangent line


Here's an excerpt from the constructor. Note, this class extends Control.
private BufferedGraphicsContext context;
private BufferedGraphics buffer;

public GraphPanel()
{
this.DoubleBuffered = false;
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);

this.context = new BufferedGraphicsContext();
using (Graphics graphics = this.CreateGraphics())
{
this.buffer = this.context.Allocate(graphics,
new Rectangle(new Point(0, 0), this.Size));
}
}

The first four lines disable automatic double buffering. I found out the hard way that all of these are actually needed.

The context allows me to manage my buffers. I use it to create a new buffer which matches the graphics of the Control, and size it to the control. I can just reuse this buffer every time I recalculate the graph, or when I'm drawing tangent lines on top of it.

Of course, I need to put similar code in the OnResize method, so I can adjust the buffer size when the window is resized.
protected override void OnPaint(PaintEventArgs e)
{
this.buffer.Graphics.FillRectangle(Brushes.White, this.ClientRectangle);

this.DrawAxis();
this.DrawGraph();

this.buffer.Render(e.Graphics);
}

The OnPaint gets fired everytime the window (or any subregion) gets invalidated. Since I set all those styles in the constructor, this is the place for things to get drawn. This event occurs when the program starts up, is resized, comes back from being minimized, or if it is invalidated programatically. It will not be fired if I move the window around, or click on things in it. Consequently, I will do a complete recalculation and drawing of the axis and graph in this method. This is important to keep in mind, since this is the most time-intensive part of the entire process.

The first thing I do is whitewash the buffer. Then I have two methods, DrawAxis and DrawGraph, which do exactly what they say. They are written to use this.buffer as well, so by the time they are finished, I can copy the buffer to the screen using the Graphics object provided in the EventArgs.
protected override void OnMouseMove(MouseEventArgs e)
{
if (this.MouseMode == MouseMode.Tangent && e.Button == MouseButtons.Left)
{
// Restore the clean version of the graph
using (Graphics graphics = this.CreateGraphics())
{
this.buffer.Render(graphics);
}

// Verify mouse is inside control
if (this.ClientRectangle.Contains(e.Location))
{
// Plot a line tangent to the graph at the location of the mouse
this.DrawTangentLine(Graph.ConvertToRealCoords(e.Location));
}
}
}

If the user presses the left button and moves the mouse, I want to draw a tangent line. I have to ensure the left button is pressed, since the same event is also fired if the mouse is moving idly across the screen. The first step is to copy the graph from the buffer onto the screen, to remove any previous tangent line. Next, I verify the mouse is inside the control and draw the tangent. The DrawTangentLine method, unlike the DrawGraph and DrawAxis methods, does not draw to the buffer, but rather to the screen directly.

The screenshot above shows this code in action. Using a buffer like this reduces drawing a tangent to just copying the buffer and drawing a line--of course I have to calculate the tangent, but the majority of the work has been removed.

No comments: