Пробираясь сквозь корни и ветви…

Одной из характерных отличительных особенностей среды программирования Borland Delphi является возможность построения программ из компонентов. Компоненты выступают в роли строительного материала приложения — кирпичиков, представляющих как визуальные (формы, кнопки, поля ввода), так и невизуальные (источники данных, интернет-подключения) объекты. Такая архитектура среды разработки позволяет создавать вполне функциональные программы с минимальным объемом кода, специфичного для приложения. Достаточно выбрать необходимые компоненты, задать их свойства, добавить код реакции на ожидаемые события — и продукт готов. Но, несмотря на богатство палитр компонентов, идущих в стандартном комплекте поставки Delphi, на множество доступных через Интернет компонентов сторонних разработчиков, со временем появляется желание сделать что-нибудь по- своему, выйдя из проложенной другими колеи. Чтобы реализовать такую возможность, нужно создать собственный компонент. Delphi не только не чинит преград в этом направлении, но и предлагает два пути. Можно создать свой компонент на базе уже существующего, добавив новую функциональность или изменив какие-нибудь параметры (например, сделать окно нестандартной формы или ввести подсветку синтаксиса в окно редактирования). Если же для реализации задуманного подходящего базового компонента не нашлось, ничто не мешает написать компонент "с чистого листа". В этой статье будет рассказано о разработке с нуля невизуального компонента для перебора файлов начиная с некоторого каталога с рекурсивным обходом дерева подкаталогов. Решение этой задачи может служить хорошей иллюстрацией практического применения рекурсии. Кроме того, в процессе разработки компонента будут рассмотрены такие сущности классов Object Pascal, как поля, методы, свойства и события.

Приступая к разработке нового компонента, в двух словах определим сам термин "компонент". Оказывается, за этим словом скрывается не что иное как класс — тип данных, введенный объектноориентированной парадигмой программирования. То есть компонент выглядит как класс, определяется как класс, и вообще он ничем не отличался бы от остальных классов, если бы не одно "но": предком класса-компонента должен быть либо другой компонент (в случае доработки существующего компонента), либо класс TComponent (в случае создания нового компонента). Это легко выполнимое требование позволяет использовать возможности среды Delphi для управления свойствами создаваемого компонента и придает ему базовую функциональность. Уточним задачу. Пусть необходимо разработать компонент TEnumFiles, позволяющий выполнять перебор файлов, имена которых удовлетворяют некоторому списку допустимых масок (например, для документов Microsoft Office этот список может содержать значения: "*.DOC", "*.RTF", "*.XLS", а для исполняемых файлов он будет другим: "*.EXE", "*.COM", "*.BAT"). Поиск файлов должен начинаться с заданного каталога и, возможно, выполняться с рекурсивным просмотром подкаталогов. Определим исходные данные. Пусть имя каталога содержится в поле FBaseDir, признак просмотра подкаталогов — в поле FWithSubdir, маски файлов — в поле FMask. Будем последовательными в реализации объектноориентированного подхода к решению задачи и поместим эти поля в закрытую секцию класса TEnumFiles. Object Pascal предлагает достаточно элегантный способ доступа к полям закрытой секции из других модулей программы — с использованием свойств. Свойство описывает не только способ доступа к таким полям, но и реализует простейший механизм разделения прав доступа — по чтению, записи или полный доступ. Сама реализация свойства очень проста: для чтения необходимо написать метод класса — функцию, возвращающую значение того же типа, что и свойство (поскольку свойство организуется для доступа к полю, при вычислении значения функции логично использовать значение поля, хотя это требование не является обязательным). Для записи соответственно разрабатывается метод класса — процедура, принимающая в качестве параметра значение того же типа, что и свойство. Принятое значение может подвергаться внутри процедуры различным проверкам и преобразованиям, но, в конце концов, как правило, присваивается полю. В простейшем случае вместо функции и процедуры в описании свойства может просто подставляться идентификатор поля. Излишне напоминать, что описания свойств нужно разместить в общедоступной секции класса. При разработке компонентов (в отличие от простых классов) подразумевается, что значения свойств могут устанавливаться в редакторе свойств "Инспектора объектов" Delphi на этапе конструирования приложения. Чтобы предоставить такую возможность, описания свойств нужно размещать в секции published. В рассматриваемой задаче для доступа к полям закрытой секции реализованы свойства BaseDir, WithSubdir и Mask соответственно. С учетом сказанного описание класса имеет следующий вид:

type TEnumFiles = class (TComponent)
private
FBaseDir: TFileName; { базовый каталог }
FWithSubdir: boolean; { признак просмотра подкаталогов }
FMask: TStringList; { маска файлов }
published
property BaseDir: TFileName read FBaseDir write FBaseDir;
property WithSubdir: boolean read FWithSubdir write FWithSubdir;
property Mask: TStringList read FMask write FMask;
end;

Механизм перебора файлов будет реализован в процедуре ProcessSubdirectory. Суть алгоритма перебора такова. В процедуру передается параметр — путь к подкаталогу для обработки. В подкаталоге выполняется поиск файлов, удовлетворяющих каждой из заданных масок. После того, как все файлы будут обработаны, начинается перебор всех подкаталогов рассматриваемого каталога. Для каждого найденного каталога рекурсивно вызывается процедура ProcessSubdirectory, и процесс повторяется. Процедура ProcessSubdirectory завершает свою работу, когда будут рассмотрены все подкаталоги рассматриваемого каталога. Первый вызов процедуры ProcessSubdirectory осуществляется из метода Enum, размещенного в открытой секции класса компонента. Что же представляет собой факт нахождения (отыскания) в рассматриваемом подкаталоге очередного файла? Обнаружение этого явления является целью решения поставленной задачи перебора файлов. В терминах Object Pascal такое ожидаемое явление называют событием. При возникновении события логично выполнять какие-то предопределенные действия. Для этого указывается обработчик события. Связка "событие — обработчик события" реализуется через механизм указателей на подпрограмму (функцию или процедуру). Обработчик события вызывается из специального метода класса компонента, который называют диспетчером событий (см. рис. 1). Общее правило при разработке компонентов таково: диспетчеры событий должны корректно работать вне зависимости от того, назначен обработчик возможному событию или нет. Поскольку при создании класса его поля, являющиеся указателями, инициализируются значениями nil, то для выполнения этого условия достаточно перед вызовом обработчика проверять значение соответствующего указателя. Обработчик события — это процедура, которая является членом класса-контейнера, внутри которого помещен компонент. Обычно в роли контейнера выступает форма, но вне зависимости от конкретного типа нужно понимать, что обработчик события — это метод. Метод отличается от обычной процедуры тем, что при его вызове наряду с объявленными параметрами ему неявно передается еще один — указатель Self на экземпляр класса. Поэтому, описывая тип указателя на обработчик события, нужно к процедурному типу добавлять спецификатор "of object".


Рис. 1. Схема вызова обработчика события

Наряду с событием отыскания подходящего файла (OnFileFound) в процессе перебора могут происходить другие, на первый взгляд, менее значительные события, как-то: погружение в подкаталог (OnSubdirFound) и выход из подкаталога (OnSubdirAbandon). Если разрешить назначать обработчики и для этих событий, можно сделать разрабатываемый компонент более универсальным, а его использование — более удобным. Желательно только ввести в класс поля, которые в любой момент (при вызове любого обработчика события) будут содержать текущий подкаталог относительно базового каталога (FSubdir) и имя текущего (найденного) файла (FFName). Поскольку использовать содержимое этих полей предполагается только из обработчиков событий компонента, их можно объявить в закрытой секции класса. С учетом сказанного класс TEnumFiles приобретает следующий вид:

type TEnumFiles = class (TComponent)
private
FBaseDir: TFileName; { базовый каталог }
FWithSubdir: boolean; { признак просмотра подкаталогов }
FMask: TStringList; { маска файлов }
FSubdir: TFileName; { подкаталог базового каталога }
FFName: TFileName; { имя файла }
FOnFileFound: TOnFileFoundEvent;
FOnSubdirFound: TOnSubdirFoundEvent;
FOnSubdirAbandon: TOnSubdirAbandonEvent;
protected
public
constructor Create(AOwner: TComponent); override;
procedure Enumerate;
destructor Destroy; override;
property Subdir: TFileName read FSubdir;
property FName: TFileName read FFName;
published
property BaseDir: TFileName read FBaseDir write FBaseDir;
property WithSubdir: boolean read FWithSubdir write FWithSubdir;
property Mask: TStringList read FMask write FMask;
property OnFileFound: TOnFileFoundEvent read FOnFileFound write FOnFileFound;
property OnSubdirFound: TOnSubdirFoundEvent read FOnSubdirFound write FOnSubdirFound;
property OnSubdirAbandon: TOnSubdirAbandonEvent read FOnSubdirAbandon write FOnSubdirAbandon;
end;

Контуры компонента вполне очерчены, можно приступать к формированию модуля компонента. Поскольку базовый компонентный класс TComponent и класс — список строк TStringList декларируются в модуле Classes, а использованные типы для выполнения поиска файлов — в модуле Sysutils, оба эти модуля подключаются в интерфейсной секции. Кроме того, перед описанием типа TEnumFiles декларируются процедурные типы обработчиков событий. Класс TEnumFiles должен иметь собственные конструктор и деструктор для управления памятью списка масок файлов Mask. Вот как выглядит модуль TEnumFiles:

unit EnumFiles;
interface
uses Classes, Sysutils;
{----------------------------}
{ Типы обработчиков событий. }
{----------------------------}
type TOnFileFoundEvent = procedure (Sender: TObject; const Subdir: TFileName; const FileInfo: TSearchRec) of object;
type TOnSubdirFoundEvent = procedure (Sender: TObject; const SubdirInfo: TSearchRec) of object;
type TOnSubdirAbandonEvent = procedure (Sender: TObject) of object;
{-----------------------------------------------------}
{ Здесь должно находиться описание класса TEnumFiles, }
{ приведенное выше. }
{-----------------------------------------------------}
implementation
{=================================}
{ Конструктор переборщика файлов. }
{=================================}
constructor TEnumFiles.Create(AOwner: TComponent);
begin
inherited Create (AOwner);
Mask := TStringList.Create
end;
{================================================================}
{ Выполнение перебора файлов. }
{ Входные данные: }
{ FBaseDir - каталог, с которого начинать поиск файлов, }
{ FWithSubdir - признак рекурсивного обхода подкаталогов, }
{ FMask - маска отбираемых файлов. }
{ При нахождении каждого подходящего файла происходит событие }
{ OnFileFound с параметрами:}
{ - базовым каталогом перебора, }
{ - относительным каталогом перебора, }
{ - детальной информацией о найденном файле. }
{ При переходе к обработке нового подкаталога происходит событие }
{ OnSubdirFound, а при завершении — событие OnSubDirAbandon. }
{================================================================}
procedure TEnumFiles.Enumerate;
procedure ProcessSubdirectory (const ASubdir: TFileName);
var
FileInfo: TSearchRec; { информация о найденном элементе каталога }
I: integer;
res: integer; { результат поиска }
begin
{-----------------------------------}
{ Перебор файлов текущего каталога. }
{-----------------------------------}
FSubdir := ASubdir;
for I := 0 to FMask.Count — 1 do { поиск файлов по каждой из заданных масок }
begin
res := FindFirst (FBaseDir + ASubdir + FMask.Strings[I], faAnyFile xor faDirectory, FileInfo);
while res = 0 do
begin
FFName := FileInfo.Name;
if Assigned (OnFileFound) then OnFileFound (Self, ASubdir, FileInfo);
res := FindNext (FileInfo)
end;
FindClose (FileInfo)
end;
{-----------------------------------------}
{ Перебор подкаталогов текущего каталога. }
{-----------------------------------------}
if FWithSubdir then
begin
res := FindFirst (FBaseDir + ASubdir + '*.*', faDirectory, FileInfo);
while res = 0 do
begin
if (FileInfo.Attr and faDirectory <> 0) and (FileInfo.Name <> '.') and (FileInfo.Name <> '..') then
begin
if Assigned (OnSubdirFound) then OnSubdirFound (Self, FileInfo);
ProcessSubdirectory (ASubDir + FileInfo.Name + '\');
if Assigned (OnSubdirAbandon) then OnSubdirAbandon (Self)
end;
res := FindNext (FileInfo)
end;
FindClose (FileInfo)
end
end;
begin
if Copy (FBaseDir, Length (FBaseDir), 1) <> '\' then FBaseDir := FBaseDir + '\';
ProcessSubdirectory ('');
end;
{================================}
{ Деструктор переборщика файлов. }
{================================}
destructor TEnumFiles.Destroy;
begin
Mask.Free;
inherited Destroy;
end;
end.

После того, как класс компонента помещен в модуль и исправлены все синтаксические ошибки, возникает желание побыстрее разместить его на панели инструментов и начать им пользоваться. Однако не следует торопиться. Сначала нужно выполнить всестороннее тестирование, ведь в противном случае, если компонент содержит ошибки, можно нарушить работу среды Delphi в целом. Тестировать компонент достаточно просто. Рассмотрим этот процесс на примере. Возьмем простейшее приложение, состоящее из единственной формы Form1 (такой каркас такого приложения автоматически получается при создании нового проекта). На форму поместим область редактирования текста Memo1 и кнопку Button1. В область редактирования текста будем записывать наименования файлов и каталогов, встречающихся в процессе перебора, который будет инициироваться нажатием кнопки. Код создания экземпляра класса компонента разместим внутри метода — обработчика события OnCreate главной формы тестового приложения. Не забудем и об утилизации динамической памяти — разместим вызов деструктора компонента внутри метода — обработчика события OnDestroy той же формы. Вызов подпрограммы поиска файлов назначим обработчику события OnClick кнопки Button1. Вот примерный текст тестового приложения:

unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, EnumFiles;
type
TForm1 = class(TForm)
ListBox1: TListBox;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
EF: TEnumFiles;
protected
procedure OnFile (Sender: TObject; const Subdir: TFileName; const FileInfo: TSearchRec);
procedure OnSubdirIn (Sender: TObject; const SubdirInfo: TSearchRec);
procedure OnSubdirOut (Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
{=================================================}
{ Обработчики событий компонента перебора файлов. }
{=================================================}
procedure TForm1.OnFile (Sender: TObject; const Subdir: TFileName; const FileInfo: TSearchRec);
begin
ListBox1.Items.Add (DateTimeToStr (FileDateToDateTime (FileInfo.Time)) + ' | ' + FileInfo.Name)
end;
procedure TForm1.OnSubdirIn (Sender: TObject; const SubdirInfo: TSearchRec);
begin
ListBox1.Items.Add ('--> ' + SubdirInfo.Name);
end;
procedure TForm1.OnSubdirOut (Sender: TObject);
begin
ListBox1.Items.Add ('<-- ' + EF.Subdir);
end;
{=================================================}
{ Обработчики событий формы тестового приложения. }
{=================================================}
procedure TForm1.FormCreate(Sender: TObject);
begin
{---------------------------------}
{ Создание экземпляра компонента. }
{---------------------------------}
EF := TEnumFiles.Create (Self);
{----------------------------------}
{ Назначение обработчиков событий. }
{----------------------------------}
EF.OnFileFound := OnFile;
EF.OnSubdirFound := OnSubdirIn;
EF.OnSubdirAbandon := OnSubdirOut;
{------------------------------------}
{ Задание параметров перебора файлов }
{ и обхода дерева подкаталогов.}
{------------------------------------}
EF.BaseDir := 'C:\Program Files\Borland\Delphi5\Source\';
EF.WithSubdir := True;
EF.Mask.Add ('*.dpr');
EF.Mask.Add ('*.res');
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
EF.Free
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Enabled := false;
EF.Enumerate;
Button1.Enabled := true
end;
end.

А вот что получается при его выполнении после нажатия на кнопку Button1 (см. рис. 2):


Рис. 2. Окно с результатом работы тестовой программы. Переход в подкаталог обозначен стрелкой <<—, а выход из подкаталога — стрелкой —>>

После того, как тестирование успешно завершено, можно приступать к внедрению разработанного компонента в среду Delphi для постоянного использования. Осталось сделать завершающие шаги. Первый шаг — добавить в модуль компонента процедуру регистрации вида:

Interface
{ … }
Procedure Register;
Implementation
{ … }
Procedure Register;
Begin
RegisterComponents ('MyComponents', [TEnumFiles]);
End;

Где 'MyComponents' — название палитры компонентов, а TEnumFiles — класс добавляемого компонента. Второй шаг — нарисовать пиктограмму размера 24x24, которой будет обозначаться компонент на панели инструментов. Саму пиктограмму нужно назвать точно так же, как и класс компонента, только прописными буквами: TENUMFILES. Ресурс с пиктограммой нужно записать в RES-файл, после чего этот RES-файл переименовать в EnumFiles.DCR и поместить в один каталог с файлом исходного текста модуля компонента EnumFiles.PAS. Добавляется компонент в среду Delphi через пункт меню Component ( Install Components. В диалоговом окне указывается путь к файлу EnumFiles.PAS, а в появившемся вслед за ним окне управления библиотеками компонентов нажимается кнопка Compile. После компиляции в палитре компонентов должна появиться новая закладка MyComponents (рис. 3).


Рис 3. Компонент TEnumFiles в палитре компонентов

Теперь с только что созданным компонентом можно обращаться точно так же, как со всеми остальными компонентами Delphi. Если поместить компонент на форму, то его свойства и обработчики событий можно задавать через окно Инспектора объектов, как показано на рис. 4.


Рис. 4. Элементы компонента в Инспекторе объектов

Пора подвести черту. В статье был рассмотрен процесс создания невизуального компонента среды Delphi путем разработки компонентного класса "с чистого листа". Из рассказанного видно, что компонент является, по сути, обычным классом Object Pascal, на который наложены некоторые требования, обеспечивающие его интеграцию в среду программирования. На основе материала статьи можно сделать вывод, что разработка компонентов не должна представлять особых трудностей для программистов, знакомых с объектноориентированным расширением языка Pascal. Более того, этот вывод можно распространить и на другие области IT-индустрии: за "модными" терминами, введение которых часто является маркетинговым ходом (например, "компонент", "OLE", "DDE", "COM", "ActiveX" — список можно продолжить), скрываются некоторые специфические способы использования вполне традиционных методов программирования или элементов операционных систем: объектноориентированного подхода и библиотек динамической компоновки. Поэтому при серьезном подходе к разработке приложений (в частности, под Windows) необходимо обратить внимание на глубокое изучение конструкций выбранного языка программирования (будь то Object Pascal, C/C++/C#, Java или Basic) и архитектуры операционной системы (будь то Windows или *NIX). Возможности визуальной среды программирования позволяют ускорить процесс разработки, сделать его более комфортным, но качество конечного продукта по-прежнему определяется уровнем знаний разработчика и его пониманием взаимосвязей использованных "кирпичиков". В шахматы компьютеры уже выигрывают, но без программистов им пока что не обойтись. Удачных разработок!

Игорь Орещенков, 2006 г.


Компьютерная газета. Статья была опубликована в номере 08 за 2006 год в рубрике программирование

©1997-2024 Компьютерная газета