A brilliant idea on how to avoid flickering during control's updating in .NET


If you're a Windows Form developer, I think that at least once in your life you've seen the problem of the noisy flickering effect when you've to update a big amount of controls on a form via code.
The common controls that usually needs to be updated (for example by adding lots of data to them) are ListBox, ComboBox, TreeView and ListView, and .NET have two wonderful methods to help this process: BeginUpdate and EndUpdate. This methods are really useful to increase performances on a Winform application and must be always used when you've to add a big amount of data to one of these controls.
The usage is really simple:
Public Sub AddToMyListBox()
   
 
' Stop the ListBox from drawing while items are added.
    listBox1.BeginUpdate()
      
 
   
 
' Loop through and add five thousand new items.
   
 Dim x As Integer
   
 For x = 1 To 4999
        listBox1.Items.Add(
"Item " & x.ToString())
   
 
Next x
   
 
' End the update process and force a repaint of the ListBox.
    listBox1.EndUpdate()
End Sub
In summary, BeginUpdate maintains performance while items are added to the ListBox (or one of the other controls above) one at a time by preventing the control from drawing until the EndUpdate method is called.
But what happens when you've to update a control that doesn't expose these two methods? Apparently, there's not a real solution to this problem.
Stimulated by this question, Francesco Balena (one that always has brilliant ideas) has found an interesting solution, unfortunately blogged only in italian, but that I think it merits to be published to the entire community.
His idea can be summarized as follow:
  1. Save the aspect of your form pixel by pixel by copying it on a bitmap image
  2. Create a PictureBox control with the size of your form and load the created bitmap into this PictureBox
  3. Add your PictureBox to the Controls collection and bring it to front, in order to cover all the other controls
  4. Update all your controls (using BeginUpdate/EndUpdate methods) while the user is seeing the fixed image of your form (no flickering effect)
  5. When the update operation is completed, delete the PictureBox and show the real form content
The class that you can use to make this work is this:
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 a nice class with a simple usage. If you have a Windows Form and you need to update its controls as previously described, you can use the code as follow:
   Dim ff As New FormFreezer(Me, True)
  
 
' update controls here
  
 ' ...
  
 
ff.Unfreeze()
Here, Me is your current form, where the controls must be updated.
As pointed by Francesco, the class implements IDisposable so you can use the Usingclause and avoid to explicitly call the Unfreeze method, as follow:
   Using New FormFreezer(Me, True)
     
 ' Update controls here
     
 ' ...
  
 
End Using
Calls to Freeze and Unfreeze methods must be balanced (n calls to Freeze requires ncalles to Unfreeze at the end).
This is a brilliant idea for a problem that sometimes is really a noise for the end user. Thanks to Francesco for this nice tip and I hope that he permits me to share his ideas to all the "non-Italian" community. 
If someone has a more brilliant way to solve this problem, I'm glad to receive suggestions (and I think Francesco will be too).
P.S. the C# version of the class is this:
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(newPoint(rect.Left, rect.Top)), new Point(0, 0), newSize(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);
   }
}

0 comments:

Post a Comment