Windows Forms Double Buffering Implementation Details

Published on May 23, 2012

Windows Forms Double Buffering Implementation Details

About what double buffering is written a lot here and here .

Here you can read how Java DB is implemented .

I will tell you how double buffering is implemented in C #. Much of what I wrote here can be read on MSDN, but without implementation details.

Manual control of double buffering (hereinafter referred to as DB)


For manual control of double buffering, the .NET Framework provides the following 3 classes:

BufferedGraphicsManager

The BufferedGraphicsManager class is used to access (via the Current static property ) an object of the BufferedGraphicsContext class associated with the current application domain ( AppDomain ). In fact, Current returns the object of the BufferedGraphicsContext class created in the static constructor . Here is the source code for the BufferedGraphicsManager class :
public sealed class BufferedGraphicsManager
{
  private static BufferedGraphicsContext bufferedGraphicsContext;
  public static BufferedGraphicsContext Current
  {
      get { return BufferedGraphicsManager.bufferedGraphicsContext; }
  }
  static BufferedGraphicsManager()
  {
    BufferedGraphicsManager.bufferedGraphicsContext = new BufferedGraphicsContext();
    AppDomain.CurrentDomain.ProcessExit += new EventHandler(BufferedGraphicsManager.OnShutdown);
    AppDomain.CurrentDomain.DomainUnload += new EventHandler(BufferedGraphicsManager.OnShutdown);
  }
  private static void OnShutdown(object sender, EventArgs e)
  {
    BufferedGraphicsManager.Current.Invalidate();
  }
}

From this code, it is seen that stored inside BufferedGraphicsManager class object BufferedGraphicsContext destroyed at discharge current of the current application domain.

BufferedGraphicsContext

BufferedGraphicsContext provides the creation (as well as destruction) of a new instance of BufferedGraphics based on the Graphics object , providing the only Allocate method for this :
public BufferedGraphics Allocate(Graphics targetGraphics, Rectangle targetRectangle)
{
  if (targetRectangle.Width * targetRectangle.Height > this.MaximumBuffer.Width * this.MaximumBuffer.Height)
    return this.AllocBufferInTempManager(targetGraphics, IntPtr.Zero, targetRectangle);
  else
    return this.AllocBuffer(targetGraphics, IntPtr.Zero, targetRectangle);
}
The method takes as a parameter a Graphics object and the area on it for which you want to create a buffer.
If the area of ​​this area does not exceed the area specified by the MaximumBuffer property, the AllocBuffer method is called and the BufferedGraphics object received from it is returned . The AllocBuffer method creates inside itself (using the CreateBuffer method described below) a new off-screen graph, wraps it in BufferedGraphics , saves it to a buffer object variable and returns it. This variable is used to subsequently destroy the BufferedGraphicsContext instance (methodDispose ), destroy the associated BufferedGraphics instance .
The CreateBuffer method is responsible for creating an off-screen (i.e., stored only in memory, without displaying on the screen) instance of Graphics . Using the native CreateDIBSection function, he creates a “device-independent bitmap” (DIB), on the basis of which he creates a new Graphics object , and returns it as a result. If the transferred area exceeds the MaximumBuffer area , then the AllocBufferInTempManager method is called , the source code of which is given below:


private BufferedGraphics AllocBufferInTempManager(Graphics targetGraphics, IntPtr targetDC, Rectangle targetRectangle)
{
  // Создаем новый "временный" контент
  var bufferedGraphicsContext= new BufferedGraphicsContext();
  // Создаем в нем буферизированное полотно (graphics), которое внутри себя (переменная context) хранит 
  // ссылку на него же
  var bufferedGraphics = bufferedGraphicsContext.AllocBuffer(targetGraphics, targetDC, targetRectangle);
  // ВОТ, этот флаг указывает, что объект bufferedGraphics 
  // при своем уничтожении должен уничтожить породившего его
  // bufferedGraphicsContext:
  bufferedGraphics.DisposeContext = true; 
  return bufferedGraphics;
}

This code shows that inside the AllocBufferInTempManager method , a new BufferedGraphicsContext instance is created , which calls the AllocBuffer method , and returns the BufferedGraphics received from it as a result. Moreover, the created temporary object BufferedGraphicsContext is not destroyed immediately, but only when the created BufferedGraphics is destroyed . To do this, BufferedGraphics stores a backlink to its creator, and upon destruction, if the DisposeContext property is true , it takes it with itself.

Bufferedgraphics

The BufferedGraphics class is very small. Its source code takes up a little over 100 lines. It is a simple wrapper over a Graphics object , and provides a Render method for copying it to another Graphics :
public void Render(Graphics target)

Copying is carried out of the native function of the BitBlt .

Auto DB


The simplest way to use the DB for rendering control is to enable automatic DB for the desired control:
control.DoubleBuffered = true;
or
control.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

Consider what happens to the control when we turn on automatic DB for it. The DoubleBuffered property , like the SetStyle method , is located in the Control class . Take a look at the source code of this class. The DoubleBuffered property code looks like this:
    protected virtual bool DoubleBuffered
    {
      get
      {
        return this.GetStyle(ControlStyles.OptimizedDoubleBuffer);
      }
      set
      {
        if (value != this.DoubleBuffered)
        {
          if (value)
            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, value);
          else
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, value);
        }
      }
    }

