«Хороший сайт делается для людей, а поисковики приходят следом»

— из принципов редакции SeoLiga.ru
Главная » Статьи » Delphi и C++ » Компоненты VCL: создание собственного компонента

Компоненты VCL: создание собственного компонента

Рано или поздно каждый разработчик на 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.

Дальше действуйте по шагам:

  1. Создайте новый пакет проектов и добавьте в него модуль компонента.
  2. Соберите пакет, затем выполните установку.
  3. Убедитесь, что новая вкладка с компонентом появилась в палитре.
  4. Бросьте компонент на форму и проверьте поведение свойств в инспекторе объектов.
  5. Добавьте файл ресурсов с иконкой, чтобы компонент выглядел аккуратно в палитре.

Типичные ошибки и отладка

  • Забыли inherited в конструкторе. Компонент создаётся наполовину инициализированным и падает в странных местах.
  • Нет Invalidate в сеттерах. Свойство меняется, а картинка остаётся прежней.
  • Работа с Canvas вне Paint. Рисунок исчезает при первой же перерисовке окна.
  • Неправильный раздел видимости. Свойство не видно в инспекторе, потому что оно в public.
  • Утечки памяти. Если внутри компонента создаются объекты, освобождайте их в деструкторе, а лучше назначайте владельцем сам компонент.
  • Код, рассчитанный на время выполнения, работает в режиме проектирования. Проверяйте csDesigning в ComponentState.

Отладка компонентов имеет особенность: когда пакет установлен, IDE использует его прямо в процессе проектирования, и ошибка в коде может уронить среду. Поэтому сначала проверяйте поведение, создавая компонент в коде в тестовом приложении, и только потом устанавливайте в палитру.

Советы по проектированию

Хороший компонент — это не только работающий код, но и продуманный интерфейс. Придерживайтесь нескольких принципов:

  • давайте свойствам понятные имена и осмысленные значения по умолчанию;
  • не раскрывайте внутренности: всё, что не должно меняться снаружи, оставляйте в private;
  • старайтесь, чтобы компонент работал сразу после броска на форму, без обязательной настройки;
  • документируйте свойства комментариями, это пригодится вам же через полгода;
  • не завязывайте компонент на конкретную форму или глобальные переменные.

Заключение

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

ИБ
Ирина Белова
Автор SeoLiga.ru. Пишет о практике: что работает на реальных проектах, а что лучше обойти стороной. Хотите предложить свою статью? Загляните в раздел «Авторам».