Динамическое создание форм Delphi. Создание компонент в DELPHI

Форма "О программе" используется в качестве информативных целей. Чаще всего в ней пишется наименование программы, назначение, авторы и лицензионное соглашение. Чаще всего размещается в главном меню в пункте Помощь->О программе....

Для создание формы "О программе" в Delphi (Делфи, Дельфи) нужно создать обычную форму, как на рисунке ниже:

Для создания любой формы в Delphi 7 используем следующую команду из меню File->New->Form, а при разработке проекта в более новой среде, к примеру Delphi XE2, нужно использовать немного другую команду: File->New->VCL Form - Delphi.

После создания формы нужно выставить нужные размеры и рекомендуется выполнить следующие настройки:

  • Выставить свойство Position. По умолчанию здесь указано значение poDesigned, которое диктует открываемой форме появляться там, где она находилась в момент создания программы. Поэтому нужно выставить, так чтобы форма появлялась по центру относительно формы, из которой она была вызвана. Для этого в свойство Position надо выставить значение poOwnerFormCenter.
  • Также можно отключить свойство AutoScroll, чтобы не появлялись полосы прокрутки.
  • Так как в форме будет краткая статическая информация, то иметь возможность изменять размеры формы во время выполнения нет никакой необходимости. Поэтому в свойстве BorderStyle выставить значение bsSingle.
  • Еще нужно настроить вложенные свойства в BorderIcons. Нужно в свойствах biMinimize и biMaximize выставить значения в false, что не позволит минимизировать и развернуть форму соответственно.

Помимо всех описанных настроек, также можно украсить форму фоном. Для этого нужно из палитры компонентов поместить Image, который находится во вкладке Additional, если вы пишите в Delphi 7. В свойстве сразу нужно выставить значение alClient, которое растянет контейнер для изображения на всю форму.

Также при использовании TLabel можно выставить Transparent в true. Данная опция сделает так, чтобы фон метки был прозрачен, что будет выглядеть намного красивее, если используется вспомогательный фон.

Заключение

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

Дата публикации

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

В каком случае форму лучше создавать динамически?

Я бы выделил два основных момента.

  1. Если форма используется редко и велика вероятность того, что её вообще не вызовут. Примером может быть окно "О программе".
  2. Если количество экземпляров нашего окна может быть больше одного.

Как создать форму динамически, я как всегда представлю на примере.

Пример динамического создания формы.

  1. Нажмите пункт главного меню "File".
  2. Затем new.

Программа сама набросала класс будущей формы. Т.к. создавать окно мы будем по нажатию кнопки, предлагаю удалить следующие строки кода.

var Form2: TForm2; // Если Вы дали форме имя, то этот участок кода будет разниться

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

Теперь возвращаемся к главный форме. Подключим к ней только что созданный модуль: File->Use Unit...-> Выбираем нужный (Можно просто приписать в разделе Uses).

Создадим две кнопки. Создавать новое окно мы будем двумя различными способами:

procedure TForm1.Button1Click(Sender: TObject); var Form2: TForm2; begin Form2:= TForm2.Create(Application); Form2.Caption:= "Новое форма, способ 1"; Form2.Show; end;

procedure TForm1.Button2Click(Sender: TObject); var Form2: TForm2; begin Application.CreateForm(TForm2,Form2); Form2.Caption:= "Новое форма, способ 2"; Form2.Show; end;

Вот, все очень просто, если же у Вас остались какие-то вопросы - пишите в комментарии. Буду раз ответить.

Профессиональная разработка приложений с помощью Delphi 5 | Средства разработки | КомпьютерПресс 2"2001

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

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

При разработке приложений с помощью Borland Delphi создавать компоненты удобно по следующим причинам:

  1. Простота использования . Компонент помещается на форму, и для него необходимо устанавливать значения свойств и писать код обработчиков событий. Поэтому если в проекте какое-либо сочетание элементов управления и обработчиков связанных с ними событий встречается в двух местах, то имеет смысл подумать о создании соответствующего компонента. Если же сочетание элементов управления и обработчиков связанных с ними событий встречается более двух раз, то создание компонента гарантированно сэкономит усилия при разработке приложения.
  2. Простая организация групповой разработки проекта . При групповой разработке отдельные части проекта можно определить как компоненты и поручить эту работу разным программистам. Компоненты можно отладить отдельно от приложения, что сделать достаточно легко.
  3. Простой и эффективный способ обмена кодом с другими программистами. Имеется немало сайтов, например http://www.torry.net/ , где можно найти свободно распространяемые компоненты или приобрести их за символическую плату.

Пакеты компонентов

В Delphi компоненты хранятся в пакетах (packages). Список используемых пакетов компонентов можно вызвать с помощью пункта меню Component/Install Packages (правда, этот диалог почему-то имеет заголовок Project Options).

При помощи этого диалога можно добавить новый пакет (Add), удалить имеющийся (Remove). Удаление означает не физическое удаление файла с диска, а удаление ссылки из среды разработки на данный пакет. При добавлении нового пакета компоненты, хранящиеся в нем, появляются на палитре, а при удалении – наоборот, исчезают. Пакет можно не удалять, а «спрятать» его содержимое на этапе разработки посредством снятия отметки напротив имени пакета в списке. Можно также просмотреть компоненты и их пиктограммы (Components). И наконец, можно отредактировать добавленные пользователем пакеты (Edit) – пакеты, поставляемые вместе с Delphi, редактировать нельзя (кнопка Edit недоступна).

В данном диалоге можно указать, каким образом создавать проект: с использованием runtime-пакетов или без них. Отсюда ясно, что пакеты компонентов бывают двух типов: runtime package (пакет, работающий во время выполнения) и design-time package (пакет, используемый во время разработки). Все они представляют собой DLL (динамически загружаемые библиотеки).

Runtime-пакеты (расширение *.bpl) поставляются конечному пользователю вместе с проектом, если проект был скомпилирован с включенной опцией Build with runtime packages. Само приложение (*.exe или *.dll) в этом случае получается небольшим, но вместе с ним надо передавать довольно объемные *.bpl-файлы. Согласно оценкам специалистов поставка проекта с runtime-пакетами дает преимущество в объеме поставляемых файлов, если только он включает пять или более модулей (*.exe или *.dll), написанных на Delphi. При совместной работе этих модулей достигается экономия ресурсов операционной системы, поскольку один загруженный в ОЗУ пакет обслуживает несколько модулей.

Design-time-пакеты (расширение *.dcp) используются только на этапе разработки. Во время разработки они поддерживают создание компонентов на форме. В скомпилированный проект Delphi включает код не из пакета компонентов, а из *.dcu-файлов. Хотя *.dcp-файл генерируется из *.dcu-файла, их содержимое может не совпадать, если в *.pas-файл были внесены изменения и пакет не был перекомпилирован. Компиляция возможна только для пакетов, созданных программистами. Это достигается нажатием кнопки Edit в вышеупомянутом диалоге. После этого появляется форма, которая позволяет производить манипуляции с пакетом.

Пакет содержит две секции. В секции Contains приведен список модулей, формирующих компоненты данного пакета (*.pas- и *.dcu-файлы) и их пиктограммы (*.dcr-файлы). Секция Required содержит ссылки на другие пакеты, необходимые для работы этих компонентов. Добавление нового компонента к пакету выполняется кнопкой Add, удаление имеющегося – кнопкой Remove. До тех пор пока пакет не будет скомпилирован нажатием кнопки Compile, все изменения, вносимые в пакет, не будут появляться в среде разработки. И наконец, команда Install доступна в том случае, когда содержимое пакета удалено из среды разработки посредством снятия отметки напротив имени пакета в предыдущем диалоге.

Команда Option позволяет выбрать для компиляции пакета опции, аналогичные опциям проекта. В них можно определить тип данного пакета: работающий во время выполнения, работающий во время разработки, или тот и другой одновременно (тип пакета по умолчанию). В опциях определяются каталоги, в которых следует искать необходимые модули и сохранять результаты компиляции. В них также определяются действия, необходимые для отладки: проверять или нет диапазон допустимых значений, как осуществлять оптимизацию, как обрабатывать ошибки ввода-вывода. И наконец, в опции может быть включена информация о версии пакета. Это очень важно, если приложение распространяется вместе с runtime-пакетами: при работе программы установки информация о версии позволит корректно заменить устаревшие версии пакетов, и наоборот, при попытке инсталлировать пакет более ранней версии, чем уже имеющийся на данном компьютере, последний не будет перезаписан.

Шаблоны компонентов

Delphi позволяет создавать простейшие составные компоненты из нескольких обычных компонентов, выбранных на форме во время разработки. Соответствующий эксперт вызывается с помощью пункта меню Components/Create Component Template. Этот пункт меню доступен, если на форме выделен хотя бы один компонент. После его выбора появляется диалоговая панель Component Template Information.

В этом диалоге следует указать имя класса и имя страницы на палитре компонентов, куда следует поместить новый компонент. Если страница с данным именем отсутствует на палитре компонентов, то она будет создана. Можно также изменить предложенную пиктограмму нового компонента, загрузив подходящий *.bmp-файл.

При создании шаблона запоминаются как свойства, измененные программистом в инспекторе объектов, так и обработчики событий, связанные с выделенными элементами управления. При этом обработчики событий запоминаются полностью, без фильтрации обращений к другим (не выделенным на форме) компонентам, глобальным переменным, методам и т.д. Соответственно, если в другом проекте такие компоненты (переменные, методы) отсутствуют, то при попытке скомпилировать такой проект будет получено диагностическое сообщение Unknown Identifier.

Когда следует пользоваться шаблонами? Прежде всего, в случаях, если необходимо изменить какие-либо свойства, которые имеются по умолчанию в базовом классе. Например, в каком-либо приложении используется элемент управления для редактирования строки текста желтого цвета. Можно поместить компонент TEdit на форму, изменить свойство Color на желтый, отметить данный компонент и сохранить как шаблон. После этого можно обращаться к данному шаблону, и помещенный на форму компонент будет иметь желтый цвет. Однако не стоит злоупотреблять данной возможностью, ведь для элемента управления с измененным цветом будет создан новый класс и в памяти будут размножены все виртуальные методы. Это отрицательно скажется на ресурсах операционной системы.

Использовать шаблоны компонентов удобно также, когда необходимо перенести ряд компонентов вместе с обработчиками событий с одной формы на другую. Для этого все они выделяются, создается шаблон компонентов, который и помещается на новую форму. При этом будут перенесены не только сами компоненты, но и обработчики событий, чего нельзя достичь при вызове команд Copy/Paste – в последнем случае обработчики событий будут утеряны.

Компоненты, создаваемые при помощи команды Create Component Template, существенно отличаются от обычных компонентов, создаваемых стандартным способом (описанным ниже). Визуально главное различие заключается в следующем: если шаблон включает в себя несколько элементов управления, то, после того как такой компонент помещен на форму, можно выделить отдельный элемент управления и удалить его – при этом остальные сохранятся на форме. Для стандартных компонентов, если они включают в себя несколько элементов управления, невозможно выделить один из них и удалить –компонент выделяется и удаляется целиком.

Создание простейшего компонента

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

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

Создание компонента начинается с выбора пункта меню Component/New components. После этого сразу же появляется диалог New Component.

В этом диалоге необходимо определить класс-предок, имя вновь создаваемого класса, страницу на палитре, куда будет помещен новый компонент, имя модуля, содержащего реализацию нового компонента, и путь к нему. Если новый компонент использует другие модули, путь к которым не описан, то их необходимо определить в поле Search Path.

Итак, первая (и, пожалуй, главная) задача – выбор класса-предка. В выпадающем списке в качестве класса-предка предлагаются все компоненты, имеющиеся на палитре, в том числе и те, которые не входят в стандартную поставку Delphi. Необходимо в качестве класса-предка выбрать класс, который максимально приближен по свойствам к создаваемому классу. Для нашей задачи можно, например, выбрать в качестве предка TWinControl, но в этом случае нам потребуется реализовывать все визуальные эффекты нажатия кнопки и т.д. Поэтому мы выбираем в качестве предка TButton.

Имя вновь создаваемого класса должно отражать содержание компонента и ни в коем случае не совпадать с именем уже зарегистрированного компонента! На этапе заполнения данного диалога имена на совпадения не проверяются – приключения, связанные с такой ошибкой, начнутся позже…

При выборе страницы необходимо знать, что если задать имя несуществующей страницы, то будет создана новая.

И наконец, при нажатии как кнопки Install, так и кнопки OK, будет создана заготовка для реализации нового компонента. Однако при нажатии кнопки Install заготовка будет помещена на палитру компонентов, а при нажатии кнопки OK – просто создана. Рекомендуется пользоваться кнопкой Install. После того как компонент будет инсталлирован, его можно поместить на форму. Теперь все изменения, вносимые в код реализации компонента, будут компилироваться вместе с проектом, и программист сразу же будет получать сообщения об ошибках. Если компонент не инсталлировать, то для поиска ошибок его необходимо компилировать через редактор пакетов (см. выше) нажатием кнопки Compile, что менее удобно.

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

В этом диалоге имеются две страницы, на первой из них можно выбрать один из существующих пакетов, а на второй – создать новый. Весьма желательно давать краткое текстовое описание пакета, именно оно будет показываться в диалоге, вызываемом по команде Component/Install packages (см. выше). После выбора пакета и нажатия клавиши OK вызывается редактор пакета, куда автоматически помещается вновь созданный модуль реализации нового компонента. Полезно не закрывать его, а сдвинуть в один из углов экрана, чтобы он мог быть активирован нажатием клавиши мыши.

Одновременно в редакторе кода будет создана «заготовка» для описания нового компонента:

Unit ButtonBeep; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TButtonBeep = class(TButton) private { Private declarations } protected { Protected declarations } public { Public declarations } published { Published declarations } end; procedure Register; implementation procedure Register; begin RegisterComponents("Samples", ); end; end.

В самом новом классе объявлены четыре секции, значение которых детально описано в разделе «Область видимости переменных и методов» предыдущей статьи данного цикла (КомпьютерПресс № 1"2001). Кроме того, в новом классе определена процедура Register, которая вызывается средой разработки Delphi при инсталляции данного модуля как компонента. Она содержит имя страницы на палитре, куда помещается данный компонент, и в квадратных скобках – имя класса. Вообще, в качестве параметра метод Register принимает массив типов классов, ведь в одном модуле может быть реализовано несколько компонентов. Поэтому они отделяются друг от друга запятой, например:

Procedure Register; begin RegisterComponents("Samples", ); end;

Продолжим решение поставленной задачи – создание кнопки, которая издает писк. Поступим сначала тривиально (но как выяснится потом, неверно) – назначим обработчик события OnClick в конструкторе кнопки. Для этого в секции private определим заголовок нового метода BtClick(Sender:TObject) и реализуем его в секции реализации:

Procedure TButtonBeep.BtClick(Sender:TObject); begin Beep; end;

constructor Create(AOwner:TComponent); override;

с обязательной директивой override! Реализуем его в секции реализации:

Constructor TButtonBeep.Create(AOwner:TComponent); begin inherited Create(AOwner); OnClick:=BtClick; end;

После этого скомпилируем компонент. Поставим со страницы Samples кнопку на форму и запустим проект на выполнение. Можно убедиться, что кнопка при нажатии пищит!

Теперь вновь перейдем в среду разработки и назначим обработчик события OnClick в инспекторе объектов. В обработчике события выведем текст в заголовок формы:

Procedure TForm1.ButtonBeep1Click(Sender:TObject); begin Caption:="Test"; end;

Запустим проект на выполнение и попробуем нажать на кнопку. Заголовок формы меняется, но кнопка пищать перестала! Ошибка заключается в том, что на одно событие кнопки OnClick мы попытались определить два обработчика: один внутри компонента BtClick, а другой назначили с помощью инспектора объектов. После отработки конструктора TButtonBeep у нас была ссылка на первый обработчик BtClick. Затем происходит загрузка ресурсов, обработчику события OnClick назначается метод ButtonBeep1Click. При этом ссылка на первый обработчик - BtClick - безвозвратно теряется.

Таким образом, при написании новых компонентов всегда следует учитывать возможность изменения свойств и обработчиков событий с помощью инспектора объектов. Если какое-либо свойство (событие) не должно меняться, его не следует отображать в инспекторе объектов. А если оно уже отображается, его следует скрыть (об этом мы поговорим позже). Программист имеет полное право изменить любые свойства в инспекторе объектов, и если после этого компонент перестает работать, в этом виноват разработчик компонента, но ни в коем случае не программист, его использующий.

Как же все-таки корректно решить данную задачу? Один из способов создания компонентов - переписывание уже имеющихся методов. При рассмотрении файла StdCtrls.pas, где реализованы исходные коды для компонента TButton, можно отметить в нем наличие динамического метода Click, который можно переписать. Поэтому вновь возвращаемся к исходному коду, созданному экспертом Delphi при создании компонента (убираем конструктор и метод BtClick). Затем в секции public определяем заголовок метода:

Procedure Click; override;

и приводим реализацию метода:

Procedure TButtonBeep.Click; begin inherited Click; beep; end;

Можно убедиться, что кнопка при нажатии издает писк. Кроме того, при назначении обработчика событий в инспекторе объектов этот обработчик выполняется и писк не исчезает! Компонент реализован корректно.

На данном примере полезно проанализировать возможные ошибки при написании кода:

  1. Забытая директива override при определении заголовка метода Click. Кнопка перестает пищать, следовательно, метод Click не вызывается.
  2. Забытый вызов метода-предка (inherited Click) в реализации процедуры Click. Кнопка продолжает пищать при нажатии, но код в назначенном в инспекторе объектов обработчике событий не выполняется. Следовательно, метод Click класса TButton вызывает событие OnClick.

Теперь поменяем пиктограмму компонента TButtonBeep на палитре. По умолчанию для нового компонента используется пиктограмма компонента-предка. Для этого вызовем редактор Image Editor командой Tools/Image Editor. В редакторе вызовем команду File/New/Component Resource File (*.dcr). После команды Resource/New/Bitmap появится диалог, в котором предлагается размер пиктограммы 32х32. Эти размеры по умолчанию следует изменить на 24х24 – такой размер обязаны иметь пиктограммы компонентов! После нажатия кнопки OK следует нарисовать какое-либо изображение при помощи стандартных инструментов, похожих на инструменты редактора Paint. Помните, что цвет левого нижнего пиксела является цветом маски – данный цвет будет «прозрачным».

После этого необходимо переопределить имя ресурса с пиктограммой, по умолчанию его имя ‑ Bitmap1. Новое имя ресурса обязано совпадать с именем класса – в нашем случае TButtonBeep.

Теперь необходимо сохранить файл с пиктограммой в том же самом каталоге, где находится модуль, содержащий процедуру Register для данного компонента, и с тем же самым именем, что и имя модуля. Только вот расширение у файла будет не *.pas, а *.dcr. Файл с пиктограммой компонента готов. Однако если мы посмотрим на палитру компонентов, то увидим, что там по-прежнему сохраняется старая пиктограмма. Если перезагрузить Delphi или даже операционную систему, старая пиктограмма по-прежнему останется на палитре. Для того чтобы поменять пиктограмму, необходима повторная регистрация компонента. Для этого необходимо:

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

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

Создание сложного компонента

Предположим, в приложении необходимо ввести список фамилий клиентов. В том же самом приложении потребуется и ввод списка телефонов. Ввод списка ‑ довольно распространенная операция, поэтому следует подумать о реализации его в виде компонента.

Для ввода нового элемента в список потребуется редактор – компонент TEdit. Далее пользователь должен иметь возможность просмотреть список – понадобится компонент TListBox. Кроме того, потребуются команды для занесения текущего значения из TEdit в список, редактирование выбранного элемента списка и его удаление. Проще всего эти команды реализовать с помощью кнопок. Для упрощения задачи поместим на форму одну кнопку, при нажатии которой будем добавлять содержимое компонента TEdit в список.

Итак, мы должны создать новый компонент, который включал бы в себя TEdit, TListBox и TButton. Как всегда, начнем его создание с команды Component/New Component. После этого появляется диалог, в котором следует определить класс-предок, имя класса, имя модуля. С именем класса и именем модуля никаких сложностей не возникает, а вот имя класса-предка неясно. У нас имеются три элемента управления. Общим классом-предком для них является TWinControl. Но если в качестве класса-предка выбрать его, нас ожидает очень длительная и утомительная реализация кода TButton, TEdit и TListBox. В таких случаях необходимо в качестве класса-предка выбирать компонент, способный быть «папой» по отношению к другим компонентам. Среди стандартных компонентов, распространяемых вместе с Delphi, таких три: TPanel, TGroupBox, TScrollBox. Выберем в качестве класса-предка панель, но не сам компонент TPanel, а класс TCustomPanel. Преимущества выбора TCustomPanel перед TPanel мы обсудим ниже.

Назовем новый класс именем TListAdd и нажмем кнопку Install. После выбора пакета компонент будет установлен в палитру, откуда его можно поместить на форму вновь созданного приложения. Это удобно, поскольку при компиляции проекта модуль компонента также будет компилироваться и при наличии ошибок компилятор выдаст сообщение.

Было бы удобно поместить наши элементы управления на какую-либо форму и затем создать из них компонент. В стандартной поставке Delphi такой эксперт отсутствует. Поэтому необходимо будет создавать компоненты самим и размещать их на панели. Создание элементов управления – TButton, TEdit и TListBox ‑ разумно выполнить в конструкторе TCustomPanel, для чего, очевидно, необходимо его переписать. Разместим пока элементы управления в квадрате 100х100. Координаты их также необходимо определять в конструкторе. При этом следует иметь в виду, что после отработки конструктора любого элемента управления он еще не имеет родителя, то есть не знает, относительно какого окна ему надо отсчитывать координаты левого верхнего угла. Попытка изменить координаты дочернего окна, у которого отсутствует родитель, немедленно приведет к генерации исключения. Поэтому первым оператором после вызова конструктора элемента управления будет назначение ему родителя, в качестве которого выберем TCustomPanel. Ее же сделаем и их владельцем, в этом случае не понадобится переписывать деструктор.

Итак, в секции uses добавляем модуль StdCtrls, где находятся описания классов TEdit, TButton и TListBox, а в секции private определяем три переменные:

Private FEdit:TEdit; FListBox:TListBox; FButton:TButton;

В секции public объявляем заголовок конструктора с обязательной директивой override:

Constructor Create(AOwner:TComponent); override;

Реализуем конструктор в секции реализации:

Constructor TListAdd.Create(AOwner:TComponent); begin inherited Create(AOwner); FButton:=TButton.Create(Self); FButton.Parent:=Self; FButton.Left:=5; FButton.Top:=5; FButton.Width:=40; FButton.Height:=25; FEdit:=TEdit.Create(Self); FEdit.Parent:=Self; FEdit.Left:=50; FEdit.Top:=5; FEdit.Width:=45; FEdit.Height:=25; FListBox:=TListBox.Create(Self); FListBox.Parent:=Self; FListBox.Left:=5; FListBox.Top:=35; FListBox.Width:=90; FListBox.Height:=60; end;

Еще раз следует подчеркнуть, что деструктор в данном случае переписывать не надо: панель является владельцем всех элементов управления, и при вызове ее деструктора деструкторы элементов управления будут вызваны автоматически.

После перекомпиляции компонента при помощи редактора пакетов изменения в компоненте уже можно увидеть визуально, на этапе разработки.

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

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

Width:=100; Height:=100;

Затем требуется улучшить поведение компонента при масштабировании. Для этого необходимо получить сообщение о том, что размеры изменились. При изменении размера какого-либо элемента управления система посылает ему сообщение WM_SIZE. Это сообщение необходимо перехватить. Для этого в секции private опишем заголовок перехватчика сообщения:

Procedure WMSize(var Message:Tmessage); message WM_SIZE;

и в секции реализации реализуем его обработчик:

Procedure TListAdd.WMSize(var Message:TMessage); begin inherited; if Width<100 then Width:=100; if Height<100 then Height:=100; FEdit.Width:=Width-55; FListBox.Width:=Width-10; FListBox.Height:=Height-40; end;

Первый оператор – вызов обработчика WM_SIZE по умолчанию (inherited). После его вызова в свойствах Width и Height будут находиться новая ширина и высота панели. После этого определяются минимальные размеры компонента, в данном случае ‑ 100х100. Если размер по горизонтали или вертикали меньше минимального, то ему присваивается минимальное значение. Затем происходит масштабирование элементов управления так, чтобы они заполняли всю панель с небольшими отступами. Скомпилировав компонент через редактор пакетов, можно уже на этапе разработки отметить корректное поведение элементов управления на панели при масштабировании, а также то, что размер компонента нельзя сделать менее чем 100х100.

Теперь полезно будет запустить весь проект на выполнение, попробовать вводить данные в однострочный редактор текста и нажимать кнопку. При этом ничего в список не добавляется. И не удивительно, что нигде в нашем компоненте не указано, что надо делать при нажатии кнопки. Для того чтобы сделать обработчик события, связанного с нажатием кнопки, можно поступить, как при написании компонента TbuttonBeep, то есть определить новый класс ‑ потомок TButton и переписать метод Click. Однако определение нового класса требует системных ресурсов (размножаются виртуальные методы). Если мы отметим компонент на форме и посмотрим на инспектор объектов, то обнаружим, что компонент TlistAdd экспонирует немного свойств и ни одного события, в том числе ни одного обработчика события кнопки OnClick. Поэтому то, что в прошлой главе мы отвергли как неправильный метод,– переопределение обработчика кнопки OnClick в данном случае применимо, поскольку программист не может в инспекторе объектов назначить новый обработчик. Итак, в секции private описываем заголовок нового метода:

Procedure BtClick(Sender:TObject);

В реализации конструктора TListAdd присваиваем этот обработчик обработчику событий FButton.OnClick:

FButton.OnClick:=BtClick;

И наконец, реализуем метод BtClick:

Procedure TListAdd.BtClick(Sender:TObject); begin if length(FEdit.Text)>0 then begin FListBox.Items.Add(FEdit.Text); FEdit.Text:=""; FEdit.SetFocus; end; end;

Сначала проверим, не пуст ли однострочный редактор: мы не будем добавлять в список пустые строки. Затем переносим содержимое редактора в список (FListBox.Items.Add(FEdit.Text);) и подготавливаем редактор к вводу следующего значения – а именно, очищаем его от текста (который уже перенесен в список) и переносим на него фокус ввода. Теперь после компиляции и запуска приложения можно убедиться, что оно работает корректно – при нажатии кнопки содержимое редактора переносится в список.

Добавление свойств и методов

Если рядом с компонентом TListAdd поместить компонент TPanel и сравнить показываемое в инспекторе объектов, то можно отметить, что для панели экспонируется достаточно большое количество свойств и событий, а для TListAdd – только несколько свойств. Между тем класс TCustomPanel является предком обоих компонентов. Для того чтобы понять причину, откроем модуль ExtCtrls.pas и рассмотрим разницу между классами TCustomPanel и TPanel. Можно отметить, что все методы и переменные, которые обеспечивают функциональность панели, определены на уровне класса TCustomPanel. В нем же определены и свойства, которые затем отображаются в инспекторе объектов для TPanel, только эти свойства определены в секции Protected. Реализация же класса TPanel чрезвычайно проста: в качестве предка определяется TCustomPanel, и свойства этого класса редекларируются, но уже в секции published. Становится понятно, что необходимо сделать в классе TListAdd для появления в инспекторе объектов свойств и методов класса TcustomPanel, а именно редекларировать свойства. В секции published класса TListAdd запишем:

Property Align; property OnMouseDown;

При редекларации свойства не требуется указывать его тип и ссылаться на переменные или методы чтения или записи свойства. После компиляции компонента через редактор пакетов в инспекторе объектов можно наблюдать появление свойства Align и события OnMouseDown. Таким образом, для потомков TCustom…-классов программист имеет возможность выбирать, какие свойства и события следует отображать в инспекторе объектов, а какие нет. Именно по этой причине TCustom…-классы рекомендуется использовать в качестве предков для создания компонентов.

Теперь рассмотрим, как можно ввести новое свойство (то, что мы делали выше –редекларация уже имеющихся свойств). В качестве подходящего свойства для отображения в инспекторе объектов можно использовать текст на кнопке: пусть программист, пользующийся компонентом TListAdd, самостоятельно меняет текст на этапе разработки. Попытка ввести новое свойство (назовем его BtCaption) с помощью объявления:

Property BtCaption:string read FButton.Caption write FButton.Caption;

приводит к ошибке при попытке компиляции компонента. Поэтому определим заголовки двух методов в секции private:

Function GetBtCaption:string; procedure SetBtCaption(const Value:string);

В секции published объявим свойство BtCaption:

Property BtCaption:string read GetBtCaption write SetBtCaption;

И наконец, реализуем два объявленных метода в секции реализации:

Function TListAdd.GetBtCaption:string; begin Result:=FButton.Caption; end; procedure TListAdd.SetBtCaption(const Value:string); begin FButton.Caption:=Value; end;

После компиляции компонента с помощью редактора пакетов в инспекторе объектов появляется новое свойство. Изменение значения этого свойства отражается прямо на этапе разработки.

Теперь определим новое событие. В данной задаче было бы разумным создать событие, позволяющее программисту, использующему данный компонент, анализировать текст перед занесением содержимого редактора в список и разрешить или запретить добавление текста в список. Следовательно, этот метод обязан в качестве параметра содержать текущее значение текста в редакторе и зависеть от логической переменной, которой программист может присвоить значение True или False. Кроме того, любой обработчик события в компоненте обязан зависеть от параметра Sender, в котором вызывающий его компонент передает ссылку на самого себя. Это необходимо делать потому, что в среде разработки Delphi один и тот же обработчик события может вызываться из нескольких различных компонентов и программист должен иметь возможность проанализировать, какой именно компонент вызвал обработчик. Итак, после слова type в секции interface перед определением TListAdd определяем новый тип метода:

Type TFilterEvent=procedure(Sender:TObject; const EditText:string; var CanAdd:boolean) of object;

FOnFilter:TFilterEvent;

И в секции published определяем свойство данного типа:

Property OnFilter:TFilterEvent read FOnFilter write FOnFilter;

При определении нового свойства ссылаемся на переменную FOnFilter, а не на методы – они здесь не требуются. Теперь, если скомпилировать компонент с помощью редактора пакетов, можно обнаружить появление в инспекторе объектов события OnFilter. Однако если мы назначим ему обработчик и запустим проект на исполнение, то он может не вызваться. Это происходит потому, что мы нигде его не вызвали в нашем компоненте. Подходящее место для вызова события OnFilter – обработчик события OnClick для FButton, который уже реализован. Поэтому мы изменим код реализации ранее определенного метода BtClick:

Procedure TListAdd.BtClick(Sender:TObject); var CanAdd:boolean; begin if length(FEdit.Text)>0 then begin CanAdd:=True; if Assigned(FOnFilter) then FOnFilter(Self,FEdit.Text,CanAdd); if CanAdd then begin FListBox.Items.Add(FEdit.Text); FEdit.Text:=""; FEdit.SetFocus; end else beep; end; end;

Итак, в приведенном выше фрагменте кода определяется логическая переменная CanAdd. При написании кода следует учитывать, что программист может не сделать обработчик события OnFilter. Поэтому устанавливаем значение переменной CanAdd по умолчанию равным True – все строки добавлять в список. Далее, перед вызовом FonFilter, следует проверить, а сделал ли программист обработчик события. Это достигается вызовом метода Assigned, который возвращает логическое значение. Для указателя вызов метода Assigned эквивалентен проверке P<>nil. Для метода объекта мы не можем использовать проверку FOnFilter<>nil, так как метод объекта характеризуется двумя адресами и такая проверка не будет разрешена компилятором. Но вызов метода Assigned прекрасно проверяет, был ли сделан обработчик события. Вышеприведенный код – абсолютно стандартный способ вызова обработчика событий из компонента.

Осталось протестировать обработчик события. Поместим два компонента TListAdd на форму, для одного разрешим добавление только целых чисел, а для другого – только слов, начинающихся с прописных английских букв. Соответственно код для обработчиков событий OnFilter будет выглядеть следующим образом:

Procedure TForm1.ListAdd1Filter(Sender: TObject; const EditText: String; var CanAdd: Boolean); var I,N:integer; begin Val(EditText,N,I); CanAdd:=I=0; end; procedure TForm1.ListAdd2Filter(Sender: TObject; const EditText: String; var CanAdd: Boolean); begin CanAdd:=False; if length(EditText)>0 then CanAdd:=(EditText>="A") and (EditText<="Z"); end;

Код прост для понимания, единственным его нюансом является проверка того, что текст представляет собой не пустую строку, перед проверкой первой буквы текста в обработчике события ListAdd2Filter. Проведение такой проверки обязательно: строки в Object Pascal ‑ это объекты, и пустой строке соответствует nil-указатель. При попытке проверить первую букву пустой строки приложение попытается дереференсировать nil, что приведет к возникновению исключения. В данном случае это не страшно: перед вызовом обработчика событий FOnFilter из компонента TListAdd проверяется строка на ненулевую длину. Однако для компонентов, исходный текст которых вам недоступен, такая проверка является обязательной!

Скрытие свойств в инспекторе объектов

Предположим, вы делаете компонент для доступа к данным, например, потомок класса TTable. Допустим, в этом компоненте анализируется список таблиц, имеющихся в базе данных, и по каким-либо признакам (например, наличие поля определенного типа и с определенным именем) выбирается одна для работы. Для нормальной работы компонента имя этой таблицы должно заноситься в свойство TableName. Но это свойство отображается в инспекторе объектов! Программист, использующий этот компонент, может изменить его значение на этапе разработки, что, предположим, сделает компонент неработоспособным. И он будет прав! Если какие-то из свойств или событий нельзя изменять, они должны быть скрыты.

Мы продолжим работу над компонентом TListAdd и в качестве модельной задачи уберем из инспектора объектов свойство Cursor. Это свойство определено в секции published в классе TСontrol и отображается в инспекторе объектов для TListAdd с самого начала разработки компонента. Исходя из этого можно попытаться переопределить данное свойство в секции protected. Компилятор разрешит такое переопределение, но к желаемому результату это не приведет: свойство Cursor как было, так и останется в инспекторе объектов… Любое свойство, будучи однажды определенным в секции published, будет всегда отображаться в инспекторе объектов для всех потомков данного класса.

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

  1. При объявлении нового свойства с именем, совпадающем с именем уже имеющегося свойства, ранее определенное свойство «затеняется».
  2. Свойства, которые имеют доступ только для чтения или только для записи, не отображаются в инспекторе объектов, даже если они объявлены в секции published.

Перед началом работы по скрытию свойства Cursor полезно удалить компоненты TListAdd с формы, иначе может произойти исключение при чтении ресурса формы. Итак, в секции private объявляем переменную FDummy:integer (имя и тип переменной могут быть любыми) и в секции published определяем новое свойство:

Property Cursor:integer read FDummy;

Новое свойство обязано называться Cursor, тип его обязан совпадать с типом переменной, определенной выше, свойство должно быть только на чтение или только на запись. После компиляции компонента с помощью редактора пакетов следует вновь поместить компонент TListAdd на форму. Можно обнаружить, что свойство Cursor уже не отображается в инспекторе объектов.

Теперь немного усложним задачу. Предположим, необходимо, чтобы курсор был показан не в виде стрелки, а в виде песочных часов (crHourGlass). Для того чтобы изменить значение свойств по умолчанию, новое значение необходимо присвоить переменной в конструкторе. При попытке в конструкторе присвоить новое значение Cursor

Cursor:=crHourGlass;

компилятор Delphi выдаст диагностическое сообщение о том, что нельзя назначить новое значение переменной, предназначенной только для чтения. Если сделать новое свойство «только для записи», то компилятор выдаст уже другое диагностическое сообщение – о несопоставимых типах данных. Если же объявить переменную FDummy:TCursor и сделать ее доступной только для записи, то компилятор разрешит данное присвоение, но при этом вид курсора не изменится: он по-прежнему будет стрелкой.

Тривиальное решение данной проблемы – объявить класс-потомок TCustomPanel, в конструкторе которого нужно присвоить новое значение переменной Cursor, а от него уже производить наш компонент TListAdd. У такого решения имеется два недостатка:

  1. Оно ресурсоемко – размножаются виртуальные методы.
  2. Свойство мы прятали в инспекторе объектов от программиста, который будет использовать данный компонент. Мы же хотим работать с данным свойством.

Поэтому решение данной задачи выглядит следующим образом: в конструкторе TListAdd объявляем оператор:

Inherited Cursor:=crHourGlass;

и все! Этого достаточно для изменения курсора.

Ранее мы пользовались служебным словом inherited только для вызова метода предка. Данная конструкция позволяет глубже понять значение inherited как обращение к классу-предку. Можно обращаться и к свойствам, и к методам. При обращении к свойству его можно как читать, так и присваивать ему новое значение; при этом служебное слово inherited стоит слева от знака присваивания. Аналогично можно вызывать скрытые методы предка. Обращения по иерархии выше, чем класс-предок, запрещено - конструкция

Inherited inherited Cursor:=crHourGlass;

не будет скомпилирована.

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

Использование Hook-процедур для создания компонентов

Ранее уже упоминалось, что каждый потомок TWinControl имеет процедуру, которая принимает и обрабатывает сообщения. Если имеется ссылка на дескриптор окна (HWND), то можно определить адрес этой процедуры и, что более важно, подменить этот адрес и таким образом обрабатывать получаемые сообщения своим способом. Как правило, никто не пишет полностью обработчики всех сообщений; чаще вызывается старый метод по умолчанию. При этом новая процедура используется как фильтр: при поступлении какого-либо события выполняется код. Фактически это «шпион» в TwinControl: нас уведомляют о приходе какого-либо сообщения и можно выполнить какой-либо код. При правильной реализации Hook-процедуры TWinControl продолжает работать как обычно, не подозревая, что своими сообщениями он делится с кем-то еще.

Hook-процедура определяется следующим образом:

Procedure(var Message:TMessage) of object;

Она зависит от переменной типа TMessage, в которой содержится вся информация о сообщении. Но определить эту процедуру – недостаточно. Она должна копироваться для каждого TWinControl, к которому будет присоединена. Это достигается вызовом WinAPI-метода MakeObjectInstance. В качестве параметра этот метод принимает метод объекта, делает его копию в памяти и возвращает адрес нового метода. Понятно, что при этом резервируются системные ресурсы, которые необходимо вернуть системе. Это достигается вызовом метода FreeObjectInstance.

Еще одно важное условие: перед разрушением TWinControl должна быть восстановлена связь со старой процедурой обработки сообщений, иначе ресурсы не будут возвращены системе. Значит, придется запоминать указатель на старую процедуру, который можно узнать вызовом метода Win API GetWindowLong с параметром GWL_WNDPROC. Этот указатель будет использоваться также для вызова обработчиков событий TWinControl по умолчанию. Обратный метод - SetWindowLong - используется для установки Hook-процедуры.

Итак, сформулируем задачу для следующего упражнения. Предположим, мы хотим создать компонент, который будет заставлять пищать при нажатии кнопки мыши другие компоненты – потомки TWinControl. Понятно, что данный компонент не следует показывать во время выполнения приложения, поэтому в качестве его класса-предка выберем TComponent. Имя класса определим как TBeepWnd. В секции private определим три переменные:

FOldProc,FNewProc:pointer; FControl:TWinControl;

Из названий ясно, что мы будем запоминать ссылку на старую процедуру в переменной FOldProc, ссылка на новую процедуру (после выполнения метода MakeObjectInstance) будет храниться в переменной FNewProc. И в переменной FControl будем сохранять ссылку на элемент управления, на который в данный момент «повешена» Hook-процедура. Определим три метода в этой же секции:

Procedure HookProc(var Message:TMessage); procedure HookWindow(W:TWinControl); procedure UnhookWindow;

и в секции implementation реализуем их:

Procedure TBeepWnd.HookProc(var Message:TMessage); begin case Message.Msg of WM_LBUTTONDOWN:begin {Our task} Beep; Message.Result:=CallWindowProc(FOldProc, FControl.Handle, Message.Msg, Message.WParam, Message.lParam); end; WM_DESTROY:begin {When window is about destroying, remove hook} Message.Result:=CallWindowProc(FOldProc, FControl.Handle, Message.Msg, Message.WParam, Message.lParam); UnhookWindow; end; {Call default handler} else Message.Result:=CallWindowProc(FOldProc, FControl.Handle, Message.Msg, Message.WParam, Message.lParam); end; end;

В самой Hook-процедуре перехватывается сообщение, на которое происходит реакция – WM_LBUTTONDOWN. Кроме того, любая Hook-процедура обязана обрабатывать сообщение WM_DESTROY. Это последнее сообщение, которое передается окну перед тем, как оно будет разрушено. Наша реакция – восстановить предыдущий метод вызовом описанного ниже метода UnhookWindow. И наконец, везде вызываются обработчики сообщений по умолчанию посредством метода CallWindowProc. Забыть обработчик события по умолчанию – то же самое, что забыть inherited в обработчике события, в 80% случаев это приведет к некорректному поведению приложения. Ни в коем случае нельзя забывать присваивать результат вызова метода CallWindowProc полю Result переменной Message! Код в этом случае работать не будет!

Procedure TBeepWnd.HookWindow(W:TWinControl); begin if csDesigning in ComponentState then begin {Checking if component at design or run-time} FControl:=W; Exit; end; if FControl<>nil then UnhookWindow; {Remove hook if it was previously installed} if W<>nil then begin FOldProc:=pointer(GetWindowLong(W.Handle,GWL_WNDPROC)); {Determines address of old procedure} FNewProc:=MakeObjectInstance(HookProc); {Make copy in memory} SetWindowLong(W.Handle,GWL_WNDPROC,integer(FNewProc)); {Set new procedure} end; FControl:=W; {Store reference at control} end;

Этот метод используется для установки новой процедуры обработки сообщений. Сначала проверяется, на каком из этапов находится данный компонент: на этапе разработки или на этапе выполнения. Если компонент находится на этапе разработки, то есть выставлен флаг csDesigning в свойстве ComponentState, то сохраняется просто ссылка на компонент без установки Hook-процедуры. Это сделано для того, чтобы избежать установки Hook-процедуры на среду разработки Delphi. Если ранее эта процедура была установлена на другом элементе управления, она снимается посредством вызова метода UnhookWindow. После этого запоминается адрес старой процедуры (GetWindowLong), делается копия в памяти новой процедуры (MakeObjectInstance) и выставляется адрес новой процедуры (SetWindowLong). Используется приведение типов от integer к pointer, и наоборот – вызываемые методы требуют (или возвращают) переменные не совсем подходящих типов. И наконец, ссылка на элемент управления запоминается в переменной FControl, которую мы определили в секции private.

Procedure TBeepWnd.UnhookWindow; begin if (FControl=nil) or (FOldProc=nil) or (FNewProc=nil) then Exit; {No hook was installed} SetWindowLong(FControl.Handle,GWL_WNDPROC,integer(FOldProc)); {Set old window procedure} FreeObjectInstance(FNewProc); {Free resources} FControl:=nil; {Initiate variables} FOldProc:=nil; FNewProc:=nil; end;

Данный метод восстанавливает старый обработчик события. Он вызывается из метода HookProc и должен еще вызываться из деструктора компонента – снимать Hook необходимо как при разрушении окна, так и при разрушении данного компонента. Метод SetWindowLong c адресом старого метода восстанавливает старый обработчик сообщений. После этого следует вернуть ресурсы системе вызовом метода FreeObjectInstance.

Итак, базовые методы для работы с Hook-процедурой определены. Теперь необходимо переписать деструктор, чтобы Hook-процедура снималась при разрушении данного компонента:

Destructor TBeepWnd.Destroy; begin UnhookWindow; inherited Destroy; end;

И наконец, в секции published определим свойство, которое будет отображаться в инспекторе объектов:

property Control:TWinControl read FControl write HookWindow;

Для установки нового компонента ссылаемся на ранее определенный метод, который во время выполнения приложения немедленно «повесит» Hook-процедуру на компонент, который станет пищать при нажатии кнопки. Напомним, что вместо оператора Beep можно написать любой исполняемый код.

Тестируется компонент достаточно просто: ставится на форму, на которую ставятся и несколько компонентов-потомков TWinControl. После выбора на фоне компонента TBeepWnd при щелчке мышью в инспекторе объектов на поле Control разворачивается список, в котором присутствуют все определенные на форме TWinControl. Следует выбрать один из них и запустить приложение. При нажатии левой кнопки мыши на выбранном компоненте он издает писк.

Редакторы свойств и редакторы компонентов

Все, о чем рассказывалось в предыдущих разделах, относится к созданию кода приложения, которое будет распространяться для пользователей. Однако среда разработки Delphi позволяет модифицировать саму себя. Для этого не требуется знаний специального языка, поскольку все методы для изменения среды разработки пишутся на Delphi. Здесь эти методы, а именно редакторы свойств и редакторы компонентов, рассмотрены частично ‑ в плане создания инструментов для работы с компонентами. При чтении материалов данного раздела следует четко понимать, что конечный пользователь, работающий с вашим приложением, никогда не увидит ни редактора свойств, ни редактора компонентов – они создаются для программистов и работают только в среде разработки Delphi.

Редакторы свойств

Во время разработки приложения свойства отображаются в инспекторе объектов. Обратите внимание: свойства в инспекторе объектов редактируются по-разному. Некоторым свойствам (Width, Caption) можно определить только новое текстовое значение. Свойство типа Cursor предоставляет раскрывающийся список, щелкнув по которому можно выбрать значение. Свойство типа TFont имеет знак «+» слева; при щелчке по нему оно разворачивается, давая возможность модифицировать отдельные поля. Кроме того, справа имеется кнопка с тремя точками (elliptic button), при щелчке на которой появляется диалог редактора свойств.

Каждое из вышеперечисленных свойств имеет свой редактор, и большим преимуществом среды разработки Delphi является возможность создать свои редакторы свойств. Новые редакторы свойств довольно часто встречаются среди распространяемых компонентов. Но к ним надо относиться осторожно: первоначально выполнить тесты на компьютере, где при необходимости можно повторно инсталлировать Delphi. Как правило, они создаются квалифицированными программистами и претензий к коду не бывает, но часто забывают включить в распространяемый редактор свойств какую-либо DLL. После инсталляции такого редактора мы получаем ряд свойств, которые невозможно редактировать, – старый редактор перекрыт, а новый не работает…

Перед созданием нового редактора свойств имеет смысл подумать, стоит ли это делать, – среди стандартных редакторов, вероятно, можно найти подходящий. Если же придется делать редактор свойств, необходимо соблюдать правило: следует избегать создания редакторов для стандартных типов данных (integer, string и др.). Другие программисты привыкли к стандартным редакторам, и ваш может им не понравиться. Следовательно, придется проявить скромность и регистрировать редактор для своего класса, а не для класса TComponent. Если ваш редактор свойств понравится программистам, большинство из них смогут сами изменить регистрацию так, чтобы редактор работал для всех компонентов. Вопрос регистрации редактора мы обсудим ниже.

Итак, поставим модельную задачу, для реализации которой необходимо будет реализовать редактор свойств. Предположим, какой-либо компонент имеет свойство ‑ день недели. В принципе, для ввода дня недели можно воспользоваться стандартным редактором с выпадающим списком. Однако мы хотим, чтобы программист на этапе разработки мог вводить день недели, задавая либо его порядковый номер (1 ‑ понедельник, 2 ‑ вторник и т.д.), либо текст на национальном или английском языке. При вводе текста допускается смешение прописные и строчные букв.

Прежде всего необходимо создать компонент, в котором будет храниться день недели. Создадим новый компонент вызовом команды Component/New component. В качестве класса-предка выберем TComponent и дадим новому классу имя TDayStore. После этого установим компонент в палитру. Теперь надо решить, в каком виде хранить день недели. Ясно, что для однозначной идентификации и экономии ресурсов его следует хранить в виде целого числа с допустимыми диапазонами 1‑7. Однако, если мы собрались создавать редактор свойств, следует вспомнить правило о несоздании новых редакторов для уже имеющихся типов. Поэтому определим новый тип – TDayWeek, причем все операции с ним будем производить как с целыми числами. Определим переменную FDay в секции private компонента. Поскольку эта переменная будет инициализироваться значением 0 при отработке конструктора по умолчанию, а это число находится за пределами допустимых значений, необходимо переписать конструктор. В заключение определим свойство DayWeek в секции published для отображения его в инспекторе объектов. Окончательный вариант компонента выглядит следующим образом:

Type TDayWeek=type integer; TDayStore = class(TComponent) private { Private declarations } FDay:TDayWeek; protected { Protected declarations } public { Public declarations } constructor Create(AOwner:TComponent); override; published { Published declarations } property DayWeek:TDayWeek read FDay write FDay; end; … implementation constructor TDayStore.Create(AOwner:TComponent); begin inherited Create(Aowner); FDay:=1; end;

Следует обратить внимание на редкую конструкцию определения нового типа

TDayWeek=type integer;

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

Теперь создадим редактор свойства TDayWeek. Для этого к имеющемуся проекту добавим новую форму, запомним ее под каким-либо подходящим именем (DayPropE.pas) и исключим из проекта. После этого откроем форму как отдельный файл и будем реализовывать в ней редактор свойств. На первом этапе форма нам не понадобится, но позднее мы реализуем на ней диалог.

Модуль для создания редакторов свойств называется DsgnIntf.pas (Design Interface), в нем определены базовый класс TPropertyEditor и классы-потомки, предназначенные для редакции стандартных свойств – TIntegerProperty, TFloatProperty, TStringProperty и др. Механизм работы редакторов свойств заключается в следующем:

  1. Он регистрируется в среде разработки Delphi вызовом метода RegisterPropertyEditor. В качестве параметров этот метод принимает следующие значения:

    a) информация о типе свойств, для редакции которых предназначен данный редактор. Из-за наличия этой информации нам пришлось определять новый тип TDayWeek;

    b) информация о компоненте, в котором применим данный редактор. Редактор будет вызываться не только для указанного компонента, но и для всех его потомков. Если установить это значение TComponent, редактор будет вызываться для любого компонента;

    c) имя свойства, для которого используется данный редактор. Если имя – пустая строка, используются два вышеупомянутых фильтра;

  2. Вызывается метод GetValue, когда необходимо считать текущее значение свойства из компонента. Этот метод для любого свойства возвращает строку, которая помещается в инспекторе объектов.
  3. Вызывается метод SetValue, когда программист ввел новое значение свойства в инспекторе объектов. В качестве параметра передается новая строка. В методе она должна быть проанализирована и приведена к типу редактируемого свойства.