As you can see from this code snippet, the above 2 ways to enable DBs are no different from each other, except that the ControlStyles.AllPaintingInWmPaint flag is also set in the DoubleBuffered setter . But since this flag is also set in the control constructor 1 , then if you did not reset it manually, both of these methods have the same effect. From the source code of the Control class , you can also see that the ControlStyles.AllPaintingInWmPaint flag is checked only inside the private WmEraseBkgnd method (and is set only in the constructor and setter of the DoubleBuffered property ), which is responsible for processing the system message
WM_ERASEBKGND 2 , and upon receiving it draws a background control. Here is its implementation:
    private void WmEraseBkgnd(ref Message m)
    {
      if (this.GetStyle(ControlStyles.UserPaint)
        && !this.GetStyle(ControlStyles.AllPaintingInWmPaint))
      {
            ...
            using (PaintEventArgs e = new PaintEventArgs(wparam, ClientRect))
              this.PaintWithErrorHandling(e, (short) 1);
      }
      ...
    }

This shows that if the AllPaintingInWmPaint flag is NOT set, then when the window receives the WM_ERASEBKGND message, the PaintWithErrorHandling method is called , with the layer parameter 1 3 , which in turn causes the control background to redraw 4 .

It is also worth considering the ControlStyles.UserPaint flag . This flag indicates that the contents of the control will be rendered using the Framework.NET tools, and not the system tools. For example, if you set a background image for your form, and clear the UserPaint flag , the picture will not be drawn.

Basic DB actions unfold inside the methodWmpaint . This method is responsible for processing the WM_PAINT system message , which arrives when any part of the control needs to be redrawn. The WmPaint method is private and can only be called from the WndProc method , provided that the ControlStyles.UserPaint flag is set :
protected virtual void WndProc(ref Message m)
{
  switch (m.Msg)
  {
    ...
    case WM_PAINT:
      if (this.GetStyle(ControlStyles.UserPaint))
        this.WmPaint(ref m);
        break;
    ...
  }
}

If we omit non-DB details, then the WmPaint method implementation looks like this:
private void WmPaint(ref Message m)
{
  if (this.DoubleBuffered || this.GetStyle(ControlStyles.AllPaintingInWmPaint) && this.DoubleBufferingEnabled)
  {
    IntPtr num; // нативный хендл Graphics
    Rectangle rectangle; // перерисовываемый участок полотна
    // Инициализация num и rectangle...
    if (rectangle.Width > 0 && rectangle.Height > 0)
    {
        Rectangle clientRectangle = this.ClientRectangle;
        using (BufferedGraphics bufferedGraphics = BufferedGraphicsManager.Current.Allocate(num, clientRectangle))
        {
          Graphics graphics = bufferedGraphics.Graphics;
          graphics.SetClip(rectangle);
          System.Drawing.Drawing2D.GraphicsState gstate = graphics.Save();
          using (PaintEventArgs e = new PaintEventArgs(graphics, rectangle))
          {
            this.PaintWithErrorHandling(e, (short) 1, false);
            graphics.Restore(gstate);
            this.PaintWithErrorHandling(e, (short) 2, false);
            bufferedGraphics.Render();
          }
        }
    }
    ...
  }
  else {
	// отрисовка без двойной буферизации...
  }
}

As you can see from the code snippet above, in order for graphics to be drawn with DB, DoubleBuffered must be true , or the ControlStyles.AllPaintingInWmPaint flag should be set ( DoubleBufferingEnabled is not taken into account since it is always true 5 here ).

Next, using the default BufferedGrpahicsContext, a graphic buffer is created.
For it, a rendering rectangle is set equal to the area for which redrawing is required and the current state is saved.
After that, the OnBackgroundPaint and OnPaint methods are called for it through a call to the PaintWithErrorHandling 3 method, and the resulting image is copied to the control graph.

As you can see for automatic buffering, the same DB method is used as for manual.





1. A fragment of the source code of the constructor of the Control class in which the ControlStyles flags are set :
    internal Control(bool autoInstallSyncContext)
    {
      ...
      this.SetStyle(ControlStyles.UserPaint | ControlStyles.StandardClick | ControlStyles.Selectable | ControlStyles.StandardDoubleClick | ControlStyles.AllPaintingInWmPaint | ControlStyles.UseTextForAccessibility, true);
      ...
    }

2. The WM_ERASEBKGND message arrives when the control is resized. A fragment of the source code in which the WM_ERASEBKGND message is processed :
    protected virtual void WndProc(ref Message m)
    {
      switch (m.Msg)
      {
        ...
        case WM_ERASEBKGND:
          this.WmEraseBkgnd(ref m);
          break;
        ...
      }
    }

3. Implementation of the PaintWithErrorHandling method:
private void PaintWithErrorHandling(PaintEventArgs e, short layer)
{
    ...
    switch (layer)
    {
      case (short) 1:
        if (!this.GetStyle(ControlStyles.Opaque))
          this.OnPaintBackground(e);
        break;
      case (short) 2:
        this.OnPaint(e);
        break;
    }
    ...
}

4. The control background is not redrawn if the ControlStyles.Opaque flag is set .

5. The private property DoubleBufferingEnabled has the following implementation:
    bool DoubleBufferingEnabled
    {
      private get
      {
        return this.GetStyle(ControlStyles.UserPaint | ControlStyles.DoubleBuffer);
      }
    }
Since the WmPaint method is called only if the ControlStyles.UserPaint flag is set , the DoubleBufferingEnabled will always be true here . And since it is closed and is not checked anywhere except WmPaint , its purpose is not clear.