Разработка собственных компонентов – Оптимизация отрисовки графики

У каждого из нас бывают ситуации, когда кажется что того набора компонент, которые доступны в стандартной поставке, недостаточно для комфортной работы. Иногда написание своих компонентов является фатальной необходимостью. Так, например, в моей практике часто возникали ситуации, когда необходимо было разрабатывать новые элементы управления, которые по своей функциональности заменяли бы несколько стандартных элементов. Зачастую к созданию новых элементов управления нас подталкивает мода на всякого рода красивости, которые так любят обычные пользователи.
Но так ли часто мы задумываемся о том, правильно ли выбрана реализация того или иного компонента, на сколько быстро и эффективно работает наш компонент и не будет ли его использование замедлять работу нашей программы? Конечно, сегодня аппаратное обеспечение позволяет всё реже и реже задумываться о таких вещах, но в случаях, когда недоработок слишком много, быстродействие может снижаться очень заметно.
Сегодня я хочу рассказать о том, как же все-таки избавить себя от головной боли при разработке элементов управления и обеспечить максимальное быстродействие при отрисовке графики.

Представим себе ситуацию, что мы решили создать свой компонент и уже определились с его функционалом (у нас это будут аналоговые часы). Прежде чем начинать работу над компонентом, неплохо было бы создать небольшое тестовое приложение, которое позволило бы проверить его работоспособность. Ничего сложного мы делать не будем, и создадим обычное GUI-приложение с одной формой:

PaintingTestMainFrame.h

#ifndef _PAINTING_TEST_MAINFRAME_H
#define _PAINTING_TEST_MAINFRAME_H

#include 
#include "wxPaintingTestCtrl.h"

class PaintingTestMainFrame : public wxFrame
{
	void CreateControls();
public:
	PaintingTestMainFrame(wxWindow * parent, wxWindowID id = wxID_ANY,
		const wxString & title = _("Painting Optimization Test"));
	bool Create(wxWindow * parent, wxWindowID id = wxID_ANY,
		const wxString & title = wxEmptyString,
		const wxPoint & pos = wxDefaultPosition,
		const wxSize & size = wxDefaultSize,
		long style = wxCAPTION|wxSYSTEM_MENU|
					 wxMINIMIZE_BOX|wxMAXIMIZE_BOX|wxCLOSE_BOX|
					 wxRAISED_BORDER|wxRESIZE_BORDER);
};

#endif

PaintingTestMainFrame.cpp

#include "PaintingTestMainFrame.h"


PaintingTestMainFrame::PaintingTestMainFrame(wxWindow * parent, wxWindowID id, const wxString & title)
{
	Create(parent, id, title);
}

bool PaintingTestMainFrame::Create(wxWindow * parent, wxWindowID id, const wxString & title,
	const wxPoint & pos, const wxSize & size, long style)
{
	bool res = wxFrame::Create(parent, id, title, pos, size, style);
	if(res)
	{
		SetMinSize(wxSize(450, 300));
		CreateControls();
	}
	return res;
}

void PaintingTestMainFrame::CreateControls()
{	
}

PaintingTestApp.h

#ifndef _PAINTING_TEST_APP_H
#define _PAINTING_TEST_APP_H

#include 

class PaintingTestApp : public wxApp
{
public:
	virtual bool OnInit();
};

#endif

PaintingTestApp.cpp

#include "PaintingTestApp.h"
#include "PaintingTestMainFrame.h"
#include 

IMPLEMENT_APP(PaintingTestApp);

bool PaintingTestApp::OnInit()
{
	wxImage::AddHandler(new wxPNGHandler);
	PaintingTestMainFrame * frame = new PaintingTestMainFrame(NULL);
	SetTopWindow(frame);
	frame->Centre();
	frame->Show();	
	return true;
}

Теперь можно приступать к написанию класса компонента:

wxPaintingTestCtrl.h

#ifndef _WX_PAINTING_TEST_CTRL_H
#define _WX_PAINTING_TEST_CTRL_H

#include 

#ifndef wxPaintingTestCtrlName 
#define wxPaintingTestCtrlName wxT("wxPaintingTestCtrl")
#endif

