Создание компонентов в Delphi

Автор: Михаил Голованов
http://delphi.mastak.ru

Источник: RSDN Magazine #1-2003
Опубликовано: 27.05.2003
Версия текста: 1.0
Процесс построения компонента
Создание модуля компонента. Рекомендации по проектированию.
Код компонента
Сохранение состояния компонентов
Загрузка формы в run-time
Сохранение непубликуемых или нестандартных свойств компонентов
Заключение

Процесс построения компонента

В этой статье мы рассмотрим основные стадии построения компонента, сосредоточившись на вопросах конструирования, тестирования и установки компонента в Delphi.

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

Мы последовательно рассмотрим все шаги данного процесса на примере Control-а. Любой control является наследником TComponent и шаги по созданию control-ов ничем не отличаются от аналогичных для чистых компонентов, а стало быть, для большей визуальности мы рассмотрим создание именно control-а. Наш компонент будет отличаться от стандартного тем, что по умолчанию цвет текста в нем будет не черным, а синим. Шрифт будет жирным. Кроме того, будут изменены ширина и высота по умолчанию. Данный компонент вряд ли поразит воображение пользователей, однако позволит подробно рассмотреть этапы создания компонента, не увлекаясь его внутренней функциональностью.

Создание модуля компонента. Рекомендации по проектированию.

Создание любого компонента начинается с создания модуля Delphi (файла с расширением pas), в котором будет храниться исходный текст компонента. В одном модуле может содержаться неограниченное количество компонентов. Однако лучше не сваливать все в одну кучу. Во-первых, разделение один модуль – один компонент позволяет нескольким разработчикам параллельно писать компоненты, являющиеся частями одного проекта. Во-вторых, в небольшом модуле гораздо проще разобраться, зная, что он содержит код, относящийся к одному компоненту.

Модуль компонента можно создать вручную или с помощью эксперта построения компонентов.

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

Начнем с последнего способа, как наиболее часто применяемого.


Рисунок 1.

Эксперт (рисунок 1) вызывается с помощью пункта меню Component/New Component в IDE Delphi. Диалоговое окно эксперта содержит следующие поля:

Ancestor type: родительский класс создаваемого компонента. Для выбора родительского класса придерживайтесь простого правила: выбирайте существующий класс компонента, реализующий наибольшую часть требуемой функциональности.

Class Name: имя класса создаваемого компонента. Старайтесь подобрать простое и понятное имя класса, выражающее назначение компонента.

Palette Page: закладка, на которой будет установлен компонент. Можно выбрать из выпадающего списка набор существующих страниц или ввести новое наименование.

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

Search Path: пути, в которых среда Delphi будет искать нужные ей файлы для компиляции кода компонента.

Для нашего компонента наиболее близким по функциональности является класс TLabel, он и будет родителем нашего компонента. Часто в качестве родителей следует выбирать не ближайший по функциональности компонент, а ближайший так называемый Custom-компонент. Например, непосредственным предком TLabel является TCustomLabel. Класс TCustomLabel реализует всю функциональность TLabel, однако не выносит объявление многих свойств в секцию published, потому что можно только увеличивать область видимости членов класса, но не уменьшать ее. Критерием для выбора между классом и custom-классом служит необходимость оставить скрытыми от пользователя некоторые поля компонента. Например, в TLabel свойство Align переобъявлено в секции published, тогда как в TCustomLabel оно объявляется как protected. Если не нужно давать пользователю компонента доступ к свойству Align, то в качестве предка можно выбрать класс TCustomLabel. Также заметим, что эксперт предлагает в качестве родителя два класса TLabel. Один из модуля StdCtrls, второй из QStdCtrls. Первый относится к иерархии классов VCL, второй к CLX. В примере мы рассмотрим создание VCL-компонента.

ПРИМЕЧАНИЕ

Как правило, модули исходного кода, начинающиеся с Q, относятся к CLX.

Назовем класс нашего компонента TmgCoolLabel. Размещаться он будет на закладке «Our components». Назовем модуль компонента umgCoolLabel.pas, размещаться он будет в отдельной папке, которую мы создали для него ранее.

Нажав кнопку ОК в окне эксперта, мы получим модуль со следующим текстом:

unit mgCoolLabel;

interface

uses
  SysUtils, Classes, Controls, StdCtrls;

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

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Our components', [TmgCoolLabel]);
end;

end.

Эксперт создает синтаксически правильную заготовку компонента, т.е минимально необходимый каркас.

Можно, конечно, написать это все вручную, но радости в этом никакой.

Обратите внимание, что помимо декларации класса, модуль содержит функцию Register. Данная функция вызывается при установке компонента и указывает среде, какие компоненты и каким образом должны быть установлены.

Следующим этапом в разработке компонента является создание свойств, методов и событий. Данный этап наиболее длителен и сложен. Какие свойства, методы и события будет содержать компонент – решать вам, однако можно дать несколько общих рекомендаций.

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

Название класса компонента желательно начинать с T, так как в этом случае Delphi при использовании компонента будет автоматически называть экземпляры, отсекая букву T. В противном случае имя экземпляра компонента будет совпадать с именем класса, и лишь цифры в конце имени экземпляра будут указывать пользователям компонента, что это экземпляр, а не класс.

Очень желательно предварять имя класса префиксом. Для компонентов в этой статье я выбрал префикс mg (мои инициалы). Класс нашего компонента будет назваться TmgCoolLabel.

Имя модуля я предваряю префиксом u (сокращение от Unit). Остальная часть имени совпадает с именем класса без буквы T. Данное требование не обязательно, однако поможет вам и вашим коллегам быстро отыскать, в каком модуле объявлен компонент, и не вызовет при этом путаницы.

Вы даже представить себе не можете, каким образом могут использоваться ваши компоненты. Поэтому стоит минимизировать предварительные условия использования компонента. Ниже перечислены соответствующие рекомендации.

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

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

Объявляете методы в секции public только если они выполняют действия, которые полезны пользователям компонента. Все методы, осуществляющие «внутреннюю» работу, прячьте в секциях protected и private.

Не стесняйтесь объявлять свойства. Свойства – основа быстрой и удобной настройки вашего компонента.

События OnChange, Before и After придают компоненту дополнительную гибкость.

Код компонента

Поскольку наш компонент лишь незначительно отличается от предка, нам не придется писать много кода. Достаточно лишь переопределить конструктор компонента, чтобы изменить начальные его настройки и переобъявить свойства ширины и высоты с новыми значениями директивы default.

unit mgCoolLabel;

interface

uses
  SysUtils, Classes, Controls, StdCtrls;

type
  TmgCoolLabel = class(TLabel)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner:TComponent);override;
  published
    { Published declarations }
    property Height default 30;
    property Width default 85;
  end;

procedure Register;

implementation

uses Graphics;

procedure Register;
begin
  RegisterComponents('Our components', [TmgCoolLabel]);
end;

{ TmgCoolLabel }

constructor TmgCoolLabel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  AutoSize:=false;
  Height:=30;
  Width:=120;
  Font.Color:=clBlue;
  Font.Style:=[fsBold];
  Font.Height:=16;
  Font.Size:=12;
end;

end.

Главная работа по установке новых начальных значений свойств выполняется в конструкторе Create. Переопределение свойств Height и Width необязательно, но очень желательно. Если этого не сделать, значения по умолчанию для данных свойств будут записываться в файл формы, что будет замедлять загрузку формы.

На практике этап проектирования и кодирования компонента является одним из самых долгих и кропотливых.

Сохранение состояния компонентов


Рисунок 2.

Delphi автоматически производит сохранение в файл формы состояния свойств, описанных в области видимости Published. Published – это область видимости, аналогичная директиве public. Помещение декларации элемента класса в секцию published заставляет компилятор добавить дополнительную информацию о типе времени выполнения (run-time type information, RTTI) для данного элемента. По этой причине в секции published могут быть объявлены не все типы данных, а лишь простые типы данных (ordinal datatypes), строки, классы, интерфейсы, указатели на методы и массивы.

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

На рисунке 1 схематично изображен процесс сохранения свойств формы в файл. Все начинается с того, что IDE Delphi вызывает метод WriteComponentResFile. Метод объявлен следующим образом:

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);

Первый параметр – имя файла, в который нужно сохранить форму, второй - сохраняемый компонент. Код метода очень прост:

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);
var
  Stream: TStream;
begin
  Stream := TFileStream.Create(FileName, fmCreate);
  try
    Stream.WriteComponentRes(Instance.ClassName, Instance);
  finally
    Stream.Free;
  end;
end;

Метод создает файловый поток (TFileStream) и вызывает его метод WriteComponentRes. Метод WriteComponentRes всего лишь вызывает WriteDescendentRes(ResName, Instance, nil). WriteDescendentRes формирует заголовок ресурса компонента и вызывает метод WriteDescendent, который и отвечает за запись свойств компонента в поток.

Код метода WriteDescendent так же прозрачен:

procedure TStream.WriteDescendent(Instance, Ancestor: TComponent);
var
  Writer: TWriter;
begin
  Writer := TWriter.Create(Self, 4096);
  try
    Writer.WriteDescendent(Instance, Ancestor);
  finally
    Writer.Free;
  end;
end;

Как видим, создается объект TWriter и вызывается его метод WriteDescendent. Таким образом, основная часть работы по сохранению свойств лежит на объекте TWriter.

Класс TWriter извлекает информацию о свойствах записываемого в поток объекта. Данный класс является наследником абстрактного класса TFiler – базового класса, используемого для записи или чтения информации о свойствах компонента в/из потока.

Рассмотрим класс TFiler более подробно. Задачей этого класса является обеспечение не только записи свойств самого компонента, но и всех компонентов, принадлежащих ему. Класс TFiler является абстрактным, то есть содержит лишь объявления методов и свойств, необходимых для выполнения поставленных задач.

Декларация класса выглядит следующим образом:

TFiler = class(TObject)
private
  FStream: TStream;
  FBuffer: Pointer;
  FBufSize: Integer;
  FBufPos: Integer;
  FBufEnd: Integer;
  FRoot: TComponent;
  FLookupRoot: TComponent;
  FAncestor: TPersistent;
  FIgnoreChildren: Boolean;
protected
  procedure SetRoot(Value: TComponent); virtual;
public
  constructor Create(Stream: TStream; BufSize: Integer);
  destructor Destroy; override;
  procedure DefineProperty(const Name: string;
    ReadData: TReaderProc; WriteData: TWriterProc;
    HasData: Boolean); virtual; abstract;
  procedure DefineBinaryProperty(const Name: string;
    ReadData, WriteData: TStreamProc;
    HasData: Boolean); virtual; abstract;
  procedure FlushBuffer; virtual; abstract;
  property Root: TComponent read FRoot write SetRoot;
  property LookupRoot: TComponent read FLookupRoot;
  property Ancestor: TPersistent read FAncestor write FAncestor;
  property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;
end;

Свойство Root содержит указатель на компонент, со свойствами которого мы работаем.

Свойство Ancestor позволяет определить, значения каких свойств должны быть записаны в поток. Дело в том, что необходимо сохранить лишь те свойства, значения которых отличаются от заданных по умолчанию директивой default. Если значение свойства Ancestor равно nil, записываются все свойства, в противном случае проводится анализ необходимости записи. Свойство Ancestor не равно nil лишь в случае сохранения форм, разработанных в визуальном дизайнере.

Свойство IgnoreChildren указывает, нужно ли, помимо свойств самого компонента, записывать свойства компонентов, владельцем которых он является. Если значение свойства равно True, "дети" данного компонента не записываются.

Свойство LookupRoot указывает на локальный корневой (записываемый/считываемый) компонент. Свойство доступно только для чтения и используется для разрешения имен вложенных фреймов. При сохранении или чтении вложенных во фрейм компонентов указывает на этот фрейм.

Метод FlushBuffer - абстрактный метод синхронизации с потоком, содержащим данные компонента.

DefineProperty – метод для чтения/записи значения свойства. Устанавливает указатели на методы чтения и записи свойства с именем, указанным в первом параметре.

DefineBinaryProperty – метод чтения/записи бинарных данных как значений свойства. Устанавливает указатели на методы чтения и записи свойства с именем, указанным в первом параметре.

Класс TFiler имеет двух наследников TWriter и TReader. TWriter отвечает за запись значений свойств, а TReader за чтение.

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

Загрузка значений свойств происходит аналогично процессу записи. При этом средой Delphi вызывается метод ReadComponentResFile, и создается объект TReader.

Данный механизм применяется при сохранении свойств в файлы формата ресурсов Windows. Последние версии Delphi (6, 7) по умолчанию сохраняют свойства в файлах текстового формата. Преобразования из одного формата в другой можно выполнить глобальными методами ObjectBinaryToText и ObjectTextToBinary.

По умолчанию свойства компонентов, агрегируемые компонентом, не сохраняются. Для изменения такого поведения необходимо вызвать SetSubComponent с параметром True.

Механизм одинаково работает и для VCL, и для CLX.

Теперь, описав общий механизм записи/чтения свойств можно перейти к примерам его использования.

Загрузка формы в run-time

