Sunday, August 16, 2009

Smoother .NET CF UIs with Double Buffering

I have a feeling this post will be obsolete very soon.  Silverlight for Windows Mobile and Windows Mobile 7 should appear soon, offering richer UI components to mobile developer. Still, the concept of double buffering is an important one to know, can really help custom .NET CF controls, and sounds harder than it really is.  In short, double buffering is just a way to avoid flicker when rendering a control by painting first on an image in memory, then copying that image to the display all at once. 

A typical, non-double buffered control may look something like this:

   1: public class SingleBufferExample : Control
   2: {
   3:    protected override void OnPaint(PaintEventArgs e)
   4:    {
   5:        e.Graphics.FillRectangle(new SolidBrush(Color.Black), 0,0, this.Width, this.Height);
   6:        e.Graphics.DrawRectangle(new Pen(Color.Green),0,0,this.Width, this.Height );
   7:        e.Graphics.DrawString(DateTime.Now.ToShortTimeString(), new Font(FontFamily.GenericSansSerif, 14, FontStyle.Regular), new SolidBrush(Color.Green), 0,0 );
   8:        base.OnPaint(e);
   9:    }
  10: }

While this will work, at runtime you’ll notice a flicker as each step executes. The black rectangle will first be rendered, then the border, then the text.  Each step takes a split second, but it is noticeable, especially on slower devices, and with more complex controls.  Here’s the code rewritten to use double-buffering:

   1: public class DoubleBufferExample : Control
   2: {
   3:     protected override void OnPaint(PaintEventArgs e)
   4:     {
   5:         using(var currentRender = new Bitmap(this.Width, this.Height))
   6:         using (var currentRenderGraphics = Graphics.FromImage(currentRender))
   7:         {
   8:             currentRenderGraphics.FillRectangle(new SolidBrush(Color.Black), 0, 0, this.Width, this.Height);
   9:             currentRenderGraphics.DrawRectangle(new Pen(Color.Green), 0, 0, this.Width, this.Height);
  10:             currentRenderGraphics.DrawString(DateTime.Now.ToShortTimeString(),
  11:                                   new Font(FontFamily.GenericSansSerif, 14, FontStyle.Regular),
  12:                                   new SolidBrush(Color.Green), 0, 0);
  13:             e.Graphics.DrawImage(currentRender, 0, 0);
  14:         }
  15:         base.OnPaint(e);
  16:     }
  17: }

Here, the drawing all happens in memory on a bitmap named ‘currentRender’.  Once it’s complete, the bitmap is painted to the ‘real’ graphics instance.  The effect is that the render appears to the user to only take a single, quick step.  Of course this is at expense of memory – the Bitmap lives in memory and an additional graphics instance has to be created.  That’s the trade-off, but for many applications, it’s well worth it for a smoother UI.

The concept can be extended further.  By creating a base class for double-buffered controls, you can simplify developing double-buffered controls:

   1: public class DoubleBufferControl : Control
   2: {
   3:   protected override void OnPaint(PaintEventArgs e)
   4:   {
   5:       using(var currentRender = new Bitmap(this.Width, this.Height))
   6:       using(var currentRenderGraphics = Graphics.FromImage(currentRender))
   7:       {
   8:           var args = new PaintEventArgs(currentRenderGraphics, e.ClipRectangle);
   9:           OnPaintBuffer(args);
  10:           e.Graphics.DrawImage(currentRender, 0, 0);
  11:       }
  12:       base.OnPaint(e);
  13:   }
  15:   protected virtual void OnPaintBuffer(PaintEventArgs args)
  16:   {
  18:   }
  19: }

With this, you have only to inherit from DoubleBufferControl and override OnPaintBuffer.  Beyond this, it’s also possible to do things such as paint the buffer on a separate thread or modify the code to keep a single instance of the bitmap and graphics – changing them only when the control resizes.

No comments: