|
|
|||||||||||||||||||||||||||||
|
Профессиональная разработка приложений с помощью Delphi5: часть 2Источник: УКЦ Interface Ltd. Сергей Трепалин
Введение в создание компонентов Delphi При разработке приложений с помощью Borland Delphi создавать компоненты удобно по следующим причинам:
В Delphi компоненты хранятся в пакетах (packages). Список используемых пакетов компонентов можно вызвать с помощью пункта меню Component/Install Packages (правда, этот диалог почему-то имеет заголовок Project Options). Рис.1. Файл проекта с комментариями и связанное с ним окно View Form При помощи этого диалога можно добавить новый пакет (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 в вышеупомянутом диалоге. После этого появляется форма, которая позволяет производить манипуляции с пакетом. Рис. 2. Использование наклонных шрифтов Пакет содержит две секции. В секции Contains приведен список модулей, формирующих компоненты данного пакета (*.pas- и *.dcu-файлы) и их пиктограммы (*.dcr-файлы). Секция Required содержит ссылки на другие пакеты, необходимые для работы этих компонентов. Добавление нового компонента к пакету выполняется кнопкой Add, удаление имеющегося - кнопкой Remove. До тех пор пока пакет не будет скомпилирован нажатием кнопки Compile, все изменения, вносимые в пакет, не будут появляться в среде разработки. И наконец, команда Install доступна в том случае, когда содержимое пакета удалено из среды разработки посредством снятия отметки напротив имени пакета в предыдущем диалоге. Команда Option позволяет выбрать для компиляции пакета опции, аналогичные опциям проекта. В них можно определить тип данного пакета: работающий во время выполнения, работающий во время разработки, или тот и другой одновременно (тип пакета по умолчанию). В опциях определяются каталоги, в которых следует искать необходимые модули и сохранять результаты компиляции. В них также определяются действия, необходимые для отладки: проверять или нет диапазон допустимых значений, как осуществлять оптимизацию, как обрабатывать ошибки ввода-вывода. И наконец, в опции может быть включена информация о версии пакета. Это очень важно, если приложение распространяется вместе с runtime-пакетами: при работе программы установки информация о версии позволит корректно заменить устаревшие версии пакетов, и наоборот, при попытке инсталлировать пакет более ранней версии, чем уже имеющийся на данном компьютере, последний не будет перезаписан. Delphi позволяет создавать простейшие составные компоненты из нескольких обычных компонентов, выбранных на форме во время разработки. Соответствующий эксперт вызывается с помощью пункта меню Components/Create Component Template. Этот пункт меню доступен, если на форме выделен хотя бы один компонент. После его выбора появляется диалоговая панель Component Template Information. Рис. 3. Эффект прорисовки фона символов В этом диалоге следует указать имя класса и имя страницы на палитре компонентов, куда следует поместить новый компонент. Если страница с данным именем отсутствует на палитре компонентов, то она будет создана. Можно также изменить предложенную пиктограмму нового компонента, загрузив подходящий *.bmp-файл. При создании шаблона запоминаются как свойства, измененные программистом в инспекторе объектов, так и обработчики событий, связанные с выделенными элементами управления. При этом обработчики событий запоминаются полностью, без фильтрации обращений к другим (не выделенным на форме) компонентам, глобальным переменным, методам и т.д. Соответственно, если в другом проекте такие компоненты (переменные, методы) отсутствуют, то при попытке скомпилировать такой проект будет получено диагностическое сообщение Unknown Identifier. Когда следует пользоваться шаблонами? Прежде всего, в случаях, если необходимо изменить какие-либо свойства, которые имеются по умолчанию в базовом классе. Например, в каком-либо приложении используется элемент управления для редактирования строки текста желтого цвета. Можно поместить компонент TEdit на форму, изменить свойство Color на желтый, отметить данный компонент и сохранить как шаблон. После этого можно обращаться к данному шаблону, и помещенный на форму компонент будет иметь желтый цвет. Однако не стоит злоупотреблять данной возможностью, ведь для элемента управления с измененным цветом будет создан новый класс и в памяти будут размножены все виртуальные методы. Это отрицательно скажется на ресурсах операционной системы. Использовать шаблоны компонентов удобно также, когда необходимо перенести ряд компонентов вместе с обработчиками событий с одной формы на другую. Для этого все они выделяются, создается шаблон компонентов, который и помещается на новую форму. При этом будут перенесены не только сами компоненты, но и обработчики событий, чего нельзя достичь при вызове команд Copy/Paste - в последнем случае обработчики событий будут утеряны. Компоненты, создаваемые при помощи команды Create Component Template, существенно отличаются от обычных компонентов, создаваемых стандартным способом (описанным ниже). Визуально главное различие заключается в следующем: если шаблон включает в себя несколько элементов управления, то, после того как такой компонент помещен на форму, можно выделить отдельный элемент управления и удалить его - при этом остальные сохранятся на форме. Для стандартных компонентов, если они включают в себя несколько элементов управления, невозможно выделить один из них и удалить -компонент выделяется и удаляется целиком. Создание простейшего компонента При написании нового компонента необходимо ясно представлять, что компонент создается для программистов, а не для конечных пользователей. При этом желательно, чтобы программист не вникал в детали реализации компонента, а просто пользовался экспонируемыми им свойствами и событиями. Это достигается очень тщательным тестированием. Новый компонент необходимо тестировать даже в ситуациях, для работы в которых он явно не предназначен. Поставим задачу следующим образом. Необходимо создать кнопку, которая будет издавать писк при нажатии, и реализовать ее в виде компонента так, чтобы программист мог поместить ее на форму и воспользоваться ею. Вообще, при рассмотрении компонентов мы довольно часто будем пользоваться простейшими внешними эффектами: писк, вывод сообщения и т.д. При этом подразумевается, что в тех местах, где используются внешние эффекты, может быть помещен любой, достаточно сложный код. Просто в данный момент он нас не интересует. Создание компонента начинается с выбора пункта меню Component/New components. После этого сразу же появляется диалог New Component. Рис. 4. Иллюстрация режима Transparent В этом диалоге необходимо определить класс-предок, имя вновь создаваемого класса, страницу на палитре, куда будет помещен новый компонент, имя модуля, содержащего реализацию нового компонента, и путь к нему. Если новый компонент использует другие модули, путь к которым не описан, то их необходимо определить в поле 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', [TButtonBeep]); end; end. В самом новом классе объявлены четыре секции, значение которых детально описано в разделе «Область видимости переменных и методов» предыдущей статьи данного цикла (КомпьютерПресс № 1'2001). Кроме того, в новом классе определена процедура Register, которая вызывается средой разработки Delphi при инсталляции данного модуля как компонента. Она содержит имя страницы на палитре, куда помещается данный компонент, и в квадратных скобках - имя класса. Вообще, в качестве параметра метод Register принимает массив типов классов, ведь в одном модуле может быть реализовано несколько компонентов. Поэтому они отделяются друг от друга запятой, например: procedure Register; begin RegisterComponents('Samples', [TButtonBeep,TButtonColor,TMyEdit]); end; Продолжим решение поставленной задачи - создание кнопки, которая издает писк. Поступим сначала тривиально (но как выяснится потом, неверно) - назначим обработчик события OnClick в конструкторе кнопки. Для этого в секции private определим заголовок нового метода BtClick(Sender:TObject) и реализуем его в секции реализации: procedure TButtonBeep.BtClick(Sender:TObject); begin Beep; end; Далее перепишем конструктор кнопки. Для этого определим в секции public заголовок конструктора: 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; Можно убедиться, что кнопка при нажатии издает писк. Кроме того, при назначении обработчика событий в инспекторе объектов этот обработчик выполняется и писк не исчезает! Компонент реализован корректно. На данном примере полезно проанализировать возможные ошибки при написании кода:
Теперь поменяем пиктограмму компонента 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; Далее определяем переменную этого типа в секции private: 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[1]>='A') and (EditText[1]<='Z'); end; Код прост для понимания, единственным его нюансом является проверка того, что текст представляет собой не пустую строку, перед проверкой первой буквы текста в обработчике события ListAdd2Filter. Проведение такой проверки обязательно: строки в Object Pascal ‑ это объекты, и пустой строке соответствует nil-указатель. При попытке проверить первую букву пустой строки приложение попытается дереференсировать nil, что приведет к возникновению исключения. В данном случае это не страшно: перед вызовом обработчика событий FOnFilter из компонента TListAdd проверяется строка на ненулевую длину. Однако для компонентов, исходный текст которых вам недоступен, такая проверка является обязательной! Скрытие свойств в инспекторе объектов Предположим, вы делаете компонент для доступа к данным, например, потомок класса TTable. Допустим, в этом компоненте анализируется список таблиц, имеющихся в базе данных, и по каким-либо признакам (например, наличие поля определенного типа и с определенным именем) выбирается одна для работы. Для нормальной работы компонента имя этой таблицы должно заноситься в свойство TableName. Но это свойство отображается в инспекторе объектов! Программист, использующий этот компонент, может изменить его значение на этапе разработки, что, предположим, сделает компонент неработоспособным. И он будет прав! Если какие-то из свойств или событий нельзя изменять, они должны быть скрыты. Мы продолжим работу над компонентом TListAdd и в качестве модельной задачи уберем из инспектора объектов свойство Cursor. Это свойство определено в секции published в классе TСontrol и отображается в инспекторе объектов для TListAdd с самого начала разработки компонента. Исходя из этого можно попытаться переопределить данное свойство в секции protected. Компилятор разрешит такое переопределение, но к желаемому результату это не приведет: свойство Cursor как было, так и останется в инспекторе объектов… Любое свойство, будучи однажды определенным в секции published, будет всегда отображаться в инспекторе объектов для всех потомков данного класса. Для скрытия свойства из инспектора объектов используем две возможности компилятора Delphi, а именно:
Перед началом работы по скрытию свойства Cursor полезно удалить компоненты TListAdd с формы, иначе может произойти исключение при чтении ресурса формы. Итак, в секции private объявляем переменную FDummy:integer (имя и тип переменной могут быть любыми) и в секции published определяем новое свойство: property Cursor:integer read FDummy; Новое свойство обязано называться Cursor, тип его обязан совпадать с типом переменной, определенной выше, свойство должно быть только на чтение или только на запись. После компиляции компонента с помощью редактора пакетов следует вновь поместить компонент TListAdd на форму. Можно обнаружить, что свойство Cursor уже не отображается в инспекторе объектов. Теперь немного усложним задачу. Предположим, необходимо, чтобы курсор был показан не в виде стрелки, а в виде песочных часов (crHourGlass). Для того чтобы изменить значение свойств по умолчанию, новое значение необходимо присвоить переменной в конструкторе. При попытке в конструкторе присвоить новое значение Cursor Cursor:=crHourGlass; компилятор Delphi выдаст диагностическое сообщение о том, что нельзя назначить новое значение переменной, предназначенной только для чтения. Если сделать новое свойство «только для записи», то компилятор выдаст уже другое диагностическое сообщение - о несопоставимых типах данных. Если же объявить переменную FDummy:TCursor и сделать ее доступной только для записи, то компилятор разрешит данное присвоение, но при этом вид курсора не изменится: он по-прежнему будет стрелкой. Тривиальное решение данной проблемы - объявить класс-потомок TCustomPanel, в конструкторе которого нужно присвоить новое значение переменной Cursor, а от него уже производить наш компонент TListAdd. У такого решения имеется два недостатка:
Поэтому решение данной задачи выглядит следующим образом: в конструкторе 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 и др. Механизм работы редакторов свойств заключается в следующем:
Методы 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[1..7] of string = ('Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'); DayWeekEn:array[1..7] of string = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'); function TDWPropED.GetValue:string; begin Result:=DayWeek[GetOrdValue]; end; procedure TDWPropED.SetValue(const Value:string); var I,N:integer; begin I:=1; {Checking if programmer has entered name of the day of week with national language} while (ANSICompareText(DayWeek[I],Value)<>0) and (I<8) do Inc(I); {Checking if programmer has entered name of the day of week with English language} if I>7 then begin I:=1; while (ANSICompareText(DayWeek[I],Value)<>0) and (I<8) do Inc(I); end; if I<8 then begin SetOrdValue(I); Exit; end; {Checking if programmer has entered order of the day of week} Val(Value,N,I); if (N>0) and (N<=7) and (I=0) then begin SetOrdValue(N); Exit; end; {Inform Delphi, that bad value was entered so that restore previous value} raise Exception.Create(Format('Bad day of week %s',[Value])); end; Внутри редактора свойств TPropertyEditor определен метод GetOrdValue, который извлекает текущее значение свойства из компонента, если оно может быть описано как ординарный тип (то есть тип, позволяющий упорядочивать переменные, к нему относящиеся). В методе GetValue вызывается этот метод, но в качестве строки возвращается название дня недели, извлеченное из массива. Это название и попадает в инспектор объектов. Реализация метода SetValue выглядит сложнее, поскольку поставленная задача достаточно сложна. Первоначально проверяется, действительно ли новый текст, набранный в инспекторе объектов и передаваемый в данный метод через параметр Value, представляет собой название дня недели. Поскольку название дня недели вводится национальным алфавитом, для сравнения строк используется метод ANSICompareText. Этот метод сравнивает строки без учета прописных и строчных букв, причем для конвертации строчных букв в прописные используется текущий языковой драйвер. Затем, если Value не обнаружено в массиве русских названий дней недели, поиск осуществляется по английским названиям. Если название совпало, то вызывается метод SetOrdValue класса TPropertyEditor. Этот метод позволяет изменить текущее значение ординарного свойства в компоненте. Если же текстовое название дня недели не было найдено ни в одном из языков, то проверяется, был ли введен порядковый номер дня недели, который может быть целым числом в диапазоне от 1 до 7. Для этого используется метод val, который пытается конвертировать строку в целое (или действительное) число и возвращает код ошибки. Если же значение параметра Value не совпадает ни с одним из предопределенных значений, значит программист ошибся при вводе нового значения свойства. Об этом необходимо информировать среду разработки, что осуществляется генерацией исключения. Созданный таким образом редактор свойств останется неработоспособным, пока не будет зарегистрирован в среде разработки. Как уже говорилось, это достигается при помощи метода RegisterPropertyEditor, который обязана вызвать среда разработки Delphi. Метод, вызываемый средой разработки, - процедура Register при регистрации компонента. Поэтому в модуле DayStore (где реализован компонент TDayStore) в секции implementation ссылаемся на модули DsgnIntf и DayPropE. Обратите внимание, что ссылка должна находиться в секции implementation - нам скоро потребуется циклическая ссылка! И теперь добавляем один метод к процедуре Register в модуле DayStore: procedure Register; begin RegisterComponents('Samples', [TDayStore]); RegisterPropertyEditor(TypeInfo(TDayWeek),TDayStore,'',TDWPropEd); end; Смысл параметров метода RegisterPropertyEditor был объяснен ранее. Теперь, поскольку мы изменили метод Register, необходимо повторно инсталлировать компонент: удалить его в редакторе пакета и перекомпилировать пакет, затем вновь добавить и снова перекомпилировать. Подробно о повторной регистрации было рассказано в разделе « Создание простейшего компонента» . Теперь можно тестировать редактор свойств. Для этого помещаем компонент TDayStore на форму и выделяем его. Сразу же видно отличие свойства DayWeek от целочисленных свойств: вместо числа в инспекторе объектов отображается название дня недели. Тестирование редактора свойств заключается в задании в инспекторе объектов нового значения свойства на русском и английском языках, а также порядкового номера дня недели. Ну и конечно, надо задавать неподходящие значения для свойства. Теперь немного модифицируем редактор свойств так, чтобы он мог показывать диалог с названиями дней недели для выбора. Если редактор свойств использует диалог, то в инспекторе объектов, при выделении соответствующего свойства, справа появляется кнопка с тремя точками (elliptic button). При нажатии этой кнопки выполняется диалог. Для информирования среды разработки о необходимости редактировать свойства специальным образом следует переписать метод GetAttributes. Кроме того, для диалога необходимо переписать метод Edit. Он будет вызываться, когда программист нажмет кнопку с тремя точками. Итак, в секции public класса TDWPropED определяем заголовки методов: function GetAttributes:TPropertyAttributes; override; procedure Edit; override; и в секции реализации реализуем метод GetAttributes: function TDWPropEd.GetAttributes:TPropertyAttributes; begin Result:=[paDialog]; end; Возвращаемый результат - набор флагов, которые информируют среду разработки о том, каким способом редактировать данное свойство. Компилятор пропускает любое сочетание флагов, но не любое сочетание является работоспособным. Например, если выставить одновременно флаги, что требует и список, и диалог, то в инспекторе объектов кнопка, которая заставляет выпадать список, спрячет кнопку диалога и добраться до нее (и, следовательно, вызвать диалог) будет невозможно. Возможные значения флагов и описание методов, которые необходимо переписывать, приведены ниже: PaValueList - редактор свойств использует выпадающий список, где предлагаются для выбора значения свойства. При этом рядом со свойством появляется кнопка - «стрелка вниз». Необходимо переписать метод GetValues, в котором даются все возможные значения свойств. PaSortList - то же, что и предыдущее, но выпадающий список будет отсортирован. PaSubProperties - используется для редакции записей, в частности классов. Слева от имени свойства появляется знак «+», при нажатии на который разворачивается список внутренних свойств. Необходимо переписать метод GetProperties, в котором перечисляются все внутренние свойства. PaDialog - появляется кнопка с тремя точками, при нажатии на которую возникает диалог редактора свойств. Реализовывать диалог следует в методе Edit, который необходимо переписать. PaMultiSelect - список, из которого можно выбрать несколько значений (флагов). Переписывается метод GetValues. PaAutoUpdate - информация о том, что метод SetValue необходимо вызывать автоматически каждый раз, когда происходит изменение данного свойства в инспекторе объектов. Пример - редактор заголовка форм (свойство Caption). Методы переписывать не надо. PaReadOnly - можно смотреть сколько угодно на значение свойства в инспекторе объектов, но изменять его нельзя. Для реализации метода Edit поставим на форму в модуле DayPropE.pas компонент TListBox и в его свойстве Items определим дни недели. Также поставим две кнопки TBitBtn, свойство Kind одной определим как bkOK, а другой - bkCancel. Далее реализуем метод Edit: procedure TDWPropEd.Edit; var F2:TForm2; begin F2:=nil; try F2:=TForm2.Create(nil); F2.ListBox1.ItemIndex:=GetOrdValue-1; {Index in the ListBox is started from zero} if F2.ShowModal=mrOK then SetOrdValue(F2.ListBox1.ItemIndex+1); finally if Assigned(F2) then F2.Release; end; end; Форма создается с nil-владельцем, поэтому для гарантированного вызова деструктора ее создание и разрушение помещены в защищенный блок. Нумерация массива в TListBox начинается с нуля, поэтому используются операторы +1 и -1 при чтении/записи в TListBox. Остальная часть кода тривиальна. После изменений необходимо заново регистрировать компонент в палитре, чтобы заново была выполнена процедура RegisterPropertyEditor. Как это сделать, было описано ранее. После любых изменений в исходных кодах редактора свойств необходима повторная регистрация для тестирования. Теперь можно поместить на форму компонент TDayStore и убедиться, что при выборе свойства DayWeek в инспекторе объектов появляется кнопка с тремя точками, нажав на которую можно вызвать созданный нами диалог для редакции свойства. Аналогично можно использовать другие возможности среды разработки. Ниже приведена реализация методов GetAttributes и GetValues для редактирования дней недели в виде выпадающего списка: function TDWPropEd.GetAttributes:TPropertyAttributes; begin Result:=[paValueList]; end; procedure TDWPropEd.GetValues(Proc:TGetStrProc); var I:integer; begin for I:=1 to 7 do Proc(DayWeek[I]); end; Напоминаем, что компонент необходимо перерегистрировать для тестирования изменений в редакторе свойств. Редакторы компонентов Редактор свойств используется средой разработки для изменения значения свойства в инспекторе объектов. Редактор же компонента используется для изменения состояния компонента, минуя инспектор объектов. Он вызывается при щелчке правой кнопки мыши над компонентом, поставленным на форму. При этом появляется всплывающее меню, и если для данного компонента имеется редактор, то он вставляет в это меню свои команды. Редактор компонента является потомком класса TComponentEditor. Для того чтобы в всплывающем меню компонента были вставлены новые команды, необходимо переписать два метода TComponentEditor: TDWComponentEditor=class(TComponentEditor) public function GetVerbCount:integer; override; function GetVerb(Index:integer):string; override; end; Сами названия этих методов позволяют догадаться, как меню, определенное программистом в редакторе компонента, вставляется во всплывающее меню компонента. Среда разработки вызывает метод GetVerbCount, и если возвращается ненулевое значение, то вызывает несколько раз метод GetVerb с соответствующим индексом в качестве параметра. Реализация методов в этом случае выглядит следующим образом: function TDWComponentEditor.GetVerbCount:integer; begin Result:=3; end; function TDWComponentEditor.GetVerb(Index:integer):string; begin case Index of 0:Result:='Copyright (C) 2001'; 1:Result:='by me'; 2:Result:='&Edit...'; end; end; Как и редактор свойств, редактор компонента нуждается в регистрации. Для этого используется метод RegisterComponentEditor, вызываемый средой разработки, при этом его вызов следует поместить в процедуру Register модуля DayStore.pas: RegisterComponentEditor(TDayStore,TDWComponentEditor); Метод зависит от двух параметров: класса компонента и класса редактора компонента. Класс компонента означает, что данный редактор компонента будет вызываться для компонента главного класса и всех его потомков. В качестве шутливого упражнения можно попробовать зарегистрировать редактор компонента с классом компонента TComponent вместо TDayStore. Теперь, какой бы компонент вы ни поместили на форму, при нажатии правой кнопки мыши верхние команды всплывающего меню будут говорить о том, что данный компонент принадлежит вам. Если для одного из потомков класса, который имеет редактор компонента, регистрируется новый редактор компонента, то старый редактор отменяется и больше не вызывается для данного класса и его потомков. Напоминаем, что после изменения в процедуре Register компонент обязан быть переинсталлирован. Сколько бы мы ни нажимали на соответствующие строки во всплывающем меню, ничего происходить не будет. Этого и следовало ожидать, поскольку в данной реализации редактора компонента нигде не описано, что необходимо делать при вызове соответствующего элемента меню. Это достигается переписыванием метода ExecuteVerb(Index:integer); Из его объявления становится ясно, что среда разработки вызывает его, когда программист выбрал какой-либо элемент меню. Индекс выбранного элемента передается в качестве параметра этого метода. Но перед тем, как реализовать данный метода необходимо поставить задачу. В качестве простой задачи можно выбрать следующую: редакция свойства дня недели через редактор компонента - вызов диалога. Однако реализовать вызов диалога редакции дня недели из редактора компонента сложнее, чем из редактора свойств. Проблема заключается в том, что в редакторе компонента отсутствуют методы GetOrdValue и SetOrdValue. Зато в редакторе компонента имеется свойство Component:TComponent, которое содержит ссылку на экземпляр компонента в среде разработки, для которого было вызвано меню. Им и следует воспользоваться для редакции свойства. Проблема того, что свойство DayWeek не реализовано на уровне TComponent, решается явным приведением типов. Поэтому в секции implementation модуля DayPropE.pas помещаем оператор uses DayStore. Теперь оба модуля циклически ссылаются друг на друга, но иначе привести тип TComponent к типу TdayStore невозможно. procedure TDWComponentEditor.ExecuteVerb(Index:integer); var F2:TForm2; begin if Index=2 then begin F2:=nil; try F2:=TForm2.Create(nil); F2.ListBox1.ItemIndex:=TDayStore(Component).DayWeek-1; if F2.ShowModal=mrOK then begin TDayStore(Component).DayWeek:=F2.ListBox1.ItemIndex+1; Designer.Modified; end; finally if Assigned(F2) then F2.Release; end; end else ShowMessage('E-mail: aaa@bbb.cc.ru'); end; Обратите внимание на обращение к свойству Designer.Modified. Вызовом этого метода мы информируем среду разработки о том, что свойства компонента изменились. Это заставляет среду разработки выполнить ряд действий: обновить содержимое инспектора объектов, сделать активным меню Save. Еще один полезный метод, который можно переписать в редакторе компонента, - метод Edit. Он вызывается при двойном щелчке мыши на компоненте на этапе разработки. Если его не переписывать, а возвращаемое значение метода GetVerbCount при этом больше нуля, то выполняется метод с нулевым индексом, то есть вызывается команда ExecuteVerb(0). В следующем разделе мы перепишем этот метод. Класс TFiler и сохранение данных в ресурсах При редакции значений свойств в инспекторе объектов новые значения сохраняются в ресурсах проекта. Соответственно при запуске приложения ресурсы загружаются после создания компонентов на форме. Среда разработки Delphi запоминает в ресурсах только данные, отображаемые в инспекторе объектов. Предположим, имеется свойство, которое никаким способом невозможно представить в инспекторе объектов в виде строки. Такого типа свойства встречаются довольно часто - данные в двоичном формате. Их можно редактировать на этапе разработки при помощи, например, редактора компонента с диалогом загрузки файла. Однако эти данные не будут сохранены в ресурсах проекта и, следовательно, загружены во время приложения. Выход из данной ситуации заключается в переписывании метода DefineProperties(Filer: TFiler), который определен на уровне TComponent. TFiler заведует сохранением в ресурсах свойств, редактируемых в инспекторе объектов. По умолчанию в ресурсы попадают все опубликованные свойства компонента. TFiler имеет методы DefineProperty и DefineBinaryProperty, используя которые, можно поместить в ресурсы неопубликованные данные. Определим задачу следующим образом. Будем помещать в ресурсы неопубликованные двоичные данные (помещать в ресурсы остальные данные неинтересно, их можно вывести в инспектор объектов). Чтобы не связываться с какими-либо форматами и упрощением ввода и редакции данных, в качестве модели двоичных данных будем рассматривать содержимое единственной строки. Строку будем редактировать в методе Edit редактора компонента. Перед началом работы необходимо убрать все компоненты TDayStore с формы проекта. Компиляцию далее следует производить только из редактора пакета. Итак, в компоненте - классе TDayStore в секции private определим переменную FData:string, а в секции public (не published!) определим свойство: Data:string read FData write FData. Затем в секции protected определим заголовок переписываемого метода: DefineProperties(Filer :TFiler); override; При реализации этого метода требуется вызвать метод TFiler - DefineBinaryProperties. В качестве параметров этот метод принимает ссылку на процедуру записи данных в поток и ссылку на процедуру считывания данных из потока. Их также необходимо реализовать в компоненте TDayStore, заголовки этих процедур следует поместить в секции private класса TDayStore. Заголовок TDayStore после внесения всех изменений выглядит следующим образом: TDayStore = class(TComponent) private FDay:TDayWeek; FData:string; procedure SaveToStream(Stream:TStream); procedure LoadFromStream(Stream:TStream); protected procedure DefineProperties(Filer:TFiler); override; public constructor Create(AOwner:TComponent); override; property Data:string read FData write FData; published property DayWeek:TDayWeek read FDay write FDay; end; В реализации переписанного метода DefineProperties в первую очередь следует вызвать метод DefineProperties класса TComponent (используя директиву Inherited). Без этого наш компонент станет вести себя некорректно: он все время будет находиться в левом верхнем углу формы. Метод DefineProperties класса TComponent записывает в ресурсы координаты левого верхнего угла пиктограммы - для невизуальных компонентов они не отображаются в инспекторе объектов: procedure TDayStore.DefineProperties(Filer:TFiler); begin inherited DefineProperties(Filer); Filer.DefineBinaryProperty('BinData',LoadFromStream, SaveToStream,length(FData)>0); end; Следующий оператор ‑ вызов метода TFiler DefineBinaryProperty. В качестве параметра он принимает имя нового свойства, которое не должно совпадать с именами уже имеющихся свойств, ссылку на процедуры чтения и записи двоичных данных, и последний параметр сообщает среде разработки, является содержимое пустым или нет. Если содержимое пустое, данные не запоминаются в ресурсах, а если нет - записываются в ресурсы. Если в компоненте имеется несколько различающихся двоичных типов данных, метод DefineBinaryProperties может быть вызван несколько раз с различными именами данных и разными процедурами чтения/записи данных. Реализация методов чтения и записи данных выглядит следующим образом: procedure TDayStore.SaveToStream(Stream:TStream); var L:longint; begin L:=length(FData); Stream.Write(L,sizeof(L)); if L>0 then Stream.Write(FData[1],L); end; procedure TDayStore.LoadFromStream(Stream:TStream); var L:longint; begin Stream.Read(L,sizeof(L)); SetLength(FData,L); if L>0 then Stream.Read(FData[1],L); end; Реализация метода SaveToStream очевидна. В методе LoadFromStream используется процедура SetLength, которая выделяет память строке заданной длины и делает строку уникальной - с числом ссылок, равным единице. Только после вызова метода SetLength строку можно использовать как буфер чтения данных, иначе может не хватить памяти или испортится содержимое других строк, которые ссылаются на ту же самую область памяти. Теперь необходимо переписать метод Edit в редакторе компонентов TDWComponentEditor так, чтобы мы могли менять содержимое свойства Data. Будем использовать метод InputQuery для показа старого и ввода нового значения свойства Data: procedure TDWComponentEditor.Edit; var S:string; begin S:=TDayStore(Component).Data; if InputQuery('New property','Data:',S) then begin TDayStore(Component).Data:=S; Designer.Modified; end; end; Обратите внимание, что не вызывается inherited-метод. В классе TComponentEditor метод Edit вызывает метод ExecuteVerb(0), что нам не требуется, поскольку у нас свой диалог! Компонент заново регистрируется, после чего можно приступать к тестированию. Создадим новый проект, поставим компонент TDayStore на форму. Дважды щелкнем по нему и наберем какую-либо строку. После этого сохраним проект, затем остановим и загрузим заново Delphi. Вновь откроем данный проект, дважды щелкнем по компоненту. Теперь диалог показывает строку, введенную в данный диалог ранее. Чтобы окончательно убедиться, что данные попали в ресурс проекта, можно щелкнуть правой клавишей мыши по форме и вызвать команду View as text: object Form1: TForm1 Left = 194 Top = 107 Width = 213 Height = 183 Caption = 'Form1' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13 object DayStore1: TDayStore DayWeek = 1 Left = 128 Top = 48 BinData = {0F0000005465737420646174612073746F7265} end end Итак, мы видим, что в ресурсах DayStore1 имеется свойство BinData. Из сказанного выше следует, что методика создания самих компонентов настолько тривиальна, что ее без труда может освоить даже начинающий программист. Выгода использования компонентов при сборке приложений очевидна: экономия времени, упрощение процесса отладки и внесения изменений. При этом созданное приложение должно подчиняться вполне определенным требованиям, но об этом мы поговорим в следующей статье данного цикла. Ссылки по теме
|
|