Детали реализации двойной буферизация в Windows Forms

в 4:49, , рубрики: .net, метки: ,

О том, что такое двойная буферизация много написано здесь и здесь.

Здесь можно почитать, как реализуется ДБ на Java.

Я расскажу, как реализуется двойная буферизация на C#. Многое из того, что здесь написал можно прочитать в MSDN, но без деталей реализации.

Ручное управление двойной буферизацией (далее ДБ)

Для ручного управления двойной буферизацией, .NET Framework предоставляет следующие 3 класса:

  • BufferedGraphics – обеспечивает буфер для временного хранения графики и средства вывода её на полотно контрола.
  • BufferedGraphicsContext — обеспечивает создание нового объекта BufferedGraphics на основе объекта Graphics;
  • BufferedGraphicsManager – предоставляет дефолтный BufferedGraphicsContext.

BufferedGraphicsManager

Класс BufferedGraphicsManager служит для получения доступа (через статическое свойство Current) к объекту класса BufferedGraphicsContext ассоциированного с текущим доменом приложения (AppDomain). По факту Current возвращает созданный в статическом конструкторе объект класса BufferedGraphicsContext. Вот исходный код класса BufferedGraphicsManager:

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();
  }
}

Из этого кода видно, что хранящийся внутри BufferedGraphicsManager объект класса BufferedGraphicsContext уничтожается при выгрузке текущего текущего домена приложения.

BufferedGraphicsContext

BufferedGraphicsContext обеспечивает создание (а также уничтожение) нового экземпляра BufferedGraphics на основе объекта Graphics, предоставляя для этого единственный метод Allocate:

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);
}

Метод принимает в качестве параметра объект Graphics и область на нем, для которой нужно создать буфер.
Если площадь этой области не превышает площадь заданную свойством MaximumBuffer , вызывается метод AllocBuffer и возвращается полученный от него объект BufferedGraphics. Метод AllocBuffer создает внутри себя (с помощью метода CreateBuffer, описанного ниже), новый внеэкранный графикс, оборачивает его в BufferedGraphics, сохраняет в переменную объекта buffer и возвращает её. Эта переменная используется для того, чтобы в дальнейшем при уничтожении экземпляра BufferedGraphicsContext (методом Dispose), уничтожить связанный с ним экземпляр BufferedGraphics.
За создание внеэкранного (т.е. хранящегося только в памяти, без отображения на экран) экземпляра Graphics отвечает метод CreateBuffer. Он с помощью нативной функции CreateDIBSection создает «аппаратно-независимый битмап» (DIB), на основе которого создает новый объект Graphics, и возвращает его в качестве результата.

Если площадь переданной области превышает площадь MaximumBuffer, то вызывается метод AllocBufferInTempManager, исходный код которого приведен ниже:

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;
}

Из этого кода видно, что внутри метода AllocBufferInTempManager создается новый экземпляр BufferedGraphicsContext, у которого вызывается метод AllocBuffer, а полученный от её BufferedGraphics возвращается в качестве результата. Причем, созданный временный объект BufferedGraphicsContext не уничтожается сразу же, а только при уничтожении созданного им BufferedGraphics. Для этого BufferedGraphics хранит обратную ссылку на своего создателя, а при уничтожении, если свойство DisposeContext равно true, забирает его с собой.

BufferedGraphics

Класс BufferedGraphics очень маленький. Его исходный код занимает чуть больше 100 строк. Он является простой оберткой над объектом Graphics, и предоставляет метод Render для копирования его на другой Graphics:

public void Render(Graphics target)

Копирование осуществляется нативной функцией BitBlt.

Автоматическая ДБ

Простейший способ использовать ДБ для отрисовки контрола — это включить автоматическую ДБ для нужного контрола:

control.DoubleBuffered = true;

или

control.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

Рассмотрим что происходит с контролом, когда мы включаем для него автоматическую ДБ. Свойство DoubleBuffered, также как метод SetStyle, располагаются в классе Control. Заглянем в исходный код этого класса. Код свойства DoubleBuffered выглядит так:

    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);
        }
      }
    }

