Выражение благодарности автору оригинальной статьи
Прежде чем перейти к анализу, хочу выразить искреннюю благодарность автору оригинальной статьи.
Его работа — редкий пример глубокого погружения в низкоуровневую отрисовку VCL, а реализация двойной буферизации с кэшированием фона действительно решает насущную проблему мерцания.
Именно такие публикации двигают малочисленное, но увлечённое сообщество C++ Builder вперёд.
Однако, как это часто бывает с техническими решениями, хорошая идея может быть реализована неоптимально с точки зрения архитектуры. И сегодня я хочу обсудить именно этот аспект — не для того, чтобы «опровергнуть», а чтобы предложить более совместимый и устойчивый подход.
Код с Delphi был переписан на С++
/*
Метод PaintWindow — это точка входа для отрисовки контрола в ответ на WM_PAINT. Именно здесь Windows предоставляет HDC, в который нужно рисовать. Стандартная реализация VCL не использует двойную буферизацию, что приводит к мерцанию. Ниже — реализация, которая решает эту проблему, но…
*/
void __fastcall TEsWinControl::PaintWindow(HDC DC)
{
HDC TempDC = nullptr;
TRect UpdateRect;
HDC BufferDC = nullptr;
HBITMAP BufferBitMap = nullptr;
HRGN Region = nullptr;
TPoint SaveViewport;
bool BufferedThis = !BufferedChildren || ComponentState.Contains(csDesigning);
if (::GetClipBox(DC, &UpdateRect) == ERROR)
UpdateRect = ClientRect;
try
{
if (BufferedThis)
{
if (!DoubleBuffered)
{
BufferDC = ::CreateCompatibleDC(DC);
if (BufferDC)
{
if (FIsCachedBuffer || FIsFullSizeBuffer)
{
if (!CacheBitmap)
{
BufferBitMap = ::CreateCompatibleBitmap(DC, ClientWidth, ClientHeight);
if (FIsCachedBuffer)
CacheBitmap = BufferBitMap;
}
else
BufferBitMap = CacheBitmap;
Region = ::CreateRectRgnIndirect(&UpdateRect);
::SelectClipRgn(BufferDC, Region);
}
else
{
BufferBitMap = ::CreateCompatibleBitmap(DC,
RectWidth(UpdateRect), RectHeight(UpdateRect));
}
::SelectObject(BufferDC, BufferBitMap);
if (!(FIsCachedBuffer || FIsFullSizeBuffer))
{
::GetViewportOrgEx(BufferDC, &SaveViewport);
::SetViewportOrgEx(BufferDC,
-UpdateRect.Left + SaveViewport.x,
-UpdateRect.Top + SaveViewport.y, nullptr);
}
}
else
BufferDC = DC;
}
else
BufferDC = DC;
}
else
BufferDC = DC;
// Background drawing
if (!ControlStyle.Contains(csOpaque))
{
if (ParentBackground)
{
if (FIsCachedBackground)
{
if (!CacheBackground)
{
TempDC = ::CreateCompatibleDC(DC);
CacheBackground = ::CreateCompatibleBitmap(DC, ClientWidth, ClientHeight);
::SelectObject(TempDC, CacheBackground);
DrawBackground(TempDC);
::DeleteDC(TempDC);
}
TempDC = ::CreateCompatibleDC(BufferDC);
::SelectObject(TempDC, CacheBackground);
::BitBlt(BufferDC, UpdateRect.Left, UpdateRect.Top,
RectWidth(UpdateRect), RectHeight(UpdateRect),
TempDC, UpdateRect.Left, UpdateRect.Top, SRCCOPY);
::DeleteDC(TempDC);
}
else
DrawBackground(BufferDC);
}
}
else
{
if (!DoubleBuffered || DC)
{
TRect rc = ClientRect;
TColor FillColor = StyleServices()->GetSystemColor(Color);
::SetDCBrushColor(BufferDC, ColorToRGB(FillColor));
::FillRect(BufferDC, &rc, (HBRUSH)::GetStockObject(DC_BRUSH));
}
}
FCanvas->Lock();
try
{
Canvas->Handle = BufferDC;
static_cast<TControlCanvas*>(Canvas)->UpdateTextFlags();
/***********************************************************/
if (OnPainting)
OnPainting(this, Canvas, ClientRect);
Paint();
if (OnPaint)
OnPaint(this, Canvas, ClientRect);
/***********************************************************/
}
__finally
{
Canvas->Handle = nullptr;
FCanvas->Unlock();
}
}
__finally
{
if (BufferedThis)
{
try
{
if (!DoubleBuffered)
{
if (!(FIsCachedBuffer || FIsFullSizeBuffer))
{
::SetViewportOrgEx(BufferDC, SaveViewport.x, SaveViewport.y, nullptr);
::BitBlt(DC, UpdateRect.Left, UpdateRect.Top,
RectWidth(UpdateRect), RectHeight(UpdateRect),
BufferDC, 0, 0, SRCCOPY);
}
else
{
::BitBlt(DC, UpdateRect.Left, UpdateRect.Top,
RectWidth(UpdateRect), RectHeight(UpdateRect),
BufferDC, UpdateRect.Left, UpdateRect.Top, SRCCOPY);
}
}
}
__finally
{
if (BufferDC != DC)
::DeleteDC(BufferDC);
if (Region != 0)
::DeleteObject(Region);
if (!FIsCachedBuffer && BufferBitMap != 0)
::DeleteObject(BufferBitMap);
}
}
}
}Недавно в одной статье на Хабре был предложен кастомный TEsWinControl со следующим событием:
void __fastcall OnPaint(TObject* Sender, TCanvas* Canvas, const TRect& Rect);
На первый взгляд — удобно: всё сразу передано. Но на деле это нарушает контракт VCL.
Ошибка №1: «Удобный» OnPaint ломает совместимость
Вспомните: у TForm, TPanel, TButton — везде один и тот же тип:
__property TNotifyEvent OnPaint; // т.е. void __fastcall(TObject* Sender)
Если вы меняете сигнатуру, ваш компонент:
Нельзя использовать в шаблонах, где ожидается наследование от
TWinControl.Требует уникального кода обработки, который не работает с другими контролами.
Ломает условную компиляцию: заменить
TWinControlнаTEsWinControlчерез#defineтеперь невозможно — придётся править множество мест, где используется данная сигнатура метода
Ошибка №2: Canvas уже есть — зачем его передавать?
При написание события возникает мысль «Почему бы не передать Canvas, чтобы пользователю было проще». Но это избыточно.
Во-первых, Canvas доступен напрямую:
void __fastcall MyPaint(TObject* Sender)
{
Canvas->Rectangle(0, 0, 100, 100);
}
Во-вторых, при двойной буферизации Canvas->Handle временно переназначается на буфер. Пользователь не должен знать об этом — он просто рисует, а компонент сам заботится о том, куда попадут пиксели.
Передача Canvas в параметре:
Нарушает инкапсуляцию,
Создаёт иллюзию «особого» канваса,
Вынуждает пользователя думать о внутренней реализации.
Canvas должен оставаться свойством. А магия отрисовки в PaintWindow
Ошибка №3: Rect вводит в заблуждение
В коде из статьи Rect всегда равен ClientRect:
OnPaint(this, Canvas, ClientRect);
Но зачем тогда его передавать? Это создаёт ложное впечатление, что:
Нужно рисовать только в
Rect,Это область повреждения (как
ps.rcPaint),Поведение отличается от стандартного
OnPaint.
На самом деле, VCL всегда ожидает полной перерисовки в OnPaint. Частичность обрабатывается на уровне оконной системы (WM_PAINT + GetClipBox), но никогда не должна просачиваться в пользовательский код.
Пример правильного использования
class TEsPaint : public
#ifdef USE_ES_WINCONTROL
TWinControl
#else
TEsWinControl
#endif // USE_ES_WINCONTROL
{
// Создаем событие с правильной сигнатурой
void __fastcall FormPaintCanvas(TObject* Sender);
void draw_shapes(TCanvas* Canvas) { /* код */ };
void draw_shapes(TCanvas* Canvas, const TPoint& AStartPoint, const TPoint& AEndPoint, const int& ATypeFigure) { /* код */ };
TPoint FStartPoint = const_value::InvalidPoint;
TPoint FEndPoint = const_value::InvalidPoint;
int FTypeFigure = const_value::InvalidInt;
bool FDrawing = false;
public: // События
__fastcall TEsPaint(TComponent* Owner);
__fastcall virtual ~TEsPaint();
__property OnPaint;
};
__fastcall TEsPaint::TEsPaint(TComponent* Owner) : TEsWinControl(Owner) {
OnPaint = FormPaintCanvas;
};
void __fastcall TEsPaint::FormPaintCanvas(TObject* Sender) {
// Используем собственный Canvas
// 1. Рисуем все сохранённые фигуры
draw_shapes(Canvas);
// 2. Если идёт рисование — рисуем
if (FDrawing)
{
draw_shape(Canvas, FStartPoint, FEndPoint, FTypeFigure);
draw_lines(Canvas, FStartPoint, FEndPoint, FTypeFigure);
}
};
Сохраняется максимальная гибкость с VCL-архитектурой
Сравнение с официальными компонентами VCL
Рассмотрим компонент TCustomPanel . Его метод TCustomPanel::Paint() — переопределяет отрисовку, но OnPaint остаётся стандартным по сигнатуре. У TGraphicControl рисуется напрямую в Canvas но не передаёт его в событие. А TDBGrid это вообще сложнейший компонент, но его OnDrawColumnCell это расширение, а не замена OnPaint. Они не ломают контракт. Они расширяют функционал, добавляя новые события, если нужно, но не меняют старые.
Кастомный контрол — не повод выдумывать свой API.
Настоящая сложность — не в том, чтобы наворотить кучу параметров, а в том, чтобы спрятать всю сложность внутри, а снаружи оставить всё так же просто, как в стандартном TWinControl.
Если приходится писать отдельный обработчик, который больше ни с чем не работает, если приходится помнить, что «тут Canvas передаётся, а тут — нет», или если замена TWinControl на ваш класс ломает половину формы — вы не упростили ему жизнь. Вы просто переложили свою головную боль на него.
А суть хорошего компонента как раз в другом: пусть он делает всё сам, а пользователь рисует в Canvas, как привык, и даже не догадывается, что под капотом — двойная буферизация, кэширование фона и прочая магия.