Обновить

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

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

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

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

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

Нет прямого доступа к чему? Если вы имеете ввиду 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()

Вот у вас есть некий контрол с 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, который он должен перекрыть и рисовать что нужно.

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

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

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

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

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

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

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

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 спокойно доступен. В чём проблема?

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

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

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


#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 контрола доступен. Что дальше?

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

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

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

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

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

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

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

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

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

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

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

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

Ошибка №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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И да:

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

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

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

И еще:

__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

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

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

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации