📜 ⬆️ ⬇️

Details of the implementation of double buffering in Windows Forms

Much has been written here and here about double buffering.

Here you can read how DB is implemented in Java .

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

Manual double buffering (hereinafter referred to as DB)


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

BufferedGraphicsManager

The BufferedGraphicsManager class is used to access (through the static Current property) an object of the BufferedGraphicsContext class associated with the current application domain ( AppDomain ). In fact, Current returns a BufferedGraphicsContext class object created in the static constructor. Here is the source code of 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, you can see that the object of the class BufferedGraphicsContext stored inside the BufferedGraphicsManager is destroyed when the current application domain is unloaded.

BufferedGraphicsContext

BufferedGraphicsContext provides the creation (and also destruction) of a new BufferedGraphics instance based on the Graphics object, providing for this a single Allocate method:
 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 an 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 resulting BufferedGraphics object is returned. The AllocBuffer method creates inside of itself (using the CreateBuffer method described below), a new offscreen graph, wraps it in BufferedGraphics , saves it into a variable of the buffer object and returns it. This variable is used to further, when destroying an instance of BufferedGraphicsContext (using the Dispose method), to destroy the associated instance of BufferedGraphics .
The creationBuffer method is responsible for creating an offscreen (i.e., stored only in memory without displaying on the screen) of the Graphics instance . Using the native function CreateDIBSection, it creates a “hardware-independent bitmap” (DIB), on the basis of which it creates a new Graphics object, and returns it as a result.

If the area of ​​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; } 

From this code, you can see that inside the AllocBufferInTempManager method, a new instance of BufferedGraphicsContext is created , in which the AllocBuffer method is called , and the resulting BufferedGraphics is returned as a result. Moreover, the temporary BufferedGraphicsContext object created is not immediately destroyed, but only when the BufferedGraphics created by it is destroyed. For this, BufferedGraphics keeps a reciprocal link to its creator, and when destroyed, if the DisposeContext property is true , it takes it with itself.

BufferedGraphics

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

Copying is done by the native BitBlt function.

Automatic db


The simplest way to use db to render a control is to turn on automatic db for the desired control:
 control.DoubleBuffered = true; 
or
 control.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); 

Consider what happens to the control when we enable automatic DB for it. The DoubleBuffered property, as well as the SetStyle method, is located in the Control class. Let's look at the source code of this class. The code for the DoubleBuffered property 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 of including dBs do not differ from each other, except that the ControlStyles.AllPaintingInWmPaint flag is set in the DoubleBuffered setter. But since this flag is also set in the constructor of the control 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 WM_ERASEBKGND 2 system message, and when it is received, it draws the background of the 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 a WM_ERASEBKGND message, it calls the PaintWithErrorHandling method, with the layer parameter equal to 1 3 , which in turn causes the redrawing of the background of the control 4 .

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

The main actions on DB are deployed inside the WmPaint method. This method is responsible for processing the WM_PAINT system message, which arrives when a section of the control needs to be redrawn. The WmPaint method is private and only 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 the details not related to the DB, the implementation of the WmPaint method is as follows:
 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 can be seen from the above code snippet, in order for graphics to be drawn with db, DoubleBuffered should be true , or ControlStyles flag should be set. AllPaintingInWmPaint ( DoubleBufferingEnabled is not taken into account because it is always true 5 here ).

Next, using the default BufferedGrpahicsContext, a graphic buffer is created.
It sets a draw rectangle equal to the area for which a redraw 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 into the graphics control.

As it can be seen, the same DB method is used for automatic buffering as for manual one.





1. A fragment of the source code of the Control class constructor 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. Source code fragment 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 background of the control is not redrawn if the ControlStyles.Opaque flag is set.

5. 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, DoubleBufferingEnabled will always be true here . And since it is closed and is not checked anywhere except WmPaint , its purpose is not clear.

Source: https://habr.com/ru/post/144294/


All Articles