3. События

Такие атрибуты объектов, как поля и методы составляют минимально необходимую основу объектно-ориентированной парадигмы и реализованы буквально в каждом невизуальном языке программирования, поддерживающим объекты. В таких языках для реализации новой функциональности объекта переопределяются (перекрываются) необходимые методы, которые возможно изменяют значения полей.

В визуальных системах разработки дело обстоит несколько иначе. Для реализации новой функциональности далеко не всегда разрабатывается новый класс. Действительно, было бы, по крайней мере, нелогично разрабатывать новый класс кнопки, которая должна всего лишь закрыть форму или вызвать диалог выбора файла. Вместо этого класс позволяют обеспечить выполнение написанного пользователем компонента кода в случае возникновения какого-либо события. Такой код называется «обработчиком события». Фактически класс как бы «поручает» (или как еще говорят «делегирует») реализацию метода приложению.

Рассмотрим как это устроено.

Для того чтобы вызвать обработчик события необходимо запомнить его адрес, перечень и темы передаваемых параметров. Вся эта информация запоминается в приватном поле класса объектного типа.

Сам адрес обработчика события устанавливается через свойство. Так что событие класса фактически является свойством объектного типа.

Примечание

Если событие это – свойство, то может возникнуть вопрос "Каким же образом Delphi определяет, какие свойства объекта показывать на странице “Properties” («Свойства»), а какие – на странице “Events” («События»)?" Для определения страницы инспектор объектов использует информацию о типе свойств. На странице “Events” появляются только свойства типа «указатель на метод».

Пример 3.8. Фрагмент кода библиотеки VCL, иллюстрирующий объявление событий.

type
  TControl = class(TComponent)
  private
…
    FOnDblClick: TNotifyEvent; 
…
  protected
    property OnDblClick: TNotifyEvent read FOnDblClick write FOnDblClick; 
…
end;	 	

3.1. Интерфейсная часть объявления события

Из листинга видно, что объявление события, как и любого свойства, состоит из двух частей. Во-первых, событие требует наличия внутреннего поля для хранения указателя на метод. Во-вторых, создается соответствующее свойство, которое предоставляет возможность во время проектирования присоединить обработчик события.

Объявление типа «указатель на метод» напоминает объявление процедурного типа с добавлением спецификатора of object.

Пример 3.9. Так, например, простейший тип ссылки на метод TNotifyEvent объявлен следующим образом:

  TNotifyEvent = procedure(Sender: TObject) of object;

Примечание

Указатель на метод может быть и функцией.

В библиотеке VCL принято первым параметром всех делегируемых методов передавать ссылку на сам объект, вызывающий событие. По соглашению этот первый параметр называется Sender и имеет тип TObject. Он позволяет определить, какой именно объект вызвал обработчик события.

3.2. Реализация вызова обработчика события

Для того, чтобы обработчик события был в какой-то момент вызван, в коде класса в нужном месте необходимо реализовать проверку существования кода обработчика события и его вызов. Операции по определению наличия делегированного метода, как правило, возлагается на так называемый "метод диспетчеризации".

Пример 3.10. Типичный код метода диспетчеризации выглядит следующим образом:

procedure TControl.DblClick;
begin
  if Assigned(FOnDblClick) then FOnDblClick(Self);
end; 

Логика работы приведенного метода следующая: сначала с помощью функции Assigned проверяется неравенство ссылки на метод значению Nil. Только в том случае, если адрес обработчика события отличен от nil, его код выполняется. Обратите внимание, что первым параметром обработчику передается ссылка на сам объект с помощью неявной ссылки Self.

Примечание

Методы диспетчеризации (а их может быть более одного на одно событие), как правило, объявляются как защищенные (protected) методы того класса, которому они принадлежат. Делается это для того, чтобы логика их работы могла быть переопределена в классах-наследниках.

3.3. События пользовательского типа

В библиотеке VCL объявлено большое количество типов "указателей на методы", которых хватает для диспетчеризации часто возникающих событий, связанных с клавиатурой, мышью и так далее. Однако в ряде случаев возникает необходимость определить свой пользовательский тип "указателя на метод".

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

В этом случае необходимо объявить новый тип "указателя на метод" (см. листинг ниже) и создать в разрабатываемом компоненте свойство, хранящее ссылку на метод. В нужный момент уже известным способом вызвать обработчик события (делегированный метод).

Пример 3.11. Объявление и использование пользовательского типа "ссылка на метод"

unit Unit8;

interface
type
  TOnResult=procedure(Res: extended) of object;

  TSolver=class
  private
    FOnResult: TOnResult;
  public
    procedure Calc;
  published
    property OnResult:TOnResult read FOnResult write FOnResult;
  end;

implementation

{ TSolver }

procedure TSolver.Calc;
var r: extended;
begin
  r:=random(); // Некоторые вычисления
  if Assigned(FOnResult) then FOnResult(r);
end;

end.

Иногда при объявлении типа "ссылка на метод" требуется в параметрах указать класс, который будет объявлен позже, например сам компонент. В этом случае прибегают к приему "упреждающего объявления класса".

Пример 3.12. Пример использования упреждающего объявления класса

unit Unit8;

interface
type
  TSolver=class; // Упреждающее объявление
  TOnResult=procedure(Comonent: TSolver;Res: extended) of object;

  TSolver=class
  private
    FOnResult: TOnResult;
  public
    procedure Calc;
  published
    property OnResult:TOnResult read FOnResult write FOnResult;
  end;

implementation

{ TSolver }

procedure TSolver.Calc;
var r: extended;
begin
  r:=random(); // Некоторые вычисления
  if Assigned(FOnResult) then FOnResult(Self, r);
end;

end.

Как видно из листинга, упреждающее объявление класса представляет собой декларацию типа со знаком равенства и ключевым словом class, закрытого точкой с запятой.

Примечание

На первый взгляд в листинге как будто присутствует два объявления класса. На самом деле компилятор, встретив упреждающее объявление класса, "считает", что такой класс есть, а "подробности ему будут сообщены позже".

С помощью такого приема становится возможным сделать казалось бы невозможно рекурсивные объявления типов.