Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Основы создания компонент в среде Delphi

Автор: Александр Карпов
Прислал: Влад Шубников

Объявление класса

Начнем с самого простого — с создания объявления класса. Для этого в меню «Component» выбираем пункт «New Component…». В появившемся окне, нужно задать следующие значения: Ancestor Type (класс родитель создаваемого класса), Class Name (имя создаваемого класса), Palette Page (название закладки палитры компонентов, на которую будет установлен данный компонент; если такой закладки не существует, то она будет создана), Unit File Name (имя модуля, в котором будет размещено описание класса).

Итак, создадим потомок TСustomControl'а — TMyClass, в результате чего получим следующие объявление класса:

type
  TMyClass = class(TCustomControl)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

Ключевое слово class указывает на то, что мы создаем новый класс TMyClass, порождая его от родителя — TCustomControl.

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

TMyClass = class;

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

TMyClass1 = class;
TMyClass2 = class(TWinControl)
FMyClass: TMyClass1;
…
end;
TMyClass1 = class(TMyClass2)
…
end;

Директивы видимости

В соответствии с концепцией объектно-ориентированного программирования (инкапсуляцией) в Delphi существуют четыре директивы видимости, предназначенные для сокрытия полей и методов внутри объекта. Итак, первая из них — private. Это сама строгая из всех директив видимости. Поля и методы, объявленные в этой секции, не будут видны во всех классах-потомках. Вторая, менее строгая — protected. Она предназначена для объявления тех полей и методов, которые должны быть видны в классах-потомках. Следующая директива — public, предназначена для объявления элементов, которые будут видны программе или модулю, если они имеют доступ к модулю, в котором объявлен класс. И, наконец, последняя директива — published. Она предназначена для объявления тех свойств, которые должны быть видны в инспекторе объектов на этапе проектирования (design-time).

Поля

Вообще, поля — это обычные переменные, инкапсулированные внутри объекта. Объявляются они точно так же, как и в обычном Паскале (смотри пример ниже, FStr — поле типа String). Также полями могут быть и объекты.

Методы

Это одна из самых главных частей объекта. Методы обеспечивают своеобразный интерфейс между внутренней (инкапсулированной) частью объекта и внешней (доступной вне объекта). Объявляются так же, как функции и процедуры в обычном Паскале. Вот простой пример:

TMyClass = class(TCustomControl)
private
  FStr: string;
protected
public
  procedure SetStr(str: string);
  function GetStr: string;
published
end;

Реализация методов производится в том же модуле, где и объявление класса, в секции implementation, следующим образом:

procedure TMyClass.SetStr(str: string);
begin
  FStr := str;
end;

function TMyClass.GetStr: string;
begin
  Result := FStr;
end;

Этот пример также хорошо иллюстрирует сущность инкапсуляции. Класс TMyClass инкапсулирует поле FStr и реализацию методов записи — SetStr и чтения — GetStr этого поля.

Перекрытие методов

Для перекрытия методов класса-предка в классе-потомке используется специальная директива override. При перекрывании метода надо учитывать следующее: метод, объявленный в классе-предке в секции private, перекрыть в потомке нельзя; нельзя также перекрывать методы, объявляя их в потомке с меньшей видимостью, чем в классе-предке; при перекрытии методов обработки сообщений директива override не используется. Для того чтобы в теле перекрывающего метода обратиться к методу предка, используется директива inherited. Пример:

TMyClass = class(TCustomControl)
  ...
    function MyMetod: string;
  ...
end;
TMyClass2 = class(TMyClass)
  ...
    function MyMetod: string; override;
  ...
end;

implementation
  ...

function TMyClass.MyMetod: string;
begin
  ...
end;

function TMyClass2.MyMetod: string;
begin
  inherited MyMetod;
  //вызов метода предка TMyClass.MyMetod
end;

Виртуальные и динамические методы

Методы могут быть виртуальными и динамическими. Они различаются способом решения классической проблемы «время — ресурс, ресурс — время» (в данном случае ресурс — это объем оперативной памяти).