Методы GetValue и SetValue являются виртуальными, при их переписывании создаются новые редакторы свойств. Итак, теперь можно начать создание нового редактора свойств.

Сошлемся в секции uses модуля DayPropE.pas на модуль DsgnIntf и определим в секции Interface новый класс:

Type TDWPropED=class(TPropertyEditor) public function GetValue:string; override; procedure SetValue(const Value:string); override; end;

В секции реализации следует реализовать оба этих метода. При этом нам дополнительно потребуется список названий дней недели – в начальной постановке задачи требуется, чтобы программист имел возможность вводить день недели:

Const DayWeek:array of string = ("Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"); DayWeekEn:array of string = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday");


Разработка собственных компонентов

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

Перед созданием своего компонента важно правильно выбрать для него предка. Кто же может быть предком для вашего компонента? Как правило, используются в виде предков TComponent, TControl, TWinControl, TGraphicControl, TCustomXXXXXX, а также все компоненты палитры компонентов. Возьмем для примера компонент TOpenDialog, который находится на странице Dialogs палитры компонентов. Он хорошо справляется со своей задачей, но у него есть одно маленькое неудобство. Каждый раз, когда его используешь необходимо каждый раз изменять значение свойства Options. И причем это, как правило, одни и те же действия.


нажатие на этой строке Ctrl + Shift + C создает шаблон для этого метода, внутри которого мы вставляем такие строки:


Установка созданного компонента Component/Install Component...

  • Install Into New Package
  • Package file name: C:\Program Files\Borland\Delphi4\Lib\OurTest.dpk
  • Package description: Our tested package

Вам не нравится, что у нашего компонента иконка такая же как у стандартного? Тогда создадим для него свою собственную. Для этого нам необходимо вызвать Tools/Image Editor. Создаем новый *.dcr файл. Вставляем в него рисунок Resource/New/Bitmap. Устанавливаем размер картинки 24x24 точек. А дальше - ваше творчество... Обратите внимание: цвет точек, совпадающий с цветом точки в левом нижнем углу рисунка, будет считаться ПРОЗРАЧНЫМ!

После того как вы создали свой рисунок, переименуйте его из Bitmap1 в TOurOpenDialog и сохраните файл с именем OurOpenDialog.dcr. Удалите компонент из пакета и установите его снова (только в этом случае добавится и ссылка на *.dcr файл).

Compile, Install и удачи!

Все компоненты Delphi являются частью иерархии, которая называется Visual Component Library (VCL). Общим предком всех компонентов является класс TComponent (рис. 9.1.1), в котором собран минимальный набор общих для всех компонентов Delphi свойств.

Свойство ComponentState содержит набор значений, указывающих на текущее состояние компонента. Приведем некоторые значения свойства:

Класс TComponent вводит концепцию принадлежности. Каждый компонент имеет свойство Owner (владелец), ссылающееся на другой компонент как на своего владельца. В свою очередь, компоненту могут принадлежать другие компоненты, ссылки на которые хранятся в свойстве Components. Конструктор ком­понента принимает один параметр, который используется для задания владельца компонента. Если передаваемый владелец су­ществует, то новый компонент добавляется к списку Components владельца. Свойство Components обеспечивает автоматическое разрушение компонентов, принадлежащих владельцу. Свойст­во ComponentCount показывает количество принадлежащих компонентов, a Componentlndex - номер компонента в массиве Components.

В классе TComponent определено большое количество мето­дов. Наибольший интерес представляет метод Notification. Он вызывается всегда, когда компонент вставляется или удаляется из списка Components владельца. Владелец посылает уведомле­ние каждому члену списка Components. Этот метод переопределя­ется в порождаемых классах для того, чтобы обеспечить действи­тельность ссылок компонента на другие компоненты. Например, при удалении компонента Tablel с формы свойство DataSet компонента DataSourcel, равное Tablel, устанавливается в Nil.

Процесс разработки компонента включает пять этапов:

выбор класса-предка;

создание модуля компонента;

добавление в новый компонент свойств, методов и событий;

тестирование;

регистрацию компонента в среде Delphi;

9.1. Выбор класса-предка

На рис. 9.1.1 изображены базовые классы, формирующие структуру VCL. В самом верху расположен TObject, который является предком для всех классов в Object Pascal. От него про­исходит TPersistent, обеспечивающий методы, необходимые для создания потоковых объектов. Потоковый объект - объект, ко­торый может запоминаться в потоке. Поток представляет собой объект, способный хранить двоичные данные (файлы). Поскольку Delphi реализует файлы форм, используя потоки, то TComponent порождается от TPersistent, предоставляя всем компонентам способность сохраняться в файле формы.


Класс TComponent представляет собой вершину иерархии компонентов и является первым из четырех базовых классов, используемых для создания новых компонентов. Прямые по­томки TComponent - невизуальные компоненты.

9.1.1. Класс TControl

Вершину иерархии визуальных компонентов представляет класс TControl.

Класс TControl вводит понятие родительских элементов управ­ления (parent control). Свойство Parent является окном, кото­рое содержит элемент управления. Например, если компонент Panel 1 содержит Button 1, то свойство Parent компонента Button 1 равно Panel 1.

Свойство ControlStyle определяет различные стили, приме­нимые только к визуальным компонентам, например:

В классе TControl определено большинство свойств, использу­емых визуальными компонентами: свойства позиционирования (Align, Left, Top, Height, Width), свойства клиентской области (ClientHeight, ClientWidth), свойства внешнего вида (Color, Enabled, Font, ShowHint, Visible), строковые свойства (Caption, Name, Text, Hint), свойства мыши (Cursor, DragCursor, DragKind, DragMode).

Кроме того, класс TControl реализует методы диспетчеризации событий.

Все визуальные компоненты подразделяют на графические элементы управления и оконные элементы управления. Каж­дый тип представляет свою иерархию классов, происходящую соответственно от TGraphicControl и TWinControl. Главная раз­ница между этими типами компонент состоит в том, что графи­ческие компоненты не поддерживают идентификатор окна, и, соответственно, не могут принять фокус ввода.

Оконные компоненты далее разбиваются на две категории. Прямые потомки TWinControl являются оболочками вокруг су­ществующих элементов управления, реализованных в Windows (например, TEdit, TButton, и др.) и, следовательно, знают, как себя рисовать.

Для компонентов, которые требуют идентификатора окна, но не инкапсулируют базовых элементов Windows, которые бы обеспечивали возможность перерисовывать себя, имеется класс TCustomControl.

9.1.2. Класс TGraphicControl

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

По умолчанию объекты TGraphicControl не имеют собствен­ного визуального отображения, но для наследников обеспечи­ваются виртуальный метод Paint (вызывается всегда, когда элемент управления должен быть нарисован) и свойство Canvas (используется как «поверхность» для рисования).

9.1.3. Класс TWinControl

Класс TWinControl используется как базовый для создания компонентов, инкапсулирующих соответствующие оконные эле­менты управления Windows, которые сами себя рисуют.

Класс TWinControl обеспечивает свойство Handle, являюще­еся ссылкой на идентификатор окна базового элемента управ­ления. Кроме этого свойства класс реализует свойства, методы и события, поддерживающие клавиатурные события и измене­ния фокуса:

Создание любого потомка этого класса начинается с вызова ме­тода CreateWnd, который вначале вызывает CreateParams для инициализации записи параметров создания окна, а затем вызыва­ет CreateWindowHandle для создания реального идентификатора окна, использующего запись параметров. Затем CreateWnd настра­ивает размеры окна и устанавливает шрифт элемента управления.

9.1.4. Класс TCustomControl

Класс TCustomControl представляет собой комбинацию клас­сов TWinControl и TGraphicControl. Являясь прямым потомком класса TWinControl, TCustomControl наследует способность управления идентификатором окна и всеми сопутствующими возможностями. Кроме этого, как и класс TGraphicControl, класс TCustomControl обеспечивает потомков виртуальным ме­тодом Paint, ассоциированным со свойством Canvas.

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

создание Windows-элемента управления (TWinControl);

создание графического элемента управления (TGraphic-Control);

создание нового элемента управления (TCustomControl); О создание невизуального компонента (TComponent).

9.2. Создание модуля компонента и тестового приложения

Определившись с выбором компонента, можно приступить к созданию модуля компонента. Для этого необходимо выпол­нить следующие шаги.

Выполните команду File/ New.../ Component или Component/ New Component.

В диалоговом окне New Component (рис. 9.2.1.) установите основные параметры создания компонента: Ancestor type (имя класса-предка), Class Name (имя класса компонента), Palette Page (вкладка палитры, на которой должен отображаться ком­понент) и Unit file name (имя модуля компонента).

После щелчка на кнопке ОК будет сгенерирован каркас но­вого класса.

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

Упражнение 9.2.1. Разработайте новый компонент, который объединяет компоненты TEdit и TLabel. Компонент Label рас­полагается выше поля редактирования (TEdit). При перемеще­нии поля редактирования TLabel следует за ним. При удалении поля редактирования TLabel также удаляется.

В качестве предка класса нового компонента используем TEdit.

Выполните команду Component/ New component. Установите следующие значения параметров окна: Ancestor type TEdit

Class Name TLabelEdit

Palette Page Test

Unit file name ...\LabelEdit\LabelEdit.pas

Щелкните на кнопке ОК, автоматически будет сгенерирован следующий код:

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type

TLabelEdit = class(TEdit)

{ Private declarations }

{ Protected declarations }

{ Public declarations }

{ Published declarations }

procedure Register;

procedure Register;

RegisterComponents("Test", );

В модуле описан каркас нового класса и написана процедура регистрации компонента (Register), которая помещает его на страницу Test. Сохраните файл модуля компонента.

Разработка тестового приложения

Создайте новый проект. Сохраните его файлы в папке...\LabelEdit: файл модуля - под именем Main.pas, файл про­екта - Test Application, dpr.

Добавьте имя модуля разрабатываемого компонента в раздел Uses формы тестового приложения:

uses ..., LabelEdit;

В общедоступный раздел класса TForml добавьте поле

В обработчике события OnCreate формы динамически со­здайте новый компонент:

procedure TForml.FormCreate(Sender: TObject);

le:=TLabelEdit.Create(Self);

Сохраните файлы проекта.

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

9.3. Добавление свойств, методов и событий

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

Добавление свойства происходит в три этапа.

1. Создание внутреннего поля класса для хранения значения свойства.

2. Описание и разработка методов доступа к значению свойства.

3. Описание свойства.

В классе TControl свойства Caption/Text, Parent и Hint опре­деляются так:

TControl = class (TComponent)

FParent: TWinControl; {внутреннее поле свойства Parent}

FText: PChar; {внутреннее поле свойства Text/Caption}

FHint: string; {внутреннее поле свойства Hint}

function GetText: Tcaption; {метод чтения свойства Text/Caption}

function IsCaptionStored: Boolean;

function IsHintStored: Boolean;

procedure SetText(const Value: TCaption);

{метод записи свойства Text/Caption}

procedure SetParent{AParent: TWinControl); virtual;

property Caption: TCaption read GetText write SetText stored IsCaptionStored;

property Text: TCaption read GetText write SetText;

property Parent: TWinControl read FParent write SetParent;

Объявление свойства имеет следующий синтаксис: property <имя свойства>: тип определители;

При объявлении свойства используется зарезервированное слово property, после которого указываются четыре ключевых фрагмента информации. Первый - имя свойства, этот иденти­фикатор используется для ссылок на значение свойства. Таким образом, свойства получают внешний вид полей данных.

Каждое объявление свойства должно определять тип свойства, для этого используется символ двоеточие после имени свойства.

Для указания метода, который будет использоваться для осуществления выборки значения свойства, используется ди­ректива read. Метод должен быть функцией, чей возвращае­мый тип является тем же самым, что и тип свойства.

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

За спецификацией метода чтения следует определитель ме­тода записи, директива write определяет, какой метод будет ис­пользоваться для присвоения свойству значения. Метод должен быть процедурой, имеющей единственный параметр, тип кото­рого должен совпадать с типом свойства.

При обращении к значению свойства происходит перена­правление на соответствующий метод. Например, оператор s: =Editl. Text; автоматически будет преобразован в оператор s: =Editl. GetText; а оператор Editl. Text: =" Test" - в опе­ратор Editl.Text("Test").

Описание свойства должно содержать определитель read или write или сразу оба. Если описание свойства включает в себя только определитель read, то оно является свойством только для чтения. В свою очередь, свойство, чье описание включает в себя только определитель write, является свойством только для записи. При присвоении свойству, определенному с директивой только для чтения, какого-либо значения или при использова­нии в выражении свойства с директивой только для записи все­гда возникает ошибка.

В отличие от внутренних полей хранения данных свойства не могут быть переданы в процедуру (или функцию) в качестве параметра переменной (параметр var), это объясняется тем, что свойство не существует в памяти.

Когда программист использует Инспектор объектов для измене­ния свойств формы или свойств компонентов, то результирующие изменения заносятся в файл формы. Файлы форм представляют собой файлы ресурсов Windows, и когда приложение запускается, то описание формы подгружается из этого файла. Для определения того, что должно сохраняться в файле формы, служат специфика­торы памяти - необязательные директивы stored, default и node-fault. Эти директивы влияют на информацию о типе во время вы­полнения, генерируемую для свойств published.

Директива stored управляет тем, будет или нет свойство дейст­вительно запоминаться в файле формы. За директивой stored дол­жны следовать либо константы True или False, либо имя поля, имеющего тип Boolean, либо имя метода, у которого нет парамет­ров, и возвращающего значение типа Boolean. Например,

property Hint: string read FHint write FHint stored IsHintStored;

Если свойство не содержит директиву stored, то оно рассмат­ривается как содержащее ее с параметром True.

Директивы default и nodefault управляют значениями свой­ства по умолчанию. За директивой default должна следовать константа того же типа, что и свойство, например:

property Tag: Longint read FTag write FTag default 0 ;

Чтобы перекрыть наследуемое значение default без указания нового значения, используется директива nodefault. Директи­вы default и nodefault работают только с порядковыми типами и множествами, нижняя и верхняя границы которых лежат в промежутке от 0 до 31. Если такое свойство описано без дирек­тив default и nodefault, то оно рассматривается как с директи­вой nodefault. Для вещественных типов, указателей и строк значение после директивы default может быть только О, NIL и

(пустая строка) соответственно.

Когда Delphi сохраняет компонент, то просматриваются спе­цификаторы памяти published свойств компонента. Если значе­ние текущего свойства отличается от default значения (или ди­ректива default отсутствует) и параметр stored равен True, то значение свойства сохраняется, иначе свойство не сохраняется.

