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

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

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


  1. HemulGM
    17.01.2026 02:49

    Передача Canvas в событии удобна тем, что не нужно кастить Sender к типу контрола и обращаться к его канве. Тем более, что Sender может даже не быть самим этим контролом. А канвас в VCL у каждого контрола свой.


    1. 1QDenisQ Автор
      17.01.2026 02:49

      Зачем выполнять лишнее приведение типа (static_cast) Sender к конкретному контролу, если нужная канва уже передаётся __poperty Canvas = { read = FCanvas } достаточно взять единственно поле которая должна быть единой точкой истины? Кроме того, вызывать событие OnPaint одного контрола через другой — архитектурно некорректно. Каждый контрол отвечает за собственную отрисовку, и его Canvas привязан именно к нему.


      1. HemulGM
        17.01.2026 02:49

        В обработчике события нет никакого прямого доступа. Это же событие. Оно может быть обратно кем угодно и когда угодно.


        1. 1QDenisQ Автор
          17.01.2026 02:49

          В обработчике события нет никакого прямого доступа

          Нет прямого доступа к чему? Если вы имеете ввиду Canvas, то я прошу вас первым делом ознакомиться с исходниками класса. Уточняйте свой ответ

          но может быть обратно кем угодно и когда угодно.

          С точки зрения использование кода - да, но с точки зрения построения архитектуры нет. И как вы это вообще представляете

          class TEsPaint;
          
          class TForm1 : public TForm
          {
          __published:	// IDE-managed Components
          
          private:
          	TEsPaint* MyControl = nullptr;
          	TEsPaint* MyControl2 = nullptr;
          public:		// User declarations
          };
          //---------------------------------------------------------------------------
          extern PACKAGE TForm1 *Form1;
          //---------------------------------------------------------------------------
          #endif
          
          
          
          #include <vcl.h>
          #pragma hdrstop
          
          #include "Unit1.h"
          
          #include <System.SysUtils.hpp>
          #include <System.Classes.hpp>
          
          #include "TEsPaint.hpp"
          
          #pragma package(smart_init)
          #pragma resource "*.dfm"
          TForm1 *Form1;
          
          __fastcall TForm1::TForm1(TComponent* Owner)
          	: TForm(Owner)
          {
              MyControl = new TEsPaint(this);
              MyControl->Parent = this;
          
              MyControl2 = new TEsPaint(this);
              MyControl->Parent2 = this;
          }
          
          __fastcall TForm1::~TForm1()
          {
              delete MyControl;
              MyControl = nullptr;
              delete MyControl2;
              MyControl2 = nullptr;
          }
          
          void __fastcall TForm1::Button1Click(TObject *Sender)
          {
              MyControl1->OnPaint(MyControl2);
          }
          

          Вы что, собирается вручную вызывать метод OnPaint и вручную передавать контрол MyControl1->OnPaint(MyControl2); ?

          У ваше контрала должен быть собственный обработчик отрисовки. И он обычно вручную не вызывается. Используется Invalidate(...) или Update()


          1. HemulGM
            17.01.2026 02:49

            Вот у вас есть некий контрол с OnPaint событием, которое просто TNotifyEvent. И есть у вас некий контроллер, который будет использовать контролы и задавать обработчики событий контролам формы (вашего представления).

            При подключении представления, будет написано
            MyControl.OnPaint := FMyControlPaint;,
            где FMyControlPaint обработчик события отрисовки этого контрола. Внутри этого обработчика нет никакого доступ к контролу, кроме как через Sender и тем более, нет никакого прямого доступа к Canvas.

            procedure TFormController.FMyControlPaint(Sender: TObject);
            begin
              //Хочу что-то рисовать на контроле
              //Каким образом я здесь получу доступ к Canvas?
            end;

            При этом, обращаться просто к MyControl - нельзя, ведь я могу использовать этот обработчик и для других таких контролов.

            MyControl1.OnPaint := FMyControlPaint;
            MyControl2.OnPaint := FMyControlPaint;
            MyControl3.OnPaint := FMyControlPaint;

            Сам контрол, вообще никогда не должен сам использовать свой OnPaint, потому что он генерирует это событие для работы с контролом именно снаружи. Контрол должен использовать напрямую метод Paint, который он должен перекрыть и рисовать что нужно.


          1. HemulGM
            17.01.2026 02:49

            Просто добавьте любой контрол на форму и создайте обработчик события отрисовки. Покажите как вы будете обращаться к Canvas напрямую.


            1. 1QDenisQ Автор
              17.01.2026 02:49

              Сам TEsWinControl уже содержит свой экземпляр Canvas, он не устанавливается извне. То есть MyControl1 , MyControl2 , MyControl3 , MyControl4 содержат свои Canvas. Зачем определять кому чей?

              Каким образом я здесь получу доступ к Canvas?

              Я вам ещё раз говорю, что является членом класса __property Canvas = { read = FCanvas }. Как он тогда вообще может быть не доступен в принципе, если он передается явно в механизме обработки сообщений?

              Он доступен для чтения, но не для модификации


              1. HemulGM
                17.01.2026 02:49

                Вы действительно не понимаете о чем я говорю? Читайте внимательно мой комментарий ещё раз. Создайте обработчик события отрисовки. И внутри него обратитесь к канвасу контрола в этом обработчике!! Попробуйте и покажите что получилось


                1. 1QDenisQ Автор
                  17.01.2026 02:49

                  Вот пример из статьи

                  class TEsPaint : public 
                  TEsWinControl
                  {в
                  	// Создаем событие с правильной сигнатурой
                  	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);
                  	}
                  };

                  Canvas спокойно доступен. В чём проблема?


                  1. HemulGM
                    17.01.2026 02:49

                    Вы делаете это внутри класса контрола, а не снаружи. События нужны для работы с контролом снаружи!

                    Обратитесь к контролу снаружи и назначьте ему обработчик события отрисовки.

                    Чтобы рисовать в контроле изнутри класса контрола вообще нельзя использовать событие отрисовки!


                    1. 1QDenisQ Автор
                      17.01.2026 02:49

                      
                      #include <vcl.h>
                      #pragma hdrstop
                      
                      #include "Unit1.h"
                      
                      #include <System.SysUtils.hpp>
                      #include <System.Classes.hpp>
                      
                      #include "TEsPaint.hpp"
                      
                      #pragma package(smart_init)
                      #pragma resource "*.dfm"
                      TForm1 *Form1;
                      
                      __fastcall TForm1::TForm1(TComponent* Owner)
                          : TForm(Owner)
                      {
                          MyControl = new TEsPaint(this);
                          MyControl->Parent = this;
                          MyControl->OnPaint = FormPaintCanvas;
                      
                      }
                      
                      __fastcall TForm1::~TForm1()
                      {
                          delete MyControl;
                          MyControl = nullptr;
                      }
                      
                      void __fastcall TForm1::FormPaintCanvas(TObject* Sender) {
                          MyControl->Canvas;
                      };
                      

                      Canvas контрола доступен. Что дальше?


                      1. HemulGM
                        17.01.2026 02:49

                        А теперь попробуйте назначить этот обработчик сразу нескольким контролам


                      1. 1QDenisQ Автор
                        17.01.2026 02:49

                        У каждого контрола должен быть свой обработчик а не один контрол на всех. Вы же не носите одни трусы всей семьёй? Так и тут так же.


                      1. HemulGM
                        17.01.2026 02:49

                        Зато мы используем одну общую дверь для входа в дом, а не каждый свою


                      1. 1QDenisQ Автор
                        17.01.2026 02:49

                        Дверью в дом, является конструктор класса и инициализация значений. А не выполнение кода (ношение белья)


                      1. HemulGM
                        17.01.2026 02:49

                        Речь о том, что логика может быть разной и один обработчик событий для нескольких контролов - это обычное дело.

                        Генерировать десяток кнопок с одним обработчиком, который различает контроля, например по тегу, это база


                      1. 1QDenisQ Автор
                        17.01.2026 02:49

                        Тогда в данном случае необходимо использовать расширенный метод. К примеру OnPaintExt с передачей Canvasи Rect. Если вы говорите о том, что я сказал фигню, то ваши слова полностью позиционируются как отрицание корректности полностью всего фреймворка в целом. Расширяйте Control и добавляйте новый метод. Метод OnPaintявляется ядром отрисовки, и не может быть модифицирован с точки зрения архитектуры VCL. TForm, TButton, TLabel- у них у всех единый метод с одной сигнатурой. И эта сигнатура не меняется с точки зрения имени метода, так как создает согласованность и единообразие. По вашей логике, можно тогда вообще все члены класса передать. А какая разница. Удобно же. Вы себя позиционируете как профессиональный разработчик на Delphi, но профессиональный разработчик, это разработчик который следует не только коду, но и философии языка.


                      1. HemulGM
                        17.01.2026 02:49

                        Философия языка тут не при чем. Здесь речь о несовершенстве фреймворка VCL. OnPaint - обычное событие, которое является конечным и должно использоваться в конечном коде, на более высоком (пользовательском) уровне. Любые низкоуровневые вещи должны работать через наследование и перекрытие методов.


                      1. 1QDenisQ Автор
                        17.01.2026 02:49

                        Мне кажется, что мы не понимаем друг друга потому что взгляд лежит в разных областях к тому, как необходимо писать код. Моё понимание лежит в области разработчика VCL, которые поправляет баги, следует стандарту и принципам проектирования API. Вы видите код, как конечный потребитель, для которого каждый метод просто "обычный", который может просто взять и расширить сигнатуру, нарушив принцип наименьшего удивление, когда вы пишет 15 лет OnPaint с одной сигнатурой, а потом узнаете что там еще есть, то нам больше не о чем говорить. Тогда с вашей точки зрения, архитектура полного контрола является неккоректной.


  1. CatAssa
    17.01.2026 02:49

    А где же тег "Delphi"?


    1. 1QDenisQ Автор
      17.01.2026 02:49

      Потерялся :) Я особо не подумал об этом теги, думал что тут обсуждает конкретно С++ синтаксис. В будущем может быть буду добавлять, с учётом того что обсуждается VCL и Delphi, конечно, не разрывно связан с ним


  1. Error1024
    17.01.2026 02:49

    Аргументированная критика - это безусловно хорошо, но как автор "оригинала", не соглашусь по всем пунктам:

    Ошибка №1: «Удобный» OnPaint ломает совместимость

    Вспомните: у TForm, TPanel, TButton — везде один и тот же тип:

    __property TNotifyEvent OnPaint; // т.е. void __fastcall(TObject* Sender)

    Если вы меняете сигнатуру, ваш компонент:

    • Нельзя использовать в шаблонах, где ожидается наследование от TWinControl.

    • Требует уникального кода обработки, который не работает с другими контролами.

    • Ломает условную компиляцию: заменить TWinControl на TEsWinControl через #define теперь невозможно — придётся править множество мест, где используется данная сигнатура метода

    Не ясно о чем вообще речь, у TWinControl и TButton - нет свойства OnPaint, следовательно - никакой контракт TWinControl не был нарушен.

    То, что у TCustomControl тоже есть свойство OnPaint, которое отличается по сигнатуре - не более чем совпадение. В VCL куча мест, где одно и тоже свойство имеет разный тип в "дальних" ветках TControl.

    TEsCustomControl это не наследник TCustomControl,

    TEsCustomControl - это наследник TWinControl, и не должен соблюдать контракты TCustomControl.

    Про #define - а зачем это вообще? Все, что я обеспечиваю - это работоспособность FreeEsVclComponents в C++Builder, игры с #define - это уже не ко мне.

    Ошибка №2: Canvas уже есть — зачем его передавать?

    При написание события возникает мысль «Почему бы не передать Canvas, чтобы пользователю было проще». Но это избыточно.

    Во-первых, Canvas доступен напрямую:

    Нет, у TEsCustomControl свойство Canvas не доступно напрямую. Свойство Canvas объявлено в секции protected, исключительно для удобства написания наследников данного компонента. Из кода "снаружи" Canvas не доступен.

    Canvas передается в событие OnPaint, и это сделано специально, чтобы у пользователя не возникало "соблазна" рисовать вне события OnPaint, что не приводит ни к чему хорошему.

    То, что в TCustomControl свойство Canvas доступно вне событий отрисовки - ошибка дизайна. Причем ошибка дизайна WinApi, которая "воссоздана" в VCL.

    Ошибка №3: Rect вводит в заблуждение

    В коде из статьи Rect всегда равен ClientRect

    Параметр Rect был добавлен для удобства, дабы из Sender не надо было вытягивать ClientRect для "заливки" цветом и т.д.. Кому не надо - могут не использовать.

    Сравнение с официальными компонентами VCL

    Рассмотрим компонент TCustomPanel . Его метод TCustomPanel::Paint() — переопределяет отрисовку, но OnPaint остаётся стандартным по сигнатуре. У TGraphicControl рисуется напрямую в Canvas но не передаёт его в событие. А TDBGrid это вообще сложнейший компонент, но его OnDrawColumnCell это расширение, а не замена OnPaint. Они не ломают контракт. Они расширяют функционал, добавляя новые события, если нужно, но не меняют старые.

    TCustomPanel - наследник TCustomControl и обязан соблюдать его контракт.

    TEsCustomControl - не наследник TCustomControl и не должен соблюдать его контракт.

    Если бы я унаследовал TEsCustomControl от компонента с событием OnPaint и поменял бы сигнатуру события, то да, я нарушил бы контракт OnPaint. Но я создал наследника от TWinControl, у которого нет никакого OnPaint, контракт которого я должен был бы соблюдать.

    Совпали имена событий в разных "ветках"? Да. Бывает. Это нормально.

    Если приходится писать отдельный обработчик, который больше ни с чем не работает, если приходится помнить, что «тут Canvas передаётся, а тут — нет», или если замена TWinControl на ваш класс ломает половину формы — вы не упростили ему жизнь. Вы просто переложили свою головную боль на него.

    Да, при использовании TEsCustomControl приходиться задуматься, когда видишь другую сигнатуру OnPaint - это фича. Приходит понимание, почему рисовать на Canvas компонента можно только в OnPaint. И почему Canvas доступный "всегда" - это "багофича".

    А суть хорошего компонента как раз в другом: пусть он делает всё сам, а пользователь рисует в Canvas, как привык, и даже не догадывается, что под капотом — двойная буферизация, кэширование фона и прочая магия.

    Если из TEsCustomControl вытащить Canvas, и начать рисовать вне события OnPaint "как привык", что часто встречается в древнем коде, то будут какие угодно глюки.

    Я сделал компонент с API запрещающим рисовать вне специально отведенного места. Это стандартный подход для GUI библиотек.

    ---

    В конце концов, это всего лишь мой взгляд на "правильный" API OnPaint, если он не нравиться, то исходники открыты, можно сделать как удобно, пока соблюдается лицензия.

    Смысл оригинальной статьи - дать набор идей и реализацию, которую каждый может доработать под себя. Вы доработали, нашли применение? - Отлично, поделитесь кейсом использования "в продакшене", как автору, это мне интереснее, чем абстрактная "красота" API.


    1. 1QDenisQ Автор
      17.01.2026 02:49

      TCustomPanel - наследник TCustomControl и обязан соблюдать его контракт.

      TEsCustomControl не наследник TCustomControl и не должен соблюдать его контракт.

      Если бы я унаследовал TEsCustomControl от компонента с событием OnPaint и поменял бы сигнатуру события, то да, я нарушил бы контракт OnPaint. Но я создал наследника от TWinControl, у которого нет никакого OnPaint, контракт которого я должен был бы соблюдать.

      Совпали имена событий в разных "ветках"? Да. Бывает. Это нормально.

      Ваш TEsWinControl является аналогом TCustomControl. Значит он должен соблюдать соглашение API.

      Если из TEsCustomControl вытащить Canvas, и начать рисовать вне события OnPaint "как привык", что часто встречается в древнем коде, то будут какие угодно глюки.

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

      Смысл оригинальной статьи - дать набор идей и реализацию, которую каждый может доработать под себя. Вы доработали, нашли применение? - Отлично, поделитесь кейсом использования "в продакшене", как автору, это мне интереснее, чем абстрактная "красота" API.

      Я бы рад выложить, но пока продукт ещё сырой и не готов выложить на публику


      1. Error1024
        17.01.2026 02:49

        Ваш TEsWinControl является аналогом TCustomControl. Значит он должен соблюдать соглашение API.

        Вы точно не запутались с использованием нейросетей? У меня нет никакого TEsWinControl, у меня есть TEsCustomControl. TEsWinControl - это ваше изобретение, соблюдайте что хотите.

        Мой TEsCustomControl является аналогом TCustomControl по концепции «кастомный компонент». Но TEsCustomControl не является наследником TCustomControl, и не должен соблюдать интерфейс TCustomControl, это ваша хотелка, не более.

        Вы когда-нибудь слышали про концепцию ООП? Про наследование классов и т.д.? Вам должно быть известно, что наследник класса должен быть полностью работоспособным в коде ожидающим объект класса-предка. Но вот только вы требуете, чтобы TEsCustomControl соблюдал интерфейс «левого» TCustomControl, который не является предком TEsCustomControl. Предок TEsCustomControl это TWinControl, его «контракт» соблюден на 100%.

        Иерархия классов, наследование, полиморфизм, вам знакомы эти слова? Вы точно профессионал и архитектор? Такое ощущение что вы базы ООП не понимаете.

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

        Что именно должен расширять TEsCustomControl? У его предка TWinControl нет события OnPaint. Я же в TEsCustomControl создал новое событие OnPaint, и создал его таким, каким захотел. И нет, ничего не сломал, в соответствии с базовыми принципами ООП, разрешающими добавлять в наследниках что угодно, пока оно не ломает интерфейс предка.


        1. 1QDenisQ Автор
          17.01.2026 02:49

          Вы точно не запутались с использованием нейросетей? У меня нет никакого TEsWinControl, у меня есть TEsCustomControlTEsWinControl - это ваше изобретение, соблюдайте что хотите.

          Причем тут использование нейросетей? Я изменил изначальное TEsCustomControl на TEsWinControl короткое данное имя лучше ассоциируется.

          Мой TEsCustomControl является аналогом TCustomControl по концепции «кастомный компонент». Но TEsCustomControl не является наследником TCustomControl, и не должен соблюдать интерфейс TCustomControl, это ваша хотелка, не более.

          Вы когда-нибудь слышали про концепцию ООП? Про наследование классов и т.д.? Вам должно быть известно, что наследник класса должен быть полностью работоспособным в коде ожидающим объект класса-предка. Но вот только вы требуете, чтобы TEsCustomControl соблюдал интерфейс «левого» TCustomControl, который не является предком TEsCustomControl. Предок TEsCustomControl это TWinControl, его «контракт» соблюден на 100%.

          Иерархия классов, наследование, полиморфизм, вам знакомы эти слова? Вы точно профессионал и архитектор? Такое ощущение что вы базы ООП не понимаете.

          Как связаны по вашему концепции наличие соглашение API и знание ООП? Я разве отрицал какие-то парадигмы и говорил что они неправильные? Не выдумывайте себе

          Что именно должен расширять TEsCustomControl? У его предка TWinControl нет события OnPaint. Я же в TEsCustomControl создал новое событие OnPaint, и создал его таким, каким захотел. И нет, ничего не сломал, в соответствии с базовыми принципами ООП, разрешающими добавлять в наследниках что угодно, пока оно не ломает интерфейс предка.

          Я вам говорю, о том, что расширение/изменение сигнатуры у обработчиков, с точки зрения дизайна VCL, не ломает общую логическую цепочку. Если у класса есть метод OnPaint, то этот метод всегда имеет одну сигнатуру, в противном случае, меняется название. Об этом статья. Вы зациклились на одном слове "API" и всякую фигню выдумываете


          1. Error1024
            17.01.2026 02:49

            Сейчас вы пишите:

            Причем тут использование нейросетей? Я изменил изначальное TEsCustomControl на TEsWinControl короткое данное имя лучше ассоциируется.

            Но до этого написали:

            Ваш TEsWinControl является аналогом TCustomControl. Значит он должен соблюдать соглашение API.

            Противоречие не находите? Сделали некий TEsWinControl, а мне пишите про «недостатки» TEsCustomControl.

            Как связаны по вашему концепции наличие соглашение API и знание ООП? Я разве отрицал какие-то парадигмы и говорил что они неправильные? Не выдумывайте себе

            Да, вы пишите, что я что-то там «сломал», хотя TEsCustomControl строго соблюдает интерфейс своего предка TWinControl. Вы же ожидаете соответствие интерфейсу TCustomControl, от которого мой класс НЕ наследуется.

            Я вам говорю, о том, что расширение/изменение сигнатуры у обработчиков, с точки зрения дизайна VCL, не ломает общую логическую цепочку. Если у класса есть метод OnPaint, то этот метод всегда имеет одну сигнатуру, в противном случае, меняется название. Об этом статья. Вы зациклились на одном слове "API" и всякую фигню выдумываете

            Где конкретно, в официальной документации Embarcadero, написано что OnPaint обязан иметь конкретную «стандартную» сигнатуру? Это ваше видение «красоты», не более. У меня оно свое - Canvas - не должен «торчать» в паблике.


            1. Error1024
              17.01.2026 02:49

              И да:

              Причем тут использование нейросетей? Я изменил изначальное TEsCustomControl на TEsWinControl короткое данное имя лучше ассоциируется. 

              У делфового TWinControl нет никаких Canvas и OnPaint, вы нарушили ожидание разработчика о том что у TWinControl нет данных свойств :)

              Т.е. в своем WinControl вы симитировали интерфейс CustomControl. Неожиданно знаете ли :)


  1. Error1024
    17.01.2026 02:49

    И еще:

    __fastcall TEsPaint::TEsPaint(TComponent* Owner) : TEsWinControl(Owner) {

    OnPaint = FormPaintCanvas;

    };

    Контракт VCL таков, что события OnXXX предназначены для пользователя компонента(программиста который кидает его на форму), не для создания наследников.

    Наследники должны перекрывать виртуальные методы.

    В случае с TEsCustomControlTCustomControl кстати тоже) это метод Paint:

    /// <summary>
    /// Descendantsmustoverride this method for custom rendering
    /// </summary>
    procedure Paint; virtual;

    https://github.com/errorcalc/FreeEsVclComponents/blob/dc6caebea54e968d544f14a20dba7655786b3932/Source/ES.BaseControls.pas#L224

    Таков контракт, вы его нарушили.


    1. 1QDenisQ Автор
      17.01.2026 02:49

      Да, я согласен, что в данном случае я некорректно воспользовался методами и действительно надо было использовать метод Paint(), а не обработчиком событий


      1. Error1024
        17.01.2026 02:49

        За много денег вы можете получить от меня консультацию по основам создания VCL компонентов, писать в ЛС.


        1. 1QDenisQ Автор
          17.01.2026 02:49

          Спасибо большое. Поучиться действительно интересно, но за "много денег" я что-то не желаю :) У меня нет столько)


          1. Error1024
            17.01.2026 02:49

            Странно, я думал у архитектора есть «много денег».


            1. 1QDenisQ Автор
              17.01.2026 02:49

              Есть, но пока что на другие хотелки