Разница между ними заключается в структуре соответствующих таблиц методов VMT — Virtual Metod Table (таблица виртуальных методов) и DMT — Dynamic Metod Table (таблица динамических методов). Диспетчеризация виртуальных методов происходит быстрее, чем динамических, но с другой стороны, таблица виртуальных методов (VMT) занимает больше места в памяти, чем таблица динамических (DMT). Выбор остается за программистом.

Для определения типа метода (виртуальный или динамический) используются две директивы — virtual и dynamic соответственно. Для указания типа метода после его объявления через точку с запятой указывается одна из этих директив. Вот простой пример:

TMyClass = class(TCustomControl)
...
procedure SetStr(str: String); dynamic;
function GetStr: String; virtual;
...
end;

Абстрактные методы

Также стоит сказать и об абстрактных методах — они определяются путем добавления после объявления метода директивы abstract. Такое объявление метода делает ненужным и даже неправомочным реализацию данного метода в текущем классе. Вызов такого метода также будет считаться неправомочным. Такие методы в основном предназначены для перекрытия их в потомках. Пример объявления абстрактного метода:

TMyClass = class(TCustomControl)
...
function GetStr: String; virtual; abstract;
...
end;

Методы обработки сообщений

Еще одна разновидность методов — это методы обработки сообщений (message-handling metods). Эти методы специально предназначены для обработки сообщений Windows. На них накладываются некоторые ограничения: они обязательно должны быть процедурами, должны иметь один параметр — переменную; при прикрытии таких методов директива override не применяется; также при перекрытии необходимо вызывать метод предка. Объявляются такие методы с помощью директивы message. Вот пример объявления и реализации метода обработки сообщений:

TMyClass = class(TCustomControl)
  ...
    procedure MyMsgMetod(var Msg: TWMMouseMove); message WM_MOUSEMOVE;
  ...
end;

implementation
...

procedure TMyClass.MyMsgMetod(var Msg: TWMMouseMove);
begin
  inherited;
  ...
end;

Константа после директивы message определяет сообщение, которое будет обрабатываться этим методом (в данном случае метод будет вызываться каждый раз, когда происходит перемещение курсора мыши над данным объектом). Тип единственного параметра процедуры задает, в каком формате методу будут переданы данные сообщения (например, в этом случае Msg.XPos и Msg.YPos — текущие координаты курсора мыши в области данного компонента). Описание констант и соответствующих типов помещено в модуле Messages.

Классовые методы (или методы класса)

Существует еще одно важное расширение методов — классовые методы. Это методы, которые можно вызывать, не создавая экземпляра объекта (например: TMyClass.MyClassMetod). В теле классового метода нельзя обращаться ни к одному методу или полю данного класса, но можно вызывать конструкторы и другие классовые методы. Обычно такие методы используются для предоставления информации о классе, версии реализации и т. д. Определяются такие методы путем добавления директивы class перед объявлением метода. Вот пример классового метода и его реализации:

TMyClass = class(TCustomControl)
  ...
    class function Version: string;
  ...
end;

implementation
...

class function TMyClass.Version: string;
begin
  Result := 'v1.0';
end;

Перерегружаемые (overload) методы

И напоследок, стоит обсудить перегружаемые методы. Они предоставляют возможность более гибко взаимодействовать с объектом. Это достигается за счет того, что может существовать несколько различных методов с одним идентификатором, но различными параметрами. Объявляются они с использованием специальной директивы — overload.

Пример:

TMyClass = class(TCustomControl)
  ...
    FX: string;
  procedure SetX(X: string); override; {1}
  procedure SetX(X: Integer); override; {2}
  ...
end;

implementation
...

procedure TMyClass.SetX(X: string);
begin
  FX := X;
end;

procedure TMyClass.SetX(X: Integer); override;
begin
  FX := IntToStr(X);
end;

В данном примере, при вызове пользователем метода SetX, в зависимости от типа передаваемого параметра будет вызван соответствующий метод (для параметра типа String — первый, для Integer — второй).

