Introduction
When drawing a custom control, if you want to avoid flicker and maximize speed, you should begin to use buffers. .NET 2.0 introduces the object BufferedGraphicsContext
to simplify that. Usually, the designing of a user control involves different layers, ordered from bottom to top. The background layer is the one that changes less.
Sometimes, drawing can be an expensive (slow) operation because of:
- You need to access data in a database (for a grid) or in binary format (for a map). Accessing and filtering data is slow.
- You need to draw complex forms (antialias, etc.).
- The drawing involves a lot of creation of pens, brushes, text, bitmaps, etc.
So, it is a good idea that, if a part of the image doesn't change a lot during the use of the control, keep it buffered and redraw it only when it changes.
How it works
The idea is to use an array of BufferedGraphicsContext
, one for each layer. Each buffer has the drawing of the previous included. For example, for a grid:
Buffer1: Background
Buffer2: Background and Fixed columns
Buffer3: Background and Fixed columns and rows and cells
Buffer4: Background and Fixed columns and cells and user selection
This way, for example, when the user selects multiple cells in a grid, you only need to copy buffer3 to buffer4 and draw the user selection on buffer4. Finally, copy buffer4 to the screen.
When the user scrolls, you copy buffer2 to buffer3 and draw the cells on buffer3, copy it to buffer4, draw the user selection on buffer4, and finally copy buffer4 to the screen.
When the user does nothing (just Alt Tab), you just copy buffer4 to the screen. Here is where the speed is most noticeable for some applications.
Of course, this has to be used depending on the requirements of the user control. For example, if the background is solid color, the drawing operation is not expensive, and it is more expensive to copy the buffer. But what if you want to draw textures or compressed images (JPG)? The same applies to the fixed columns. If you draw images on them and you use antialias to draw the text, then it is faster to keep them in a buffer and redraw them only when the user resizes them.
To make it work correctly, a variable is needed which tells where to draw from. For example, when you scroll, that state is buffer3. When you change the background, you must start with buffer1. When the user is selecting rows, you can start with buffer4.
The code
First of all, you declare an array of BufferedGraphics
, an enum with the layers, and the "from" variable:
BufferedGraphics[] buffergraphs;
enum TypeOfChange
{
TChangeBackGround,
TChangeRectangles,
TChangeDotWidth,
TChangeSelCircle,
TChangeNone,
};
const int NumLayers = 4;
TypeOfChange vartchange;
Then the controls are created. You need to set the control so that all the drawing is done into the paint event and disable the double buffer. You also need to create the array of buffers and initialize the variable:
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
SetStyle(ControlStyles.Opaque, true);
buffergraphs = new BufferedGraphics[NumLayers];
vartchange = TypeOfChange.TChangeBackGround;
Then the drawing methods:
private void DrawFirstLayer(Graphics g, Rectangle r);
private void DrawSecondLayer(Graphics g, Rectangle r);
private void DrawThirdLayer(Graphics g, Rectangle r);
private void DrawFourthLayer(Graphics g, Rectangle r);
Then the paint method:
private void UsrCtlBufferDrawing_Paint(Object sender, PaintEventArgs e)
{
if (vartchange == TypeOfChange.TChangeBackGround)
{
DrawFirstLayer(buffergraphs[(int)TypeOfChange.TChangeBackGround].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeBackGround].Render(
buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics);
}
if (vartchange == TypeOfChange.TChangeRectangles)
buffergraphs[(int)TypeOfChange.TChangeBackGround].Render(
buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics);
if (vartchange == TypeOfChange.TChangeBackGround || vartchange ==
TypeOfChange.TChangeRectangles)
{
DrawSecondLayer(buffergraphs[(int)TypeOfChange.TChangeRectangles].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeRectangles].Render(
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics);
}
if (vartchange == TypeOfChange.TChangeDotWidth)
buffergraphs[(int)TypeOfChange.TChangeRectangles].Render(
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics);
if (vartchange == TypeOfChange.TChangeBackGround || vartchange ==
TypeOfChange.TChangeRectangles || vartchange == TypeOfChange.TChangeDotWidth)
{
DrawThirdLayer(buffergraphs[(int)TypeOfChange.TChangeDotWidth].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Render(
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics);
}
if (vartchange == TypeOfChange.TChangeSelCircle)
buffergraphs[(int)TypeOfChange.TChangeDotWidth].Render(
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics);
if (vartchange != TypeOfChange.TChangeNone)
{
DrawFourthLayer(buffergraphs[(int)TypeOfChange.TChangeSelCircle].Graphics,
e.ClipRectangle);
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Render(e.Graphics);
}
if (vartchange == TypeOfChange.TChangeNone)
buffergraphs[(int)TypeOfChange.TChangeSelCircle].Render(e.Graphics);
vartchange = TypeOfChange.TChangeNone;
}
When the user resizes, it is a good moment to create the buffers (because their size is dependent on the size of the control).
private void UsrCtlBufferDrawing_Resize(Object sender, EventArgs e)
{
BufferedGraphicsContext context;
context = BufferedGraphicsManager.Current;
context.MaximumBuffer = new System.Drawing.Size(this.Width + 1, this.Height + 1 );
for (int i = 0; i < NumLayers; i++)
buffergraphs[i] = context.Allocate(this.CreateGraphics(),
new System.Drawing.Rectangle(0,0, this.Width, this.Height ));
vartchange = TypeOfChange.TChangeBackGround;
Invalidate();
}
Finally, you have to trap when the user changes things to trigger the redraw:
public Color ColorBackGround
{
get
{
return m_ColorBackGround;
}
set
{
m_ColorBackGround = value;
vartchange = TypeOfChange.TChangeBackGround;
Invalidate();
}
}
public Color ColorRectangle
{
get
{
return m_ColorRectangle;
}
set
{
m_ColorRectangle = value;
vartchange = TypeOfChange.TChangeRectangles;
Invalidate();
}
}
private void UsrCtlBufferDrawing_MouseDown(Object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mousestartpoint.X = e.X;
mousestartpoint.Y = e.Y;
vartchange = TypeOfChange.TChangeSelCircle;
Invalidate();
}
}
private void UsrCtlBufferDrawing_MouseUp(Object sender, MouseEventArgs e)
{
if (mousestartpoint.X != -1)
{
ResetSelectPtos();
vartchange = TypeOfChange.TChangeDotWidth;
mousestartpoint.X = -1;
Invalidate();
}
}
Other uses
This idea can be applied to MFC (CMemDC
) and Win32. Instead of BufferedGraphics
, use CreateCompatibleBitmap
and CBitmap
, instead of render, Bitblt.
The sample
The sample code is a little basic, just to see the idea working. It uses timers to change the background. Even when you are drawing a circle and the background is changing at the same time, the app does not flicker.
Improvements
I added a method to dispose the array when the control exits. In the designer:
this.HandleDestroyed += new System.EventHandler(this.UsrCtlBufferDrawingDestroy);
private void UsrCtlBufferDrawingDestroy(Object sender, EventArgs e)
{
for (int i = 0; i < NumLayers; i++)
{
if (buffergraphs[i] != null)
buffergraphs[i].Dispose();
}
}
I also added some metrics to see how fast it is really compared to simple double buffering. The results are:
Running on a slow machine without separate graphics card:
- If you're drawing lines and shapes, it is faster to just use double buffer.
- If you're drawing solid gradients, the speed is the same with double buffer and layers.
- If you're drawing text with gradients, or just a lot of text, it is faster with layers (more than three times as fast).
Using the sample source code (that draws gradients and gradient text), I got 200 for double buffer and 60 for layers (more than three times faster).
Running on a fast machine with separate graphics card:
It is way faster to use layers. I think it is because the buffer management is done inside the card. Using the sample source code (that draws gradients and gradient text), I got 110 for the double buffer and 13 for the layers (nearly 10x faster).