Protected //методыдлядиспетчеризациисобытий
procedure DoOnResizeEvent; // Методдиспетчеризациисобытия OnResize
end;
Свойства
Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолируя их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значений свойств; внешний интерфейс класса не изменится. Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и т.д.
При работе с объектом свойства выглядят как обычные поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти и непосредственно значения не содержат, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Например, в объекте класса MyRectangle присваивание свойству width значения 50 сразу же изменит внешний вид прямоугольника. Создание сопутствующего эффекта достигается тем, что за присваиванием свойству значения стоит вызов метода.
Для объявления свойства используется ключевое слово property, записанное перед именем переменной (объявлять за раз можно только одну переменную). Т.о. описание свойства имеет следующий вид:
property<имя>:<тип>read<ф_чтения> [write<п_записи>] [default<знач.>];
,где
<имя> - имя переменной, обозначающей свойства;
<тип> - тип данных, который хранит свойство;
<ф_чтения> - поле или метод класса (функция), который вызывается при чтении;
<п_записи> - поле или метод класса (процедура), который вызывается при изменении;
<знач.> - значение, присваиваемое по умолчанию.
Помимо самого свойства, описанного в разделе public (обычно), в классе должно присутствовать поле, которое будет хранить значение свойства. Его обычно описывают с ограничением видимости в разделе private (см. листинг выше). Значение свойства может не храниться вовсе, а вычисляться при каждом обращении к свойству. Примером является свойство ItemCount, значение которого может вычисляться, как длина массива.
Согласно стандарту оформления исходного кода, названия методов чтения/записи начинаются со слов Set и Get, далее следует имя свойства. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field). Мы в дальнейшем также будем пользоваться этим соглашением.
Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства — это всегда функция, возвращающая значение того же типа, что и <тип> свойства. Метод записи свойства — это обязательно процедура, принимающая параметр того же типа, что и <тип> свойства, потому что это значение, которое должно заменить текущее значение свойства. В остальных отношениях это обычные методы объекта.
Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read), либо только записывать (задан спецификатор write). Например, классы часто содержат свойство Count показывает количество каких-то элементов. Поскольку оно определяется в результате внутренних операций, пользователю объекта разрешено лишь узнавать количество элементов.
В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var- и out-параметрах процедур и функций.
Кроме обычных свойств в объектах существуют свойства-массивы (array properties). Свойство-массив — это индексированное множество значений. Например, если писать программу для работы с текстом, то удобно хранить каждую строку текста в отдельном элементе массива и это удобно представить в виде свойства-массива:
Листинг 1.13
Type
TSimpleText = class
Protected
fItems: array of string;
function GetItem(index: integer): string;
procedure SetItem(index: integer; data: string);
function GetCount:integer;
Public
property Items[index: integer]: string read GetItem write SetItem;
property ItemCount:integer read GetCount;
end;
function TSimpleText.GetItem(Index: Integer): string;
Begin
Result := fItems[Index];
end;
procedure TSimpleText.SetItem(index, data: string);
Begin
fItems[index]:=data;
end;
Элементы массива Items можно только читать, поскольку это указано в объявлении.
В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства.
Основная выгода от применения свойства-массива — возможность выполнения итераций с помощью цикла for, например:
Листинг 1.14
Var
Reader: TSimpleText;
I: Integer;
...
for I := 0 to Reader.ItemCount - 1 doWriteln(Reader.Items[I]);
Свойство-массив может быть многомерным. В этом случае методы чтения и записи элементов должны иметь столько же индексных параметров соответствующих типов, что и свойство-массив.
Свойства-массивы имеют два важных отличия от обычных массивов:
· их индексы не ограничиваются диапазоном и могут иметь любой тип данных, а не только Integer. Например, можно создать свойство-массив, в котором индексами будут строки, так называемый ассоциативный массив. Обращение к такому свойству могло бы выглядеть примерно так: Reader.Items['FirstName'] := 'Alexander';
· операции над свойством-массивом в целом запрещены; разрешены операции только с его отдельно взятыми элементами.
Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default (обратите внимание на растановку точек с запятыми):
Листинг 1.15
Type
TSimpleText = class
...
property Items[Index: Integer]: string read GetItem; default;
...
end;
Такое объявление свойства Items позволяет рассматривать сам объект класса TSimpleText как массив и опускать имя свойства-массива при обращении к нему из программы, например:
Листинг 1.16
Var
Reader: TSimpleText;
I: Integer;
...
for I := 0 to Reader.ItemCount - 1 do
Writeln(Reader[I]);
Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.
Один и тот же метод может использоваться для получения (установки) значений нескольких свойств одного типа. В этом случае каждому свойству назначается целочисленный индекс, который передается в метод чтения (записи) первым параметром. В следующем примере уже известный Вам метод GetItem обслуживает три свойства: FirstName, LastName и Phone:
Листинг 1.17
Type
TSimpleText = class
...
property FirstName: string index 0 read GetItem;
property LastName: string index 1 read GetItem;
property Phone: string index 2 read GetItem;
end;
Обращения к свойствам FirstName, LastName и Phone заменяются компилятором на вызовы одного и того же метода GetItem, но с разными значениями параметра Index:
Листинг 1.18
Var Reader: TSimpleText;
...
Writeln(Reader.FirstName); // Эквивалентно: Writeln(Reader.GetItem(0));
Writeln(Reader.LastName); // Эквивалентно: Writeln(Reader.GetItem(1));
Writeln(Reader.Phone); // Эквивалентно: Writeln(Reader.GetItem(2));
Обратите внимание, что метод GetItem обслуживает как свойство-массив Items, так и свойства FirstName, LastName и Phone. Удобно, не правда ли!
Обработка сообщений
Для классов реализована дополнительная возможность – обработка сообщений, получаемых от других объектов программы и Windows. Для этого применяется ключевое слово message. Процедура, использующая эту директиву, будет вызываться автоматически, когда объект соответствующего класса получит сообщение с указанным идентификатором. Значение идентификатора должно лежать в диапазоне 1..49151 в случае обычного обработчика сообщений или должно соответствовать одному из идентификаторов стандартных сообщений Windows, которые описаны в модуле messages. Пример:
Листинг 1.19
Type T1 = class(TObject)
Procedure MyMSG(var MSG: TMessage); message 12590;
Procedure WMKeyDown(var Msg:TWMKeyDown); message WM_KEYDOWN;
End;
Единственный параметр процедуры-обработчика сообщения должен быть описан с ключевым словом var (передаваться по ссылке). При описании реализации обработчиков событий Windows можно сразу указывать название события и тип параметра.
События
Программирование, ориентированное на события - неотъемлемая черта Windows. Некоторые программные среды для быстрой разработки приложений (RAD) пытаются скрыть от пользователя эту черту совсем, как будто она настолько сложна, что большинство не могут ее понять. Истина заключается в том, что событийное программирование само по себе не так уж сложно. Однако есть некоторые особенности воплощения данной концепции в Windows, которые в некоторых ситуациях могут вызвать затруднения.
Delphi предоставляет полный доступ к подструктуре событий, предоставляемой Windows. С другой стороны, Delphi упрощает программирование обработчиков таких событий.
Под событием понимается проишествие, которое происходит в некоторый (заранее не известный точно) момент времени и влечёт к изменению состояния объекта. Например, это нажатие клавиши, изменение размера, ввод данных и прочее. Объекты имеют свой набор свойств и свое поведение - набор откликов на события, происходящие с ними. Такой отклик (для объекта это будет внешний метод), который определяет, как будет вести себя объект при возникновении события, называется обработчиком события. В качестве примера можно привести список событий для объектов VCL, на которые они реагирует, и который можно посмотреть в Инспекторе Объектов на вкладке событий. На этой вкладке видно, какие события может обрабатывать выбранный объект (на самом деле, на этой вкладке представлен список свойств, которые имеют процедурный тип), но реально он будет делать это только, если программист определит соответствующий обработчик. В противном случае возникшее событие просто игнорируется, потому что объект просто не знает, что с ним делать.
Существует соглашение по названиям событий. Название должно начинаться с префикса On. Далее следует слово(а) с большой буквы, которое характеризует событие. Если слов несколько, то пробелы или другие разделители между ними не ставяться. Например, OnDblClick соответствует двойному щелчку мыши, а OnKeyUp - событию, когда нажатая клавиша была отпущена. Далее в таблице приведены некоторые стандартные события с их описанием.
Таблица 1.1 – События
Событие | Тип | Причина возникновения |
OnActivate | TNotifyEvent | Происходит в момент, когда приложение получает фокус (становиться активным) |
OnClick | TNotifyEvent | Пользователь кликнул по элементу |
OnDeactivate | TNotifyEvent | Происходит в момент, когда приложение теряет фокус (становиться неактивным) |
OnHelp | THelpEvent | Происходит, когда приложение принимает запрос пользователя о вызове справки |
OnHint | TNotifyEvent | Происходит в момент, когда указатель мыши позиционируется над компонентом, свойство hint которого содержит непустую строку |
OnIdle | TIdleEvent | Событие происходит в момент простоя приложения. Параметр Done по умолчанию равен true, что означает ожидание приложением сообщений от Windows. |
OnMessage | TMessageEvent | Включается в момент, когда приложение принимает сообщение от Windows |
OnMinimize | TNotifyEvent | Происходит в момент сворачивания приложения. Событие происходит автоматически при вызове метода Minimize или при нажатии кнопки сворачивания. |
OnMouseDown | TMouseEvent | Возникает при нажатии любой кнопки мыши. |
OnMouseUp | TMouseEvent | Возникает при отпускании, ранее нажатой кнопки на мыше. |
OnMouseMove | TMouseMoveEvent | Возникает при движении указателя над элементом управления. |
OnPaint | TNotifyEvent | Происходит, когда требуется выполнить перерисовку элемента. |
OnResize | TNotifyEvent | Происходит в момент, когда изменяются размеры объекта |
OnRestore | TNotifyEvent | Происходит в момент, когда прежде свёрнутое приложение востанавливается до первоначального размера. |
OnShowHint | TShowHintEvent | Происходит перед тем, как приложение отобразит подсказку, поэтому обработка данного события предоставляет возможность изменить отображение подсказки. Например, сменить цвет подсказки. |
Так что же такое событие для Delphi на уровне реализации кода? Это поле класса, имеющее процедурный тип. Отличительная особенность заключается в том, что такому полю можно не только присваивать значение, но и вызывать как функцию. При этом надо только не забывать ставить в конце скобки. Так как по соглашению поля делаются закрытыми для доступа из вне, то это поле обычно дополнительно представляют через свойство. Примеры типов приведены в только, что рассмотренной таблице. Самым простым является TNotifyEvent. Он обычно используется, если требуется простое уведомляющее событие. Тип объявляется следующим образом:
Type TNotifyEvent = procedure (Sender: TObject) of object;
Следует обратить внимание здесь на несколько вещей. Первое – название процедуры не указывается, пишется только ключевое слово procedure. Использование функций разрешено. В конце обязательно указывается сочетание «ofobject». К параметрам предъявляются теже требования, что и к параметрам обычных процедур. Можно использовать всё то, что используется в обычных заголовках процедур и функций, т.е. "var", "const", "out" и др.
Помимо стандартных процедурных типов (см. некоторые из них в таблице выше) можно создавать собственные. Например:
Type TMyEvent2 = function(Sender: TObject; Value: Integer): byte of object;
Здесь следует отметить, что первым параметром крайне желательно указывать «Sender: TObject». Это позволит в дальнейшем идентифицировать объект, от какого поступил запрос.
Итак, чтобы создать простое событие, типа TNotifyEvent нужно сделать поле и свойство этого типа:
Листинг 1.20
Type
TMyClass = class(TObject)
Private
FOnMyEvent: TNotifyEvent; //полеобозначающеесобытие
public // или published, если это компонент (для отображения в Object Inspector)
property OnMyEvent: TNotifyEvent read FOnMyEvent write FOnMyEvent;
end;
Затем рекомендуется создать, так называемые, методы диспетчеризации событий. Эти методы будут проверять, присвоен ли свойству-событию объекта/компонента обработчик, и вызывать его. Учитывая, что события генерирует сам объект, то методы диспетчеризации должны быть скрытыми от общего доступа, т.е. находиться в разделе protected. Так же следует обратить внимание на то, что в метод диспетчеризации должны передаваться все те же параметры, что и в вызов обработчика события, за исключением параметра «Sender: TObject». Онпринимаетсяравным Self.
Листинг 1.21
Interface
Type
TMyClass = class(...)
Private
FOnMyEvent: TNotifyEvent;
Protected
procedure DoOnMyEvent; // Методдиспетчеризации
public // или published, еслиэтокомпонент (дляотображенияв Object Inspector)
property OnMyEvent: TNotifyEvent read FOnMyEvent write FOnMyEvent;
end;
Implementation
procedure TMyClass.DoOnMyEvent;
Begin
if Assigned(FOnMyEvent) // Обработчикприсвоен?
then FOnMyEvent(Self); // Вызовобработчика
end;
end;
Обработчик события назначается обычной командой присваивания. Происходит так называемое делигирование управления. Но здесь есть ограничения.
1) Обработчик должен быть обязательно методом класса (вспоминаем запись в конце процедурного типа ofobject). Разрешено использовать в качестве обработчика события класса, метод этого же класса.
2) Параметры процедуры и процедурного типа должны совпадать.
Чтобы назначить обработчик события компоненту, его можно выделить, перейти на вкладку с событиями и кликнуть дважды на пустом поле, рядом с названием требуемого события или выбрать обработчик из выпадающего списка. Также можно задавать обработчики динамически, внутри кода программы.
Например, разместим на форме две кнопки и будем обрабатывать их события OnClick. Для первой кнопки создадим новый обработчик методом, указанным в прошлом абзаце. Для второй кнопки обработчик в инспекторе объектов назначать не будем, а зададим его только тогда, когда пользователь нажмёт на первую кнопку. Следовательно, пока не будет нажата первая кнопка, вторая никаких действий производить не будет. Сам обработчик, разумеется, должен быть создан заранее. Поэтому создадим в классе TForm1 процедуру ButtonDown(Sender: TObject), которая совместима по параметрам с типом TNotifyEvent. Необходим данный тип, потому что его имеет событие OnClick. Вызовем в этом обработчике функцию ShowMessage, что выдать на экран простое сообщение и тем самым продемонстрировать, что обработчик связан с событием.
Листинг 1.22
Type
TForm1 = class(TForm)
Button1: TButton; //кнопка 1
Button2: TButton; //кнопка 2
procedure Button1Click(Sender: TObject); //обработчикназначеныйвинспекторе
Public
procedure ButtonDown(Sender: TObject); //обычныйметодкласса
end;
…
procedure TForm1.ButtonDown(Sender: TObject);
Begin
ShowMessage('Hello'); //реализацияметода
end;
procedure TForm1.Button1Click(Sender: TObject);
Begin
Form1.Button2.OnClick:= ButtonDown; //назначаемобработчиксобытиякнопке 2
end;
Рисунок 1.1 – Результат при нажатии последовательности Button1, Button 2
Пример можно доработать, перенеся назначение обработчиков событий обеих кнопок из инспектора объектов в событие OnCreate формы. Тогда в инспекторе ничего отображаться не будет, но кнопки функционируют – при нажатии на них будет появляться сообщение с названием соответствующей кнопки.
Рисунок 1.2 – Динамически назначаемые обработчики
Предложенный способ можно считать одним из методов сокрытия данных. Также это позволяет менять функциональность элементов, просто назначая другие обработчики. Например, одна и та же кнопка может открывать и закрывать документ. По умолчанию обработчики событий, добавленные с помощью инспектора объектов, становяться методами класса формы. Используя динамическое назначение, можно сделать обработчиками событий компонентов методы стороннего класса, к примеру, класса-документа, тем самым создав логическую завершённость и уменьшив зависимость функциональной части от оформления программы.
Пример практического применения смотреть в разделе «Построение меню со стрелочным управление».
Case-технологии