Выражение благодарности автору оригинальной статьи

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

Комментарии (0)