Спецификаторы памяти не поддерживаются свойствами-мас­сивами, а директива default при описании свойства-массива имеет другое назначение.

9.3.1. Простые свойства

Простые свойства - это числовые, строковые и символьные свойства. Они могут непосредственно редактироваться в Инс­пекторе объектов и не требуют специальных методов доступа.

Рассмотрим создание простого свойства Color, описанного в классе TContol (модуль controls.pas):

TControl = class (TComponent)

function IsColorStored: Boolean;

property Color: TColor read FColor write SetColor stored IsColorStored default clWindow;

function TControl.IsColorStored: Boolean;

Result:= not ParentColor;

procedure TControl.SetColor (Value: TColor);

if FColor <> Value then

FParentColor:= False;

Perform(CM_COLORCHANGED, 0, 0) ;

{вызов Perform позволяет обойти очередь сообщений Windows и послать сообщение, в данном случае - изменить цвет, элементу управления}

9.3.2. Свойства перечислимого типа

Определенные пользователем перечислимые и логические свойства можно редактировать в окне инспектора объектов, вы­бирая подходящее значение свойства в раскрывающемся списке. Рассмотрим создание свойства перечислимого типа на при­мере компонента Shape (модуль extctrls.pas).

TShapeType = (stRectangle, stSquare, stRoundRect, stRoundSquare, stEllipse, stCircle);

{вначале необходимо определить новый тип - перечислить возможные значения}

FShape: TShapeType;

procedure SetShape(Value: TShapeType);

property Shape: TShapeType read FShape write SetShape

default stRectangle;

procedure TShape.SetShape{Value: TShapeType);

if FShape <> Value then

Inva 1 idate; {гарантирует перерисовку компонента}

9.3.3. Свойства типа множества

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

При создании свойства типа множества нужно создать соот­ветствующий тип, описать методы доступа, после чего описать само свойство. В модуле Controls.pas свойсво Align описано сле­дующим образом:

TAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient);

TAlignSet = set of TAlign; TControl = class(TComponent)

procedure SetAlign(Value: TAlign);

property Align: TAlign read FAlign write SetAlign default alNone;

procedure TControl.SetAlign(Value: TAlign);

var OldAlign: TAlign;

if FAlign <> Value then

OldAlign:= FAlign;

Anchors:= AnchorAlign;

(not (csDesigning in ComponentState) or (Parent <> NIL))

if ((OldAlign in )=(Value in )) and not (OldAlign in ) and not (Value in ) then SetBounds(Left, Top, Height, Width)

{изменение границ компонента}

else AdjustSize; {устанавливает заданные размеры компонента}

Request Align; {нструктирует «родителя» переставить компонент

в соответствии со значением свойства Align }

9.3.4. Свойство-объект

Свойства могут являться объектами или другими компонен­тами. Например, у компонента Shape есть свойства-объекты Brush и Реп. Когда свойство является объектом, то оно может быть развернуто в окне инспектора так, чтобы его собственные свойства также могли быть модифицированы. Свойства-объек­ты должны быть потомками класса TPersistent, чтобы их свой­ства, объявленные в разделе published, могли быть записаны в поток данных и отображены в инспекторе объектов.

Для определения объектного свойства компонента необходимо сначала определить объект, который будет использоваться в каче­стве типа свойства. В модуле graphics.pas описан класс TBrush:

TBrush = class(TGraphicsObject)

procedure GetData(var BrushData: TBrushData);

procedure SetData(const BrushData: TBrushData);

function GetBitmap: TBitmap;

procedure SetBitmap(Value: TBitmap);

function GetColor: TColor;

procedure SetColor(Value: TColor);

function GetHandle: HBrush.;

procedure SetHandle(Value: HBrush);

function GetStyle: TBrushStyle;

procedure SetStyle(Value: TBrushStyle);

constructor Create; destructor Destroy; override;

procedure Assign(Source: TPersistent); override;

property Bitmap: TBitmap read GetBitmap write SetBitmap;

property Handle: HBrush read GetHandle write SetHandle;

property Color: TColor read GetColor write SetColor

default clWhite;

property Style: TBrushStyle read GetStyle write SetStyle

default bsSolid;

Метод Assign предназначен для копирования значения свойств экземпляра TBrush:

procedure TBrush.Assign (Source: TPersistent);

if Source is TBrush then

Lock; {блокирует использование объекта}

TBrush(Source).Lock;

BrushManager.AssignResource(Self, TBrush(Source).FResource);

finally TBrush(Source).Unlock;

finally Unlock; {завершает секцию кода, начатую методом Lock,

снимая блокировку объекта}

inherited Assign(Source);

Чтобы определить свойство-объект, нужно определить внут­реннее поле. Так как свойство представляет объект, его нужно создать, а по завершении - уничтожить, поэтому в код включены конструктор Create и деструктор Destroy. Кроме того, объявлен метод доступа SetBrush, предназначенный для записи свойства Brush.

TShape = class(TGraphicControl)

procedure SetBrush(Value: TBrush);

constructor Create (AOwner: TComponent) ; overrider;

property Brush: TBrush read FBrush write SetBrush;

constructor TShape.Create(AOwner: TComponent);

inherited Create(AOwner);

FBrush:= TBrush.Create;

FBrush.OnChange:= StyleChanged;

destructor TShape.Destroy;

FBrush. Freer-inherited Destroy;

procedure TShape.SetBrush (Value: TBrush);

FBrush.Assign(Value);

9.3.5. Свойство-массив

Примерами свойств-массивов могут служить такие свойства, как TMemo.Lines, TScreen.Fonts, TStringGrid.Cells.

Особенности свойства-массива заключаются в следующем:

свойства-массивы объявляются с помощью индексных па­раметров, цель которых - указать количество и тип ин­дексов, которые будут использоваться свойством;

спецификации методов чтения и записи должны ссылать­ся на методы доступа. Методом для определителя read должна быть функция, список параметров которой совпа­дает со списком параметров, описывающих индекс свойст­ва, и возвращающей значение того же типа, что и свойст­во. В свою очередь, методом в определителе write должна быть процедура, список параметров которой совпадает со списком параметров, описывающих индекс свойства. Спи­сок параметров такой процедуры может содержать и до­полнительные свойства.

TCanvas = class (TPersistent)

function GetPixel (X, Y: Integer) : TColor; {метод чтения}

procedure SetPixel (X, Y: Integer; Value: TColor);

{метод записи}

constructor Create;

destructor Destroy; override;

property Pixels: TColor read GetPixel write SetPixel;

constructor TCanvas.Create;

inherited Create;

CanvasList. Add (Self) ; {добавляет в список ссылки на объекты}

destructor TCanvas.Destroy;

CanvasList .Remove (Self) ; {удаляет из списка ссылки на объекты}

inherited Destroy; end;

function TCanvas.GetPixel (X, Y: Integer): TColor;

RequiredState();

GetPixel:= Windows.GetPixel(FHandle, X, Y) ; end;

procedure TCanvas.SetPixel(X, Y: Integer; Value: TColor);

RequiredState();

Windows.SetPixel(FHandle, X, Y, ColorToRGB(Value));

Доступ к такому свойству-массиву осуществляется следую­щим образом:

Canvas.Pixels :=clRed; что означает:

Canvas.SetPixel (10, 20, clRed);

За описанием свойства-массива может следовать директива default. В данном случае это будет означать, что это свойство становится свойством по умолчанию для данного класса. На­пример:

TStringArray = class public property Strings: string . . . ; default;

Если у класса есть свойство по умолчанию, то доступ к это­му свойству может быть осуществлен оператором

<имя компонента>, который эквивалентен оператору

<имя компонента>.<имя свойства>.

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

9.3.6. Массив свойств

Определитель Index позволяет разным свойствам иметь один и тот же метод доступа. Его описание состоит из директивы index и последующей за ней константой целого типа в промежутке от -2147483647 до 2147483647. Если у свойства есть определи­тель Index, то определители read и write должны ссылаться на методы, а не на поля. Например:

TRectangle = class private

FCoordinates: array of Longint;

function GetCoordinate(Index: Integer): Longint;

procedure SetCoordinate{Index: Integer; Value: Longint); public

property Left: Longint index 0 read GetCoordinate

write SetCoordinate; property Top: Longint index 1 read GetCoordinate

write SetCoordinate; property Right: Longint index 2 read GetCoordinate

write SetCoordinate; property Bottom: Longint index 3 read GetCoordinate

write SetCoordinate;

Обращение к свойству, определенному с директивой index, например,

Rectangle.Right:= Rectangle.Left + 100;

{Rectangle: TRectangle}

автоматически преобразуется к вызову метода,

Rectangle.SetCoordinate (2, Rectangle.GetCoordinate(0) + 100);

9.3.7. Перекрытие и переопределение свойств

Описание свойства без указания типа называется перекры­тием свойства. Самый простой способ перекрытия состоит в ис­пользовании зарезервированного слова property и идентифика­тора - имени свойства. Данный способ используется для смены видимости свойства.

Перекрытия свойств могут содержать директивы read, write, stored, default и nodefault. Перекрытие может заменить суще­ствующие наследуемые определители доступа, добавить недо­стающие, увеличить видимость свойства, но оно не может уда­лить существующий определитель или уменьшить видимость свойства. Следующий пример демонстрирует использование пе­рекрытия свойств:

TAncestor = class

property Size: Integer read FSize; property Text: String read GetText write SetText; property Color: TColor read FColor write SetColor stored False;

TDerived = class(TAncestor)

property Size write SetSize; published property Text; property Color stored True default clBlue;

Перекрытие свойства Size добавляет определитель write, что позволяет редактировать свойство, а перекрытие свойств Text и Color меняет их видимость с protected на published. Перекры­тие свойства Color указывает, что оно должно быть сохранено, если его значение отлично от clBlue.

Переопределение свойства включает указание его типа, оно скрывает наследуемое свойство. Это означает, что создается но­вое свойство с тем же именем, что и у предка. Любое описание свойства, которое содержит указание типа, должно быть завер­шенным и включать в себя как минимум один определитель до­ступа:

type TAncestor = class

property Value: Integer read Methodl write Method2; end;

TDescendant = class(TAncestor)

property Value: Integer read Method3 write Method4; end;

9.3.8. Создание событий

Событие - это любое происшествие, вызванное вмешатель­ством пользователя, операционной системы или логикой про­граммы. Событие связано с некоторым программным кодом, отвечающим на это происшествие. Совокупность события и кода, выполняющегося в ответ на это событие, называется свойством-событием и реализуется в виде указателя на некото­рый метод. Метод, на который указывает это свойство, называ­ется обработчиком события.

Свойства-события являются не более чем указателями на ме­тоды. В модуле Controls.pas определены стандартные свойст­ва-события.

Описание свойства-события начинается с описания нового типа, который представляет собой процедуру, одним из параметров которой, является Sender типа TObject, а директива of object делает эту процедуру методом:

TMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState; X, Y: Integer) of object;

Когда происходит какое-либо событие, например, перемеще­ние мыши, в систему Win32 посылается соответствующее сооб­щение, в нашем случае WM_MOUSEMOVE. Система Win32 пе­редает это событие элементу управления, для которого оно предназначено и на которое он должен тем или иным способом ответить. Элемент управления может ответить на это событие, сначала проверив наличие кода, предусмотренного для выпол­нения. Для этого он проверяет, ссылается ли свойство-событие на какой-либо код. Если да, то элемент выполняет этот код, на­зываемый обработчиком события. Операция по определению наличия метода, связанного с событием-свойством, возлагается на метод диспетчеризации. Эти методы объявляются как защи­щенные методы того компонента, которому они принадлежат.

Описание свойства-события состоит из двух частей: во-пер­вых, событие требует внутреннего поля данных, которое испо­льзуется для хранения указателя на метод; во-вторых, создает­ся соответствующее свойство, которое во время проектирования дает возможность присоединения обработчиков событий:

TControl = class(TComponent) private

FOnMouseMove: TMouseMoveEvent; {внутреннее поле события} procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;

procedure MouseMove(Shift: TShiftState; X, Y: Integer);

dynamic; {метод диспетчеризации}

property OnMouseMove: TMouseMoveEvent read FOnMouseMove

write FOnMouseMove;

Метод диспетчеризации определяет, существует ли свойство-событие на какой-нибудь метод, и если это так, то передает управление соответствующей процедуре:

procedure TControl.MouseMove(Shift: TShiftState; X, Y: Integer); begin

if Assigned (FOnMouseMove) then FOnMouseMove (Self, Shift, X, Y) ; end;

Чтобы обеспечить возможность переопределения обработки события, необходимо перехватить возникшее событие, обрабо­тать его стандартным образом и передать управление методу диспетчеризации:

procedure TControl.WMMouseMove(var Message: TWMMouseMove); begin inherited; if not (csNoStdEvents in ControlStyle) then

{включение csNoStdEvents во множество ControlStyle заставляет игнорировать стандартные события мыши, клавиатуры. Этот флаг позволяет ускорить запуск приложения, если оно при этом не нуждается в обработке этих событий} with Message do MouseMove(KeysToShiftState(Keys), XPos, YPos) ; end;

9.3.9. Создание методов

Добавление в компонент методов не отличается от добавле­ния методов в любой другой класс. Однако следует придержи­ваться следующих правил:

исключить взаимозависимость методов;

метод, вызываемый пользователем, не должен приводить компонент в такое состояние, при котором другие методы не действуют;

метод должен иметь осмысленное имя.

Упражнение 9.2.1 (продолжение). Добавим в описание ново­го класса свойство объектного типа TLabel:

TLabelEdit = class (TEdit)

{ Private declarations }

FLabel: Tlabel; {внутреннее поле}

{ Protected declarations }

function GetLabelCaption: string; virtual;

{метод чтения свойства Caption объектного свойства Label}

procedure SetLabelCaption(Const Value: String); virtual;

{метод записи свойства

Caption объектного свойства Label} public

{ Public declarations }

constructor Create(Aowner: TComponent); override;

destructor Destroy; overrider;

{ Published declarations }

property LabelCaption: string read GetLabelCaption write SetLAbelCaption; end;

В конструкторе необходимо создать экземпляр типа TLabel, сохранить ссылку на него во внутреннем поле и задать значе­ние свойства Caption созданного объекта:

constructor TLabelEdit.Create(AOwner: TComponent); begin

inherited Create(Aowner);

FLabel:= TLabel.Create(NIL);

{владельца у свойства-объекта не существует}

FLabel.Caption:= "Label for Edit"; end;

При разрушении компонента необходимо освободить ресур­сы, занятые созданным объектным свойством:

destructor TLabelEdit.Destroy;

if (FLabel <> NIL) and (FLabel.parent = NIL) Then FLabel.free;

inherited Destroy;

Методы доступа чтения и записи соответственно считывают и записывают значение свойства Caption во внутреннее поле FLabel:

function TLabelEdit.GetLabelCaption: String;

Result:= FLabel.Caption;

procedure TLabelEdit.SetLabelCaption(Const Value: string);

Flabel.Caption:= value;

Эксперимент. Сохраните модуль компонента. Запустите тестовое приложение. Как отображается создаваемый компо­нент? ♦

По-прежнему отображается только компонент Edit. Это свя­зано с тем, что не определено свойство Parent внутреннего ком­понента Label. Напомним, свойство Parent задает компонент, который отвечает за прорисовку принадлежащих ему компо­нентов.

Добавьте в раздел protected описания класса TLabelEdit про­цедуру

procedure SetParent(value: TWinControl); override;

В разделе implementation модуля компонента опишите код процедуры:

procedure TLabelEdit.SetParent{Value: TWinControl};

if (Owner=NIL) or not(csDestroying in Owner.ComponentState)

{если владелец компонента не определен или владелец не разрушается}

then FLabel.Parent:=Value;

{устанавливаем владельца компонента}

inherited SetParent(Value);

Эксперимент. Модифицируйте тестовое приложение в соот­ветствии с рис. 9.3.1. Значения Value компонентов SpinEdit определяют положение компонента LabelEdit.

Запустите тестовое приложение.

Проверьте отображение компонента LabelEdit при различ­ных значениях свойств Left и Тор. ♦

При перемещении компонента Edit надпись (Label) должна перемещаться за ним. Для этого необходимо перехватить собы­тие перемещения - WM_MOVE. Опишите в разделе private описания компонента TLabelEdit заголовок обработчика собы­тия WMMove:

procedure WMMove(var Msg: TWMMove); message WM_MOVE;

Обработчик события WMMove, кроме стандартной обработки события, содержит операторы перемещения компонента Label:

procedure TLabelEdit.WMMove(var Msg: TWMMove); begin inherited;

if Flabel <> NIL then with FLabel do SetBounds(Msg.XPos, Msg.Ypos-Height-5, Width, Height);

{procedure SetBounds(ALeft, ATop, A Width, AHeight: Integer) устанавливает сразу все граничные свойства элемента управления} end;

Эксперимент. Сохраните код модуля компонента. Запусти­те тестовое приложение, убедитесь в правильности перемеще­ния компонента. ♦

9.4. Регистрация компонента в среде Delphi

В процессе регистрации компонент помещается в палитру компонентов Delphi.

Рассмотрим процесс установки компонента на примере ком­понента TLabelEdit, разработанного в упр. 9.2.1.

Выполните команду Component/Install Component. В диало­говом окне Install Component в строке Unit file name укажите имя модуля нового компонента - ...\LabelEdit\LabelEdit.pas (рис. 9.4.1). Щелкните на кнопке ОК.

Появится диалоговое окно Confirm с сообщением «Package dclusr50.bpl will be built then installed. Continue?» (Пакет dclusr50.bpl будет переустановлен. Продолжить?), щелкните на кнопке Yes.

Если нет ошибок в файле модуля нового компонента, то ком­понент будет зарегистрирован в палитре компонентов Delphi и будет отображено окно Information с сообщением «Package c:\program files\borland\delphis\Projects\Bpl\ dclusrSO.bpl has been installed. The following new component(s) have been registered: TLabelEdit» (). Щелкните на кнопке ОК.

В палитре компонентов на странице Test появится новый компонент (рис. 9.4.2).

Примечание. Чтобы изменить пиктограмму нового компонента, вос­пользуйтесь программой Image Editor. Выполните команду File/ New/ Component Resource File, затем Resource/ New/ Bitmap. В появившемся диалоговом окне Bitmap Properties установите размер рисунка 24x24 пикселя, установите цвет - VGA (16 color), нажмите OK и измените имя Bitmapl на имя компонента (в нашем случае TLABELEDIT, вводите обя­зательно прописными символами). Затем выполните команду Resource/ Edit и создайте нужный рисунок. После этого сохраните файл в катало­ге, в котором хранится модуль компонента, под тем же именем, но с расширением.DCR, установите компонент еще раз.

В случае повторной установки компонента или же в случае наличия ошибок в модуле компонента будет отображен редак­тор пакета компонентов Package-dclusr50.dpk (рис. 9.4.3), ко­торый позволяет удалять, добавлять, компилировать пакет.

Для сохранения изменений, проведенных в пакете Dclusr50, щелкните на кнопке ОК в диалоговом окне Confirm «Save changes to project Dclusr50?» (Сохранить изменения в проекте DclusrSO?).

Упражнение 9.4.1. Разработайте компонент SimpleTree, ото­бражающий структуру файловой системы в древовидной форме (рис. 9.4.4).

Создайте каталог SimpleTree.

Выполните команду File\New\Component. В диалоговом окне New Component установите основные параметры:

введите имя класса предка - TCustomControl, так как этот класс предоставляет возможность рисования на ком­поненте и разрешает получать фокус ввода;

имя создаваемого класса - TSimpleTree;

название страницы палитры компонентов, на которую бу­дет помещен компонент - Test;

имя файла модуля, содержащего описания создаваемого класса, - ...\SimpleTree\SimleTree.pas;

□ значение строки указания путей для поиска файла оставь­те без изменения.

После щелчка на кнопке ОК откроется окно редактирования модуля...\SimpleTree\SimleTree.pas, который содержит описа­ние класса TSimpleTree и процедуру Register.

В разделе public описания класса опишите конструктор Create: constructor Create{AOwner: TComponent); override;

в котором определим возможность обрабатывать события мыши и установим объем рамки компонента:

constructor TSimpleTree.Create(AOwner: TComponent); begin

inherited Create{AOwner);

ControlStyle:= ;

{Свойство ControlStyle отвечает за различные атрибуты компонента: csFramed - элемент управления имеет рамку и нуждается в эффектах Ctrl 3D; csCaptureMouse - данный элемент перехватывает события мыши; csDoubleClicks - когда на элементе дважды щелкнули мышью, генерируется событие OnDlClick; csClickEvents - когда на элементе нажата и отпущена мышь, генерируется событие OnClick}

FBorcier:= bsSingle; Width:= 150; Height:= 150;

Tabs top:= True; {возможность перехода на компонент

при нажатии на клавишу Tab}

9.2. Переопределите деструктор класса TSimpleTree.

Чтобы предоставить возможность пользователю компонента изменять внешний вид компонента и его положение на форме, создайте свойство Border и выполните перекрытие свойств Align, Anchors, Color, Ctl3D, Font, TabOrder, TabStop:

FBorder: TBorderStyle

published property Align; property Anchors;

property Border: TBorderStyle read FBorder write SetBorder default bsSingle;

property Color; property Ctl3D; property Font; property TabOrder; property TabStop;

procedure TSimpleTree.SetBorder(const Value: TBorderStyle);

if FBorder Value then begin FBorder:=Value ; RecreateWnd;

{разрушает существующее окно, после чего создает заново}

Эксперимент. Сохраните файл компонента.

Создайте тестовое приложение. Сохраните файл модуля под именем Main.pas, файл проекта Test.dpr.

Положите на форму компонент Button (измените свойство Caption на «Создать компонент»), создайте обработчик события onClick кнопки:

procedure TForml.buttonlclick (Sender: TObject);

Tree:=TSimpleTree.Create(Forml);

with Tree do begin Parent:= forml; Left:=5; Top:=5; end; end;

Опишите переменную Tree и подключите модуль SimpleTree.

Запустите тестовое приложение. После щелчка на кнопке на форме должен отобразиться экземпляр класса TSimpleTree. За­кройте приложение, убедитесь, что при этом не происходит ни­каких ошибок. ♦

Добавим в компонент возможность вертикального скрол­линга дерева (ScrollBar). Перекройте метод CreateParams, ко­торый вызывается перед созданием окна (перед вызовом функ­ции Win API CreateWindow):

procedure CreateParams(var Params: TCreateParams); override;

procedure TSimpleTree.CreateParams{var Params: TCreateParams);

inherited CreateParams(Params); with Params do begin

if FBorder = bsSingle then Style:=Style or WS_BORDER; Style:«Style or WS_VSCROLL; end; end;

Эксперимент. Запустите тестовое приложение. Убедитесь в появлении вертикальной полосы прокрутки на создаваемом ком­поненте.

Используя справочную систему Delphi, определите, какие стили окон управления существуют и как каждый стиль влия­ет на функциональность окна управления. ♦

Перед отображением компонента вызывается метод Paint. Для рисования древовидной структуры файловой системы не­обходимо его переопределить:

Procedure Paint; override;

procedure TSimpleTree.Paint;

Рассмотрим процесс добавления большого количества эле­ментов (узлов) в дерево. При добавлении каждого узла (до того времени, когда они отобразятся в компоненте) происходит пе­рерисовка дерева, вызывающая мигание. Чтобы предотвратить этот эффект, создадим механизм блокировки «отрисовки» при добавлении узлов в дерево, который будет содержать два мето­да BeginPaint (начало блокировки) и EndPaint (окончание блокировки):

FUpdateCount: integer;

{в конструкторе задайте начальное значение равным нулю} public

procedure BeginUpdate;

procedure EndUpdate;

procedure TSimpleTree.BeginUpdate; begin

inc(FUpdateCount); end;

procedure TSimpleTree.EndUpdate; begin

Dec(FUpdateCount);

if FUpdateCount = 0 then Invalidate; end;

Для того чтобы не происходила перерисовка дерева в про­цессе добавления узлов, в метод Paint добавляем оператор:

if FUpdateCount > 0 then exit;

Для вычисления положения узла используем значение ши­рины и высоты символа «А». Введем два поля FCharWidth и FCharHeight, соответственно, длина и высота символа текста, обнов­ление значений которых будет происходить в следующем методе:

procedure TSimpleTree.UpdateCharMetrics; begin

Canvas.Font:= Self.Font;

FCharHeight:= Canvas.TextHeight("A") + 2; FCharWidth:= Canvas.TextWidth("A1); end;

Метод UpdateCharMetrics будет вызываться в ответ на собы­тие смены шрифта и размеров компонента:

procedure CMFontChanged(var Msg: TMessage);

message CM_FONTCHANGED; procedure WMSize(var Msg: TWMSize); message WM_SIZE;

procedure TSimpleTree.CMFontChanged(var Msg: TMessage); "negin

UpdateCharMetrics; end;

procedure TsimpleTree.WMSize(var Msg: TWMSize); begin

UpdateCharMetrics; end;

Вернемся к процедуре Paint. Рисование дерева каталогов бу­дем осуществлять последовательно: сначала отобразим узлы де­рева, а затем, если нужно, - линии. Определите свойство DrawLines логического типа, значение True которого задает не­обходимость рисования линий дерева, False - рисование дере­ва без линий.

property DrawLines: boolean read FDrawLines write SetDrawLines default True;

procedure TSimpleTree.SetDrawLines(const Value: boolean); begin

if FDrawLines <> Value then begin

FDrawLines:=Value; Repaint; end; end;

He забудьте в конструкторе определить начальное значение поля FDrawLines. После этого метод Paint можно записать сле­дующим образом:

procedure TSimpleTree.Paint;

procedure DoDrawNodes;

procedure DoDrawLines;

if FUpdateCount > 0 then exit;

DoDrawNodes; {рисуем узлы}

if FDrawLines then DoDrawLines; {рисуем линии}

Процедура DoDrawNodes рисует узлы дерева каталогов. Од­нако в конкретный момент времени нужно нарисовать только раскрытые пользователем узлы дерева. Список узлов дерева бу­дем хранить в защищенном (private) поле FDrawList типа TList класса TSimpleTree.

Задание для самостоятельного выполнения

9.3. В конструкторе класса TSimpleTree создайте FDrawList (эк­земпляра класса TList), а в деструкторе освободите память, ассоциированную с этой переменной.

Список FDrawList содержит указатели на узлы дерева. Каж­дый узел представляет собой экземпляр класса TSimpleNode:

TSimpleNode = class(TObject) private

FTree: TSimpleTree/ {указатель на дерево}

FParent: TSimpleNode; {родительский узел}

FChildren: TList; {список дочерних узлов}

FCaption: string; {текст для отображения}

FLevel: integer; {уровень узла}

FIndex: integer;

{индекс в списке дочерних узлов родительского узла}

FX, FY: integer;

{последние координаты, по которым рисовался узел}

FExpanded: boolean; {развернутли}

FAbsolutelndex: integer; {индекс узла в дереве}

procedure Redraw; {перерисовка узла по последним координатам}

procedure DrawAt(X, Y: integer);

{нарисовать узел по координатам X, Y}

function GetChildren(Index: integer): TSimpleNode;

function GetChildrenCount: integer;

function GetSelected: boolean;

procedure SetSelected(const Value: boolean);

procedure SetCaption(const Value: string);

procedure SetExpanded(const Value: boolean); public

constructor Create(ATree: TSimpleTree);

destructor Destroy; override;

procedure ClearChildren; {очистить все дочерние узлы}

property Children: TSimpleNode read GetChildren;

property ChildrenCount: integer read GetChildrenCount;

property Caption: String read FCaption write SetCaption;

property Level: integer read FLevel;

property Selected: boolean read GetSelected write SetSelected;

{выбран ли узел}

property Absolutelndex: integer read FAbsolutelndex;

property Index: integer read FIndex;

property Expanded: boolean read FExpanded write SetExpanded; end;

Обновление названия каталога:

procedure TSimpleNode.SetCaption(const Value: String); begin

if reaction <> Value then

rtaction:=Value; ГТгее.Invalidate;

Задания для самостоятельного выполнения

9.4. Реализуйте методы GetChildrenCount (возвращает коли­чество элементов, содержащихся в списке FChildren) и GetChildren (возвращает элемент списка FChildren под но­мером Index) класса TSimpleNode.

Обратите внимание на то, что описание класса TSimpleTree содержит элемент типа TSimpleNode, и наоборот. Чтобы сооб­щить компилятору о существовании класса TSimpleTree в раз­деле Туре, опишите классы следующим образом:

TSimpleTree = class; TSimpleNode = class (TObject)

TSimpleTree = class(TCustomControl)

Реализуем методы класса TSimpleNode. Конструктор иници­ализирует значения полей, а также выделяет память под пере­менную, которая будет содержать ссылки на подкаталоги:

constructor TSimpleNode.Create(ATree: TSimpleTree); begin

inherited Create;

FChildren:=TList.Create;

FExpanded:=False; end;

Деструктор освобождает память, ассоциированную с пере­менной FChildren (список ссылок на дочерние каталоги):

destructor TSimpleNode.Destroy; begin

ClearChildren; FChildren. Freer-inherited Destroy; end;

Удаление всех дочерних подкаталогов выполняет рекурсив­ная процедура, которая освобождает память, занятую под хра­нение ссылок на дочерние подкаталоги текущего подкаталога:

procedure TSimpleNode.ClearChildren;

var i: Integer; begin

for i:=0 to FChildren.Count - 1 do

9.5. При отображении узлов дерева использовались следующие свойства класса TSimpleTree:

property TextColor: TColor index 0 read GetTreeColor

write SetTreeColor; property LinesColor: TColor . ..; property SelTextColor: TColor property SelBackColor: TColor ...;

Используя массив свойств, реализуйте перечисленные выше свойства. Не забудьте в конструкторе Create установить началь­ные значения этих свойств.

К реализации методов GetSelected, SetSelected, SetExpanded вернемся немного позднее.

Таким образом, внутренняя процедура DoDrawNodes метода TSimpleTree.Paint, отображающая узлы дерева, должна выпол­нить такую последовательность операторов:

var i: Integer; begin

for i:=0 to FDrawList.Count - 1 do

TSimpleNode(FDrawList[i]) .DrawAt (0, i * FCharHeight); end;

Метод NodelnView предназначен для проверки видимости узла (опишите в разделе private класса TSimpleTree):

function TSimpleTree.NodelnView (Node: TSimpleNode) : Boolean; begin

Result:=FDrawList.IndexOf(Node) > -1; end;

Для прорисовки дерева в методе Paint осуществляется рисо­вание линий) внутренняя процедура которого DoDrawLines ме­тода Paint:

procedure DoDrawLines; var MaxLevel: integer; i: integer; j: integer; begin

MaxLevel:=0; Canvas.Pen.Color:=LinesColor;

{устанавливаем цвет рисования линий} for i:=0 to FDrawList.Count - 1 do

{просматриваем все узлы дерева} with TSimpleNode(FDrawList[i]) do if FLevel > 0 then

Canvas.MoveTo(FX + FCharWidth, FY + FCharHeight div 2) ; Canvas.LineTo(FX, FY + FCharHeight div 2); if (Flndex > 0) and

(not NodeInView(FParent.Children)) then Canvas.LineTo(FX, 0) else

if FIndex=0 then Canvas .LineTo (FX, FY - FCharHeight div 2) else Canvas.LineTo (FX, FParent.Children.FY); if Flndex < FParent.ChildrenCount - 1 then if not NodelnView(FParent.Children) then Canvas.LineTo(FX, ClientHeight); if MaxLevel < FLevel then MaxLevel:=FLevel; end;

for i:=l to MaxLevel do begin j:=0; while (j < FDrawList.Count) and

(TSimpleNode(FDrawList[j]).Level <> i) do Inc(j); if j = FDrawList.Count then begin

Canvas.MoveTo((i + 1) * FCharWidth, 0) ; Canvas.LineTo((i + 1) * FCharWidth, ClientHeight); end; end; end;

Задание для самостоятельного выполнения

9.6. Поясните каждый оператор метода TSimpleTree.DoDrawLmes. Приведите все возможные варианты выполнения метода DoDrawLines.

Сформируем список FNodes узлов. В описание класса TSimple-Тгее введем следующие элементы:

FNodes: TList; {глобальный массив всех узлов}

FStartlndex: Integer; {абсолютный индекс узла,

с которого начинаем рисовать. Начальное значение равно нулю} FMaxLinesInView: Integer;

{максимальное количество отображаемых узлов} FMaxLines: Integer; {максимальное количество видимых узлов}

function GetNode(Index: integer): TSimpleNode; function GetNodeCount: integer;

property Nodes: TSimpleNode

read GetNode; default; {возвращает узел под номером Index} property NodeCount: integer read GetNodeCount;

{общее количество узлов}

Задание для самостоятельного выполнения

9.7. Реализуйте методы GetNode и GetNodeCount. He забудьте выделить память под переменную FNodes в конструкторе, а в деструкторе - освободить.

Формирование списка узлов дерева осуществляется в методе UpdateDrawList, который будет вызываться в ответ на каждое из следующих событий:

изменение размеров компонента,

добавление новых узлов, а скроллинг,

сворачивание или разворачивание какого-либо узла.

procedure TSimpleTree.UpdateDrawList;

function ListFull: Boolean; {проверка на полноту списка}

Result:=FDrawList.Count >= FMaxLinesInView; end;

procedure FormDrawList(Node: TSimpleNode);

{формирование списка} var i: Integer; begin

if not ListFull then FDrawList.Add(Node); Inc(FMaxLines);

if Node . FExpanded then begin {если узел раскрыт}

for i:=0 to Node.ChildrenCount - 1 do FormDrawList(Node.Children[i]) ; Inc(FMaxLines, Node.ChildrenCount); end; end;

var i, Min: Integer; begin

FMaxLinesInView:=(ClientHeight div FCharHeight) + 1; FDrawList.Clear; FMaxLines:=0;

if FStartlndex + FMaxLinesInView > GetNodeCount then Min:=GetNodeCount -FStartlndex else Min:=FMaxLinesInView; for i:=FStartIndex to FStartlndex + Min - 1 do

FDrawList .Add (FNodes [i]) ; {добавляем в список узлы}

for i:=0 to GetNodeCount - 1 do

{вычисляем максимальное количество видимых узлов} with Nodes [i] do

if FParent = nil then Inc(FMaxLines)
else if FParent.FExpanded then Inc(FMaxLines);
UpdateScrollBar; {обновляем состояние ScrollBar}

Обновление состояния компонента ScrollBar осуществляет метод UpdateScrollBar:

procedure TSimpleTree.UpdateScrollBar; var Scrolllnfo: TScrollInfo;

{структура, которая содержит параметры отображения полосы прокрутки} begin

if FMaxLinesInView >= FMaxLines then ShowScrollBar(Handle, SBJVERT, False)

{спрятать вертикальную полосу прокрутки} else begin

FillChar(Scrolllnfo, SizeOf(TScrollInfo), 0) ; Scrolllnfo.cbSize:=SizeOf{TScrollInfo);

Scrolllnfo. fMask:=SIF_ALL; {ограничиваетразмер страницы пропорционально отображению полосы прокрутки, минимальное и максимальное значение для диапазона скроллинга } Scrolllnfo.nMax:=FMaxLines;

{максимальное количество отображаемых строк}

ScrollInfo.nPage:=FMaxLinesInView; {общее количество строк} ScrollInfo.nPos:=FStartIndex; ShowScrollBar(Handle, SB_VERT, True);

{показать вертикальную полосу прокрутки} SetScrollInfo(Handle, SB_VERT, Scrolllnfo, True);

{установить назначенные параметры} end; end;

В раздел private класса TSimpleTree добавьте описание мето­дов UpdateDrawList и UpdateScrollBar.

Метод UpdateScrollBar вызывается также и на изменение размеров компонента. Для полноценной поддержки скроллин­га необходимо обрабатывать сообщение WMVSCROLL:

procedure WMVScroll(var Msg: TWMVScroll); message WM_VSCROLL;

procedure TSimpleTree.WMVScroll(var Msg: TWMVScroll); begin

case Msg.ScrollCode of SBJTHUMBPOSITION: begin

{прокручивает на абсолютную позицию.
Текущая позиция определяется значением параметра npos}
SetScrollPos(Handle, SB_VERT, Msg.Pos, True);
FStartlndex:=Msg.Pos;
end;
SB_LINEUP: {вверх}

if FStartlndex > 0 then Dec (FStartlndex) else exit;
SB_LINEDOWN: {прокрутить на одну строку вниз}

if FStartlndex < FMaxLines - FMaxLinesInView + 1 then

Inc(FStartlndex) else exit; else exit; end;

UpdateDrawList; {обновление списка}

Invalidate; {перерисовка компонента}

Эксперимент. Сохраните модуль компонента. Запустите те­стовое приложение. Убедитесь, что при перемещении «бегун­ка» полосы прокрутки компонент ScrollBar исчезает. ♦

Добавим в обработчик события WMSize вызов методов об­новления списка узлов и перерисовки полосы прокрутки:

procedure TsimpleTree.WMSize(var Msg: TWMSize); begin inherited;

UpdateCharMetrics ; UpdateDrawList; UpdateScrollBar; end;

Эксперимент. Запустите тестовое приложение. Отображает­ся ли полоса прокрутки? Объясните, почему. ♦

Создаваемое дерево состоит как минимум из одного узла. Главный узел хранится в поле FMainNode типа TSimpleNode и доступен через свойство только для чтения MainNode.

Свойство SelectedNode типа TSimpleNode является указате­лем на выбранный узел, для записи в этот узел вызывается ме­тод SetSelectedNode.

Опишите перечисленные свойства и методы класса TSimple­Tree. Метод SetSelectedNode реализуется следующим образом:

procedure TSimpleTree.SetSelectedNode(const Value: TSimpleNode); var OldNode: TSimpleNode; begin if FSelectedNode <> Value then begin

{если выделенным должен стать другой узел} OldNode:=FSelectedNode; FSelectedNode:= Value; if (OldNode <> nil) and NodelnView(OldNode) then

{если узел, который был выделен ранее, виден -

его следует перерисовать} OldNode.Redraw;

if NodelnView(FSelectedNode) then {если выделенный сейчас узел видим, его также следует перерисовать}

FSelectedNode.Redraw; end; end;

В конструктор TSimpleTree.Create добавьте следующие опе­раторы:

FNodes:=TList.Create;

FMainNode:^TSimpleNode.Create(Self);

FSelectedNode:= FMainNode;

FNodes.Add(FMainNode);

FMainNode.FAbsolutelndex:= 0;

Чтобы обработать события мыши, выполним перекрытие ме­тода MouseDown:

procedure TSimpleTree.MouseDown (Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var Node: TSimpleNode; begin

inherited MouseDown(Button, Shift, X, Y) ;

{вызываем обработчик события нажатия кнопок мыши по умолчанию}
Node:=NodeAt ; {определяем узел}

if Node <> nil then SelectedNode:=Node;

if (Shift = ) and (FSelectedNode <> nil) then
if FSelectedNode . FExpanded {если выделенный узел раскрыт}
then Collap