
Double Buffering
Windows Forms
As much as we would
like it not to be the case, graphics can be slow enough to watch as the
screen is refreshed. Our eyes, sensitive to movement and particularly to
edge detection so that we don't walk off of cliffs or run into trees,
often pick up the redraw cycle of computer graphics which, at best is
mildly annoying or at worst can cause headaches, eyestrain and in
susceptible people, even fits. Much of this effect
is caused by the sequential re-drawing of many graphical elements such as
a chart with many lines or a game with many moving items. In a perfect
world, the sequential redraw would be completely hidden from the user and
the completed graphic presented in its entirety. This is essentially the
technique used in double buffering. Windows Forms
provides an automatic method of double buffering that can be used by
simply setting a few styles in your form or control. For most
applications, this is enough but in certain cases, more control over the
process is desirable so manually double buffering a control is also
possible. First, take a look
at the standard and built in method of double buffering. This is
accomplished by setting the styles:
When these are all
set true, the draw process is modified so that instead of your Paint
handler being passed a Graphics for the screen, it is passed a Graphics
for an in-memory bitmap. When you draw to this Graphics object, you are
drawing on an invisible image. At the end of the draw cycle, this bitmap
is copied to the main window automatically and the actual pixels you see
are all changed in a fraction of a second instead of one at a time as the
draw cycle progresses. To set up automatic
double buffering for a Form, you would use the following line of code in
the constructor, after the InitializeComponent method call. C# this.SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer,true); VB
me.SetStyle( ControlStyles.AllPaintingInWmPaint OR _ ControlStyles.UserPaint OR _ ControlStyles.DoubleBuffer,true) Manual double
buffering can be useful if you don't want the system so make assumptions
for you such as whether the background is opaque or transparent or perhaps
if you want to create a more complex buffering system. There are a few
simple rules that you need to follow to get manual double buffering right. First, don’t
create a new back-buffer every draw cycle. Only create or destroy the
bitmap when the window's client size changes. Second, only create a bitmap
of the size you need. Clearing pixels takes time and so if there are more
pixels than you need, you're just wasting processor cycles. Lastly, use
the simplest draw method to copy the bitmap to the screen.
DrawImageUnscaled is the way to go here. Before the big
demo, listing 1 shows all the salient points of a manual double buffered
application.
private Bitmap _backBuffer;
protected override
void OnPaint(PaintEventArgs e)
{
if(_backBuffer==null)
{
_backBuffer=new Bitmap(this.ClientSize.Width,this.ClientSize.Height);
}
Graphics g=Graphics.FromImage(_backBuffer);
//Paint your graphics on g here
g.Dispose();
//Copy the back buffer to the screen
e.Graphics.DrawImageUnscaled(_backBuffer,0,0);
//base.OnPaint (e); //optional but not
recommended
}
protected override
void OnPaintBackground(PaintEventArgs
pevent)
{
//Don't allow the background to paint
}
protected override
void OnSizeChanged(EventArgs e)
{
if(_backBuffer!=null)
{
_backBuffer.Dispose();
_backBuffer=null;
}
base.OnSizeChanged (e);
} VB Private _backBuffer As Bitmap
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) If _backBuffer Is Nothing Then _backBuffer = New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height) End If
Dim g As Graphics = Graphics.FromImage(_backBuffer)
'Paint on the Graphics object here
g.Dispose()
'Copy the back buffer to the screen e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0) End Sub 'OnPaint
'base.OnPaint (e); //optional but not recommended
Protected Overrides Sub OnPaintBackground(ByVal pevent As PaintEventArgs) End Sub 'OnPaintBackground
'Don't allow the background to paint
Protected Overrides Sub OnSizeChanged(ByVal e As EventArgs) If Not (_backBuffer Is Nothing) Then _backBuffer.Dispose() _backBuffer = Nothing End If MyBase.OnSizeChanged(e) End Sub 'OnSizeChanged
The following
listing shows a practical double buffered application. A checkbox enables
you to choose whether double buffering is used or if the graphics are
drawn directly to the screen for the sake of comparison. C# using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; namespace DoubleBuffer {
/// <summary>
///
Summary description for Form1.
/// </summary>
public class
Form1 : System.Windows.Forms.Form
{
private
System.Windows.Forms.Timer timer1;
private
System.ComponentModel.IContainer components;
private
System.Windows.Forms.CheckBox checkBox1;
float _angle;
bool _doBuffer;
public Form1()
{
//
// Required for
Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any
constructor code after InitializeComponent call
//
}
///
<summary>
///
Clean up any resources being used.
///
</summary>
protected override
void Dispose( bool
disposing )
{
if(
disposing )
{
if
(components != null)
{
components.Dispose();
}
}
base.Dispose(
disposing );
} #region Windows Form Designer generated
code
///
<summary>
///
Required method for Designer support - do not modify
///
the contents of this method with the code editor.
///
</summary>
private void
InitializeComponent()
{
this.components
= new System.ComponentModel.Container();
this.timer1
= new System.Windows.Forms.Timer(this.components);
this.checkBox1
= new System.Windows.Forms.CheckBox();
this.SuspendLayout();
//
// timer1
//
this.timer1.Enabled
= true;
this.timer1.Tick
+= new System.EventHandler(this.timer1_Tick);
//
// checkBox1
//
this.checkBox1.Location
= new System.Drawing.Point(8, 8);
this.checkBox1.Name
= "checkBox1";
this.checkBox1.TabIndex
= 0;
this.checkBox1.Text
= "Double Buffer";
this.checkBox1.CheckedChanged
+= new System.EventHandler(this.checkBox1_CheckedChanged);
//
// Form1
//
this.AutoScaleBaseSize
= new System.Drawing.Size(5, 13);
this.ClientSize
= new System.Drawing.Size(292, 273);
this.Controls.Add(this.checkBox1);
this.Name =
"Form1";
this.Text =
"Form1";
this.ResumeLayout(false);
} #endregion
///
<summary>
///
The main entry point for the application.
///
</summary>
[STAThread]
static void
Main()
{
Application.Run(new
Form1());
}
private void
timer1_Tick(object sender,
System.EventArgs e)
{
_angle+=3;
if(_angle>359)
_angle=0;
Invalidate();
}
private Bitmap _backBuffer;
protected override
void OnPaint(PaintEventArgs e)
{
if(_backBuffer==null)
{
_backBuffer=new
Bitmap(this.ClientSize.Width,this.ClientSize.Height);
}
Graphics g=null;
if(_doBuffer)
g=Graphics.FromImage(_backBuffer);
else
g=e.Graphics;
g.Clear(Color.White);
g.SmoothingMode=SmoothingMode.AntiAlias;
Matrix mx=new
Matrix();
mx.Rotate(_angle,MatrixOrder.Append);
mx.Translate(this.ClientSize.Width/2,this.ClientSize.Height/2,MatrixOrder.Append);
g.Transform=mx;
g.FillRectangle(Brushes.Red,-100,-100,200,200);
mx=new
Matrix();
mx.Rotate(-_angle,MatrixOrder.Append);
mx.Translate(this.ClientSize.Width/2,this.ClientSize.Height/2,MatrixOrder.Append);
g.Transform=mx;
g.FillRectangle(Brushes.Green,-75,-75,149,149);
mx=new
Matrix();
mx.Rotate(_angle*2,MatrixOrder.Append);
mx.Translate(this.ClientSize.Width/2,this.ClientSize.Height/2,MatrixOrder.Append);
g.Transform=mx;
g.FillRectangle(Brushes.Blue,-50,-50,100,100);
if(_doBuffer)
{
g.Dispose();
//Copy
the back buffer to the screen
e.Graphics.DrawImageUnscaled(_backBuffer,0,0);
}
//base.OnPaint
(e); //optional but not recommended
}
protected override
void OnPaintBackground(PaintEventArgs
pevent)
{
//Don't allow the
background to paint
}
protected override
void OnSizeChanged(EventArgs e)
{
if(_backBuffer!=null)
{
_backBuffer.Dispose();
_backBuffer=null;
}
base.OnSizeChanged
(e);
}
private void
checkBox1_CheckedChanged(object sender,
System.EventArgs e)
{
_doBuffer=this.checkBox1.Checked;
} } VB
Imports System Imports System.Drawing Imports System.Drawing.Drawing2D Imports System.Collections Imports System.ComponentModel Imports System.Windows.Forms Imports System.Data
Namespace DoubleBuffer '/ <summary> '/ Summary description for Form1. '/ </summary>
Public Class Form1 Inherits System.Windows.Forms.Form
Private WithEvents timer1 As System.Windows.Forms.Timer Private components As System.ComponentModel.IContainer Private WithEvents checkBox1 As System.Windows.Forms.CheckBox
Private _angle As Single Private _doBuffer As Boolean
Public Sub New() ' ' Required for Windows Form Designer support ' InitializeComponent() End Sub 'New
' ' TODO: Add any constructor code after InitializeComponent call '
'/ <summary> '/ Clean up any resources being used. '/ </summary> Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub 'Dispose
#Region "Windows Form Designer generated code"
'/ <summary> '/ Required method for Designer support - do not modify '/ the contents of this method with the code editor. '/ </summary> Private Sub InitializeComponent() Me.components = New System.ComponentModel.Container Me.timer1 = New System.Windows.Forms.Timer(Me.components) Me.checkBox1 = New System.Windows.Forms.CheckBox Me.SuspendLayout() ' ' timer1 ' Me.timer1.Enabled = True ' ' checkBox1 ' Me.checkBox1.Location = New System.Drawing.Point(8, 8) Me.checkBox1.Name = "checkBox1" Me.checkBox1.TabIndex = 0 Me.checkBox1.Text = "Double Buffer" ' ' Form1 ' Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13) Me.ClientSize = New System.Drawing.Size(292, 273) Me.Controls.Add(checkBox1) Me.Name = "Form1" Me.Text = "Form1" Me.ResumeLayout(False) End Sub 'InitializeComponent #End Region
'/ <summary> '/ The main entry point for the application. '/ </summary> <STAThread()> _ Shared Sub Main() Application.Run(New Form1) End Sub 'Main
Private Sub timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles timer1.Tick _angle += 3 If _angle > 359 Then _angle = 0 End If Invalidate() End Sub 'timer1_Tick
Private _backBuffer As Bitmap
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) If _backBuffer Is Nothing Then _backBuffer = New Bitmap(Me.ClientSize.Width, Me.ClientSize.Height) End If
Dim g As Graphics = Nothing If _doBuffer Then g = Graphics.FromImage(_backBuffer) Else g = e.Graphics End If g.Clear(Color.White)
g.SmoothingMode = SmoothingMode.AntiAlias
Dim mx As New Matrix mx.Rotate(_angle, MatrixOrder.Append) mx.Translate(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2, MatrixOrder.Append) g.Transform = mx g.FillRectangle(Brushes.Red, -100, -100, 200, 200)
mx = New Matrix mx.Rotate(-_angle, MatrixOrder.Append) mx.Translate(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2, MatrixOrder.Append) g.Transform = mx g.FillRectangle(Brushes.Green, -75, -75, 149, 149)
|