class wxPaintingTestCtrl : public wxControl
{
protected:
	wxBitmap m_BackgroundBitmap;
	wxBitmap m_CenterBitmap;
	virtual wxSize DoGetBestSize() const;
	void DoDraw(wxDC & dc);
public:
	wxPaintingTestCtrl(wxWindow * parent, wxWindowID id = wxID_ANY,
		const wxBitmap & bitmap = wxNullBitmap,
		const wxPoint & pos = wxDefaultPosition,
		const wxSize & size = wxDefaultSize,
		long style = wxSIMPLE_BORDER,
		const wxString name = wxPaintingTestCtrlName);
	~wxPaintingTestCtrl();
	bool Create(wxWindow * parent, wxWindowID id = wxID_ANY,
		const wxBitmap & bitmap = wxNullBitmap,
		const wxPoint & pos = wxDefaultPosition,
		const wxSize & size = wxDefaultSize,
		long style = wxSIMPLE_BORDER,
		const wxString name = wxPaintingTestCtrlName);

	void SetBackgroundBitmap(wxBitmap & bitmap);
	const wxBitmap & GetBackgroundBitmap();

	void SetCenterBitmap(wxBitmap & bitmap);
	const wxBitmap & GetCenterBitmap();

	DECLARE_EVENT_TABLE()
	void OnPaint(wxPaintEvent & event);
};

Класс компонента содержит два объекта класса wxBitmap – первый хранит себе фоновое изображение (m_BackgroundBitmap), а второй – изображение циферблата (m_CenterBitmap).

Метод DoGetBestSize() позволяет переопределить размер компонента по умолчанию (у нас это будет wxSize(100,100), см. ниже)

Get/SetBackgroundBitmap и Get/SetCenterBitmap – это, соответственно, aceessor-методы для фонового изображения и для изображения циферблата

OnPaint – Обработчик события wxEVT_PAINT (отвечает за отрисовку графики)

wxPaintingTestCtrl.cpp

#include "wxPaintingTestCtrl.h"
#include 
#include 
#include 

using namespace std;

BEGIN_EVENT_TABLE(wxPaintingTestCtrl, wxControl)
EVT_PAINT(wxPaintingTestCtrl::OnPaint)
END_EVENT_TABLE()

wxPaintingTestCtrl::wxPaintingTestCtrl(wxWindow * parent, wxWindowID id, const wxBitmap & bitmap,
	const wxPoint & pos, const wxSize & size, long style, const wxString name)
{
	Create(parent, id, bitmap, pos, size, style, name);
}

wxPaintingTestCtrl::~wxPaintingTestCtrl()
{
	
}

bool wxPaintingTestCtrl::Create(wxWindow * parent, wxWindowID id, const wxBitmap & bitmap,
	const wxPoint & pos, const wxSize & size, long style, const wxString name)
{
	bool res = wxControl::Create(parent, id, pos, size, style, wxDefaultValidator, name);
	if(res)
	{
		m_BackgroundBitmap = bitmap;		
	}
	return res;
}

wxSize wxPaintingTestCtrl::DoGetBestSize() const
{
	if(m_CenterBitmap.Ok())
	{
		return wxSize(m_CenterBitmap.GetWidth(), m_CenterBitmap.GetHeight());
	}
	return wxSize(100,100);
}

void wxPaintingTestCtrl::SetBackgroundBitmap(wxBitmap & bitmap)
{
	m_BackgroundBitmap = bitmap;
}

const wxBitmap & wxPaintingTestCtrl::GetBackgroundBitmap()
{
	return m_BackgroundBitmap;
}

void wxPaintingTestCtrl::SetCenterBitmap(wxBitmap & bitmap)
{
	m_CenterBitmap = bitmap;
}

const wxBitmap & wxPaintingTestCtrl::GetCenterBitmap()
{
	return m_CenterBitmap;
}

void wxPaintingTestCtrl::OnPaint(wxPaintEvent & event)
{
	wxPaintDC dc(this);
	DoDraw(dc);
}