Как видно из этого фрагмента кода, приведенные выше 2 способа включения ДБ ничем друг от друга не отличаются, за исключением того, что в сеттере DoubleBuffered устанавливается еще флаг ControlStyles.AllPaintingInWmPaint. Но так как этот флаг устанавливается и в конструкторе контрола1, то если вы его не сбрасывали вручную, оба этих способа имеют одинаковый эффект.
Из исходного кода класса Control, так же можно увидеть что флаг ControlStyles.AllPaintingInWmPaint проверяется только внутри закрытого метода WmEraseBkgnd (а устанавливается только в конструкторе и сеттере свойства DoubleBuffered), который отвечает за обработку системного сообщения WM_ERASEBKGND2, и при получении его отрисовывает фон контрола. Вот его реализация:

    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);
      }
      ...
    }

Отсюда видно, что если флаг AllPaintingInWmPaint НЕ установлен, то при получении окном сообщения WM_ERASEBKGND, происходит вызов метода PaintWithErrorHandling, с параметром layer равным 13, который в свою очередь вызывает перерисовку фона контрола4.

Так же стоит рассмотреть флаг ControlStyles.UserPaint. Этот флаг указывает на то, что содержимое контрола будет отрисовываться средствами Framework.NET, а не средствами системы. Например, если вы зададите фоновую картинку для вашей формы, и сбросите флаг UserPaint, то картинка не будет отрисовываться.

Основные действия по ДБ разворачиваются внутри метода WmPaint. Этот метод отвечает за обработку системного сообщения WM_PAINT, которое прилетает когда какой-либо участок контрола нуждается в перерисовке. Метод WmPaint является закрытым и вызывается только из метода WndProc при условии, что установлен флаг ControlStyles.UserPaint:

protected virtual void WndProc(ref Message m)
{
  switch (m.Msg)
  {
    ...
    case WM_PAINT:
      if (this.GetStyle(ControlStyles.UserPaint))
        this.WmPaint(ref m);
        break;
    ...
  }
}

Если опустить детали не относящиеся к ДБ, то реализация метода WmPaint имеет следующий вид:

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 {
	// отрисовка без двойной буферизации...
  }
}

Как видно из приведенного выше фрагмента кода, для того чтобы графика отрисовывалась с ДБ нужно чтобы DoubleBuffered был равен true, или или чтобы был установлен флаг ControlStyles.AllPaintingInWmPaint (DoubleBufferingEnabled не учитывается т. к. он здесь всегда равен true5).

Далее с помощью дефолтного BufferedGrpahicsContext, создается графический буфер.
Для него устанавливается прямоугольник отрисвовки равный области для кторой требуется перерисовка и сохранятется текущее состояние.
После этого для него вызывается метод OnBackgroundPaint и OnPaint через вызов метода PaintWithErrorHandling3, и полученная картинка копируется в графикс контрола.

Как видно для автоматической буферизации используется тот же самый способ ДБ что и для ручной.


1. Фрагмент исходного кода конструктора класса Control в котором устанавливаются флаги ControlStyles:

    internal Control(bool autoInstallSyncContext)
    {
      ...
      this.SetStyle(ControlStyles.UserPaint | ControlStyles.StandardClick | ControlStyles.Selectable | ControlStyles.StandardDoubleClick | ControlStyles.AllPaintingInWmPaint | ControlStyles.UseTextForAccessibility, true);
      ...
    }

2. Сообщение WM_ERASEBKGND приходит при изменении размера контрола. Фрагмент исходного кода, в котором обрабатывается сообщение WM_ERASEBKGND:

    protected virtual void WndProc(ref Message m)
    {
      switch (m.Msg)
      {
        ...
        case WM_ERASEBKGND:
          this.WmEraseBkgnd(ref m);
          break;
        ...
      }
    }

3. Реализация метода PaintWithErrorHandling:

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. Фон контрола не перерисовывается если установлен флаг ControlStyles.Opaque.

5. Закрытое свойство DoubleBufferingEnabled, имеет следующую реализацию:

    bool DoubleBufferingEnabled
    {
      private get
      {
        return this.GetStyle(ControlStyles.UserPaint | ControlStyles.DoubleBuffer);
      }
    }

Так как метод WmPaint вызывается только при условии что установлен флаг ControlStyles.UserPaint, то DoubleBufferingEnabled здесь всегда будет true. А так как оно закрытое и нигде кроме WmPaint не проверяется, назначение его не понятно.

Автор: fiftin

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js