Одним из интересных методов использования механизма загрузки/чтения свойств является загрузка формы из файла dfm в ходе выполнения программы. Так как в последних версиях Dephi по умолчанию используется текстовый формат хранения свойств формы (второй способ - это хранение в формате ресурсов Windows), то появляется возможность изменять вид формы без перекомпиляции программы.

Создадим новый проект с помощью пункта меню File/New Applcation. На главной форме приложения разместим две кнопки и диалог открытия файла. Установим свойство Caption кнопки Button1 равным LoadForm, а Button2 - SaveForm. Внешний вид формы приведен на рисунке 3.


Рисунок 3.

Ниже приведен текст модуля главной формы.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenDialog1: TOpenDialog;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    procedure ReadFormProperties(DfmName:String; Form:TComponent);
    procedure WriteFormProperties(DfmName: String; Form: TComponent);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  RunTimeForm:TForm;
begin
  RunTimeForm:=TForm1.CreateNew(Self);
  with RunTimeForm do try
    if OpenDialog1.Execute then
    begin
      ReadFormProperties(OpenDialog1.FileName, RunTimeForm);
      ShowModal;
    end;
  finally
    RunTimeForm.Free;
  end;
end;

procedure TForm1.ReadFormProperties(DfmName: String; Form: TComponent);
var
  FileStream:TFileStream;
  BinStream: TMemoryStream;
begin
  FileStream := TFileStream.Create(DfmName, fmOpenRead);
  try
    BinStream := TMemoryStream.Create;
    try
      ObjectTextToBinary(FileStream, BinStream);
      BinStream.Seek(0, soFromBeginning);
      BinStream.ReadComponent(Form);
    finally
      BinStream.Free;
    end;
  finally
    FileStream.Free;
  end;
end;
procedure TForm1.WriteFormProperties(DfmName: String; Form: TComponent);
var
  BinStream:TMemoryStream;
  FileStream: TFileStream;
begin
  BinStream := TMemoryStream.Create;
  try
    FileStream := TFileStream.Create(DfmName, fmOpenWrite or fmCreate);
    try
      BinStream.WriteComponent(Form);
      BinStream.Seek(0, soFromBeginning);
      ObjectBinaryToText(BinStream, FileStream);
    finally
      FileStream.Free;
    end;
  finally
    BinStream.Free
  end;
end;


procedure TForm1.Button2Click(Sender: TObject);
begin
  if OpenDialog1.Execute then
  begin
    WriteFormProperties(OpenDialog1.FileName, Self);
    ShowModal;
  end;
end;

end.

Основную работу по загрузке формы выполняет метод ReadFormProperties, а по записи WriteFormProperties. Оба метода осуществляют конвертацию между двоичным и текстовым представлением свойств формы, используя для этого два потока и вызовы ObjectBinaryToText и ObjectTextToBinary.

Запустим приложение. При нажатии на кнопку LoadForm создается второй экземпляр главной формы приложения. Отредактировав во время выполнения программы файл формы (Unit1.dfm) в текстовом редакторе, и снова нажав кнопку LoadForm, можно убедиться, что сделанные изменения отражаются на внешнем виде формы. Нажатие кнопки SaveForm записывает форму в указанный файл.

Сохранение непубликуемых или нестандартных свойств компонентов

Рассмотрим пример сохранения значения непубликуемого (не published) свойства. Наш компонент будет сохранять историю изменений во время проектирования своего свойства Text, и использовать эту историю на этапе выполнения программы. Вот код данного компонента.

{ ==================================================
    Компонент, сохраняющий историю изменений
    свойства Text.
    Демонстрирует сохранение непубликуемых свойств
  ==================================================  }
unit TextLogger;

interface

uses
  Windows, Messages, SysUtils, Classes;

type
  TTextLogger = class(TComponent)
  private
    { Private declarations }

    FText: String;
    FTextHistory: TStrings;
    procedure SetText(const Value: String);

    // Метод загрузки истории
    procedure ReadTextHistory(Reader:TReader);
    // Метод сохранения истории
    procedure WriteTextHistory(Writer:TWriter);

  protected
    { Protected declarations }

    // Переопределенный метод для сохранения свойства TextHistory
    procedure DefineProperties(Filer: TFiler); override;
  public
    { Public declarations }
    constructor Create(AOwner:TComponent);override;
    destructor Destroy;override;
    // Свойство TextHistory, хранящее историю изменений свойства Text
    property TextHistory:TStrings read FTextHistory;

  published
    { Published declarations }

    // Свойство Text, история изменений которого сохраняется
    property Text:String read FText write SetText;
  end;


procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TTextLogger]);
end;

{ TTextLogger }

constructor TTextLogger.Create(AOwner:TComponent);
begin
  inherited;
  FTextHistory:=TStringList.Create;
end;

procedure TTextLogger.DefineProperties(Filer: TFiler);
begin
  inherited DefineProperties(Filer);
  // Определить методы сохранения свойства TextHistory в файл формы
  Filer.DefineProperty('TextHistory', ReadTextHistory, 
    WriteTextHistory, true);
end;

destructor TTextLogger.Destroy;
begin
  FTextHistory.Free;
  inherited;
end;

procedure TTextLogger.ReadTextHistory(Reader: TReader);
begin
  try
    //Найти маркер начала списка
    Reader.ReadListBegin;
    // Загрузить элементы списка истории
    while not Reader.EndOfList do
    begin
      FTextHistory.Add(Reader.ReadString);
    end;
    // Прочитать маркер окончания списка
    Reader.ReadListEnd;
  except
    FTextHistory.Clear;
    raise;
  end;
end;

procedure TTextLogger.SetText(const Value: String);
begin
  if Value <> FText then
  begin
    FTextHistory.Add(FText);
    FText := Value;
  end;
end;

procedure TTextLogger.WriteTextHistory(Writer: TWriter);
var
  Cnt:integer;
begin
  // Записать маркер начала списка
  Writer.WriteListBegin;
  // Записать историю изменений
  for Cnt:=0 to FTextHistory.Count-1 do
  begin
    Writer.WriteString(FTextHistory[Cnt]);
  end;
  // Записать маркер окончания списка
  Writer.WriteListEnd;
end;

end.

Итак, для хранения истории изменений на этапе проектирования свойства Text компонент имеет свойство property TextHistory : TStrings read FTextHistory;

Метод SetText, отвечающий за установку значения свойства Text, сохраняет старое значение. При сохранении формы необходимо сохранить историю изменений, а при загрузке восстановить ее. Так как свойство TextHistory объявлено в секции public, то автоматически оно сохранено не будет.

Чтобы среда Delphi узнала о необходимости сохранения свойства TextHistory и о том, как это делать, необходимо переопределить метод DefineProperties компонента. Переопределенный метод DefineProperties после вызова метода предка производит вызов:

Filer.DefineProperty('TextHistory', ReadTextHistory, WriteTextHistory, true);

чтобы указать, какие методы необходимо использовать для загрузки и сохранения свойства TextHistory. Загрузка производится методом ReadTextHistory, а сохранение WriteTextHistory.

Метод ReadTextHistory вызывается в ходе загрузки свойств компонента. В качестве параметра он получает объект Reader : TReader. Метод находит и считывает из потока данных маркер начала списка, затем в цикле загружает строки элементов и считывает маркер окончания списка.

Метод WriteTextHisory отвечает за сохранение свойства TextHistory. В качестве параметра он принимает объект TWriter. Метод записывает маркер начала списка в поток данных, и в цикле сохраняет каждый элемент списка в поток. При достижении конца списка в поток записывается маркер конца списка.

Остается только скомпилировать и зарегистрировать пакет, в который помещен данный компонент.

Для тестирования сохранения свойства создайте новое приложение. На главную форму положите наш компонент TTextLogger. В среде Delphi или текстовом редакторе просмотрите dfm-файл главной формы. Затем измените несколько раз значение свойства Text в инспекторе объектов и убедитесь, что история изменений сохраняется в dfm-файле. Ниже приводится текст dfm-файла после вышеописанных действий.

object Form1: TForm1
  Left = 192
  Top = 114
  Width = 870
  Height = 640
  Caption = 'Form1'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object TextLogger1: TTextLogger
    Text = '678'
    Left = 368
    Top = 160
    TextHistory = (

      '123'
      '345')
  end
end

Как видим, свойство TextHistory действительно сохраняется в файле формы.

Заключение

Конечно, это только введение в такую обширную и многогранную область программирования на Borland Delphi, как создание компонентов. В будущем мы еще не раз вернемся к созданию компонентов Delphi, и обсудим более тонкие моменты этого процесса.

Статья написана на основе материалов новой книги Михаила Голованова, посвященной компонентам Delphi.


Эта статья опубликована в журнале RSDN Magazine #1-2003. Информацию о журнале можно найти здесь