void wxPaintingTestCtrl::DoDraw(wxDC & dc)
{	
	int x, y, w, h, i;
	double angle;
	GetClientSize(&w, &h);	
	int cx(w/2), cy(h/2), radius(min(cx,cy)-10);	
	if(m_BackgroundBitmap.Ok())
	{		
		for(y = 0; y < h; y += m_BackgroundBitmap.GetHeight())
		{
			for(x = 0; x < w; x += m_BackgroundBitmap.GetWidth())
			{
				dc.DrawBitmap(m_BackgroundBitmap, x, y, true);
			}
		}
	}
	else
	{
		dc.SetBackground(wxBrush(GetBackgroundColour(), wxSOLID));
		dc.Clear();
	}	
	if(m_CenterBitmap.Ok())
	{
		radius = (m_CenterBitmap.GetWidth()/2) * 5.0 / 6.0;
		dc.DrawBitmap(m_CenterBitmap, cx-m_CenterBitmap.GetWidth()/2, 
			cy-m_CenterBitmap.GetHeight()/2, true);
	}
	wxPen linePen(*wxBLACK, 2, wxSOLID);
	dc.SetPen(linePen);
	for(i = 0; i < 12; i++)
	{
		angle = 2.0*3.1415926/12.0*(double)i;
		dc.DrawLine(cx+radius*cos(angle), cy+radius*sin(angle),
			cx+(radius/5.0*4.0)*cos(angle), cy+(radius/5.0*4.0)*sin(angle));
	}
	wxDateTime now = wxDateTime::Now();
	linePen.SetColour(*wxRED);
	linePen.SetWidth(3);
	dc.SetPen(linePen);
	angle = 2.0*3.1415926/12.0*(double)now.GetHour()-3.1415926/2;
	dc.DrawLine(cx, cy, cx+radius*cos(angle), cy+radius*sin(angle));
	linePen.SetWidth(2);
	dc.SetPen(linePen);
	angle = 2.0*3.1415926/60.0*(double)now.GetMinute()-3.1415926/2;
	dc.DrawLine(cx, cy, cx+radius*cos(angle), cy+radius*sin(angle));
	linePen.SetColour(wxColour(0,0,127));
	linePen.SetWidth(1);
	dc.SetPen(linePen);
	angle = 2.0*3.1415926/60.0*(double)now.GetSecond()-3.1415926/2;
	dc.DrawLine(cx, cy, cx+radius*cos(angle), cy+radius*sin(angle));
}
[/sourcecode]
Теперь нам нужно добавить наш компонент на форму

PaintingTestMainFrame.h

...
class PaintingTestMainFrame : public wxFrame
{
	wxPaintingTestCtrl * m_TestCtrl1;
	...
public:
	...	
};
...

PaintingTestMainFrame.cpp

...
void PaintingTestMainFrame::CreateControls()
{
	wxBoxSizer * sizer = new wxBoxSizer(wxVERTICAL);
	SetSizer(sizer);	
	wxBitmap background(wxT("background.png"), wxBITMAP_TYPE_PNG);
	wxBitmap center(wxT("center.png"), wxBITMAP_TYPE_PNG);	
	m_TestCtrl1 = new wxPaintingTestCtrl(this, ID_TEST_CTRL1, background);
	m_TestCtrl1->SetCenterBitmap(center);

	sizer->Add(m_TestCtrl1, 1, wxGROW|wxALL, 5);	
}
...

Запускаем наше приложение и смотрим
При изменении размеров формы наблюдаем артефакты отрисовки
Оптимизация отрисовки графики в wxWidgets - 1
Для того чтобы убрать эти артефакты, нам необходимо переопределить обработчик события изменения размеров нашего компонента

wxPaintingTestCtrl.h

...
class wxPaintingTestCtrl : public wxControl
{
...
	DECLARE_EVENT_TABLE()
	...	
	void OnSize(wxSizeEvent & event);
};
...

wxPaintingTestCtrl.cpp

...
BEGIN_EVENT_TABLE(wxPaintingTestCtrl, wxControl)
...
EVT_SIZE(wxPaintingTestCtrl::OnSize)
END_EVENT_TABLE()
...
void wxPaintingTestCtrl::OnSize(wxSizeEvent & event)
{	
	Refresh();
}
...

Отлично. Собираем наше приложение, запускаем. И что мы видим: сильное мерцание при изменении размеров. Но с этой проблемой мы тоже можем довольно легко справиться, сделав небольшие изменения в исходном коде

wxPaintingTestCtrl.h

...
class wxPaintingTestCtrl : public wxControl
{
...
	DECLARE_EVENT_TABLE()
	...	
	void OnEraseBackground(wxEraseEvent & event);
};
...

wxPaintingTestCtrl.cpp

...
#include 

BEGIN_EVENT_TABLE(wxPaintingTestCtrl, wxControl)
...
EVT_ERASE_BACKGROUND(wxPaintingTestCtrl::OnEraseBackground)
...
END_EVENT_TABLE()
...
void wxPaintingTestCtrl::OnPaint(wxPaintEvent & event)
{
	wxBufferedPaintDC dc(this);
	DoDraw(dc);
}
...

Класс wxBufferedPaintDC обеспечивает doublebuffering при отрисовке, что позволяет избавиться от мерцания.
Пустой обработчик события wxEVT_ERASE_BACKGROUND также позволяет немного ускорить отрисовку.

Собираем приложение, запускаем…. отлично, мерцание исчезло. Но стрелки наших аналоговых часов изменяют положение только при перерисовке содержимого компонента. Это значит, что нам не обходимо добавить таймер, который будет инициировать отрисовку через определенный промежуток времени