Пример вызова первого варианта метода:

var
  Str: string;
  Test: TMyClass;
begin
  Test := TMyClass.Create(nil);
  Test.SetX(Str);
  Test.Free;
end;

Пример вызова второго варианта метода:

var
  Int: Integer;
  Test: TMyClass;
begin
  Test := TMyClass.Create(nil);
  Test.SetX(Int);
  Test.Free;
end;

Удобства, предоставляемые данной возможностью, очевидны, например, в данном случае пользователю не нужно самому конвертировать Integer в String.

Конструкторы и деструкторы

Конструкторы и деструкторы применяются для создания экземпляра класса и его удаления. Конструктор выделяет необходимую память, инициализирует поля, производит первоначальную подготовку объекта к последующему использованию. Конструктор объявляется с помощью директивы constructor и обязательно должен называться — Create. В теле конструктора первым же делом необходимо вызвать конструктор предка. Деструктор освобождает выделенную конструктором память, закрывает используемые файлы, потоки и т. д. Деструктор объявляется с использованием директивы destructor, называться он должен Destroy. В теле деструктора деструктор предка вызывается в самую последнюю очередь. Пример:

TMyClass = class(TCustomControl)
protected
  FStr: TStringList;
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
  ...
end;

implementation

constructor TMyClass.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FLis := TStringList.Create;
  ...
end;

destructor TMyClass.Destroy;
begin
  FList.Free; //удаление объекта
  ...
    inherited Destroy;
end;

Свойства

Свойства — это второе по важности (после методов) средство обеспечения взаимодействия объекта с окружающим миром. Через свойства обеспечивается доступ к полям объекта, только свойства могут быть видны в инспекторе объектов (Object Inspector) Delphi.

Объявление свойства, чтение и запись.

Вот один из вариантов объявления свойства:

property Str: String read FStr write SetStr;

Свойства объявляются с использованием директивы property, за ней следует имя свойства и тип (как объявление переменной в обычном Паскале), затем директива read и указание «источника» чтения (это либо поле такого же типа, как свойство, либо функция без параметров, возвращающая значение того же типа), далее следует директива write с указанием «приемника» записи (поле с тем же типом, что и свойство, либо процедура с одним параметром, того же типа). В нашем случае чтение осуществляется напрямую из поля, а запись через процедуру. Здесь есть одна хитрость. Соответствующее поле не обязательно должно существовать или иметь тот же тип, что и свойство. Это позволяет снизить избыточность хранимой информации и уменьшить расход памяти. Распространенным примером такого подхода является предоставление информации о размере какого-либо массива, например так:

TMyClass = class(TCustomControl)
protected
  FStrArr: array of string;
  function GetStrCount: Integer;
  procedure SetStrCount(Value: Integer);
public
  property StrCount: Integer read GetStrCount write SetStrCount;
  ...
end;

implementation

function TMyClass.GetStr Count: Integer;
begin
  result := Length(FStrArr);
end;

procedure TMyClass.SetStr Count(Value: Integer);
begin
  SetLength(StrArr, Value);
end;

Также особого труда не представляет создать свойства только для чтения или только для записи. Свойства только для записи нельзя выносить в секцию published, так как они не могут быть отображены в инспекторе объектов. Для этого нужно в объявлении свойства опустить соответствующую директиву:

property StrCount: Integer read GetStrCount; — свойство только для чтения (read-only)
property StrCount: Integer write SetStrCount; — свойство только для записи (write-only).

Применение методов для чтения и записи зачастую обусловлено необходимостью в так называемом побочном эффекте. Это ситуация, когда при изменении значения свойства необходимо произвести еще какие-либо изменения. Так, например, при изменения свойства Height или Width для любого визуального объекта тут же происходит его перерисовка, и он меняет свои размеры. Методы записи также часто используются для проверки присваемого свойству значения на допустимость.

Массивы свойств

Теперь разберемся, как организовать свойство, предоставляющее доступ непосредственно к элементам массива — строкам для предыдущего примера. Для этого обязательно чтение и запись производить через соответствующий метод, добавляя в его описание один параметр — индекс, и задать тип индекса для самого свойства. На практике это выглядит так:

TMyClass = class(TCustomControl)
private
  FStr: array of string;
protected
  procedure SetStr(Index: Integer; Value: string);
  function GetStr(Index: Integer): string;
public
  property Str[Index: Integer]: string read GetStr write SetStr;
  ...
end;

implementation

procedure TMyClass.SetStr(Index: Integer; Value: string);
begin
  FStr[Index] := Value;
end;

function TMyClass.GetStr(Index: Integer): string;
begin
  result := FStr[Index];
end;

Тип индекса может быть и другим, например строкой.

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

...
procedure SetStr(Index1, Index2: Integer; Value: String);
function GetStr(Index1, Index2: Integer): String;
...
property Str[Index1, Index2: Integer]: String read GetStr write SetStr;
...

К таким свойствам следует обращаться, как к массивам, например для экземпляра Test класса TMyClass — Test.Str[5].

Индексные свойства

Этот тип свойств применяется для экономии, когда для нескольких однотипных свойств используется один метод чтения и/или записи. Для этого в объявление свойства вводится директива index и индекс после нее, а методы чтения и записи должны иметь такой же вид как и для случая одномерного массива. Пример:

TMyClass = class(TCustom Control)
private
  FStr1: string;
  FStr2: string;
  FStr3: string;

protected
  procedure SetStr(Index: Integer; Value: string);
  function GetStr(Index: Integer): string;
  function GetStr3: string;

public
  property Str1: string index 1 read GetStr write SetStr;
  property Str2: string index 2 read GetStr write FStr2;
  property Str3: string index 3 read GetStr3 write SetStr;
  ...
end;

Как видно из примера, если для свойства определен индекс, совершенно необязательно для чтения или записи использовать метод, поддерживающий индексирование. Методом чтения и записи в качестве индекса (параметр Index, если у них таковой имеется) для каждого свойства будет передаваться то значение, которое указано после директивы index. Ну а в теле метода по полученному значению индекса выясняем, к какому из свойств было осуществлено обращение, и предпринимаем соответствующие действия.

Переобъявление свойств

Свойства совсем необязательно объявлять в public или published секциях. Очень часто наоборот бывает удобно объявить свойство в секции protected для последующего переобъявления его в классах-потомках. Но не все так хорошо — нельзя понизить видимость, т. е. если свойство объявлено в классе-предке в секции public, то переобъявив его в потомке в секции protected, невидимым его сделать не получится.

Переобъявляются свойства очень просто, возьмем наш пример с массивом строк:

property Str; 

Директива default

Эта директива применяется для задания значения по умолчанию для свойств. На практике выглядит так:

propety Int: Integer read FInt write SetInt default 10;

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

С этой директивой связано одно распространенное заблуждение — многие считают, что с ее помощью можно инициировать значения свойств. Это неверно. Дело в том, что все значения свойств, установленные программистом для определенного объекта, в инспекторе объектов сохраняются в файл формы (.dfm), который потом компилируется вместе с кодом. При запуске приложения автоматически создаются объекты, заданные на этапе проектирования, и для них загружаются и устанавливаются значения свойств. Это не выгодно, во-первых, потому, что увеличивает размер откомпилированного exe файла, а во-вторых, на все это тратится изрядная доля времени при запуске приложения.

Задание значения по умолчанию позволяет избавиться от этих неудобств. Так, если значение, указанное как значение по умолчанию, совпадает со значением, установленным программистом на этапе проектирования, то это значение не сохраняется в файл формы и соответственно потом при запуске приложения не загружается. Но задание значения по умолчанию не освобождает нас от инициализации соответствующего поля. Делать это надо в конструкторе, причем так, чтобы значение поля совпадало с значением по умолчанию, заданным для соответствующего свойства.

Для отмены значения по умолчанию, определенного в классе-предке, нужно переобъявить свойство с использованием директивы nodefaul, например:

propety Int nodefault;

Для изменения значения по умолчанию нужно преобъявить свойство и с помощью директивы default указать новое значение по умолчанию, например:

propety Int nodefault 100;

Если в классе определены несколько или хотя бы один массив свойств, то одно из таких свойств можно определить как свойство, используемое по умолчанию. Для этого необходимо в объявление свойства добавить директиву default, например:

property Str[Index: Integer]: String read GetStr write SetStr default;

На такое свойство можно ссылаться без указания его имени, например для экземпляра MyClass класса TMyClass — MyClass[10] равносильно MyClass.Str[10].

Директива stored

Эта директива предназначена для жесткого определения — сохранять значение свойства в файле формы или нет. Она добавляется в конец определения свойства, и после нее должно идти либо булево (boolean — FALSE или TRUE) значение, либо функция, возвращающая булево значение, либо поле булевого типа. Если булево значение ИСТИНА (TRUE), то сохранение значения свойства происходит, если ЛОЖЬ (FALSE) — нет. Этот механизм может быть полезен, если существует какая-либо избыточность полей и соответствующих им свойств. Возможно совместное использование директив default и stored. Пример (взят из модуля Controls):

TControl = class(TComponent)
private
  ...
    function IsEnabledStored: Boolean;
  ...
  public
  ...
    property Enabled: Boolean read GetEnabled write SetEnabled stored
      IsEnabledStored default True;
  ...
end;

События

События — это единственный для объекта способ спровоцировать какое-либо внешнее действие в ответ на изменение его состояния.

Событие как свойство

В Delphi все события реализуются как свойства определенного типа. Этот тип должен быть предварительно определен. Вот определение самого распространенного в Delphi свойства:

type TNotifyEvent = procedure (Sender: TObject) of object;

Как видно, это обычная процедура (также может быть использована и функция) с добавлением директивы of object.

Само объявление же события будет выглядеть так:

TMyClass = class(TCustomControl)
private
  FMyEvent: TNotifyEvent;
  ...
  public
  property MyEvent: TNotifyEvent read FMyEvent write FMyEvent;
  ...
end;

Вызов события

Вызывается сообщение путем обращения к соответствующему полю как к процедуре или функции. Для предыдущего примера это будет выглядеть так:

if Assigned(FMyEvent) then FMyEvent(Self);

Перед вызовом события обязательно нужно проверить, существует ли обработчик этого события — это выполняется с помощью функции Assigned. Если этого не сделать, то при отсутствии обработчика получим ошибку времени выполнения, доступа к памяти. Self — внутри каждого объекта — это указатель на самого себя, который мы и передаем в качестве параметра Sender.

Общепринятые правила именования

Напоследок стоит сказать об общепринятых правилах именования классов и их составляющих:

  • из названия должно быть понятно, для чего предназначен данный класс, метод, поле или свойство;
  • название классов принято называть с T, от type;
  • к названию иногда добавляют инициалы разработчика компонента или часть названия фирмы. Для Васи Иванова это может быть — TviComponent;
  • имена полей принято начинать с F, от Field (поле, область);
  • свойства принято называть точно так же, как и поля (если свойству в соответствие ставится поле), опуская F. Поле FStr — свойство Str;
  • имена методов чтения и записи принято образовывать путем добавления перед именем свойства Get и Set, соответственно. Свойство Str — метод чтения GetStr, метод записи SetStr;
  • именование полей и методов чтения и записи для событий подчиняются тем же правилам, что и для свойств;
  • имена событий принято начинать с On: OnClick, OnMouseMove.

Установка класса

Для того чтобы установить созданный класс в палитру компонентов, в меню «Component» выбираем пункт «Install Component…». В появившемся окне нужно задать следующие значения: Unit file name (имя модуля, в котором содержится описание устанавливаемого класса), Search path (пути через «;» по которым Delphi будет искать указанный модуль), Package file name (путь и имя файла пакета, в который будет добавлен устанавливаемый вами модуль с описанием класса).

Проект Delphi World © Выпуск 2002 - 2004
Автор проекта: ___Nikolay