Рано или поздно каждый разработчик на Delphi сталкивается с ситуацией, когда стандартных компонентов не хватает. Нужен особый индикатор, кнопка с нестандартным поведением или поле ввода с проверкой. Можно собирать всё это на форме из готовых кусочков, но куда правильнее оформить решение как отдельный компонент и использовать повторно. Давайте разберёмся, как это делается, на примере простого индикатора прогресса.
Зачем нужен свой компонент
Компонент — это класс, который можно положить на форму из палитры и настраивать в инспекторе объектов. У него есть свойства, события и методы. Главные причины писать собственные компоненты:
- повторное использование: одна реализация работает в десятках проектов;
- единый внешний вид и поведение во всём приложении;
- инкапсуляция: сложная логика спрятана внутри, снаружи остаются понятные свойства;
- удобство команды: коллеге достаточно бросить компонент на форму.
Если же логика нужна ровно в одном месте, возможно, достаточно обычного класса или фрейма. Не создавайте компонент ради самого компонента.
Выбор предка
Первый и самый важный шаг — решить, от какого класса наследоваться. Неправильный выбор потом дорого обходится.
| Предок | Когда подходит |
|---|---|
| TComponent | Невизуальный компонент: таймер, обёртка над сервисом |
| TGraphicControl | Лёгкий визуальный элемент без окна и фокуса, рисуется на канве родителя |
| TCustomControl | Элемент с собственным окном, фокусом, клавиатурным вводом |
| TWinControl и потомки | Когда нужно расширить готовый стандартный элемент |
Для нашего индикатора фокус не нужен, поэтому берём TGraphicControl. Он экономит ресурсы системы: у него нет собственного дескриптора окна.
Если вы хотите лишь слегка изменить поведение существующего компонента, наследуйтесь от него. Например, нужна кнопка, которая сама блокируется после нажатия, — пишите потомок от TButton, а не рисуйте всё с нуля.
Каркас класса
Создайте новый модуль и опишите класс. Обратите внимание на разделы видимости: published отвечает за то, что увидит инспектор объектов.
unit ProgressGauge;
interface
uses
Classes, Controls, Graphics;
type
TProgressGauge = class(TGraphicControl)
private
FMin: Integer;
FMax: Integer;
FPosition: Integer;
FBarColor: TColor;
FOnChange: TNotifyEvent;
procedure SetPosition(Value: Integer);
procedure SetMax(Value: Integer);
procedure SetBarColor(Value: TColor);
protected
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
published
property Min: Integer read FMin write FMin default 0;
property Max: Integer read FMax write SetMax default 100;
property Position: Integer read FPosition write SetPosition default 0;
property BarColor: TColor read FBarColor write SetBarColor default clGreen;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property Align;
property Visible;
property Hint;
property ShowHint;
end;
implementation
В published мы заодно «пробросили» стандартные свойства предка, такие как Align и Visible. Они объявлены в защищённой части предка, и без повторного объявления в published инспектор их не покажет.
Конструктор и значения по умолчанию
В конструкторе задаются начальные значения. Это место, где часто допускают ошибки, поэтому запомните два правила: всегда вызывайте inherited первым делом, и значения в конструкторе должны совпадать с директивами default в объявлении свойств.
constructor TProgressGauge.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Width := 200;
Height := 20;
FMin := 0;
FMax := 100;
FPosition := 0;
FBarColor := clGreen;
end;
Директива default не присваивает значение сама. Она лишь сообщает механизму сохранения формы, что значение по умолчанию не нужно записывать в файл формы. Если конструктор и default расходятся, получите неожиданные сбросы свойств.
Свойства и методы записи
Свойство, которое влияет на внешний вид, должно вызывать перерисовку. Для этого пишем метод записи.
procedure TProgressGauge.SetPosition(Value: Integer);
begin
if Value < FMin then Value := FMin;
if Value > FMax then Value := FMax;
if Value <> FPosition then
begin
FPosition := Value;
Invalidate;
if Assigned(FOnChange) then
FOnChange(Self);
end;
end;
procedure TProgressGauge.SetMax(Value: Integer);
begin
if Value <= FMin then Exit;
FMax := Value;
if FPosition > FMax then FPosition := FMax;
Invalidate;
end;
procedure TProgressGauge.SetBarColor(Value: TColor);
begin
if FBarColor <> Value then
begin
FBarColor := Value;
Invalidate;
end;
end;
Обратите внимание на проверку изменения значения. Без неё вы будете зря перерисовывать компонент и вызывать событие, когда ничего не поменялось. А метод Invalidate лишь помечает область как требующую обновления, реальная отрисовка произойдёт позже, в удобный момент.
Отрисовка
Весь внешний вид формируется в методе Paint. Рисуем на свойстве Canvas.
procedure TProgressGauge.Paint;
var
W: Integer;
begin
with Canvas do
begin
Brush.Color := clWhite;
Pen.Color := clGray;
Rectangle(0, 0, Width, Height);
if FMax > FMin then
W := Round((Width - 2) * (FPosition - FMin) / (FMax - FMin))
else
W := 0;
Brush.Color := FBarColor;
FillRect(Rect(1, 1, 1 + W, Height - 1));
end;
end;
Здесь есть важный нюанс. В Paint нельзя менять размеры компонента и вызывать действия, которые провоцируют повторную перерисовку, иначе получите бесконечный цикл. Метод должен только рисовать на основе текущего состояния.
Если вы заметили мерцание, помогает включить двойную буферизацию: рисуйте сначала в TBitmap, а потом одним вызовом копируйте результат на канву.
События
В нашем примере уже есть OnChange. Правило хорошего тона: имя события начинается с On, тип события выбирается из стандартных или объявляется свой. Вызывать событие нужно через проверку Assigned, потому что пользователь может его не назначить.
Если нужно передавать данные, объявите собственный тип процедуры:
type
TGaugeLimitEvent = procedure(Sender: TObject; Value: Integer) of object;
Ключевое слово of object важно: оно говорит, что это метод объекта, а не обычная процедура, и позволяет назначать обработчики формы.
Регистрация в палитре
Чтобы компонент появился в палитре IDE, нужна процедура Register в разделе interface, и пакет, в который вы его добавите.
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Мои компоненты', [TProgressGauge]);
end;
end.
Дальше действуйте по шагам:
- Создайте новый пакет проектов и добавьте в него модуль компонента.
- Соберите пакет, затем выполните установку.
- Убедитесь, что новая вкладка с компонентом появилась в палитре.
- Бросьте компонент на форму и проверьте поведение свойств в инспекторе объектов.
- Добавьте файл ресурсов с иконкой, чтобы компонент выглядел аккуратно в палитре.
Типичные ошибки и отладка
- Забыли inherited в конструкторе. Компонент создаётся наполовину инициализированным и падает в странных местах.
- Нет Invalidate в сеттерах. Свойство меняется, а картинка остаётся прежней.
- Работа с Canvas вне Paint. Рисунок исчезает при первой же перерисовке окна.
- Неправильный раздел видимости. Свойство не видно в инспекторе, потому что оно в public.
- Утечки памяти. Если внутри компонента создаются объекты, освобождайте их в деструкторе, а лучше назначайте владельцем сам компонент.
- Код, рассчитанный на время выполнения, работает в режиме проектирования. Проверяйте csDesigning в ComponentState.
Отладка компонентов имеет особенность: когда пакет установлен, IDE использует его прямо в процессе проектирования, и ошибка в коде может уронить среду. Поэтому сначала проверяйте поведение, создавая компонент в коде в тестовом приложении, и только потом устанавливайте в палитру.
Советы по проектированию
Хороший компонент — это не только работающий код, но и продуманный интерфейс. Придерживайтесь нескольких принципов:
- давайте свойствам понятные имена и осмысленные значения по умолчанию;
- не раскрывайте внутренности: всё, что не должно меняться снаружи, оставляйте в private;
- старайтесь, чтобы компонент работал сразу после броска на форму, без обязательной настройки;
- документируйте свойства комментариями, это пригодится вам же через полгода;
- не завязывайте компонент на конкретную форму или глобальные переменные.
Заключение
Создание собственного компонента — это не магия, а последовательность понятных шагов: выбрать предка, описать свойства, написать отрисовку, добавить события и зарегистрировать результат. Начните с простого элемента вроде нашего индикатора, отработайте приёмы, а затем переходите к более сложным задачам. Со временем у вас накопится собственная библиотека, которая заметно ускорит разработку новых программ.