wxPaintingTestCtrl.h

...
class wxPaintingTestCtrl : public wxControl
{
protected:
	...
	wxTimer * m_Timer;
	...
	DECLARE_EVENT_TABLE()
	void OnRefreshTimer(wxTimerEvent & event);
};
...

wxPaintingTestCtrl.cpp

...

enum
{
	ID_PAINTING_TEST_CTRL_TIMER = 10001
};
...

BEGIN_EVENT_TABLE(wxPaintingTestCtrl, wxControl)
...
EVT_TIMER(ID_PAINTING_TEST_CTRL_TIMER, wxPaintingTestCtrl::OnRefreshTimer)
END_EVENT_TABLE()
...
void wxPaintingTestCtrl::OnRefreshTimer(wxTimerEvent & event)
{
	Refresh();
}

Собираем, запускаем.
Отлично. Видно что часы идут. Но на этом работа не заканчивается. Разворачиваем окно программы на весь экран, запускаем Task Manager и начинаем перемещать окно Task Manager над компонентом

Оптимизация отрисовки графики в wxWidgets - 2

В результате получаем загрузку процессора на 100%. Это происходит потому что при каждой перерисовке объект класса wxBufferedPaintDC создает изображение размером с наш компонент, отрисовка производится на это изображение в памяти, а потом это изображение отрисовывается на компонент.
Попробуем сделать так, чтобы наш компонент работал быстрее и не потреблял такое огромное количество ресурсов. Для этого мы попробуем реализовать doublebuffering вручную.
Добавим новуые переменные в класс компонента

wxPaintingTestCtrl.h

...
class wxPaintingTestCtrl : public wxControl
{
protected:
	wxBitmap m_DoubleBuffer;
	wxMemoryDC m_DoubleBufferDC;
	...
};
...

wxPaintingTestCtrl.cpp

...
void wxPaintingTestCtrl::OnPaint(wxPaintEvent & event)
{
	wxPaintDC dc(this);
	dc.Blit(0,0, dc.GetSize().GetWidth(), dc.GetSize().GetHeight(), &m_DoubleBufferDC, 0, 0); 
}
...
void wxPaintingTestCtrl::OnSize(wxSizeEvent & event)
{	
	m_DoubleBufferDC.SelectObject(wxNullBitmap);
	m_DoubleBuffer = wxBitmap(event.GetSize().GetWidth(), event.GetSize().GetHeight());
	m_DoubleBufferDC.SelectObject(m_DoubleBuffer);
	DoDraw(m_DoubleBufferDC);
	Refresh();
}

void wxPaintingTestCtrl::OnRefreshTimer(wxTimerEvent & event)
{
	DoDraw(m_DoubleBufferDC);
	Refresh();
}

Как видно, при изменении размеров мы пересоздаем изображение, используемое для doublebuffering’а, ассоциируем с ним контекст устройства m_DoubleBuffer и производим отрисовку на изображение в памяти. А в обработчике OnPaint производим копирование из контекста устройства изображения на контекст устройства компонента. В результате загрузка процессора значительно снизилась
Оптимизация отрисовки графики в wxWidgets - 3
Ну и результатом всей нашей работы будет вот такой компонент – аналоговые часы
Оптимизация отрисовки графики в wxWidgets - 4
Хотелось бы заметить, что реализация doublebuffering’а вручную не всегда является самым удачным выбором. Для компонентов, которые будут иметь небольшой размер на форме, можно использовать wxBufferedPaintDC (или даже wxPaintDC если не требуется отрисовка большого количества графических объектов)

Скачать исходный код примера и проект для VisualStudio 2005 и wxDev-CPP.

3 comments

  1. Begemot   •  

    Спасибо за хорошую статью, мне сегодня на работе сказали применить double buffering и статья мне очень помогла!

    А не могли бы вы еще написать про синхронизацию потоков с использованием критических секций и особенности работы с openCV ? 🙂

  2. admin   •  

    Допросишься у меня 🙂
    Особенности работы с OpenCV… у тебя ведь тоже блог есть. Я думал ты напишешь, но если лень, то могу и я.

  3. Begemot   •  

    Я то напишу, как только минутку свободную найду и дух переведу 🙂 Вообще я хотел прочесть про то что я еще не знаю, а не про то что уже раскопал.. Например про генерацию статических либ под маком или особенности управления камерой.

Leave a Reply

Your email address will not be published. Required fields are marked *

Please leave these two fields as-is: