понедельник, 15 июня 2015 г.

Двойная буферизация GDI+ с WTL

В этом посте рассказано о том, как я использовал двойную буферизацию в простом графическом редакторе, написанном на WTL. Приложение позволяет создавать и редактировать холст с тремя простейшими фигурами — прямоугольником, треугольником и эллипсом. Оно использует GDI+ для рисования фигур, а его исходный код выложен на github.

Мерцание и тиринг

Вывод графики в типичном Windows-приложении, написанном на C++ с MFC или WTL, реализован очень грубо:
  1. Системные и пользовательские контролы, а также пользовательская графика рисуются командами очень-очень древнего GDI или просто древнего GDI+
  2. В качестве холста для рисования выступает клиентская часть окна приложения или дочерние окна-контролы
  3. Команды рисования выполняются в immediate mode — безотлагательно, и изменения в формируемом изображении появляются сразу же. Обычно изображение сначала очищается, а затем заполняется текстом, картинками и геометрическими фигурами.
  4. Операционная система выводит картинку на монитор тогда, когда сочтёт нужным, даже если в этот момент рисунок из предыдущего кадра очищен, а новый нарисован ещё не полностью.
В итоге кадр за кадром на монитор попадают картинки с разной степенью завершённости, и возникает мерцание (flickering). Чтобы его устранить, следует рисовать содержимое окна в Bitmap, а затем одной командой выводить этот Bitmap на экран — это даст гарантию, что неполная картинка не попадёт на монитор. Такая техника называется двойной буферизацией (double buffering).
Даже при двойной буферизации останется неприятное явление под названием тиринг: система по-прежнему выводит картинку на монитор когда ей вздумается, и на экране возникают разрывы — половина экрана уже заполнена новой картинкой, вторая половина ещё держит старую, а где-то посреди экрана лежит резкая граница между частями двух разных кадров.
Эта статья не про борьбу с тирингом, а про двойную буферизацию ради устранения мерцания (flickering); тиринг пускай ещё поживёт.

Двойная буферизация средствами GDI+

Теневой буфер для рисования реализуется классом BackBuffer. Вот файл BackBuffer.h:
class CBackBuffer
{
public:
 CBackBuffer();
 ~CBackBuffer();
 
 std::unique_ptr<gdiplus::graphics> StartRender();
 void SetSize(const Gdiplus::Size &size);
 Gdiplus::Bitmap *GetBitmap() const;

private:
 Gdiplus::Size m_size;
 std::unique_ptr<gdiplus::bitmap> m_bitmap;
};
А вот BackBuffer.cpp:
CBackBuffer::CBackBuffer() = default;

CBackBuffer::~CBackBuffer() = default;

std::unique_ptr<gdiplus::graphics> CBackBuffer::StartRender()
{
 if (!m_bitmap) {
  m_bitmap.reset(new Gdiplus::Bitmap(m_size.Width, m_size.Height));
 }
 const Gdiplus::Color WHITE_COLOR(0xff, 0xff, 0xff);
 std::unique_ptr<gdiplus::graphics> ret{ Gdiplus::Graphics::FromImage(m_bitmap.get()) };
 ret->Clear(WHITE_COLOR);
 return ret;
}

void CBackBuffer::SetSize(const Gdiplus::Size &size)
{
 if (m_size.Width != size.Width || m_size.Height != size.Height) {
  m_size = size;
  m_bitmap.reset();
 }
}

Gdiplus::Bitmap *CBackBuffer::GetBitmap() const
{
 return m_bitmap.get();
}
В свой класс добавьте новое поле для хранения теневого буфера
std::unique_ptr m_backBuffer;
Затем инициализируйте его в конструкторе MyView или в обработчике события WM_CREATE:
LRESULT MyGraphicsView::OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
 [...]
 m_backBuffer.reset(new CBackBuffer);
 return 0;
}
И не забудьте установить размер теневого буфера при событии WM_SIZE:
LRESULT MyGraphicsView::OnResize(UINT, WPARAM, LPARAM lParam, BOOL&)
{
 const int width = LOWORD(lParam);
 const int height = HIWORD(lParam);
 m_backBuffer->SetSize(Gdiplus::Size(width, height));
 return 0;
}
Затем в обработчике WM_PAINT вы создаёте объект Gdiplus::Graphics для вывода в теневой буфер, рисуете всю графику и в конце выводите полученный Bitmap на экран:
LRESULT MyGraphicsView::OnPaint(UINT, WPARAM, LPARAM, BOOL&)
{
 std::unique_ptr<gdiplus::graphics> backGraphics{ m_backBuffer->StartRender() };
 backGraphics->SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
 // [...] Draw content to backGraphics

 CPaintDC dc(m_hWnd);
 std::unique_ptr<gdiplus::graphics> mainGraphics{ Gdiplus::Graphics::FromHDC(dc.m_hDC) };
 mainGraphics->DrawImage(m_backBuffer->GetBitmap(), 0, 0);
 mainGraphics.reset();

 return 0;
}

Комментариев нет:

Отправить комментарий