Yesterday I got an email from reader Claudio Fontana, with the following, deceiptively simple request: how can you avoid flickering while updating many controls on a form? The problem is especially serious when you need to add thousands of items to a ListView or a TreeView.
In VB6 this problem can be solved quite simply by temporarily setting the Visible (or Enabled) property to False for all the controls about to be updated: the control isn't actually hidden, yet the result of the update operation appears istantaneously when the property is reset to True. Just as interesting: the update operation is carried out much faster if the control is invisible, often twice as faster. Alas, this trick doesn't work in .NET, because as soon as you set the Visible property to False the control is immediately hidden. It's necessary to find another solution.
A few Windows Forms controls - namely the ListBox, ComboBox, ListView, and TreeView controls - do expose the BeginUpdate and EndUpdate methods, which allow you to "freeze" the control while you add items to it. Not only do they solve the flickering problem, they also speed up the update operation, tipically by a factor of 2.5x. However, if your form contains many controls that do NOT expose these methods, you must devise something else, and this was exactly the problem that Claudio submitted, after he unsuccessfully googled around on the 'Net looking for a solution.
The problem is quite intriguing, thus I decided to spend some time on it, until I came to the following solution. The idea is simple, and can be split in the following steps: (1) take a snapshot of the current form's appearance, by making a pixel-by-pixel copy into a bitmap, (2) create a PictureBox control as large as the form, and load the bitmap into the PictureBox, (3) add the PictureBox to the form's Controls collection and bring the PictureBox in front of all other controls, (4) while the user looks at the "frozen" image of the form, update your controls, using the BeginUpdate/EndUpdate mthods if possible to speed up execution, (5) when the update operation is completed, remove the PictureBox from the Controls collection, so that the user can now see the real form.
You just need one dozen statements to implement this algorithim, but I prepared a class to make the code more reusable and to ensure that it releases all resources correctly:
Public Class FormFreezer
Implements IDisposable
' The form being frozen
Dim Form As Form
' the auxiliary PictureBox that will cover the form
Dim PictureBox As PictureBox
' the number of times the Freeze method has been called
Dim FreezeCount As Integer = 0
' create an instance associated with a given form
' and optionally freeze the form right away
Public Sub New(ByVal form As Form, Optional ByVal freezeIt As Boolean = False)
Me.Form = form
If freezeIt Then Me.Freeze()
End Sub
' freeze the form
Public Sub Freeze()
' Remember we have frozen the form once more
FreezeCount += 1
' Do nothing if it was already frozen
If FreezeCount > 1 Then Exit Sub
' Create a PictureBox that resizes with its contents
PictureBox = New PictureBox()
PictureBox.SizeMode = PictureBoxSizeMode.AutoSize
' create a bitmap as large as the form's client area and with same color depth
Dim frmGraphics As Graphics = Form.CreateGraphics()
Dim rect As Rectangle = Form.ClientRectangle
PictureBox.Image = New Bitmap(rect.Width, rect.Height, frmGraphics)
frmGraphics.Dispose()
' copy the screen contents, from the form's client area to the hidden bitmap
Dim picGraphics As Graphics = Graphics.FromImage(PictureBox.Image)
picGraphics.CopyFromScreen(Form.PointToScreen(New Point(rect.Left, rect.Top)), New Point(0, 0), New Size(rect.Width, rect.Height))
picGraphics.Dispose()
' Display the bitmap in the picture box, and show the picture box in front of all other controls
Form.Controls.Add(PictureBox)
PictureBox.BringToFront()
End Sub
' unfreeze the form
' Note: calls to Freeze and Unfreeze must be balanced, unless force=true
Public Sub Unfreeze(Optional ByVal force As Boolean = False)
' exit if nothing to unfreeze
If FreezeCount = 0 Then Exit Sub
' remember we've unfrozen the form, but exit if it is still frozen
FreezeCount -= 1
' force the unfreeze if so required
If force Then FreezeCount = 0
If FreezeCount > 0 Then Exit Sub
' remove the picture box control and clean up
Form.Controls.Remove(PictureBox)
PictureBox.Dispose()
PictureBox = Nothing
End Sub
' return true if the form is currently frozen
Public ReadOnly Property IsFrozen() As Boolean
Get
Return FreezeCount > 0
End Get
End Property
' ensure that resources are cleaned up correctly
Public Overridable Sub Dispose() Implements IDisposable.Dispose
Me.Unfreeze(True)
End Sub
End Class
This is the C# version, translated from VB by Claudio:
public class FormFreezer: IDisposable
{
// The form being frozen
Form form;
// the auxiliary PictureBox that will cover the form
PictureBox pictureBox;
// the number of times the Freeze method has been called
int FreezeCount = 0;
// create an instance associated with a given form
// and freeze the form in base of flag freezeIt
public FormFreezer(Form form, bool freezeIt)
{
this.form = form;
if (freezeIt) this.Freeze();
}
// freeze the form
public void Freeze()
{
// Remember we have frozen the form once more
// Do nothing if it was already frozen
if (++FreezeCount > 1)
return;
// Create a PictureBox that resizes with its contents
pictureBox = new PictureBox();
pictureBox.SizeMode = PictureBoxSizeMode.AutoSize;
// create a bitmap as large as the form's client area and with same color depth
Graphics frmGraphics = form.CreateGraphics();
Rectangle rect = form.ClientRectangle;
pictureBox.Image = new Bitmap(rect.Width, rect.Height, frmGraphics);
frmGraphics.Dispose();
// copy the screen contents, from the form's client area to the hidden bitmap
Graphics picGraphics = Graphics.FromImage(pictureBox.Image);
picGraphics.CopyFromScreen(form.PointToScreen(new Point(rect.Left, rect.Top)), new Point(0, 0), new Size(rect.Width, rect.Height));
picGraphics.Dispose();
// Display the bitmap in the picture box, and show the picture box in front of all other controls
form.Controls.Add(pictureBox);
pictureBox.BringToFront();
}
// unfreeze the form
// Note: calls to Freeze and Unfreeze must be balanced, unless force=true
public void Unfreeze(bool force)
{
// exit if nothing to unfreeze
if ( FreezeCount == 0 )
return ;
// remember we've unfrozen the form, but exit if it is still frozen
FreezeCount -= 1;
// force the unfreeze if so required
if (force)
FreezeCount = 0;
if (FreezeCount > 0)
return;
// remove the picture box control and clean up
pictureBox.Controls.Remove(pictureBox);
pictureBox.Dispose();
pictureBox = null;
}
// return true if the form is currently frozen
public bool IsFrozen
{
get { return (FreezeCount > 0); }
}
void IDisposable.Dispose()
{
this.Unfreeze(true);
}
}
Using the FormFreezer class is quite simple. Here's a code sample, which assumes that it is located inside a form class so that the Me keyword points to the current form:
Dim ff As New FormFreezer(Me, True)
' update controls here
' ...
ff.Unfreeze()
The class implements IDisposable, thus you can bracket the update code in a Using block, either in C# or in VB2005, and avoid an explicit call to Unfreeze:
Using New FormFreezer(Me, True)
' Update controls here
' ...
End Using
Notice that calls to Freeze and Unfreeze must be balanced. If you call Freeze twice you then need two calls to Unfreeze to actually restore the updated form. This behavior allows you to call Freeze and then invoke a method that calls Freeze again and still have the code work correctly (provided that all methods use the same instance of the FormFreeze class).