Процес побудови компонента. У цій статті ми розглянемо основні стадії побудови компонента, зосередившись на питаннях конструювання, тестування та встановлення компонента в Delphi. Нижче будуть докладно розібрані підготовка каркаса компонента, а також створення властивостей, методів і подій компонента.

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

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

Модуль компонента можна створити вручну або з допомогою експерта побудови компонентів.

Першим кроком, незалежно від використовуваного методу, є створення окремої папки для файлів компонента. Звичайно, можна зберігати файли всіх компонентів в одній папці, але через деякий час вона перетвориться просто в смітник.

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

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 необов’язково, але дуже бажано. Якщо цього не зробити, значення за замовчуванням для даних властивостей будуть записуватися у файл форми, що буде сповільнювати завантаження форми.

На практиці етап проектування і кодування компонент є одним із самих довгих і кропітких.

Збереження стану компонентов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
Одним з цікавих методів використання механізму завантаження/читання властивостей є завантаження форми з файлу дрт в ході виконання програми. Так як в останніх версіях Dephi за замовчуванням використовується текстовий формат зберігання властивостей форми (другий спосіб — це зберігання в форматі ресурсів Windows), то з’являється можливість змінювати вигляд форми без перекомпіляції програми.

Створимо новий проект за допомогою пункту меню File/New Applcation. На головній формі програми розмістимо дві кнопки і діалог відкриття файлу. Установимо властивість Caption кнопки Button1 рівним LoadForm, а Button2 — SaveForm
Нижче наведено текст модуля головної форми.

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, і обговоримо більш тонкі моменти цього процесу.