Выражение благодарности автору оригинальной статьи
Прежде чем перейти к анализу, хочу выразить искреннюю благодарность автору оригинальной статьи.
Его работа — редкий пример глубокого погружения в низкоуровневую отрисовку 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, как привык, и даже не догадывается, что под капотом — двойная буферизация, кэширование фона и прочая магия.