Text
                    ББК 32.973-018,2
УДК 681.3.06
К98
Кэнгу М.
К98 Delphi 7: Для профессионалов. — СПб.: Питер, 2004. — 1101 с.: ил.
ISBN 5-94723-593-5
Среда Delphi была и до сих пор является наилучшим сочетанием объектно-ориентированного
и визуального программирования не только для Windows, но теперь уже и для Linux, а в бли-
жайшем будущем — и для .NET.
В этой книге автор попытался практически полностью исключить справочный материал,
сконцентрировавшись на технологиях эффективного использования Delphi. В книге приведено
более 300 примеров. Как сказал одни из подписчиков групп новостей, «книги Кэнту — это по сути
„delphi.filtered", только больше и лучше».
Кинга предназначена для программистов, разработчиков и всех, серьезно интересующихся
программированием в среде Delphi.
ББК 32.973-018.2
УДК 681.3.06
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги.
© 2003 Sybex Inc.
ISBN 0-7821-4201-Х (англ.)	© Перевод на русский язык, ЗАО Издательский дом «Питер», 2004
ISBN 5-94723-593-5	© Издание на русском языке, оформление, ЗАО Издательский дом «Питер», 2004
0004

Краткое содержание Благодарности...................................................25 Введение........................................................27 Часть I. Основы.................................................31 Глава 1. Среда Delphi 7 и ее IDE..............................32 Глава 2. Язык программирования Delphi.........................78 Глава 3. Run-Time-библиотека.................................119 Глава 4. Классы базовой библиотеки...........................149 Глава 5. Визуальные элементы управления......................192 Глава 6. Создание интерфейса пользователя....................247 Глава 7. Работа с формами....................................299 Часть II. Объектно-ориентированные архитектуры Delphi...............345 Глава 8. Архитектура Delphi-приложений.......................346 Глава 9. Создание Del phi-компонентов........................390 Глава 10. Библиотеки и пакеты................................451 Глава 11. Моделирование и ООП-программирование...............485 Глава 12. От СОМ к COM4-.....................................513 Часть III. Механизмы работы с базами данных....................568 Глава 13. Встроенная в Delphi архитектура работы с базами данных..569 Глава 14. Клиент-серверная архитектура с использованием dbExpress.629 Глава 15. Технология ADO.....................................700 Глава 16. Многозвенные приложения DataSnap...................742 Глава 17. Разработка компонентов, работающих с данными.......770 Глава 18. Формирование отчетов с использованием Rave.........817 Часть IV. Delphi, Интернет и a.NET. Предварительный обзор ...843 Глава 19. Интернет-программирование: сокеты и Indy...........844 0005
Глава 20. Веб-программирование с использованием структур WebBroker и WebSnap.........................................873 Глава 21. Веб-программирование с использованием IntraWeb.....916 Глава 22. Использование технологий XML.......................941 Глава 23. Веб-службы и SOAP..................................988 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi..1013 Глава 25. Обзор Delphi for .NET: язык и RTL.................1037 Приложение А. Дополнительные инструменты для Delphi, разработанные автором........................................1065 Приложение Б. Дополнительные инструменты для Delphi из других источников...................................................1071 Приложение В. Бесплатные сопутствующие книги о Delphi..........1074 Веб-узел автора книги..........................................1077 Исходный код примеров книги....................................1078 Алфавитный указатель...........................................1080 0006
Содержание Благодарности..........................................................25 От издательства.....................................................26 Введение...............................................................27 Семь версий.........................................................27 Структура книги.....................................................28 Свободно доступный исходный программный код в Сети..................29 Как найти автора....................................................30 Часть I. Основы........................................................31 Глава 1. Среда Delphi 7 и ее IDE.......................................32 Различные версии Delphi.............................................32 Обзор IDE...........................................................33 IDE для двух библиотек...........................................34 Настройки рабочего стола.........................................35 Параметры окружения..............................................36 О меню...........................................................36 Диалоговое окно Environment Options..............................37 Список To-Do.....................................................37 Расширенные сообщения компилятора и результаты поиска в Delphi 7.39 Редактор Delphi.....................................................40 Code Explorer....................................................42 Поиск в редакторе................................................43 Завершение классов...............................................44 Code Insight.....................................................45 Дополнительные «горячие» клавиши редактора.......................47 Загружаемые виды.................................................48 Конструктор форм....................................................50 Object Inspector.................................................52 Object TreeView..................................................54 Секреты палитры компонентов.........................................56 Копирование и вставка компонентов................................57 От шаблонов компонентов к фреймам................................58 Управление проектами................................................60 Project Options..................................................62 Компиляция и компоновка проектов.................................63 Изучение классов проекта.........................................66 0007
А конвертирование валют?............................................ 139 Управление файлами с помощью Syslltils.............................. 142 Класс TObject....................................................... 144 Вывод сведений о классе.......................................... 146 Что далее?.................................................... 147 Глава 4. Классы базовой библиотеки......................................149 RTL-пакет, CLX и VCL................................................ 150 Традиционные разделы VCL......................................... 150 Структура CLX.................................................... 151 Разделы библиотеки, характерные для VCL.......................... 152 Класс TPersistent................................................... 153 Ключевое слово published....................................... 155 Обращение к свойствам по имени................................... 156 Класс TComponent.................................................... 158 Принадлежность................................................... 159 Свойство Name.................................................... 162 Удаление полей формы............................................. 163 Сокрытие полей форм.............................................. 164 Настраиваемое свойство Тад....................................... 165 События ..........:............................................... 166 События в Delphi................................................. 166 Указатели методов................................................ 167 События — это свойства........................................... 168 Списки и контейнеры классов......................................... 170 Списки и списки строк.......................................... 170 Коллекции........................................................ 173 Классы-контейнеры................................................ 173 Контейнеры и списки, обеспечивающие сохранность типа............. 176 Поточная система.................................................... 178 Класс TStream.................................................... 178 Специальные классы-потоки........................................ 180 Использование файловых потоков................................... 180 Классы TReader и TWriter......................................... 182 Потоки и длительное хранение..................................... 182 Сжатие потоков с помощью ZLib.................................... 187 Итог сравнения модулей Core VCL и BaseCLX........................... 189 Модуль Classes................................................... 189 Другие стержневые модули......................................... 190 Что далее?.......................................................... 191 Глава 5. Визуальные элементы управления.................................192 VCL по сравнению с VisualCLX........................................ 192 Двойная поддержка библиотек в Delphi............................. 195 Выбор визуальной библиотеки...................................... 198 Конвертирование существующих приложений...........................201 TControl и производные классы........................................202 Parent и элементы управления.................................... 203 Свойства положения и размера......................................203 Свойства Activation и Visibility..................................204 Шрифты........................................................... 204 Цвета.............................................................205 0008
Дополнительные и внешние средства Delphi..............................67 Файлы, создаваемые системой...........................................68 Просматривая файлы с исходным кодом................................72 Object Repository.....................................................73 Новшества в отладчике Delphi 7........................................76 Что далее?............................................................77 Глава 2. Язык программирования Delphi....................................78 Базовые характеристики языка..........................................79 Классы и объекты......................................................79 Дополнительные сведения о методах..................................81 Динамическое создание компонентов..................................82 Инкапсуляция..........................................................83 Private, Protected и Public........................................84 Инкапсуляция со свойствами.........................................85 Инкапсуляция и формы...............................................88 Конструкторы................................ .................. 90 Деструкторы и метод Free...........................................91 Модель объектных ссылок Delphi........................................92 Присвоение объектов................................................92 Объекты и память...................................................94 Наследование существующих типов.......................................95 Защищенные поля и инкапсуляция.....................................97 Наследование и совместимость типов.................................99 Позднее связывание и полиморфизм.................................... 100 Подмена и переопределение методов................................ 101 Виртуальные методы против динамических........................... 103 Абстрактные методы............................................... 104 Безопасное приведение типов......................................... 105 Использование интерфейсов........................................... 106 Работа с исключениями............................................... 109 Поток программы и блок finally................................... 110 Классы исключений................................................ 111 Протоколирование ошибок............................................. 113 Ссылки класса..................................................... 115 Создание компонентов с помощью ссылок классов.................... 116 Что далее?.......................................................... 117 Глава 3. Run-Time-библиотека.......................................... 119 Модули RTL.......................................................... 119 Модули System и Syslnit.......................................... 122 Модули Syslltils и SysConst...................................... 123 Модуль Math...................................................... 127 Модули Convlltils и StdConvs..................................... 130 Модуль Datelltils................................................ 130 Модуль StrUtils.................................................. 130 От Pos к PosEx................................................... 132 Модуль Types..................................................... 132 Модули Variants и VarUtils....................................... 133 Модули DelphiMM и ShareMem....................................... 135 СОМ-модули....................................................... 135 Преобразование данных................................................135 0009
10 Содержание Класс TWinControl (VCL)...........................................206 Класс TWidgetControl (CLX)........................................206 Набор компонентов.....................................................207 Компоненты ввода текста...........................................208 Выбор параметров..................................................211 Списки............................................................212 Диапазоны.........................................................217 Команды...........................................................219 Технологии, связанные с элементами управления.........................222 Обработка фокуса ввода............................................222 Привязка элементов управления.....................................225 Использование компонента Splitter.................................226 Клавиши быстрого вызова...........................................228 Использование всплывающих подсказок...............................229 Собственные элементы управления и стили...........................231 Элементы управления Listview и TreeView...............................235 Графический список ссылок.........................................236 Дерево данных.....................................................240 Настройка узлов дерева............................................244 Что далее?............................................................246 Глава 6. Создание интерфейса пользователя.............................247 Многостраничные формы.................................................248 Компоненты PageControl и TabSheet.....................................248 Средство просмотра изображений Image Viewer с «собственными» вкладками.........................................................253 Пользовательский интерфейс мастера................................256 Элемент управления ToolBar............................................259 Пример RichBar....................................................260 Меню и поле со списком в панели инструментов......................261 Простая строка состояния..........................................262 Темы и стили..........................................................26S Стили CLX.........................................................265 Темы Windows ХР...................................................266 Компонент ActionList..................................................268 Предопределенные действия в Delphi................................270 Практическое применение действий..................................271 Панель инструментов и компонент ActionList редактора..............275 Контейнеры панели инструментов........................................277 ControlBar........................................................278 Поддержка стыковки в Delphi.......................................281 Стыкуемые панели инструментов в ControlBars.......................282 Стыковка к PageControl............................................286 Архитектура ActionManager.............................................288 Создание простой демо-версии......................................289 Редко используемые пункты меню....................................293 Перенос существующей программы....................................294 Использование действий списка.....................................295 Что далее?............................................................298 Глава 7. Работа с формами................................................299 Класс TForm...........................................................299 0010
Содержание 11 Использование простых форм......................................300 Стиль формы.....................................................302 Стиль границы...................................................302 Значки границы..................................................304 Установка некоторых стилей окна.................................306 Прямой ввод данных в форму.........................................307 Контроль ввода с клавиатуры.....................................307 Ввод с помощью мыши.............................................309 Перетягивание и рисование с помощью мыши........................310 Рисование в формах.................................................314 Необычные методы: Alpha Blending, Color Key и Animate API..........316 Положение, размер, прокрутка и масштабирование.....................317 Положение формы.................................................317 Пристыковывание к экрану (в Delphi 7)...........................318 Размер формы и ее клиентская область............................318 Ограничения форм................................................319 Прокрутка формы.................................................320 Масштабирование форм............................................324 Автоматическое масштабирование формы............................326 Создание и закрытие форм...........................................327 События создания формы..........................................328 Закрытие формы..................................................329 Диалоговые окна и другие вторичные формы...........................330 Добавление в программу второй формы.............................331 Создание вторичной формы во время выполнения....................331 Создание одного экземпляра вторичной формы......................332 Создание диалогового окна..........................................333 Диалоговое окно примера RefList.................................334 Немодальное диалоговое окно................................... 336 Предопределенные диалоговые окна...................................338 Общие диалоги Windows...........................................338 Окна сообщений..................................................340 Окна About и окна-заставки.........................................341 Создание окна-заставки..........................................341 Что далее?.........................................................344 Часть II. Объектно-ориентированные архитектуры Delphi.............................................................345 Глава 8. Архитектура Delphi-приложений................................346 Объект Application............................................... 34б Вывод окна приложения...........................................348 Активизация приложений и форм...................................349 Отслеживание форм с объектом Screen.............................3S0 От событий к потокам...............................................3S3 Программирование на основе событий..............................3S3 Доставка Windows-сообщений......................................3SS Фоновая обработка и многозадачность.............................3SS Многопоточность Delphi..........................................3S6 Проверка существования экземпляра приложения.......................3S8 Поиск копии главного окна.......................................3S9 Использование мьютексов.....................................,....3S9 0011
12 Содержание Поиск в списке окон...............................................360 Обработка определяемых пользователем Windows-сообщений............361 Создание MDI-приложений..............................................362 MDI в Windows: технический обзор..................................362 Фрейм и дочерние окна в Delphi.......................................363 Построение полного меню Window....................................364 Пример MdiDemo....................................................365 MDI-лриложения с различными дочерними окнами.........................367 Дочерние формы и слияние меню.....................................367 Главная форма.....................................................368 Перехват сообщений, посланных клиентскому MDI-окну................369 Визуальное наследование форм.........................................371 Наследование от базовой формы.....................................371 Полиморфные формы.................................................374 Понятие фреймов......................................................376 Фреймы и страницы.................................................379 Множество фреймов без страниц.....................................381 Базовые формы и экземпляры...........................................383 Использование класса базовой формы................................383 Использование интерфейсов.........................................386 Диспетчер памяти Delphi..............................................387 Что далее?...........................................................389 Глава 9. Создание Delphi-компонентов....................................390 Расширение библиотеки Delphi.........................................390 Пакеты компонентов................................................391 Правила создания компонентов......................................392 Базовые классы компонентов........................................393 Создание вашего первого компонента...................................394 Fonts Combo Box...................................................394 Создание пакета...................................................398 Использование Font Combo Box......................................401 Значки в палитре компонентов......................................402 Создание составных компонентов.......................................403 Внутренние компоненты.............................................403 Публикация подкомпонентов.........................................404 Внешние компоненты................................................406 Обращение к компонентам с помощью интерфейсов.....................409 Сложный графический компонент........................................412 Определение перечисляемого свойства...............................413 Написание метода Paint............................................415 Добавление свойств TPersistent....................................416 Определение события...............................................418 Регистрация категорий свойств.....................................420 Настройка элементов управления Windows...............................422 Numeric Edit Box..................................................423 Кнопка Sound......................................................425 Обработка внутренних сообщений: Active Button.....................426 Сообщения и уведомления компонента................................428 Диалоговое окно в компоненте.........................................432 Использование невизуапьного компонента............................435 Свойства коллекций...................................................436 0012
Содержание 13 Определение действий................................................439 Создание редакторов свойств........................................ 442 Редактор для свойства Sound......................................442 Установка редактора свойств......................................445 Создание редактора компонента.......................................446 Использование в качестве основы класса TComponentEditor..........447 Редактор компонента для ListDialog...............................447 Регистрация редактора компонента.................................449 Что далее?..........................................................449 Глава 10. Библиотеки и пакеты..........................................451 Роль DLL в Windows..................................................451 Что такое динамическое связывание?...............................4S2 Для чего нужны DLL?..............................................453 Правила для разработчиков DLL Delphi.............................4S4 Использование существующих DLL......................................454 Использование DLL языка C++......................................45S Создание DLL в Delphi...............................................457 Ваша первая Delphi-DLL...........................................458 Вызов Delphi-DLL.................................................461 Дополнительные особенное™ Delphi-DLL................................462 Изменение имени проекта и библиотек..............................462 Вызов DLL-функций во время выполнения............................464 Помещение форм Delphi в библиотеку...............................466 Библиотеки в памяти: код и данные...................................467 Совместный доступ к данным с помощью отображаемого на память файла... 469 Использование пакетов Delphi........................................471 Контроль версий пакетов..........................................472 Формы в пакетах.....................................................474 Загрузка пакетов во время выполнения.............................476 Использование интерфейсов в пакетах..............................477 Структура пакета....................................................480 Что далее?..........................................................484 Глава 11. Моделирование и ООП-программирование........................485 Понятие внутренней модели ModelMaker................................486 Моделирование и URL.................................................487 Схемы классов....................................................487 Схемы последовательное™ действий.................................489 Использование CASE- и других схем................................490 Прочие схемы.....................................................491 Общие элементы схем..............................................492 Особенности составления программного кода в ModelMaker..............493 Интеграция Delphi/ModelMaker.....................................494 Управление моделью кода..........................................496 Редактор Unit Code Editor........................................497 Редактор Method Implementation Code Editor.......................499 Представление Difference........................................ 500 Event Types View.................................................SOI Документация и макросы..............................................501 Документация по сравнению с комментариями....................... 502 Работа с макросами.............................................. 503 Регенерация программного кода.......................................50z 0013
14 Содержание Применение шаблонов дизайна......................................506 Шаблоны программного кода........................................509 Малоизвестные изюминки..............................................511 Что далее?..........................................................512 Глава 12. От СОМ к СОМ+................................................513 Краткая история OLE и СОМ...........................................514 Реализация IUnknown.................................................515 Глобально уникальные идентификаторы..............................517 Роль «фабрик классов»............................................518 Первый СОМ-сервер...................................................519 COM-интерфейсы и СОМ-объекты.....................................520 Инициализация СОМ-объекта........................................523 Тестирование СОМ-сервера.........................................523 Использование свойств интерфейса.................................525 Вызов виртуальных методов........................................526 OLE-автоматизация...................................................526 Координация вызова автоматизации.................................528 Создание сервера автоматизации...,........................... .... 530 Редактор библиотеки типов........................................531 Серверный программный код........................................532 Регистрация сервера автоматизации................................534 Написание клиентской части.......................................535 Границы действия объектов автоматизации..........................537 Сервер в компоненте..............................................538 Типы данных СОМ..................................................539 Использование программ пакета Office.............................540 Использование составных документов..................................541 Контейнерный компонент...........................................542 Использование внутреннего объекта................................544 Введение в элементы управления ActiveX..............................545 Элементы управления ActiveX и Delphi-компоненты..................547 Использование элементов управления ActiveX в Delphi..............547 Создание элементов управления ActiveX...............................549 Построение элемента управления ActiveX Arrow.....................550 Добавление новых свойств.........................................551 Добавление страницы свойств......................................552 ActiveForms.................................................... 554 ActiveX на веб-страницах.........................................556 Введение в СОМ+.....................................................557 Создание компонента СОМ+........................................ 558 Модули данных транзакций.........................................560 СОМ+ события.....................................................562 СОМ и .NET в Delphi 7...............................................565 Что далее?..........................................................567 Часть Ш. Механизмы работы с базами данных..............................568 Глава 13. Встроенная в Delphi архитектура работы с базами данных......................................................569 Доступ к базе данных: dbExpress, локальные данные и другие альтернативы.570 Библиотека dbExpress (DBX)...................................... 570 Rnriand Database Enaine (BDE)................................... 571 0014
Содержание 15 InterBase Express (IBX).......................................... 572 MyBase и компонент ClientDataSet..................................573 dbGo для ADO......................................................573 Собственные компоненты наборов данных.............................574 MyBase: автономный компонент ClientDataSet...........................575 Подключение к существующей локальной базе данных..................575 От динамической библиотеки Midas к модулю MidasLib................577 Форматы XML и CDS.................................................577 Создание новой локальной таблицы..................................578 Индексация....................................................... 579 Фильтрация........................................................580 Переход к заданной записи.........................................581 Отмена и точка восстановления.....................................581 Включение и выключение журнала....................................582 Использование элементов управления, работающих с данными.............583 Данные в компоненте DBGrid........................................584 DBNavigator и действия, связанные с набором данных................584 Текстовые элементы управления работы с данными....................585 Элементы управления, основанные на списке.........................585 Пример DbAware....................................................586 Использование элементов сопоставления значений из нескольких таблиц.... 587 Графические элементы управления для работы с данными..............589 Компонент DataSet....................................................589 Состояние компонента DataSet......................................593 Поля набора данных...................................................594 Использование объектов полей......................................597 Иерархия классов, производных от TField...........................598 Добавление вычисляемого поля......................................600 Сопоставляемые поля...............................................603 Обработка значений Null с использованием событий объектов полей...606 Навигация внутри набора данных.......................................608 Итоговая сумма значений столбца таблицы...........................609 Использование закладок............................................610 Редактирование столбца таблицы....................................612 Настройка элемента DBGrid............................................612 Рисование сетки DBGrid............................................613 Поддержка множественного выделения................................615 Перетаскивание в сетку.......................................... 617 Работа с базами данных с использованием стандартных элементов управления 617 Имитация стандартного поведения Delphi............................618 Пересылка запросов в базу данных................................. 620 Группировка и агрегатные значения....................................622 Группировка.......................................................622 Определение агрегатных значений...................................623 Инфраструктура Master/Detail.........................................625 Поддержка Master/Detail с использованием ClientDataSet............626 Обработка ошибок при работе с базой данных...........................627 Что далее?...........................................................628 Глава 14. Клиент-серверная архитектура с использованием dbExpress.............................................629 Архитектура «клиент-сервер»........................................ 630 Элементы дизайна базы данных........................................ 632 0015
16 Содержание Модель Entity-Relation............................................632 От первичных ключей к идентификаторам объектов OID................634 Дополнительные ограничения........................................637 Однонаправленные курсоры..........................................637 Знакомство с InterBase..............................................638 Использование IBConsole.............................................641 InterBase: программирование на стороне сервера......................643 Сохраненные процедуры (Stored Procedures).........................644 Триггеры (и генераторы)...........................................645 Библиотека dbExpress................................................646 Работа с однонаправленными курсорами..............................646 Платформы и базы данных...........................................648 Проблемы с версиями драйверов и встроенные модули.................648 Компоненты dbExpress................................................649 Компонент SQLConnection...........................................649 Компоненты наборов данных dbExpress...............................653 Компонент SQLMonitor................................................655 Несколько демонстрационных программ dbExpress.......................656 Использование одного компонента или нескольких компонентов........657 Доступ к метаданным базы данных с использованием SetSchemalnfo....660 Запрос с параметром...............................................662 Когда одного направления достаточно: распечатка данных..............663 Пакеты и кэш........................................................666 Манипулирование обновлениями......................................667 Обновление данных.................................................669 Использование транзакций..........................................672 Использование InterBase Express.....................................675 Наборы данных IBX.................................................676 Административные компоненты IBX...................................677 Построение примера IBX............................................678 Построение редактируемого запроса.................................679 Мониторинг функционирования InterBase Express.....................683 Получение дополнительных системных данных.........................684 Задачи из реального мира............................................684 Генераторы и ID...................................................685 Поиск текста вне зависимости от регистра символов.................687 Обработка информации об адресах и людях...........................689 Построение пользовательского интерфейса...........................691 Оплата учебных курсов.............................................693 Построение диалогового окна сопоставления значений................696 Форма с редактируемым SQL-запросом................................698 Что далее?..........................................................699 Глава 15. Технология ADO................................................700 MDAC (Microsoft Data Access Components).............................701 Провайдеры OLE DB.................................................702 Использование компонентов dbGo......................................704 Практический пример.............................................. 705 Компонент ADOConnection.......................................... 707 Файлы связи с данными (Data Link Files)...........................708 Динамические свойства ............................................708 Получение информации о схеме......................................709 0016
Содержание 17 Использование механизма Jet...........................................711 Доступ к Paradox через Jet.........................................711 Доступ к Excel через Jet...........................................713 Доступ к текстовым файлам через Jet................................714 Импорт и экспорт...................................................716 Работа с курсорами....................................................717 Положение курсора (свойство CursorLocation)........................717 Тип курсора (свойство CursorType)..................................718 Вы не всегда получаете то, о чем просите...........................720 Отсутствие счетчика.............................................. 721 Клиентские индексы.................................................721 Клонирование.......................................................722 Обработка транзакций..................................................724 Вложенные транзакции...............................................725 Атрибуты компонента ADOConnecbon...................................725 Типы блокировки....................................................726 Обновление данных.....................................................727 Пакетные обновления (Batch updates)................................729 Оптимистическая блокировка.........................................731 Разрешение конфликтов, связанных с обновлением данных..............734 Отключенные наборы записей............................................735 Накопление соединений (Connection Pooling).........................736 Сохранение набора записей в постоянной памяти (Persistent Recordset).738 Модель Briefcase (Портфель)........................................739 Пара слов o6ADO.NET............................................... 740 Что далее?............................................................740 Глава 16. Многозвенные приложения DataSnap..............................742 Одно, два и три звена в истории Delphi................................743 Технические основы DataSnap........................................745 Интерфейс lAppServer...............................................746 Протокол соединения............................................... 746 Пакеты данных..................................................... 748 Компоненты поддержки DataSnap (на стороне клиента).................749 Компоненты поддержки DataSnap (на стороне сервера).................750 Построение простого приложения........................................751 Самый первый сервер приложений.....................................751 Самый первый тонкий клиент....................................... 753 Добавление в сервер ограничений, накладываемых на данные..............755 Ограничения, накладываемые на поля и на набор данных...............755 Свойства полей.....................................................756 События, связанные с полями и таблицами............................757 Добавление дополнительных возможностей на стороне клиента.............758 Последовательность обновления......................................759 Обновление данных..................................................760 Дополнительные возможности DataSnap...................................762 Запросы с параметрами..............................................762 Вызов методов сервера..............................................763 Отношения типа «основное/подробности»..............................764 Использование брокера соединений...................................766 Дополнительные параметры провайдера................................766 Компонент SimpleObjectBroker.......................................767 0017
18 Содержание Накопление объектов................................................768 Настройка пакетов данных...........................................768 Что далее?.............................................................769 Глава 17. Разработка компонентов, работающих сданными...............................................................770 Связь с данными........................................................770 Класс TDataLink....................................................771 Классы, производные от TDataLink...................................772 Разработка элементов управления, ассоциируемых с полем набора данных.772 Компонент ProgressBar — только для чтения..........................773 Компонент TrackBar, поддерживающий чтение-запись...................776 Создание собственного объекта DataLink.................................779 Компонент просмотра записей........................................780 Модернизация компонента DBGrid.........................................785 Разработка компонентов наборов данных..................................788 Определение классов................................................789 Раздел I. Инициализация, открытие и закрытие.......................792 раздел 11. Перемещение и управление закладками.....................797 Раздел 111. Буферы записей и управление полями.....................800 раздел IV. Из буфера в поле........................................803 Тестирование набора данных, основанного на файловом потоке.........805 Листинг каталога в наборе данных.......................................807 Список как набор данных............................................807 Данные каталога....................................................808 Набор данных, содержащий информацию об объектах........................811 Что далее?.............................................................815 Глава 18. Формирование отчетов с использованием Rave...................................................................817 Знакомство с Rave......................................................818 Rave, Report Authoring Visual Environment..........................818 Page Designer (Дизайнер страниц) и Event Editor (Редактор событий).819 Использование компонента RvProject.................................822 Преобразование форматов............................................823 Подключение к данным...............................................825 Компоненты Rave Designer...............................................827 Базовые компоненты.................................................827 Компонент FontMaster...............................................828 Объекты доступа к данным...........................................830 Регионы и полосы...................................................831 Компоненты, связанные с данными....................................834 Дополнительные возможности Rave........................................836 Отчеты типа «основное/подробности».................................837 Отчеты со сценариями...............................................838 Отражение (Mirroring)..............................................839 Комплексные вычисления.............................................840 Что далее?.............................................................841 0018
Содержание 19 Часть IV. Delphi, Интернет и a.NET. Предварительный обзор..............................................843 Глава 19. Интернет-программирование: сокеты и indy.............................................................844 Создание сокет-приложений........................................844 Основы программирования сокета................................846 Использование TCP-компонентов Indy............................848 Передача данных базы данных через сокетное соединение.........851 Отправка и получение почты.......................................855 Работа по протоколу HTTP.........................................858 Захват НТТР-содержания........................................858 Ваш собственный браузер.......................................863 Простой НТТР-сервер...........................................864 Генерация файлов HTML............................................865 Что далее?.......................................................872 Глава 20. Веб-программирование с использованием структур WebBroker и WebSnap.......................................873 Динамические веб-страницы........................................874 Обзор CGI.....................................................874 Использование динамических библиотек..........................876 Технология WebBroker среды Delphi................................876 Отладка с помощью Web Арр Debugger............................879 Создание многоцелевого WebModule..............................881 Динамическое создание отчетов базы данных.....................882 Запросы и формы...............................................883 Работа с Apache...............................................887 Практические примеры.............................................889 Графический счетчик обращений.................................889 Поиск с помощью поисковой машины..............................891 WebSnap..........................................................893 Управление множеством страниц.................................896 Серверные сценарии...............................................898 Адаптеры......................................................900 Размещение файлов.............................................905 WebSnap и базы данных............................................905 WebSnap Data Module...........................................905 DataSetAdapter................................................906 Редактирование данных в форме.................................908 Отношение Master/Detail в WebSnap.............................910 Сеансы, пользователи и разрешения................................912 Использование сеансов.........................................912 Запрос входа в систему........................................913 Что далее?.......................................................915 0019
20 Содержание Глава 21. Веб-программирование с использованием IntraWeb........................................................ 916 Введение в IntraWeb................................................ 917 От веб-сайтов к веб-приложениям................................. 917 Заглянем за кулисы...............................................920 Архитектуры IntraWeb.............................................922 Построение IntraWeb-приложений......................................923 Написание многостраничных приложений.............................925 Управление сеансами..............................................929 Интеграция с WebBroker (и WebSnap)...............................931 Управление размещением...........................................932 Приложения сетевых баз данных.......................................933 Подключение к уточнениям (подчиненным данным)....................935 Передача данных клиентской стороне...............................938 Что дал ее?.........................................................940 Глава 22. Использование технологий XML.................................941 Знакомство с XML................................................... 941 Основной синтаксис XML...........................................942 Хорошо оформленный XML...........................................943 Работа с XML.....................................................944 Обработка XML-документов.........................................94б Программирование с использованием DOM...............................948 Отображение документа XML в элементе управления TreeView.........949 Создание документов с использованием DOM.........................952 Интерфейсы, сформированные на основе XML-кода....................956 Проверка корректности и схемы....................................959 Использование SAX API............................................961 Трансформация XML-документов.....................................965 XML и Internet Express..............................................969 Компонент XMLBroker..............................................971 Поддержка JavaScript.............................................971 Построение примера...............................................973 Использование XSLT..................................................977 Использование XPath..............................................978 Практическое использование XSL.T.................................978 Обработка XSL.T с использованием WebSnap.........................979 Выполнение XSL-преобразования напрямую с использованием DOM......981 Обработка крупных документов XML....................................983 Из ClientDataSet в XML-документ..................................983 Из документа XML в набор данных ClientDataSet....................985 Что далее?..........................................................987 Глава 23. Веб-службы и SOAP............................................988 Веб-службы..........................................................988 SOAP и WSDL......................................................989 Лингвистический перевод при помощи BabelFish.....................990 Построение веб-служб................................................993 Веб-служба конвертации валют.....................................994 Запрос на получение данных из базы данных........................997 Отладка заголовков SOAP.........................................1001 0020
Содержание 21 Доступ к существующему классу как к веб-службе..................1002 DataSnap через SOAP................................................1003 Построение сервера DataSnap SOAP................................1003 Построение клиента DataSnap SOAp................................1005 Преимущество SOAP по сравнению с другими соединениями DataSnap...1005 Обработка вложений.................................................1006 Поддержка UDDI.....................................................1008 Что такое UDDI?.................................................1008 UDDI в Delphi 7.................................................1010 Что далее?.........................................................1012 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi................................................................1013 Установка Delphi for .NET Preview..................................1014 Тестирование и установка........................................1016 Платформа Microsoft .NET...........................................1017 CLI (Common Language Infrastructure)............................1018 Common Language Runtime (CLR)...................................1019 Агрегаты........................................................1021 Промежуточный язык.................................................1022 Управляемый и безопасный код....................................1024 Common Type System (CTS)........................................1025 Сборка мусора......................................................1028 Сборка мусора и эффективность...................................1032 Установка агрегатов и обработка версий.............................1033 Что далее?.........................................................1036 Глава 25. Обзор Delphi for .NET: язык и RTL...........................1037 Устаревшие возможности языка Delphi................................1038 Устаревшие типы.................................................1038 Строки и другие типы............................................1038 Устаревшие возможности кода.....................................1039 Новые возможности языка Delphi.....................................1040 Пространства имен...............................................1040 Расширенные идентификаторы......................................1043 Ключевые слова final и sealed...................................1043 Новые спецификаторы видимости и доступа.........................1044 Члены class static..............................................1044 Вложенные типы..................................................1046 События с несколькими слушателями...............................1046 Специальные атрибуты............................................1047 Помощник класса.................................................1048 Библиотека времени исполнения и VCL................................1049 Помощники классов для RTL.......................................1050 VCL................................................................1051 Исходный код VCL.NET............................................1051 Дополнительные примеры использования VCL........................1054 Использование библиотек Microsoft..................................1054 Использование ASP.NET в языке Delphi...............................1061 Что далее?.........................................................1063 0021
22 Содержание Приложение А. Дополнительные инструменты для Delphi, разработанные автором..........................1065 Мастеры CanTools..............................................1065 Программа преобразования VclToClx.............................1067 Object Debugger (отладчик объектов)...........................1068 Memory Snap (снимок памяти)...................................1069 Лицензирование и модификация..................................1070 Приложение Б. Дополнительные инструменты для Delphi из других источников............................1071 Предустановленные компоненты Delphi с открытым исходным кодом.1071 Другие проекты с открытым исходным кодом......................1072 Проект JEDI................................................1072 GExperts...................................................1072 Delphree...................................................1073 DUnit......................................................1073 Приложение В. Бесплатные сопутствующие книги о Delphi...................................................1074 Essential pascal..............................................1074 Essential Delphi..............................................1075 Delphi Power Book.............................................1076 Веб-узел автора книги............................................1077 Исходный код примеров книги......................................1078 Где его найти.................................................1078 Что в комплекте...............................................1078 Алфавитный указатель.............................................1080 0022
Отзывы о предыдущем издании книги Mastering Delphi — победителя конкурса журнала «Mastering Delphi» за 2002 год в номинации «Лучшая книга по мнению читателей» «Ее охват потрясающий, рассмотрение Delphi всеобъемлющее, и она крайне по- лезна в качестве отправной точки. Если вы только знакомитесь с Delphi-програм- мироваиием и хотите узнать больше, чем написано в руководстве, либо вам нужна универсальная книга о среде Delphi, Mastering Delphi 6 — подойдет вам больше всего». Журнал Delphi Informant «В книге Mastering Delphi 6 Марко обеспечил высокопробное, востребованное качество (вы получите удовольствие после прочтения этой книги, даже если чита- ли предыдущее издание).» Боб Суорт (Bob Swart), «Курсы программирования доктора Боба» (Dr. Bob’s Programming Clinic) (www.drbob42.com) «Считаете ли вы себя новичком или давно испытываете любовь к широко извест- ному объектно-ориентированному средству разработки компании Borland, книга Mastering Delphi 6 крайне полезна для вас. Написанная известным во всем мире экспертом Марко Кэнту, эта книга, ориентированная на выпуск Delphi 6, продол- жает традиции непревзойденного мастерства, сочетания внимания к подробнос- тям с авторской простотой и очень удобной формой изложения. Разработанная в обучающем формате, эта книга изобилует практическими примерами програм- мирования. В общем, в ней насчитывается около 300 примеров, каждый из кото- рых снабжен четким авторским объяснением рассматриваемых ключевых момен- тов. От основ объектно-ориентированной библиотеки классов Delphi до раздела, Посвященного построению веб-приложений, — ничего не осталось неисследован- ным. Если вы ищете средство развития своих познаний и повышения мощности ваших приложений, вам не найти ничего лучше этого отличного источника». Питер Ланн (Peter Lunn), Amazon.com — официальный обзор «Mastering Delphi 6 — это полностью перестроенное и обновленное издание всем известной книги по программированию в Delphi и она предоставляет самый пол- ный из существующих охват вопроса программирования в Delphi 6. Эта книга, яв- ляясь образцом ясности, притягательности и практичности, поможет вам обрести Ключевые навыки, опыт решения проблем и построения совершенных Windows- Приложений. Просматривая книгу, я предполагал иайти какие-нибудь недостат- ки. У меня ничего не получилось». Зарко Гаджич (Zarko Gajic), специалист по Delphi на About.com 0023
Посвящается покойному Андрэа Гнесутта (Andrea Gnesutta), другу итальянского Delphi-сообщества 0024
Благодарности Это седьмое издание книги «Mastering Delphi» (Delphi для профессионалов) сле- дует за седьмым выпуском среды разработки Delphi компании Borland, появление которой зимой 1994 года стало революционным событием. Как и для многих дру- гих программистов Delphi (и ее Linux-близнеца, Kylix), эта среда стала для меня основным увлечением. А разработка, консультирование, обучение и выступление на конференциях, связанных с Delphi, занимают у меня все больше и больше вре- мени, оставляя другие языки и средства программирования в пыли моего офиса. Поскольку моя работа и моя жизнь сильно взаимосвязаны, в них имеется множе- ство людей, которых я бы хотел поблагодарить. Я лишь отмечу нескольких и ска- жу теплое «Спасибо» всему Delphi-сообшеству (в особенности за присуждение «Spirit of Delphi 1999 Award», которую я был рад разделить с Бобом Суортом (Bob Swart)). Первая официальная благодарность — программистам и менеджерам компа- нии Borland, которые сделали Delphi реальностью и продолжают развивать ее: Чаку Джазджевски (Chuck Jazdzewski), Дэни Торпу (Danny Thorpe), Эдди Чарчилю (Eddie Churchill), Элин Бауэр (Allen Bauer), Стиву Тодду (Steve Todd), Марку Эдингтону (Mark Edington), Джиму Тёрни (JimTierney), Рэви Кумэру (Ravi Kumar), Йоргу Вайнгартену (Jnrg Weingarten), Андерсу Олесону (Anders Ohlsson) и всем остальным, с кем мне не довелось познакомиться. Я также рад выразить отдель- ную благодарность моим друзьям Джону Кэстэру (John Raster) и Дэвиду Интер- саймону (David Intersimone) (отдел по работе с разработчиками компании Borland) и остальным сотрудникам компании Borland, включая Чарли Калверта (Charlie Calvert) и Зака Арлокера (Zack Urlocker). Далее я хотел бы поблагодарить издательский и редакторский коллективзы из- дательства Sybex, многих членов которых я не знаю. Особая благодарность Брайэ- ну Эгейтэпу (Brianne Agatep), Дэнису Санторо Линкольну (Denise Santoro Lincoln), Тифани Тэйлор (Tiffany Taylor), Рози Нэррис (Rozi Harris) и Кэлли Винкуист (Kelly Winquist); также хотелось поблагодарить Джоэл Фугаззотто (Joel Fugazzot- to) и Монику Баум (Monica Baum). Это издание книги было очень детально и скрупулезно проверено Delphi-rypy Брайаном Лонгом (Brian Long) (www.blong.com). Его комментарии и замечания улучшили книгу во всех областях: техническом содержании, точности, примерах, а также в удобочитаемости и грамматике! Спасибо большое. Я имел определен- ную поддержку (различной степени) при написании глав, касающихся средств- надстроек и в области .NET-программирования от (в алфавитном порядке) Джона Бушакра (John Bushakra), Джима Ганкеля (Jim Gunkel), Чэда Хауэра (Chad Hower) 0025
26 Благодарности и Роберта Лии (Robert Leahey). Краткая биография и способ связи с ними пред- ставлены в соответствующих главах. Предыдущие издания также писались при безвозмездной поддержке ряда лю- дей. Тим Гуч (Tim Gooch) работал иад Mastering Delphi 4, а Гусипп Мадаффари (Giuseppe Madaffan) помогал при работе над материалами, посвященными ба- зам данных Delphi 5. Для Mastering Delphi 6 Гай Смит-Фэррер (Guy Smith- Ferrier) написал главу об ADO, а Нэндо Дэссена (Nando Dessena) — главу об InterBase. Многие улучшения в текст и примеры программ были предложены техниче- скими корректорами последних изданий (членами команды Delphi R&D Дэни Сор- дом (Danny Thorpe), Джанкарло Анезом (Juancarlo Anez), Ральфом Фридманом (Ralph Friedman), Тимом Гучем (Tim Gooch) и Элэйн Тадросом (Alain Tadros), а также другими корректорами: Бобом Суортом (Bob Swart), Гусиппом Мадаффа- ри (Giuseppe Madaffan) и Стивом Тендоном (Steve Tendon). Умберто Барбини (Uberto Barbini) помогал мне в написании Mastering Kylix 2 и некоторые из его идей повлияли и на данную книгу. Отдельные благодарности направляются моим друзьям Брюсу Экелю (Bruce Eckel), Андрэа Проваджлио (Andrea Provaglio), Норму МакИнтошу (Norm McIntosh), Джоан и Филу из BUG-UK (Johanna and Phil, BUG-UK), Рэю Конопка (Ray Konopka), Марку Миллеру (Mark Miller), Кэри Йенсену (Cary Jensen), Крису Фризэль (Chris Frizelle) из журнала The Delphi Magazine, Майку Орриссу (Mike Orriss), Дэну Майзэру (Dan Miser, моему компаньону Паоло Росси (Paolo Rossi) и всей команде Italian D&D Team (www.dedonline.com). Кроме того, большое Спа- сибо всем посетителям моих семинаров, курсов и конференций по Delphi-програм- мированию в Италии, США, Франции, Великобритании, Сингапуре, Нидерлан- дах, Германии и Швеции. Большая благодарность моей жене Лэлле, которая выдержала испытание еще од- ного сеанса написания книги и слишком много поздних ночей (после проведения времени с нашей дочерью, Бенедиттой) — крепко обнимаю ее). Многие из наших друзей (и их детей) доставляли приятные минуты в перерывах между работой: Сан- дро и Моника с Лукой, Стефано и Элена, Марко и Л аура с Маттэо и Филиппо, Биан- ка и Паоло, Лука и Элена с Томасо, Чайра и Даниэл с Леонардо и Маттэо, Лацра, Вито и Марика с Софией. Наши родители, братья, сестры, их семьи также очень помогали. Было очень приятно провести свободное время с ними и нашими племян- никами: Маттэо, Андрэа, Джакомо, Стефано, Андрэа, Пьетро и Эленой. И, наконец, я хотел бы поблагодарить всех людей, многих из которых я не знаю, кто восхищается жизнью и помогает построить лучший мир. Я никогда не пере- стану верить в будущее и лучший мир, в основном благодаря им. От издательства Ваши замечания, предложения, вопросы отправляйте по адресу электронной по- чты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На web-сайте издательства http://www.piter.com вы найдете подробную инфор- мацию о наших книгах. 0026
Введение Первый раз, когда Зак Арлокер (Zack Urlocker) показал мне почти готовый к вы- пуску продукт с условным названием Delphi, я осознал, что это изменит мою рабо- ту и работу множества разработчиков программного обеспечения. Я боролся с биб- лиотеками C++ для Windows, a Delphi была и до сих пор является наилучшим сочетанием объектно-ориентированного программирования и визуального про- граммирования не только для этой операционной среды, но и для Linux, а в бли- жайшем будущем — для .NET. Среда Delphi 7 построена на этих традициях, на солидном фундаменте VCL, предоставляя ошеломляющее и всеобъемлющее средство разработки программ. Ищете клиент-серверное, многоуровневое, Интернет- или внутрисетевое решение? Ищете мощь и вседоступность? С помощью Delphi и изобилия методик и советов, представленных в этой книге, вы сможете достичь всего. Семь версий Среди оригинальных особенностей Delphi, которые привлекли меня, были: осно- ванный иа формах и ориеитироваииый на объекты подход, скоростной компиля- тор, великолепная поддержка баз данных, тесная интеграция с Windows-програм- мированием и компонентная технология. Но самым важным элементом был язык Object Pascal, являющийся фундаментом для всего остального. Среда Delphi 2 стала еще лучше! Среди наиболее важных нововведений можно выделить появление объекта Multi-Record и улученной визуальной таблицы баз Данных (grid), поддержки OLE Automation и вариантного типа данных, полная поддержка и интеграция с Windows 95, тип данных OLE Automation и визуальное наследование форм. В Delphi 3 к этому добавились технология code insight, под- держка отладки DLL-библиотек, шаблоны компонентов, технологии TeeChart, Decision Cube, WebBroker, пакеты компонентов, ActiveForms и, благодаря интер- фейсам, великолепная интеграция с СОМ. Delphi 4 дала нам редактор AppBrowser, новые возможности Windows 98, улуч- шенную поддержку OLE и СОМ, расширенные компоненты баз данных и много Дополнений в основные классы VCL, включая поддержку пристыковки (docking), ограничений и привязки элементов управления. Delphi 5 добавила к общей карти- не еще множество улучшений IDE (слишком большой список для перечисления здесь), расширила поддержку баз данных (наборами данных ADO и InterBase), улучшенной версией MIDAS с поддержкой Internet, средство контроля версий TeamSource, возможиости перевода, концепцию фреймов и иовые компоненты. 0027
28 Введение В Delphi 6 ко всем существующим возможностям добавились поддержка кросс- платформенной разработки с использованием Component Library for Cross-Platform (CLX), расширенной библиотеки времени выполнения, процессор баз данных dbExpress, веб-сервисы, исключительная поддержка XML, мощная структура веб- разработок, дальнейшие улучшения IDE и множество компонентов и классов, под- робно рассмотренных на последующих страницах этой книги. Среда Delphi 7 сделала эти технологии более надежными за счет улучшения и исправления (касается поддержки SOAP и DataSnap), а также предлагает под- держку новых технологий (таких, как темы Windows ХР и UDDI), но наиболее важно — доступность интересных наборов сторонних производителей: процессора отчетов RAVE, структуры разработки веб-приложений IntraWeb и среды конст- руирования ModelMaker. И, наконец, эта версия Delphi открыла совершенно новый мир, предоставив (даже в предварительной версии) первый компилятор компа- нии Borland для языка Pascal/Delphi ориентированный не на процессор компании Intel, а на платформу .NET CIL. Хотя Delphi является великолепным средством программирования, она явля- ется довольно сложной средой разработки, содержащей множество элементов. Эта книга поможет вам овладеть тонкостями программирования в среде Delphi. Мы рассмотрим сам язык Delphi, компоненты (использование существующих и разра- ботку собственных), базы данных и поддержку клиент-серверных приложений, ключевые элементы Windows- и COM-программирования, а также разработку Интернет- и веб-прнложений. Перед чтением этой книги вам не потребуются глубокие познания любого из этих вопросов, но вы должны быть знакомы с основами програм.мирования. Нали- чие некоторых навыков работы с Delphi значительно поможет вам, особенно после вводных глав. Книга сразу начинается с подробного рассмотрения вопросов; боль- шая Часть вступительного материала, имеющегося в предыдущих изданиях, была удалена. Часть этого материала и введение в язык Pascal можно найти иа моем веб- сайте (см. Приложение С). Структура книги Книга поделена на четыре части: О Часть I «Основы», В главе 1 представлены новые особенности интегрирован- ной среды разработки (Integrated Development Environment, IDE) Delphi 7, да- лее рассматривается язык Delphi, а также библиотека времени выполнения (run- time library, RTL) и библиотека визуальных компонентов (Visual Component Library, VCL). Четыре главы этой части включают как основы, так и рассмотре- ние наиболее распространенных элементов управления, разработку сложных пользовательских интерфейсов и использование форм. О Часть II «Объектно-ориентированные архитектуры Delphi» охватывает архи- тектуру Delphi-приложений, разработку собственных компонентов и исполь- зование библиотек и пакетов, а также моделирование с помощью ModelMaker и СОМ+. 0028
Свободно доступный исходный программный код в Сети 29 О Часть III «Архитектуры баз данных в Delphi» охватывает обычный доступ к ба- зам данных, углубленный анализ элементов управления, относящихся к работе с базами данных, программирование клиент-серверных приложений, рассмот- рение технологий dbExpress, InterBase, ADO, DataSnap. Кроме того, будет рас- смотрена разработка собственных компонентов работы с базами данных и на- борами данных, а также компоненты создания отчетов. О Часть IV «Обзор Internet, .NET и Delphi». Сначала в ней изучаются TCP/IP- сокеты, Internet-протоколы и Indy, далее осуществлен переход к рассмотрению серверных веб-расширений (с Web Broker, WebSnap и IntraWeb), а завершает- ся глава — XML и разработкой веб-сервисов. Вкратце можно сказать, что книга затрагивает вопросы, интересующие всех пользователей Delphi с любым уровнем подготовки: от «опытного новичка» до раз- работчика компонентов. В этой книге я попытался практически полностью исключить справочный ма- териал, сконцентрировавшись на технологиях эффективного использования Delphi. Поскольку имеется достаточное количество онлайновой документации, было бы излишним включать в данную книгу списки методов и свойств компонентов, кро- ме того, они быстро устаревают при незначительном изменении программного обес- печения. Я предполагаю, что вы читаете эту книгу, имея под рукой справочные файлы Delphi, без труда обращаясь к необходимым материалам. Однако я сделал все возможное, чтобы вы могли читать книгу вдали от компь- ютера, если вам так удобнее. В этом вам помогут снимки с экрана и ключевые фраг- менты листингов. Для обеспечения удобства чтения в книге используются лишь незначительные соглашения: все элементы исходного программного кода, такие как ключевые слова, свойства, классы и функции, представлены вот таким шриф- том. Фрагменты программного кода отформатированы так, как они выглядят в ре- дакторе Delphi (ключевые слова — жирным шрифтом; комментарии — курсивом). Свободно доступный исходный программный код в Сети Книга сконцентрирована на примерах. После представления каждой концепции или Delphi-компонента вы найдете работающий пример программы (иногда не один), демонстрирующий ее использование. Говорят, что в книге описано более 300 примеров. Эти программы можно найти в одном ZIP-файле размером менее 2 Мбайт, как на веб-сайте Sybex (www.sybex.com), так и на моем веб-сайте (www. marcocantu.com). Большинство из примеров очень просты и посвящены одной фун- кции (особенности). Более сложные примеры строятся, как правило, шаг за ша- гом, в которых промежуточные шаги включают частные решения и обеспечивают постепенное повышение возможностей. СОВЕТ--------------------------------------------------------------- Некоторые примеры баз данных требуют установки файлов-примеров баз данных Delphi; они вхо- дят в комплект стандартной установки Delphi. Для других примеров необходим пример базы данных EMPLOYEE, относящейся к InterBase (и, конечно же, InterBase-сервер). 0029
30 Введение На моем веб-сайте также имеется HTML-версия исходного программного кода с полноценным синтаксическим выделением и перекрестными ссылками ключе- вых слов и идентификаторов (классов, функций, методов имен свойств и т. п.). Перекрестные ссылки используются в HTML-файле, поэтому вы сможете исполь- зовать свой браузер для быстрого поиска всех программ, использующих опреде- ленное ключевое слово Delphi или идентификатор (не полноценная поисковая машина, но этого вполне достаточно). Структура каталогов примеров программного кода очень простая. Каждая гла- ва книги имеет свой каталог, с подкаталогами для каждого примера (т. е. 03\FilesList). В тексте ссылки на примеры осуществляются просто по имени (т. е. FilesList). СОВЕТ--------------------------------------------------------------------------- Обязательно прочитайте файл readme архива исходного программного кода, который содержит важ- ную информацию о законности и эффективности использования этого программного обеспечения. Как найти автора Если вы столкнетесь с какими-то проблемами в тексте или в примерах этой книги, издатель и я будем рады узнать о них. Помимо сообщения об ошибке или пробле- ме, пожалуйста, выразите свое мнение о содержании книги, а также укажите, ка- кие примеры вы находите чрезвычайно полезными, а какие — излишними. Для «обратной связи» существует несколько каналов: О на веб-сайте Sybex (vyvyvy.sybex.com) При необходимости вы найдете обновления текста и примеров. Для того чтобы прокомментировать эту книгу, щелкните на ссылке Contact Sybex, а затем выберите Book Content Issues (Проблемы в содер- жании книги). Эта ссылка откроет форму, в которой вы сможете указать свое мнение; О на моем веб-сайте (www.marcocantu.com) размещена Дополнительная информа- ция о книге и о Delphi. Здесь вы сможете найти ответы па многие вопросы. Этот сайт содержит новости н советы, технические статьи, бесплатные онлайновые книги (см. Приложение С), «белые страницы», Delphi-ссылки и мои коллек- ции Delphi-компонентов и средств (см. приложение А); О кроме того, я открыл раздел рассылок/конференций, специально посвященных моим книгам и общим вопросам и ответам по Delphi. Список рассылок и поря- док подписки на них вы также сможете найти на моем сайте. (На самом деле эти рассылки совершенно бесплатны, но требуют регистрации пароля.) К рас- сылкам/конференциям также можно обратиться через веб-интерфейс, исполь- зуя ссылку, размещенную на моем веб-сайте; О и, наконец, вы можете обратиться непосредственно на мой адрес: marco@marcocantu.com. По техническим вопросам сначала попытайтесь почи- тать конференции. Вероятно, ответы на свои вопросы вы сможете получить быстрей и от многих людей. Мой почтовый ящик обычно переполнен и, к сожа- лению, я не могу быстро отвечать на каждый запрос. (Пишите мне на английс- ком или итальянском языке.) 0030
ЧАСТЬ I Основы В этой части: ♦ Глава 1. Среда Delphi 7 и ее IDE ♦ Глава 2. Язык программирования Delphi ♦ Глава 3. Run-Time библиотека ♦ Глава 4. Классы базовой библиотеки ♦ Глава 5. Визуальные элементы управления ♦ Глава 6. Создание интерфейса пользователя ♦ Глава 7. Работа с формами 0031
1 Среда Delphi 7 и ее IDE В таких средствах визуального программирования, как Delphi, роль интегриро- ванной среды разработки (integrated development environment, IDE) порой более важна, чем сам язык программирования. Delphi 7 предоставляет ряд новых инте- ресных свойств, которые превосходят IDE среды Delphi 6. В данной главе рассмат- риваются совершенно новые функциональные возможности, а также возможно- сти, появившиеся в последних версиях Delphi. Мы также обсудим некоторые традиционные функциональные возможности Delphi, которые не столь очевидны или просто незнакомы новичкам. Эта глава не является всеобъемлющим учебным пособием по IDE (для чего потребовалось бы значительно больше места); по боль- шому счету — это совокупность подсказок и советов, рассчитанных на среднего пользователя Delphi. Если вы являетесь начинающим программистом, не переживайте. IDE языка Delphi является интуитивно понятной. Комплект Delphi сам по себе включает ру- ководство по эксплуатации (на компакт-диске Delphi Companion Tools в формате Acrobat) с учебным пособием, являющимся введением в разработку Delphi-при- ложений. Книга написана с расчетом, что вы уже знаете, как выполнить основные практические операции IDE; все последующие после этой главы сконцентрирова- ны на проблемах и технологиях программирования. В этой главе рассмотрены следующие вопросы; О перемещения по IDE; О редактор; О технология Code Insight; О разработка форм; О Project Manager; О файлы Delphi. Различные версии Delphi Перед тем как углубиться в подробности программирования в среде Delphi, давай- те несколько отклонимся в сторону, чтобы разобрать два ключевых момента. Во-первых, не существует одной редакции Delphi, их — множество. Во-вторых, любая среда Delphi может быть перенастроена. Поэтому снимки экрана (скрин- шоты), которые вы увидите в данной главе, могут отличаться от тех, что вы увиди- те на своем компьютере. Вот существующие в настоящее время редакции Delphi: 0032
Обзор IDE 33 О Personal — рассчитана на новичков или на людей, которые занимаются про- граммированием нерегулярно. Эта версия не имеет ни поддержки программи- рования баз данных, ни других расширенных возможностей Delphi. О Professional Studio — рассчитана на профессиональных разработчиков. Она включает все основные функциональные возможности, плюс поддержку раз- работки баз данных (включая поддержку ADO), поддержку базового веб-сер- вера (WebBroker), а также некоторые дополнительные средства, включая Мо- delMaker и IntraWeb. Данная книга предполагает, что вы работаете, по меньшей мере, с этой редакцией. О Enterprise Studio — редакция рассчитана на разработчиков приложений пред- приятия. Она включает все технологии XML и расширенные технологии веб- сервисов, а также множество других средств. Ряд глав этой книги рассматрива- ют возможности, имеющиеся только в редакции Delphi Enterprise. Эти разделы отмечены специально. О Architect Studio — в этой редакции по сравнению с Enterprise добавлена под- держка Bold — среды построения приложений, которые в ходе выполнения уп- равляются посредством UML-модели. Эти приложения благодаря изобилию расширенных компонентов способны отображать свои объекты как на базу дан- ных, так и на пользовательский интерфейс. В этой книге поддержка Bold ие рассматривается. Помимо различных редакций существуют различные способы настройки сре- ды Delphi. В сиимках, представленных в этой книге, я старался использовать стан- дартный пользовательский интерфейс (каким ои является после выполнения ус- тановки); однако я, конечно же, имею собственные предпочтения и устанавливал множество надстроек (add-on), которые могут быть отражены в скриншотах. Professional и более мощные версии Delphi 7 включают рабочие копии Kylix 3 — версии Delphi для Linux. В этой книге не рассматривается визуальная среда Kylix и операционная система Linux, за исключением ссылок на CLX-библиотеку и кросс- платформенные возможности Delphi. Более подробно эти вопросы можно изучить с помощью книги Mastering Kylix 2 (издательство Sybex, 2002)1. В версии Delphi нет существенных отличий между Kylix 2 и Kylix 3. Самым большим нововведе- нием Kylix 3 является поддержка языка C++. Обзор IDE При работе в среде визуального проектирования работа распределяется на две от- дельные задачи: на работу с визуальными конструкторами и на работу с программ- ным кодом. Конструкторы позволяют оперировать с компонентами на визуальном уровне (например, помещать кнопку на форму) или на невизуальном уровне (на- пример, помещение компонента DataSet в модуль данных). Форму и модуль дан- ных вы видите в действии (рис. 1.1). В обоих случаях конструкторы позволяют Из книг, посвященных Kylix, в издательстве «Питер» вышла книга С. Бобровского «Delphi б и Kylix: библиотека программиста», Питер, 2003. — Лримеч. ред. 0033
34 Глава 1. Среда Delphi 7 и ее IDE выбрать необходимый компонент и установить начальные значения свойств этого компонента. Редактор кода — это то, где пишется программный код. Наиболее очевидным приложением программного кода в визуальной среде является реагирование на различные события, начиная с событий, связанных с действиями, выполняемыми пользователем программы, например, щелчок на кнопке или выбор пункта в спис- ке. Тот же подход используется при обработке внутренних событий, например со- бытий, включающих изменения базы данных или поступление уведомления от операционной системы. По мере приобретения опыта программисты зачастую начинают с написания, в основном, кода обработки событий, а затем переключаются на написание соб- ственных классов и компонентов, проводя большую часть времени в работе с ре- дактором. Поскольку эта книга охватывает не только визуальное программирова- ние, но и пытается помочь овладеть всей мощью Delphi, по ходу текста будет все больше программного кода и меньше текста. [OentDaiaSeii I Afifa» " Fab д 3 St ChentD ataS etl D alaS outce! iwsesw jCafcFieid» Tijj Jiaofc MocfelMaksr Window }pNone 5 | j "tu * © > О | S АЙййюиаЦ SvsbMtJ D^eAtees» dbExpteit | DatsSnao^ 8DE | AM | Lv£4. J® й*> i\ ~ 88 ! v »П b -jSF ® f ft Fr4i < Рис. 1.1. Форма и модуль данных в IDE Delphi 7 ‘if I IDE для двух библиотек Это важное изменение первоначально появилось в Delphi 6. Теперь IDE позволя- ет работать с двумя различными визуальными библиотеками, библиотекой визу- альных компонентов (Visual Component Library, VCL) и библиотекой компонен- тов для кросс-платформенных решений (Component Library for Cross-Platform, CLX). СОВЕТ ------------------------------------------------------------ CLX — кросс-платформенная библиотека, позволяющая перекомпилировать программный код с по- мощью Kylix для запуска под Linux. Подробное сравнение CLX и VCL производится в главе 5. Исполь- зование библиотеки CLX в Delphi 7 представляет интерес, так как Kyhxтеперь поставляется совместно с Windows-версией Delphi. 0034
Обзор IDE 35 При создании нового проекта или открытии существующего обновляется па- литра компонентов (Component Palette), представляя только те элементы управ- ления, которые соответствуют текущей библиотеке (хотя большинство компонен- тов совместно используются в обеих библиотеках). При работе с невизуальными компонентами (например, с модулем данных) вкладки палитры компонентов, со- держащие только визуальные компоненты, скрыты. Настройки рабочего стола Пользователи могут настраивать IDE Delphi различным образом, как правило, от- крывая различные окна, упорядочивая и стыкуя их друг с другом. Однако зачас- тую в ходе разработки требуется открыть один набор окон, а в ходе отладки — дру- гой. Аналогично, при работе с формами требуется одна раскладка, при написании компонентов — другая, а при работе с низкоуровневым программным кодом — толь- ко редактор. Переупорядочивание IDE для каждого из этих случаев является тру- доемкой задачей. Теперь Delphi позволяет сохранять порядок окон IDE (называемый рабочим столом {desktop) или Global Desktop, для отличия от Project Desktop) под опреде- ленным именем и легко восстанавливать его. Вы также можете сделать один из рабочих столов отладочным окружением по умолчанию, которое будет автомати- чески восстанавливаться при запуске отладчика. Все эти возможности доступны на панели Desktops (Рабочие столы). Также можно обратиться к настройкам рабо- чего стола с помощью команды меню View (Вид) ► Desktops (Рабочие столы). Сведения о настройках рабочего стола сохраняются в файлах с расширением DST (располагаемых в каталоге BIN), которые похожи на INI-файлы. К сохраняе- мым настройкам относятся: положение основного окна, Project Manager (Менед- жера проекта ), Alignment Palette (Палитры выравнивания), Object Inspector (Ин- спектора объектов) (вместе с выбранной категорией свойств), окна редактора (вместе с состоянием Code Explorer и Message View), а также множество других, плюс статус стыковки различных окон. Вот небольшая выдержка из DST-файла, которая вполне понятна: [Main Window] Create»! Visible»! State=0 Left=0 Top=0 Width=1024 Height=105 ClientWidth=1016 ClientHeight=78 [ProjectManager] Create»! Visible»!) State=0 Dockable»1 [AlignmentPalette] 0035
36 Глава 1. Среда Delphi 7 и ее IDE Create=l Visible=O Настройки рабочего стола перекрывают настройки проекта, которые сохраня- ются в DSK-файл с подобной структурой. Настройки рабочего стола позволяют исключить проблемы, которые могут возникнуть при перемещении проекта с од- ной машины на другую (или между различными разработчиками), выражающие- ся в необходимости переупорядочивания окон на свой вкус. Delphi отличает на- стройки глобального рабочего стола на уровне пользователей от настроек на уровне проекта, что обеспечивает улучшенную поддержку работы групп разработчиков. ПРИМЕЧАНИЕ------------------------------------------------------------------- Если при открытии Delphi не видно формы или других окон, проверьте (или удалите) файл настроек рабочего стола (в каталоге BIN среды Delphi). Если вы открываете проект, полученный от другого пользователя, и не удовлетворены расположением (или отсутствием) некоторых окон, загрузите ваши настройки глобального рабочего стола или удалите DSK-файл проекта. Параметры окружения Немало появившихся в последних версиях обновлений относятся к диалоговому окну Environment Options (Параметры окружения). Страницы этого окна были пе- реупорядочены в Delphi 6 за счет перемещения свойств конструктора форм со стра- ницы Preferences (Настройки) на новую страницу Designer (Конструктор): О страница Preferences (Настройки) диалогового окна Environment Options (Пара- метры окружения) имеет флажок, предотвращающий автоматическую стыков- ку окон Delphi друг с другом; О страница Environment Variables (Переменные окружения) позволяет просмотреть переменные окружения (стандартные пути, настройки операционной системы), а также установить определяемые пользователем переменные. Приятным мо- ментом является возможность использовать как системные, так и пользователь- ские переменные в каждом из диалоговых окон IDE, например, можно избежать жесткого кодирования обычно используемых путей, заменяя их переменными. Другими словами, переменные окружения работают так же, как переменная SDELPHI, указывающая на основной каталог Delphi, но могут определяться са- мим пользователем; О на странице Internet (Интернет) можно выбрать расширение файлов по умол- чанию, используемое для HTML- и XML-файлов (преимущественно структу- рой WebSnap), а также связать внешний редактор в каждом из расширений. О меню Основная панель меню Delphi (которая в Delphi 7 имеет более современный вне- шний вид) является важнейшим элементом взаимодействия с IDE, хотя большин- ство задач можно выполнить с помощью «горячих» клавиш и контекстных меню. По реакции на текущую операцию панель меню не претерпела значительных из- менений: для получения полного списка доступных операций в текущем объекте или компоненте необходимо щелкнуть правой кнопкой мыши. 0036
Обзор IDE 37 Панель меню может значительно измениться после установки средств и масте- ров сторонних производителей. В Delphi 7 ModelMaker имеет собственное меню. Установив такие популярные надстройки, как GExperts или даже мои мастера, вы увидите и другие меню (подробности см. в Приложении В «Дополнительные сред- ства Delphi из сторонних источников» и Приложении А «Дополнительные сред- ства Delphi, предлагаемые автором книги»). Важным меню, добавленным в Delphi последних редакций, является Window (Окно). Это меню содержит перечень открытых окон; ранее этот список можно было получить с помощью сочетания Alt+O или с помощью команды меню View (Вид) ► Window List (Список окон). Меню Window очень удобно, поскольку зачас- тую одни окна оказываются позади других и их сложно найти. Можно установить алфавитный порядок сортировки пунктов этого меню, настроив Реестр Windows: найдите подраздел Delphi Main Window (в ветви HKEY_CURRENT_USER\Software\Borland\ Delphi\7.0). Этот ключ реестра использует строчные значения (вместо двоичных), где '-1' и 'True' соответствуют значению «истинно», а 'О' или 'False' — «ложно». ПРИМЕЧАНИЕ ---------------------------------------------------------- В Delphi 7 меню Window (Окно) завершается новой командой: Next Window (Следующее окно). Эта команда особенно полезна в виде «горячей» клавиши — Alt+End. Переход по различным окнам IDE ранее никогда не был столь простым (по крайней мере без специальных надстроек). Диалоговое окно Environment Options Хак упоминалось ранее, некоторые настройки IDE требуют непосредственного редактирования реестра Windows. В этой главе будут рассмотрены еще несколько [Таких настроек. Конечно же, распространенные настройки значительно проще под- корректировать с помощью диалогового окна Environment Options (Параметры ок- ружения), обратиться к которому можно из меню Tools (Сервис) вместе с Editor Options (Параметры редактора) и Debugger Options (Параметры отладчика). Боль- шинство из настроек интуитивно понятны и хорошо описаны в справочной системе Delphi. На рис. 1.2 представлены мои стандартные настройки страницы Preferences (Настройки) этого диалогового окна. Список To-Do Другой функциональной возможностью, появившейся в Delphi 5, но до сих пор используемой не в полной мере, является список To-Do (что сделать). Это список задач, которые необходимо выполнить до завершения проекта — т. е. совокупность заметок программиста (или программистов; это средство очень полезно при рабо- те группы разработчиков). Хотя идея не нова, ключевая концепция списка To-Do заключается в том, что он является двойственным. Вы можете добавлять или изменять пункты списка дел посредством добавле- ния специальных комментариев TODO к исходному программному коду любого файла проекта; затем вы можете просмотреть соответствующие элементы списка. Кроме того, для изменения соответствующего комментария в исходном коде мож- но визуально редактировать пункты в списке. Например, вот как может выглядеть пункт списка To-Do в исходном коде: 0037
38 Глава 1, Среда Delphi 7 и ее IDE Environment Options TmLiw | ' EnyintwertVanabtes | internet J Delphi Direct , ..Preferences | Qessjyrer j Object Inspector | | Espkxer ccj-Autosave options------------------? f"Compiling and tunning------- 17 Editotfles \ 17 Show gompilet progress f“ Project desktop i j f“ Wain on package rebuild -----------—I j (ул ,. r Desktop «tntwts------------------1 & on run ’: <• desktop orty Г Desktop and ж>Ьо Dockarg T Auto Лад docking . Pressing the Control key white dragging wl allow window docking p-Shared repository I Directory [ Рис. 1.2. Страница Preferences диалогового окна Environment Options procedure TForml.FormCreate(Sender: TObject): begin // TODO -oMarco. Добавить код создания формы end: Этот же пункт можно визуально редактировать в окне, представленном на рис. 1.3. Рис. 1.3. Окно Edit To-Do Item используется для модификации пункта To-Do. Эту операцию также можно выполнить непосредственно в программном коде Исключением из этого правила двойственности является определение пунктов в масштабе проекта. Такие пункты должны добавляться непосредственно в спи- сок. Для этого необходимо либо нажать сочетание клавиш Ctrl+A при активном окне 0038
Обзор IDE 39 To-Do List, либо щелкнуть правой кнопкой на этом окне и в контекстном меню вы- брать пункт Add from (Добавить из). Эти пункты сохраняются в специальном фай- ле с тем же корневым именем, что и файл проекта (с расширением .TODO). Совместно с комментариями TODO можно использовать множество ключей. Например, ключ -о (как в предыдущем примере), для указания владельца (про- граммиста/который ввел этот комментарий), ключ -с — для указания категории, или просто число от 1 до 5 — для указания приоритета (0 или отсутствие числа указывает, что уровень приоритета не установлен). Например, использование ко- манды Add To-Do Item (Добавить пункт списка «что сделать») в контекстном меню редактора (или сочетание Ctrl+Shift+T), производит генерацию такого комментария: { T0D0 2 -oMarco : Button pressed } В зависимости от типа комментария Delphi воспринимает в качестве текста пункта списка To-Do все от двоеточия до конца строки или закрывающей фигур- ной скобки. И, наконец, в окне To-Do List можно сбросить флажок, указывающий, что этот пункт выполнен. Комментарий исходного программного кода изменится с TODO на DONE (выполнено). Можно также вручную в программном коде изменить коммен- тарий и просмотреть отметку, появившуюся в окне To-Do List. Одним из самых мощных элементов этой структуры является основное окно To-Do List, которое может автоматически собирать все сведения о необходимости выполнения каких-либо действий из различных файлов программного кода по мере их ввода, сортировать и фильтровать их, а также экспортировать в буфер обмена в виде обычного текста или в виде таблицы HTML. Все параметры доступны из контекстного меню. Расширенные сообщения компилятора и результаты поиска в Delphi 7 Небольшое окно Messages (Сообщения) по умолчанию появляется под редакто- ром; оно представляет как сообщения компилятора, так и результаты поиска. В Delphi 7 это окно значительно усовершенствовано. Во-первых, результаты по- иска выводятся на отдельной вкладке, не смешиваясь с сообщениями компилято- ра, как было ранее. Во-вторых, каждый раз при новом поиске вы можете указать вывод результатов на новой странице, оставляя доступными результаты предыду- щего поиска: !► С VPrograrn Fies\Borland\Oelphi7\PTO|ect$\Pioiecl1 dpr(4)- Forms. | C \Pfogram F4es\Bofland\Delphi7\Pro»ects\Ro»ecl1 dpf(5| Un*1 mUnitl pas'(Form 1}. \Bu*d Xs earch for unrt’^Saarch to tom*/ Для циклического переключения вкладок этого окна можно использовать со- четания Alt+Page Down и Alt+Page Up (эти же сочетания работают и на других окнах с вкладками). При возникновении ошибки компилирования, можно активизировать еще одно новое окно с помощью команды меню View (Вид) ► Additional Message Info (Допол- нительные сообщения). При компилировании программы окно Message Hints (Со- общения-подсказки) предоставит дополнительные сведения для некоторых рас- пространенных сообщений об ошибках, предлагая способы их устранения: 0039
40 Глава 1. Среда Delphi 7 и ее IDE Message Hints ! f X Q Gfe . ^Check to see that the file is located on your Library path Такой вариант помощи в большей степени предназначен для новичков, но он может быть полезен и для других пользователей. Важно осознать, что эти сведе- ния полностью настраиваемы: руководитель проекта может поместить соответству- ющие описания распространенных ошибок в форме, указывающей что-либо осо- бенное для новых разработчиков. Для реализации этой возможности следуйте комментариям, расположенным в файле, в котором содержатся настройки данной функции — файл msginfo70.ini в каталоге Delphi\bin. Редактор Delphi На первый взгляд редактор Delphi не претерпел значительных изменений. Однако на самом деле он является совершенно новым средством. Помимо работы над фай- лами с программным кодом языка Object Pascal (или языка Delphi, как его сейчас предпочитает называть компания Borland) теперь вы можете использовать его для работы и с другими файлами, используемыми в среде разработки Delphi (напри- мер, SQL, XML, HTML и XSL-файлами), а также с файлами, содержащими исходный программный код других языков (включая C++ и С#). Редактиро- вание XML и HTML стало доступно в Delphi 6, но в новой версии произошли значительные изменения. Например, в ходе редактирования HTML-файла поддерживается как синтаксическое выделение, так и технология завершения кода (code completion). Настройки редактора, используемые для всех файлов (включая поведение при нажатии таких клавиш, как Tab) зависит от расширения открытого файла. Эти на- стройки можно изменить с помощью страницы Source Options (Параметры источ- ника) диалогового окна Editor Properties (Свойства редактора) (рис. 1.4). Эта функ- ция была расширена и стала более открытой, причем настолько, что теперь можно настроить редактор даже предоставлением DTD1 для форматов XML-файлов или написанием пользовательского мастера, осуществляющего выделение синтаксиса для других языков программирования. Еще одна функциональная возможность редактора — шаблоны программного кода — теперь зависит от языка (предопреде- ленные Delphi-шаблоны будут чувствительны к HTML или С#). СОВЕТ------------------------------------------------------------------- C# — это новый язык компании Microsoft, появившийся в их архитектуре .NET. В дальнейшем компа- ния Borland планирует поддерживать C# в своей собственной среде .NET, имеющей в настоящее время название Galileo. 1 Document Type Definition (определение типа документа) — шаблон, стандартизующий процедуры обработки. 0040
Редактор Delphi 41 {Editor Properties General Source Options | Degfeyj Key Mappings | £«for | Code Insight | Sourcefle («ж |c# r Option* " Extensions 1 p Autoindent :p Use tab J Г Sjgarttab § Г Obtimalfi# r p Backspace gnrndents fielete 3 c# Default HTML IDL Pascal SQL XML ep trailing blanks ov< tab character ow sgace character I/" Use jB«ia* Wghfeht Рис. 1.4. Множество языков программирования, поддерживаемых IDE Delphi, могут быть связаны с различными расширениями файлов с помощью страницы Source Options диалогового окна Editor Properties Редактор, входящий в состав IDE, который учитывал только язык Delphi, не сильно изменился в последних версиях. Однако он имеет новые возможности, ко- торые неизвестны большинству программистов, поэтому я считаю необходимым провести небольшое исследование. Редактор Delphi позволяет работать одновременно с несколькими файлами, подобно блокноту с вкладками. Можно переходить с одной страницы редактора на следующую нажатием Ctrl+Tab (или Ctrl+Shift+Tab для перехода в обратном направ- лении). Можно «перетаскивать» вкладки с именами модулей на более верхнюю позицию для изменения их порядка, что позволит использовать однократное нажатие Ctrl+Tab для перехода между модулями, над которыми вы работаете в на- стоящее время. Кроме того, контекстное меню редактора имеет пункт Pages (Стра- ницы), который в своем подменю представляет список всех доступных страниц (удобная функция при наличии множества загруженных страниц). Также можно открыть множество окон редактора, в каждом из которых будет содержаться множество вкладок. Это единственный способ просмотреть исходный код двух модулей, расположив их друг с другом. (На самом деле, когда у меня воз- никала необходимость сравнить два модуля Delphi, я всегда использовал Beyond Compare — www.scootersoftware.com — великолепная, недорогая утилита сравнения файлов, написанная на Delphi). На работу редактора влияют несколько параметров, расположенных в диалого- вом окне Editor Properties (см. рис. 1.4). Однако для того чтобы установить функ- цию AutoSave (Автосохранение), необходимо перейти страницу Preferences (На- стройки) диалогового окна Environment Options (Параметры окружения) (см. рис. 1.2). Этот параметр заставляет редактор сохранять все файлы с исходным кодом при 0041
42 Глава 1. Среда Delphi 7 и ее IDE каждом запуске программы, предотвращая потерю данных в (редких) случаях не- удачного завершения программы в отладчике. Редактор Delphi предоставляет множество команд, включая те, которые воз- вращают к предшественникам эмуляции WordStar (до появления компиляторов Turbo Pascal). Я не буду рассматривать различные установки редактора, посколь- ку они интуитивно понятны и достаточно подробно рассмотрены в онлайновой справочной системе. Хотя замечу, что страница справки, описывающая «горячие» клавиши контекстного меню, доступна только целиком при поиске раздела shortcuts (контекстные меню). ПРИМЕЧАНИЕ------------------------------------------------------------- Советуем запомнить, что использование команд Cut (Вырезать) и Paste (Вставить) не являются единственным способом перемещения исходного программного кода. Также можно выбрать и пере- тащить слова, выражения или целые строки кода. Кроме того, вместо перемещения текста его можно скопировать, нажав клавишу Crtl в ходе перетаскивания. Code Explorer Окно Code Explorer (Исследователь кода), которое обычно пристыковано к боко- вой стороне окна редактора, содержит список всех типов, переменных и процедур, определенных в текущем модуле, плюс список других модулей, присутствующих в выражении uses. Для таких сложных типов, как классы, Code Explorer может со- держать подробную информацию, включая списки полей, свойства и методы. Все сведения обновляются, как только вы начинаете набор в редакторе. Code Explorer может использоваться в редакторе для перемещения по коду. При двойном щелчке на одном из элементов Code Explorer, редактор «перепрыгнет» на соответствующее объявление. В Code Explorer имеется возможность изменять имена переменных, свойств и методов. Однако, как вы увидите в дальнейшем, если необходимо иметь визуальное средство для работы с классами, модуль ModelMaker обеспечивает большую функциональность. Хотя все возможности Code Explorer становятся очевидными после работы с ним в течение нескольких минут, некоторые функции Code Explorer не столь явны. Имеется полный контроль над размещением сведений. Можно сократить глубину представления дерева, обычно выводимого в этом окне (свертывание дерева помо- жет более быстро выполнять выделения). Настройка Code Explorer осуществляет- ся посредством соответствующей страницы диалогового окна Environment Options (рис. 1.5). Обратите внимание, что при снятии выделения одного из пунктов Explorer Catego- ries с правой стороны страницы диалогового окна, Explorer не удаляет соответству- ющие элементы из просмотра, он просто добавляет в дерево узлы. Например, при сбросе флажка Uses среда Delphi не прячет список используемых модулей, а напро- тив, используемые модули вместо помещения в папке Uses будут перечислены как основные узлы. Я обычно сбрасываю выделения Types, Classes и Variables/Constants. Поскольку каждый элемент дерева Code Explorer имеет значок, указывающий его тип, упорядочивание по полям и методу менее важно, чем упорядочивание по спецификатору доступа. Я предпочитаю выводить все элементы в отдельной груп- пе, поскольку это упорядочивание для доступа к каждому элементу требует мень- ше щелчков мыши. 0042
Редактор Delphi 43 Environment Options TypeUrrery | EdvvonmentVariables | Internet | f/ Preferences | Pesipne I Object Inspector | Palette j Libiay ; Explorer options 5 P Automatically v ' F7 HljrtjhtjncoriK ; " IShovijjeclaret.- Г Explorer sorting" ; <• AfcMjeticel ; C gpuce “ Class completion option '-у P EHsh incomplete properties j^r Initial browser view---------- r <• passes C Units ' C £lo> 5 L™______________ f Browser scope 'В'орй symbols cr«. i <jjT AS symbols Delphi Direct Explorer - Extfaer categories ' Private Protected । I Public j I Published Field Properties * 3* Hetr | Рис. 1.5. Настройка Code Explorer Выбор элементов в Code Explorer делает удобным перемещение по исходному программному коду большого модуля: при двойном щелчке на методе в Code Explorer фокус переместится на его определение в определении классов. Для пе- рехода от определения метода или процедуры к его окончанию и обратно можно использовать Module Navigation (Перемещение по модулю) (сочетание Ctrl+Shift с клавишами Up или Down). СОВЕТ-------------------------------------------------------------------- Некоторые из категорий Explorer Categories, представленных на рис. 1.5, используются Project Browser, а не Code Explorer. Среди прочих к ним относятся параметры группировки Virtuals, Statics Inherited и Introduced. Поиск в редакторе Еще одной функцией редактора является Tooltip symbol insight (Подсказка знатока идентификаторов). Поместите мышь над любым идентификатором в редакторе и Tooltip (Контекстное окно подсказки) покажет, где объявлен этот идентифика- тор. Эта функция может быть особо важной для отслеживания определенных вами идентификаторов, классов и функций приложения, а также для ссылки на исход- ный код библиотеки. ВНИМАНИЕ----------------------------------------------------— Хотя сначала это может показаться хорошей идеей, Tooltip symbol insight нельзя использовать для поиска модуля, в котором объявлен идентификатор, который вы хотели использовать. Если соот- ветствующий модуль на момент использования функции не подключен (с помощью include), то окно Tooltip не появится. 0043
44 Глава 1. Среда Delphi 7 и ее IDE Реальным выигрышем данной функции является возможность включения ее в навигационный прибор, называемый code browsing (обзор кода). При удержании нажатой клавиши Ctrl и помещении указателя мыши над идентификатором, Delphi вместо вывода Tooltip создает активную связь с определением. Эти связи выводят- ся синим цветом с подчеркиванием, что является типичным для гиперссылок в веб- браузерах, а указатель мыши при наведении на такую ссылку меняется на «руку». Например, если нажать Ctrl и щелкнуть на идентификаторе Tlabel, то откроется его описание в исходном коде VCL. При выборе ссылок редактор отслеживает, на какие позиции вы «перепрыгивали», что позволяет возвращаться и снова перехо- дить вперед по ним (с помощью кнопок Browse Back и Browse Forward, расположен- ных в правом верхнем углу окна редактора или с помощью сочетаний Alt+Стрелка влево или Alt+Стрелка вправо), точно так же как в веб-браузерах. Также можно щел- кнуть на раскрывающихся стрелках, расположенных рядом с кнопками Back и For- ward для просмотра подробного списка строк и файлов исходного кода, куда вы «перепрыгивали», что обеспечивает дополнительное удобство при перемещении. Как можно непосредственно переходить к исходному коду VCL, если он не яв- ляется частью вашего проекта? Редактор может находить не только модули, ука- занные в пути Search (Поиск) (которые компилируются как часть проекта), но также и те, на которые указывают пути Debug Source, Browsing и Library. Эти ка- талоги просматриваются в том порядке, в котором они только что были перечис- лены. Определение путей производится с помощью страницы Directories/Conditionals (Каталоги/Условности) диалогового окна Project Options (Параметры проекта) и на странице Library (Библиотека) диалогового окна Environment Options. По умолча- нию Delphi добавляет каталоги исходного кода VCL в путь окружения Browsing. Завершение классов Редактор среды Delphi также приходит на помощь, автоматически генерируя про- граммный код, завершающий то, что вы уже написали. Эта функция называется class completion (завершение класса); она активизируется нажатием сочетания Ctrl+Shift+C. Добавление обработчика события в приложение является довольно быстрой операцией, поскольку Delphi автоматически добавляет объявление ново- го метода обработки данного события в класс и предоставляет скелет метода в раз- деле реализации модуля. Это является одним из аспектов поддержки средой Delphi визуального программирования. Более новые версии Delphi аналогичным образом упрощают жизнь програм- мистам, которым в обработчике события необходимо написать лишь небольшой дополнительный код. Данная функция генерации программного кода реализована в отношении общих методов, методов обработки сообщений и свойств. Например, если в объявлении класса вы наберете следующий код: public procedure Hello (MessageText: string): а затем нажмете Ctrl+Shift+C, то Delphi предоставит определение данного метода в разделе реализации модуля, сгенерировав следующие строки программного кода: ( TForml } procedure TForml.HellolMessageText: string); begin end; 0044
Редактор Delphi 45 Эта функция действительно удобна по сравнению с традиционным подходом большинства Delphi-программистов, который заключается в использовании опе- раций копирования и вставки одного и более объявлений, добавлении имен клас- сов и последующим копированием кода begin...end для каждого копируемого ме- тода. Завершение класса также работает и в обратном порядке: можно написать реализацию метода непосредственно в виде его программного кода, азатем нажать Ctrl+Shift+C для генерации требуемого содержания в разделе объявления класса. Самый важный и полезный пример завершения класса заключается в автома- тической генерации программного кода для поддержки свойств, объявленных в классах. Например, если вы введете в классе: property Value: Integer; и нажмете Ctrl+Shift+C, то Delphi превратит эту строку в: property Value: Integer read fValue write SetValue: Delphi также добавляет метод SetValue в объявление класса и предоставляет для него стандартную реализацию. Code Insight Помимо Code Explorer, функции завершения класса, и функций навигации реак- тор Delphi поддерживает технологию code insight (знаток кода). В общем смысле технология «предсказания» основана на непрерывном фоновом анализе как вво- димого пользователем исходного программного кода, так и программного кода системных модулей, на которые ссылается ваш программный код. Code insight состоит из пяти «способностей»: завершение кода (code completion), шаблоны программного кода (code templates), параметры кода (code parameters), подсказка значения выражения (Tooltip expression evaluation) и подсказка знато- ка идентификаторов (Tooltip symbol insight). Последняя функция уже рассматри- валась в разделе «Поиск в редакторе»; четыре же остальные рассматриваются в последующих подразделах. Имеется возможность включить, выключить или на- строить каждую из этих «способностей» с помощью страницы Code Insight диало- гового окна Editor Properties. Завершение кода Функция завершения кода позволяет выбрать свойство или метод объекта, просто увидев его в списке или вводом начальных букв. Для активизации списка просто вводится имя объекта, например Buttonl, азатем добавляется точка. Для принуди- тельного вывода списка можно нажать Ctrl+nробел; для его удаления, если он не нужен — нажмите Esc. Завершение кода также позволяет выбирать верное значе- ние в назначаемом выражении. Как только вы начинаете ввод, список фильтрует свое содержание, основыва- ясь на начальной части добавляемого элемента. Список завершения кода исполь- зует цвета и выводит дополнительные сведения, помогая отличать различные эле- менты. В Delphi имеется возможность настроить эти цвета с помощью страницы Code Insight диалогового окна Editor Options. Еще одна особенность заключается в том, что если функция имеет параметры, то в генерируемый код будут добавлены круг- лые скобки и немедленно будет выведена подсказка со списком параметров. 0045
46 Глава 1. Среда Delphi 7 и ее IDE Как только после переменной или свойства будет введено :=, Delphi представит список всех иных переменных или объектов того же типа, плюс объекты, имеющие свойства того же типа. Пока виден список, можно щелкнуть на нем правой кноп- кой для изменения порядка элементов, упорядочивая их по области действия или по имени; кроме того, можно изменить размер окна. Начиная с Delphi 6, функция завершения кода также функционирует в разделе интерфейса модуля. При нажатии Ctrl+n робел при нахождении курсора внутри оп- ределения класса открывается список виртуальных методов, которые можно пере- крыть (в том числе и абстрактными методами), методы реализованных интерфей- сов, базовые свойства класса и частично системные сообщения, которые можно обработать. Выбор одного из них приведет к добавлению соответствующего мето- да в объявление класса. В этом случае список завершения кода позволяет выбрать несколько пунктов. ПРИМЕЧАНИЕ --------------------------------------------------------------------- Если введен неверный программный код, функция Code insight работать не будет, и появится лишь обычное сообщение об ошибке, описывающее ситуацию. Имеется возможность вывести специаль- ные ошибки code insight в Message рапе (панели сообщений), которая должна быть открыта зара- нее: автоматического открытия этой панели при ошибке компиляции не происходит). Для активизации этой возможности необходимо установить недокументированный элемент реестра, установив у строч- ного параметра \Delphi\ZO\Compiling\ShowCodeInsiteErrors значение 'Г. Завершение кода включает некоторые расширенные возможности, которые сложно заметить. Практически полезная среди них возможность связана с поис- ком в модулях идентификаторов, не используемых вашим проектом. При ее вызо- ве (Ctrl+пробел) на пустой строке в список также будут включены идентификаторы из общих модулей (Math, StrUtils и DateUtils), не указанных в выражении uses текущего модуля. При выделение одного из этих внешних идентификаторов Delphi добавляет этот модуль в выражение uses. Эта функция (не работающая внутри выражений) приводится в действие настраиваемым списком дополнительных мо- дулей, хранящемся в разделе реестра \Delphi\7.0\CodeCompletion\Extrallnits. ПРИМЕЧАНИЕ -------------------------------------------------------------- В среде Delphi 7 добавлена возможность обзора объявлений элементов в списке завершения кода посредством нажатия клавиши Crtl и щелчка на любом идентификаторе в этом списке. Шаблоны кода Эта функция позволяет вставлять один из предопределенных шаблонов программ- ного кода, например, сложное выражение с внутренним блоком begin...end. Шаб- лоны кода должны активизироваться вручную, нажатием сочетания Ctrl+J, кото- рое приводит к выводу списка всех шаблонов. Если перед нажатием этого сочетания набрано несколько букв (ключевого слова), то Delphi выведет список только тех шаблонов, которые начинаются с этих букв. Имеется возможность самостоятельно добавлять необходимые шаблоны, что позволяет создавать собственные комбинации сокращенного ввода наиболее час- то используемых блоков. Например, если вы часто используете функцию MessageDlg, то для нее можно создать шаблон. Для изменения существующих шаблонов пе- рейдите на страницу Source Options (Параметры источника) диалогового окна Editor 0046
редактор Delphi 47 Options (Параметры редактора), и выберите в списке Source File Туре (Тип файла исходного кода) тип Pascal, а затем щелкните на кнопке Edit Code Templates (Редак- тировать шаблон кода). Это приведет к открытию нового диалогового окна Delphi 7 Code Templates. Щелкните здесь на кнопке Add (Добавить), введите имя нового шаб- лона (например, mess), введите его описание, а затем добавьте в тело шаблона сле- дующий текст: MessageDlg (' |'. misinformation. [mbOK]. 0): Теперь каждый раз при необходимости создания диалогового окна просто вве- дите mess и нажмите Ctrl+J, что приведет к появлению полного текста. Символ «вер- тикальная черта» (|) указывает место, в которое должен перейти курсор редактора после вывода всего шаблона. Его позицию можно указать и в другом удобном для вас месте, с которого вы собираетесь продолжить заполнение шаблона. Хотя шаблоны кода, на первый взгляд, относятся только к ключевым словам языка программирования, они представляют собой более общий механизм. Шабло- ны сохраняются в файле DELPHI32.DCI — текстовом файле с весьма простым форма- том, который можно легко редактировать. Delphi 7 также позволяет экспортировать настройки языка в файл, а затем импортировать их, упрощая обмен собственными шаблонами между разработчиками. Параметры кода При вводе функции или метода механизм Параметры кода (Code Parameters) вы- водит тип данных параметров этой функции или метода в виде подсказки или в окне Tooltip. Просто введите имя функции или метода и открывающую круглую скобку и тут же в всплывающей области подсказки появятся наименования и типы па- раметров. Для принудительного вывода параметров кода можно нажать Ctrl+Shift+ пробел. Для большей наглядности текущий параметр выделяется жирным шрифтом. Подсказка значения выражения Подсказка значения выражения (Tooltip Expression Evaluation) — это функция, работающая в период отладки программы. Она показывает значение идентифика- тора, свойства или выражения, которое находится под указателем мыши. Если это выражение, то необходимо сначала выделить его в редакторе, а затем поместить указатель мыши над выделенным текстом. Дополнительные «горячие» клавиши редактора В редакторе имеется множество дополнительных «горячих» клавиш, которые оп- ределяются выбранным стилем редактора. Вот несколько наименее известных «го- рячих» клавиш: О Ctrl+Shift + цифровая клавиша от 0 до 9 активизирует закладку, помеченную в редакторе отступом. Для перехода на эту закладку нажмите клавишу Ctrl и Цифровую клавишу. Полезность закладок в редакторе ограничена тем, что но- вая закладка может перекрыть существующую, а также тем, что они непостоян- ны (при закрытии файла они теряются). О Ctrl+E активизирует последующий поиск. Можно нажать сочетание Ctrl+E, а за- тем ввести слово, которое необходимо найти, не открывая специального диало- гового окна, и щелкнуть на клавише Enter для выполнения поиска. 0047
48 Глава 1, Среда Delphi 7 и ее IDE О Ctrl+Shift+I выполняет отступ сразу нескольких строк программного кода. Ко- личество пробелов устанавливается параметром Block Indent (Отступ блока) на странице Editor (Редактор) диалогового окна Editor Options (Параметры редакто- ра). Сочетание Ctrl+Shift+U выполняет обратное действие, устраняя отступы. О Ctrl+0+U переключает регистр выделенного кода; для перевода всего выделен- ного текста в нижний регистр можно использовать сочетание Ctrl+K+E и Ctrl+K+F — в верхний. О Ctrl+Shift+R начинает запись макроса, который впоследствии может быть вос- произведен с помощью сочетания Ctrl+Shift+P. В макрос последовательно запи- сывается ввод с клавиатуры, операции перемещения и удаления в файле ис- ходного кода. Воспроизведение макроса лишь повторяет эту последовательность действий — операция, которая теряет смысл при переходе к другому файлу. Макросы редактора полезны при повторном выполнении многоэтапной опера- ции, например, изменении форматирования исходного программного кода или упорядочивании данных в исходном программном коде для облегчения их вос- приятия. О При удержании клавиши Alt можно «перетащить» указатель мыши и выделить в редакторе прямоугольную область, а не последовательные строки и слова. Загружаемые виды Еще одной важной функциональной возможностью, впервые появившейся в Del- phi 6, является поддержка множества видов редактора. Один и тот же файл, загру- женный в IDE, редактор может предоставить во множестве видов, определенных программно и учтенных в системе, что позволяет позже загрузить их; поэтому они называются загружаемые (loadable). Наиболее часто используемый вид — страница Diagram (схема), которая была доступна для модулей данных, начиная с версии Delphi 5, хотя была менее мощ- ной. Для веб-приложений доступен другой набор видов: HTML Script (HTML-сце- нарий), HTML Result (результирующий вид HTML) и прочие, более подробно рас- смотренные в главе 20 «Веб-программирование с использованием структур Web- Broker и WebSnap» и главе 22 «Использование технологий XML». Для цикличес- кого переключения между нижними вкладками редактора (определяющими вид) можно использовать сочетания Alt+Page Down и Alt+Page Up; сочетание Ctrl+Tab про- изводит смену страниц (или файлов), представленных верхними вкладками. Вид Diagram Вид Diagram (Схема) показывает связи между компонентами, включая родитель- ско-дочерние отношения, принадлежность, связанные свойства и общие отноше- ния. Для компонентов Dataset в ней также представлены отношения основной/под- чиненный и соединения подстановки. Сюда также можно добавить комментарии в текстовых блоках, связанных со специальными компонентами. Автоматически построение схемы не производится. Необходимо самостоятель- но «перетащить» на схему компоненты из вида Tree (Дерево); при этом автомати- чески будут представлены связи, существующие между «перетаскиваемыми» ком- понентами. В Object TreeView можно выделить и одновременно «перетащить» на страницу Diagram множество элементов. 0048
Редактор Delphi 49 Удобно, что имеется возможность установить свойства простым «перетаскива- нием» стрелок между компонентами. Например, после перемещения на схему эле- ментов управления edit (строка редактирования) и label (надпись) можно выбрать значок Property Connector, щелкнуть на label и «перетащить» указатель мыши на элемент edit. При отпускании кнопки мыши вид Diagram установит соотношение свойств, основанное на свойстве FocusControl, являющемся единственным свойством элемента label, ссылающимся на элемент управления edit (рис. 1.6). Рис. 1.6. Вид Diagram представляет отношения между компонентами (и даже позволяет настраивать их) Как можно заметить, настройки свойств являются направленными', если «пере- тащить» линию связи свойств от элемента edit к элементу label, то произойдет по- пытка использовать label в качестве значения свойства строки редактирования. Поскольку это невозможно, будет выведено сообщение об ошибке, указывающее на это и предлагающее связать компоненты другим способом. Вид Diagram позво- ляет создавать множество схем для каждого модуля Delphi, т. е. для каждой фор- мы или модуля данных. Вы присваиваете схеме наименование (и при необходимо- сти — описание), щелкаете на кнопке New Diagram, и подготавливаете новую схему, после чего сможете переходить вперед и назад по схемам с помощью комбиниро- ванного списка в панели инструментов вида Diagram. Хотя вид Diagram можно использовать для установки отношений, его основное предназначение заключается в документировании структуры. Поэтому немаловаж- но, что данный вид можно вывести на печать. Когда вид Diagram является актив- ным, используйте стандартную команду меню File (Файл) ► Print (Печать). Delphi предложит указать параметры (рис. 1.7). Вывод можно настроить различным об- разом. Сведения из вида Diagram сохраняются в отдельном файле, а не как часть DFM- файла. Среда Delphi 5 использовала DTI-файлы (design-time information), имею- щие структуру, подобную INI-файлам. В Delphi 6 и 7 по-прежнему сохранена 0049
50 Глава 1. Среда Delphi 7 и ее IDE возможность чтения старого .DTI-формата, но они используют новый .DDP-фор- мат (Delphi Diagram Portfolio). Эти файлы используют двоичный формат DFM (или аналогичный), что не позволяет их редактировать в текстовом редакторе. По- нятно, что все эти файлы бесполезны в ходе выполнения программы (при созда- нии исполняемого файла они не компилируются). [Print Diagram Ж P Pnntpage^oideK Г Flirt ^scnptons (lower corner) Р ftsrt On single рада Рис. 1.7. Параметры печати для вида Diagram СОВЕТ--------------------------------------------------------------------------- Если имеется желание поэкспериментировать с видом Diagram, то начинать лучше с открытия про- екта DiagramDemo, используемого в примерах данной главы. Форма программы имеет две связан- ные схемы: одна представлена на рис. 1.6, а другая, более сложная, с раскрывающимся меню и его пунктами. Конструктор форм Еще одно окно Delphi, с которым приходится часто взаимодействовать, это конст- руктор форм (Form Designer) — визуальное средство размещения компонентов на формах. В конструкторе форм можно непосредственно с помощью мыши выбрать компонент; также можно использовать Object Inspector (Инспектор объекта) или Object TreeView (Дерево объектов), что удобно, когда элемент управления находит- ся за другим элементом или является очень маленьким. Если один элемент управ- ления закрывает другой элемент полностью, то для выбора элемента управления, являющегося родительским по отношению к текущему, можно воспользоваться клавишей ESC. Для выбора формы необходимо неоднократно нажать на эту клави- шу или нажать и удерживать клавишу Shift при щелчке на выбранном компоненте. Это приведет к снятию выделения текущего компонента и выбору формы по умол- чанию. Существует две альтернативы применению мыши для установки положения компонента. Можно либо непосредственно установить значения свойств Left и Тор, либо использовать кнопки со стрелками при удержании нажатой клавиши Ctrl. Использование кнопок со стрелками более полезно для подстройки позиции эле- мента (приустановленном параметре Snap То Grid (Привязать к сетке) при нажатой клавише Alt при использовании мыши для перемещения элемента управления. При нажатии сочетания Ctrl+Shift совместно с клавишами со стрелками компонент бу- дет передвигаться только с интервалом сетки. 0050
Конструктор форм 51 При нажатии клавиш со стрелками при нажатой клавише Shift можно подстро- ить размер компонента. И, опять же, этого же можно достичь с помощью мыши и клавиши Alt. Для выравнивания множества компонентов или установки одинакового разме- ра можно выделить их и установить сразу для всех требуемые значения свойств Top, Left, Width или Height. Для выделения нескольких компонентов щелкните на них мышью при нажатой клавише Shift; либо, если все необходимые компоненты попадают в прямоугольную область, «перетащите» мышь и «нарисуйте» прямо- угольник, охватывающий их. Для выбора дочерних элементов управления (ска- жем, кнопок, находящихся на панели) «перетащите» мышь над панелью, удержи- вая нажатой клавишу Ctrl (другими словами — охватите панель). После того как выделено несколько компонентов, вы можете задать их взаимное расположение с помощью диалогового окна Alignment (Выравнивание) (пункт Align в контекстном меню формы) или Alignment Palette (Палитры выравнивания) (доступной из меню View (Вид) ► Alignment Palette (Палитра выравнивания)). По окончании разработки формы можно воспользоваться пунктом Lock Controls (Заблокировать элементы управления) меню Edit (Правка) для предотвращения случайного изменения местоположения компонентов. Этот пункт особенно поле- зен, поскольку количество операций Undo (Отменить) в отношении формы огра- ничено (можно выполнить только Undelete (Восстановить)), но настройки не вос- станавливаются. Среди прочих функциональных возможностей конструктор форм предлагает ряд Tooltip-подсказок: О При помещении указателя мыши над компонентом появляющаяся подсказка укажет имя и тип компонента. Начиная с версии 6, Delphi предлагает расши- ренные подсказки с уточнениями, касающимися положения, размера, tab-по- рядка элемента управления и т. д. Это помимо параметра среды Show Component Captions (Показать заголовки компонентов), который я оставляю активным. О При изменении размера элемента управления подсказка покажет текущий раз- мер (свойства Width и Height). Конечно же, эта функция доступна только для визуальных элементов управления (а не тех, которые представлены значками). О При перемещении компонента подсказка покажет текущее положение (свой- ства Left и Тор). И, наконец, вместо сохранения в формате открытого текста, используемого по умолчанию, вы можете сохранить DFM-файлы (Delphi Form Module) в старом двоичном формате. Для каждого отдельного модуля переключение на этот формат осуществляется из контекстного меню конструктора форм. Для всех вновь созда- ваемых форм — установкой значения по умолчанию на странице Designer (Конст- руктор) диалогового окна Environment Options (Параметры среды) — на той же стра- нице, на которой вы указываете, будут ли при запуске программы автоматически создаваться вторые формы. Наличие DFM-файлов, сохраненных в текстовом формате, позволяет более эффективно оперировать системой управления версиями. Программисты не по- лучат реального преимущества, поскольку вы могли уже открыть двоичные DFM- 0051
Э4 Глава 1. Среда Delphi 7 и ее IDE файлы в редакторе Delphi с помощью специального пункта из контекстного меню конструктора. С другой стороны, системы управления версиями требуют сохране- ния тестовой версии DFM-фалов для того, чтобы иметь возможность сравнивать их и фиксировать разницу между двумя версиями одного и того же файла. В любом случае при наличии текстовой версии DFM-файла среда Delphi перед использованием их в исполняемом файле программы все равно преобразует его в формат двоичного ресурса. DFM-файлы компонуются в исполняемый файл в дво- ичном виде для того, чтобы сократить размер (хотя особой компрессии и не проис- ходит) и для повышения производительности в ходе выполнения (двоичный фор- мат загружается более быстро). СОВЕТ Во всех версиях Delphi текстовые DFM-файлы более компактны, чем их двоичные представления. Хотя более старые версии Delphi могут не воспринимать новые свойства элементов управления, используемых в DFM-файлах более новых версий, они по-прежнему смогут понять остальную часть текстового DFM-файла. Если же в новой версии Delphi добавлен новый тип данных, то старые вер- сии Delphi вообще не смогут прочитать двоичные DFM. Даже если пока их нет, необходимо помнить, что 64-разрядные операционные системы уже на подходе. При наличии каких-либо сомнений со- храняйте DFM в текстовом формате. Учтите также, что все версии Delphi поддерживают текстовые DFM, используя утилиту командной строки Convert из каталога bin. И, наконец, CLX-библиотека, как в Delphi, так и в Kyhx, вместо DFM использует расширение XFM. Object Inspector Для просмотра и изменения свойств компонентов, размещенных на форме (или другом конструкторе), на этапе разработки используется Object Inspector (Инс- пектор объектов). Object Inspector имеет ряд новых функциональных возможнос- тей по сравнению с более ранними версиями Delphi. Последняя, которая впервые появилась в Delphi 7, заключается в выделении жирным шрифтом свойств, имею- щих значение, отличающееся от значения по умолчанию. Еще одним важным изменением (появившемся в Delphi 6) является возмож- ность расширять (expand) ссылки компонента. Свойства, ссылающиеся на другие компоненты, отмечаются различным цветом и могут быть раскрыты с помощью расположенного слева символа «+», также как в случае наличия внутренних под- компонентов. После этого можно изменять свойства других компонентов, не вы- деляя их. Здесь можно в ходе работы с другим компонентом (список) просмотреть связанные компоненты (в появляющемся меню), раскрытые в Object Inspector. JListBoxI Й0₽«1|«« | Everts | j РагешСвЗО True sPorertFort Tro® ParertShowHmtTroe True 0 Ail shown 0052
Конструктор форм □J функциональная возможность расширения интерфейса также поддерживает подкомпоненты, что продемонстрировано с помощью нового элемента управле- ния LabeledEdit. Эта функция Object Inspector позволяет выбрать связанный ком- понент по свойству. Для этого дважды щелкните на значении свойства левой кноп- кой мыши при нажатой клавише Ctrl. Например, если на форме имеется компонент MainMenu, а вы в Object Inspector просматриваете свойства формы, то выбрать ком- понент MainMenu можно переходом на свойства Menu формы и двойным щелчком на его значении при нажатой клавише Ctrl. Это приведет к тому, что в Object Inspector основное меню будет представлено как значение этого свойства. Вот некоторые из последних изменений Object Inspector: О Список в верхней части Object Inspector показывает тип объекта и позволяет выбрать компонент. Можно удалить этот список для того, чтобы сохранить про- странство, решив, что для выбора компонентов можно использовать Object TreeView (по умолчанию также размещен в верхней части окна Object Inspector). О Свойства, которые ссылаются на объект, теперь имеют другой цвет и могут рас- крываться, сохраняя выделение. О Дополнительно в Object Inspector можно видеть свойства, имеющие атрибут «только для чтения». Конечно же, они будут представлены серым. О Object Inspector имеет диалоговое окно Properties (Свойства), позволяющее настроить цвета элементов управления различных типов и общее поведение этого окна. О Начиная с Delphi 5, раскрывающийся список свойства может содержать гра- фические элементы. Эта функция используется для таких свойств, как Color и Cursor, и особенно полезна для свойства Imageindex тех компонентов, которые связаны с ImageList. СОВЕТ--------------------------------------------------------------------- Свойства интерфейса теперь могут настраиваться с помощью Object Inspector на режиме разработки. Эта возможность использует модель Interfaced Component Reference, впервые появившуюся в Kylix/ Delphi 6, где компоненты могут применять и содержать ссылки на интерфейсы точно так же, как интерфейсы могут реализовываться компонентами. Модель Interfaced Component References работает как открытые ссылки старых компонентов, но свойства интерфейса могут привязываться к любому компоненту, реализующему необходимый интерфейс. В отличие от свойств компонента, свойства интерфейса не ограничиваются определенным типом компонента (классом или производными класса- ми). При щелчке в редакторе Object Inspector на раскрывающемся списке данного свойства интерфейса будут показаны все компоненты текущей формы (и связанных форм), реализующие этот интерфейс. Раскрывающийся список шрифтов в Object Inspector Некоторые свойства в Object Inspector среды Delphi имеют графические раскрывающиеся списки. Возможно, вам захочется добавить показ реально- го изображения выбираемого шрифта, соответствующего подсвойству Name свойства Font. Эта возможность встроена в Delphi, но отключена, поскольку на большинстве компьютеров установлено много дополнительных шриф- тов, и их «воспроизведение» может значительно замедлить работу компью- тера. Для включения этой функции необходимо установить пакет Delphi, разрешающий глобальную переменную FontNamePropertyDisplayFontNames моду- ля VCLEditors. Я сделал это с помощью пакета OiFontPk, который можно най- ти в примерах программ к данной главе на сайте автора или издательства. :---- — — продолжение £’’ 0053
Глава 1. Среда Delphi 7 и ее II Раскрывающийся список шрифтов в Object Inspector [продолжение) После установки этого пакета вы можете зайти в свойство Font любого компонента и увидеть раскрывающееся графическое меню Name: £uc(da CcdRgrajjdy Lucida console LucidaSans Unicode D'X<T“ Microsoft Sans Serif Modern xo ooooooo Существует второй, более сложный способ настройки Object Inspector, который мне нравится больше: настройка шрифтов для всего Object Inspector, для повышения удобочитаемости. Эта функция особенно полезна для пуб- личных презентаций. Информацию о том, как получить эту надстройку, мож- но найти в Приложении А. Категории свойств В среде Delphi имеется понятие «категория свойств» (property categories), активи- зируемое пунктом Arrange (Упорядочить) контекстного меню Object Inspector. При выборе этого пункта свойства будут упорядочены не в алфавитном порядке, а по группам, причем допускается, что одно и то же свойство может быть представлено в различных группах. Категории позволяют снизить «сложность» Object Inspector. Для того чтобы спрятать свойства определенных категорий, можно использовать пункты View контекстного меню независимо от вида, в котором они представлены (т. е. даже если вы предпочитаете традиционное упорядочивание по имени, вы все равно можете спрятать свойства некоторых категорий). Хотя понятие «категорий свойств» появилось в Delphi 5, оно редко используется программистами. Object TreeView Для представления модулей данных в Delphi 5 появился вид TreeView, в котором можно просмотреть отношения между таким невизуальными компонентами, как наборы данных, поля, действия и т. д. Delphi 6 расширила эту идею, предоставив вид Object TreeView для каждого конструктора, включая открытые формы. По умол- чанию Object TreeView размещается над Object Inspector. Object TreeView показывает все компоненты и объекты на форме, представляя их взаимоотношения в виде дерева. Наиболее очевидными являются отношения «ро- 0054
конструктор форм 55 дитель-наследник»: если поместить на форму элемент управления «панель», кнопку на этой панели и кнопку на форме, то в дереве одна кнопка будет под формой, а другая — под панелью. jobject TreeView Обратите внимание, что TreeView синхронизирован с Object Inspector и Form Designer. Поэтому если вы выберете элемент и смените фокус в любом из этих трех инструментов, фокус сменится и в остальных двух инструментах. Помимо отношений «родитель-наследник» Object TreeView представляет и дру- гие связи: «владелец/владеемый», «компонент/подобъект» и «коллекция/эле- мент», плюс различные специфические связи, включая отношения «набор данных/ подключение» и «источник данных/набор данных». Вот пример структуры меню в дереве: -j M&nMenul - >5, File {File!} New {New1} 0pen{0pen1} 4^ Save {Save!} Exrt {Exiil} ' 4 Edt{Edn1} U ndo {ijtidoll'l Временами TreeView также показывает «пустые» узлы, которые не имеют соот- ветствующих объектов, но соответствуют предопределенным объектам. В качестве примера такого случая «перетащите» компонент Table (Таблица) (со страницы BDE); вы увидите два серых значка для сеанса и псевдоним. Технически Object TreeView использует серые значки для компонентов, не имеющих постоянства существова- ния на этапе разработки Они являются реальными компонентами (во время раз- работки и в ходе выполнения), но ввиду того, что они являются объектами по умол- чанию, создаваемыми при выполнении программы и не имеющими постоянных Данных при редактировании на этапе разработки, Data Module Designer (конст- руктор модулей данных) не позволяет редактировать их свойства. Если «перета- щить» элемент управления «таблица» на форму, вы также увидите пункты, рядом с которыми расположен красный восклицательный знак, помещенный в желтый кРУг. Этот символ указывает на наличие частично неопределенных элементов. Object TreeView поддерживает различные типы «перетаскиваниям: ° Можно выбрать компонент на палитре компонентов (щелкнув на нем, не пере- таскивая), поместить указатель мыши над деревом, а затем щелкнуть компо- нент для перемещения его в это место. Такая методика позволяет перетаскивать 0055
56 Глава 1. Среда Delphi 7 и ее IDE компонент в необходимый контейнер (форму, панель и другие), независи- мо от того, что их поверхность может быть полностью закрыта другими ком- понентами. Это позволяет избежать переупорядочивания существующих компонентов. О В TreeView имеется возможность «перетащить» компоненты, например, пере- местить компонент из одного контейнера в другой. В конструкторе форм это можно сделать только посредством операций «вырезать/вставить». Операция перемещения по сравнению с операцией вырезания имеет преимущество в том, что в при ее использовании связи между компонентами не нарушаются, что неизбежно при удалении компонента в ходе операции вырезания. О Имеется возможность «перетащить» компоненты из TreeView в вид Diagram. Щелчок правой кнопкой на любом элементе TreeView выводит контекстное меню, подобное меню компонента, доступному при нахождении компонента на форме (в обоих случаях контекстное меню включает пункты, связанные с редакторами настройки компонентов). Вы можете даже удалить элементы из дерева. TreeView, так же как редактор коллекций, производит дублирование (см. свойство Columns элемента управления ListView). В этом случае вы можете не только упорядочить и удалить пункты, но и добавить в коллекцию новые пункты. object Tteewew |и1Й ♦ ♦ 23 Butter# «аЯ ListView! S Columns >5 0 • TListColumn 2 TLislColumn 3 TListColumn ПРИМЕЧАНИЕ -------------------------------------------------------------- Имеется возможность в целях документирования распечатать содержимое Object TreeView. Просто укажите окно и выберите меню File (Файл) ► Print (Печать) (в контекстном меню пункта Print нет). Секреты палитры компонентов Component Palette (Палитра компонентов) используется для выбора компонен- тов, которые вы хотите поместить в текущий конструктор. При помещении указа- теля мыши над компонентом вы увидите его наименование. В Delphi 7 в подсказке будет указано и имя модуля, в котором определен этот компонент. Палитра компонентов имеет множество вкладок, даже слишком! Возможно, вы захотите спрятать вкладки, содержащие компоненты, которые вы не планируете использовать, и переупорядочить палитру по своему вкусу. В Delphi 7 можно так- же переупорядочить вкладки, воспользовавшись механизмом «drag and drop». С по- мощью страницы Palette (Палитра) диалогового окна Environment Options (Па- раметры среды) можно полностью переупорядочить компоненты на различных страницах, добавляя новые элементы и перемещая их со страницы на страницу. 0056
Секреты палитры компонентов 57 При наличии слишком большого числа страниц в палитре для нахождения не- обходимого компонента возникает необходимость «прокрутки». В этом случае можно переименовать страницы, присвоив им более короткие имена, с которыми на экране поместятся все вкладки. (Это очевидное решение — вы уже подумали о нем.) Delphi 7 предлагает еще одну новую возможность. При наличии на одной стра- нице большого числа компонентов Delphi выводит двойную стрелку; она исполь- зуется для просмотра оставшихся компонентов без необходимости «прокрутки» всей страницы палитры. Контекстное меню палитры компонентов имеет подменю Tabs (Вкладки), в ко- тором в алфавитном порядке перечислены все страницы палитры. Это подменю используется для изменения активной страницы и очень полезно, когда необходи- мая страница на экране не видна. ПРИМЕЧАНИЕ------------------------------------------------------------------- Имеется возможность изменить порядок элементов подменю Tabs контекстного меню палитры ком- понентов на тот же порядок, в котором они представлены на самой палитре, а не в алфавитном порядке. Для этого перейдите в раздел реестра Delphi Main Window (в разделе \Software\Borland\Delphi\ 7.0 текущего пользователя) и установите параметр Sort Palette Tabs Menu в 0. Эта важная недокументированная функция палитры компонент является «го- рячей» активизацией. Установкой специальных параметров реестра можно про- сто выбрать страницу палитры, поместив над ее вкладкой указатель мыши, не щел- кая на ней. Того же эффекта можно достичь в отношении указателей прокрутки компонентов, расположенных по обе стороны палитры, появляющихся, когда стра- ница содержит очень много элементов. Для активизации этих скрытых возмож- ностей добавьте ключ Extras в раздел Borland\Delphi\7.0 реестра в разделе HKEY_CURRENT_ USER\Software. В этом ключе введите два строчных значения AutoPaletteSelect и AutoPaletteScroll, а затем установите для каждого строчное значение Т1. Копирование и вставка компонентов Интересной функцией конструктора форм является возможность копирования и вставки компонентов из одной формы в другую, или дублирование компонента в той же форме. В ходе этой операции Delphi дублирует все свойства, сохраняет связанные обработчики событий и, при необходимости, изменяет имя элемента управления (которое в одной форме не должно повторяться). Можно также копировать компоненты из конструктора форм в редактор и на- оборот. При копировании компонента в буфер обмена среда Delphi помещает в не- го компонент в виде текстового описания. Можно редактировать текстовую вер- сию компонента, скопировать текст в буфер обмена и вставить его обратно в форму как новый компонент. Например, если поместить на форму элемент «кнопка», ско- пировать его, а затем вставить в редактор (который может быть как собственным Редактором среды Delphi, так и любым текстовым процессором), вы получите сле- дующее описание: object Buttonl. Tbutton Left = 152 Top = 104 0057
58 Глава 1. Среда Delphi 7 и ее IDE Width = 75 Height = 25 Caption = 'Buttonl' TabOrder = 0 end А теперь, если вы измените, например, имя объекта, заголовок или его положе- ние, либо добавите новое свойство, то эти изменения могут быть скопированы и вставлены обратно в форму. Вот несколько простых изменений: object Buttonl TButton Left = 152 Top = 104 Width = 75 Height - 25 Caption = 'My Button TabOrder = 0 Font Name = 'Arial' End Копирование этого описания и вставка его в форму приведет к созданию в ука- занном месте кнопки с заголовком Му Button, выполненным шрифтом Arial. Для использования этой методики необходимо уметь редактировать текстовое представление компонента, знать, какие свойства являются для него действитель- ными, а также представлять, как указать значения для строчных свойств, наборов свойств или прочих специальных свойств. Когда среда Delphi интерпретирует тек- стовое описание компонента или формы, она может изменить значения некото- рых свойств, связанных с измененными вами свойствами, а также может изменить положение компонента таким образом, чтобы он не закрывал предыдущую копию. Конечно же, если вы введете что-либо совершенно некорректное и попытаетесь вставить это в форму, Delphi выведет сообщение об ошибке с указанием причины. Можно также выделить несколько компонентов и копировать их одновремен- но либо в другую форму, либо в текстовой редактор. Такой прием может оказаться полезным при необходимости работы с сериями подобных компонентов Их мож- но скопировать в редактор, размножить несколько раз, сделать необходимые из- менения, а затем снова вставить в форму целую группу. От шаблонов компонентов к фреймам При копировании одного или нескольких компонентов из одной формы в другую вы копируете лишь все их свойства. Более мощный прием заключается в создании шаблона компонентов, который создает копию как свойств, так и программного кода обработчиков событий. При вставке шаблона в новую форму выбором псев- докомпонента из палитры, Delphi реплицирует исходный программный код обра- ботчиков событий в эту форму. Для создания шаблона компонента выберите один или несколько компонентов и выполните команду меню Component (Компонент) ► Create Component Template (Создать шаблон компонентов). Эта команда открывает диалоговое окно Component Template Information (Сведения о шаблоне компонентов), в котором следует ука- зать имя шаблона и страницу палитры компонентов, на которой необходимо раз- местить компонент и значок: 0058
Секреты палитры компонентов 59 По умолчанию именем шаблона является имя первого выбранного компонента с добавлением слова Template. Значком шаблона по умолчанию является значок первого выбранного компонента, но его можно заменить другим значком из фай- ла. Указанное имя шаблона будет использовано для описания его в палитре ком- понентов (при появлении всплывающей подсказки). Все сведения о шаблоне компонентов хранятся в отдельном файле DELPHI32.DCT, но порядок извлечения и редактирования шаблона в нем не документирован. Од- нако можно поместить шаблон в совершенно новую форму, редактировать его и снова назначить его в качестве шаблона компонентов с использованием того же имени. Это позволит перезаписать предыдущее определение. ПРИМЕЧАНИЕ---------------------------------------------------------------------- Группа программистов Delphi может совместно использовать шаблон компонентов, расположив его в общедоступном каталоге, добавив в реестр элемент CCLibDir в разделе \Software\Borland\Delphi\7.0\ Component Templates. Шаблон компонентов удобен в тех случаях, когда на различных формах требу- ется размещение одинаковых групп компонентов и соответствующих им обработ- чиков событий. Проблема заключается в том, что после помещения экземпляра шаблона на форму среда Delphi делает копию компонентов в их программном коде, который более не связан с этим шаблоном. Нет никакой возможности изменить само определение шаблона и, естественно, нет возможности выполнить точно та- кие же изменения во всех формах, которые используют этот шаблон. Я слишком много хочу? Совсем нет! Именно это и позволяет сделать технология фреймов. Фрейм — это такой тип рабочей панели, используемой во время разработки, с которой можно работать как с формой. Просто создается новый фрейм, на нем размещаются необходимые компоненты и добавляется код обработчиков событий. После того как фрейм готов, можно открыть форму, выбрать на странице Standard (Стандартные) палитры компонентов псевдо-компонент Frame и выбрать один из Доступных фреймов (текущего проекта). После помещения фрейма на форму она будет выглядеть так, как будто на нее скопированы компоненты. При изменении исходного фрейма (с помощью его собственного конструктора) изменения будут отображены во всех экземплярах этого фрейма. На рис. 1.8 представлен простой пример, названный Frames 1. Рисунок в дан- ном случае не очень нагляден; необходимо открыть программу или перекомпоно- вать другую программу для того, чтобы ощутить достоинства фреймов. Как и формы, фреймы определяют классы, поэтому они согласовываются с объектно-ориентированной моделью VCL значительно проще, чем шаблоны ком- 0059
60 Глава 1. Среда Delphi 7 и ее IDE понентов. Более подробно VCL и фреймы рассмотрены в главе 8 «Архитектура Delphi-приложений». Как можно понять из данного краткого описания, фреймы являются очень мощной технологией. - lol *1 DEFAULT cF^mdowTtut 41 «$$№$««( fpOeJarf a n w 0 TS______________ Ftamel 1 Tl-wSwF BFw» Cefar < 8«ЙЯ Steh Ss» EStyia 4 И*#* ЬНрГдое HW ВНои&сийБа Uft... Г Heme -AliihOWft Г Рис. 1.8. Пример Framesl демонстрирует использование фреймов. Фрейм (слева) и его экземпляр на форме (справа) синхронны Управление проектами Многоцелевой менеджер проектов Delphi (View (Вид) ► Project Manager (Менеджер проектов)) работает над группой проектов, в которую может входить один и более проектов. Например, группа проектов может состоять из DLL и исполняемого файла или из множества исполняемых файлов. Все открытые пакеты будут пред- ставлены в Project Manager как проекты, даже если они не добавлены в группу проектов. На рис. 1.9 представлен Project Manager с простой группой проекта, содержа- щей все примеры к данной главе. Project Manager основан на древовидном пред- ставлении, позволяющем понять иерархическую структуру группы проектов, отдельных проектов и всех форм и модулей, составляющих каждый проект. С по- мощью простой панели инструментов и более сложного контекстного меню Project Manager можно оперировать с проектами этой группы. Контекстное меню зависит от контекста; его параметры зависят от выбранного элемента. Существуют пункты меню, осуществляющие добавление новых или существующих проектов в группу проектов, компиляцию и компоновку указанного проекта и открытие модуля. ПРИМЕЧАНИЕ-------------------------------------------------- Начиная с Delphi б, Project Manager также показывает все открытые пакеты, даже если они не добавлены в группу проектов. Пакет — это совокупность компонентов или других модулей, скомпи- лированных для отдельного исполняемого файла (см. главу 10). Только один из проектов в группе может быть активным; это проект, с которым вы оперируете при выборе пункта Project (Проект) ► Compile (Компилировать). Меню Project имеет две команды, позволяющие выполнить компиляцию или ком- поновку всех проектов группы. (Довольно странно, но эти пункты недоступны 0060
управление проектами 61 с помощью контекстного меню Project Manager.) Когда необходимо скомпоновать множество проектов, с помощью команд Build Sooner (Скомпоновать ранее) и Build Later (Скомпоновать позже) можно указать относительный порядок компоновки. Эти команды осуществляют переупорядочивание проектов в списке. DiagiamDemo ехе еЙШИШ У Framesl ехе jjjl Form Form pas Forml 1= Frame Й Frame pas Ш Framel '+} Др ToDoTestexe +! Др DiagramDemo E \book»\md7code\01 E Vbooks\md7code\01\Frame$1 E \book$\md7code\01\Frames1 E Vbook$\md7code\01 \Frames1 E \booksVnd7code\01\Frames1 E \books\md7code\0l \Frame$1 E \booksVnd7code\0l\Frames1 E \book$\md7code\0l \Frames1 E \book$Vnd7code\01\ToDoTe$t E \book$Vnd7code\01M)iagram0emo Рис. 1.9. Многоцелевой Project Manager среды Delphi ПРИМЕЧАНИЕ-------------------------------------------------------------- В Delphi 7 локальное меню Project Manager позволяет с помощью пунктов Make All From Here (Сде- лать все, начиная с) или Build All From Неге (Скомпоновать все, начиная с) компилировать проекты, начиная с заданного. При использовании этих пунктов в отношении первого проекта группы будет получен тот же эффект, что и при использовании пунктов контекстного меню Compile All и Build All. Среди дополнительных функциональных возможностей Project Manager мож- но отметить возможность перетаскивания файлов с исходным программным ко- дом из папок Windows или Проводника Windows в проект, находящийся в окне Project Manager, добавляя, таким образом, их в этот проект (механизм «drag-and- drop» также поддерживается при открытии файлов в редакторе программного кода). Можно сразу просмотреть, какой проект в текущий момент выбран, и изменить его либо с помощью раскрывающегося списка в верхней части окна Project Manager, либо с помощью раскрывающей стрелки, расположенной рядом с кнопкой Run па- нели инструментов Delphi. Помимо добавления в Project Manager Pascal-файлов и отдельных проектов в него можно добавить файлы ресурсов. Они будут откомпилированы вместе с про- ектом. Просто перейдите в проект, выберите в контекстном меню команду Add (До- бавить) и выберите в качестве добавляемого тип файла Resource File (*.гс). Файл Ресурса будет автоматически связан с этим проектом, даже без соответствующей Директивы SR. Delphi сохраняет группу проектов в виде файла с расширением .BGP, являю- щимся сокращением фразы «Borland Project Group». Эта функциональная возмож- ность пришла из C++Builder и из последних компиляторов Borland C++. Это можно заметить, открыв исходный код группы проектов, который, по сути, является make- file в среде разработки C/C++. Вот простой пример: Version = bws oi 0061
62 Глава 1. Среда Delphi 7 и ее IDE #-------------------------- hfndef ROOT ROOT = $(MAKEOIR)\ 'endif #-------------------------- MAKE = $(R00T)\bin\make exe -$(MAKEFLAGS) -f$** OCC = $(R00T)\bin\dcc32 exe $** BRCC = $(R00T)\bm\brcc32 exe $** #-------------------------- PROJECTS = Projectl exe #-------------------------- default $(PROJECTS) #-------------------------- Projectl exe Projectl dpr $(DCC) Project Options Project Manager не позволяет одновременно устанавливать параметры для двух различных проектов. Вместо этого для каждого проекта можно вызвать диалого- вое окно Project Options (Параметры проекта) из Project Manager. Первая страница окна Project Options (Forms) содержит список форм, которые должны создаваться автоматически при запуске программы, а также форм, которые создаются вруч- ную при работе программы. Следующая страница (Application) используется для определения имени приложения и имени файла справки, а также выбора значка приложения. Прочие настройки Project Options относятся к компилятору и компо- новщику Delphi, сведениям о версии и использовании run-time-пакетов. Существует два способа установки параметров компилятора. Первый заключа- ется в использовании страницы Compiler (Компилятор) диалогового окна Project Options (Параметры проекта). Второй — в установке или удалении индивидуаль- ных параметров непосредственно в исходном программном коде с помощью ди- ректив {$Х+} и {$Х-}, в которых X заменяется параметром, который необходимо из- менить. Второй подход более гибкий, поскольку он позволяет изменять параметры только для определенного файла исходного кода или даже нескольких строк этого кода. Параметры на уровне программного кода перекрывают параметры, опреде- ленные на уровне компилятора. Все параметры проекта автоматически сохраняются вместе с проектом, но в от- дельном файле с расширением DOF. Этот текстовой файл доступен для редакти- рования. Нельзя удалить этот файл, если изменено значение по умолчанию любо- го из его параметров. Delphi также сохраняет параметры компилятора в формате .CFG-файла для компиляции, запускаемой из командной строки. Два файла име- ют одинаковое содержание, но различный формат: запускаемый из командной стро- ки компилятор dec не понимает .DOF-файлы; ему требуется .CFG-файл. Еще одним альтернативным вариантом сохранения параметров компилятора является нажатие Ctr 1+0+0 (при нажатой клавише Ctrl дважды нажимается клави- ша 0). Это сочетание вставляет (в верхней части текущего модуля) директивы ком- пилятора, соответствующие текущим параметрам проекта (включая все новые на- стройки предупреждений компилятора): {$Ав В- С+ D+ Е- F- G+ Н+ I+.J-.K- L+ М- Ь‘+ 0+ Р+ Q- R- S-.T- U- И+ Ы- Х+ Y+ Z1} {SMINSTACKSIZE $00004000} 0062
управление проектами 63 /fMAXSTACKSIZE $00100000} г$IMAGEBASE $00400000} (SAPPTYPE GUI} {$UARN SYMBOL ^DEPRECATED ON} (WARN SYMBOL_LIBRARY ON} (WARN SYMBOL_PLATFORM ON} {$NARN UNIT_LIBRARY ON} [$WARN UN IT-PLATFORM ON} [$'MRN UNIT_DEPRECATED ON} /WARN HRESULT_COMPAT ON} /WARN HIDING-MEMBER ON} ($'MRN HIDDEN-VIRTUAL ON} [$'MRN GARBAGE ON} {$UARN BOUNDS-ERROR ON} {SIiIARN ZERO-NIL_COMPAT ON} {WARN STRING-CONSTJRUNCED ON} ($'MRN FOR_LOOP-VAR-VARPAR ON} ($WARN TYPED_C0NSTJARPAR ON} (f'NARN ASG_TO_TYPED_CONST ON} {$UARN CASE_LABEL_RANGE ON} {$UARN FORJARIABLE ON} {WARN CONSTRUCTING-ABSTRACT ON} ($NARN COMPARISON-FALSE ON} {$NARN COMPARISON_TRUE ON} {$UARN COMPARING_SIGNED_UNSIGNED ON} {$WARN COMBINING_SIGNED_UNSIGNED ON} {WARN UNSUPPORTED_CONSTRUCT ON} {WARN FILE_OPEN ON} {WARN FILE_OPEN_UNITSRC ON} {WARN BAD_GLOBAL_SYMBOL ON} {WARN DUPLICATE-CTOR-DTOR ON} {WARN INVALID-DIRECTIVE ON} {WARN PACKAGE-NO-LINK ON} {$WARN PACKAGED-THREADVAR ON} {$NARN IMPLICIT-IMPORT ON} {WARN HPPEMITJGNORED ON} {$UARN NO-RETV AL ON} {WARN USE_BEFORE-DEF ON} {WARN FOR-LOOP-VAR UNDEF ON} {WARN UNIT_NAME-MISMATCH ON} {WARN NO-CFG-FILE-FOUND ON} {WARN MESSAGE-DIRECTIVE ON} {WARN IMPLICIT-VARIANTS ON} {WARN UNICODE-TO-LOCALE ON} {WARN LOCALE_TO-UNICODE ON} {WARN IMAGEBASE_MULTIPLE ON} (WARN SUSPICIOUSJYPECAST ON} {$NARN PRIVATE-PROPACCESSOR ON} {WARN UNSAFE_TYPE OFF} (WARN UNSAFE_CODE OFF} {tUARN UNSAFE_CAST OFF} Компиляция и компоновка проектов Существует несколько путей компиляции проектов. Если вы запускаете проект (нажатием клавиши F9 или щелчком на значке Run панели инструментов), Delphi 0063
64 Глава 1. Среда Delphi 7 и ее IDE сначала его откомпилирует. При этом компилируются только измененные файлы. Если же выбрать Project (Проект) ► Build All (Скомпоновать все), то будут отком- пилированы все файлы, даже если они не были изменены. Второй вариант требу- ется редко, поскольку среда Delphi обычно может сама определить, какие файлы были изменены, и при необходимости их откомпилировать. Единственным исклю- чением является случай, когда изменяются некоторые параметры проекта, и для вступления их в силу требуется использование пункта Build All. Для компоновки проекта Delphi сначала компилирует каждый файл исходного кода, генерируя откомпилированный модуль Delphi (Delphi Compiled Unit, DCU). (Этот шаг выполняется, только если существующий DCU-файл устарел.) Следу- ющий шаг выполняется компоновщиком (редактором связей), который осуществ- ляет слияние всех DCU-файлов в исполняемый файл, иногда совместно с отком- пилированным кодом из VCL-библиотеки (если вы решили не использовать пакеты во время исполнения). Третьим шагом осуществляется привязка к исполняемому файлу дополнительных файлов ресурсов, таких как RES-файлов проекта, в кото- рых располагаются основной значок приложения и DFM-файлы форм. Вы сможе- те лучше понять и отследить порядок выполнения этой операции, если включите параметр Show Compiler Progress (Показать процесс компиляции) (на странице Prefe- rences (Настройки) диалогового окна Environment Options (Параметры среды)). ВНИМАНИЕ -------------------------------------------------------------- Delphi не всегда корректно выполняет отслеживание при перекомпоновке модулей, основанных на других измененных модулях. Особенно это относится к тем случаям (и таких много), когда вмеша- тельство пользователя нарушает логику работы компилятора. Например, переименование файлов, изменение исходных файлов не-IDE средствами, копирование более старых исходных файлов или DCU, либо наличие множества копий исходного файла модуля в пути поиска могут привести к прерыванию процесса компиляции. Каждый раз, когда компилятор выдает странные сообщения об ошибках, необходимо сначала попробовать выполнить пункт Build All, осуществляющий ресинхро- низацию функции make с существующими на диске файлами. Команда Compile (Компилировать) может использоваться только после загруз- ки проекта в редактор. Если отсутствует активный проект, и вы загрузили файл с исходным кодом Pascal, его откомпилировать не удастся. Однако если загрузить файл с исходным кодом таким образом, чтобы он являлся проектом, это будет улов- кой, которая позволит откомпилировать его. Для этого просто щелкните кнопку Open Project (Открыть проект) на панели инструментов и загрузите PAS-файл. Те- перь в нем можно проверить синтаксис и откомпилировать, создав DCU-файл. Как уже упоминалось выше, среда Delphi позволяет использовать run-time-па- кеты, которые влияют на возможность распространения программ даже больше, чем сам процесс компиляции. Delphi-пакеты — это динамически подсоединяемые библиотеки (dynamic link libraries, DLL), содержащие компоненты Delphi. За счет использования этих пакетов вы можете сделать исполняемый файл значительно меньше. Однако программа не сможет быть выполнена, пока необходимые DDL- библиотеки (такие, как vcl7O.bpl, которая является достаточно большой) не будут доступны на компьютере, на котором вы пытаетесь запустить эту программу. Если сложить размер динамической библиотеки и визуально небольшого ис- полняемого файла, общий размер получится значительно больше, чем размер файла этой же программы, скомпонованной с run-time-пакетами. Конечно же, при разме- 0064
управление проектами 65 щении на одной системе множества приложений удастся сэкономить большой объем дисковой памяти и оперативной памяти в ходе выполнения. Использование пакетов рекомендовано часто, но не всегда. Все последствия использования паке- тов мы более подробно рассмотрим в главе 10. В обоих случаях исполняемые файлы Delphi компилируются очень быстро, а скорость выполнения полученного приложения совместима со скоростью С и C++ программ. Откомпилированный код Delphi выполняется, по меньшей мере, в пять раз быстрее, чем эквивалентный код в интерпретирующих или «не полностью ком- пилирующих» средах. Подсказки и предупреждения в сообщениях компилятора Как я уже говорил в начале этой главы (см. раздел «Расширенные сообщения ком- пилятора и результаты поиска в Delphi 7»), помимо классических сообщений ком- пилятор Delphi 7 предоставляет новое окно с дополнительными сведениями к не- которым сообщениям об ошибках. Это окно активизируется с помощью пункта меню View (Вид) ► Additional Message Info (Дополнительные сообщения). Оно вы- водит сведения, хранящиеся в локальном файле, который можно обновить, загру- зив более свежую версию с веб-сайта компании Borland. Еще одно изменение в Delphi 7 связано с предоставлением больших возможно- стей в отношении предупреждений компилятора. Теперь в диалоговом окне Project Options (Параметры проекта) имеется страница Compiler Messages (Сообщения ком- пилятора), на которой можно выбрать множество отдельных предупреждений. Эта возможность, вероятно, была внедрена ввиду того, что Delphi 7 имеет новый набор предупреждений, обладающих в будущем совместимостью с технологией .NET в Delphi. Эти предупреждения достаточно объемные и я отключил их (рис. 1.10). ИгеаоггмГСопЛопак | Version Irfo | Packages Forms I ДррБсаГюл | Conner Согфйег Messages | Linker p General----------------------------- | P P SfMiw warnings Ц & Unit identifier does not match file name No configuration files found User message И Implicit use of Variants unit Error converting Unicode char to locale charset । Error converting locale string to Unicode У Imagebase is not a multiple of 64k I S/ Sijspicrous typecast 3 ;Р|0регЬ,' declaration references pm/ate. ancestor member... I __ Unsafe type C Unsafe code i Unsafe typecast Г default Help Рис. 1.10. Новая страница Compiler Messages в диалоговом окне Project Options Включить или отключить некоторые из этих предупреждений можно с помощью Директив компилятора, подобных этим: 0065
66 Глава 1. Среда Delphi 7 и ее IDE {$Uarn UNSAFE_CODE OFF} {$Ыагп UNSAFE_CAST OFF} ($Narn UNSAFEJYPE OFF} В общем, лучше сохранить эти настройки вне исходного кода или программы — наконец, Delphi 7 позволяет это сделать. Изучение классов проекта Delphi уже имеет средство поиска идентификаторов (symbols) откомпилирован- ного проекта, хотя наименование этого инструмента менялось несколько раз (Object Browser, Project Explorer, а теперь — Project Browser). В Delphi 7 окно Project Browser активизируется с помощью меню View (Вид) ► Browser (Браузер) (рис. 1.11). Браузер позволяет просмотреть иерархическую структуру классов проекта и най- ти их идентификаторы и строки исходного кода, которые на них ссылаются. loring Classes Jabals• pSseT i УгШ I Я TCustomTreeView TTreeView Я *4 TCustomUpDown 3^ TUpDown T PageS croller > TProgressBar TRedirectorWindow TScrollBar \ '7I 9^ TScrollingWinControl E TCustomForm TCustotnActiveForm x. TCustomDockForm TToolDockForm < fe TForm "31 *4 Ws1 Scope | Inhentance | Beferences | E 2J Pushed Edrtl FormCreate Labell Lis tB ox1 PopupMenul one1 two1 TMessageForm F TCustomFrame IFrame TScrollBox T TabSheet Fl T Too Window TCoolBar TToolBar ЗТТгаскВаг TWinControlAccess +1 Private Я Protected Я □ Public Action Action Active ActiveControl ActiveMDIChild ActiveOleControt AherConstruction AfterConstruction Align AhgnDisabled Anchors *4 4 4 4 4 4 9 4 4 »а Рис. 1.11. Project Browser В отличие от Code Explorer, Project Browser обновляется только после пере- компиляции проекта. Этот браузер позволяет просмотреть список классов, моду- лей, глобальных элементов, а также просмотреть только обозначения, определен- ные внутри вашего проекта или обозначения вашего проекта и VCL. Настройки Project Browser и Code Explorer можно изменить на странице Explorer диалогового окна Environment Options или выбрав пункт Properties (Свойства) в контекстном меню Project Browser Некоторые из категорий, которые представлены в этом окне, ха- рактерны только для Project Browser, в то время как другие — для обоих инстру- ментов. 0066
Дополнительные и внешние средства Delphi 67 Дополнительные и внешние средства Delphi В дополнение к IDE при установке Delphi предлагаются другие, внешние сред- ства. Некоторые из них, такие как Database Desktop, Package Collection Editor (PCE.exe) и Image Editor (ImagEdit.exe), доступны с помощью меню Tools (Сервис) IDE. Помимо них в редакции Enterprise имеется ссылка на SQL Monitor (SqLMon. ехе). Другие средства недоступны непосредственно из IDE (множество утилит, запускаемых из командной строки, размещенных в каталоге BIN). Например, сре- ди этих средств можно найти компилятор Delphi, запускаемый из командной строки (DCC32.exe), компилятор ресурсов Borland (BRC32.exe и BRCC32.exe), а также про- грамма просмотра (TDump.exe). И, наконец, некоторые программы-примеры, поставляемые совместно с Delphi, являются действительно полезными средствами, которые можно откомпилировать и «подержать в руках». Некоторые из этих средств будут рассмотрены по мере не- обходимости далее в этой книге. Вот несколько полезных средств высокого уров- ня, большинство из которых доступны в каталоге \Delphi7\biп и с помощью меню Tools (Сервис). Web Арр Debugger (WebAppDbg.exe) Отладочный веб-сервер появился в Delphi 6. Он используется для отслеживания запросов, направляемых к вашему приложению, и позволяет отлаживать их. В Del- phi 7 этот отладчик полностью обновлен: теперь он является CLX-приложением и его подключения основаны на сокетах Это средство будет рассмотрено в главе 20. XML Mapper (XmlMapper.exe) Средство создания XML-преобразований, применяемых к формату, производимо- му компонентом ClientDataSet. Дополнительную информацию об этом средстве можно найти в главе 22. External Translation Manager (etm60.exe) Автономная версия Integrated Translation Manager (Интегрированный менеджер перевода). Это внешнее средство может придаваться к внешним трансляторам. Впервые оно появилось в Delphi 6. Borland Registry Cleanup Utility (D7RegClean.exe) Средство, которое помогает удалять все элементы реестра, добавленные в компь- ютер средой Delphi 7. TeamSource Расширенная система управления версиями, поставляемая вместе с Delphi начиная с версии 5. Этот инструмент очень похож на свою предыдущую версию и устанав- ливается отдельно от Delphi. Delphi 7 поставляется вместе с версией TeamSour- се 1.01, эта же версия становится доступной после установки в Delphi 6 програм- мы-«заплатки». WinSight (Ws32.exe) Windows-программа «шпион сообщений» располагается в каталоге bin. Database Explorer Средство, которое можно активизировать из IDE или как автономное приложе- ние, запустив программу DBExplor.exe в каталоге bin. Поскольку Database Explorer “л предназначен для BDE, в настоящее время он неактуален. 0067
68 Глава 1. Среда Delphi 7 и ее IDE OpenHelp (oh.exe) Средство, используемое для управления структурой собственных файлов справки Delphi и интеграции файлов сторонних производителей в эту справочную систему. Convert (Convert.exe) Утилита командной строки, используемая для преобразования DFM-файлов в эк- вивалентное текстовое описание и наоборот. Turbo Grep (Grep.exe) Утилита поиска, запускаемая из командной строки, которая работает значительно быстрее, чем встроенный механизм Find In Files, но не так проста в использовании. Turbo Register Server (TRegSvr.exe) Средство, которое может использоваться для регистрации библиотек ActiveX и сер- веров СОМ. Исходный код этого средства располагается в каталоге \Demos\ActiveX\ TregSvr. Resource Explorer Мощное средство просмотра ресурсов (но не являющееся полномасштабным ре- дактором ресурсов), которое можно найти в \Demos\ResXplor. Resource Workshop ,v Старый 16-битный редактор ресурсов, который также может использоваться для управления 32-разрядными файлами ресурсов. Установочный компакт-диск Delphi содержит отдельную программу установки Resource Workshop. Ранее он входил в состав компиляторов Borland C++ и Pascal, предназначенных для работы в Win- dows, и был значительно лучше стандартных редакторов ресурсов, предлагаемых компанией Microsoft. Хотя его пользовательский интерфейс не обновлен, и он не поддерживает длинных имен файлов, этот инструмент по-прежнему очень поле- зен для создания настраиваемых или специальных ресурсов. Он также позволяет исследовать ресурсы существующих исполняемых файлов. Файлы, создаваемые системой Для каждого проекта среда Delphi создает большое количество файлов, и вы дол- жны знать, для чего они предназначены и как называются. На имя файлов в основ- ном влияют два элемента: имена, присваиваемые проекту и его модулям, а также предварительно определенные расширения файлов, используемые средой Delphi. В табл. 1.1 перечислены расширения файлов, с которыми можно столкнуться в ка- талоге, содержащем проект Delphi. В таблице также перечислено: когда и из каких соображений создаются эти файлы, и их важность для последующих операций компиляции. Таблица 1.1. Расширения файлов проекта Delphi Расширение Тип файла и описание Время создания Требуется ли при компиляции? .BMP, .ICO, .CUR Файл битового изображе- ния, значка или курсора: стандартные файлы Win- При разработке: Image Editor Обычно нет, но они могут понадобиться в ходе выполнения и для 0068
Файлы, создаваемые системой 69 расширение Тип файла и описание Время создания Требуется ли при компиляции? .BPG dows, используемые для хранения битовых изображений Borland Project Group: При разработке последующего редактирования Требуется для переком- .BPL файлы, используемые новым многоцелевым Project Manager, являю- щимся подвидом makefile Borland Package Library: При компиляции и пиляции всех проектов группы одновременно Пакеты необходимо пре- САВ DLL-библиотека, включа- ющая VCL-компоненты, которые используются средой Delphi во время разработки или приложе- нием во время выполнения (В Delphi 3 эти файлы имели расширение .DPL) Microsoft Cabinet: формат компоновке При компиляции доставлять другим разра- ботчикам Delphi и, воз- можно, конечным пользователям Используются для рас- гге сжатия файлов, использу- емый для веб-разверты- вания посредством Delphf. САВ-файл может содержать множество сжатых файлов Файл конфигурации с па- При разработке пространения программ Требуется только при DCP раметрами проекта. Подобен DOF-файлу Delphi Compiled Package: При компиляции установке специального параметра компилятора Требуется при исполь- DCU файл со сведениями об идентификаторах для программного кода, компи- лируемого в пакет. В нем не содержится компилиро- ванного кода, который помещается в DCU-файлы или в BPL-файл Delphi Compiled Unit: ре- При компиляции зовании run-time-пакетов. Распространяется только для других разработчиков совместно с BPL-файлами. Можно скомпилировать приложение с его моду- лями из пакета, лишь имея DCP-файл и BPL (без DCU-файлов) Только если исходный DDP зультат компиляции Pascal-файла Delphi Diagram Portfolio, При разработке программный код недоступен. DCU-файлы для написанных модулей являются промежуточным шагом, ускоряющим общую компиляцию Нет. Этот файл хранит DFM — используемый для вида Diagram в редакторе (файл .DTI в Delphi 5) Delphi Form File: двоичный файл с описанием свойств формы (или модуля дан- ных), а также содержа- щихся в нем компонентов При разработке сведения «только этапа разработки» и не требу- ется конечной программе, но очень важен для программиста Да. Каждая форма хра- нится как в PAS-файле, так и в DFM-файле продолжение 0069
70 Глава 1. Среда Delphi 7 и ее IDE Таблица 1.1. {продолжение) Расширение Тип файла и описание Время создания Требуется ли при компиляции? .~DF Резервная копия файла формы Delphi (DFM) При разработке Нет. Этот файл создается при сохранении новой версии модуля, связан- ного с формой, и файла формы DFN Вспомогательный файл для ин- тегрированной среды перево- да (Integrated Translation Environ- ment) (существует только один DFN-файл для каждой формы и для каждого целевого языка) При разработке (ITE) Да (для ITE). Эти файлы содержат переведенные строки, которые редакти- руются в Translation Manager .DLL Dynamic link library: дополни- тельный вариант исполняемо- го файла При компиляции: компоновка См. EXE .DOF Delphi Option File: текстовой файл с текущими установками параметров проекта При разработке Требуется только при установке специальных параметров компилятора .DPK, а также .DPKW и .DPKL Delphi Package: пакет файла исходного кода проекта (или специальный файл проекта для Windows или Linux) При разработке Да .DPR Файл Delphi-проекта. (Этот файл в действительности содержит исходный код Pascal) При разработке Да •~DP Резервная копия файла про- екта Delphi (.DPR) При разработке Нет. Этот файл генери- руется автоматически при сохранении новой версии файла проекта .DSK Файл Desktop: содержит сведе- ния о позициях окон Delphi, от- крытых в редакторе файлах и другие настройки рабочего стола При разработке Нет. При копировании проекта в новый каталог его необходимо просто уничтожить DSM Delphi Symbol Module: хранит всю идентификационную информацию браузера При компиляции (но только если установлен пара- метр Save Symbols) Нет. Object Browser ис- пользует этот файл вместо данных из памяти, если вы не можете пере- компилировать проект .EXE Исполняемый файл: созданное вами Windows-приложение При компиляции: компоновка Нет. Это тот файл, который распространя- ется. Он содержит все откомпилированные модули, формы и ресурсы .HTM Или HTML: Hypertext Markup Language: формат файлов, используемый для веб-страниц Интернета При размещении в Сети ActiveForm Нет. Он не участвует в процессе компиляции проекта .LIC Файлы лицензии, относящиеся к ОСХ-файлу Используется ActiveX Wizard и дру- Нет. Он требуется для использования элементов 0070
файлы, создаваемые системой 71 Расширение Тип файла и описание Время создания Требуется ли при компиляции? гими инструменталь- ными средствами управления в иной среде разработки .OBJ Объектный (откомпилирован- ный) файл, используемый обычно в среде C/C++ Промежуточный этап компиляции. В Delphi обычно не исполь- зуется Может потребоваться в отдельном проекте для слияния откомпилирован- ного кода Delphi с кодом C++ OCX OLE Control Extension: специаль- ная версия DLL, содержащая элементы управления и формы ActiveX При компиляции См. .EXE .PAS Pascal-файл: исходный програм- мный код модуля на языке Pascal При разработке Да .~РА Резервная копия Pascal-файла (.PAS) При разработке Нет. Этот файл генериру- ется средой Delphi авто- матически при сохране- нии новой версии исход- ного кода .RES, .RC Файл ресурсов: двоичный файл, связанный с проектом. В нем, как правило, содержится значок. Допускается добавле- ние в проект нескольких файлов этого типа. При создании поль- зовательского файла ресурсов можно использовать и тексто- вой формат: .RC Определяется на- стройками диалого- вого окна Develop- ment Options. ITE (Интегрированная среда перевода) ге- нерирует файлы ресурсов со специ- альными коммен- тариями Да. Основной RES-файл приложения перестра- ивается средой Delphi в соответствии с настрой- ками страницы Application диалогового окна Project Options RPS Translation Repository (Храни- лище перевода) (часть Integra- ted Translation Environment) При разработке (ITE) Нет. Требуется для управ- ления переводами •TLB Библиотека типов: файл, созда- ваемый для приложений OLE- сервера автоматически или редактором Type Library Editor При разработке Этот файл может потре- боваться для других OLE- программ TODO Файл списка To-Do, содержа- щий элементы, связанные со всем проектом При разработке Нет. Этот файл содержит заметки для програм- мистов UDL Microsoft Data Link При разработке Используется ADO для ссылки на провайдер данных. Подобен псевдо- ниму в среде BDE (см. главу 12) Помимо файлов, генерируемых в ходе разработки проекта Delphi, существует множество других, генерируемых и используемых самой IDE. Список заслужива- ющих внимание расширений представлен в табл. 1.2. Большинство из этих файлов являются собственными и недокументированными форматами, поэтому с ними вряд ли что нужно делать. 0071
72 Глава 1. Среда Delphi 7 и ее IDE Таблица 1.2. Некоторые расширения файлов настройки IDE Delphi Расширение Описание .DCI .DRO Шаблоны программного кода Delphi Хранилище объектов Delphi (хранилище должно модифицироваться с помощью команды Tools ► Repository) .DMT .DBI .DEM Шаблоны меню Delphi Сведения Database Explorer Маска редактирования Delphi (файлы с форматами масок, характерными для определенной страны) .DCT .DST Шаблоны компонентов Delphi Настройки рабочего стола (по одному для каждой из определенных пользователем настроек) Просматривая файлы с исходным кодом Сейчас только что были представлены несколько файлов, относящихся к разра- ботке приложений Delphi, но на преобразование их действительного формата потребовалось очень мало времени. Базовые файлы Delphi представляют собой файлы, содержащие исходный код на'языке Pascal, являясь, по сути, текстовыми ASCII-файлами. Использование жирного шрифта, курсива и цвета, которые мож- но наблюдать в редакторе, основаны на синтаксическом выделении, которое в файле не сохраняется. ПРИМЕЧАНИЕ --------------------------------------------------- В листингах, представленных в этой книге, выделение жирным шрифтом соответствует ключевым словам языка Pascal, а курсив — строчным значениям и комментариям. Pascal-файл формы содержит объявление класса формы и исходный код обра- ботчиков событий. Значения свойств, которые установлены в Object Inspector (Инспекторе объектов), хранятся в отдельном файле описаний (с расширением .DFM). Единственным исключением из этого правила является свойство Name, которое используется в объявлении формы для ссылки на компоненты формы. .DFM-файл по умолчанию является текстовым представлением формы, но он также может быть сохранен в двоичном формате ресурса Windows Resource. Фор- мат, используемый в новых проектах, можно установить на странице Designer (Кон- структор) диалогового окна Environment Options (Параметры окружения), а пере- ключение формата отдельных форм — с помощью пункта Text DFM контекстного меню формы. Редактор открытого текста может воспроизводить только текстовую версию. Однако в редактор Delphi можно загрузить любую версию DFM-файла, что приведет, при необходимости, к преобразованию последнего в текстовой вид. Самый простой способ открыть текстовое описание формы (независимо от фор- мата) заключается в выборе пункта View As Text (Просмотр в виде текста) в контек- стном меню конструктора форм (Form Designer). Выбор этого пункта приводит к закрытию формы, ее сохранения (при необходимости) и открытию DFM-файла в редакторе. Позже можно вернуться к форме, выбрав пункт View As Form (Про- смотр в виде формы) в контекстном меню окна редактора. Можно редактировать текстовое содержание формы, хотя делать это рекомен- дуется очень внимательно. Как только файл будет сохранен, он будет проанали- 0072
Object Repository 73 зирован и использован при генерации формы. Если были внесены некорректные изменения, компиляция прекратится сообщением об ошибке; перед повторным от- крытием формы вам придется исправить содержание DFM-файла. Поэтому реко- мендуется воздержаться от ручного изменения текстового описания формы до тех пор, пока вы хорошо не освоите программирование в Delphi. ПРИМЕЧАНИЕ-------------------------------------------------------------- В этой книге представлены лишь выдержки из DFM-файлов. Обычно в них представлены только наиболее значимые компоненты и свойства. Как правило, я удалял свойства позиционирования, значения, представленные в двоичном формате, а также малоинформативные строки. Помимо этих двух файлов, описывающих форму (PAS и DFM), третьим фай- лом, жизненно необходимым для перестроения приложения, является файл про- екта Delphi (DPR), который является еще одним файлом с исходным кодом на языке Pascal. Этот файл создается автоматически и его редко приходится изме- нять вручную. Просмотреть этот файл можно с помощью пункта меню Project (Про- ект) > View Source (Источник). Среди прочих менее значимые файлы, создаваемые IDE, используют структу- ру Windows INI-файлов, в которых каждый раздел выделен именем, заключенным в квадратные скобки. Например, вот фрагмент файла параметров (DOF): [Compiler] А=1 В=0 ShowHints=l ShowWarm ngs=l [Linker] MinStackSize=16384 MaxStackSize=1048576 ImageBase=4194304 [Parameters] RunParams= HostApplication= Такая же структура используется файлами рабочего стола (DSK), которые хра- нят настройки IDE среды Delphi определенного проекта, перечисляя позиции каж- дого окна. Вот небольшой фрагмент: [MainWindow] Create=l Visible=l State=0 Left-2 Top=0 Width=800 Height=97 Object Repository Delphi имеет пункты меню, которые можно использовать для создания новой фор- мы, нового приложения, нового модуля данных, нового компонента и т. д. Эти пун- кты находятся в меню File (Файл) ► New (Новый) и других раскрывающихся меню. 0073
74 Глава 1. Среда Delphi 7 и ее IDE Можно также просто выбрать File ► New ► Other (Другие) и Delphi откроет Object Repository (Хранилище объектов). Его можно использовать для создания новых элементов любого типа: форм, приложений, модулей данных, объектов-потоков, библиотек, компонентов, объектов автоматизации и других. . Диалоговое окно New Items (Новые элементы) (рис. 1.12) имеет несколько стра- ниц, на которых размещены все элементы, которые можно создать, а также суще- ствующие формы и проекты, сохраненные в хранилище объектов, мастера Delphi и формы текущего проекта (для визуального наследования форм). Наличие конк- ретных страниц и элементов определяется версией Delphi, поэтому я не буду пере- числять их. bftaWeb | WabSendtes | | WebSnap | АейубХ | Mufofer | Project | Fams | 1 Console Application В ® 0 8S2SS3S8 Batch Frle CLX Component Application Contiol Panel Application Frame Package CorrtidPanel DataModUe DLLWizad Module Project Group Resource DLL Wizard Рис. 1.12. Первая страница диалогового окна New Items (известна как хранилище объектов) ПРИМЕЧАНИЕ---------------------------------------------------------------- Хранилище объектов имеет контекстное меню, которое может использоваться для сортировки его элементов различным образом (по имени, по автору, по дате или по описанию) и для просмотра в различном виде (крупные значки, маленькие значки, список и таблица). При просмотре в виде таблицы можно увидеть описание, автора и дату создания элемента, т. е. сведения, которые явля- ются чрезвычайно важными при выборе мастеров, проектов или форм, добавленных в хранилище. Самый простой способ перестройки хранилища объектов заключается в добав- лении новых проектов, форм и модулей данных в качестве шаблонов. Кроме того, можно добавлять новые страницы и упорядочивать пункты на некоторых из них (исключая страницы New (Новый) и страницу «текущего проекта»). Добавление нового шаблона в хранилище объектов Delphi является настолько же простой опе- рацией, как и использование существующего шаблона для создания нового прило- жения. При наличии работающего приложения, которое вы хотите использовать в качестве начальной точки для последующих разработок подобных программ, которые можно использовать дальше, достаточно лишь сохранить текущее состоя- ние в шаблон. Воспользуйтесь пунктом Project (Проект) ► Add То Repository (Доба- вить в хранилище) и заполните его диалоговое окно. Аналогично добавлению в хранилище объектов новых шаблонов проектов мож- но добавить новые шаблоны форм. Перейдите на форму, которую вы хотите доба- вить, и выберите в ее контекстном меню пункт Add То Renositoru ( ЛлАяпить n vnaw«. 0074
Object Repository 75 лище). После этого укажите на конечной диалоговой странице заголовок, описа- ние, сведения об авторе, странице и значке. Учтите, что если вы копируете проект или форму в хранилище, а потом переносите их в другой каталог, вы просто вы- полняете операцию «вырезать-вставить», что практически не отличается от руч- ного копирования файлов. Пустой шаблон проекта При создании нового проекта Delphi автоматически открывает пустую фор- му. Однако если вы хотите в качестве основы использовать один из объек- тов-форм или мастер, вам не нужна эта форма. Для решения этой проблемы можно добавить в Галерею (Gallery) шаблон Empty Project (Пустой проект). Выполняется это очень просто: 1. Создайте новый проект как обычно. 2. Удалите только форму проекта. 3. Добавьте этот проект к остальным шаблонам, назвав его Empty Project. При выборе этого проекта в хранилище будут предоставлены два пре- имущества: вы получаете проект без формы и можете указать каталог, в ко- торый будут скопированы файлы шаблона проекта. Существует также и один недостаток: после этого необходимо не забыть воспользоваться пунктом File ► Save Project As для указания проекту нового имени, поскольку при со- хранении проекта любым другим способом автоматически будут использо- ваны имена, указанные в этом шаблоне по умолчанию. При дальнейшей настройке хранилища можно воспользоваться командой Tools (Сервис) ► Repository (Хранилище). Оно открывает диалоговое окно Object Repository (Хранилище объектов), с помощью которого можно переместить элементы на дру- гие страницы, добавить новые элементы или удалить существующие. Можно даже добавлять новые страницы, переименовывать их и удалять, а также изменять по- рядок страниц. Важным элементом настройки хранилища объектов является ука- зание значений по умолчанию: О форма, которая будет использоваться при создании новой формы (с помощью команды File ► New Form). Установите флажок Use the New Form, расположенный ниже списка объектов; О флажок Main Form (Основная форма) указывает, какой тип формы использо- вать при создании основной формы нового приложения (File ► New Application), если не выбран New Project; О флажок New Project, доступный при выборе какого-либо проекта, отмечает про- ект по умолчанию, который будет использоваться Delphi при выборе пункта меню File ► New Application. Только одна форма и только один проект в хранилище объектов может иметь эти три установки, которые будут представлены специальной отметкой, помещен- ной над их значками. Если в качестве New Project не выбран ни один проект, Delphi создает проект по умолчанию, основанный на форме, отмеченной как Main Form. Если в качестве основной формы (Main Form) не отмечена ни одна форма, то Delphi создает проект по умолчанию с пустой формой. 0075
76 Глава 1. среда Dgfrttidhatejeg При работе с хранилищем объектов вы работаете с формами и объектами, со- храненными в подкаталоге ОBJREPOS основного каталога Delphi. В то же время при использовании формы или любого другого объекта непосредственно, без его ко- пирования, вы теряете часть файлов проекта в этом каталоге. Это важно для осоз- нания сути функционирования хранилища, поскольку при необходимости мо- дификации проекта или объекта, сохраненного в хранилище, самым лучшим вариантом является работа с оригинальными файлами без копирования данных из хранилища и обратно. Установка новых DLL-мастеров Технически новые мастера поступают в двух различных формах: они могут быть частью компонентов или пакетов, а также могут распространяться в виде автономных DLL. В первом случае они устанавливаются так же, как компо- нент или пакет, с помощью команды меню Components ► Install Packages. При получении автономной DLL необходимо добавить имя этой библиотеки в ре- естр Windows в разделе \Software\Borland\Delphi\7.0\Experts. Просто добавь- те в этом разделе новый ключ строчного типа, выберите любое имя (не важ- но, какое), а в качестве текста укажите путь и имя файла устанавливаемой библиотеки. Как должен выглядеть путь, вы можете посмотреть у других мастеров, которые уже присутствуют в разделе Experts. Новшества в отладчике Delphi 7 При запуске программы в IDE Delphi вы, как правило, запускаете ее во встроен- ном отладчике. Можно устанавливать точки останова, выполнять код по строкам и исследовать внутренние детали, включая выполняемый ассемблерный код, а так- же просматривать регистры процессора. В этой книге не хватит места, чтобы охва- тить процесс отладки в Delphi; дополнительную информацию по этому вопросу вы можете найти в Приложении С, однако я бы хотел вкратце рассмотреть пароч- ку новых возможностей. Во-первых, диалоговое окно Run Parameters в Delphi 7 позволяет устанавливать рабочий каталог отлаживаемой программы. Это означает, что текущая папка бу- дет таковой, если вы это укажете, а не папкой, в которую будет компилироваться программа. iWatch List Welch Нате Value В form! caplion Form!' 13 self ([csInhentaUe] False False (0 0] rd Ц S1303GCO) 0 button! fell 104 /ciealion / Еще одно значимое изменение относится к Watch List. Теперь на нем имеется множество вкладок, позволяющих держать несколько наборов активных просто- ров для различных областей отлаживаемой программы, не создавая нагиоможле- 0076
Что дале| 77 ния в одном окне. В Watch List с помощью контекстного меню можно добавить новую группу, а с помощью соответствующих флажков изменить видимость заго- ловков столбцов и доступность отдельных наблюдений. СОВЕТ------------------------------------------------------------------------ В данной книге отладчик Delphi не рассматривается, но это очень важный вопрос. В Приложении С представлены ссылки на онлайновые материалы и порядок загрузки бесплатной главы, посвя- щенной вопросу отладки в Delphi. Что далее? В этой главе представлен обзор новых и существующих возможностей, которые были расширены в среде программирования Delphi 7, включая советы и подсказ- ки, относящиеся к мене известным возможностям, которые были доступны и в предыдущих версиях Delphi. Здесь нет последовательного рассмотрения IDE, от- части по той причине, что проще сразу приступить к использованию Delphi, чем читать, как его использовать. Кроме того, существует справочная система, в кото- рой подробно рассмотрена данная среда и разработка простого проекта. Вполне возможно, что у вас уже имеется опыт работы с предыдущими версиями Delphi или аналогичными средами разработки. А теперь вы готовы перейти к следующей главе, заглядывающей в язык про- граммирования Delphi. Далее мы продолжим изучение библиотек классов и биб- лиотек этапа исполнения (run-time library, RTL), входящих в состав Delphi. 0077
2 Язык программирования Delphi Среда разработки Delphi основана на объектно-ориентированном расширении язы- ка Pascal, известном как Object Pascal. В последнее время Borland собирается на- зывать этот язык «языком Delphi», поскольку она хочет сказать, что Kylix исполь- зует язык Delphi. Кроме того, компания Borland собирается предоставить язык Delphi на платформе .NET компании Microsoft. Вследствие многолетней привыч- ки я буду поочередно использовать оба названия. Самые современные языки программирования поддерживают объектно-ори- ентированное программирование (ООП). ООП-языки основаны на трех основных принципах: инкапсуляция (обычно реализуемая с помощью классов), наследова- ние и полиморфизм (или динамическое/позднее связывание). Хотя Delphi-npo- грамму можно написать, не вникая в базовые принципы языка, вы не сможете достичь вершин мастерства в данной среде, не разобравшись с языком программи- рования. СОВЕТ-------------------------------------------------------------------- Ввиду ограничения объема книги и из-за того, что сам язык не претерпел за последние годы значи- тельных изменений, в данной главе будет представлено лишь краткое введение в язык. Более подробное описание языка можно найти в последних выпусках книги в виде материалов, представ- ленных на моем веб-сайте (см. Приложение С «Свободно доступные книги по Delphi»). Эти матери- алы также включают Essential Pascal — полное введение в стандартный язык Pascal. В данной главе рассмотрены следующие вопросы: О классы и объекты; О инкапсуляция: private и public, О использование свойств; О конструкторы; О объекты и память; О наследование; О виртуальные методы и полиморфизм; о безопасное приведение типов (RTTI); о интерфейсы; О работа с исключениями; о ссылки классов. 0078
Классы и объекты 79 Базовые характеристики языка Язык Delphi является ООП-расширением классического языка Pascal, который в течение длительного времени с помощью своих компиляторов Turbo Pascal про- двигала компания Borland. Синтаксис языка Pascal известен своей многословно- стью и более прост в восприятии, чем, например, синтаксис языка С. Его ООП- расширение придерживается аналогичного подхода, предоставляя такую же мощь, как и недавно появившиеся ООП-языки Java и С#. Хотя даже ядро языка является предметом продолжающихся изменений, но лишь некоторые из этих модификаций повлияют на вашу повседневную деятельность. В Delphi 6, например, компания Borland добавила поддержку некоторых возможно- стей, более-менее связанных с разработкой Kylix (версией Delphi для ОС Linux): О новая директива условной компиляции ($IF); о набор директив-подсказок (platform, deprecated и library, из которых лишь пер- вая иногда используется) и новая директива $WARN — для их отключения; О директива $ М ESS AG Е для передачи пользовательской информации в компьютер- ных сообщениях. В Delphi 7 добавлено три дополнительных предупреждения компилятора: unsafe type (ненадежный тип), unsafe code (ненадежный код) и unsafe cast (ненадежное, приведение). Эти предупреждения издаются в случае обнаружения операций, ко- торые нельзя использовать для создания надежного «управляемого» кода на плат- форме .NET компании Microsoft (более подробно этот вопрос рассмотрен в гла- ве 25 «Обзор Delphi for .NET: язык и RTL»). Прочие изменения связаны с именами модулей, которые теперь можно состав- лять из множества слов, разделенных точкой (модуль marco.test можно сохранить как файл marco.test.pas). Эта возможность обеспечивает поддержку пространств имен и более удобных ссылок на модули в Delphi for .NET в последующих версиях компилятора Delphi для Windows, хотя в Delphi 7 она имеет ограниченное исполь- зование. Классы и объекты Delphi основана на принципе ООП и особенно на определении новых типов клас- сов. Использование ООП отчасти исходит от визуальной среды разработки, по- скольку каждая новая форма, определенная в ходе разработки, автоматически оп- ределяется Delphi как новый класс. Кроме того, каждый компонент, визуально размещенный на форме, является объектом типа класса, доступным или добавля- емым в системную библиотеку. Как и в большинстве современных ООП-языков (включая Java и С#), в Delphi Переменная типа класса не является хранилищем объекта, но она является един- ственным указателем или ссылкой на объект, расположенный в памяти. Перед тем как использовать объект, для него необходимо выделить оперативную память по- средством создания нового экземпляра или присвоением существующего экземп- ляра этой переменной: 0079
80 Глава 2. Язык программирования Qjphi var Objl. 0bj2: TMyClass; begin // assign a newly created object Objl := TMyClass.Create; // assign to an existing object 0bj2 := ExistingObject; Вызов Create производит запуск стандартного конструктора, доступного для каждого класса, если только класс не переопределяет его (этот случай рассмотрен далее). Для объявления типа данных нового класса, имеющего несколько локаль- ных полей данных и ряд методов, используется следующий синтаксис: type TDate = class Month. Day. Year: Integer; procedure SetValue (m. d. y: Integer); function LeapYear: Boolean: end: СОВЕТ------------------------------------------------------------------------------------- Понятия «класс» и «объект» являются широко распространенными и зачастую употребляются не- правильно. Давайте согласуем их определение. Класс — это определенный пользователем тип дан- ных, имеющий состояние (его представление или внутренние данные) и ряд операций (его поведение или методы). Объект — это экземпляр класса или переменная типа данных, определенного этим классом. Объекты являются действительными элементами. При запуске программы объекты зани- мают часть памяти для внутреннего представления. Отношения между объектами и классами — это то же самое, что и отношение между переменной и ее типом. СОВЕТ-------------------------------------------------------------------- Согласно существующему в Delphi соглашению в качестве префикса имени каждого создаваемого класса и других типов используется буква «Т» (сокращение от «Туре»). Это лишь соглашение, для компилятора Т — такая же буква, как и все остальные, но это соглашение настолько распростране- но, что его соблюдение сделает ваш программный код проще для восприятия другими разработчи- ками. Метод определяется с помощью ключевого слова function или procedure, в зави- симости от необходимости наличия возвращаемого значения. Методы могут объяв- ляться только внутри определения класса; они должны также быть определены в разделе implementation того же модуля. В этом случае имя каждого метода имеет в качестве префикса имя класса, к которому он принадлежит, отделенное точкой: procedure TDate.SetValue (m. d. у: Integer): begin Month := m: Day := d; Year := y: end; function TDate.LeapYear: Boolean; begin // вызвать IsLeapYear из SysUtils.pas Result := IsLeapYear (Year): end; , 0080
ипассы и объекты 81 ПРИМЕЧАНИЕ----------------------------------------------------------------— Нажатие Ctrl+Shift+C при нахождении курсора в пределах определения класса активизирует функ- цию Class Completion (Завершение кода) редактора Delphi, которая сгенерирует «скелет» определе- ния методов, объявленных в классе. Вот как можно использовать объект из определенного нами класса; var ADay; TDate; begin // создать объект ADay := TDate.Create; try // использовать объект ADay.SetValue (1. 1. 2000); if ADay.LeapYear then ShowMessage ('Leap year; ' + IntToStr (ADay.Year)); finally // уничтожить объект ADay.Free; end; end; Обратите внимание, что ADay.LeapYear является выражением, подобным ADay.Year, хотя в первом случае — это вызов функции, а во втором — прямой доступ к дан- ным. После вызова функции без параметров можно добавить круглые скобки. Про- граммный код этого примера представлен в программе Datesl с единственным отличием: программа создает дату с учетом года, представленного в поле редакти- рования (edit box). СОВЕТ----------------------------------------------------------------------------------- Представленный выше фрагмент кода использует блок try/finally для гарантии уничтожения объек- та, даже если при выполнении кода возникнет исключение. Использование исключений рассмотре- но в конце этой главы. Дополнительные сведения о методах О методах можно говорить очень много. Вот некоторые небольшие примечания о возможностях, доступных в Delphi: ° Delphi поддерживает перегрузку (overloading) метода. Это означает, что допус- кается существование двух методов с одинаковым именем, помеченных ключе- вым словом overload и тем, что списки параметров этих методов полностью отличаются. Выбор нужного метода осуществляется компилятором путем про- верки параметров; ° методы могут иметь один или несколько параметров со значениями по умолча- нию. Если при вызове эти параметры упущены, то им будут присвоены значе- ния по умолчанию; ° внутри метода можно использовать ключевое слово Self для обращения к теку- щему объекту. При обращении к локальным данным объекта ссылка на self яв- ляется неявной. Например, в методе SetValue класса TDate, рассматриваемого 0081
82 Глава 2. Язык программных-—rniiBT'r*"? выше, для обращению к полю текущего объекта можно использовать Month, а компилятор транслирует ее в Self.Month; о можно определять методы класса с пометкой ключевым словом class. Метод класса не имеет экземпляр объекта для действия, поскольку он может приме- няться к объекту класса или к классу целиком. Delphi (в настоящее время) не имеет возможности определить данные класса, но можно имитировать эту воз- можность, добавив глобальные данные в раздел реализации модуля, определя- ющего этот класс; о по умолчанию методы используют регистровое соглашение вызова register: па- раметры и возвращаемые значения передаются из вызывающего кода к функ- ции и обратно с помощью регистров центрального процессора, а не через стек, что значительно ускоряет вызовы методов. Динамическое создание компонентов Чтобы подчеркнуть тот факт, что компоненты Delphi практически не отличаются от других объектов (а также для демонстрации использования ключевого слова Self), я разработал пример CreateComps. Эта программа имеет пустую форму (без компонентов) и обработчик события OnMouseDown, которое было выбрано ввиду того, что оно в качестве параметра получает местоположение курсора, при кото- ром был произведен щелчок (от отличие от события OnClick). Эти координаты тре- буются для создания в этом месте компонента «кнопка». Вот программный код этого метода: procedure TForml FormMouseDown (Sender. TObject. Button. TMouseButton. Shift. TShiftState. X. Y: Integer); var Btn- TButton; begin Btn .= TButton.Create (Self); Btn.Parent := Self. Btn.Left .= X; Btn.Top .= Y; Btn Width .= Btn.Width + 50; Btn Caption Format ('Button at fcd. fcd'. [X. Y]); end; Действие этого кода заключается в создании кнопок в том месте, где был про- изведен щелчок мыши (рис. 2.1). Обратите внимание на особенность использова- ния в этом коде ключевого слова Self в качестве параметра метода Create (для ука- зания владельца объекта) и в качестве значения свойства Parent. Эти два элемента (принадлежность и свойство Parent) будут рассмотрены в главе 4 «Классы базовой библиотеки». При написании подобного программного кода вы, вероятно, будете склоняться к использованию вместо Self переменной Forml. В данном примере такое измене- ние не имеет никакой практической разницы, но при существовании множества экземпляров формы использование переменной Forml будет ошибкой. Фактиче- ски, если переменная Forml ссылается на первую созданную форму этого типа, то щелчок на другой форме этого же типа приведет к выводу кнопки на первой фор- ме. Владельцем (Owner) и родителем (Parent) кнопки будет Forml, а не форма, на 0082
инкапсуляция 83 которой щелкает пользователь. В общем, ссылка на конкретный экземпляр класса в тех случаях, когда требуется определенный объект, является с точки зрения ООП неверным подходом. Рис. 2.1. Результат выполнения примера CreateComps, который во время выполнения создает элементы «кнопка» Инкапсуляция Класс может содержать большое количество данных и любое число методов. Од- нако для соблюдения объектно-ориентированного подхода данные должны быть скрыты или инкапсулированы внутри использующего их класса. При обращении к данным само по себе изменение их значения, например, даты, не имеет большого значения. Фактически же значение дня может оказаться недействительным, на- пример, 30 февраля. Использование для обращения к внутреннему представле- нию объекта методов ограничивает риск возникновения ошибочной ситуации, поскольку эти методы могут проверять действительность нового значения и от- клонять его. Инкапсуляция важна потому, что она позволяет создателю класса изменять в последующих версиях внутреннее представление. Концепция инкапсуляции зачастую обозначается «черным ящиком». Нет не- обходимости беспокоиться о его содержимом: необходимо лишь знать, как осуще- ствлять взаимодействие с этим черным ящиком, т. е. использовать его, не вникая во внутреннюю структуру. Раздел «как им пользоваться»-называется интерфей- сом класса. Он позволяет другим частям программы обращаться к объектам этого класса и использовать их. Однако при использовании объектов большая часть кода остается скрытой. Редко удается узнать, какими внутренними данными оперирует объект, и, как правило, отсутствует возможность непосредственного доступа к этим Данным. Конечно же, предполагается, что для обращения к этим данным исполь- зуются методы, защищающие от несанкционированного доступа. Это является ооъектно-ориентированным подходом к классической концепции программиро- вания, известной как сокрытие информации (information hiding). Однако, как вы Увидите далее в этой главе, в Delphi существует дополнительный уровень сокры- Тия посредством свойств. 0083
84 Глава 2. Язык программирования Delphi Delphi осуществляет инкапсуляцию, основанную на классах, но используя струк- туру модулей, все еще поддерживает классическую инкапсуляцию, основанную на модулях. Каждый идентификатор, который объявлен в разделе interface (интер- фейс) модуля, становится видимым в других модулях программы, что обеспечива- ется использованием выражения uses, в котором помещается ссылка на модуль, в котором объявлен идентификатор. А идентификаторы, объявленные в разделе implementation (реализации) модуля, являются локальными для этого модуля. Private, Protected и Public Для инкапсуляции, основанной на классах, язык Delphi предлагает три специфи- катора доступа: private (частный), protected (защищенный) и public (общий). Чет- вертый, published (опубликованный), управляет RTTI-информацией (run-time type information) и информацией периода разработки (design-time information) (более подробно будет рассмотрен в главе 4), но он дает ту же программную доступность, как и спецификатор public. Вот описание трех классических спецификаторов дос- тупа: О директива private представляет поля и методы класса, которые недоступны из- вне модуля, объявляющего этот класс; О директива protected используется для указания методов и полей с ограничен- ной «видимостью». К элементам, помеченным как protected, могут обращаться только текущий класс и его наследники. Точнее, только этот класс, его под- классы и любой программный код, расположенный в том же модуле, что и дан- ный класс, могут обращаться к защищенным элементам, которые открываются с помощью «обходного маневра», рассмотренного во вставке «Доступ к защи- щенным данным других классов» далее в этой главе. Это ключевое слово мы также рассмотрим в разделе «Защищенные поля и инкапсуляция»; О директива public представляет поля и методы, которые свободно доступны из любого раздела программы, а также из модуля, в котором они определены. В общем случае поля класса должны быть объявлены как private, а методы — как public. Однако это не всегда имеет место. Методы могут быть частными или защищенными, если они нужны только для внутреннего применения в целях вы- полнения каких-либо частичных вычислений или для реализации свойств. Поля могут быть объявлены как защищенные, для того чтобы ими можно было управ- лять в классах-наследниках, хотя это не считается хорошим стилем ООП. ВНИМАНИЕ-------------------------------------------------------------- Спецификаторы доступа только ограничивают доступ внешнего, по отношению к данному модулю, программного кода к некоторым членам классов, объявленных в разделе интерфейса вашего моду- ля. Это означает, что если два класса находятся в одном и том же модуле, то их частные поля защитить невозможно. В качестве примера изучите новую версию класса TDate: type TDate = class private Month. Day. Year Integer. 0084
Инкапсуляция 85 public procedure SetValue (у. m, d. Integer), overload. procedure SetValue (NewDate- TDateTime). overload, function LeapYear Boolean. function GetText string, procedure Increase.- end. Возможно, вам захочется добавить и другие функции, такие как GetDay, GetMonth и GetYear, которые бы возвращали соответствующие защищенные данные, но на самом деле подобные функции непосредственного доступа к данным совершенно не нужны. Предоставляя функции доступа для всех и каждого, поля приводят к сниж- ению возможностей инкапсуляции и затрудняют модификацию внутренней реа- лизации класса. Функции доступа должны предоставляться лишь в том случае, если они являются частью логического интерфейса реализуемого вами класса. Другим новым методом является процедура Increase, которая производит уве- личение даты на один день. Это вычисление достаточно сложное, поскольку тре- буется учитывать различную длительность месяцев, а также високосные года. Для упрощения написания программы я изменю внутреннюю реализацию класса для внутренней реализации типа TDateTime. Определение класса будет заменено на сле- дующее (полный код представлен в примере DateProp). type TDate = class private fDate TDateTime: public procedure SetValue (y, m, d- Integer); overload. procedure SetValue (NewDate. TDateTime); overload; function LeapYear. Boolean; function GetText- string. procedure Increase. end. Обратите внимание, что ввиду изменения только в разделе private данного класса, нет необходимости модифицировать все использующие его программы. Вот в чем преимущество инкапсуляции! СОВЕТ --------------------------------------------------------------------------------- Тип TDateTime — это число с плавающей запятой. Целая часть этого числа указывает дату (началом отсчета является 12/30/1899; эта же дата используется OLE-автоматизацией и М/1п32-приложения компании Microsoft. Для указания предшествующих дат используются отрицательные значения), а десятичная часть указывает доли времени. Например, значение 3,75 соответствует 2 января 1900 г., 6;00Дтри четверти дня). Для добавления или вычитания дат можно добавлять или вычитать число Дней, что значительно проще, чем добавление дней в представлении день/месяц/год. Инкапсуляция со свойствами Свойства являются довольно значимым механизмом ООП или хорошо обосно- ванным приложением идеи инкапсуляции. По существу, вы имеете имя, которое полностью скрывает детали выполнения. Это позволяет значительно изменять ^Дасс, не воздействуя на использующий его код. Удачное определение свойств зву- чит как виртуальные поля. С точки зрения пользователя класса, свойства выглядят 0085
86 Глава 2. Язык программирования Delphi именно как поля, поскольку их значения обычно можно прочитать или записать. Например, этим программным кодом можно прочитать значение свойства Caption кнопки и назначать его свойству Text строки редактирования: Editl Text .= Buttonl Caption; Это выглядит как операции чтения поля и записи в поле. Однако для чтения и записи значения свойства могут непосредственно отображаться на данные и на методы доступа. Когда свойства отображаются на методы, то данные, к которым они обращаются, могут быть частью объекта или быть за его пределами, что может вызвать побочные эффекты, например, перерисовку элемента управления после изменения одного из его значений. Технически свойство — это идентификатор, который отображен на данные или методы с использованием инструкции read или write. Например, определение свойства Month для класса дат выглядит следующим образом: property Month. Integer read FMonth write SetMonth, Для обращения к значению свойства Month программа производит чтение за- щищенного поля FMonth; для изменения значения свойства — вызывает метод SetMonth (который, конечно же, должен быть объявлен внутри класса). Допускаются различные комбинации (например, для чтения можно использо- вать метод, а для изменения поля — непосредственно директиву write), по обычно для изменения значения свойства используется метод. Вот два альтернативных определения свойства, отображенные на два метода доступа или отображенные непосредственно на данные в обоих направлениях: property Month: Integer read GetMonth write SetMonth. property Month- Integer read FMonth write FMonth. Зачастую фактические данные и методы доступа являются частными (private) (или защищенными (protected)), в то время как свойство — опубликованным (public). По этой причине для доступа к этим методам или данным необходимо использовать свойство: эта технология обеспечивает как расширенную, так и уп- рощенную версию инкапсуляции. Расширенную, поскольку имеется возможность изменять не только представление данных и функции доступа к ним, но и добав- лять или удалять функции доступа без изменения вызывающего программного кода. Пользователь лишь должен повторно скомпилировать программу, использу- ющую это свойство. ПРИМЕЧАНИЕ------------------------------------------------------------------- При определении свойства воспользуйтесь функцией редактора Delphi «завершение расширенного класса», которая активизируется нажатием сочетания клавиш Ctrl+Shift+C. После того как введено имя свойства, тип и точка с запятой, нажмите Ctrl+Shift+C и Delphi предоставит законченное опре- деление и «скелет» метода-«установщика» (setter method). Введите Get перед именем идентифика- тора после ключевого слова read, и вы получите метод-«получатель» (getter method), практически не занимаясь вводом. Свойства класса TDate В качестве примера я добавил в объект рассматриваемого ранее класса TDate свойства доступа к году, месяцу и дню. Эти свойства не отображаются на определенные поля, но все они отображаются на единое поле fDate, хранящее полные сведения о дате. Вот rrzx.r/M/w тгрглт ксртотткт-<гпплV4атрпи» и мртплы-«установшш<и»: 0086
Инкапсуляция 87 type TDate = class public property Year. Integer read GetYear write SetYear: property Month- Integer read GetMonth write SetMonth; property Day. Integer read GetDay write SetDay; Каждый из этих методов легко реализуется с помощью функций, доступных в модуле DateUtils (более подробно рассмотрен в главе 3). Ниже представлен про- граммный код двух из них (остальные очень просты): function TDate.GetYear. Integer: begin Result = YearOf (fDate); end; procedure TDate.SetYearlconst Value Integer): begin fDate = RecodeYear (fDate. Value): end. Программный код для этого класса имеется в примере DateProp. Для принуди- тельной инкапсуляции эта программа использует вторичный модуль, определяю- щий класс TDate, а также создает объект «отдельная дата», который хранится в форме переменной и содержится в памяти в ходе всего выполнения программы. Исполь- зуя стандартный подход, объект создан в форме обработчика события OnCreate и раз- рушается с помощью обработчика события OnDestroy. Вид программы (рис. 2.2) имеет три строки редактирования и кнопки, осуществляющие копирование значе- ния этих строк редактирования в свойства и из свойств этого объекта «дата». Рис. 2.2. Форма примера DateProp ВНИМАНИЕ---------------------------------------------------------- При вводе значений вместо установки каждого из свойств программа использует метод SetValue. Отдельное назначение месяца и дня может привести к проблемам несоответствия месяца текущей Дате. Например, сейчас 31 января, а необходимо установить дату 20 февраля. Если сначала назна- чить месяц, то возникнет ошибка вследствие того, что 31 февраля не существует. Если сначала назначить день, то снова возникнет проблема назначения. Учитывая правила достоверности дат, лучше назначать все сразу. Расширенные возможности свойств Свойства имеют ряд расширенных возможностей, которые я рассмотрю в после- дующих главах. В частности, в главе 4 мы рассмотрим класс TPersistent, RTTI и ор- ганизацию потока, а в главе 9 — написание собственных компонентов. Вот краткая сводка наиболее важных характеристик: 0087
88 Глава 2. Язык nporpaMMnpoeawwftBiphi О директива write свойства может быть опущена, что сделает его свойством «только для чтения». При попытке изменить свойства компилятор выдаст ошибку. Так- же можно опустить директиву read и определить таким образом свойство «только для записи», но такой подход лишен смысла и используется редко; О IDE среды Delphi дает специальную трактовку свойствам этапа разработки, которые объявлены с помощью спецификатора доступа published и для выбран- ного компонента представлены в инспекторе объектов (Object Inspector). Бо- лее подробно директива published рассмотрена в главе 4; О альтернативным способом является объявление свойств с помощью специфи- катора public (такие свойства зачастую называются «свойство времени выполне- ниям). Эти свойства могут использоваться в программном коде; О можно определить свойства на основе массива, которые для обращения к эле- менту списка используют характерную нотацию в квадратных скобках. Свой- ства на основе списка строк, такие как Lines, являются типичным примером этой группы; О свойства имеют специальные директивы (включая stored и default), которые управляют поточной системой передачи компонентов (представленной в главе 4 и подробно рассмотренной в главе 9). СОВЕТ-------------------------------------------------------------------- Обычно имеется возможность присвоить значение свойству или прочитать его и даже использовать свойства в выражениях, но далеко не всегда можно передавать свойство в процедуру или метод в качестве параметра. Дело в том, что свойство — это не указатель на определенное место в памя- ти, поэтому оно не может использоваться в качестве параметра var или out; оно не может переда- ваться посредством ссылки. Инкапсуляция и формы Одна из ключевых идей инкапсуляции заключается в сокращении числа глобаль- ных переменных, используемых программой. К глобальной переменной можно обратиться из любого места программы, поэтому изменение глобальной перемен- ной влияет на всю программу. С другой стороны, при изменении представления поля класса необходимо лишь изменить программный код некоторых методов клас- са, и все! Таким образом, можно сказать, что сокрытие информации можно назвать инкапсуляцией изменений. Позвольте мне пояснить эту идею примером. Когда в программе имеется мно- жество форм, можно обеспечить доступность каких-либо данных в каждой форме, объявив их как глобальные переменные в разделе interface одной из форм: vac Forml TForml. nClicks Integer, Такой подход сработает, но эти данные будут связаны со всей программой, а не с определенным экземпляром формы. Если создаются две формы одинакового типа, то они смогут совместно использовать эти данные. А если вы хотите, чтобы каждая форма одинакового типа имела собственную копию данных, то единственным ре- шением является добавление их в класс формы: 0088
ин^псуляция 89 TForml = class(TForm) public nClicks Integer. end. Добавление свойств в форму Предыдущий класс использовал общие (public) данные, поэтому в интересах ин- капсуляции необходимо вместо них использовать данные и функции доступа к дан- ным, объявленные в разделе частные (private). Еще лучше: добавить свойство в форму. Каждый раз при необходимости сделать некоторую информацию формы доступной для других форм, надо использовать свойство, несмотря на причины, рассмотренные в разделе «Инкапсуляция со свойствами». Для этого измените объявление поля формы (в представленном выше программном коде), добавив перед ним ключевое слово property, а затем для активизации функции «заверше- ние программного кода» нажмите сочетание Ctrl+Shift+C. Delphi автоматически сге- нерирует весь необходимый программный код. Полный вариант программного кода этого класса можно найти в примере FormProp (рис. 2.3). Программа может создавать множество экземпляров формы (т. е. мно- жество объектов, основанных на одном и том же классе), но каждый со своим соб- ственным счетчиком щелчков. Рис. 2.3. Две формы примера FormProp во время выполнения СОВЕТ------------------------------------------------------------------------- Обратите внимание, что добавление свойства в форму не производит его добавление в список свойств формы, представленный в Object Inspector. По-моему, для инкапсуляции доступа к компонентам формы свойства также Должны использоваться в классах формы. Например, если главная форма содер- жит элемент управления «строка состояния» (у которой свойство Simple Panel име- ег значение True), используемая для представления различной информации, а вам Необходимо представить в ней текст со второй формы, то вам захочется написать: Forml StatusBarl SimpleText .= 'new text'. Это стандартный способ, но его нельзя назвать хорошим, поскольку он не обес- печивает инкапсуляцию структуры формы или компонентов. Если такой же 0089
90 Глава 2. Язык программировани^эР|а1рь; программный код встречается во многих местах, а позже возникнет необходимость изменить пользовательский интерфейс формы (например, заменить строку состо- яния другим элементом управления или активизировать множество панелей), то вам придется везде исправлять текст программы. Вместо этого можно использо- вать метод, а лучше свойство, позволяющее «спрятать» указанный элемент управ- ления. Это свойство можно определить как: property StatusText: string read GetText write SetText: где методы GetText и SetText осуществляют чтение и запись свойства Si m рLeText строки состояния (или caption одной из панелей). В других формах программы можно сослаться на свойство StatusText данной формы, а изменение пользовательского интерфейса повлияет только на setter- и getter-методы данного свойства. СОВЕТ------------------------------------------------------------------------- Подробности о том, как избежать необходимости опубликования полей формы для компонентов, что улучшит инкапсуляцию, изложены в главе 4. Но не торопитесь: для освоения этого описания потребуются хорошие знания Delphi, а рассмотренная методика имеет несколько недостатков. Конструкторы До сих пор для выделения памяти для объектов я вызывал метод Create. Это — конструктор, т. е специальный метод, применяемый к классу для выделения па- мяти для размещения экземпляра этого класса. Экземпляр возвращается конст- руктором, и для того чтобы сохранить этот объект и использовать это позже, мо- жет быть присвоен переменной. Все данные нового экземпляра установлены в ноль. Если необходимо, чтобы данные экземпляра имели определенные значения, то вы должны написать собственный конструктор. Перед конструктором ставится ключевое слово constructor. Хотя можно задать любое имя, лучше придерживаться стандартного имени: Create. Если используется другое имя, то конструктор Create базового класса TObject так и останется доступ- ным, а программист, вызвавший этот стандартный конструктор, может пропустить созданный вами код инициализации. За счет определения конструктора Create с некоторыми параметрами, вы заме- няете заданное по умолчанию определение новым и делаете его использование обязательным. Например, после того как будет определено: type TDate = class public constructor Create (y. m, d: Integer): Можно будет вызвать только этот конструктор, а не стандартный: var ADay: TDate: begin И ошибка, не компилируется: ADay .— TDate.Create: // правильно: ADay :- TDate.Create (1. 1. 2000): 0090
Конструкторы 91 Как вы увидите в главе 9, правила по написанию конструкторов для собствен- ных компонентов различны. Причина заключается в том, что в этом случае необ- ходимо подменить (override) виртуальный конструктор. Подмена особенно уместна для конструкторов, поскольку в класс можно добавлять множество конструкто- ров и вызывать их всех по имени Create; этот подход делает конструкторы просты- ми для запоминания и является стандартным путем, поддерживаемым другими ООП-языками, в которых все конструкторы должны иметь одинаковое имя. В ка- честве примера я добавил в класс два отдельных конструктора Create; один из них — без параметров. Он скрывает заданный по умолчанию конструктор. А другой — со значениями инициализации. Конструктор без параметра использует сегодняшнюю дату как заданное по умолчанию значение (полный программный код можно най- ти в примере DataView): type TDate = class public constructor Create; overload; constructor Create (y. m. d: Integer); overload; Деструкторы и метод Free Точно так же как класс может иметь пользовательский (измененный) конструк- тор, он может иметь пользовательский деструктор — метод, объявленный с ключе- вым словом destructor, и вызываемый по имени Destroy. Так же, как вызов конст- руктора выделяет память для объекта, вызов деструктора освобождает память. Деструкторы нужны только для объектов, которые запрашивают внешние ресурсы у своих конструкторов или во время существования. Можно написать новый де- структор, полностью подменяющий стандартный деструктор Destroy, позволив объекту выполнить некоторую очистку прежде, чем он сам будет разрушен. Destroy — это виртуальный деструктор класса TObject. Нельзя определять дру- гой деструктор, потому что объекты обычно уничтожаются вызовом метода Free, а этот метод вызывает виртуальный деструктор Destroy определенного класса (вир- туальный метод будет обсужден далее в этой главе). Free — это метод класса TObject, наследуемый всеми другими классами. Метод Free перед вызовом виртуального деструктора Destroy обычно проверяет, не явля- ется ли текущий объект (Self) нулевым (nil). Метод Free не сбрасывает объекте ноль автоматически; это то, что вы должны сделать сами! Объект «не знает», какие пе- ременные могут ссылаться на него, поэтому не имеет возможности сделать их все нулевыми. В Delphi 5 имеется процедура FreeAndNil, которую можно использовать сразу Для освобождения объекта и сброса его ссылки в ноль. Вызывайте метод FreeAndNil (Objl) вместо написания: Objl.Free- 0ЬЛ nil; СОВЕТ---------------------------------------------------------------------— Б°лее подробно этот вопрос рассмотрен в разделе «Однократное удаление объектов» далее в этой главе. 0091
92 Глава 2. Язык программирования Bjphi Модель объектных ссылок Delphi В некоторых ООП-языках объявление переменной типа класса создает экземпляр этого класса. Вместо этого Delphi основан на модели объектных ссылок (object reference model). Основная идея заключается в том, что переменная, имеющая тип- класс (например, переменная TheDay в рассмотренном ранее примере ViewDate), не содержит этот объект в качестве значения. Она лишь содержит указатель (pointer) или ссылку, указывающую, в каком месте памяти расположен объект (рис. 2.4). TheDay TDay object ---------- rTforonra -----* internal Wo ' reference Л r fDate field Рис. 2.4. Представление структуры объекта в памяти и переменная, ссылающаяся на него Единственная проблема этого подхода состоит в том, что при объявлении пере- менной вы не создаете объект в памяти (который несовместим со всеми другими переменными, что сбивает новых пользователей Delphi); вы лишь резервируете место в памяти для ссылки на объект. Экземпляры объекта должны быть созданы вручную, по крайней мере для определенных вами объектов классов. Экземпляры компонентов, помещаемых на форме, строятся автоматически библиотекой Delphi. Вы видели, как создать экземпляр объекта, применив конструктор к «его» клас- су. После того как объект был создан и необходимость в нем отпала, требуется удалить его (чтобы избежать потери уже «ненужной» памяти, что ведет к так на- зываемой «утечке памяти»). Освобождение памяти осуществляется методом Free. Если вы создаете объекты по мере необходимости и освобождаете их по завер- шении работы с ними, модель объектных ссылок работает без сбоя. Как вы увидите далее, использование модели объектных ссылок имеет далеко идущие последствия. Присвоение объектов Если переменная, содержащая объект, лишь содержит ссылку на этот объект, раз- мещенный в памяти, то что произойдет в случае копирования значения этой пере- менной? Предположим, что вы записали метод BtnTodayClick примера ViewDate сле- дующим образом: procedure TDateForm BtnTodayClickCSender. TObject); var NewDay TDate. begin NewDay = TDate.Create; TheDay = NewDay. LabelDate Caption .= TheDay.GetText. end; Этот программный код копирует адрес памяти объекта NewDay в переменную TheDav (рис. 2.5); это не приводит к копированию данных одного объекта в другой. 0092
Модель объектных ссылок Delphi 93 В данном случае — это не очень хороший подход — при каждом щелчке кнопки для каждого нового объекта будет выделяться память, но освобождения памяти, на которую до этого указывала переменная TheDay, никогда не происходит. NewDay TDate object TheDay Рис. 2.5. Представление операции присваивания объектной ссылки на другой объект в отличие от копирования содержимого объекта в другой объект Эта специфичная проблема может быть разрешена путем освобождения старо- го объекта нижеприведенным программным кодом (который лишь упрощен: он не использует явной переменной для только что созданного объекта): procedure TDateForm BtnTodayClickC Sender. TObject): begin TheDay Free. TheDay = TDate Create. Важно помнить, что при присвоении объекта другому объекту Delphi копирует ссылку на этот объект в памяти в качестве новой объектной ссылки. Не стоит счи- тать это отрицательным моментом: в большинстве случаев возможность определе- ния переменной, ссылающейся на существующий объект, может быть плюсом. Например, можно хранить объект, возвращенный обращением к свойству, и ис- пользовать его в последующих выражениях: var ADay TDate. begin ADay = Userinformation GetBirthOate; // использование ADay To же самое происходит при передаче объекта в функцию в качестве парамет- ра: вы не создаете новый объект, а ссылаетесь на один и тот же объект в двух раз- личных местах программного кода. Например, при написании представленной ниже процедуры ее вызов приводит к изменению свойства Caption объекта Buttonl, а не к копированию ее данных в память (которая совершенно бесполезна): procedure CaptionPIus (Button TButton). begin Button Caption = Button Caption + end. /'/ ВЫЗОВ CaptronPlus (Buttonl) Это означает, что объект передается ссылкой без использования ключевого слова var и без любого другого очевидного семантического признака «передачи-по-ссылке», 0093
94 Глава 2. Язык программированию^ЫрЫ что также смущает новичков. Что случится, если вы действительно хотите изме- нить данные внутри существующего объекта таким образом, чтобы они соответ- ствовали данным другого объекта? Необходимо копировать каждое поле объекта, что возможно, только если все они объявлены как public, или предоставить специ- альный метод для копирования внутренних данных. Некоторые VCL-классы име- ют метод Assign (присвоить), который выполняет эту операцию копирования. А точ- нее, этот метод имеют большинство VCL-классов, являющихся наследниками класса TPersistent, но не наследниками TComponent. Другие классы, исходящие от TComponent, имеют этот метод, но при его вызове вызывают исключение. В примере DateCopy я добавил метод Assign в класс TDate и вызвал его с помощью кнопки Today посредством следующего кода: procedure TDate.Assign (Source: TDate): begin fDate := Source.fDate: end: procedure TDateForm.BtnTodayC11ck(Sender: TDbject); var NewDay: TDate: begin NewDay := TDate.Create; TheDay.Asslgn(NewDay): Label Date.Caption := TheDay.GetText: NewDay.Free; end; Объекты и память Управление памятью в Delphi подчиняется трем правилам, по меньшей мере, если вы позволяете среде работать гармонично, без нарушений доступа (Access Viola- tions) и вхолостую расходуя лишнюю память: О перед использованием любого объекта его необходимо создать; О после использования любого объекта его необходимо уничтожить; О каждый объект должен уничтожаться только один раз. Должны ли вы руководствоваться этими инструкциями или можете позволить заниматься этим среде Delphi, зависит от выбранного вами подхода. Для динамических элементов Delphi поддерживает три типа управления па- мятью: О при явном создании объекта в программном коде приложения вы обязаны ос- вободить его (с единственным исключением ряда системных объектов и объек- тов, которые используются посредством интерфейсных ссылок). Если этого не делать, то до завершения программы память, используемая этим объектом, не будет освобождена для других объектов; О при создании компонента вы можете указать владельца этого компонента, ука- зав владельца конструктору компонента. Владелец компонента (зачастую — форма) становится ответственной за уничтожение всех объектов, которыми она владеет. Другими словами, при освобождении формы освобождаются все ком- поненты, расположенные на ней. Поэтому при создании компонента и указа- 0094
наследование существующих типов 95 НИИ его владельца вам не надо заботиться о его удалении. Это — стандартное поведение компонентов, которые создаются на этапе разработки и помещаются в модуле данных или на форме. Однако требуется, чтобы выбирался владелец, который будет гарантированно уничтожен; например, формы обычно принад- лежат глобальным объектам Application, который разрушается библиотекой при завершении программы; О когда RTL среды Delphi распределяет память для строк и динамических масси- вов, она будет автоматически освобождать память, когда ссылка выходит за гра- ницу. Нет необходимости освобождать строки: когда строка становится недо- ступной, занимаемая ею память освобождается. Однократное удаление объектов При повторном вызове метода Free (или вызове деструктора Destroy) объекта про- исходит ошибка. Однако если установить объект в ноль (nil), то повторный вызов Free не вызывает проблем. СОВЕТ----------------------------------------------------------------- Можно удивиться, почему при установке объекта в nil можно спокойно вызвать метод Free, a Destroy — нельзя. Причина в том, что Free — это метод, известный в данном месте памяти, в то время как виртуальная функция Destroy определена в ходе выполнения посредством поиска типа объекта — очень опасной в случае отсутствия объекта операцией. В качестве итога — пара рекомендаций: О для уничтожения объекта всегда вызывайте Free, а не деструктор Destroy; О используйте FreeAndNiI или после вызова Free устанавливайте объектную ссыл- ку в nil, если только ссылка сразу же не выходит за границу. В общем случае с помощью функции Assigned также можно проверить, имеет ли объект значение nil. Эти два выражения в большинстве случаев идентичны: If Assigned (ADate) then ... if ADate <> nil then ... Обратите внимание, что эти выражения лишь проверяют, является ли указа- тель ненулевым; они не проверяют, является ли он допустимым (действительным) указателем. При написании представленного ниже программного кода проверка будет удовлетворена, и вы получите сообщение об ошибке в строке, вызывающей метод объекта: ToDestroy.Free: if ToDestroy <> nil then ToDestroy.DoSomethi ng: Важно осознать, что вызов метода Free не устанавливает объект в ноль. Наследование существующих типов Довольно часто необходимо использовать несколько отличающуюся версию су- ществующего класса. Например, необходимо добавить новый метод или слегка Изменить существующий. Если скопировать исходный класс в буфер и вставить, а Потом изменить его (конечно же, это некрасивый вариант, если нет достаточно 0095
96 Глава 2. Язык программирования Delphi весомой причины так поступать), го вы продублируете ваш программный код, ошибки и головную боль. Вместо этого при таких же условиях необходимо ис- пользовать ключевой принцип ООП: наследование. Чтобы унаследовать в Delphi существующий класс, необходимо лишь указать на этот класс в начале определения нового класса. Например, это осуществляется каждый раз при создании новой формы: type TForml = class(TForm) end. Это определение указывает, что класс TForml наследует все методы, поля, свой- ства и события класса TForm. Для объекта типа TForm можно вызвать любой опуб- ликованный метод класса TForm. TForm, в свою очередь, наследует некоторые мето- ды от другого класса, и т. д. до базового класса TObject. В качестве примера наследования вы можете получить новый класс из TDate и изменить его функцию GetText. Этот программный код можно найти в модуле Dates примера NewDate: type TNewDate = class (TDate) publ1c function GetText string: end. Для реализации новой версии функции GetText я использовал функцию Format- DateTime, которая использует (помимо прочих характеристик) предопределенные наименования месяцев, используемые в ОС Windows; эти наименования опреде- ляются региональными и языковыми настройками пользователя. (Многие из этих настроек копируются средой Delphi в константы, определенные в ее библиотеке (такой, как Long Month Names, ShortMonth Names и других, которые рассмотрены в раз- деле «Currency and date/time formatting variables» справочной системы Delphi). Вот метод GetText (‘dddddd’ — это сокращение для длинного формата даты): function TNewDate GetText string. begin GetText .= FormatDateTime (.'dddddd'. fDate). end. ПРИМЕЧАНИЕ--------------------------------------------------------------------- Используя сведения региональной настройки, программа NewDate автоматически адаптируется под различные настройки пользователя. При запуске этой программы на компьютере, использующем язык, отличающийся от английского, наименования месяцев автоматически будут представлены на этом языке. Для проверки такого поведения достаточно просто сменить региональные настройки (модуль «язык и стандарты» в панели управления Windows). Обратите внимание, что изменение настроек немедленно проявится во всех запущенных программах. После того как определен новый класс, необходимо использовать этот новый тип данных в программном коде формы примера NewDate, определив объект TheDay типа TNewDate и создав объект этого нового класса в методе FormCreate. Вы не долж- ны изменить программный код с вызовами метода, потому что унаследованные методы работают точно таким же образом; однако эффект их выполнения изме- нится (рис. 2.6). 0096
наследование существующих типов 97 Thursday, December 2S, 21 |, 1пае«е Leap Vest? Рис. 2.6. Результат выполнения программы NewDate с использованием наименования дней и месяцев в соответствии с региональными установками Защищенные поля и инкапсуляция Программный код метода GetText класса TNewDate компилируется только в том слу- чае, если он находится в том же модуле, что и класс TDate. Фактически, он обращает- ся к защищенному полю fDate родительского класса. Если необходимо поместить класс потомка в новый модуль, то для чтения значения частного поля необходимо либо объявить поле fDate как защищенное (protected), либо добавлять защищен- ный метод доступа в родительский класс. Многие разработчики полагают, что первое решение в любом случае будет наи- лучшим, поэтому объявление большинства полей защищенными сделает класс более расширяемым и облегчит написание классов-наследников. Однако такой подход нарушает идею инкапсуляции. В большой иерархии классов изменение определения защищенных полей основных классов становится столь же трудным, как изменение глобальных структур данных. Если 10 производных классов обра- щаются к этим данным, то изменение его определения означает потенциальную необходимость изменения программного кода в каждом из этих 10 классов. Дру- гими словами, удобство, возможность расширения и инкапсуляция часто проти- воречат друг другу. В таких случаях предпочтение необходимо отдать инкапсуля- ции. Если это можно сделать без ущерба удобству, то вы только выиграете. Часто это промежуточное решение может быть получено с помощью виртуального мето- да. Этот аспект будет рассмотрен в разделе «Позднее связывание и полиморфизм». Если вы в ущерб инкапсуляции предпочтете обеспечить быстрое написание про- граммного кода классов-наследников, то может получиться, что ваша разработка не будет отвечать принципам ООП. Доступ к защищенным данным других классов (или «Взлом защиты») Вы уже видели, что private- и protected-данные класса доступны для любых Функций или методов, которые представлены в том же самом модуле, что и данный класс. Например, рассмотрим класс (часть примера Protection): type TTest - class пподолжение - 0097
98 Глава 2. Язык программирования Delphi Доступ к защищенным данным других классов (или «Взлом защиты») {продолжение) protected ProtectedData Integer end Как только вы поместите этот класс в отдельный модуль, вы не сможете непосредственно обратиться к его защищенной части из других модулей. Со- ответственно, если написать: Var Obj TTest. Begin Obj = TTest.Create. Obj ProtectedData =20. // компилироваться не будет то компилятор выдаст сообщение об ошибке: «Undeclared identifier: ‘Protec- tedData.’» (Необъявленный идентификатор «ProtectedData»). Тут можно предположить, что вообще не существует возможности обратиться к защи- щенным данным класса, определенным в другом модуле. Однако такая воз- можность имеется. Подумайте, что случится, если вы создадите соответству- ющий, ничего не делающий класс-потомок: type TTestHack = class (TTest). Теперь, если сделать прямое приведение объекта к новому классу и через него обратиться к защищенным данным, то это будет выглядеть следующим образом: var Obj TTest. begin Obj = TTest Create. TTestHack (Obj) ProtectedData =20. // компилируется! Теперь этот программный код компилируется и работает должным обра- зом, что можно увидеть, запустив программу Protection. Как получилось, что такой подход работает? Класс TTestHack автоматически наследует защищен- ные поля базового класса TTest, а поскольку класс TTestHack находится в том же самом модуле, что и программный код, который пробует обращаться к данным в унаследованных полях, защищенные данные становятся доступ- ны. Можно было предположить, что при перемещении объявления класса TTestHack во вторичный модуль программа больше не будет компилироваться. Теперь, когда я показал, как это сделать, необходимо предупредить, что нарушение механизма защиты класса подобным образом может привести к ошибкам в программе (доступ к данным, которого не должно быть), и это вступает в противоречие с ООП-методикой. Однако бывают случаи, когда использование этой методики является наилучшим решением, что можно увидеть, просматривая исходный программный текст VCL и многих компо- нентов Delphi. Два примера, которые приходят на ум — это обращение к свой- ству Text класса TControl и позициям Row и Col элемента управления DBGrid. 0098
Наследование существующих типов 99 Эти две идеи демонстрируются примерами TextProp и DBGridCol соответствен- но. (Эти примеры весьма сложны, поэтому я предполагаю, что, прочитав дан- ный текст, разобраться с ними могут только опытные программисты; дру- гим же читателям лучше вернуться к ним позже.) Хотя первый пример представляет рациональное использование «взломщика» защиты, основан- ного на приведении типа, но пример с использованием полей Row и Col эле- мента DBGrid является контрпримером и иллюстрирует риск доступа к раз- рядам, которые автор класса хотел бы оставить закрытыми. Предназначение полей Row и Cot (строка и столбец) элемента OBGrid совершенно отличается от их использования в DrawGrid или StringGrid (основные классы). Во-пер- вых, DBGrid не считает фиксированные ячейки фактическими (он отличает ячейки данных от художественного оформления), поэтом индексы строки и столбца должны быть откорректированы, какие бы «декорации» не исполь- зовались для оформления табличной сетки (которые могут изменяться «на лету»). Во-вторых, компонент DBGrid — это виртуальное представление дан- ных. При прокрутке DBGrid данные могут продвигаться ниже, но выбранная в настоящее время строка не может изменяться. Эта методика — объявление локального типа только для того, чтобы обес- печить доступ к защищенным данным класса — часто описывается как ха- керская, и ее необходимо по возможности избегать. Проблема заключается не в обращении к защищенным данным класса, а в объявлении класса с един- ственной целью доступа к защищенным данным существующего объекта другого класса! Опасность этой методики заключается в «жестком» кодиро- вании приведения типа объекта из одного класса в другой. Наследование и совместимость типов Pascal является строго типизированным языком. Это означает, что нельзя, напри- мер, присвоить целое значение логической переменной, кроме как с помощью яв- ного приведения типов. Здесь существует правило: две переменные считаются со- вместимыми, если они имеют один тип данных или (что будет более точным) если их типы данных ссылаются на одно и то же определение типа. Для облегчения жизни программистов Delphi делает совместимыми некоторые предопределенные типы данных; допускается присвоение Extended значения Double и наоборот, с ав- томатическим повышением и понижением (с потенциальной потерей точности). ВНИМАНИЕ-----------------------------------------------—------- Если вы переопределяете тот же тип данных в различных модулях, то эти типы не будут совместимы, Даже если их названия идентичны. Программа, использующая два типа с одинаковыми названиями, определенными в двух различных модулях в ходе компиляции и отладки, будет просто кошмаром. Существует важное исключение из этого правила в отношении типов класса. При объявлении класса, например, TAnimal (животное), и создании от него нового Дочернего класса, допустим TDog (собака), в дальнейшем можно назначить объект типаTDog переменной типа TAnimal. Это допустимо, поскольку собака — это живот- ное! В соответствии с общим правилом всегда можно использовать объект класса 0099
100 Глава 2. Язык программирования Delphi потомка, когда ожидается использование объекта класса предка. Но не наоборот! Нельзя использовать объект класса предка, когда ожидается использование объекта класса-потомка. Для облегчения восприятия повторим то же самое, но в виде про- граммного кода: vac MyAmmal: TAmmal; MyDog: TDog: begin MyAmmal = MyDog. // Допускается MyDog := MyAnimal; // Это ошибка!!! Позднее связывание и полиморфизм Функции и процедуры языка Pascal обычно основываются на статическом или раннем связывании. Это означает, что вызов метода уточняется (т. е. разрешается) компилятором и компоновщиком, которые заменяют этот вызов запросом к опре- деленному месту памяти, в котором находится данная функция или процедура (адрес процедуры). ООП-языки позволяют использование другой формы связы- вания, известной как динамическое или позднее связывание. В этом случае действи- тельный адрес вызываемого метода определяется во время выполнения, основы- ваясь на типе экземпляра, использовавшегося для выполнения вызова. Эта методика называется полиморфизмом. Полиморфизм означает, что метод может вызываться применительно к переменной, но какой метод будет вызван, определяется средой Delphi в зависимости от типа объекта, на который ссылается переменная. Учитывая правила совместимости типов, рассматривавшихся в пре- дыдущем разделе, Delphi не может произвести определение класса объекта до тех пор, пока в ходе выполнения к нему не произойдет обращения переменной. Пре- имущество полиморфизма заключается в том, что имеется возможность написать более простой программный код, верно трактующий несопоставимые типы объек- тов, как будто бы они одного типа, и обеспечивающий верное поведение програм- мы в ходе выполнения. Например, предположим, что класс и его класс-потомок (пускай будут TAnimal и TDog) оба определяют одинаковый метод, и этот метод использует позднее свя- зывание. Данный метод можно применить к общей переменной, например, MyAnimal, которая в ходе выполнения может обращаться как к объекту класса TAnimal, так и к объекту класса TDog. Какой метод действительно будет вызван — определяется в ходе выполнения в зависимости от класса текущего объекта. Эта методика продемонстрирована в примере PolyAnimals. Классы TAnimal и TDog имеют метод Voice, который воспроизводит звук, издаваемый выбранным живот- ным, как в виде текста, так и звука (вызывая API-функцию PlaySound, определен- ную в модуле MMSystem). Метод Voice определен в классе TAnimal как виртуальный (с использованием ключевого слова virtual), а позже при определении TDog он подменяется (с использованием ключевого слова override): type TAmmal = class publ1c function Vmrp- ctrinn virtual 0100
Позднее связывание и полиморфизм 101 «х . I ili.HI» TDog = class (TAnimal) public function Voice: string; override: Эффект вызова MyAnimaLVoice будет различным. Если переменная MyAnimal в текущий момент ссылается на объект класса TAnimal, то будет вызван TAnimal.Voice. Если она ссылается на объект классаТОод, то уже будет вызван TDog.Voice. Это про- исходит лишь благодаря тому, что функция является виртуальной (можете поэкс- периментировать, удалив ключевое слово virtual и перекомпилировав пример). Вызов MyAnimal.Voice будет работать для объекта, который является экземпля- ром любого из производных от TAnimal класса, даже для классов, определенных в других модулях или еще вовсе не написанных! Компилятор может и не знать обо всех потомках для того, чтобы сделать вызов, совместимый с ним; требуется лишь класс-предок. Другим словами, вызов MyAnimal.Voice совместим со всеми будущи- ми классами-потомками класса TAnimal. СОВЕТ------------------------------------------------------------------------------ Это ключевая техническая причина, почему объектно-ориентированные языки поощряют возмож- ность повторного использования. Можно написать программный код, который использует иерархи- чески связанные классы, не имея представления о конкретных классах, входящих в эту иерархию. Другими словами, иерархия (и сама программа) по-прежнему сохраняет расширяемость, даже если вы написали тысячи строк программного кода. Конечно же, с одним условием: класс-предок иерар- хии должен быть разработан очень осторожно. На рис. 2.7 представлен результат работы примера PolyAnimals. Запустив эту программу, вы услышите соответствующие звуки, реализуемые вызовом PlaySound. Animals /' Г Anneal р- ! C £og L“ ArfArf Рис. 2.7. Работа программы PolyAnimals Подмена и переопределение методов Как вы только что видели, для того чтобы подменить метод с поздним связывани- ем в классе-потомке, необходимо использовать ключевое слово override (подме- нить). Обратите внимание, что это имеет место только в случае, когда метод был определен в классе-предке как виртуальный (или динамический) (virtual или dynamic). С другой стороны, если это статический метод, то отсутствует возмож- ность активизировать позднее связывание; это возможно только с помощью изме- нения программного кода класса-предка. Правила просты: метод, определенный как статический, остается статическим во всех производных классах, если только вы не «спрячете» его с помощью нового виптия nT.xir.rr. чотпчз ммрюгггргп то жр самое имя. Метод, определенный как 0101
102 Глава 2. Язык программирования Delphi виртуальный, остается методом, поддерживающим позднее (динамическое) свя- зывание, во всех производных классах (если только вы не «спрячете» его с по- мощью нового статического метода, выполняющего совершенно пустое действие). Это поведение ничем не изменить ввиду того, что для динамически связываемых методов компилятор генерирует различный код. Для переопределения статического метода в классе-наследнике необходимо добавить метод, имеющий те же или другие параметры, не указывая дальнейших спецификаций. Для подмены виртуального метода необходимо указать те же па- раметры и использовать ключевое слово override: type TMyClass = class procedure One virtual. procedure Two {статический метод} end. TMyDerivedClass = class (MyClass) procedure One. override: procedure Two. end. Как правило, подменить метод можно двумя способами: заменить метод клас- са-предка новой версией, либо добавить дополнительный программный код в су- ществующий метод. Это можно выполнить с помощью ключевого слова inherited при вызове того же метода класса-предка. Например, можно написать: procedure TMyDerivedClass One begin // новый программный код // вызов унаследованной процедуры MyClass One inherited One end. СОВЕТ------------------------------------------------------------------------------- При создании объекта класса TMyDerivedClass, используя только что представленное определение класса, можно вызывать его метод One со строчным параметром, а не без параметра, как было определено в базовом классе. Если именно это и надо, то можно выполнить переопределенный метод (метод класса-родителя), пометив его ключевым словом overload (перегрузка). Если этот метод имеет параметры, отличающиеся от параметров базового класса, то он действительно стано- вится перегруженным методом; в противном случае он заменит метод базового класса. Обратите внимание, что в базовом классе этот метод не должен помечаться как overload, однако если в базовом классе метод является виртуальным, то компилятор выдаст предупреждение: «Method 'One' hides virtual method of base type 'TMyClass'» (Метод 'One' прячет виртуальный метод базового типа 'TMyClass'). Для того чтобы избежать появления такого сообщения и известить компилятор о ваших намерениях, можно использовать директиву reintroduce. Если вас интересует этот вопрос, можете обратиться к примеру Remtr и поэкспериментировать с ним. При подмене существующего виртуального метода базового класса вы должны использовать те же параметры. При создании новой версии метода в классе-на- следнике необходимо объявить его с любыми параметрами. На самом деле это бу- дет новый метод, никак не связанный с методом родителя, имеющим то же имя, — они лишь случайно имеют одно и то же имя. Вот пример: type TMvClass = class 0102
Позднее связывание и полиморфизм 103 procedure One. end TMyOerivedClass = class (TMyClass) procedure One (S string), end Виртуальные методы против динамических В Delphi существует два способа активизации позднего связывания. Можно объя- вить метод как виртуальный (как вы уже видели), или объявить его динамиче- ским. Синтаксис использования ключевых слов virtual и dynamic абсолютно одина- ковый и результат их использования тоже одинаковый. Различным является лишь внутренний механизм, используемый компилятором для реализации позднего свя- зывания. Виртуальные методы основываются на таблице виртуальных методов (virtual method table, VMT, иногда используется наименование vtable), которая представ- ляет собой массив адресов методов. Для вызова виртуального метода компилятор генерирует код перехода на адрес, хранимый в n-й ячейке таблицы виртуальных методов объекта. VMT-таблицы обеспечивают быстрое выполнение вызовов ме- тодов, но они требуют наличия элемента таблицы для каждого виртуального мето- да каждого класса-потомка, даже если в наследуемом классе метод не подменяется. С другой стороны, динамические (Dynamic) вызовы методов координируются с помощью уникального числа, указывающего на метод, который хранится в клас- се только в том случае, если класс определяет или подменяет его. Поиск соответ- ствующей функции становится более длительным по сравнению с таблицей соот- ветствия виртуальных методов. Преимущество заключается в том, что элементы, соответствующие динамическим методам, распространяются в потомках, только если потомки подменяют данный метод. Обработчики сообщений Метод, осуществляющий позднее связывание, может также использоваться для обработки сообщений Windows, хотя методика несколько отличается. Для этой цели Delphi предоставляет еще одну директиву, message, предназначенную для идентификации методов, обрабатывающих сообщения, которые должны представ- лять из себя процедуру с единственным параметром var. Директива message сопро- вождается номером Windows-сообщения, которое данный метод будет обрабаты- вать. ВНИМАНИЕ------------------------------------------------------------ Директива message также доступна в Kylix и полностью поддерживается RTL-библиотеками и биб- лиотеками языка. Однако визуальная часть структуры CLX-приложения не использует методы, об- рабатывающие сообщения, для передачи уведомлений элементам управления. Поэтому там, где это возможно, необходимо пользоваться виртуальным методом, предоставляемым библиотекой, а не обрабатывать Windows-сообщение напрямую. Конечно же, это уточнение важно лишь при со- здании перемещаемого программного кода. Например, представленный ниже программный код позволяет обрабатывать определенное пользователем сообщение с числовым значением, указанным в кон- стантр wm I kor ОС Windows* 0103
104.Глава 2. Язык программирования Delphi type TForml = cl ass(TForm) procedure WMUser (var Msg: TMessage): message wmJJser; end: Имя процедуры и тип параметров определяются вами, хотя есть несколько пред- определенных типов записей для различных сообщений Windows. Позже можно с помощью соответствующего метода сгенерировать это сообщение: PostMessage (Forml.Handle. wmJJser. 0. 0): Эта методика может быть чрезвычайно полезна для бывалых программистов, которые знают все о сообщениях Windows и API-функциях. Вы можете также не- медленно посылать сообщение, вызывая API-функцию SendMessage или VCL-ме- тод Perform. Абстрактные методы Ключевое слово abstract используется для объявления методов, которые будут оп- ределены только в классах-потомках текущего класса. Директива abstract полно- стью определяет метод. Если вы попробуете предоставить уточнение (формулиров- ку) этого метода, то компилятор выразит недовольство. В Delphi можно создавать экземпляры классов, имеющие абстрактные методы. Однако при этом 32-разряд- ный компилятор Delphi выдает предупреждающее сообщение «Constructing in- stance of <class name> containing abstract methods» (Создание экземпляра <имя класса>, содержащего абстрактные методы). Если в ходе выполнения будет выз- ван абстрактный метод, Delphi вызовет исключение (см. пример AbstractAnimals — расширение примера PolyAnimals): type TAnimal = class public function Voice: string: virtual: abstract: СОВЕТ------------------------------------------------------------------------- Многие другие ООП-языки используют более жесткий подход: запрещается создавать экземпляры классов, содержащих абстрактные методы. Вы можете поинтересоваться, а зачем использовать абстрактные методы? При- чина заключается в поддержке полиморфизма. Если класс TAnimal имеет вирту- альный метод Voice, то каждый производный от него класс может переопределить его. Если он имеет абстрактный метод Voice, то каждый производный от него класс должен переопределить его. В ранних версиях Delphi, если метод, подменяющий абстрактный метод объяв- лен как inherited (унаследованный), то результат был в абстрактном запросе мето- да. Начиная с Delphi 6, компилятор был усовершенствован и стал замечать при- сутствие абстрактного метода и пропускать запрос inherited. Это означает, что вы можете безопасно использовать inherited в каждом подменяемом методе, пока вы специально не хотите отключить выполнение некоторого программного кода ба- зового класса. 0104
Безопасное приведение типов 105 Безопасное приведение типов Правило совместимости типов Delphi для классов-наследников позволяет исполь- зовать класс-наследник там, где ожидается использование класса-родителя. Как упоминалось ранее, обратный вариант невозможен. Теперь давайте представим, что класс TDog имеет метод Eat, который не представлен в базовом классе TAnimal. Если переменная MyAnimal обращается к собаке, она сможет вызвать эту функцию, но если переменная относится к другому классу, то попытка вызова приведет к ошибке. Явное приведение типов может привести к неприятной ошибке време- ни выполнения (или, еще хуже, проблеме наложения при записи в памяти), по- скольку компилятор не сможет определить, является ли верным тип объекта и дей- ствительно ли существует вызываемый метод. Для решения этой проблемы можно использовать методики, основанные на технологии «информация о типах в процессе исполнения» (run-time type information, сокращенно RTTI). По существу, поскольку каждый объект «знает» свой тип и свой родительский класс, можно получить эту информацию с помощью оператора is (либо, в особых случаях — с помощью метода InheritsFrom класса TObject). Парамет- ры оператора is — объект и тип класса, а возвращаемое значение имеет логический тип: if MyAnimal is TDog then ... Выражение is будет иметь значение True, только если объект MyAnimal в настоя- щее время относится к объекту класса TDog, либо к классу, исходящему от TDog. Это означает, что при проверке является ли объект TDog типом TAnimal, результат будет положительным. Иначе говоря, это выражение равно True, если можно безо- пасно присвоить данный объект (MyAnimal) переменной этого типа данных (TDog). Теперь, когда вы уверены, что данным животным является собака, можно вы- полнить безопасное приведение типов (преобразование). Явное приведение мож- но выполнить с помощью следующего программного кода: var MyDog: TDog; begin if MyAmmal is TDog then begin MyDog : = TDog (MyAmmal); Text := MyDog.Eat; end; Это же действие может быть выполнено непосредственно вторым RTTI-onepa- тором as, который преобразует тип объекта только в том случае, если запрашивае- мый класс совместим с текущим. Параметрами оператора as являются объект и тип класса, а результатом — объект, преобразованный в новый тип класса. Например, можно написать следующий фрагмент: MyDog := MyAmmal as TDog; Text .= MyDog.Eat: Если необходимо лишь вызвать функцию Eat, также можно использовать и бо- лее короткую запись: (MyAnimal as TDog).Eat: 0105
106 Глава 2. Язык программирования Delphi Результатом этого выражения является объект, имеющий тип класса TDog, по- этому к нему можно применить любой метод данного класса. Разница между тра- диционным приведением и использованием оператора as в том, что во втором ва- рианте в случае если тип объекта несовместим с типом, к которому вы хотите его привести, генерируется исключение. Этим исключением является ElnvalidCast (ис- ключения рассмотрены в конце данной главы). Для того чтобы избежать этого исключения, используйте оператор is и при по- ложительном результате выполняйте прямое приведение (фактически, нет при- чины последовательно использовать is и as, что приводит к двойной проверке типа): if MyAnimal is TDog then TDog(MyAnimal).Eat: Оба RTTI-оператора очень полезны в Delphi, поскольку довольно часто возника- ет необходимость написать общий программный код, который может использовать- ся в нескольких компонентах одного типа или даже разных типов. При передаче компонента в качестве параметра в метод, реагирующий на событие, используется общий тип данных (TObject); поэтому зачастую необходимо привести его обратно к исходному типу компонента: procedure TForml.ButtonlClickCSender: TObject); begin if Sender is TButton then end; В Delphi это довольно обычная практика, и я буду использовать ее в примерах этой книги. Два RTTI-оператора, is и as, чрезвычайно мощны, что может ввести в заблуждение: вы будете воспринимать их как стандартные конструкции програм- мирования. Хотя они действительно мощны, необходимо ограничивать их исполь- зование только в исключительных случаях. При необходимости решить сложную проблему, включающую несколько классов, пробуйте сначала использовать поли- морфизм. Только в отдельных случаях, где один полиморфизм не справляется, можно пробовать использовать RTTI-операторы. Не используйте RTTIвместо по- лиморфизма. Это неправильная практика программирования, которая ведет к сни- жению скорости выполнения программ. RTTI-операторы отрицательно влияют на производительность, поскольку для того, чтобы увидеть, является ли приведение типов корректным, им приходится пройти по всей иерархии классов. Как вы уже видели, вызовы виртуального метода требуют лишь поиска в памяти, что осуще- ствляется гораздо быстрее. СОВЕТ---------------------------------------------------------------------------- RTTI-информация — это не только операторы as и is. С ее помощью в ходе выполнения можно узнать подробные сведения о классе и типе, особенно для свойств, событий и методов, объявлен- ных как published. Подробности см. в главе 4. Использование интерфейсов При определении абстрактного класса, для того чтобы представить базовый класс иерархии, вы можете прийти в точку, в которой абстрактный класс является на- столько абстрактным, что он лишь перечисляет ряд виртуальных функций, не обес- 0106
Использование интерфейсов 107 речивая их реализацию. Этот вид «чисто» абстрактного класса также может быть определен с помощью специальной технологии, именуемой interface (интерфейс). Ввиду этого мы обращаемся к этим классам как к интерфейсам. Технически интерфейс — это не класс, хотя и напоминает его. От класса его отличает то, то он воспринимается как полностью отдельный элемент с опреде- ленными характеристиками: О в отношении объектов типа interface ведется подсчет числа ссылок, и интер- фейсы автоматически уничтожаются при отсутствии ссылок на них. Этот ме- ханизм подобен тому, как Delphi управляет длинными строками; это делает управление памятью практически автоматизированным; О класс может исходить от единственного базового класса, но может реализовы- вать множество интерфейсов; О так же, как все классы исходят от TObject, все интерфейсы исходят от Ilnterface, формируя полностью отдельную иерархию. СОВЕТ---------------------------------------------------------------------- До версии Delphi 5 включительно базовым классом интерфейса был IUnknown, но в Delphi 6 ему дано новое имя, Ilnterface, более точно указывающее на тот факт, что эта функциональная возмож- ность языка совершенно независима от COM-технологии компании Microsoft (которая использует имя IUnknown как собственный базовый интерфейс). Интерфейсы Delphi также доступны и в Kylix. Важно подчеркнуть, что интерфейсы поддерживают несколько иную, чем клас- сы, модель ООП. Интерфейсы обеспечивают менее ограниченную реализацию полиморфизма. Полиморфизм объектной ссылки основан на определенной ветви иерархии. Полиморфизм интерфейса работает по всей иерархии. Безусловно, ин- терфейсы придерживаются идеи инкапсуляции и обеспечивают более свободное соединение между классами, чем наследование. Обратите внимание, что самые современные ООП-языки, от Java до С#, имеют понятие интерфейсов. Вот синтаксис объявления интерфейса (чье имя по существующим соглашени- ям начинается с буквы i): type ICanFly = Interface [’{EAD9C4B4-E1C5-4CF4-9FA0-3BB12CBB0A21} ’] function Fly: string; end: Этот интерфейс имеет глобально уникальный идентификатор (Globally Unique Identifier, GUID) — числовой идентификатор, имеющий определение, и основан- ный на Windows-соглашениях. Эти идентификаторы можно генерировать в редак- торе Delphi нажатием сочетания клавиш Ctrl+Shift+G. СОВЕТ -------------------------------------------------------------------------- Хотя интерфейс можно компилировать и использовать без указания GUID, последний обязательно придется сгенерировать, поскольку он требуется для запроса интерфейса или динамического при- ведения типов as с использованием типа этого интерфейса. Все преимущество интерфейсов заклю- чается (обычно) в предоставлении гибкости во время выполнения; поэтому, в отличие от типов класса, интерфейсы без GUID не очень полезны. После того как интерфейс объявлен, для его реализации можно определить класс: 0107
108 Глава 2. Язык программирования Delphi type TAirplane = class (TInterfacedObject. ICanFly) function Fly: string; end: RTL уже предоставляет несколько базовых классов для реализации базового поведения, требуемого интерфейсом Ilnterfасе. Для внутренних объектов исполь- зуйте класс TInterfacedObject. Методы интерфейса можно реализовать с помощью статических методов (как в предыдущем примере) или с помощью виртуальных методов. В классах-потом- ках с помощью директивы override можно подменить виртуальные методы. Если не использовать виртуальные методы, то в классе-наследнике все еще можно пре- доставить новую реализацию, повторно объявив тип интерфейса и повторно вы- полнив привязку методов интерфейса к новым версиям статических методов. На первый взгляд, использование виртуальных методов для реализации интерфей- сов позволяет в классах-наследниках осуществить более «гладкое» кодирование, но оба подхода одинаково мощны и удобны. Однако использование виртуальных методов влияет на объем программного кода и использование памяти. СОВЕТ-------------------------------------------------------------------------------- Для того чтобы скорректировать точки входа вызова интерфейса на соответствующий метод реали- зации класса, компилятор должен сгенерировать процедуры-«заглушки», а также скорректировать указатель self. «Заглушки» метода интерфейса для статических методов должны корректировать self и «перепрыгнуть» к реальному методу данного класса. «Заглушки» метода интерфейса для виртуальных методов гораздо более сложны и требуют приблизительно в четыре раза больший объем кода (от 20 до 30 байт) по сравнению со статическими «заглушками». К тому же добавление в реализацию класса дополнительных виртуальных методов только увеличивает размер таблицы виртуальных методов (VMT), которая гораздо больше в классе реализации и всех его потомках. Интерфейс уже имеет собственную VMT, а повторное объявление интерфейса в потомках для по- вторной привязки интерфейса к новым методам столь же полиморфно, как использование вирту- альных методов, но требует значительно меньший объем программного кода. Теперь, после того как определена реализация интерфейса, можно написать программный код, использующий объект этого класса посредством переменной соответствующего типа: var Flyerl: ICanFly; begin Flyerl := TAirplane.Create: Flyerl Fly; end; Как только вы присвоили объект переменной типа интерфейс, Delphi автома- тически с помощью оператора as просматривает, реализует ли объект этот интер- фейс. Можно явно выразить это действие следующим образом: Flyerl := TAirplane Create as ICanFly: СОВЕТ-------------------------------------------------------------------------------- Когда оператор as используется в отношении классов или интерфейсов, компилятор генерирует различный код. Для классов компилятор вводит проверки во время выполнения для того, чтобы убедиться, что объект действительно «совместим по типу» с данным классом. Для интерфейсов ком- пилятор «представляет», что он может извлечь необходимый интерфейс из доступного типа класса. Эта операция подобна «времени компиляции as», а не чему-то, что существует во время выполнения. 0108
Работа с исключениями 109 Используете ли вы непосредственное присвоение или выражение as, среда Delphi делает одно и то же действие: она вызывает метод _AddRef данного объекта (объявленный Ilnterface). Стандартная реализация этого метода, как и реализация, предоставляемая TInterfacedObject, приводит к увеличению счетчика ссылок на объект. В то же время, как только переменная Flyerl выйдет из области действия, Delphi вызывает метод _Release (снова часть Ilnterface). Реализация метода -Release классом TInterfacedObject уменьшает счетчик ссылок, проверяя, не имеет ли он зна- чение «ноль», и в случае необходимости уничтожает объект. Вот поэтому в преды- дущем примере нет программного кода, освобождающего ресурсы, используемые созданным объектом. Иначе говоря, объекты, на которые ссылаются «интерфейсные» переменные, являются в Delphi переменными с подсчетом ссылок. Эти объекты автоматически высвобождают занимаемую ими память, когда на них нет ссылок «интерфейсных» переменных. ВНИМАНИЕ ---------------------------------------------------------------------- При использовании объектов, основанных на интерфейсах, обращаться к ним можно только с по- мощью объектных ссылок или только с помощью интерфейсных ссылок. Смешение двух подходов нарушает реализуемую Delphi работу схемы учета ссылок, и может вызвать проблемы использования памяти, которые очень сложно отследить. На практике, если вы решили использовать интерфейсы, необходимо использовать только «интерфейсные» переменные. Если вместо этого вам необходимо смешивать их, то отключите подсчет ссылок, написав вместо TInterfacedObject собственный базо- вый класс. Работа с исключениями Еще одной ключевой характеристикой Delphi является поддержка исключений (exceptions). Исключения делают программы более надежными, предоставляя стан- дартный способ предупреждения и обработки ошибок и внезапно возникающих ситуаций. Исключения облегчают написание, восприятие и отладку программ, поскольку они позволяют отделить код обработки ошибки от остального программ- ного кода вместо их взаимного «переплетения». Обеспечение логического разде- ления основного кода, кода обработки ошибки и ответвления к обработчику ошибки автоматически делает более понятной основную логику программы. Вы придете к написанию программного кода, который будет более компактным и менее загро- можденным рутинными операциями, не связанными с действительным целями программирования. Во время выполнения библиотеки Delphi генерируют (вызывают) исключения, когда что-то идет неверно (в коде времени выполнения, в компоненте или в опера- ционной системе). От той точки кода, в которой оно вызвано, исключение переда- ется в вызывающий его код и т. д. В конечном счете, если ваш программный код не обрабатывает исключение, оно будет обработано VCL посредством вывода стан- дартного сообщения об ошибке, а затем будет выполнена попытка продолжить выполнение программы, обрабатывая следующее системное сообщение или пользо- вательский запрос. В целом весь механизм основан на четырех ключевых словах: О try разграничивает начало защищаемого блока программного кода; 0109
110 Глава 2. Язык программирования Delphi О except разграничивает конец защищаемого блока программного кода и вводит выражения обработки исключения; О finally указывает блок программного кода, который должен быть выполнен в лю- бом случае, даже если произошло исключение. Этот блок обычно используется для выполнения операций «зачистки», которые должны быть выполнены при любых обстоятельствах: например, закрытие файлов или таблиц баз данных, освобождение объектов и высвобождение памяти и других ресурсов, получен- ных в этом же программном блоке; О raise генерирует исключение. Большинство исключений, с которыми вы стал- киваетесь в Delphi-программировании, будут сгенерированы системой, но име- ется возможность самостоятельно вызвать исключение при обнаружении в ходе выполнения недействительных или несовместимых данных. Ключевое слово raise также может использоваться в обработчике, повторно вызывая исключе- ние, распространяя его на следующий обработчик. ПРИМЕЧАНИЕ--------------------------------------------------------------- Обработка исключения — это не замена требуемого потока управления программы. Для проверки ввода пользователя и других предсказуемых состояний ошибки по-прежнему используется выраже- ние if. Исключения используются только для аномальных или неожиданных ситуаций. Поток программы и блок finally Могущество исключений Delphi связано с тем, что они «передаются» из процеду- ры или метода вызывающей программе, вплоть до глобального обработчика (если такой предоставляется программой, что обычно и делает Delphi), вместо следова- ния стандартного порядка выполнения программы. Поэтому реальной проблемой, с которой вы можете столкнуться, является не «как избежать исключения», а «как выполнить программный код, даже если произошло исключение». Рассмотрите этот программный код, выполняющий некоторые трудоемкие опе- рации и использующий указатель курсора «песочные часы» для того, чтобы пока- зать пользователю, что идет выполнение: Screen.Cursor = crHourglass. // длинный алгоритм. . Screen Cursor •= crDefault. ' В случае возникновения ошибки в алгоритме (которую я добавил для этого в обработчик события примера TryFinally) программа прервется, но не сможет вос- становить стандартный курсор. Вот для чего и нужен блок try/finally: Screen.Cursor : = crHourglass; try 11 длинный алгоритм. finally Screen.Cursor .= crDefault. end; Когда программа выполнит эту функцию, она всегда восстановит курсор, неза- висимо от возникновения исключения (любого типа). Этот код не производит обработки исключения; он лишь делает программу на- дежной в случае возникновения исключения. Блок try может сопровождаться либо 0110
Работа с исключениями 111 выражением except, либо выражением finally, но не обоими сразу. Поэтому если вы хотите также обработать исключение, то обычно используются вложенные блоки try. Внутренний блок связывается с выражением finally, а внешний — с except, или наоборот, в зависимости от ситуации. Вот основа программного кода третьей кноп- ки примера TryFi n ally: Screen.Cursor : = crHourglass: try try // длинный алгоритм ... finally Screen.Cursor :- crDefault: end: except on E: EDivByZero do ... end: Всякий раз, когда в конце метода имеется некоторый код завершения, необхо- димо пометить этот код в блоке finally. Вы должны всегда неизменно (как еще это можно подчеркнуть?!) защищать ваш код с помощью выражений finally, избегая утечек памяти или ресурсов в случае возникновения исключения. ПРИМЕЧАНИЕ------------------------------------------------------------------------- Вообще обработка исключения менее важна, чем использование блоков finally, поскольку Delphi может «пережить» большинство исключений. Слишком много блоков обработки исключений в про- граммном коде, вероятно, указывает на ошибки в логике самой программы и, возможно, недопони- мание роли исключений в языке. Далее в примерах книги вы увидите множество блоков try/finally, несколько raise и практически не встретите блоки try/except. Классы исключений В представленных ранее выражениях обработки исключений вы могли заметить исключение EdivByZero, определенное RTL среды Delphi. Кроме того, имеются дру- гие классы исключений, связанные с проблемами времени выполнения (например, неверное динамическое преобразование), проблемами ресурсов Windows (напри- мер, недостаток памяти) или ошибки компонентов (например, неверный индекс). Программисты также могут определить собственные исключения; можно создать класс-потомок стандартного класса исключения или одного из его потомков: type EArrayFul1 = class (Exception); При добавлении нового элемента в массив, который уже заполнен (ввиду ошиб- ки в логике программы) вы можете вызвать соответствующее исключение, создав объект этого класса: if МуАггау.Ful 1 then raise EArrayFul! Create (.'Array full")-. Конструктор Create (унаследованный от класса Exception) имеет строчный па- раметр, содержащий описание исключения для пользователя. Нет необходимости беспокоиться об уничтожении объекта, который вы создали для данного исключе- ния, поскольку он автоматически будет удален механизмом обработки исключений. Представленный выше программный код является частью программы-приме- ра Exceptionl. Некоторые процедуры были незначительно изменены, например, Функция DivideTwicePlusOne: 0111
112 Глава 2. Язык программирования Delphi function DivideTwicePlusOne (А В Integer) Integer, begin try // error if В equals 0 Result = A div В // другие действия пропустить, еспи вызвано исключение Result = Result div В Result = Result + 1 except on EDivByZero do begin Result = 0 MessageDlg ('Divide by zero corrected mtError. [mbOK]. 0). end on E Exception do begin Result = 0 MessageDlg (E Message. mtError. [mbOK], 0). end end !! конец исключения end В программном коде Exceptionl после одного и того же блока try существует два различных обработчика событий При наличии нескольких обработчиков вычис- ляться они будут последовательно. С учетом иерархии исключений обработчик также вызывается для классов-на- следников этого типа, к которым он обращается, как делает любая процедура. По- этому необходимо помещать более «широкие» обработчики (обработчики предка классов Exception) в конце, но помнить, что использование обработчика для всех исключений, подобных представленным выше, является неудачным вариантом. Лучше оставить неизвестные исключения среде Delphi. Стандартный обработчик исключений в VCL выведет сообщение об ошибке класса исключений в окне сооб- щений и затем вернется к нормальной работе программы Можно изменить обыч- ный обработчик исключения с помощью события Application.OnException или со- бытия On Exception компонента Application Events (см. пример ErrorLog в следующем разделе). Еще один важный элемент представленного выше программного кода заклю- чается в использовании в обработчике объекта-исключения (см. on Е: Exception do) Ссылка Е класса Exception обращается к объекту-исключению, переданному посред- ством выражения raise. При работе с исключениями необходимо помнить прави- ло: исключение вызывается путем создания объекта и обрабатывается путем ука- зания его типа. Тут имеется большое преимущество, поскольку, как вы уже видели, при обработке типа исключения в действительности обрабатывается исключение типа, указанного так же, как любой тип-потомок. Отладка и исключения При запуске программы в среде Delphi (например, нажатием клавиши F9) она запускается в среде отладчика. При столкновении с исключением от- ладчик по умолчанию приостановит программу. Этот результат — обычно то, что и требуется, поскольку вы будете знать, где произошло исключение, 0112
Протоколирование ошибок 113 и можете просмотреть запрос обработчика в пошаговом режиме. Кроме того, для просмотра последовательности вызовов функций и методов, которые заставили программу вызывать исключение, можно использовать функцию Stack Trace. Однако в программе-примере Exceptionl это поведение смутит програм- миста, который не очень хорошо представляет, как работает отладчик Delphi. Даже если код способен должным образом обработать исключение, отлад- чик остановит выполнение программы на строке исходного текста, самой близкой к тому месту, из которого было вызвано исключение. Далее, шаг за шагом перемещаясь по программному коду, вы можете просмотреть, как оно обрабатывается. Если исключение обрабатывается должным образом и необходимо лишь выполнить программу, запустите ее из Проводника Windows или временно отключите параметр Stop on Delphi Exceptions (Останавливать на исключени- ях Delphi) на странице Language Exceptions (язык исключений) диалогового окна Debugger Options (Параметры отладчика) (активизируемого командой Tools (Сервис) ► Debugger Options) вот здесь: jDebugger Options Eeneisl j gvent Los | (Й Emepnons | < / VCL E Abort Exceptions / Indy EIDConnCfosedGracefuly Exception V Mciosotl DAO Exceptions ✓ VisiBioker Internal Exceptions CORBA System Exceptions . 1 CORBA User Exceptions Щ I J-u- | p Integrated debt^ng Cancel J Неф Протоколирование ошибок Как правило, вы не знаете, какой из операторов вызвал исключение, и не можете (и не должны) помещать весь программный код в блок try/except. Общий подход заключается в том, чтобы позволить Delphi обработать все исключения и в конеч- ном итоге довести их до вас посредством обработки события OnException глобаль- ного объекта Application либо, что проще, с помощью компонента Application Events. В примере ErrorLog я добавил в основную форму экземпляр компонента Appli- cation Events и написал обработчик для его события OnException: 0113
114 Глава 2. Язык программирования Delphi procedure TFormLog LogException(Sender TObject E Exception), var Filename string LogFile TextFile begin 11 подготовить файл протокола Filename = ChangeFileExt (Application Exename, ' log'), AssignFile (LogFile Filename) if FileExists (FileName) then Append (LogFile) // открыть существующий файл else Rewrite (LogFile) // создать новый try // записать в файл и показать ошибку Writein (LogFile, DateTimeToStr (Now) + ’ ’ + E Message). if not CheckBoxSilent Checked then Application ShowException (E) finally 11 закрыть файл CloseFile (LogFile). end end СОВЕТ--------------------------------------------------------------------------------------- Пример ErrorLog использует поддержку текстовых файлов, предоставляемую традиционным TextFile типом данных языка Turbo Pascal. Вы можете присвоить переменной text file действительный файл и затем читать или записывать в нее. Подробности использования TextFile см. в главе 12 электрон- ной книги Essential Pascal, представленной в приложении С. В глобальном обработчике исключений можно осуществить запись в файл про- токола, например, даты и времени события, а также определить, показывать ли исключение, как это обычно делает Delphi (выполняя метод ShowException класса TApplication) По умолчанию Delphi выполняет ShowException, только если не опре- делено ни одного обработчика OnException На рис. 2.8 представлен результат вы- полнения программы ErrorLog и простой протокол исключений, открытый в Соп- ТЕХТ (прекрасный редактор программиста, написанный в Delphi и доступный по адресу www.fixedsys.COM/context). i ‘ty trr*rto< « ®JЯ» IB | ’2002 ’2002 '2002 '2002 ’2002 10 10 10 10 10 10 41 41 41 41 41 JAM 37 39 42 43 43 AM AM AM AM AM AM Рис. 2.8. Пример ErrorLog и созданный им протокол 0114
Ссылки класса 115 Ссылки класса Последней особенностью языка, которую мы рассмотрим в этой главе, является использование ссылок класса (class references), которые предполагают идею само- стоятельного манипулирования классами с помощью программного кода Первый момент, который необходимо помнить, ссылка класса — это не объект, это ссылка на тип класса Ссылка класса определяет тип переменной ссылки класса Звучит непонятно? Несколько строк кода помогут прояснить эту концепцию. Предположим, что вы определили класс TMyClass Теперь можно определить новый тип ссылки класса, связанной с данным классом: type TMyClassRef = class of TMyClass А теперь можно определить переменные обоих типов. Первая переменная ссы- лается на объект, вторая — на класс. var AnObject TMyClass AClassRef TMyClassRef begin AnObject = TMyClass Create, AClassRef = TMyClass Вы можете поинтересоваться: для чего используются ссылки классов? В об- щем случае ссылки классов позволяют манипулировать типом данных класса в ходе выполнения Ссылку класса можно использовать в любом выражении, где исполь- зование типа данных является законным. Таких выражений не очень много, но некоторые случаи, такие как создание объекта, являются довольно интересными. Последнюю строчку предыдущего примера можно переписать: AnObject = AClassRef Create На этот раз конструктор Create применяется к ссылке класса, а не к действи- тельному классу; ссылка класса используется для создания объекта этого класса. Типы ссылок класса не были бы столь полезными, если бы они не поддержива- ли то же правило совместимости типов, что и типы классов. При объявлении пере- менной ссылки класса, такой как MyCLassRef, потом ей можно присвоить тот же класс и любой класс-наследник. Поэтому если TMyNewCLass является классом-наследни- ком моего класса TMyClass, то можно также написать: AClassRef = TMyNewClass Delphi объявляет множество ссылок классов в RTL- и VCL-библиотеках, на- пример. TClass = class of TObject TComponentClass = class of TComponent, TFormClass = class of TForm В частности, тип ссылки класса TClass может использоваться для хранения ссыл- ки на любой класс, написанный в Delphi, поскольку каждый класс, разумеется, исходит от TObject Ссылка TFormClass используется в исходном программном коде большинства Delphi-проектов. Метод CreateForm объекта Application требует в ка- честве параметра класс создаваемой формы: Application CreateFormlTForml Forml), 0115
116 Глава 2. Язык программирования Delphi Первым параметром является ссылка класса, вторым — переменная, которая хранит ссылку на создаваемый экземпляр объекта. И, наконец, имея ссылку класса, можно применить ее к методам связанного класса. Учитывая, что каждый класс является потомком TObject, к каждой ссылке класса можно применить некоторые методы TObject (см. главу 3). Создание компонентов с помощью ссылок классов Каково практическое применение ссылок классов в Delphi? Способность манипу- лировать типом данных во время выполнения является фундаментальной особен- ностью среды Delphi. При добавлении нового компонента на форму путем выбора ее в панели компонентов вы выбираете тип данных и создаете объект этого типа. (На самом деле этим без вашего участия занимается Delphi.) Иначе говоря, ссылка класса обеспечивает полиморфизм при создании объекта. Чтобы дать вам возможность получше разобраться, как работает ссылка клас- са, я создал пример ClassRef. Форма, выводимая этим примером, имеет три пере- ключателя, размещенных в панели в верхней части формы. При выборе одного из переключателей и щелчке на форме вы сможете создать новые компоненты этих трех типов, указанных надписями кнопок: переключатель, кнопка и поле редакти- рования. Для правильного функционирования этой программы необходимо изменить имена этих компонентов. Форма также должна иметь поле ссылки класса, объяв- ленное как ClassRef: TControLCLass. Оно хранит новый тип данных при каждом щелч- ке пользователя на переключателях, выполняя присвоение, подобное: ClassRef := TEdit. Интересная часть программного кода выполняется при щелчке пользовате- ля на форме. Опять же, для того чтобы получить значения местоположения указа- теля мыши, я выбрал событие формы OnMouseDown: procedure TForml FormMouseDown(Sender: TObject, Button. TMouseButton: Shift: TShiftState: X. Y: Integer): var NewCtrl• TControl: MyName. String: begin 11 создать элемент управления NewCtrl = ClassRef.Create (Self): // спрятать его временно, для того чтобы избежать мерцания NewCtrl .Visible False: // установить Parent и местоположение NewCtrl.Parent = Self: NewCtrl Left := X: NewCtrl.Top :- Y. // создать уникальное имя (и надпись) Inc (Counter): MyName .= ClassRef.ClassName + IntToStr (Counter): Delete (MyName. 1. 1); NewCtrl Name = MyName: // и показать его NewCtrl Visible := True: end. 0116
Что далее? 117 Самой важной является первая строка представленного фрагмента. Она созда- ет новый объект, имеющий тип данных класса, хранящегося в поле ClassRef. Это осуществляется применением конструктора Create к ссылке класса. Теперь вы мо- жете установить значение свойства Parent, местоположение нового компонента, дать ему имя (которое автоматически будет также использовано как значение свойства Caption или Text) и сделать его видимым. Результат работы программы представ- лен на рис. 2.9. ^'component Butter Г Sultan Рис. 2.9. Пример результата работы программы ClassRef СОВЕТ-------------------------------------------------------------------------- Для того чтобы полиморфная конструкция работала, тип базового класса ссылки класса должен иметь виртуальный конструктор. Если вы используете виртуальный конструктор (как в примере), запрос конструктора применяется к ссылке класса и вызывает конструктор того типа, к которому в настоящее время обращается переменная ссылки класса. Но без виртуального конструктора ваш программный код вызовет конструктор фиксированного типа класса, указанного в объявлении ссылки класса. Виртуальные конструкторы требуются для полиморфной конструкции точно так же, как виртуальные методы для полиморфизма. Что далее? в этой главе мы рассмотрели основы объектно-ориентированного программиро- вания в Delphi. Мы обсудили определение классов и использование методов, ин- капсуляции и управление памятью, и, кроме того, дополнительные концепции, такие как свойства и динамическое создание компонентов. Далее мы перешли к на- следованию, виртуальным и абстрактным методам, полиморфизму, безопасному приведению типов, интерфейсам, исключениям и ссылкам классов. Если вы новичок, то для вас это, конечно же, представляет очень большой объем информации. Но если вы бегло знакомы с другим ООП-языком или прежними версиями Delphi, то вы вполне способны применять на практике вопросы, охва- ченные в этой главе. 0117
118 Глава 2. Язык программирования Delphi Понимание секретов языка и библиотеки Delphi жизненно важно для того, что- бы стать специалистом по программированию в Delphi. Рассмотренные вопросы формируют базис работы с VCL- и CLX-библиотеками классов; после изучения их в последующих двух главах, мы, наконец, перейдем к разработке реальных при- ложений, использующих различные компоненты, предоставляемые Delphi. Перед этим глава 3 предоставит краткий обзор RTL-библиотеки Delphi (глав- ным образом совокупность функций с небольшим привлечением ООП). RTL — это совокупность различных процедур для выполнения основных задач с Delphi. В главе 4 вы получите больше сведений о языке, рассмотрев особенности, связан- ные со структурой библиотеки классов Delphi, таких как влияние ключевого сло- ва published и роли событий. В целом глава 4 посвящена общей архитектуре биб- лиотеки компонентов. 0118
О Run-Time-библиотека Язык программирования Delphi поддерживает объектно-ориентированный под- ход, связанный с визуальным стилем разработки. Именно этим и прекрасен этот язык. Далее мы рассмотрим визуальную разработку, основанную на использова- нии компонентов, однако я хотел подчеркнуть тот факт, что многие готовые к ис- пользованию возможности Delphi доступны благодаря его Run-Time-библиотеке (RTL). Это — большая коллекция функций, которые можно применять для вы- полнения простых, а также более сложных задач с помощью программного кода на языке Pascal. (Здесь я использовал название «Pascal», поскольку Run-Time-биб- литотека в основном содержит процедуры и функции, написанные на обычных конструкциях языка, а не на ООП-расширениях, добавленных в язык компанией Borland.) Существует и вторая причина того, что для рассмотрения Run-Time-библиоте- ки в этой книге выделена целая глава: Delphi 6 в этой области имеет множество усовершенствований, а в Delphi 7 также введен ряд доработок. Доступны новые группы функций, некоторые функции перемещены в новые модули, а ряд элемен- тов изменен, что создает некоторую несовместимость со старым кодом, на котором основывались ваши проекты. Если вы использовали самые последние версии Delphi и уверены в знании RTL, вам все равно необходимо прочитать окончание этой главы. В этой главе рассмотрены следующие темы: о обзор RTL; о RTL-функции Delphi; ° новые модули RTL, связанные с обработкой дат и строк; О класс TObject; о вывод сведений о классе времени выполнения. Модули RTL В самых последних версиях Delphi RTL-библиотека имеет новую структуру и не- сколько новых модулей. Ввиду добавления новых функций компания Borland пред- ставила новые модули. Практически все ранее существовавшие функции остались в тех же модулях, а новые функции помещены в новые модули. Нспример, новые 0119
120 Глава 3. Run-Time-библиотека функции, связанные с обработкой дат, теперь содержатся в модуле Dated tils, а су- ществовавшие ранее функции обработки дат так и остались в SysUtils для того, что- бы избежать несовместимости с имеющимся исходным программным кодом. Исключением из этого правила являются функции с вариантными типами дан- ных, которые были удалены из модуля System во избежание нежелательного свя- зывания со специальными библиотеками Windows, даже в программах, не исполь- зующих эти возможности. Вариантные функции теперь находятся в модуле Variants, который мы рассмотрим далее. ВНИМАНИЕ --------------------------------------------------------------- Возможно, придется прибегнуть к перекомпоновке исходного программного кода Delphi 4 и Delphi 5 в связи с использованием модуля Variants. Среда Delphi достаточно «умна» и в состоянии самосто- ятельно включить модуль Variants в проекты, использующие тип Variant, выдав лишь предупрежде- ние об этом. Небольшая подстройка также произойдет и в отношении исполняемого файла, размер которого будет сокращен за счет избавления от нежелательного включе- ния неиспользуемых глобальных переменных и кода их инициализации. Размер исполняемого файла под микроскопом Взявшись за RTL, разработчики компании Borland могли несколько сокра- тить «излишество» всех Delphi-приложений, сократив размер программы на несколько избыточных килобайт, характерных для всех «раздутых» со- временных программ, но для разработчиков это очень удобно. В некоторых случаях даже эти несколько килобайт (умноженные в нескольких приложе- ниях) могут уменьшить размер и, в конечном итоге, время загрузки програм- мы. В качестве простого теста я разработал программу MiniSize, которая не являлась попыткой построить минимально возможную программу, а лишь очень маленькую программу, выполняющую какое-либо действие: она со- общает размер собственного исполняемого файла. Вот весь ее программный код: program Mini Size: uses Windows: {$R *.RES} var nSize: Integer: hFlle: THandle: strSIze: String: begin // open the current file and read the size hFlle := CreateFIle (PChar (ParamStr (0)). 0. FILE_SHARE_READ. nil. OPEN_EXISTING. 0. 0); nSize := GetFIleSize (hFlle. nil): CloseHandle (hFlle): // copy the size to a string and show it 0120
Модули RTL 121 SetLength (strSize. 20); Str (nSize, strSize); MessageBox (0. PChar (strSize). 'Mini Program'. MB_OK): end. Эта программа открывает свой собственный исполняемый файл, извле- кая свое имя в качестве первого параметра командной строки (ParamStr (0)), извлекает размер, преобразует его в строчное значение с помощью простой функции Str и выводит результат в виде сообщения. Эта программа не име- ет собственного окна. Тем не менее для преобразования типа целое/строка я использовал функцию Str. Это позволило избежать включения модуля Sys- Utils, содержащего более сложные процедуры форматирования, что избав- ляет от дополнительной нагрузки. При компилировании этой программы в среде Delphi 5 вы получаете размер исполняемого файла, равный 18 432 бай- там. Среда Delphi 6 сокращает этот размер до 15 360 байт, отбросив еще око- ло 3 Кбайт. За счет замены типа long string (длинная строка) на short string (короткая строка) и небольшой модификации программного кода можно и дальше сократить программу до размера менее 10 Кбайт. (Закончите вы отказом от распределителя памяти и процедур обработки строк, что возмож- но только в программах, использующих исключительно низкоуровневые вы- зовы.) Обе версии программного кода можно найти в файле-примере на веб- сервере автора или издательства. Обратите внимание, что подобного рода решения всегда предполагают некоторый компромисс. Избавляясь от внедрения модуля variants в Delphi- приложения, которые не используют эти типы данных, Borland несколько затрудняет работу приложений, которые их используют. Хотя реальное пре- имущество этой операции заключается в снижении нагрузки на память Del- phi-приложениями, не использующими variants, за счет отсутствия необхо- димости загрузки нескольких мегабайт библиотек ОЬЕ2-системы. Самым важным, по-моему, является размер полнофункциональных Del- phi-приложений, основанных на Run-Time-пакетах. Простой тест «ничего не делающей» программы, пример MiniPack, показывает размер исполняе- мого файла, равный 17 408 байтам. В последующих разделах вы найдете список RTL-модулей Delphi, включая все модули (с полным исходным кодом), доступные в подкаталоге Source\RtL\Sys ка- талога Delphi и некоторых из модулей, доступных в каталоге Source\RtL\Common. Во втором подкаталоге размещен исходный программный код модулей, которые состав- ляют новый RTL-пакет, содержащий как библиотеки функций, так и базовые клас- сы, рассматриваемые в конце этой главы и в главе 4 «Классы базовой библиотеки».* СОВЕТ--------------------------------------------------------------------------— Исходный VCL-пакет, поставляемый до версии Delphi 5, разделен на VCL- и RTL-пакеты, поэтому Невизуальные приложения, использующие Run-Time-пакеты, не вызывают нагрузки за счет раз- мещения визуальной части VCL. Это изменение также помогает обеспечить Linux-совместимость, поскольку новый пакет совместно используется VCL- и CLX-библиотеками. Кроме того, обратите внимание, что имена пакетов в Delphi б и Delphi 7 не содержат номер версии, хотя при компиляции в имени файла BPL указан номер версии (подробности см. в главе 10 «Библиотеки и пакеты»). 0121
1 122 Глава 3. Run-Time-библиотека Далее будет представлен краткий обзор роли каждого модуля и групп входя- щих в него функций. Много внимания также будет уделено новым модулям. Я не буду представлять полный список функций, поскольку этот вспомогательный ма- териал можно найти в Сетевых справочных материалах. Я старался отобрать наи- более интересные и малоизвестные функции и рассмотреть их коротко. Ф Модули System и Syslmt System является основным модулем RTL и автоматически включается в любую компиляцию (хотя автоматически и явно на него указывает выражение uses). При попытке добавить этот модуль в выражение uses программы, вы получите следую- щее сообщение об ошибке: [Error] Identifier redeclared: System Помимо прочего, модуль System включает: О класс TObject, являющийся базовым классом для любого класса, определенного в языке Object Pascal, включая все классы VCL. (Этот класс будет рассмотрен далее в этой главе.) О интерфейсы Ilnterface, Ilnvokable, IUnknown и Idispatch, а также простой класс реализации TInterfacedObject. Ilnterface появился в Delphi 6 для того, чтобы под- черкнуть, что тип интерфейса в определении языка Delphi никак не зависит от операционной системы Windows. Ilnvokable был добавлен в Delphi 6 для под- держки вызовов на основе SOAP; О код поддержки вариантных типов, включая константы вариантных типов, тип записи TVarData и новый тип TVariantManager, а также большое число процедур преобразования вариантных типов, вариантных записей и поддержки динами- ческих массивов. В этой области по сравнению с Delphi 5 появилось очень мно- го изменений. Основные сведения о вариантных типах представлены в главе 10 книги Essential Pascal (см. приложение В); О множество базовых типов данных, включая типы указателей и массивов, а так- же тип TDateTime, рассмотренный в главе 2 «Язык программирования Delphi»; О такие процедуры распределения памяти, как GetMem и FreeMem, а также менед- жер действительной памяти, определенный с помощью записи TMemoryManager, обращение к которому осуществляется функциями GetMemoryManager и SetMe- moryManager. К сведению, функция GetHeapStatus возвращает структуру данных THeapStatus. Две глобальные переменные (ALLocMemCount и AllocMemSize) хранят число и общий размер выделенных блоков памяти. Дополнительные сведения о памяти и использовании этих функций представлены в главе 8 «Архитектура Delphi-приложений» (в частности, пример ObjsLeft); О код поддержки пакетов и модулей, включая тип указателя Packageinfo, глобаль- ную функцию GetPackage-InfoTable и процедуру EnumModules (содержимое паке- та рассмотрено в главе 12); О огромный список глобальных переменных, включая экземпляр Windows-при- ложения Mainlnstance; IsLibrary, указывающей, является ли исполняемый файл библиотекой или самостоятельной программой; IsConsole, указывающей на кон- 0122
Модули RTL 123 потоков и CmdLine — командная строка. (Для облегчения доступа к параметрам командной строки в модуль также включены ParamCount и ParamStr.) Некоторые из этих переменных характерны только для платформы Windows, другие дос- тупны в Linux, и третьи характерны только для Linux; О программный код поддержки потоков с функциями BeginThread и EndThread; за- писи поддержки обращения к файлам и процедуры, связанные с обработкой файлов; «широкие строки» и процедуры преобразования OLE-строк, а также множество других низкоуровневых и системных процедур (включая функции автоматического преобразования типов). «Напарник» модуля System, называемый Syslnit, включает программный код инициализации системы. В него входят функции, к которым редко происходит непосредственное обращение. Это еще один модуль, который неявно включается в компиляцию, поскольку используется модулем System. Последние изменения в модуле System Я уже описал в предыдущем разделе наиболее интересные возможности модуля System. Большинство из изменений в последних версиях направлены на то, чтобы обеспечить большую перемещаемость основной RTL между платформами за счет замены характерных для Windows возможностей на общие реализации, которые теперь могут совместно использоваться Delphi и Kylix. Среди них — новые имена типов интерфейсов, полностью исправленная поддержка вариантных типов, но- вые типы указателей, поддержка динамических массивов, а также функции для настройки управления объектов-исключений. СОВЕТ------------------------------------------------------------------ Если изучить исходный программный код System.pas, то можно отметить интенсивное использова- ние условной компиляции с большим числом фрагментов {$IFDEF LINUX} и {$IFDEF MSWINDOWS}, используемых для распознания двух операционных систем. Обратите внимание, что для Windows компания Borland использует определение MSWINDOWS, указывающее на всю платформу, посколь- ку имя WINDOWS использовалось в 1б-разрядной версии операционных систем (в отличие от иден- тификатора WIN32). Например, дополнительная совместимость между Linux и Windows связана с разделителями строк в текстовых файлах. Переменная DefaultTextLineBreakStyle вли- яет на поведение процедур, осуществляющих чтение и запись файлов, включая по- чти все процедуры обработки текстовых потоков. Возможными значениями этой гло- бальной переменной являются tlbsLF (значение по умолчанию для Kylix) и tlbsCRLF (значение по умолчанию для Delphi). Тип разбиения строк также может с помощью Функции SetTextLineBreakStyle устанавливаться по принципу «файл-за-файлом». Подобным образом глобальная строчная константа sLineBreak имеет в Windows- версии IDE значение #13#10, а в Linux-версии — значение #10. Еще одно измене- ние заключается в том, что модуль System теперь включает структуры TFileRec и TTextRec, которые ранее были определены в модуле SysUtils. Модули SysUtils и SysConst Модуль SysConst определяет ряд констант-строк, используемых другими RTL-mo- Дулями для вывода сообщений. Эти строки объявлены с помощью ключевог о слова 0123
124 Глава 3. Run-Time-библиотека resourcestring и хранятся в ресурсах программы. Как и прочие ресурсы, они могут быть переведены посредством Integrated Translation Manager (Интегрированный диспетчер перевода) или External Translation Manager (Внешний диспетчер пере- вода). Модуль Syslltils является коллекцией системных функций различного назначе- ния. В отличие от других RTL-модулей, он в большей степени зависит от операци- онной системы. Модуль Syslltils не имеет специализированной направленности, но включает всего понемногу: от управления строками до поддержки локализации и многобайтных символов, от класса Exception и ряда других вторичных классов ис- ключений до множества констант и процедур форматирования строк. В частности далее в этой главе мы рассмотрим некоторые процедуры управления именами фай- лов модуля. Некоторые возможности Syslltils ежедневно используются каждым программи- стом (например, функции форматирования строк IntToStr или Format); другие ме- нее известны (глобальные переменные Windows с информацией о версии). Они уточняют платформу Windows (Windows 9х илиМТ/2000/ХР), номер версии опе- рационной системы, номер компоновки и сведения об установленных сервисных пакетах (service pack). Они могут использоваться следующим образом (фрагмент из примера WinVersion): case Wiп32Р1atform of VER_PLATFORM_WIN32_WINDOWS ShowMessage ('Windows 9x') VER_PLATFORM_WIN32_NT ShowMessage ('Windows NT') end ShowMessage ('Running on Windows ' + IntToStr (Win32MagorVersion) + ' + IntToStr (Win32MinorVersion) + ' (Build ' + IntToStr (Win32BuildNumber) + ') ' + #10#13 + 'Update ' + Win32CSDVersion) Второй фрагмент кода выдает вот такое сообщение (конечно же, с указанием установленной у вас версии операционной системы): Winversion Running on Windows! 5.Q ЩИ 2195) Update: Service Peck 3 Еще одной малоизвестной функциональной возможностью этого модуля явля- ется класс TMultiReadExclusiveWriteSynchronizer. Вероятно, это VCL-класс с самым длинным именем. Компания Borland определила более короткий псевдоним для этого класса: TMREWSync (они абсолютно равнозначны). Этот класс поддерживает многопоточность: он позволяет работать с ресурсами, которые могут использоваться несколькими потоками одновременно для чтения (многократное чтение), но в ходе записи должен использоваться одним потоком (монопольная запись). Это означа- ет, что запись невозможна до тех пор, пока не будут завершены все потоки чтения. Реализация класса TMultiReadExclusiveWriteSynchronizer в Delphi 7 был обновле- на, но аналогичные улучшения доступны с помощью неофициальной программы- — — — — —— W»T rm TTlTArrrr ТТЛ ГчАттп ттпгттггт тл Т"«-«It т С l_JТ»Z4г* Т^ТТП 0124
Модули RTL 125 оптимизирована и не приводит к взаимным блокировкам, что ранее вызывало про- блемы в коде синхронизации. СОВЕТ------------------------------------------------------------------------ Многопоточный синхронизатор является уникальным в том плане, что он поддерживает рекурсив- ные блокировки, и блокировки чтения ставит выше, чем блокировки записи. Основное предна- значение этого класса — упростить многопоточность, ускорить доступ к совместно используемым ресурсам, но, тем не менее, сохранить один поток для получения монопольного контроля над ре- сурсом в интересах выполнения относительно редкого обновления. В Delphi имеются и другие клас- сы синхронизации, объявленные в модуле SyncObjs (расположенном в Source/Rtl/Common) и тесно взаимодействующие с объектами синхронизации операционной системы (например, с разделами events и critical в Windows). Новейшие функции SysUtils В ходе выпуска двух последних версий Delphi в модуль SysUtils были добавлены несколько новых функций. Одна из них относится к преобразованию типа логи- ческий/строка (Boolean-to-string). Функция BoolToStr обычно возвращает ‘-Г и ‘О’ для истинных и ложных значений. Если указан второй необязательный параметр, функция возвращает первую строку в массивы TrueBoolStrs и FalseBoolStrs (по умол- чанию ‘TRUE’ и ‘FALSE’): BoolToStr (True) // возвращает '-Г BoolToStr (False True) // возвращает 'FALSE' по умолчанию Обратной функцией является StrToBool, которая может преобразовать строку, содержащую либо одно из значений двух упоминавшихся только что логических массивов, либо числовое значение. В последнем случае результат будет истинным, за исключением случая, когда числовое значение равно 0. Простой пример преоб- разования логических значений — функция StrDemo, представленная далее в этой главе. В последнее время в SysUtils также добавлены функции, относящиеся к преоб- разованию значений с плавающей точкой в денежный тип или в тип дата-время: FloatToCurr и FloatToDateTime используются вместо явного приведения типов. TryStr- ToFloat и TryStrToCurr производят попытку преобразования строчного типа в денеж- ное значение или в значение с плавающей точкой и вместо генерации исключения (как делают классические функции StrToFloat и StrToCurr) возвращают False. Функция AnsiDequotedStr, которая удаляет кавычки из строки, соответствует Функции AnsiQuoteStr, добавленной в Delphi 5. Говоря о строках, невозможно не упомянуть версию Delphi 6, в которой с помощью ряда процедур, включая Wide- UpperCase, WideLowerCase, WideCompareStr, WideSameStr, WideCompareText, WideSameText и WideFormat, была значительно расширена поддержка широких строк. Все эти фун- кции работают точно так же, как и их аналоги AnsiString. Три функции (TryStrToDate, TryEncodeDate и TryEncodeTime) пытаются выполнить преобразование строки в дату или осуществить кодировку даты или времени, не вызывая исключения, аналогично ранее рассмотренным функциям. Кроме того, Функция DecodeDateFully возвращает более подробную информацию (такую, как День недели), а функция CurrentYear возвращает год сегодняшней даты. Компактная, дружелюбная, обновленная версия функции GetEnvironmentVariable использует вместо параметра типа PChar строчные параметры и значительно про- ще г, „л,, игуппная ирпгия пгиованная на указателях типа PChar: 0125
126 Глава 3. Run-Time-библиотека function GetEnvironmentVariable(Name: string): string: Появились также функции, относящиеся к поддержке интерфейса. Две обнов- ленные версии малоизвестной функции Support позволяют проверить: чем поддер- живается данный интерфейс — объект или класс. Эта функция относится к пове- дению оператора is в отношении классов и отображается на метод Queryinterface. Вот пример: var Wl: (Walker; Л: [Jumper; begin Wl TAthlete.Create: // more code... if Supports (wl, [Jumper) then begin JI := Wl as [Jumper; Log (JI.Walk); end: Модуль SysUtils также включает функцию IsEqualGUID и две функции преобра- зования строк в глобальные идентификаторы (GUID) и обратно. Функция Create- GUID была перемещена в SysUtils для ее доступности в Linux (в настраиваемой ин- терпретации, конечно же). И, наконец, в последних версиях было добавлено множество функций, улуч- шающих кросс-платформенную поддержку. Так, функция AdjustLineBreaks теперь может выполнять различные корректировки в последовательностях «возврат ка- ретки» и «перевод строки», а в модуле System (как указывалось ранее) были введе- ны новые глобальные переменные для текстовых файлов. Функция FileCreate име- ет обновленную версию, в которой можно указать права доступа к файлу также как и в Unix. Функция ExpandFileName может находить файлы (в операционных системах чувствительных к регистру), даже если их регистр не имеет точного со- ответствия. Функции, связанные с разделителями пути (косая черта и обратная косая черта) имеют более общий подход, чем в ранних версиях Delphi, и переиме- нованы соответствующим образом. (Например, старая функция IncludeTrailing- Backslash теперь известна как IncludingTrailingPathDelimiter.) Что касается файлов, то в модуль SysUtils среды Delphi 7 добавлена функция GetFileVersion, осуществляющая чтение номера версии из сведений о версии, кото- рые добавляются (необязательно) в исполняемый файл Windows (вот почему эта функция не работает в Linux). Расширенные процедуры форматирования строк в Delphi 7 Большинство процедур форматирования строк (подробности о том, как можно получить электронную книгу, в которой они рассмотрены более полно, см. в При- ложении С) для определения десятичных и тысячных разделителей, форматов представления даты/времен и прочих региональных настроек используют глобаль- ные переменные. Значения этих переменных при запуске программы считываются из операционной системы (в Windows — компонент Язык и стандарты), а впослед- ствии они могут перекрываться другими настройками. Однако если при запу- щенной программе пользователь изменит компонент Язык и стандарты (Regional Settings) панели управления Windows, то данная программа прореагирует на ши- 0126
Модули RTL 127 роковещательное сообщение обновлением переменных, что, скорее всего, приве- дет к потере жестко запрограммированных изменений. Если в различных местах программы требуется использование разных форма- тов вывода, то можно воспользоваться преимуществами обновленного набора про- цедур форматирования строк; они имеют дополнительный параметр вывода Т Format- Settings, включающий все относящиеся настройки. Например, существует две версии функции Format: function Format(const Format: string; const Args: array of const): string; overload: function Format(const Format: string; const Args: array of const: const Formatsettings: TFormatSettings): string: overload; Десятки функций имеют этот новый дополнительный параметр, который в даль- нейшем используется вместо глобальных настроек. Однако их можно иницииро- вать с помощью функции GetLocaleFormatSettings, которая возвращает настройки компьютера, на котором выполняется программа, к настройкам по умолчанию (фун- кция доступна только в Windows, а не в Linux). Модуль Math Модуль Math содержит математические функции: около 40 тригонометрических функций, логарифмические и экспоненциальные функции, функции округления, полиномиальные вычисления, свыше 30 статистических функций и дюжину де- нежных функций. Описание всех функций этого модуля будет довольно утомительным, хотя не- которым читателям, вероятно, интересны математические возможности Delphi. Исходя из этого, я решил сконцентрировать внимание на математических функ- циях, появившихся в последних версиях Delphi (особенно в Delphi 6), а затем рас- смотреть вопрос, с которым чаще всего у программистов возникают проблемы — с округлением. Новые математические функции В последних версиях в модуль Math было добавлено довольно много возможнос- тей. К ним относится поддержка бесконечных констант (Infinity и Neglnfinity) и соответствующих им функций сравнения (Islnfinite и IsNan), новые тригономет- рические функции для косекансов и котангенсов, а также новые функции преоб- разования угловых значений. Удобной является обновленная функция IfThen, которая возвращает одно из Двух возможных значений в зависимости от значения логического выражения. (Аналогичная функция доступна и для строк.) Она может использоваться, напри- мер, для определения минимального из двух значений: пМщ ;= IfThen (nA < пВ, па. пВ); СОВЕТ--------------------------------------------------------------------- Функция IfThen аналогична оператору ?: языка C/C++. Я считаю ее удобной, поскольку ею можно полностью заменить выражение if/then/else, сокращая объем ввода и объявляя меньше временных переменных. Вместо традиционной функции Random можно использовать RandomRange и RandomFrom, которые обеспечивают полный контроль над случайными значениями, 0127
128 Глава 3. Run-Time-библиотека генерируемыми RTL. Первая функция возвращает число из указанного диа- пазона, а вторая — из массива допустимых чисел, передаваемых в качестве па- раметра. Логическая функция InRange может использоваться для проверки, находится ли число между двумя другими значениями. С другой стороны, функция Ensure- Range гарантирует нахождение значения в указанном диапазоне. Она возвращает само число, либо верхний или нижний предел, если число находится за пределами диапазона. Вот пример: // делает что-либо, если значение находится в диапазоне от min до max if InRange (value, min, max) then // гарантирует. что значение находится между min и max я-> value := EnsureRange (value, min, max). Еще один набор полезных функций относится к сравнениям. Числа с плаваю- щей запятой по определению являются приближенными; число с плавающей за- пятой является аппроксимацией теоретически действительного значения. При выполнении математических операций над этими числами неточность исходных значений аккумулируется в результатах. Умножение и деление одних и тех же чисел может вернуть значение, отличающееся от исходного, но очень близкое к нему. Функция SameValue позволяет проверить, являются ли два значения достаточно близкими, для того чтобы считать их одинаковыми. Можно указать, насколько близкими должны быть значения, чтобы позволить Delphi вычислить разумный диапазон ошибки для используемого вами представления. (Вот почему функция обновлена.) Аналогично, функция IsZero сравнивает числа со значением 0, исполь- зуя подобную «нечеткую логику». Функция CompareValue использует те же правила, что и для чисел с плавающей запятой, но предназначена для целых чисел; она возвращает одну из трех констант LessThanValue, EqualsValue или GreaterThanValue (соответствующиезначениям -1,0 или 1). Точно так же новая функция Sign возвращает -1, 0 или 1 для отрицательного, нулевого или положительного значения. Функция DivMod является эквивалентом обоих операций: div и mod, возвращая одновременно результат целого деления и остаток (или модуль). Функция RoundTo позволяет указать степень округления, например, округление до ближайшей ты- сячи или до двух знаков после запятой: RoundTo (123827, 3). // результат -- 124.000 RoundTo (12.3827, -2). // результат -- 12 38 ВНИМАНИЕ --------------------------------------------------------------------- Обратите внимание, что функция RoundTo использует положительные числа для округления до степеней 10 (например, 2 — до сотен) или отрицательные числа — для округления долей 10, то есть противоположно функции Round, используемой в электронных таблицах, например, в Excel. В стандартные операции округления, выполняемые функцией Round, также внесены некоторые изменения: теперь можно управлять округлением (выпол- няемым модулем FPU центрального процессора) с помощью функции SetRound- 0128
Модули RTL 129 Помимо этой также существуют функции, управляющие режимом точности и исключениям FPU. «Головная боль» округления Округление с использованием классической функции Round и более новыми фун- кциями RoundTo отражается на алгоритм округления CPU/FPU. По умолчанию, процессоры компании Intel осуществляют так называемое банковское округление, которое обычно используется в приложениях электронных таблиц. Банковское округление основано на предположении, что при округлении чи- сел, которые лежат точно посередине двух значений (заканчивающихся на 0,5), все они округляются вверх или все вниз, что статистически увеличит или умень- шит общую сумму (как правило, денег). Из-за этого правило банковского округле- ния указывает, что «половинные» числа должны округляться вверх или вниз в за- висимости от того, является ли само число четным или нечетным. Такой способ округления является сбалансированным, по меньшей мере, статистически. При- мер результатов банковского округления можно видеть на рис. 3.1. Здесь пред- ставлена работа примера Rounding, который я написал для демонстрации различ- ных типов округления. Rounding Simple ftound Troubles 0.5 > 0 1,5 > 2 2.5 > 2 3,5 > 4 4,5 •> 4 5,5 > 6 6,5 > 6 7,5 •> 8 8,5 > 8 Э.5-Я0 10,5 > 10 1,5 2,5 3.5 4,5 Г г* Рис. 3.1. Пример Rounding демонстрирует арифметическое и банковское округление В этой же программе используется и другой тип округления, предоставляемый модулем Math (функция SimpleRoundTo), в которой используется асимметричное арифметическое округление. В данном случае все «половинные» числа округлены к большему значению. Однако, как было подчеркнуто в примере Rounding, эта фун- кция при округлении чисел меньше 0 (т. е. при использовании отрицательного вто- рого параметра) работает не так, как ожидалось. В этом случае из-за ошибок пред- ставления чисел с плавающей запятой округление «обрезает» значения; например, 1-15 превращается в 1.1, вместо ожидаемых 1.2. Решение этой проблемы заключа- ется в умножении значения на 10 перед округлением, выполнении округления до Долей 10, а затем — делении его на 10: SimpleRoundTo (d * 10. 0) / 10) 0129
130 Глава 3. Run-Time-библ иотека Модули ConvUtils и StdConvs Модуль ConvUtils содержит «сердцевину» механизма преобразования, впервые по- явившегося в Delphi 6. Он использует преобразование констант, объявленных во втором модуле, StdConvs. Эти модули будут более подробно рассмотрены далее в этой главе, и я покажу, как они расширяются новыми модулями единиц измере- ний. СОВЕТ------------------------------------------------------------------ В Delphi 7 в модуль преобразования добавлено только одно усовершенствование: он добавляет поддержку для стоун (Британская единица измерений, равная 14 фунтам, или 6,34 кг). В любом случае, если вы имеете дело с единицами измерений другой системы измерений, вы высоко оцени- те возможности этого механизма. Модуль DateUtils Модуль DateUtils является коллекцией функций, связанных с датой и временем, включая функции получения значений из переменной TDateTime или вычисление значений с определенным интервалом, например: // получить значение function DayOf(const AValue TDateTime) Word function HourOftconst AValue TDateTime) Word // значение в диапазоне function WeekOfYear(const AValue TDateTime) Integer function HourOfWeek(const AValue TDateTime) Integer function SecondOfHour(const AValue TDateTime) Integer Некоторые из этих функций совершенно устарели, например, Mi UiSecondOfMonth или SecondOfWeek, но разработчики компании Borland решили предоставить пол- ный набор функций, не взирая на то, что некоторые из них непрактичны. (Некото- рые из этих функций действительно использовались в главе 2 для построения клас- са TDate.) Имеются функции для вычисления начального и конечного значения данного временного интервала (день, неделя, месяц, год), а также функции проверки и зап- роса попадания значения в диапазон; например: function DaysBetweenfconst ANow AThen TDateTime) Integer function WithinPastDays(const ANow AThen TDateTime const ADays Integer) Boolean Другие функции относятся к прямому или обратному отсчету всевозможных временных интервалов, кодирование и «запись» (замену одного элемента значе- ния TDateTime, например, дня — новым), а также реализацию «нечеткого» сравне- ния (приближение при сравнении, при котором разница в миллисекунды делает две даты одинаковыми). Кроме того, DateUtils является весьма интересным и очень простым в использовании модулем. Модуль StrUtils Модуль StrUtils впервые появился в Delphi 6 и включал некоторые новые функ- ции, связанные с обработкой строк. Одной из ключевых возможностей этого модуля является наличие функций, выполняющих сравнение множества строк. Имеются функции, основанные на алгоритме soundex (AnsiResembleText), другие обеспечивают 0130
Модули RTL 131 поиск массивов строк (AnsiMatchText и AnsilndexText), нахождение местонахожде- ния подстрок и замену текста (AnsiContainsText и AnsiReplaceText). СОВЕТ----------------------------------------------------------------------------------- Soundex — это алгоритм, который сравнивает имена, основываясь на том, как они звучат, а не на том, как они пишутся. Этот алгоритм вычисляет номер для каждого звука слова таким образом, что сравнением двух таких номеров можно определить, одинаково ли звучат эти два имени. Эта систе- ма впервые была применена в 1880 в U.S. Bureau of the Census; она была запатентована в 1918 и сейчас является всеобщим достоянием. Код системы soundex является определенной системой ин- дексации, которая переводит имена в четырехзначный код, состоящий из одной буквы и трех чисел. Дополнительные сведения доступны на страничке www.nara.gov/genealogy/coding.html. Помимо сравнения, имеются функции, обеспечивающие двунаправленную про- верку (прекрасная функция IfThen, подобная функции, которую мы уже рассмот- рели для чисел, копирования и переворота строк, а также замены подстрок). Боль- шинство из этих строчных функций были добавлены для удобства программистов Visual Basic, переходящих на Delphi. Я использовал некоторые из этих функций в примере StrDemo, в котором также используются ряд преобразований логический тип/строчный тип, определенные в модуле SysUtils. Эта программа не просто проверка работы имеющихся функций. Например, в ней используется soundex-сравнение между строками, введенными в поле редактирования, преобразование логического результата в строчный тип и его вывод: ShowMessage (BoolToStr (AnsiResemblesText (EditResemblel Text EditResemble2 Text) True)) В этой же программе представлены функции AnsiMatchText и AnsilndexText, ис- пользуемые после заполнения динамического массива строк (названного strArray) значениями строк из элемента управления «список» Я бы мог использовать более простой метод IndexOf класса TStrings, но при этом не смог бы достигнуть демонстра- тивной цели примера. Сравнение двух списков происходит следующим образом: procedure TForml ButtonMatchesCl ick(Sender TObject) begin ShowMessage (BoolToStr (AnsiMatchText(EditMatch Text strArray) True)), end procedure TForml ButtonIndexClick(Sender TObject) var nMatch Integer begin nMatch = AnsiIndexText(EditMatch Text strArray) ShowMessage (IfThen (nMatch >= 0 Matches the string number ' + IntToStr (nMatch) 'Wo match )) end Обратите внимание на использование функции IfThen в последних строках; она имеет две альтернативные строки вывода в зависимости от результатов начально- го теста (nMatch >=0). Три дополнительные кнопки выполняют простой вызов трех новых функций в следующих строках программного кода (по одной для каждого): И duplicate (3 times) a string ShowMessage (DupeString (EditSample Text 3)). 0131
132 Глава 3. Run-Time-библиотека // reverse the string ShowMessage (Reversestring (EditSample.Text)): // choose a random string ShowMessage (RandomFrom (strArray)); От Pos к PosEx В Delphi 7 в модуль StrUtils внесены небольшие изменения. Новая функция PosEx будет полезна для большинства разработчиков и заслуживает внимания. При по- иске множества вхождений строки в другой строке классическое решение Delphi основывалось на использовании функции Pos с повтором поиска в оставшейся ча- сти строки. Например, подсчитать количество вхождений одной строки в другую можно было с помощью такого программного кода: function CountSubstr (text, sub: string): Integer: van nPos: Integer: begin Result := 0: nPos := Pos (sub. text): while nPos > 0 do begin Inc (Result): text := Copy (text. nPos + Length (sub). Maxint): nPos := Pos (sub. text); end: end; Новая функция PosEx позволяет указать начальную позицию поиска внутри строки, что избавляет от необходимости изменения исходной строки (которое все же требует некоторого времени). Таким образом, представленный выше программ- ный код можно упростить следующим образом: function CountSubstrEx (text, sub: string): Integer: var nPos: Integer: begin Result := 0: nPos := PosEx (sub. text. 1): // default while nPos > 0 do begin Inc (Result): nPos := PosEx (sub. text. nPos + Length (sub)): end: end; Оба фрагмента кода используют тривиальный подход в рассмотренном ранее примере StrDemo. Модуль Types Модуль Types содержит типы данных, общие для множества операционных сис- тем. В последних версиях Delphi такие же типы были определены в модуле Windows; а сейчас они перемещены в общий модуль, совместно используемый Delphi и Kylix. Определенные здесь типы являются простыми и включают, помимо прочего, струк- туры записей TPoint, TRect и TSmallPoint плюс связанные с ними типы указателей. 0132
Модули RTL 133 ВНИМАНИЕ--------------------------------------------------------------------— Обратите внимание, что существует необходимость обновления старых Delphi-программ, которые обращались к TRect или TPoint, посредством добавления модуля Types в выражение uses; иначе их невозможно будет откомпилировать. Модули Variants и VarUtils Variants и VarUtils — это еще два модуля, появившиеся в Delphi 6 для хранения части библиотеки, связанной с вариантными типами данных. Модуль Variant содержит исходный код вариантных типов. Как упоминалось ранее, некоторые из процедур этого модуля перемещены сюда из модуля System. Функции включают общую под- держку преобразования вариантных типов, вариантных массивов, копирования вариантных типов и динамических массивов вариантных типов. Кроме того, класс TCustomVariantType определяет пользовательские типы данных. Модуль Variants полностью независим от платформы и использует модуль VarUtils, который содержит программный код, зависящий от операционной систе- мы. Для манипулирования данными вариантного типа в Delphi этот модуль ис- пользует системные API; в Kylix — настраиваемый программный код, предостав- ляемый RTL-библиотекой. СОВЕТ-------------------------------------------------------------------- В Delphi 7 эти модули были расширены, в них были исправлены некоторые неточности. Реализация вариантности была полностью переработана, что позволило повысить скорость работы этого меха- низма и снизить использование памяти программным кодом. В Delphi 7 коренным изменениям подверглась возможность управления реали- зацией вариантности, особенно правил сравнения. Изменения вариантного кода в Delphi 6 заключались в невозможности сравнения неопределенных значений (null-значений) с любыми другими значениями. Такое поведение является пра- вильным с формальной точки зрения, особенно для полей наборов данных (dataset) (область, в которой наиболее часто используются вариантные типы данных), но это изменение повлекло за собой и побочный эффект, пагубным образом влияю- щий на существующий программный код. Теперь вы можете управлять этим пове- дением с помощью глобальных переменных NullEqualityRule и NullMagnitudeRule, каждая из которых может иметь одно из следующих значений: О псгЕггог — любой тип сравнения приводит к генерации исключения, поскольку невозможно сравнивать неопределенное значение; в Delphi 6 это было значе- ние по умолчанию; О ncrStrict — любое сравнение всегда приводит к неудаче (возвращая False) неза- висимо от значений; О ncrLoose — проверка равенства будет выполнена только между null- значения- ми (null, отличающиеся от любых иных значений). При сравнении null-значе- ний считается, что они равны пустым значениям или нулям. Другие настройки, такие как NullStrictConvert и NullAsStringValue, управляют вы- полнением преобразования при наличии null-значений. Я предполагаю, что вы смо- жете самостоятельно провести эксперименты с помощью примера VariantComp, Фрагменты которого представлены в данной главе. Эта программа имеет форму 0133
134 Глава ^^ЩЬ-Пте-библиотека с группой переключателей (рис. 3.2), используемых для изменения глобальных пе- ременных NullEqualityRuLe и NullMagnitudeRule, а также несколько кнопок, выполня- ющих различные сравнения. Equal №& 0 |( * ’: Совйвал МЛ5 |;~ '.Z Сошрав Йи1, >5 :' " ' - CorapansonRutes <• Losse Г Sfaict z * : с Рис. 3.2. Форма примера VariantComp в окне конструктора Пользовательские вариантные типы и комплексные числа Еще одним из последних нововведений в концепции вариантных типов данных является возможность расширения систем типов за счет пользовательских вари- антных типов (custom variants). Этот механизм позволяет определять новые типы данных, которые в отличие от класса перегружают стандартные арифметические операторы. Вариантный тип — это тип, содержащий как спецификацию типа, так и дей- ствительное значение. Один вариантный тип может содержать строку, другой — число. Система определяет автоматическое преобразование между вариантными типами, позволяя смешивать их в ходе операций (включая пользовательские типы). Такая гибкость достигнута высокой ценой: операции над вариантными типами выполняются значительно медленнее, чем над собственными типами, и, кроме того, вариантные типы используют больше памяти. В качестве примера вариантного типа в состав установочного пакета Delphi входит интересное определение комплексных чисел (модуль VarCmplx, исходный код которого можно найти в каталоге Rtl\Common). Можно создавать комплексные вариантные типы данных с помощью функций VarComplexCreate и использовать их в любых выражениях: var vl. v2- Variant; begin vl .= VarComplexCreate (10. 12); v2 .= VarComplexCreate (10. 1): ShowMessage (vl + v2 + 5): Эти комплексные числа определены с использованием классов, но размещают- ся как вариантные типы, наследуя от класса TCustomVariantType (определенного в модуле Variants) новый класс, перекрывающий некоторые виртуальные абстрак- тные функции, и создавая глобальный объект, который заботится о регистрации в системе. Помимо этих внутренних определений модуль Variants включает боль- шой список процедур для работы с вариантными типами, включая математиче- 0134
Преобразование данных 135 ские и тригонометрические операции. Оставим их для самостоятельного изуче- ния, поскольку не всем читателям приходится использовать комплексные числа в своих программах. ВНИМАНИЕ----------------------------------------------------------------- Разработка пользовательских вариантных типов, конечно же, не является столь простой задачей, и я едва ли найду причины, по которым их необходимо использовать вместо объектов или классов. При использовании пользовательских вариантных типов вы получаете преимущество использова- ния оператора, накладывающегося на собственную структуру данных, но теряете возможность про- верки в ходе компиляции, получаете замедление выполнения, потерю некоторых характеристик ООП и вынуждены писать весьма сложный программный код. Модули DelphiMM и ShareMem Модули DelphiMM и ShareMem относятся к управлению памятью. Стандартный дис- петчер памяти Delphi объявлен в модуле System. Модуль DelphiMM определяет библиотеку альтернативного диспетчера памяти, которая будет использована при передаче строк из исполняемой программы в DLL (динамически подключаемую библиотеку Windows), которые обе были созданы в Delphi. Эта библиотека диспетчера памяти по умолчанию скомпилирована в файл библиотеки Borlndmm.dll, который должен устанавливаться вместе с вашей про- граммой. Интерфейс этого диспетчера памяти определен в модуле ShareMem. Его необхо- димо включать (он должен быть первым модулем) в проекты как исполняемых файлов, так и библиотек (подробности см. в главе 10 «Библиотеки и пакеты»). СОВЕТ---------------------------------------------------------------- Kylix, в отличие от Delphi, не содержит модулей DelphiMM и ShareMem, поскольку управление памя- тью обеспечивается собственными библиотеками Linux (в частности, Kylix использует malloc из glibc), чем и достигается совместное использование памяти различными модулями. Однако приложения Kylix с множеством модулей должны использовать модуль ShareExcept, позволяющий реализовать генерацию исключений в модуле, налагаемом на другой модуль. СОМ-модули Модули ComConst, ComObj и ComServ обеспечивают низкоуровневую поддержку тех- нологии СОМ. С моей точки зрения эти модули не являются частью RTL, поэтому я не буду рассматривать их подробно. Относящаяся к ним информация представ- лена в главе 12. В последних версиях Delphi эти модули не претерпели значитель- ных изменений. Преобразование данных Как упоминалось ранее в этой главе, Delphi содержит новый механизм (ядро) пре- образования, определенный в модуле СоnvUtils. Сам по себе механизм не содержит описания действительных единиц измерения, вместо этого конечному пользова- телю предоставляется ряд основных функций. 0135
136 Глава 3. Run-Time-библиотека Ключевой функцией является запрос преобразования — функция Convert. Вы просто предоставляете значение, единицы, в котором оно выражено, и единицы, в которые вы хотите преобразовать. Вот как выполняется преобразование темпе- ратуры 31° по Цельсию в шкалу Фаренгейта. Convert (31. tuCelsws. tuFahrenheit) Обновленная версия функции Convert позволяет конвертировать значения, со- держащие две единицы измерения, например, скорость (в которой используется единица длины и единица времени). В частности, теперь можно преобразовать «мили в час» в «метры в секунду»: Convert (20. duMlles. tuHours. duMeters. tuSeconds) Имеющиеся в этом модуле функции позволяют конвертировать результаты сложения или вычитания, проверять возможность конвертирования и даже выве- сти список допустимых единиц преобразования и их семейств. Предопределенный набор единиц измерения предоставляется модулем StdConvs. Этот модуль имеет семейства преобразований и впечатляющее число значений (сокращенный фрагмент): // Distance Conversion Units И basic unit of measurement is meters cbDistance: TConvFamily; duAng stroms: TConvType. duMicrons: TConvType; duMillimeters: TConvType: duMeters: TConvType; duKilometers: TConvType; dulnches: TConvType; duMiles: TConvType. duLightYears: TConvType; duFurlongs: TConvType; duHands: TConvType: duPicas: TConvType; Это семейство и различные единицы измерения регистрируются в механизме преобразования раздела инициализации модуля, что обеспечивает предоставле- ние коэффициентов преобразования (сохраненных в ряде констант, таких как MetersPerlnch, представленного в следующем примере): cbDistance := RegisterConversionFamily('Distance'); duAngstroms := RegisterConversionType(cbDistance. 'Angstroms'. IE-10); duMillimeters := RegisterConversionTypefcbDistance. 'Millimeters'. 0.001); dulnches •- RegisterConversionType(cbDistance. 'Inches'. MetersPerlnch); Для проверки работы механизма преобразования я построил обобщенный при- мер (ConvDemo), позволяющий поработать со всем набором имеющихся преобра- зований. В комбинированном списке программы представлены семейства доступ- ных преобразований, а в списке — доступные единицы для активного семейства. Вот фрагмент программного кода: procedure TForml.FormCreate(Sender: TObject): var i: Integer; begin GetConvFamilies (aFamilies); 0136
Прео! 137 for i Low(aFamilies) to High(aFamilies) do ComboFamilies.Items.Add (ConvFamilyToDescription (aFami1ies[i])): // get the first and fire event ComboFamilies.Itemindex 0; ChangeFamily (self); end: procedure TForml.ChangeFamily(Sender: TObject); var aTypes: TConvTypeArray; i: Integer; begin ListTypes.Clear; CurrFamily := aFamilies [ComboFamilies.Itemindex]; GetConvTypes (CurrFamily. aTypes); for i := Low(aTypes) to High(aTypes) do ListTypes.Items.Add (ConvTypeToDescription (aTypes[i])): end; Переменные aFamilies и CurrFamily объявлены в разделе private формы: aFamilies: TConvFamilyArray: CurrFamily: TConvFamily: Теперь пользователь в соответствующих полях формы может ввести две еди- ницы измерения и значение (рис. 3.3). Для ускорения операции можно выбрать значение из списка и «перетащить» его в одно из двух полей Туре. Поддержка «пе- ретаскивания» описана во врезке «Простое перетаскивание в Delphi». ConvDemo (Conversion Demo) 3 [Distance Тзгрвк' bghtYews Parsecs Cubits Fathoms Furlongs Hands Paces Rods Chains Links ................v z> enter ................;4 &nourfc z ______________________ [Centimeteis....- |100 ... ~ ' Ogjianatfaii т'я* |237.1063015836G1 £отеи1 | Points Рис. 3.3. Работа примера ConvDemo Простое перетаскивание в Delphi Пример ConvDemo, созданный для демонстрации использования механиз- ма преобразования, использует интересную технологию: «перетаскивание» (dragging). Можно поместить указатель мыши над списком, выделить пункт, а затем, удерживая нажатой левую кнопку мыши, «перетащить» пункт в поля редактирования, расположенные в центре формы. ~--- продолжение & 0137
138 Глава 3. Run-Time-библиотека Простое перетаскивание в Delphi [продолжение) Для реализации этой функциональной возможности мне необходимо было установить у свойства DragMode списка (элемент-источник) значение dmAuto- matic и обработать события OnDragOver и OnDragDrop в целевых компонентах — полях редактирования (оба поля подключены к тем же обработчикам собы- тий, совместно используя общий программный код). В первом методе про- грамма указывает, что поля ввода всегда получают значения операции «перетаскивания» независимо от источника. Во втором методе программа копирует текст, выделенный в списке (элемент-источник операции «пере- таскивания») в поле ввода, которое запускается событием (объект Sender). Вот эти два метода: procedure TForml.EdltTypeDragOver(Sender. Source: TObject: X. Y: Integer: State: TDragState; var Accept: Boolean): begin Accept := True; end: procedure TForml.EditTypeDragDropCSender. Source: TObject; X, Y: Integer); begin (Sender as TEdit).Text : = (Source as TLIstBox).Items [(Source as TLIstBox).Itemindex]; end; Единицы измерения должны соответствовать текущему семейству. В случае ошибки текст в полях Туре будет красным. Это результат первой части метода DoConvert формы, который активизируется, как только появляется значение в од- ном из полей ввода единиц измерения или при изменении конвертируемого зна- чения. После проверки типов данных в полях ввода метод DoConvert выполняет преобразование, показывая результат в четвертом, недоступном для редактирова- ния поле. В случае ошибки там же будет представлено соответствующее сообще- ние. Вот фрагмент программного кода: procedure TForml.DoConvert(Sender: TObject): var BaseType. DestType: TConvType; begin // get and check base type if not Descr1ptionToConvType(CurrFam11y. EditType.Text. BaseType) then EdltType.Font.Color .-= clRed else EdltType.Font.Color := cl Bl ack; 11 get and check destination type if not DescriptionToConvType(CurrFamily. EditDestination.Text. DestType) then EditDestination.Font.Col or := clRed el se EditDestination.Font.Color := clBlack: if (DestType = 0) or (BaseType = 0) then 0138
А конвертирование валют? 139 'ЗЯММ EditConverted.Text : = 'Invalid type' ' else EditConverted.Text : = FloatToStr (Convert ( StrToFloat (EdltAmount.Text), BaseType. DestType)); end; Если это не очень интересно, считайте, что представленные типы преобразова- ния служат лишь демонстрацией: можно полностью перенастроить механизм, пре- доставив интересующие единицы измерения (см. следующий раздел). А конвертирование валют? Преобразование денежных значений происходит не так как преобразование раз- личных систем измерений, поскольку курсы валют постоянно изменяются. Тео- ретически, в механизме преобразования можно зарегистрировать коэффици- ент пересчета, а время от времени проверять новые курсы валют, удалять старые и регистрировать новые. Однако поддержка курсов предполагает настолько час- тое изменение, что эта операция потребует значительных усилий. Кроме того, вам необходимо триангулировать преобразование: требуется определить базовую еди- ницу (если вы живете в США — американский доллар) и преобразовывать значе- ния через него, даже если вы конвертируете две другие валюты. Более интересно использовать механизм для преобразования валюты стран- представителей евро. Во-первых, коэффициенты пересчета фиксированы. Во-вто- рых, преобразование между евровалютами выполняется путем конвертирования любой валюты в евро, а затем евро-значение преобразуется в другую валюту — точно так же работает механизм преобразования Delphi. Существует лишь одна небольшая проблема: на каждом этапе преобразования необходимо использовать алгоритм округления. Мы рассмотрим эту проблему после изучения базового про- граммного кода для интеграции евровалют с механизмом преобразования Delphi. СОВЕТ----------------------------------------------------—---- Среди примеров Delphi имеется пример Convertlt, осуществляющий евро-преобразования, исполь- зующий несколько отличающийся подход к округлению, который по точности не удовлетворяет правилами преобразования европейских валют, Я решил сохранить этот пример, поскольку он яв- ляется поучительным в вопросе создания новой системы измерения. Этот пример, назовем его EuroConv, позволит научиться, как регистрировать новую систему измерений в механизме преобразования. В соответствии с шабло- ном, предоставляемым модулем StdConvs, я создал новый модуль (названный Euro- ConvConst). В разделе interface я объявил две переменные для семейства и для спе- циальных единиц измерения: interface var // Euro Currency Conversion Units cbEuroCurrency: TConvFamily: CuEUR: TConvType; cuDEM: TConvType; // Germany 0139
140 ГлОа 3. Кип-Т|те-библиотуу| cuESP. TConvType; // Spain cuFRF. TConvType; // France // и т д В разделе implementation этого модуля определены константы для различных официальных коэффициентов преобразования: implementation const DEMPerEuros = 1.95583; ESPPerEuros = 166.386: FRFPerEuros = 6.55957; // и т.д. И, наконец, программный код раздела initialization регистрирует семейство и различные валюты, каждую с собственным коэффициентом преобразования и наи- менованием: initialization // Euro Currency’s family type cbEuroCurrency := RegisterConversionFamilyt'Eurocurrency'); cuEUR ;= RegisterConversionTypet cbEuroCurrency. 'EUR'. 1); cuDEM := RegisterConversionTypef cbEuroCurrency. 'DEM'. 1 / DEMPerEuros); cuESP := RegisterConversionTypef cbEuroCurrency. 'ESP'. 1 / ESPPerEuros): cuFRF RegisterConversionType( cbEuroCurrency. 'FRF'. 1 / FRFPerEuros): COBET--------------------------------------------------------------------------------------- Механизм преобразования использует в качестве переводного коэффициента, необходимого для вычисления второй единицы, значение базовой единицы в виде константы, подобной MetersPerlnch. С другой стороны, определены официальные соотношения европейских валют. Поэтому я оставил константы преобразования с официальными значениями (DEMPerEuros) и передал их в механизм преобразования в виде дроби (1/DEMPerEuros). После того как эта единица зарегистрирована, конвертировать 120 немецких марок в итальянские лиры можно следующим образом: Convert (120. cuDEM. cuITL) Данная демонстрационная программа способна на большее: она предоставляет два списка имеющихся валют, извлеченных из предыдущего примера, и поля для ввода значений и получения результата (рис. 3.4): Программа работает прекрасно, но далека от совершенства, поскольку в ней не выполняется необходимое округление; требуется округлить не только конечный результат, но и промежуточные значения. Использование для округления непос- редственно механизма преобразования довольно сложно. Механизм позволяет либо предоставить пользовательскую функцию преобразования, либо коэффициент преобразования. Но написание идентичных функций преобразования для всех валют является нерациональным подходом, поэтому я решил пойти другим путем. (Примеры пользовательских функций преобразования можно найти в модуле StdConvs в части, относящейся к температуре.) 0140
А конвертирование’ 141 В примере EuroConv я добавил к модулю с коэффициентами преобразования пользовательскую функцию EuroConv, выполняющую необходимое преобразова- ние. Простой вызов этой функции вместо стандартной функции Convert выполняет фокус (и я не нашел в таком подходе недостатков, поскольку в таких программах сложно смешать валюту с расстояниями или температурами). В качестве альтер- нативы я мог бы наследовать от TConvTypeFactor новый класс, создав новые версии методов FromCommon и ToCommon; либо вызвать обновленную версию RegisterConver- sionType, которая получает эти две функции в качестве параметров. Однако ни одна из этих технологий не позволит мне обработать специальные случаи, такие как преобразование валют. Euro Conversion lai xi «J Euro (6) German M art •• (DEM I Spanish Pesetas (ESP) French Francs (FRF) Irish Pounds (IEP) Italian Lire (ITL) Belgian Francs (BEF) Dutch Guilders (NLG) Austrian Schillings (ATS) Portuguese Escudos (PTE) Finnish Marks (AM) Greek Drachmas (GRD) Luxembourg Francs (LUF) Г, I Euro(€) German Marks (DEM) Spanish Pesetas (ESP) French Francs (FRF) Irish Pounds (IEP) Italian Lire (ITU Belgian Francs (BEF) Dutch Guilders (NLG) Austrian Schillings (ATS) Portuguese Escudos (PTE) Finnish Marks (AM) Greek Drachmas (GRD) Luxembourg Francs (LUF) i ValueiJUO Рис. 3.4. Модуль EuroConv, демонстрирующий использование механизма преобразования Delphi с пользовательской системой единиц Вот программный код функции EuroConv, использующей внутреннюю функцию EuroRound для округления до указанного в параметре Decimals количества цифр (которое в соответствии с официальными правилами должно быть 3-6 знаков после запятой): type TEuroDecimals * 3..6; function EuroConvert (const AValue: Double; const AFrom. ATo. TConvType; const Decimals: TEuroDecimals = 3): Double; function EuroRound (const AValue. Double): Double; begin Result :- AValue * Power (10, Decimals): Result .- Round (Result); Result .= Result / Power (10, Decimals). end; begin // check special case: no conversion if AFrom - ATo then Result :- AValue else 0141
142 begin // convert to Euro then round Result = ConvertFrom (AFrom AValue) Result = EuroRound (Result). // convert to currency then round again Result = ConvertTo (Result ATo), Result = EuroRound (Result) end end. Конечно же, вам, вероятно, захочется расширить этот пример, добавив преоб- разование неевропейских валют, в конечном счете, автоматически получая курсы валют с веб-сайта. Я оставлю это для вас в качестве дополнительного упражнения. Управление файлами с помощью SysUtils Для обращения к файлам и их сведениям практически всегда можно положиться на стандартные функции, доступные в модуле SysUtils. Доверившись этим доволь- но традиционным библиотекам, вы сможете получить легко переносимый про- граммный код (хотя необходимо обратить особое внимание на файловую архитек- туру различных операционных систем, особенно на чувствительность к регистру в Linux). Так, пример FilesList, использующий сочетание FindFirst, FindNext и FindClose для извлечения из каталога списка файлов по определенной маске, можно без измене- ния программного кода использовать в Kylix и Linux (результат выполнения пред- ставлен на рис. 3.5) J^HlesUrt find Souf'ceFfes~1| Recurse Е Xbooks\md7code\03\FilesList\FilesList dpr Е Xbooks\md7code\03\Classlnfo\Classlnfo dpr < Е \books\md7code\03\lfSender\lfSender dpr E \books\md7code\03\ConvDemo\ConvDemodpr > E Xbooks\md7code\03\EuroConv\EuroConv dpr E Xbook$\md7code\03\MiniPack\MinrPack dpr E \books\md7code\03\MiniSize\MtniSize dpr E \books\md7code\03\Sl(Demo\SlrDemo dpr r E \books\md7code\03\WinVersion\WinVersrondpr 5 ; E \bookskmd7code\03\Rounding\Rounding dpr , E Xbook$Vnd7code\03\VariantCorTipXVariaritComp dpr 4 E \books\md7code\03\FjlesLi$t\F)lesLi5tForm pas * E \books\md7code\03\Clas$lnfo\)nfoForm pas E Xbook$Xmd7code\03\lfSender\SendForm pas E Xbooks\md7code\03\ConvDemo\ConvForm pas E \book$\md7code\03\EuroConv\EuroC£X)vConst pas Рис. 3.5. Результат выполнения примера FilesList Этот код добавляет имена файлов в список, названный lbFiles: procedure TForml AddFilesToList(Filter Folder string Recurse Boolean) 0142
управление файлами с помощью %sUtils 143 var sr TSearchRec. ** begin if FindFirst (Folder + Filter, faAnyFile. sr) - 0 then repeat Wiles Items Add (Folder + sr Name). until FindNext(sr) <> 0 FindClose(sr), Если установлен флажок Recurse (Рекурсия), то процедура AddFilesToList полу- чит список подкаталогов и повторно опросит локальные файлы, а затем вызовет себя из каждого подкаталога. Список каталогов помещается в список строк следу- ющим кодом: procedure GetSubDirs (Folder string. sList TStringList), var sr TSearchRec, begin if FindFirst (Folder + '* *’ faDirectory sr) = 0 then try repeat if (sr Attr and faDirectory) = faDirectory then sList Add (sr Name) until FindNext(sr) <> 0, finally FindClose(sr), end, end, И, наконец, для запроса у пользователя начального каталога поиска, данная программа использует интересную технологию вызова процедуры SelectDi rectory (рис, 3,6): if Sei ectDirectory ('Choose Folder'. " CurrentDir) then Browse for Folder g ChooseFolder f -------------------- ------------------------------- I H i—1 °? jtJ 1. I Classinfo _J ConvDemo 'J _J EuroConv J I ' I IfSender —• HI I MiniPack Я " 1 MiniSize *1 Rounding Я ' I StrDemo П I VanantComp I 41 WinVersion , I Г+ ' I П4 -TJ OK i Cancel Рис. 3.6. Диалоговое окно процедуры SelectDirectory, выводимой приложением FilesList 0143
144 Класс TObject Как упоминалось ранее, ключевым элементом модуля System является определе- ние класса TObject, являющегося родителем всех классов Delphi. Каждый класс сис- темы наследует класс TObject либо непосредственно (если TObject будет указан как базовый класс), либо неявно (если базовый класс неуказан), либо косвенно (когда в качестве родителя указан другой класс). Общая иерархия классов в программе Object Pascal имеет единый корень. Поэтому тип данных TObject можно использо- вать вместо типа данных любого класса системы в соответствии с правилами соот- ветствия типов, рассмотренными в главе 2 в разделе «Наследование и совмести- мость типов». Например, обработчики событий компонентов обычно используют параметр Sender типа TObject. Это указывает, что объект Sender может быть любого класса, поскольку все классы исходят из TObject. Типичным недостатком такого подхода является необходимость знать этот тип данных. На самом деле при наличии пере- менной или параметра типа TObject к нему можно применять только методы и свой- ства, определенные самим классом TObject. Если эта переменная или параметр ссы- лается на объект типа (например) TButton, то вы не сможете непосредственно обратиться к его свойству Caption. Решение этой проблемы заключается в исполь- зовании «операторов безопасного преобразования» (safe down-casting) и RTTI- операторов (run-time type information) (is и as), рассмотренных в главе 2. Можно также использовать и другой подход. Для любого объекта можно выз- вать методы, определенные в классе TObject. Например, метод ClassName возвраща- ет строку с именем класса. Поскольку он является методом класса (подробности см. в главе 2), его можно применять как к объекту, так и к классу. Предположим, что вы определили класс TButton и объект Buttonl этого класса. Тогда одинаковый эффект будут иметь следующие выражения: Text := Buttonl.ClassName; Text := TButton.ClassName; В некоторых случаях вам необходимо использовать имя класса, но оно также может использоваться для ссылки класса на самого себя или на базовый класс. Ссылка класса позволяет оперировать с этим классом во время выполнения (что было показано в предыдущей главе), несмотря на то что имя класса — это просто строка. Получить эти ссылки класса можно с помощью методов ClassTyре и ClassParen. Первый возвращает ссылку класса на класс объекта, а второй — ссылку класса на базовый класс объекта. После получения ссылок класса можно применить их к любым методам класса объекта TObject, например, вызвать метод ClassName. Еще одним полезным методом является InstanceSize, который возвращает раз- мер объекта во время выполнения. Можно предположить, что эти сведения можно получить с помощью глобальной функции SizeOf, но на самом деле эта функция вместо размера самого объекта возвращает размер объектной ссылки — указателя, размер которого неизменно составляет четыре байта. Полное определение класса TObject, извлеченное из модуля System, представ- ленно в листинге 3.1. Помимо методов, которые я уже упоминал, обратите внима- ние на метод InheritsFrom, который обеспечивает проверку, аналогичную исполь- зованию оператора is, но который также может быть применен к классам и ссылкам классов (первым аргументом оператора is должен быть объект). 0144
Класс TObject __________________________________________________________________Ш|_, 145 Листинг 3.1. Определение класса TObject (в RTL-модуле System) type TObject = class constructor Create: procedure Free: class function Initlnstancednstance: Pointer): TObject; procedure Cleanupinstance; function ClassType: TCI ass: class function ClassName: ShortString; class function ClassNameIs( const Name: string): Boolean: class function ClassParent: TClass: class function Classinfo: Pointer: class function InstanceSize: Longint; class function InheritsFrom(AClass: TClass): Boolean: class function MethodAddress(const Name: ShortString): Pointer: class function MethodName(Address: Pointer): ShortString; function FieldAddresstconst Name: ShortString): Pointer; function Getlnterfacetconst IID: TGUID:out Obj): Boolean; class function GetInterfaceEntry( const IID: TGUID): PInterfaceEntry; class function GetlnterfaceTable; PInterfaceTable: function SafeCallExceptiontExceptObject:. TObject; ExceptAddr: Pointer): HResult; virtual: procedure AfterConstruction; virtual; procedure BeforeDestruction: virtual; procedure Dispatchtvar Message); virtual: procedure DefaultHandlerfvar Message); virtual; class function Newlnstance: TObject: virtual; procedure Freelnstance; virtual; destructor Destroy; virtual; end; СОВЕТ-------------------------------------------------------------------------------------— Метод Classinfo возвращает указатель на внутренние RTTI-сведения (run-time type information) класса, которые представлены в следующей главе. Эти методы класса TObject доступны для объектов каждого класса, поскольку TObject является общим родительским классом для каждого класса. Вот как можно использовать эти методы для доступа к сведениям класса: procedure TSenderForm.ShowSender(Sender: TObject): begin Memol.Lines.Add ('Class Name: ‘ + Sender.ClassName); if Sender.ClassParent <> nil then Memol.Lines.Add (.’Parent Class: ' + Sender.ClassParent.ClassName): Memol.Lines.Add ('Instance Size: ' + IntToStr (Sender.InstanceSize)); end; Программный код проверяет, имеет ли ClassParent значение nil в случае исполь- зования экземпляра объекта TObject, у которого отсутствует базовый тип. Метод ShowSender является частью примера IfSender. Этот метод подключен к со- бытию OnClick некоторых элементов управления: трех кнопок, флажка и поля ввода. При щелчке на любом из этих элементов управления вызывается метод'ShowSender 0145
146 Глава 3. Run-Time-библио’ 1 1 I 'J । 111 И! JiN'Ji’S.' ' -"Я с указанием соответствующего элемента управления в качестве отправителя (sen- der) (более подробно события рассмотрены в главе 4). Одна иэ кнопок является кнопкой с изображением, объектом подкласса TButton. Результат выполнения этой программы представлен на рис. 3.7. BUttOrtZ | 8Д1П1' | |Ed#1" Class Name TButton Parent Class TButtonControl Instance Size 536 TButton OassType This is Button! Sender nhents from TButton Sender is a TButton Class Name TButton Parent Class TButtonControl Instance Size 536 TButton OassType Sender nhents from TButton Sender is a TButton Class Name TCheckBox Parent Class TCustomCheckBox Instance See 536 Class Name TEcht Parert Cass TCustorriEd* Instance See 544 Рис. 3.7. Результат работы программы IfSender Для выполнения проверки можно использовать и другие методы. Например, можно проверить, является ли объект-Sender объектом указанного типа: if Sender.ClassType = TButton then ... 5 Можно также проверить, соответствует ли параметр Sender данному объекту: if Sender = Buttonl then... Вместо проверки конкретного класса или объекта вам обычно приходится про- верять совместимость типа объекта с данным классом, т. е. необходимо проверить, является ли класс объекта определенным классом или одним из его подклассов. Это позволяет узнать, можно ли оперировать с объектом с помощью методов, оп- ределенных для этого класса. Такая проверка может быть выполнена с помощью метода InheritsFrom, который также вызывается при использовании оператора is. Эти две проверки эквивалентны: if Sender InheritsFrom (TButton) then ... if Sender is TButton then Вывод сведений о классе Я расширил пример IfSender для того, чтобы показать полный сш.сок базовых клас- сов данного объекта или класса. После того как вы получили ссылку класса, вы можете добавить все его базовые классы в список ListParent с помощью следующе- го программного кода: with ListParent Items do begin Clear. while MyClass ClassParent <> ml do begin MyClass = MyClass.ClassParent: Add (MyClass.ClassName): 0146
Что далее?; i 147 end: end. Вы могли заметить, что я использовал ссылку класса в середине цикла while, который проверяет отсутствие родительского класса (т. к. текущим классом явля- ется TObject). Помимо этого я мог бы написать выражение while любым способом: while not MyCl ass .Cl assNamels ('TObject") do... while MyClass <> TObject do... Программный код в выражении with, ссылающийся на элемент ListParent, явля- ется частью примера Classinfo, который выводит список родительских классов и ряд дополнительных сведений о некоторых компонентах VCL (которые обычно рас- положены на странице Standard палитры компонентов). Эти компоненты вруч- ную добавляются в динамический массив, содержащий классы, и объявленный как: private ClassArray: array of TCI ass: При запуске программы этот массив используется для представления в списке имен всех классов. Выбор элемента списка приводит к запуску визуального пред- ставления дополнительных сведений и представления его базовых классов (рис. 3.8). Class Info gClawName TButton TBitBtn ТЕ dil TPopupMenu TRadioButton TRadroG ioud ^Nairne TPanel - See 544 bytes BaseCIwse»' TPanel TCheckBox TForm TComboBox TGroupBox TSpeedButton T Label TCustomPanel TCustomControl TWmControl T Control TComponent TPersistent TObject Рис. 3.8. Результат работы программы Classinfo СОВЕТ--------------------------------------------------------------------— В качестве дальнейшего расширения возможностей этого примера вы можете создать дерево иерар- хии со всеми базовыми классами различных компонентов. Для этого я создал мастер VdHierarchy, рассмотренный в Приложении А «Дополнительные инструменты для Delphi, разработанные авто- ром». Что далее? В этой главе я сконцентрировался на новых возможностях Run-Time-библиотеки Delphi, основанной на функциях. Я предоставил лишь обобщенную сводку RTL, а не полный обзор (который бы занял много места). Дополнительные примеры по базовым функциям RTL можно найти в бесплатных электронных книгах, разме- тенных на моем веб-сайте (см. Приложение В). В следующей главе мы перейдем от RTL, основанной на функциях, к RTL, о, Нованной на классах, которая является ядром библиотеки классов Delphi. Я не буду 0147
148 Глава 3. Run-Time-библио *вь рассуждать, является ли ядро классов общим для VCL и CLX (например, TObject, который обычно относится как к RTL, так и к библиотеке классов). В этой главе я рассмотрел все, что определено в модулях System, SysUtils и других модулях, со- держащих функции; следующая глава посвящена модулю Classes и другим базо- вым модулям, определяющим классы. Совместно с предыдущей главой, посвященной языку Delphi, глава 4 предста- вит основы для рассмотрения визуальных классов и классов баз данных (или компонентов, если вам так больше нравится). Рассматривая различные модули биб- лиотек, вы найдете большое количество глобальных функций, которые не принад- лежат к ядру RTL, но также являются очень полезными. 0148
4 Классы базовой библиотеки В предыдущей главе вы видели, что в состав Delphi входит большое число функ- ций и процедур, но реальная сила визуального программирования заложена в ог- ромной библиотеке классов. Стандартная библиотека классов Delphi содержит сотни классов с тысячами методов, и она настолько велика, что я, естественно, не могу представить их подробное описание в данной книге. Вместо этого мы иссле- дуем различные области этой библиотеки, начав с данной главы, и продолжим в по- следующих. Эта глава посвящена базовым классам библиотеки, а также некоторым стан- дартным технологиям программирования, таким как определение событий. Мы рассмотрим некоторые наиболее распространенные классы: lists (списки), string lists (списки строк), collections (коллекции) и streams (потоки). Большую часть главы я посвятил исследованию содержания модуля Classes, но мы также рассмотрим и другие «стержневые» модули библиотеки. Классы Delphi могут использоваться либо полностью в программном коде, либо в конструкторе визуальных форм. Некоторые из них являются классами компо- нентов, представленных на палитре компонент, а другие имеют более общее на- значение. Понятия «класс» и «компонент» могут использоваться в Delphi почти как синонимы. Компоненты — центральные элементы Delphi-приложений. Разра- ботка программы основывается на выборе числа компонентов и определения их взаимодействия — вот и все, в чем заключается визуальное программирование в Delphi. Перед тем как перейти к изучению данной главы, необходимо иметь четкое пред- ставление о языке программирования, включая наследование, свойства, визуаль- ные методы, ссылки классов и т. п. (т. е. то, что обсуждалось в главе 2 «Язык про- граммирования Delphi»). В данной главе рассмотрены следующие темы: о RTL-пакет, CLX и VCL; ° TPersistent и published; ° базовый класс TComponent и его свойства; ° компоненты и принадлежность (владение); ° события; ° списки, контейнеры классов и коллекции; ° организация потоков; ° модули RTL-пакета. 0149
150 Глава 4^ RTL-пакет, CLX и VCL До появления Delphi пятой версии библиотека классов была известна как VCL, что является сокращением от Visual Components Library. Это библиотека компо- нентов, отображаемая на вершину API Windows. Kylix, Linux-версия Delphi, пред- ставляет новую библиотеку компонентов, названную CLX (Component Library for X-Platform или Cross Platform; сокращение произносится «кликс»), Delphi 6 была первой версией, в которой были представлены и VCL, и CLX. Для визуальных компонентов две библиотеки классов являются взаимоисключающими. Однако базовые классы, а также разделы обеих библиотек, относящиеся к работе с базами данных и Интернетом, используются совместно. VCL рассматривалась как отдельная большая библиотека, хотя программисты обычно обращаются к ее различным частям (компонентам, элементам управления, невизуальным компонентам, наборам данных, элементам управления, относящимся к работе с данными, Интернет-комопнентам и т. д) В CLX введено различие меж- ду четырьмя частями: BaseCLX, VisualCLX, DataCLX и NetCLX. Только в исполь- зовании VisualCLX существует полностью различный подход при работе с плат- формами Windows и Linux; остальные части программного кода наследственно перемещаемы в Linux. В следующем разделе мы рассмотрим эти библиотеки; ос- тальная часть главы посвящена общим базовым классам. В последних версиях Delphi эта разница особо подчеркивается тем фактом, что базовые невизуальные компоненты и классы библиотеки являются частью нового RTL-пакета, который используется как VCL, так и CLX. Тем не менее использование этого пакета в невизуальных приложениях (например, программы веб-сервера) по- зволяет значительно сократить размер устанавливаемых файлов и нагрузку на память. Традиционные разделы VCL Delphi-программисты при ссылке на эти разделы VCL используют имена, которые изначально предложила компания Borland в своей документации — имена, кото- рые для различных групп компонентов впоследствии стали общими. Технически компоненты являются подклассами классаTComponent, являющегося одним из кор- невых классов иерархии (рис. 4.1). Класс ТСотропеп1является наследником класса TPersistent; роль этих двух классов будет объяснена в следующем разделе. Помимо компонентов, библиотеки включают классы, которые наследуются не- посредственно от классов TObject и TPersistent. Обобщенно эти классы в разделах документации называются Objects (Объекты) — совершенно сбивающее с толку название. Эти бескомпонентные классы зачастую используются в качестве значе- ний свойств или как вспомогательные классы, используемые в программном коде; не являясь наследниками TComponent, эти классы не могут непосредственно исполь- зоваться в визуальном программировании. СОВЕТ----------------------------------------------------------- Точнее говоря, некомпонентные классы не могут стать доступными через палитру компонентов и не могут быть «перетащены» непосредственно в форму, но они могут визуально управляться с по- мощью окна инспектора объектов (Object Inspector) как подсвойства других свойств или пункты коллекций различных типов. Поэтому при работе в конструкторе форм как раз некомпонентные классы зачастую проще в использовании. 0150
RTL-пакет, i 151 (other TComponent subclasses) Рис. 4.1. Графическое представление основных групп VCL-компонентов Далее компонентные классы могут быть разделены на две основные группы: элементы управления и невизуальные компоненты. Элементы управления Это все классы, которые исходят от TControL Элементы управления характеризу- ются положением и размером на экране и видны в формах как во время разработ- ки, так и во время выполнения на одном и том же месте. Элементы управления имеют две различные подспецификации — относящуюся к окнам и графическую. Более подробно эти подспецификации будут рассмотрены в главе 5 «Визуальные элементы управления». Невизуальные компоненты Это все компоненты, не являющиеся элементами управления — все классы, исходя- щие от TComponent, а не от TControL Во время разработки невизуальные компоненты представлены на форме или в модуле данных в виде значка с расположенными под ними наименованиями (в формах наименование является необязательным). Во время выполнения некоторые из этих компонентов могут быть видимы (напри- мер, стандартные диалоговые окна), а другие — вовсе невидимы (например, ком- понент «таблица базы данных»). ПРИМЕЧАНИЕ----------------------------------------------------------- Для того чтобы посмотреть всплывающую подсказку с именем и типом класса (а также некоторую Дополнительную информацию), можно в конструкторе форм поместить указатель мыши над эле- ментом или элементом управления. Для того чтобы под значком невизуального компонента было представлено его наименование, необходимо установить параметр среды Show Component Captions (Показывать названия компонентов). Традиционная классификация VCL очень знакома Delphi-программистам. Даже после введения CLX и новых схем именования традиционные имена, вероятно, Уцелеют и по-прежнему сохранятся в лексиконе Delphi-программистов. Структура CLX настоящее время компании Borland при ссылке на различные разделы CLX-биб- лиотеки в Linux использует одну терминологию, и совершенно другую (или менее четкую) структуру имен при работе с Delphi. Новое подразделение кросс-плат- 0151
152 Глава 4. Классы базовой библиотеки форменной библиотеки представляет больше логических областей, чем структура иерархии классов: BaseCLX Ядро библиотеки классов: наивысшие классы (такие, как TComponent) и ряд общих вспомогательных классов (включая списки, контейнеры, коллекции и потоки). По сравнению с соответствующими классами VCL, BaseCLX почти совершенно неиз- менна и перемещаема между платформами Windows и Linux. Данная глава посвя- щена в основном изучению BaseCLX и общих базовых классов VCL. VisualCLX Коллекция визуальных компонентов, обычно называемых элементами управле- ния. Это раздел библиотеки, который более тесно связан с операционной систе- мой: VisualCLX реализована на верхнем уровне библиотеки Qt, доступной как в Windows, так и в Linux. Использование VisualCLX обеспечивает полную пере- мещаемость визуальной части вашего приложения между Delphi в Windows и Kylix в Linux. К тому же большинство из компонентов VisualCLX имеют соответствую- щие элементы управления в VCL, так что можно довольно легко переместить про- граммный код из одной библиотеки в другую. Элементы управления VisualCLX и VCL будут рассмотрены в главе 5. DataCLX Это все компоненты библиотеки, относящиеся к базам данных. DataCLX — это интерфейсная часть нового механизма dbExpress, входящего как в Delphi, так и в Kylix. Delphi также включает интерфейсную часть BDE, dbGo и InterBase Express (IBX). Если вы будете рассматривать эти компоненты как часть DataCLX, то толь- ко интерфейсная часть dbExpress и IBX переносимы между Windows и Linux. По- мимо этого DataCLX содержит компонент ClientDataSet, который теперь называет- ся MyBase, а также другие связанные с ним классы. Delphi-компоненты доступа к данным рассмотрены в третьей части этой книги. NetCLX Это компоненты, относящиеся к Internet: начиная с компонента WebBroker frame- work до HTML producer, от Indy (Internet Direct) до Internet Express, от WebSnap до поддержки языка XML. Этот раздел библиотеки опять же обладает высокой переносимостью между Windows и Linux. Поддержка Интернета рассмотрена в четвертой части книги. (Имя, короткое для Internet CLX, и не имеет ничего об- щего с технологией .NET компании Microsoft, которой она предшествовала.) Разделы библиотеки, характерные для VCL Рассмотренные ранее области библиотеки с упомянутыми различиями доступны как в Delphi, так и в Kylix. Однако в Delphi ряд разделов VCL по одной или другой причине специфичны только для Windows: О структура Delphi ActiveX (DAX) обеспечивает поддержку для COM, OLE Auto- mation, ActiveX и прочих элементов, связанных с СОМ технологий. Более под- робно этот вопрос рассмотрен в главе 12 «От СОМ к СОМ+». О компонент Decision Cube обеспечивает поддержку Online Analytical Processing (OLAP), но связан с BDE и в последнее время не обновлялся. Decision Cube в этой книге не рассматривается. 0152
Класс TPersistent 153 И, наконец, в стандартный установочный пакет Delphi входят ряд компонентов сторонних производителей, такие как TeeChart для деловой графики, RAVE для создания и вывода на печать отчетов и IntraWeb для Интернет-разработок. Неко- торые из этих компонентов будут рассмотрены в этой книге, но они не являются непосредственной частью VCL. RAVE и IntraWeb также доступны и в Kylix. Класс TPersistent Первым базовым классом библиотеки Delphi, который мы рассмотрим, будет TPersis- tent, который является весьма странным классом: он представлен очень малым объемом программного кода и совершенно не имеет непосредственного примене- ния, но является фундаментом для всей идеи визуального программирования. Определение класса представлено в листинге 4.1. Листинг 4.1. Определение класса TPersistent в модуле Classes TPersistent = class(TObject) private procedure AssignError(Source: TPersistent): protected procedure AssignTotDest: TPersistent): virtual: procedure DefinePropertles(F11er: TFiler): virtual: function GetOwner: TPersistent: dynamic: public destructor Destroy: override: procedure Assign(Source: TPersistent): virtual: function GetNamePath: string; dynamic: end: Как можно понять из имени, этот класс оперирует постоянством, т. е. сохраняет значение объекта в файл для последующего использования при воссоздании объекта в том же виде и с теми же данными. Постоянство (перманентность) — ключевой эле- мент визуального программирования. На самом деле (как вы уже знаете из главы 1 «Среда Delphi 7 и ее IDE») во время разработки в Delphi вы манипулируете действи- тельными объектами, которые сохраняются в DFM-файлах и воссоздаются в ходе выполнения программы при создании специального контейнера (формы или модуля). СОВЕТ------------------------------------------------------------------------------ Все, что сказано об DFM-файлах, также относится и к формату XFM-файлов, используемых CLX- приложением. Их форматы идентичны. Разница в расширении имени файла важна, поскольку она используется Delphi для определения, основана ли форма на CLX/Qt или на VCL/Windows. В Kylix каждая форма является CLX/Qt-формой, независимо оттого, какое расширение используется; пото- му расширение XFM/DFM-фалов в Kylix практически не имеет никакого значения. Поддержка потоков в класс TPersistent не встроена, но она обеспечивается дру- гими классами, которые нацелены на TPersistent и его подклассы. Другими слова- ми, вы можете «обеспечивать постоянство» стандартных исключительно-поточ- ных объектов классов, наследуя их от TPersistent. Одна из причин такого подхода заключается в том, что этот класс компилируется с включением специального па- раметра — {$М+}. Этот флаг активирует генерацию расширенной RTTI-информа- Чии для публикуемого раздела класса. 0153
154 Глава 4. Классы базовой библиотеки Поточная система Delphi не пытается сохранить находящиеся в памяти данные объекта, которые могут быть комплексными ввиду наличия множества указателей на другие места памяти и полной их бессмысленности при повторной загрузке объекта. Вместо этого Delphi сохраняет объекты посредством перечисления зна- чений всех свойств в разделе published класса. Когда свойство ссылается на другой объект, в зависимости от его типа и отношения к основному объекту Delphi сохра- няет имя этого объекта или весь объект (с помощью аналогичного механизма). Сравнение с другими подходами представлено во вставке «Поточная передача объектов и генерация программного кода» Единственный метод класса TPersistent, который вы будете использовать, — это процедура Assign, которая может использоваться для копирования фактического значения объекта. В библиотеке этот метод реализуется многими бескомпонент- ными классами, но лишь несколькими компонентами. Большинство подклассов повторно реализуют виртуальный защищенный метод AssignTo, вызываемой стан- дартной реализацией Assign. К другим методам относятся метод DefineProperties, используемый для настрой- ки потоковой системы и добавления дополнительной информации (псевдосвойств); а также методы GetOwner и GetNamePath, используемые коллекциями и другими специальными классами для собственной идентификации в инспекторе объектов. Поточная передача объектов и генерация программного кода Подход, используемый Delphi (и Kylix), отличается от подхода, используе- мого другими языками и визуальными средствами разработки. Например, в Java эффект определения формы внутри IDE — это генерация исходного текста Java, используемого для создания компонентов и установки их свойств. Установка свойств в инспекторе влияет на исходный программный код. Что-то подобное происходит и в С#, хотя в этом языке свойства ближе к понятию свойств в Delphi. Вы уже видели это в Delphi; вы можете напи- сать программный код, генерирующий компоненты вместо того, чтобы пе- реложить это на поток, но поскольку в IDE нет специальной поддержки это- го варианта, то приходится писать этот код вручную. Каждый из двух этих подходов имеет свои преимущества и недостатки. При генерации исходного кода осуществляется полный контроль над тем, что происходит, а также над точной последовательностью создания и ини- циализации. Delphi перезагружает объекты и их свойства, но откладывает некоторые назначения до более поздней стадии привязки, что позволяет избегать проблем ссылок на еще не инициализированные объекты. На са- мом деле этот процесс очень сложный, но он полностью скрыт от програм- миста. Язык Java позволяет с помощью такого инструмента, как JBuilder, по- вторно перекомпилировать класс формы и загружать его в выполняемую программу при каждом изменении. В компилирующей среде (такой, как Delphi), этот подход был бы более сложен (во время разработки Delphi ис- пользует подставную версию формы, технически называемую заменителем, а не действительную форму). Преимущество такого подхода, используемого в Delphi, заключается в том, что DFM-файлы могут быть перенесены на различные платформы, не 0154
IPiacc TPersistent 155 влияя на исходный программный код; вот почему Java предлагает XML-no- стоянство форм. Другое различие заключается в том, что Delphi, вместо того чтобы ссылаться на внешние файлы, внедряет графические элементы ком- понента в DFM-файл. Это упрощает установку готовой программы (потому что в конечном итоге все содержится в исполняемом файле), но приводит к увеличению ее размеров. Ключевое слово published Delphi имеет четыре директивы, определяющие доступ к данным: public (общий), protected (защищенный), private (частный) и published (опубликованный). Первые три уже рассматривались в главе 2 «Язык программирования Delphi». А сейчас пришло время рассмотреть, что означает published. Для любого опубликованного поля, свойства или метода компилятор генери- рует расширенную RTTI-информацию, что позволило бы программе или среде выполнения Delphi запросить у класса его опубликованный интерфейс. Напри- мер, каждый компонент Delphi имеет опубликованный интерфейс, используемый IDE, в частности, инспектором объектов. Надлежащее использование опублико- ванных пунктов важно при самостоятельном создании компонентов. Обычно опуб- ликованная часть компонента не содержит никаких полей или методов, а лишь свойства и события. Когда Delphi генерирует форму или модуль данных, она помещает определе- ния его компонентов и методов (обработчиков событий) в первом разделе его оп- ределения, перед ключевыми словами public и private. Поля и методы, помещенные в начальном разделе класса, являются опубликованными. Когда перед элементом класса компонента отсутствует какое-либо ключевое слово, то значением по умол- чанию является published. Если быть более точным, published является заданным по умолчанию ключевым словом только в том случае, если класс компилировался с директивой компилято- ра $М+ или исходит от класса, компилируемого с $М+. Эта директива используется в классе TPersistent, поэтому большинство классов VCL и все классы компонентов по умолчанию являются опубликованными. Однако бескомпонентные классы (типа TStream и TList) компилируются с директивой $М-, обеспечивающей види- мость public. Методы, используемые для обработки событий в IDE (и в DFM файлах), дол- жны быть опубликованы, и поля, соответствующие компонентам в форме, также должны быть опубликованы для того, чтобы автоматически подсоединяться к объ- ектам, описанным в DFM-файле и созданным вместе с формой. (Позже в этой гла- ве мы рассмотрим детали этого процесса и его проблемы.) Обращение к опубликованным полям и методам Как я уже говорил, в разделе published-класса имеют смысл три различных объявления: поля, методы и свойства. В программном коде обращение к опуб- ликованным элементам осуществляется как к общим элементам, т. е. с по- ~ продолжение 0155
156 Глава 4. Классы базовой библиотеки Обращение к опубликованным полям и методам {продолжение) мощью соответствующих идентификаторов. Тем не менее в отдельных слу- чаях имеется возможность обратиться к опубликованным элементам на эта- пе выполнения по имени. Я рассмотрю динамический доступ к свойствам в разделе «Доступ к свойствам по имени». Здесь я лишь кратко представлю способы взаимодействия с полями и методами во время выполнения. В классе TObject имеется три интересных метода: MethodAddress, MethodName и Field- Address. Первая функция, MethodAddress, возвращает адрес размещения в памяти компилированного кода (своего рода указатель функции) метода, передава- емого в строке как параметр. Выполняя присвоение адреса этого метода полю Code структуры TMETHOD и назначая объект полю Data, можно получить пол- ный указатель метода. Теперь для вызова метода необходимо привести его к надлежащему типу указателя метода. Вот фрагмент кода, подчеркиваю- щий ключевые моменты этой технологии: var Method: TMethod: Evt: TNotifyEvent; begin Method.Code := MethodAddress ('ButtonlClick'); Method.Data ;= Self: Evt := TNotifyEvent(Method): Evt (Sender): // вызвать этот метод end: Delphi использует подобный код для назначения обработчика случая при загрузке DFM-файла, потому что эти файлы хранят имена методов, кото- рые обычно используются для обработки событий, в то время как компо- ненты хранят указатель метода. Второй метод, MethodName, делает обратное преобразование, возвращая имя метода, расположенное в указанном адресе памяти. Этот метод может использоваться для получения имени обработчи- ка события по его значению, что и делает Delphi при поточной передаче ком- понента в DFM-файл. И, наконец, FieldAddress, метод класса TObject, который возвращает место- положение в памяти опубликованного поля по его имени. Delphi использу- ет этот метод для подсоединения компонентов, созданных на основе DFM- файла, вместе с их полями (например, формой), имеющими то же имя. Обратите внимание, что эти методы редко используются в «нормальных» программах, но играют ключевую роль в работе самой среды Delphi. Они прочно связаны с поточной системой. Их рекомендуется использовать только при написании чрезвычайно динамичных программ, мастеров специально- го назначения или других расширений Delphi. Обращение к свойствам по имени Инспектор объектов (Object Inspector) выводит список опубликованных свойств объекта, даже если им является написанный вами компонент. При этом он осно- вывается на RTTI-информации, имеющейся у опубликованных свойств. С по- 0156
Класс TPersistent 157 мощью дополнительных технологий приложение может извлечь список опубли- кованных свойств и использовать их. Хотя об этом не все знают, в Delphi можно обратиться к свойствам по имени, просто указав строку с именем свойства, и затем извлечь его значение. Доступ к RTTI-информации свойств обеспечивается через группу недокументированных подпроцедур части модуля Typlnfo. ВНИМАНИЕ--------------------------------------------------------------- В последних версиях Delphi эти подпроцедуры никогда не документировались. Компания Borland хочет оставить за собой возможность их изменения. Однако от версий Delphi 1 до 7, эти изменения были очень незначительны и связаны лишь с поддержкой новых возможностей, что обеспечивало высокий уровень обратной совместимости. В Delphi 5 компания Borland добавила несколько поло- жительных моментов и ряд «вспомогательных» процедур, которые представлены официально (хотя в справочной системе имеются лишь комментарии). Вместо рассмотрения всего модуля Typlnfo мы познакомимся лишь с той его частью, которая обеспечивает доступ к свойствам по имени. До версии Delphi 5, для того чтобы извлечь указатель на некоторую внутреннюю информацию свой- ства и затем применить одну из функций доступа типа GetStrProp, к этому указате- лю необходимо было использовать функцию GetPropInfo. Вы также должны были проверить существование и тип свойства. Теперь можно использовать новый набор процедур Typlnfo, включая удобную процедуру GetPropValue, которая возвращает вариантный тип со значением свой- ства и генерирует исключение, если свойство не существует. Для того чтобы избе- жать генерации исключения, сначала можно вызывать функцию IsPublishedProp. Вы просто передаете этим функциям объект и строку с именем свойства. Следую- щий дополнительный параметр GetPropValue позволяет выбирать формат возвра- щения значений свойств любого набора типов (строчное либо числовое значение набора). Например, можно вызвать: ShowMessage (GetPropValue (Buttonl. 'Caption')); Этот запрос имеет тот же самый эффект, что и запрос ShowMessage, с передачей в качестве параметра Buttonl.Caption. Единственное реальное различие заключает- ся в том, что данная версия программного кода выполняется значительно медлен- нее, поскольку компилятор разрешает нормальный доступ к свойствам более эф- фективным способом. Преимущество доступа во время выполнения состоит в том, что его можно сделать очень гибким, как в примере RunProp. Эта программа отображает в списке значение свойства любого типа для каждо- го компонента формы. Имя необходимого свойства можно ввести в строке редак- тирования. Это делает программу очень удобной. Помимо этого поля и списка форма имеет кнопку, осуществляющую вывод и добавление других компонентов для проверки их свойства. При щелчке на этой кнопке выполняется следующий код: uses Typlnfo; Procedure TForml.ButtonlClick(Sender: TObject); var I: Integer; Value: Variant: begin ListBoxl.Clear; 0157
158 Глава 4 for I := 0 to Componentcount -1 do ' begin if IsPublishedProp (Components!;!]. Editl.Text) then begin Value := GetPropValue (Components!;I], Editl.Text); ListBoxl.Items.Add (Components!;!].Name + + Editl Text + ' = ' + string (Value)); end el se ListBoxl Items.Add ('Wo ' + Components!’].Name + + Editl.Text); end; end; На рис. 4.2 представлен результат щелчка на кнопке Fill List (Заполнить спи- сок) при использовании заданного по умолчанию в строке редактирования значе- ния Caption. Вы можете пробовать указать другое имя свойства. Числа будут кон- вертированы к строчным значениям вариантным преобразованием. Объекты (типа значения свойства Font) будут отображены как адреса памяти. Labell Caption bPtoperty No Bevell Caption No Edkl Caption Buttonl Caption « bFii List No ListBoxl Caption RadioButtonl Caption RadioButtonl CheckBoxI Caption CheckBoxI No SciollBad Caption NoSpnEditl Caption No ComboBoxI Caption Рис. 4.2. Результат работы примера RunProp, который во время выполнения осуществляет доступ к свойствам по их имени ВНИМАНИЕ ---------------------------------------------------------------- Не стоит использовать модуль Typlnfo вместо поддержки идеи полиморфизма и других методик доступа к свойствам. Сначала используйте доступ к базовому свойству класса, при необходимо- сти — безопасное приведение типов, а RTTI-доступ к свойствам оставьте как последний вариант. Использование методов модуля Typlnfo замедляет выполнение программного кода, усложняет его и делает более уязвимым к ошибке оператора; фактически, он уклоняется от проверки типов на этапе компиляции. Класс TComponent На первый взгляд класс TPersistent более важен, но ключевым классом в ядре базиру- ющейся на компонентах библиотеки классов Delphi является TComponent, наследу- емый от TPersistent (и от TObject). Класс TComponent определяет множество стержне- вых элементов компонента, однако он не настолько сложный, как может показаться, поскольку практически все, что вам может понадобиться, предоставляют базовые классы и сам язык. 0158
Класс TComponent 159 Я не буду рассматривать все детали класса TComponent, ряд из которого более важен для разработчиков компонентов, а не для пользователей компонентов. Мы рассмотрим только вопрос принадлежности (который учитывается для некоторых public-свойств класса) и двух published-свойств класса: Name и Тад. Принадлежность Одной из стержневых характеристик класса TCom ponent является определение при- надлежности (ownership). При создании компонента он может быть присвоен вла- дельцу компонента, который будет отвечать за его уничтожение. Поэтому каждый компонент может иметь владельца, а может сам являться владельцем (owner) дру- гих компонентов. Несколько public-методов и свойств данного класса специально предназначе- ны для обработки обеих сторон владения. Вот список, извлеченный из объявления класса (в модуле Classes библиотеки VCL): type TComponent = cl ass(TPersistent. Ilnterface. IlnterfaceComponentReference) public constructor Create(AOwner: TComponent): virtual: procedure DestroyComponents. function FindComponentfconst AName: string). TComponent: procedure InsertComponenttAComponent: TComponent): procedure RemoveComponent(AComponent: TComponent): property Components[Index: Integer]: TComponent read GetComponent: property Componentcount. Integer read GetComponentCount; property ComponentIndex: Integer read GetComponentIndex write SetComponentIndex: property Owner- TComponent read FOwner; Если вы создаете компонент и назначаете ему владельца, то он будет добавлен в список компонентов (InsertComponent), который доступен с помощью свойства- массива Components. Указанный компонент имеет Owner (владельца) и с помощью свойства Componentindex «знает» его положение в списке владельцев компонента. И, наконец, деструктор владельца с помощью DestroyComponents будет заботиться о разрушении «своего» объекта. Имеются еще несколько protected-методов, но сказанного уже вполне достаточно, чтобы иметь полное представление. Важно подчеркнуть, что принадлежность компонента (если он используется Должным образом) может решить многие из проблем управления памятью ваших приложений. При использовании конструктора форм или конструктора модуля Данных IDE эта форма или модуль данных будет владеть любым компонентом, помещенным на нее. В то же время даже в программном коде создавать компонен- ты необходимо с привязкой принадлежности. Из этих соображений требуется лишь не забывать уничтожать контейнеры компонентов (форма или модуль данных), когда они больше не нужны, забыв о расположенных на них компонентах. Напри- мер, можно удалить форму, чтобы одновременно уничтожить все расположенные на ней компоненты, вместо отдельного уничтожения каждого объекта. По большо- му счету формы и модули данных вообще принадлежат объекту Application, кото- рый уничтожается VCL-кодом завершения, при котором освобождаются все кон- тейнеры компонентов, очищающие расположенные на них компоненты. 0159
160 Глава 4. Классы базовой Массив Components Свойство Components также может использоваться для обращения к компоненту, принадлежащему другому компоненту, скажем, форме. Это свойство может быть очень удобно (по сравнению с использованием непосредственно указанного ком- понента) для написания общего программного кода, воздействующего на все или множество компонентов одновременно. Например, для добавления к списку имен всех компонентов формы можно использовать следующий программный код (этот фрагмент — часть примера ChangeOwner, представленного в следующем разделе): procedure TForml.ButtonlClick(Sender: TObject): var I: Integer; begin ListBoxl.Items.Clear: for I := 0 to Componentcount - 1 do ListBoxl.Items.Add (Components [IJ.Name); end. Этот программный код использует свойство ComponentCount, которое содержит общее количество компонентов, принадлежащих текущей форме, а также свойство Components, являющееся списком находящихся в собственности компонентов. При обращении к значению из этого списка вы получаете значение типа TComponent. Поэтому можно непосредственно использовать только свойства, общие для всех компонентов, например, свойство Name. Для использования свойств, специфич- ных для определенных компонентов, необходимо использовать соответствующее приведение as. СОВЕТ------------------------------------------------------------------------------- В Delphi некоторые компоненты сами по себе являются контейнерами компонентов: группа пере- ключателей (GroupBox), панель (Panel), страница с вкладками (PageControl) и, конечно, компонент форма (Form). При использовании этих элементов управления в них необходимо добавлять другие компоненты. В этом случае контейнер является родителем для этих компонентов (что указывается в свойстве Parent), а форма также является их владельцем (что указывается в свойстве Owner). Для перехода от одного дочернего элемента к другому можно воспользоваться свойством Controls груп- пы переключателей или формы, а для перехода между всеми подчиненными компонентами, неза- висимо от их родителей, можно использовать свойство Controls группы переключателей или формы. Используя свойство Components, можно всегда обратиться к любому компонен- ту формы. Однако если необходимо обратиться к определенному компоненту, вме- сто сравнения каждого имени с именем искомого компонента можно позволять Delphi выполнить эту работу с помощью метода FindComponent формы. Этот метод для поиска совпадающего имени просто просматривает массив Components. Под- робности см. в разделе «Свойство Name». Смена владельца Вы видели, что почти каждый компонент имеет владельца. Когда компонент со- здан на этапе разработки (или из результирующего DFM-файла), его владельцем неизменно будет его форма. При создании компонента во время выполнения вла- делец указывается конструктору Create в качестве параметра. Owner — это свойство, предназначенное только для чтения, поэтому изменить его нельзя. Владелец установлен в ходе создания и вообще не должен изменяться 0160
161 Класс во время жизни компонента. Чтобы понять, почему не стоит изменять владельца компонента во время дизайна и не стоит свободно изменять его имя, прочитайте последующее обсуждение. Хочу заранее предупредить, что рассматриваемая тема не проста; если вы только начинаете знакомиться с Delphi, то лучше вернуться к этому разделу позже. Для изменения владельца компонента можно вызывать методы Insertcomponent и RemoveComponent самого владельца, передав текущий компонент в качестве па- раметра. Однако эти методы нельзя применить непосредственно в обработчике события формы, как я пытаюсь сделать здесь: procedure TForml.ButtonlClick(Sender: TObject): begin RemoveComponent (Buttonl): Form2.Insertcomponent (Buttonl): end: Выполнение этого фрагмента приводит к нарушению доступа к памяти, посколь- ку при вызове RemoveComponent Delphi отключает данный компонент от поля фор- мы (Buttonl) и устанавливает его в nil. (Более подробно о полях формы я расскажу в разделе «Удаление полей форм».) Решением проблемы будет написание следу- ющей процедуры: procedure ChangeOwner (Component. NewOwner TComponent): begin Component.Owner.RemoveComponent (Component): NewOwner.Insertcomponent (Component): end: Этот метод (который позаимствован из примера ChangeOwner) производит сме- ну владельца компонента. Он вызывается совместно с более простым кодом, ис- пользуемым для изменения предка компонента; совместно две команды полностью перемещают кнопку на другую форму, меняя ее владельца: procedure TForml.ButtonChangeClickCSender: TObject): begin if Assigned (Buttonl) then begin // сменить предка Buttonl Parent := Form2: // сменить владельца ChangeOwner (Buttonl. Form2). end. end; Этот метод проверяет, ссылается ли поле Buttonl на элемент управления, по- скольку в ходе перемещения компонента Delphi должна была установить Buttonl в nil. Вот результат его выполнения. Для того чтобы продемонстрировать фактическое изменение владельца компо- нента Buttonl, к обеим формам добавлена другая функция. Кнопка List заполняет список именами компонентов, которыми владеет каждая форма. Щелкните этими Двумя кнопками до и после перемещения компонента и вы увидите, что происхо- дит. И в качестве заключительной особенности программы, компонент Buttonl имеет простой обработчик своего события OnClick, отображающий надпись владель- ца формы: 0161
162 Глава 4. Классы базо Рис. 4.3. Щелчок на кнопке Change в примере Changeowner перемещает компонент Button 1 на вторую форму procedure TForml.ButtonlClick(Sender: TObject); begin ShowMessage ('My owner is ‘ + ((Sender as TButton).Owner as TForm).Caption): end. Свойство Name В Delphi каждый компонент должен иметь имя. Имя должно быть уникальным в пределах владельца компонента, которым обычно является форма, на которой вы разместили этот компонент. Это означает, что приложение может иметь две различные формы, на каждой из которых могут быть 2 компонента с одинаковым именем, хотя желательно избегать подобной практики. Лучше поддерживать уни- кальность имен компонентов в масштабе приложения. Установка соответствующего значения свойства Name очень важна: если оно слишком длинное, то это затруднит его использование (много вводить); если оно слишком короткое, то можно перепутать различные объекты. Как правило, имя компонента имеет префикс, соответствующий типу компонента; это делает код более простым для восприятия и позволяет Delphi группировать компоненты в списке инспектора объектов (Object Inspector), где они сортируются по имени. Со свойством Name компонента связаны три важных элемента: О во время разработки значение свойства Name используется для определения имени поля формы в объявлении класса формы. Это имя, которое обычно ис- пользуется в программном коде для обращения к объекту. Ввиду этого значе- ние свойства Name должно быть допустимым идентификатором языка Delphi (оно не должно иметь никаких пробелов и должно начинаться с буквы, а не с числа); О если установить свойство Name элемента управления до изменения его свой- ства Caption или свойства Text, то новое имя зачастую автоматически копирует- ся в заголовок этого элемента, т. е. если имя и заголовок идентичны, то измене- ние имени также приведет к изменению заголовка; 0162
Класс TCompjMIWH WOOarQl > «МЛ1 . ... 163 О Delphi использует имя компонента при создании заданных по умолчанию имен методов, связанных с его событиями. Если вы имеете компонент Buttonl, то по умолчанию его обработчик события OnClick будет называться ButtonlClick, если не будет определено другое имя. При изменении имени компонента Delphi со- ответствующим образом изменит имена связанных методов. Например, при изменении имени кнопки на MyButton, метод ButtonlClick автоматически стано- вится MyButtonClick. Как упоминалось ранее, если известно имя компонента, то можно получить его экземпляр, вызвав метод FindComponent его владельца. Этот метод возвращает nil, если компонент не найден. Например, можно написать: var Comp: TComponent: begin Comp := FindComponent (.'Buttonl'): if Assigned (Comp) then with Comp as TButton do // дальнейшие действия... СОВЕТ--------------------------------------------------------------------------------- В Delphi имеется функция FindGlobalComponent, которая находит вышестоящий компонент (форму или модуль данных), имеющий указанное имя. FindGlobalComponent вызывает одну или более уста- новленных функций, поэтому теоретически вы можете изменять путь работы этой функции. Однако поскольку FindGlobalComponent используется системой поточной передачи, я настоятельно реко- мендую не устанавливать собственных функций замены. Если вы хотите настроить путь поиска компонентов в других контейнерах, просто напишите новую функцию со своим именем. Удаление полей формы Каждый раз при добавлении компонента в форму Delphi, наряду с некоторыми из его свойств, добавляет в DFM-файл связанный с ним пункт. В Pascal-файл Delphi добавляет соответствующее поле в объявление класса формы. Это поле формы — ссылка на соответствующий объект, как и любая переменная типа класса. После того как форма создана, Delphi загружает DFM-файл и использует его для воссоз- дания всех компонентов и устанавливает их свойства в соответствии со значения- ми, заданными в ходе разработки (и сохраненным непосредственно в DFM-фай- ле). Далее среда Delphi подключает новый объект к полю формы, соответствующему его свойству Name. Вот почему в программном коде можно использовать поле фор- мы для работы с соответствующим компонентом. Благодаря этому допустимым является иметь компонент без имени. Если при- ложение не будет манипулировать компонентом или изменять его во время вы- полнения, то можно удалить имя компонента из инспектора объектов. К примерам можно отнести статическую «надпись» с фиксированным текстом или пункт меню, или даже, что более наглядно, разделители пунктов меню. Гашением имени вы Удаляете соответствующий пункт из объявления класса формы. Это уменьшает размер объекта формы (только на четыре байта — размер объектной ссылки) и уменьшает DFM-файл, в который не включается бесполезная строка (имя ком- понента). Уменьшение размера DFM-файла также предполагает сокращение раз- мера конечного исполняемого файла, хотя лишь ненамного. 0163
164 Глава 4. Классь ВНИМАНИЕ ------------------------------------------------------------------------- Если вы удаляете имена компонентов, то убедитесь, что остался, по крайней мере, один именован- ный компонент каждого класса, для того чтобы «разумный» компоновщик и поточная система смог- ли связать требуемый код класса и распознали его в DFM-файле. Например, если удалить из формы все поля, ссылающиеся на компоненты TLabel, то при загрузке системой формы во время выполне- ния она не сможет создать объект неизвестного класса и выдаст ошибку, указывающую, что класс недоступен. Как вы увидите в следующем разделе, во избежание этой ошибки можно вызывать процедуры Registerclass или Registerclasses. Вы можете также оставить имя компонента и вручную удалить соответствую- щее поле класса формы. Даже если компонент не имеет соответствующего поля формы, оно создается в любом случае, хотя его использование (например, с по- мощью метода FindComponent) будет несколько сложней. Сокрытие полей форм Многие борцы за чистоту ООП жалуются, что Delphi не в полной мере соответ- ствует правилу инкапсуляции, поскольку все компоненты формы отображаются на public-поля и становятся доступными из других форм и модулей. Поля компо- нентов перечислены в первом, безымянном разделе определения класса, который по умолчанию имеет видимость published. Однако Delphi поступает так только по умолчанию для ускорения изучения визуальной среды программирования начи- нающими. Программист может воспользоваться другим подходом, и для опериро- вания формами использовать свои свойства и методы. Тем не мене существует риск, который заключается в том, что другой программист из этой же команды может по невнимательности не заметить этого подхода и напрямую обращаться к компо- нентам, если они остались в опубликованном разделе. Решение, которое известно не всем программистам, заключается в перемещении компонентов в private-раздел объявления класса. В качестве примера я создал простую форму со строкой редактирования, кноп- кой и списком. Когда в строке редактирования имеется текст, пользователь щел- кает на кнопке и этот текст добавляется в список. Когда строка пуста, кнопка от- ключена. Вот программный код примера HideComp. procedure TForml.ButtonlClick(Sender: TObject); begin ListBoxl.Items.Add (Editl.Text); end: procedure TForml.EditlChange(Sender: TObject): begin Buttonl.Enabled : = Length (Editl.Text) <> 0: end: Я перечислил эти методы только для того, чтобы показать, что, определяя взаи- модействие компонентов в коде формы, вы обычно ссылаетесь на доступные ком- поненты. Поэтому кажется невозможным избавиться от полей, соответствующих этим компонентам. Однако их можно спрятать, переместив из опубликованного по умолчанию раздела в раздел частных определений класса формы: TForml = class(TForm) procedure ButtonlClick(Sender: TObject): procedure EditlChange(Sender: TObject); 0164
Класс TComponent £83 procedure FormCreateCSender: TObject); ' • private Buttonl: TButton; Editl: TEdit; LiStBoxl; TListBox; end; Теперь при запуске программы у вас возникнет проблема: форма будет загру- жена, но поскольку частные поля не инициализированы, события будут использо- вать объектные ссылки nil. Опубликованные поля формы Delphi обычно инициа- лизирует, используя компоненты, созданные на основе DFM-файла. А что если вы сделаете это самостоятельно вот так: procedure TForml.FormCreate(Sender: TObject); begin Buttonl := FindComponent (’Buttonl') as TButton: Editl := FindComponent ('Editl') as TEdit: LiStBoxl := FindComponent (.'LiStBoxl') as TListBox; end; Это почти сработает, но сгенерирует системную ошибку, подобную рассмот- ренной в предыдущем разделе. На этот раз частные определения приведут к тому, что компоновщик свяжет реализации этих классов; проблема в том, что поточная система передачи должна знать имена классов для того, чтобы определять место- нахождение ссылки класса, требуемой для конструирования компонентов в ходе загрузки DFM-файла. Последним мазком является регистрация кода, позволяющая сообщить Delphi в ходе выполнения о существовании классов компонентов, которые вы собирае- тесь использовать. Это необходимо сделать перед тем, как будет создана форма, поэтому этот программный код я обычно помещаю в раздел инициализации модуля: initialization Registerclasses ([TButton, TEdit, TListBox]); Вопрос в том, стоит ли это затраченных усилий? Вы получаете большую сте- пень инкапсуляции, защищающей компоненты формы от других форм (и разраба- тывающих их программистов). Повтор этих шагов для каждой формы может стать трудоемкой задачей, поэтому я разработал мастер, генерирующий код «на лету». Этот мастер далек от совершенства, поскольку не может автоматически обрабаты- вать изменения, но он достаточно полезен. Подробности о получении этого мастера см. в приложении А. Для крупных проектов, создаваемых в соответствии с прин- ципами объектно-ориентированного программирования, я бы рекомендовал при- держиваться этой или подобной технологии. Настраиваемое свойство Тад Свойство Тад является странным, поскольку оно не оказывает никакого влияния. Оно является лишь дополнительным местом в памяти, представленным для каж- дого класса компонентов, в котором хранятся пользовательские значения. Тип хранимой информации и способ их использования полностью определяется вами. Зачастую полезно иметь дополнительное место в памяти для подсоединения к компоненту сведений без необходимости их предварительного определения в классе компонента. Технически свойство Тад является длинным целым (long inte- 0165
166 Гл—4. Классы базовой библиотеки ger), так что вы можете хранить в нем, например, входной номер массива или спис- ка, соответствующего объекту. Используя приведение типов, в свойстве Тад можно хранить указатель, объектную ссылку или еще что-нибудь, имеющее размер 4 бай- та. Программист с помощью ярлыка компонента может связать с ним практически все. Использование этого свойства вы увидите в ряде примеров, рассматриваемых в последующих главах. События Теперь, когда мы рассмотрели класс TComponent, я должен представить еще один элемент Delphi. Компоненты Delphi программируются с помощью РМЕ: свойств (properties), методов (methods) и событий (events). Методы и свойства должны быть уже вам понятны, но мы еще не рассматривали события. Причина в том, что события не воспринимаются как новая особенность языка, это просто стандартная технология написания текста программы. Технически, событие — это свойство. Единственное различие в том, что оно относится к методу (точнее, к типу указате- ля метода), а не к другим типам данных. События в Delphi Когда пользователь выполняет с компонентом какие-либо действия, например, щелкает на нем, то компонент генерирует событие. В ответ на запрос метода или изменение одного из свойств данного компонента (или даже смену компонента) система генерирует другие события. Например, если установить фокус на компо- ненте, то тот компонент, который в настоящий момент имел фокус, теряет его, вызывая соответствующее событие. Технически, большинство событий Delphi запускается при получении соответ- ствующего сообщения операционной системы, хотя события не имеют точного соответствия сообщениям. События Delphi стремятся быть на более высоким уров- не, чем сообщения операционной системы, а кроме того, Delphi имеет множество дополнительных межкомпонентных сообщений. С теоретической точки зрения, событие — это результат запроса, посланного компоненту, или элемент управления, который может ответить на это сообщение. В соответствии с этим подходом, для того чтобы обработать событие «щелчок кноп- ки», вы должны были бы создать подкласс класса TButton и добавлять в новый класс код нового обработчика события. С0ВЕТ------------------------------------------------------------ Как вы увидите, прочитав следующий раздел, события Delphi основаны на указателях на методы. Это совершенно отличается от языка Java, в котором используются классы-«слушатели» с метода- ми для семейства событий. Эти методы и вызывают обработчики события. C# и .NET используют подобную идею делегирования классов, рассматриваемую в главе 24. Обратите внимание, что по- нятие «делегировать» имеет значение, традиционно используемое в литературе по Delphi для объяс- нения идеи обработчиков события. На практике создание нового класса для каждого компонента будет слишком сложным, чтобы считать это разумным решением. В Delphi обработчик события 0166
События компонента — это метод формы, содержащей компонент, а не самого компонента. Другими словами, компонент полагается на своего владельца, форму. Эта методи- ка называется делегированием и является фундаментальной для модели Delphi. Таким образом, вы не должны изменять класс TButton, если только не хотите опре- делить новый тип компонента; для того чтобы изменить поведение кнопки, можно всего лишь настроить его владельца. Указатели методов События опираются на характерную особенность языка Delphi: указатели метода (methodpointers). Тип «указатель метода» подобен процедурному типу, но являет- ся типом, указывающим на метод. Технически тип «указатель метода» — проце- дурный тип, имеющий явный параметр Self. Иначе говоря, переменная процедур- ного типа, хранящая адрес вызова функции, предоставляемая им, имеет данный набор параметров. Переменная типа «указатель метода» хранит два адреса: адрес кода метода и адрес экземпляра объекта (данные). При вызове кода метода с по- мощью указателя метода адрес экземпляра объекта представлен как Self внутри тела метода. СОВЕТ ------------------------------------------------------------------------- Это объясняет общий тип Delphi, TMethod — запись с полями Code и Data. Объявление типа «указатель метода» подобно объявлению процедурного типа, за исключением того, что в конце определения используется ключевое слово object: type IntProceduralType = procedure (Num. Integer); IntMethodPointerType = procedure (Num: Integer) of object: После того как объявлен этот тип, можно объявлять переменную этого типа и присваивать ей совместимый метод (метод, который имеет ту же самую сигнату- ру1 (параметры, возвращаемый тип, соглашение вызова)) другого объекта. При создании обработчика события кнопки OnClick Delphi поступает именно так. Кнопка имеет свойство типа «указатель метода», названное OnClick, и вы мо- жете явно или косвенно присвоить ему метод другого объекта, например, формы. Когда пользователь щелкает на кнопке, выполняется этот метод, даже если он оп- ределен внутри другого класса. Далее следует скелет кода, который используется Delphi для определения об- работчика события компонента «кнопка» и связанного с ним метода формы: type TNotifyEvent = procedure (Sender: TObject) of object: MyButton = class OnClick- TNotifyEvent: end: TForml = class (TForm) procedure ButtonlClick (Sender: TObject): Типовая часть спецификации элемента определения класса; включает тип результата для атрибута и функции, для процедур включает также число и типы их аргументов. 0167
16S Buttonl: MyButton; end; var Forml: TForml; Теперь внутри процедуры можно написать: MyButton.OnClick := Forml.ButtonlClick; Единственное реальное отличие между этим фрагментом кода и кодом VCL заключается в том, что OnClick является именем свойства, а данные, к которым он обращается, — FOnClick. Событие, которое представлено на странице Events инспек- тора объектов, не что иное, как свойство типа «указатель метода». Это означает, что во время разработки вы можете динамически изменять обработчик события, присоединенный к компоненту, или даже создать во время выполнения новый ком- понент и назначать ему обработчик события. События — это свойства Я уже говорил, что события — это свойства. Для обработки событий компонента на соответствующее свойство события назначается метод. При двойном щелчке на значении события в инспекторе объектов в форму-владельца будет добавлен новый метод, и он будет присвоен к соответствующему свойству события компонента. Допускается, что во время выполнения несколько событий могут совместно использовать один и тот же обработчик или изменять его. Для реализации этой возможности не требуется больших знаний языка. Фактически, выбрав событие в инспекторе объекта, можно щелкнуть на кнопке со стрелкой справа от имени собы- тия и в раскрывающемся списке просмотреть совместимые методы (методы, имею- щие ту же самую сигнатуру типа указателя метода). С помощью инспектора объек- тов можно просто выбрать тот же метод для того же самого события различных компонентов или для различных совместимых событий того же самого компонента. Также, как вы добавляли некоторые свойства к классу TDate в главе 2, можно добавить и еще одно событие. Событие будет очень простое. Оно будет называться OnChange и может использоваться для предупреждения пользователя компонента об изменении значения даты. Для определения события просто определите свой- ство, соответствующее ему, и добавьте некоторые данные для хранения указателя метода, на который ссылается событие. Эти новые определения, добавляемые в класс, можно найти в примере Date Evt: type TDate = class private FOnChange; TNotifyEvent; protected procedure DoChange; dynamic; public property OnChange.- TNotifyEvent read FOnChange write FOnChange; end; 0168
Событи , . 11>9 Определение свойств очень простое. Пользователь этого класса может присво- ить новое значение этому свойству и, следовательно, частному полю FOnChange. Класс не присваивает значение полю FOnChange; это делает пользователь компо- нента. При изменении значения даты класс TDate просто вызывает метод, храни- мый в поле FOnChange. Конечно, вызов имеет место, только если свойство события присвоено. Метод DoChange (объявленный как динамический метод, является тради- ционным методом с запуском от события) выполняет проверку и вызывает метод: procedure TDate.DoChange; begin if Assigned (FOnChange) then FOnChange (Self); end; Метод DoChange в свою очередь вызывается каждый раз при изменении одного из значений: procedure TDate.SetValue (у, m, d: Integer); begin fDate := EncodeDate (y. m. d); // запуск события DoChange; Если просмотреть программу, использующую данный класс, то вы сможете зна- чительно упростить его код. Во-первых, добавьте в класс формы пользовательский метод: type TDateForm = class(TForm) procedure DateChangeCSender; TObject): Программный код этого метода просто обновляет элемент «надпись» текущим значением свойства Text объекта TDate: procedure TDateForm.DateChange; begin LabelDate.Caption : = TheDay Text; end; Этот обработчик события позже устанавливается в метод FormCreate: procedure TDateForm.FormCreateCSender: TObject); begin TheDay := TDate.Init (2003. 7. 4); LabelDate.Caption : = TheDay.Text; // присвоение обработчика события для будущих изменений TheDay.OnChange ;= DateChange; end; Как видно, требуется приложить довольно много усилий. Я сказал неправду, говоря, что обработчик события избавит вас от дополнительной работы? Нет. Те- перь, после того как мы добавили некоторый программный код, можно забыть об обновлении элемента «надпись» при изменении данных объекта. Например, вот обработчик события OnClick одной из кнопок: Procedure TDateForm.BtnIncreaseClick(Sender; TObject): begin TheDay.Increase; end; 0169
170 Тот же упрощенный код присутствует во многих других обработчиках собы- тий. После того как установлен обработчик события, вам нет нужды постоянно помнить о необходимости обновления надписи. Это устраняет существенный по- тенциальный источник ошибок программы. Обратите также внимание, что внача- ле вы должны были написать некоторый код, поскольку это не компонент, уста- новленный в Delphi, а просто класс. Для компонента вы выбираете обработчик события в окне инспектора объекта и пишете одну строчку программного кода для обновления надписи, и — все. СОВЕТ-------------------------------------------------------------------- Это лишь краткое введение в определение событий. Основное понимание этих особенностей важно для каждого программиста Delphi. Если ваша цель состоит в том, чтобы создавать новые компонен- ты с набором сложных событий, то дополнительную информацию по всем этим вопросам вы найде- те вы главе 9. Списки и контейнеры классов Довольно часто возникает необходимость обработки группы компонентов или объектов. Помимо использования стандартных и динамических массивов, некото- рые VCL-классы представляют списки других объектов. Эти классы можно разде- лить на три группы: простые списки, коллекции и контейнеры. Списки и списки строк Списки представляются общим списком объектов TList и двумя списками строк TStrings и TstringList: О TList определяет список указателей, который может использоваться для хране- ния объектов любого класса. TList более удобен, чем динамический массив, по- скольку его можно автоматически расширить, добавив в него новые пункты. Преимущество динамического массива заключается в том, что он позволяет указывать тип помещаемых объектов и выполнять проверку типов во время компиляции; О TStrings — это абстрактный класс, предназначенный для представления всех видов списков строк независимо от реализации их хранения. Этот класс опре- деляет абстрактный список строк. Поэтому объекты TStrings используются толь- ко как свойство компонентов, допускающих хранение самих строк, например, элемент «список»; О TStringList — это подкласс TStrings, определяющий список строк с собственным хранилищем. Этот класс может использоваться для определения списка строк в программе. Объекты TStringList и Tstrings имеют как список строк, так и список объектов, связанных с этими строками. Эти классы имеют несколько различных примене- ний. Например, они могут использоваться либо как словари связанных объектов, либо для хранения битовых изображений или других элементов, помещаемых в элемент «список». 0170
171 Для хранения или загрузки содержания в или из текстового файла два класса списков строк имеют готовые к использованию методы: SaveToFile и LoadFromFile. Для организации цикла списка можно использовать простое выражение for, ис- пользующее индекс списка, как будто это не список, а массив. Все эти списки имеют множество методов и свойств. Как для чтения, так и для изменения элементов можно использовать нотацию, используемую в массивах ([ и ]). Существует свойство Count, а также типичные методы доступа, например Add, Insert, Delete, Remove; методы поиска (например, IndexOf); и поддержка сортировки. Класс TList имеет метод Assign, который помимо копирования исходных данных мо- жет исполнять ряд операций в отношении двух списков, включая and, or и хог. Для заполнения списка строк пунктами и последующей проверки наличия од- ного из них можно написать следующий программный код: var si: TStringList; idx: Integer: begin si := TStnngList.Create; try si .Add ('one")-. si.Add ('two'); si.Add ('three'); // позже idx .= si.IndexOf ('two'); if idx >= 0 then ShowMessage (.'String found’); finally si.Free: end: end; Пары «имя-значение» Класс TStnngList всегда имел другую прекрасную черту: поддержку пар «имя-зна- чение». При добавлении в список строки, подобной «lastname=john», в дальней- шем поиск наличия этой пары можно осуществлять с помощью функции Index- OfName или свойства-массива Values. Например, значение «john» можно извлечь вызовом Values ['lastname']. Эту особенность можно использовать для создания более сложных структур Данных, например словарей, а также получить выгоду от возможности присоеди- нения объектов к списку. Эта структура данных отображается непосредственно на файлы инициализации и другие общие форматы. В Delphi 7 и дальше расширена поддержка пар «имя-значение» посредством предоставления пользователю возможности настройки сепаратора, содержащего теперь более одного символа (с помощью свойства NameValueSeparator). Кроме того, новое свойство ValueFromlndex предоставляет непосредственный доступ к значе- нию части строки, начиная с указанной позиции; нет больше необходимости «вруч- ную» извлекать значение имени из всей строки, используя громоздкое (и очень медленное) выражение: str ;= MyStnngList.Values [MyStringList.Names [I]]: // старое str := MyStringList.ValueFromlndex [I]; // новое 0171
172 Использование списков объектов Я написал пример, посвященный использованию общего класса TList. Когда требу- ется список данных любого типа, вы обычно объявляете объект TList, заполняете его данными, а затем обращаетесь к ним, приводя их к требуемому типу. Пример ListDemo демонстрирует этот подход и его «проколы». Форма содержит частные переменные, содержащие список дат: private ListDate: TList: Этот объект-список создается при создании самой формы: procedure TForml.FormCreate(Sender. TObject): begin Randomize: ListDate := TList Create: end. Кнопка на форме добавляет случайную дату в список (конечно же я включил в проект модуль, содержащий компонент date, созданный в предыдущей главе): procedure TForml.ButtonAddClick(Sender: TObject); begin ListDate.Add (TDate Create (1900 + Random (200). 1 + Random (12). 1 + Random (30))). end; При извлечении пунктов из списка необходимо выполнить обратное приведе- ние к соответствующему типу с помощью представленного ниже метода, связан- ного с кнопкой List (результат работы программы представлен на рис. 4.4): procedure TForml.ButtonListDateClick(Sender• TObject): var I: Integer; begin ListBoxl.Cl ear: for I •= 0 to ListDate Count - 1 do Listboxl.Items Add ((TObject(L1stDate [I]) as TDate).Text); end. В конце этого кода, перед тем как выполнить приведение as, сначала необходи- мо выполнить жесткое приведение указателя, возвращающего TList в ссылку TObject. Такой тип выражения может привести к исключению «неверного приведения типа» или вызвать ошибку памяти, если этот указатель не является ссылкой на объект. ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Если в списке были бы не объекты-даты, извлечение их со статическим приведением вместо приве- дения as будет более эффективно. Однако когда есть даже небольшой риск наличия неверного объекта, я предложил бы использовать приведение as. Для демонстрации того, что в действительности все может пойти не так как надо, я добавил еще одну кнопку, которая, вызывая ListDate.Add(Sender), добавляет в список объект TButton. При щелчке на этой кнопке и последующем обновлении одного из списков вы получите ошибку. И не забывайте, что при уничтожении объекта-списка сначала необходимо уничтожить все объекты этого списка. Про- грамма ListDemo делает это с помощью метода формы FormDestroy: procedure TForml FormDestroy(Sender TObject): 0172
173 I: Integer: begin for I := 0 to Li stDate.Count - 1 do TObjectdistOate [I]).Free; ListDate.Free: end: 25/02/2033 23/03/1986 02/04/2053 22/05/1917 12/02/2031 19/10/1937 19/08/1985 26/06/1909 22/01/1947 17/06/1985 Рис. 4.4. Список дат, выводимый примером ListDemo Коллекции Вторая группа, коллекции, содержит только два VCL-класса: TCollection и TCollec- tionltem. Класс TCollection определяет однородный список объектов, владельцем которого является класс-коллекция. Объекты коллекции должны быть потомка- ми класса TCollection. Если требуется, чтобы коллекция содержала определенные объекты, необходимо создать как подкласс TCollection, так и соответствующий под- класс TCollectionltem. Коллекции полезны для указания значений свойств компонента и совершенно не подходят для хранения собственных объектов. Более подробно коллекции бу- дут рассмотрены в главе 9. Классы-контейнеры В последних версиях Delphi добавлены ряд классов-контейнеров, определенных в модуле Contnrs. Классы-контейнеры расширяют класс TList путем добавления идеи владения (определения принадлежности), а также определением специальных правил извлечения (имитация стека и очереди) и возможностей сортировки. Основное отличие между TList и новым классом TObjectList в том, что последний определен как список объектов TObject, а не список указателей. Однако, что даже более важно, если свойство списка объектов OwnsObjects установить в True, оно ав- томатически удалит объект, когда он будет замещен другим объектом, и удалит каждый объект, когда сам список будет уничтожен. Вот список классов-контейне- ров: О класс TObjectList (уже рассмотренный) представляет собой список объектов, ппиияллржаших в конечном счете, самому списку; 0173
174 ' / О унаследованный класс TComponentList представляет собой список компонентов с полной поддержкой уведомления уничтожения (важная функция безопасно- сти, когда два компонента связаны с помощью свойств; т. е. когда один компо- нент является значением свойства другого компонента); О класс TCLassList — это список ссылок классов. Он унаследован OTTList и не требу- ет специального уничтожения, поскольку в Delphi нет необходимости уничто- жить ссылки классов; О классы TStack и TObjectStack представляют собой списки указателей и объектов, из которых извлекать элементы можно, только начиная с последнего вставлен- ного. Стек реализует правило LIFO (last in, first out - последний пришел — первый вышел). Типичными методами стека являются Push (для вставки), Pop (для извлечения) и Реек (для предварительного просмотра первого пункта без его уда- ления). Кроме того можно использовать все методы базового класса — TList; О классы TOueue и TObjectQueue представляют собой списки указателей и объек- тов, из которых всегда удаляется первый из вставленных пунктов (FIFO: first in, first out — первый вошел, первый вышел). Методы этих классов те же самые, что и у «стековых» классов, но они ведут себя по-другому. ВНИМАНИЕ ------------------------------------------------------------------ В отличие от TObjectList, классы TObjectStack и TObjectQueue не являются владельцами объектов и не смогут удалить эти объекты при собственном уничтожении. Можно просто выполнить метод Pop в отношении всех пунктов и уничтожить их после того, как вы закончите их использовать, а затем уничтожить сам контейнер. Для демонстрации использования этих классов я модифицировал упомянутый ранее пример ListDate в примере Contain. Изменен на TObjectList тип переменной ListDate. В методе FormCreate создание списка теперь производится следующим про- граммным кодом (активизирующим принадлежность списка): ListDate TObjectList.Create (True): Здесь можно упростить код деструктора, поскольку применение к списку мето- да Free автоматически освободит содержащиеся в нем данные. Кроме того, в программу добавлены объекты «стек» и «очередь», заполненные числами. Одна из двух кнопок формы выводит список чисел в каждом контейнере, а другая удаляет последний пункт (выводимый в окне сообщения (message box)): procedure TForml.btnQueueClick(Sender. TObject); var I: Integer, begin ListBoxl.Clear. for I := 0 to Stack.Count - 1 do begin ListBoxl.Items.Add (IntToStr (Integer (Queue.Peek))): Queue.Push(Queue.Pop); end: ShowMessage (.'Removed: ’ + IntToStr (Integer (Stack.Pop))): end. При нажатии двух кнопок можно увидеть, что вызов Pop для каждого контейне- ра возвращает последний пункт. Разница заключается в том, что класс TQueue (оче- редь) вставляет элементы в начало, a TStack (стек) — в конец. 0174
Списки и контейнеры классов 175 —<-----------------------------------------------emiuMuiweBBW'iu \ Хэшированные ассоциативные списки Начиная с Delphi 6, в набор предопределенных классов-контейнеров входят TBucket- List и TObjectBucketList. Эти два списка являются ассоциативными. Это значит, что они имеют ключ и действительный элемент. Ключ используется для идентифика- ции и поиска пунктов. Для добавления пункта вызывается метод Add с двумя пара- метрами: ключ и данные. При использовании метода Find вы указываете ключ и получаете данные. Того же эффекта можно добиться использованием свойства- массива Data, передавая ключ в качестве параметра. Эти списки основаны на хэш-системе (системе равномерного распределения). Списки создают внутренние массивы пунктов, называемых сегментами, каждый из которых имеет подсписок элементов списка. В ходе добавления пункта его зна- чение ключа используется для вычисления хэш-значения, которое определяет сег- мент, в который будет добавлен пункт. При поиске пункта снова вычисляется хэш- функция и список немедленно осуществляет захват подсписка, содержащего пункт, в котором и производится окончательный поиск. Эти действия выполняются в ин- тересах ускорения вставки и поиска, но только в том случае, если хэш-алгоритм обеспечивает равномерное распределение пунктов по различным сегментам и су- ществует достаточное количество различных элементов массива. При помещении множества элементов в один сегмент поиск замедляется, поэтому при создании TObjectBucketList с помощью параметра конструктора можно указать количество элементов списка, выбрав значение в диапазоне от 2 до 256. СОВЕТ------------------------------------------------------------------- Я не считаю этот алгоритм хэш-системы достаточно убедительным, но замена его собственным предполагает лишь подмену виртуальной функции BucketFor и, в конечном итоге, изменению числа элементов массива посредством установки иного значения свойства Bucketcount. Еще одной интересной особенностью, отсутствующей у списков, является ме- тод ForEach, позволяющий выполнять определенную функцию в отношении каж- дого пункта, содержащегося в списке. Вы передаете методу ForEach указатель на данные и процедуру, которая получает четыре параметра: пользовательский ука- затель, ключ с объектом списка и параметр логического типа, который можно ус- тановить в False для остановки выполнения. Вот эти две сигнатуры: type TBucketProc = procedure(AInfo. Altem. AData: Pointer; out AContinue Boolean); function TCustomBucketList ForEach(AProc: TBucketProc; AInfo- Pointer) Boolean; СОВЕТ-----------------------------------------------------------------------------—----- Помимо этих контейнеров Delphi имеет класс THashedStringList, который происходит от TStringList. Этот класс не имеет никакого прямого отношения к хэш-спискам и определен в другом модуле, IniFiles. Хэшированный список строк имеет две связанные хэш-таблицы (типа TStringHash), которые полностью обновляются каждый раз при изменении содержания списка строки. Поэтому данный класс полезен только для чтения большого набора фиксированных строк, а не для обработки часто изменяющегося списка строк. С другой стороны, вспомогательный класс TStringHash, вероятно, будет весьма полезен в общих случаях, и он имеет хороший алгоритм для вычисления хэш-значе- ния строки. 0175
176 Контейнеры и списки, обеспечивающие сохранность типа Контейнеры и списки имеют одну проблему: они не сохраняют тип, что показано в обоих примерах в виде добавления объекта-кнопки в список дат. Чтобы гаранти- ровать однородность данных в списке, можно проверять тип извлекаемых данных до того, как вы их вставите, но в качестве дополнительной меры также можно прове- рять тип данных после их извлечения. Однако введение проверки, осуществляе- мой в ходе выполнения, приводит к замедлению работы программы и является опасным: программист в некоторых случаях может пропустить проверку. Для решения этих проблем можно создать специальный класс списков для кон- кретных типов данных и приспособить программный код из существующих клас- сов TList или TObjectList (либо других классов-контейнеров). Вот два способа реа- лизации этого подхода: О произведите новый класс от класса-списка и настройте метод Add и методы до- ступа, относящиеся к свойству Items. Это подход, используемый компанией Borland для всех классов-контейнеров, производных от TList; СОВЕТ-------------------------------------------------------------- Классы-контейнеры Delphi для удобства работы с типами (параметры и результаты функций жела- емого типа) используют статические подмены. Статические подмены (static overrides) не относятся к полиморфизму; кто-либо, использующий класс-контейнер посредством переменной типа TList, не будет вызывать специализированные функции контейнера. Статическая подмена — простая и эф- фективная методика, но она имеет одно очень важное ограничение: методы потомков не должны ничего делать кроме простого приведения типов, поскольку нет никакой гарантии, что методы потомков будут вызываться. К списку можно было бы обращаться и управлять им с помощью мето- дов предка так же, как и с помощью методов потомка, поэтому выполняемые ими операции должны быть идентичны. Единственное различие — это тип, используемый в методах-потомках, который позволяет избежать дополнительного приведения типов. О создать совершенно новый класс, содержащий объект TList, и используя соот- ветствующую проверку типов, отобразить методы нового класса на внутрен- ний список. Такой подход определяет класс-оболочку, класс, который «охва- тывает» существующий класс в интересах обеспечения иного и ограниченного доступа к его методам (в нашем случае для выполнения преобразования ти- пов). Оба подхода реализованы в примере Date List, который определяет списки объек- тов TDate. В представленном ниже фрагменте вы встретите объявление двух клас- сов — основанный на наследовании класс TDateListI и класс-оболочку TDateListW: type //на основе наследования TDateListI = class (TObjectList) protected procedure SetObject (Index: Integer: Item: TDate); function GetObject (Index: Integer)- TDate: public function Add (Obj- TDate) Integer: procedure Insert (Index Integer: Obj- TDate): property Objects [Index- Integer]- TDate read GetObject write SetObject; default: 0176
.......................m e\i; // А использованием оболочки TDateListW = class(TObject) private Flist: TObjectList; fuhction GetObject (Index; Integer): TDate; procedure Setobject (Index; Integer; Obj: TDate); function GetCount: Integer; public constructor Create; destructor Destroy, override. function Add (Obj• TDate): Integer; function Remove (Obj TDate): Integer; function IndexOf (Obj: TDate)- Integer. property Count: Integer read GetCount. property Objects [Index: Integer]: TDate read GetObject write Setobject, default: end: Очевидно, что первый класс более прост в написании, он имеет меньше мето- дов и они лишь вызывают наследуемые методы. Удобство заключается в том, что объект TDateListI может передаваться в параметры, ожидающие тип TList. Пробле- ма состоит в том, что программный код, манипулирующий экземпляром этого спис- ка посредством общей переменной TList, не будет вызывать специализированные методы, поскольку они не виртуальные и могут привести к добавлению в объекты- списки других типов данных. Вместо этого, если вы решили не использовать наследование, то вы приходите к необходимости написания большого объема программного кода; необходимо вос- произвести каждый из исходных методов TList, просто вызывая методы внутрен- него объекта FList. Недостаток заключается в том, что класс TDateListW несовмес- тим по типу с TList, что ограничивает его полезность. Он не может передаваться в качестве параметра в методы, ожидающие тип TList. Оба из этих подходов обеспечивают хорошую проверку соблюдения типов. После создания экземпляра одного из этих классов-списков в него можно добав- лять только объекты соответствующего типа, а извлекаемые объекты будут, есте- ственно, иметь верный тип. Эта методика демонстрируется примером DateList. Эта программа имеет несколько кнопок, поле со списком, позволяющее пользователю выбирать, который из списков вывести, и элемент управления «список» для пред- ставления значений списка. Программа пополняет списки, пробуя добавить эле- мент управления «кнопка» в список объектов TDate. Для добавления объекта ино- го типа в список TDateListI можно конвертировать список к его базовому классу — TList. Это может произойти случайно, если вы передадите список как параметр в метод, который ожидает класс предка класса-списка. И наоборот, для того чтобы список TDateListW потерпел неудачу, перед вставкой требуется явно привести объект к типу Tdate; это то, что программист никогда не должен делать: procedure TForml ButtonAddButtonClick(Sender: TObject): begin ListW.Add (TDate(TButton Create (nil))); TList(ListI).Add (TButton.Create (nil)); UpdateList. end; 0177
178 Глава 4. Клас / Запрос UpdateList вызывает исключение, отображенное непосредственно В эле- мент управления «список», поскольку в пользовательских классах-списках я использовал приведение типа as. Грамотный программист никогда не будет исполь- зовать приведенный выше программный код. Подведем итог: создание пользова- тельского списка для определенного типа данных делает программу гораздо на- дежней. Создание списка-оболочки вместо списка, основанного на наследовании, будет немного более безопасным, хотя это потребует написания большего объема программного кода. СОВЕТ------------------------------------------------------------------------ Вместо повторного написания классов-оболочек различных типов можно воспользоваться моим ма- стером List Template Wizard. Подробности см. в приложении А. Поточная система Еще одна центральная область библиотеки классов Delphi заключается в поддер- жке поточной передачи данных, включающей управление файлами, памятью, со- кетами и прочими источниками информации, располагаемой последовательно. Идея использования потоков заключается в том, что перемещение по данным осуществляется в ходе их чтения, в основном функциями Read и Write, традицион- но используемыми языком Pascal (и рассмотренными в главе 12 электронной кни- ги Essential Pascal — см. приложение В). Класс TStream VCL определяет абстрактный класс TStream и несколько подклассов. Класс-пре- док, TStream, имеет лишь несколько свойств, и вы никогда не будете создавать его экземпляр, но он имеет интересный состав методов, используемых при работе про- изводных классов-потоков. Класс TStream определяет два свойства: Size и Position. Все объекты-потоки име- ют определенный размер (который растет по мере записи чего-либо в конец пото- ка), и вы можете определить положение внутри потока, с которого необходимо прочитать или записать информацию. Запись и чтение разрядов зависит от действительного используемого класса- потока, но в обоих случаях для записи и для чтения данных вполне достаточно знать лишь размер потока и относительное положение в подтоке. Фактически это и есть одно из преимуществ использования потоков. Базовый интерфейс остается тем же, независимо оттого, что используется либо дисковый файл, либо BLOB-поле (боль- шой двоичный объект данных), либо длинная последовательность разрядов памяти. Помимо свойств Size и Position класс TStream также определяет ряд важных ме- тодов, большинство из которых являются виртуальными и абстрактными. (Иначе говоря, класс TStream не определяет, что делают эти методы; следовательно, за их реализацию отвечают классы-наследники). Некоторые из этих методов важны толь- ко в контексте чтения или записи компонентов (например, ReadComponent и Write- Component), но часть из них полезна и в других случаях. В листинге 4.2 представле- но объявление класса TStream (фрагмент модуля Classes). 0178
Поточная система 179 Листинг 4.2. Раздел Public определения класса TStream TStream - class(TObject) public // запись и чтение буфера function Read(var Buffer; Count: Longint): Longint; virtual; abstract; function Write(const Buffer; Count: Longint): Longint: virtual; abstract; procedure ReadBuffer(var Buffer: Count: Longint); procedure WriteBuffer(const Buffer: Count: Longint); // перемещение в определенное местоположение function Seek(Offset: Longint; Origin: Word): Longint: overload: virtual: function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload: virtual; // копирование потока function CopyFrom(Source: TStream: Count: Int64); Int64; // запись и чтение компонента function ReadComponent(Instance: TComponent): TComponent; function ReadComponentRes(Instance: TComponent): TComponent: procedure WriteComponentIInstance: TComponent); procedure WriteComponentRes(const ResName: string; Instance: TComponent): procedure WriteDescendentdnstance. Ancestor: TComponent); procedure WriteDescendentRes( const ResName: string: Instance. Ancestor: TComponent): procedure WriteResourceHeader(const ResName: string: out Fixupinfo: Integer); procedure FixupResourceHeader(FixupInfo: Integer): procedure ReadResHeader; // свойства property Position: Int64 read GetPosition write SetPosition: property Size: Int64 read GetSize write SetSize64; end: Основное применение потока заключается в вызове методов ReadBuffer и Write- Buffer, которые являются очень мощными, но ужасно непростыми. Первый пара- метр — это буфер неопределенного типа, в который можно передавать перемен- ную для сохранения или для загрузки из него. Например, можно сохранять в файл число (в двойном формате) и строку следующим образом: var stream: TStream; n: integer; str: string; begin n :- 10; str := 'test string'; stream := TFileStream.Create (.‘c:\tnp\tesf. fmCreate): stream.WriteBuffer (n, sizeOf(integer)); stream.WriteBuffer (strflj. Length (str)): stream.Free; Альтернативный подход заключается в предоставлении возможности специ- альным компонентам загружать данные в поток и из потока. Многие VCL-классы °пределяют метод LoadFromStream или SaveToStream, например, TStrings, TStringList, TBlobField, TMemoField, Ticon и TBitmap. 0179
180 / Специальные классы-потоки Создание экземпляра TStream не имеет смысла, потому что этот класс абстрактен и не обеспечивает непосредственной поддержки сохранения данных. Вместо этого для загрузки данные из или в действительный файл, BLOB-поле, сокет или блок памяти можно использовать один из его производных классов. TFileStream исполь- зуется для работы с файлом, передавая имя файла и некоторые параметры доступа к файлам в метод Create. Для управления потока в памяти, а не в действительном файле, используется класс TMemoryStream. Некоторые модули определяют производные от TStream классы. Модуль Classes включает следующие классы: О THandleStream определяет поток, который управляет дисковым файлом, пред- ставленным дескриптором файла (file handle); О TFileStream определяет поток, который управляет дисковым файлом (файл, ко- торый существует на локальном или сетевом диске), представленным именем файла. Он исходит от класса THandleStream; О TCustomMemoryStream — это базовый класс для потоков, хранимых в памяти, но он не используется непосредственно; О TMemoryStream определяет поток, который управляет последовательностью бай- тов в памяти. Он исходит от класса TCustomMemoryStream; О TStringStream обеспечивает простой способ организации потока для строки, раз- мещенной в памяти, что позволяет обращаться к строке с интерфейсом TStream и копировать строку из одного потока в другой; О TResourceStream определяет поток, который управляет последовательностью байтов в памяти и обеспечивает доступ «только-для-чтения» к ресурсным дан- ным, связанными с исполняемым файлом приложения (DFM файлы являются примером этих данных-ресурсов). Он исходит от класса TCustomMemoryStream. Классы-потоки, определенные в других модулях: О TBlobStream определяет поток, который обеспечивает простой доступ к BLOB- полям базы данных. Существуют подобные BLOB-потоки для не-BDE техно- логий доступа к базам данных, включая TSQLBlobStream и TClientBlobStream. (Об- ратите внимание, что каждый тип набора данных использует для BLOB-полей специальный поточный класс.) Все эти классы исходят от TMemoryStream. О TOleStream определяет поток для чтения и записи информации поверх интер- фейса для обеспечения поточной передачи предоставляемой OLE-объектом; О TWinSocketStream обеспечивает поддержку поточной передачи через сокетное подключение. Использование файловых потоков Создание и использование файловых потоков такое же простое, как создание пе- ременной типа, исходящего от TStream, и вызов методов компонента для загрузки содержания из файла: var S TFileStream; 0180
.. . ......... . . „, .181 begin if OpenDialogl Execute then begin S := TFileStream.Create (OpenDialogl.FileName. fmOpenRead); try Memol Lines.LoadFromStream (S); finally S.Free; end; end: end. Как можно заметить, метод Create для файловых потоков имеет два параметра: имя файла и флаг, указывающие требуемый режим доступа. В этом случае необхо- димо прочитать файл, поэтому используется флаг fmOpenRead (другие доступные флаги описаны в справочной системе Delphi). СОВЕТ--------------------------------------------------------------------------------------- Среди различных режимов наиболее важным является fmShareDenyWrite, который используется при чтении данных из совместно используемого файла, а также fmShareExclusive, который исполь- зуется при записи данных в совместно используемый файл. В TFileStream.Create имеется и третий параметр, называемый Rights (права). Этот параметр используется для передачи разрешений дос- тупа к файлу в файловой системе Linux при использовании режима доступа fmCreate (то есть только при создании нового файла). В Windows этот параметр игнорируется. Большим преимуществом потоков над другими технологиями доступа к файлу является то, что они являются взаимозаменяемыми, т. е. можно работать с потока- ми памяти, а затем сохранять их в файл; или наоборот. Это может быть простым способом повышения производительности программы, интенсивно использующей файлы. Вот фрагмент функции копирования файла, дающий вам иную идею ис- пользования потоков: procedure CopyFile (SourceName. TargetName: String): var Streaml. Stream2: TFileStream: begin Streaml .= TFileStream.Create (SourceName. fmOpenRead): try Stream2 .= TFileStream Create (TargetName. fmOpenWrite or fmCreate): try Stream2 CopyFrom (Streaml. Streaml Size), finally Stream2 Free. end finally Streaml.Free. end end: Еще одним важным использованием потоков является непосредственная обра- ботка BLOB-полей баз данных или других больших полей. Можно экспортиро- вать такие данные в поток или читать их из потока вызовом методов SaveToStream и LoadFromStream класса TbLobField. 0181
182 СОВЕТ------------------------------------------------------------------------------------- В Delphi 7 в поддержку потоков добавлен новый класс исключения EFileStreamError. Его конструктор получает в качестве параметра имя файла для составления отчета об ошибках. Этот класс стандар- тизирует и в значительной степени упрощает уведомление об ошибках в потоках, связанных с фай- лами. Классы TReader и TWriter Сами по себе потоковые классы VCL не предоставляют значительной поддержки записи или чтения. Фактически потоковые классы не реализуют ничего, кроме простого чтения и записи блока данных. Если необходимо загрузить или сохра- нить в поток определенные типы данных (не выполняя объемного приведения ти- пов), то можно воспользоваться классами TReader и TWriter, исходящими от общего класса TFiler. В основном классы TReader и TWriter существуют для упрощения загрузки и со- хранения поточных данных в соответствии с их типом, а не просто как последова- тельность байт. Для этого TWriter включает в поток специальные сигнатуры, указы- вающие тип каждого объекта данных. С другой стороны, класс TReader считывает эти сигнатуры из потока, создает соответствующие объекты, а затем инициализи- рует эти объекты, используя последовательные данные из потока. Например, записать в поток число и строку можно следующим образом: var stream: TStream; n: integer: str: string; w: TWriter; begin n := 10; str := 'test string'; stream : = TFileStream.Create ('C:\tmp\test.txt'. fmCreate); w := TWriter.Create (stream. 1024); w.Writeinteger (n); w.WriteString (str); w.Free; stream.Free; На этот раз файл будет содержать дополнительные символы сигнатуры, кото- рые можно прочитать из файла с помощью объекта типа TReader. Ввиду этого TReader и TWriter обычно предназначены для последовательной передачи компонентов и ред- ко используются для общего управления файлами. Потоки и длительное хранение В Delphi потоки играют значительную роль в длительном хранении. По этой при- чине многие методы TStream относятся к сохранению и загрузке компонента и его подкомпонентов. Например, можно сохранить форму в потоке: stream.WriteComponent(Forml); При просмотре структуры DFM-файла можно отметить, что это — действитель- но лишь файл ресурсов, содержащий ресурс пользовательского формата. В этом ресурсе можно найти сведения о данном компоненте-форме или модуле данных, 0182
183 а также о каждом из расположенных в нем компонентов. Как можно было предпо- ложить, классы-потоки обеспечивают два метода чтения и записи этих пользова- тельских данных ресурса компонентов: WriteComponentRes — для сохранения дан- ных, и ReadComponentRes — для их загрузки. Тем не менее для экспериментов работы с памятью (не используя DFM-фай- лы) более подходит WriteComponent. После того как создан поток в памяти и в него сохранена текущая форма, проблема состоит в том, как ее вывести на экран. Это можно сделать, преобразовывая двоичное представление формы в текстовое пред- ставление. Даже при том, что начиная с версии Delphi 5 IDE может сохранять DFM- файлы в текстовом формате, внутреннее представление, используемое для компи- лируемого кода, — неизменно двоичный формат. IDE обычно выполняет преобразование формы с помощью команды View as Text (В виде текста) конструктора формы и другими способами. Каталог Bin также со- держит утилиту командной строки CONVERT.EXE. В пределах собственного кода, стан- дартным способом выполнения преобразования является вызов специальных VCL- методов. Существует четыре функции преобразования из внутреннего формата объекта, полученного WriteComponent методом: procedure ObjectBinaryToTextdnput. Output: TStream); overload; procedure ObjectBinaryToTextdnput. Output: TStream; var Original Format: TStreamOriginalFormat); overload; procedure ObjectTextToBinarydnput. Output: TStream): overload; procedure ObjectTextToBinarydnput. Output: TStream: var Original Format; TStreamOriginalFormat); overload; Четыре другие функции с одинаковыми параметрами и именами, содержащи- ми имя Resource вместо Binary (например, ObjectResourceToText), осуществляют преобразование формата ресурса, получаемого с помощью WriteComponentRes. Пос- ледний метод, TestStreamFormat, указывает хранится ли DFM-файл в двоичном или текстовом представлении. В программе FormToText для копирования двоичного определения формы в дру- гой поток я использовал метод ObjectBinaryToText, а затем я отобразил результиру- ющий поток в поле МЕМО (рис. 4.5). Вот программный код двух используемых для этого методов: procedure TformText.btnCurrentClick(Sender: TObject); var MemStr: TStream; begin MemStr := TMemoryStream.Create: try MemStr.WriteComponent (Self): ConvertAndShow (MemStr); finally MemStr.Free end: end; procedure TformText.ConvertAndShow (aStream: TStream); var ConvStream: TStream: begin aStream.Position := 0: 0183
184 ГлияЧ, Kipoa ConvStream TMemoryStream Create: try ObjectBinaryToText (aStream. ConvStream), ConvStream Position := 0. MemoOut Lines LoadFromStream (ConvStream); finally ConvStream.Free end. end; inform To Teat Object | Fotm m ExecwWfete Fite object lormT ext TformText Left«191 T op = 113 Width = 545 Height • 374 ActtveControl« btnCurrent Caption - ‘Form To Text' Color = cIB InFace Font Charset = DEFAULT.CHARSET Font Color « cWndowT ext Font Height = -11 Font Name «145 Sans Serf Font Style = 0 OldCreateOrder = True Visible «True PixelsPerlnch « 96 TextHeight = 13 object memoOut TMemo Left = 0 Top = 41 Width = 537 Height» 306 Align = alChent ScrollBars « ssVertical TabOrder = 0 end object pB ar T Panel Left «0 Top = Q Width « 537 Height = 41 Align = allop Рис. 4.5. Текстовое описание компонента-формы, представленное в самой форме примером FormToText Обратите внимание, что при многократном щелчке на кнопке Current Form Object вы получаете все больше текста, и текст поля МЕМО включается в поток. Через несколько раз выполнение всей операции становится чрезвычайно медленным, вплоть до того, что создается впечатление «зависания» программы. В этом фраг- менте видна гибкость использования потоков — можно написать общую процеду- ру, которую затем использовать для конвертирования любого потока. СОВЕТ-------------------------------------------------------------------------- Важно подчеркнуть, что после того как данные записаны в поток, перед тем как использовать поток далее, необходимо явно перейти назад на начало (или установить свойство Position в 0), конечно, если вы не хотите присоединить данные в конец потока. Еще одна кнопка, названная Panel Object, показывает текстовое представление определенного компонента, панели, передавая этот компонент в метод WriteCom- ponent. Третья кнопка, Form in Executable File, выполняет другую операцию. Вместо поточного представления существующего объекта в памяти она загружает в объект 0184
Поточная систвМКбд w______________________________________________________________185 TResourceStream представление формы времени разработки (т. е. его DFM-файл) из соответствующего ресурса, внедренного в исполняемый файл: procedure TFormText btnResourceClick(Sender TObject). var ResStr: TResourceStream. begin ResStr = TResourceStream.Create(hlnstance. 'TFORMTEXT'. RT_RCDATA); try ConvertAndShow (ResStr). finail у ResStr Free end. end. Последовательно щелкая на этих кнопках (или изменяя данную форму про- граммы), вы сможете сравнить форму, сохраненную в DFM-файл с текущим объек- том времени выполнения. Создание пользовательского класса-потока Помимо использования существующих классов-потоков Delphi-программи- сты могут написать собственные классы и использовать их вместо существу- ющих. Для этого необходимо лишь определить, как будет сохраняться и за- гружаться блок «сырых» данных. Для работы с новым типом носителя может не потребоваться создавать совершенно новый класс, достаточно будет толь- ко настроить существующий поток. В этом случае все, что надо сделать, — написать соответствующие методы записи и чтения. В качестве примера я создал класс, кодирующий и декодирующий ис- ходный файловый поток. Хотя этот пример ограничен использованием со- вершенно бессмысленного механизма кодирования, он полностью интегри- рован с VCL и работает должным образом. Новый поточный класс просто объявляет два стержневых метода чтения и записи, а также имеет свойство, хранящее ключ кода: type TEncodedStream = class (TFileStream) pri vate FKey Char: public constructor Create(const FileName string. Mode: Word), function Readtvar Buffer. Count- Longint) Longint. override, function Write(const Buffer. Count Longint): Longint. override: property Key Char read FKey write FKey. end. Значение ключа добавляется в каждый байт, сохраняемый в файл, и вы- читается при чтении данных. Вот полный код методов Write и Read, весьма интенсивно использующих указатели: constructor TEncodedStream.Create( const FileName string. Mode Word): begin inherited Create (FileName, Mode). FKey = 'A '. // по умолчанию end. — - — продолжение 0185
186 Глава 4. Классы базовой библиотеки Создание пользовательского класса-потока [продолжение) function TEncodedStream.Write(const Buffer: Count: Longint): Longint: var pBuf. pEnc: PChar: I. EncVal: Integer; begin // вылепить память для буфера кодирования GetMem (pEnc, Count). try // испопьзовать этот буфер как массив символов pBuf := PChar (©Buffer). // для каждого символа в буфере for I := 0 to Count - 1 do begin // закодировать значение и сохранить его EncVal := ( Ord (pBuf[IJ) + Ord(Key) ) mod 256; pEnc [I] := Chr (EncVal): end. // записать буфер кодирования в файл Result := inherited Write (pEncA, Count): finally FreeMem (pEnc, Count); end. end: function TEncodedStream.Read(var Buffer; Count; Longint): Longint; var pBuf. pEnc PChar; I. CountRead. EncVal: Integer; begin // выделить память для буфера декодирования GetMem (pEnc. Count): try // выполнить чтение из файла в буфер CountRead .= inherited Read (pEncA. Count): // использовать выходной буфер как строчный тип pBuf := PChar (©Buffer): // для каждого действительного прочитанного символа for I := 0 to CountRead - 1 do begin // декодировать значение и сохранить его EncVal := ( Ord (pEnc[ID - Ord(Key) ) mod 256: pBuf [I] = Chr (EncVal): end. finally FreeMem (pEnc, Count): end. // вернуть число прочитанных символов Result CountRead: end. Комментарии в этом довольно сложном программном коде должны по- мочь понять все детали. 0186
Поточнаясистема 187 Теперь я использовал этот кодированный поток в демонстрационной про- грамме, называемой EncDemo. Форма этой программы имеет два компонента МЕМО и три кнопки: i Encoded Stream Demo JfciWJKaa*““ ’»cu)NKaaaalC,{ai«>c4NKaa*1E ОДКаааа^'рЧОц ‘’aflcuF" pal«-fc^(a'|i»"laf‘W(<»ti4NKddda§r« urfaT" !“wla' ” NKaaaaSroy^afцаЛ§§ГЫ'Гм.{аГ” > «Г" » ula-p*VJNKaaaa^Wa<l,(at«>c’a)!cMa«»a?» цЖа¥$с1 uahfrJNKaa{”M|NKNK»*s Kucu‘‘"NK ! NK°*~ pal»-fc •}(aVr 1аГ¥КаР^Щ *"NKaa»'©p*u!Maf|Сц|аЙ»-| .: 1С»|гМ^Каав«а(-аМ1НКГМ1Н*Ж§Г^ < ' «jxrf J aT »"WNK c’NKaa±l1§ma±r»(aWNKea|mal 4 C-(ai“u} flNKEf i'NKaappaC--acpia*t«n’a§,’ap®W _H^a£WHKaa||ul}*a±rtWrwNKaai?,NKa aaappaT}aji©JaE1§^ae'ae’ aCeC’a*§ac’^>CJCoii® N Каааа*|Ц§а{~а (ФсМ WNKaaaappaTa! C?C°Ui!a'§au.©,1a£t§§?NKiaaa§*,a|a{''e<w,ai‘V ца naraM'NKaaaaE) |“NKaaaaaappa{"B*Vlau®,la c 1|ac "Ma p>^NKaaaaaarBfc-a(~atf¥a<±i1&|JHlaW ►a|a®'MasvwiNKaaaaaa±rt5aWa{~'a<®*a4 *4ic*||NKa ааа1гМ!^Кддаарра.чр}ацФ|аГн'ЦМа£.1§^ац‘ац©‘га wj unit EncodStr. * nt efface uses □asset, type TEncodedStream dess (TFieStream) private FKey Char, pubic constructor Create( const FteName string. Mode Word), funebon Readfvar Buffer, Count Longnt): Longnt, override funebon Wntef const Buffet. Count Longnt): Longrt. override, property Key Char read FKey wnte FKey default A’, end. mplementabon Первая кнопка загружает файл открытого текста в первое поле МЕМО; вторая — сохраняет этот файл в кодированном виде; а третья кнопка — за- гружает текст из файла во второе поле МЕМО, расшифровывая его. В этом примере после кодирования текста в файл я повторно загрузил этот файл в первое поле МЕМО как будто он имеет незакодированный текст (см. сле- ва), который, конечно же, является нечитаемым. Так как теперь у нас имеется класс кодированного потока, то исходный текст этой программы очень похож на любую другую программу, использу- ющую потоки. Например, вот метод, который используется для сохранения «закодированного» файла (сравните его с представленными ранее примера- ми, основанными на использовании потоков): procedure TFormEncode.BtnSaveEncodedClicktSender: TObject): var EncStr TEncodedStream; begin if SaveDialogl.Execute then begin EncStr .= TEncodedStream.CreatetSaveDialogl.Filename. fmCreate): try Memol.Lines.SaveToStream (EncStr): finally EncStr Free; end. end. end; Сжатие потоков с помощью ZLib Новой особенностью Delphi 7 является официальная поддержка библиотеки сжа- тия ZLib (сама библиотека и документация находятся на сайте www.gzip.org/zlib). 0187
188 Глава 4. Классы базовой библиотеки Модуль, обеспечивающий интерфейс с ZLib, был доступен в течение длительного времени на дистрибутивных компакт-дисках Delphi, но сейчас он включен в ядро и представлен среди других исходных программных кодов VCL (модули ZLib и ZLibConst). Помимо предоставления интерфейса к данной библиотеке (которая написана на языке С и может непосредственно внедрятся в программы Delphi без необходимости переустанавливать DLL), Delphi 7 определяет пару вспомогатель- ных классов потоков: TCompressStream и TDecompressStream. В качестве примера использования этих классов я написал небольшую програм- му, названную ZCompress, которая сжимает и декомпрессирует файлы. Программа имеет две строки редактирования, в которых вводится имя сжимаемого файла и имя конечного файла, который при наличии существующего будет автоматически со- здан. При щелчке на кнопке Compress на основе исходного файла создается конеч- ный; щелчок на кнопке Decompress перемещает сжатый файл обратно в поток памя- ти. В обоих случаях результат сжатия или декомпрессии отображен в поле МЕМО. На рис. 4.6 представлен сжатый файл (который иногда может быть и исходным текстом формы текущей программы). ^ZCompress J СздфГвдо# fife |e \lx^$V^7code\(M\ZCo<npfe$$^ Comptess Рис. 4.6. Пример ZCompress осуществляет сжатие файла с помощью библиотеки ZLib Для возможности повторного использования программного кода этой програм- мы я написал две функции сжатия и декомпрессии одного потока в другой поток. Вот их исходный текст: procedure CompressStream (aSource. aTarget: TStream): var comprStream. TCompressionStream; begin comprStream .= TCompressionStream Create( cl Fastest. aTarget). try comprStream.CopyFrom(aSource. aSource Size): comprStream CompressronRate. finally comprStream Free. end; end; procedure DecompressStream (aSource. aTarget- TStream) ; var decompStream• TDecompress ionSt ream; nRead- Integer; Buffer: array [0 .1023] of Char: 0188
Итог 189 begin decompStream := TDecompresswnStream.Create(aSource). try // aStreamDest.CopyFrom (decompStream, size) не работает II корректно, поскольку заранее неизвестен размер. II поэтому я использовал аналогичный «ручной» код repeat nRead := decompStream ReadCBuffer. 1024); aTarget Write (Buffer. nRead), until nRead = 0: finally decompStream.Free: end; end. Как сказано в комментарии, операция декомпрессии более сложна, поскольку отсутствует возможность использовать метод CopyFrom: заранее неизвестен окон- чательный размер. При передаче в метод значения 0, будет осуществлена попытка получить размер исходного потока, которым является TDecompressionStream. Одна- ко эта операция вызывает исключение, поскольку потоки сжатия и декомпрессии могут от начала до конца иметь атрибут «только для чтения» и не позволят найти конец файла. Итог сравнения модулей Core VCL и BaseCLX Большую часть этой главы я посвятил рассмотрению классов из одного модуля библиотеки — Classes. И фактически этот модуль содержит большинство стержне- вых классов библиотеки. В данном разделе я представлю обзор того, что еще дос- тупно в модуле Classes и ряде других модулей библиотеки. Модуль Classes Модуль Classes является сердцем как VCL-, так и CLX-библиотек, и хотя в последних версиях Delphi он имеет незначительные внутренние изменения, он несколько нов Для средних пользователей. (Большинство изменений связано с модифицированной интеграцией IDE и предназначено для опытных разработчиков компонентов.) Вот список того, что можно найти в модуле Classes: О множество перечисленных типов, стандартные типы указателей методов (вклю- чая TNotifyEvent) и много классов исключения; О стержневые классы библиотеки, включая TPersistent и TComponent также многие Другие, которые редко используются непосредственно; О классы списков, включая TList, TThreadList (поточная версия списка), TInterfaceList (список интерфейсов для внутреннего использования), TCollection, TCollectionltem, TOwnedCollection (который являются просто коллекцией с владельцем), Tstrings и TStringList; ° все поточные классы рассмотренные в предыдущем разделе, которые я не буду повторно здесь перечислять. Также имеются классы TFiler, Treader и TWriter, а так- же TParser, используемый внутри среды для анализа DFM-файлов; 0189
190 О вспомогательные классы типа TBits — для манипуляции с разрядами, а также ряд сервисных процедур (например, конструкторы точки и прямоугольника, процедуры манипуляции списком строк типа LineStart и Extractstrings). Есть так- же много классов регистрации, предназначенных для уведомления системы существования компонентов, классов, специальных сервисных функций, кото- рые можно заменить, и т. д. О класс TDataModule — простой контейнер объектов, альтернативный форме. Мо- дули данных могут содержать только невизуальные компоненты и в общем плане используются в сетевых приложениях и в СУДЕ; СОВЕТ------------------------------------------------------------------------- В ранних версиях Delphi класс TDataModule был определен в модуле Forms; начиная с Delphi 6 он был перемещен в модуль Classes. Это было сделано для того, чтобы устранить перегрузку кода GUI- классов невизуальными приложениями (например, модулями веб-сервера) и в большей степени разделить неперемещаемый код Windows от классов, независимых от операционной системы, на- пример, TDataModule. Прочие изменения касаются модулей данных, например позволяющие созда- вать веб-приложения с множеством модулей данных. О новые классы, связанные с интерфейсом типа TInterfacedPersistent, нацеленного на обеспечение дальнейшей поддержки интерфейсов. Этот специфический класс позволяет коду Delphi «держаться» ссылки на объект TPersistent или любого потомка, реализующего интерфейсы. Он является стержневым элементом но- вой поддержки интерфейсных объектов в инспекторе объектов (см. пример в главе 9); О новый класс TRecaLL, используемый для обслуживания временной копии объек- та. Этот класс особенно полезен для графических ресурсов; О новый класс TClass Finder, который используется для поиска зарегистрированно- го класса вместо метода FindClass; О класс TThread, обеспечивающий основу независимой от операционной системы поддержки многопоточных приложений. Новое в модуле Classes В Delphi 7 модуль Classes имеет лишь незначительные добавления. Помимо изме- нений, уже упомянутых в данной главе (например, расширенная поддержка пар «имя-значение» в классе TStringList), можно упомянуть появление двух новых гло- бальных функций: AncestorlsValid и IsDefaultPropertyValue. Обе функции обеспечивают поддержку «подсветки» в окне инспектора объек- тов свойств, имеющих значение, отличающееся от значения по умолчанию. Они предназначены немного для других целей, но я сомневаюсь, что вы получите выго- ду от их использования в приложении, если только вы не заинтересуетесь сохра- нением состояния компонента и формы, а также написанием пользовательского механизма обработки потоков. Другие стержневые модули Обычные Delphi-программисты не используют другие модули, являющиеся частью RTL-пакета, так часто, как Classes. Вот список этих модулей: 0190
191 О модуль Typlnfo включает поддержку доступа к RTTI-информации для опубли- кованных свойств (см. раздел «Обращение к свойствам по имени»); О модуль SyncObjs содержит несколько исходных классов для синхронизации по- токов; О модуль ZLib включает сжатие и декомпрессию потоков (см. раздел «Сжатие по- токов с помощью ZLib»); О модуль ObjAuto содержит код вызова опубликованных методов объекта по име- ни, с передачей параметров в массиве вариантного типа. Этот модуль является частью расширенной поддержки динамического вызова метода, продвигаемого SOAP и другими новейшими технологиями Delphi. Конечно же, в RTL-пакет помимо Classes входят модули, рассмотренные и в предшествующих главах, например, Math, Syslltils, Variants, VarUtils, StrLItils, DateUtils ит. д. Что далее? Как вы поняли из этой главы, библиотека классов Delphi имеет несколько корне- вых классов, играющих значительную роль, которые необходимо изучить в макси- мально возможной степени. Некоторые программисты стремятся стать экспертом по повседневно используемым компонентам, и это важно; но без понимания стер- жневых классов (и таких идей, как принадлежность и потоки) для максимального использования возможностей Delphi вам потребуется приложить неимоверные усилия. Конечно, в этой книге я также должен обсудить визуальные классы и классы баз данных. Теперь, когда я представил все основные элементы Delphi (язык, RTL, основные классы), можно перейти к рассмотрению разработки реальных прило- жений. Глава 5 охватывает структуру визуальной части библиотеки компонентов. Последующие главы посвящены примерам использования различных компо- нентов при создании приложений с современным интерфейсом пользователя и эф- фективным использованием форм. Мы рассмотрим расширенное использование традиционных элементов управления и меню, познакомимся с архитектурой дей- ствий и классом TForm, а затем изучим панель инструментов, строку состояния, диалоговые окна и MDI-приложения. 0191
5 Визуальные элементы управления Теперь, когда вы познакомились со средой и языком Delphi, а также с базовыми элементами библиотеки компонентов, можно приступить к использованию ком- понентов и разработке пользовательского интерфейса приложений. Именно для этого и предназначена среда Delphi. Визуальное программирование с использова- нием компонентов — ключевая функциональная возможность этой среды разра- ботки. Delphi поставляется с большим набором готовых к использованию компонен- тов. Я не буду подробно рассматривать каждый из них, представляя их свойства и методы; если вам требуется эта информация, то ее можно найти в справочной системе Delphi. Цель этой и последующих глав — представить особенности исполь- зования некоторых расширенных возможностей, предлагаемых существующими компонентами Delphi, при постройке приложений, а также рассмотреть специфи- ческие методики программирования. Начнем мы со сравнения библиотек VCL и VisualCLX и охватим базовые клас- сы (особенно TControl). Затем изучим различные визуальные компоненты, посколь- ку правильный выбор элементов управления зачастую помогает быстрее разрабо- тать необходимый проект. Данная глава охватывает следующие вопросы: О VCL по сравнению с VisualCLX; О классы TControl, TWinControl и TWidgetControl: О обзор стандартных компонентов; О базовая и расширенная разработка меню; О изменение системного меню; О изображения в меню и списках; О «собственные» элементы и стили. VCL по сравнению с VisualCLX Как вы уже знаете из главы 4, в Delphi имеется две библиотеки визуальных клас- сов: кросс-платформенная библиотека (CLX) и традиционная Windows-библио- тека (VCL). Естественно, между разработкой программ, особенно для Windows 0192
VCL по сравнению с VisualCLX 193 и при кросс-платформенном подходе, существует много различий, но более всего они поразительны в пользовательском интерфейсе. Визуальная часть VCL является оболочкой API Windows. Она содержит обо- лочки собственных элементов управления Windows (таких, как кнопки, строки редактирования), обобщающих элементов (например, дерево, список) плюс ряд собственных элементов управления Delphi для привязки к реализованной в Win- dows концепции окон. Кроме того, класс TCanvas служит оболочкой для базовых графических вызовов, что позволяет легко раскрасить поверхность окна. VisualCLX, визуальная часть CLX, является оболочкой для библиотеки Qt (чи- тается «къют»). Она включает оболочки собственных компонентов Qt (от простых до расширенных), очень похожих на стандартные и общие элементы управления Windows. Кроме того, с помощью другого, подобного TCanvas класса, она включает поддержку графики. Qt — это библиотека классов языка C++, разработанная нор- вежской компанией Trolltech (www.trolltech.com), тесно взаимодействующей с ком- панией Borland. В Linux библиотека Qt де-факто является одной из стандартных библиотек пользовательского интерфейса и основой среды KDE. В Windows Qt предостав- ляет альтернативу использования «родных» API. В отличие от VCL, которая слу- жит оболочкой для собственных элементов управления Windows, Qt обеспечива- ет альтернативную реализацию этих элементов. Даже если каждая из них основана на окнах Windows, элемент «кнопка» библиотеки Qt не является элементом класса BUTTON (это можно увидеть, запустив WinSight32). Ввиду отсутствия видимых отличий, создаваемых операционной системой (либо негласно вносимых произ- водителем операционной системы), это на самом деле обеспечивает переносимость программ. Кроме того, этот факт также позволяет избежать дополнительного слоя; предполагается три слоя: CLX над Qt, Qt над собственными элементами управле- ния Windows, но фактически в каждом решении существует лишь два слоя (эле- менты CLX над Qt, элементы VCL — над Windows). СОВЕТ------------------------------------------------------------------—— Установка готового Qt-приложения в среде Windows предполагает наличие самой Qt-библиотеки. На платформе Linux наличие этой библиотеки воспринимается как должное, но, тем не менее, ^охраняется необходимость установки библиотеки интерфейса. CLX компании Borland в Linux при- вязана к определенной версии Qt (для которой в Kylix 3 специально выпущена программа-«заплат- ка»), вам в любом случае придется устанавливать и ее. Распространение Qt-библиотек с помощью Профессионального приложения (в отличие от проекта с открытым исходным программным кодом) Обычно подразумевает оплату лицензии компании Trolltech. Однако если для построения Qt-прило- жений использовать Delphi или Kylix, то оплату лицензии за вас уже произвела компания Borland. Вы должны использовать, по меньшей мере, один класс CLX в качестве оболочки Qt: если вы экс- клюзивно используете QT-класс (вообще без CLX), то на Qt необходимо получить лицензию, даже если вы используете их в Delphi или Kylix. Технически между собственными приложениями Windows, построенными с Помощью VCL, и Qt-программами, разработанными с помощью VisualCLX, не- гласно существуют большие различия. Достаточно сказать, что на нижнем уровне рля осуществления связи между элементами управления Windows использует рызов функций API и сообщения, в то время как Qt использует методы класса Й непосредственные обратные вызовы методов, и не использует внутренние сооб- щения. Формально классы Qt предлагают объектно-ориентированную архитекту- 0193
194 Глава 5. Визуальные элементы управления ру высокого уровня, но API Windows наследственно связаны с языком С и осно- ванной на сообщениях системе, датированной 1995 годом (когда была представле- на ОС Windows). VCL предлагает объектно-ориентированную абстракцию над низ- коуровневым API, в то время как VisualCLX отображает интерфейс уже высокого уровня в более известную библиотеку классов. (Дополнительные сведения об ар- хитектуре Qt см. во врезке «От Qt к CLX».) СОВЕТ-------------------------------------------------------------------- Компания Microsoft пришла к пониманию того, что необходимо отказаться от традиционного низко- уровневого API Windows для собственных библиотек классов высокого уровня, части архитектуры .NET. Дополнительные сведения по этому вопросу можно найти в четвертой части этой книги. Если нижестоящие архитектуры API Windows, с одной стороны, и Qt, с другой стороны, взаимосвязаны, то две библиотеки классов, построенные компанией Bor- land (VCL и CLX) сглаживают большинство различий, делая программный код приложений Delphi и Kylix крайне схожим. Использование VisualCLX в Linux предоставляет Delphi-программистам знакомую библиотеку классов на совершенно новой платформе. С внешней стороны для обеих библиотек «кнопка» — это объект класса TButton, содержащий практически одинаковый набор методов, свойств и со- бытий. В большинстве случаев, если существующие программы не используют низкоуровневые вызовы API, платформо-зависимые возможности (ADO или СОМ) либо устаревшие возможности (например, BDE), то их можно перекомпилировать в течение нескольких минут. От Qt к CLX Qt является библиотекой классов языка C++ и содержит полный набор эле- ментов, включая не только базовые компоненты Windows (кнопки, списки и т. п.), но и большинство из обобщающих элементов управления (таких, как TreeView и ListView). Поскольку Qt — это библиотека классов C++, она не может вызываться непосредственно из программного кода языка Delphi. Вместо этого API (пользовательским интерфейсам) Qt доступна посредством слоя привязки, определенного в модуле Qtpas. Этот слой привязки состоит из большого списка оболочек практически каждого класса Qt, снабженного окончанием Н. Так, например, класс QPainter библиотеки Qt в слое привязки становится типом QPainterH. Модуль Qt.pas также содержит большой список всех общих (public) методов этих классов, преобразованных в стандартные функции (а не методы класса), которые в качестве первого параметра применяют объект, в отношении которого они выполняются. Единственным исключением из этого подхода являются кон- структоры классов, которые трансформируются в функции, возвращающие новый экземпляр класса. Обратите внимание, что использование, по меньшей мере, одного класса отображающего слоя является обязательным условием лицензирования Qt (для Delphi и Kylix). Qt является бесплатной для некоммерческого исполь- зования под X Window (так называемая Qt Free Edition), но при разработке коммерческих приложений необходимо выплатить лицензию компании Troll- tech. При покупке Delphi лицензия на использование библиотеки Qt уже 0194
VCL по сравнению с VisualCLX 195 оплачена компанией Borland, но Qt должна использоваться посредством CLX (даже при использовании низкоуровневых вызовов к Qt из CLX). Нельзя непосредственно использовать модуль Qt.pas и избегать включения модуля QForms (что является обязательным). Компания Borland принудительно ре- ализует это ограничение путем удаления из интерфейса Qt конструкторов QFormH и QapplicationН. В большинстве программ, упомянутых в этой книге, я использовал только объекты и методы CLX. Важно понимать, что при не- обходимости можно непосредственно использовать некоторые дополнитель- ные возможности; либо использовать низкоуровневые вызовы, обходя ошиб- ки CLX. Документация по Qt не включена в справочную систему Delphi, но ее всегда можно найти на веб-сайте компании Trolltech (http://www.trolltech. com) в HTML- и PDF-формате. Двойная поддержка библиотек в Delphi Delphi имеет полную поддержку для обеих библиотек во время разработки и во время выполнения. Как только вы начинаете разрабатывать новое приложение, вы выбираете File (Файл) ► New Application (Новое приложение) для создания но- вой программы на основе VCL, либо File (Файл) ► New CLX Application (Новое CLX- Приложение) — для создания программы на основе CLX-библиотеки. После выбо- ра одного из этих пунктов IDE Delphi создаст VCL- или CLX-форму и обновит панель компонентов таким образом, что на ней будут представлены лишь визуаль- ные компоненты, совместимые с типом выбранного приложения (рис. 5.1). Невоз- можно поместить VCL-кнопку на CLX-форму и вообще невозможно в одном вы- полняемом файле смешивать формы этих библиотек. Иначе говоря, пользовательский интерфейс каждого приложения должен строиться только с использованием одной из библиотек, что для меня (помимо технических проблем) более важно. Если вы ранее не пробовали, я предлагаю поэкспериментировать, создав CLX- "Йриложение, просмотрев доступные элементы управления и попытавшись их ис- йользовать. Вы найдете ряд отличий в использовании этих компонентов, и если ®ы уже работали в Delphi, то, скорее всего, быстро адаптируетесь к CLX. «ашк! Машек! OenConwal «Ежеен! Омвмо! Ш i АО» I МШееяяв!1Хап>К«<е»1 -Ш. SxMrl ОМАетк! МаСМЫе! «Сапга1 СачЗ»| ВЭЕ I ЛОО I hlaBmt WebSamonl IkkkCkKk! !LL AnMlMMoW Е^|8т1а^А<М|0пСмя>1<К«<>«<!в«4<ж>1авЕ 1М» I Мж! ««Кеми.1 МепвоакШЛ 3 ГТ СХ ЗЗЖ Г—-U-U ► JSfisSaKOl О/ ________________________________ SBMMf Aektorf 0*аАая»|В«й1л*1 Мяш! 1*Вш1«М««№ГМЫГж| нс. 5.1. Сравнение первых трех страниц палитры компонентов для CXL-приложения (вверху) и VCL-приложения (внизу) 0195
196 Глава 5. Визуальные элементы управления Одинаковые классы, различные модули Одним из краеугольных камней совместимости исходных программных кодов CLX и VCL является использование одинаковых имен у аналогичных классов двух биб- лиотек. Например, в обеих библиотеках имеется класс под названием TButton, пред- ставляющий обычную кнопку; его методы и свойства настолько похожи, что ни- жеприведенный программный код будет работать с обеими библиотеками: with TButton.Create (Self) do begin SetBounds (20. 20. 80. 35): Caption := 'New'; Parent - Self: end: Два классаТВиПоп могут иметь одинаковое имя потому, что они хранятся в двух разных модулях, названных StdCtrls и QStdCtrls. Конечно, в ходе разработки невоз- можно иметь оба этих компонента одновременно на панели компонентов, поскольку IDE Delphi может регистрировать только компоненты с уникальными именами. Целиком вся библиотека VisualCLX определена модулями, соответствующими модулям VCL, но с использованием в качестве префикса буквы Q — существуют модуль QForms, модуль Qdialogs, модуль QGraphics и т. д. Несколько странных моду- лей, таких как QStyle, не имеют соответствия в VCL, поскольку они отображают только особенности Qt, не имеющие аналога в API операционной системы Windows. Обратите внимание, что для различения двух библиотек не существует настро- ек компилятора или других скрытых технологий; важным является лишь набор модулей, на которые ссылается программный код. Помните, что эти ссылки долж- ны быть согласованными: нельзя в одной форме или даже в отдельной программе смешивать визуальные элементы управления двух библиотек. DFM и XFM При создании формы во время разработки она сохраняется в файле определения формы. Традиционные VCL-приложения используют для этого файла расшире- ние DFM, являющееся сокращением понятия Delphi form module (модуль Delphi- формы). CLX-приложения используют расширение XFM, являющееся сокраще- нием от «cross-platform (X) form module». Модуль формы — это результат потоковой передачи сведений о форме и ее компонентах: две библиотеки совместно исполь- зуют один поток передачи кода, поэтому он производит аналогичный эффект. Формат DFM- и XFM-файлов, которые могут содержать как двоичное, так и тек- стовое представление, является идентичным. Поэтому необходимость иметь два различных расширения основывается не на внутренних тонкостях работы компи- лятора или несовместимости форматов. Они лишь указывают программистам и IDE тип компонентов, с которыми можно встретиться внутри файла с определенным расширением (это указание не включается в сам файл). Если возникает необходимость конвертировать DFM-файл в XFM-файл, вы можете просто переименовать его. Однако будьте готовы встретиться с некоторы- ми отличиями в свойствах, событиях и доступных компонентах — открытие опре- деления формы с «другой» библиотекой может вызвать появление некоторых пре- т^ппежлений. 0196
VCL по сравнению с VisualCLX 197 ПРИМЕЧАНИЕ---------------------------------------------------------------- IDE Delphi выбирает активную библиотеку, просматривая расширение модуля формы, не учитывая ссылки в выражении uses. Поэтому расширение необходимо изменять лишь в том случае, если вы планируете использовать CLX. В Kylix другие расширения просто бесполезны, поскольку любая форма открывается в IDE как CLX-форма, независимо от расширения. В Linux существует только CLX-биб- лиотека, основанная на Qt, которая одновременно является и «родной» и кросс-платформенной библиотекой. В качестве примера я разработал два идентичных приложения — LibComp и Qlib- Comp, содержащие лишь несколько компонентов и один обработчик события. В лис- тинге 5.1 представлены текстовые определения форм двух приложений, которые построены с помощью одинаковых действий в IDE Delphi после выбора CLX- или VCL-приложения. Отличия я выделил жирным шрифтом; как можно заметить, их очень мало и они относятся к самой форме и шрифту. OldCreateOrder — это свой- ство, обеспечивающее наследственную преемственность с Delphi 3 и более ранни- ми версиями программного кода; стандартные цвета имеют различные имена; a CLX сохраняет диапазоны линеек. Листинг 5.1. XFM-файл (слева) и его DFM-эквивалент Object Forml: TForml Left = 192 Left = 192 Top = 107 Width = 350 Height = 210 Caption = 'QLibComp' Color = clBackground VertScrollBar.Range = 161 HorzScrollBar.Range = 297 object Forml: TForml Left = 192 Left = 192 Top = 107 Width = 350 Height = 210 Caption = 'QLibComp' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif Font.Style = □ TextHeight = 13 Textwidth = 6 PixelsPerlnch = 96 object Buttonl: TButton Left = 56 Top = 64 Width = 75 Height = 25 Caption = 'Add' TabOrder = 0 OnClick = ButtonlClick end object Editl: TEdit Left = 40 ; Top = 32 Width = 105 Height = 21 TabOrder - 1 Text = ‘my name' end object ListBoxl: TListBox Left = 176 TextHeight = 13 OldCreateOrder = False PixelsPerlnch = 96 object Buttonl: TButton Left = 56 Top = 64 Width = 75 Height = 25 Caption = 'Add' TabOrder - 0 OnClick = ButtonlClick end object Editl: TEdit Left = 40 Top = 32 Width = 105 Height = 21 TabOrder = 1 Text = 'my name' end object ListBoxl: TListBox Left = 176 л 0197
198 Глава 5. Визуальные элементы управления Листинг 5.1 (продолжений) Тор = 32 Width - 121 Height = 129 Rows = 3 Items.Strings = ( 'marco' 'John' 'helen') TabOrder = 2 end end Top = 32 Width = 121 Height = 129 ItemHeight = 13 Items.Strings = ( 'marco ’ ‘John' 'helen') TabOrder = 2 end end Выражения uses Если просмотреть исходный программный код VCL- или CLX-приложения, един- ственным отличием будет содержание выражения uses. Форма CLX-приложения начинается с: unit QLibCompForm: interface uses SysUtils. Types. Classes. QGraphics. QControls. QForms. QDIalogs, QStdCtrls; Форма VCL-программы имеет традиционное выражение uses: unit LibCompForm: interface uses Windows. Messages. SysUtils. Variants. Classes. Graphics. Controls. Forms. Dialogs. StdCtrls: Программные коды класса и единственного обработчика событий абсолютно идентичны. Конечно, в CLX-версии стандартной программы директива компиля- тора {$R *.dfm} заменена на {$R *.xfm}. Отключение двойной поддержки в справочной системе При нажатии клавиши F1 в редакторе и запросе справки о процедуре, классе или методе библиотеки Delphi вам будет предоставлена возможность выбора VCL- и CLX-определений запрашиваемой информации. Каждый раз необходимо делать выбор, что через некоторое время начинает раздражать (особенно когда две стра- ницы практически идентичны). Если вы не интересуетесь CLX и планируете использовать только VCL (или наоборот), можно отключить часть разделов справки, выбрав команду Help (Справ- ка) ► Customize (Настройка), удалив из Contents (Содержание), Index (Указатель) и Link (Поиск) все разделы справки, в названии которых встречается CLX, и сохра- нив проект. После перезапуска IDE Delphi справочная система больше не будет всякий раз уточнять у вас требуемые разделы. Конечно же, если вы решите ис- пользовать CLX, не забудьте добавить необходимые файлы. Подобным образом, отключая все относящиеся к CLX пакеты, вы сможете снизить расход памяти и время загрузки IDE Delphi. Выбор визуальной библиотеки Поскольку в Delphi доступны две библиотеки пользовательского интерфейса, для -------—DQM ППТЛ ТТА'ГГ'СТ DLrfrUnOTU Г\ТТТ_ГХГ m WTAV И ТТСТ ТТОТЛМСТ- 0198
VCL по сравнению с VisualCLX 199 тия правильного решения, которое нельзя назвать простым, вы должны учесть множество критериев. Первым критерием является перемещаемость программы. Если основным ус- ловием является выполнение вашей программы под Windows и под Linux с одина- ковым пользовательским интерфейсом, то использование CLX может облегчить вашу жизнь и позволит обойтись одним файлом исходного программного кода с ограниченным использованием директивы IFDEF. То же самое можно сказать, если в качестве основной ОС используется (или возможно будет) Linux. С другой сто- роны, если большинство пользователей используют Windows, а вы лишь хотите расширить ваше предложение Linux-версией, вы, возможно, захотите сохранить двойную систему VCL/CLX. Это означает, что вам, вероятно, понадобятся два на- бора файлов исходного теста программы, либо придется слишком часто использо- вать директиву IFDEF. Следующим критерием является интуитивность восприятия. При использова- нии CLX в Windows некоторые элементы управления будут вести себя чуть-чуть не так, как ожидает пользователь, во всяком случае, опытный. Для простых эле- ментов интерфейса (строк редактирования, кнопок и проч.) это может не иметь большого значения, но если имеется множество деревьев или списков (TreeView или ListView), разница будет заметна явно. С другой стороны, с помощью CLX вы смо- жете позволить пользователям ощутить разницу по сравнению с основным видом Windows и согласованно использовать его в различных представлениях, т. е. эле- мент fan в Motif (стандарт графического интерфейса пользователя для ОС UNIX) сможет выбрать этот стиль даже при использовании платформы Windows. Хотя это удобство характерно для Linux, в Windows редко применяется интуитивность восприятия. Использование «родных» элементов управления также предполагает, что при переходе на новую версию ОС Windows ваше приложение (вероятно) адаптируется к нему. Это удобно для пользователя, но в случае несовместимости добавляет го- ловной боли программисту. Различия в библиотеках общих компонентов Microsoft за последние годы стали источником разочарования Windows-программистов в общем и Delphi-программистов в частности. Следующим критерием является развертывание (размещение программы на компьютере пользователя): при использовании CLX обязательно необходимо в комплект поставки Windows- и Linux-приложений включать библиотеки Qt. Я выполнил небольшую проверку и выяснил, что скорость работы VCL и CLX- приложений практически одинакова. Выполнялось создание 1000 компонентов и вывод их на экран — разница в скорости была незначительной; VCL-решение обеспечивало тридцатипроцентное преимущество. Проверить мои результаты мож- но с помощью примеров LibSpeed и QlibSpeed. И еще один важный критерий выбора использования CLX вместо VCL заклю- чается в необходимости поддержки Unicode. CLX по умолчанию имеет в элемен- тах управления поддержку Unicode (даже на Win9x-плarфopмax, где он не под- держивается компанией Microsoft). A VCL имеет лишь незначительную поддержку Unicode даже в версиях ОС Windows, обеспечивающих его, что значительно ус- ложняет создание VCL-приложений для стран, в которых управление представле- нием локальных символов осуществляется гораздо легче, когда оно основывается Ий Unicode. 0199
200 Глава 5. Визуальные элементы управления Запуск под Linux Реальная проблемой выбора используемой библиотеки основывается на важнос- ти для вас и ваших пользователей возможности использования Linux или Unicode. Важно отметить, что при создании CLX-приложения его впоследствии можно бу- дет перекомпилировать в Kylix, не внося никаких дополнительных изменений (с тем же исходным программным кодом), создав «родное» Linux-приложение, если не выполнялось обращения к API ОС Windows, при котором обязательной явля- ется компиляция по условию (условная компиляция). В качестве примера я перекомпилировал упомянутый ранее пример QLibComp. Вот результат его работы (кроме того, здесь вы видите IDE среды Kylix в KDE) (рис. 5.2). Рис. 5.2. Приложение, написанное с использованием CLX, может быть непосредственно откомпилировано в Kylix под Linux Условная компиляция библиотек Если вы хотите иметь лишь один файл исходного программного кода, но планиру- ете компилировать его с VCL под Windows или с CXL под Linux, то для различе- ния двух вариантов в случае условной компиляции можно использовать харак- терные для платформы указатели (например, $1FDEF LINUX). Но что делать, если вы захотите иметь возможность компилировать часть кода для обеих библиотек под Windows? Вы можете либо определить собственные указатели, либо использовать услов- ную компиляцию, либо (временами) проверять наличие идентификаторов, кото- рые существуют лишь в CLX или в VCL: пгг Dpclared(OForms)} 0200
VCL no сравнению c VisualCLX 201 ...характерный для CLX код {$IFEND} Конвертирование существующих приложений Помимо создания новых CLX-приложений может возникнуть необходимость адап- тировать существующие VCL-приложения к новой библиотеке классов. Для этого необходимо самостоятельно, без какой-либо помощи со стороны IDE среды Delphi, выполнить ряд операций: о изменить расширение DFM-файла на XFM и заменить все выражения {$ R *. DFM} на {$R*.XFM}; о обновить в программе (в модулях и файле проекта) все выражения uses: вместо VCL они должны ссылаться на CLX-модули. Если будет пропущено хотя бы несколько ссылок, при запуске приложения вы столкнетесь с множеством про- блем; ПРИМЕЧАНИЕ----------------------------------------------------------- Для предотвращения компиляции CLX-приложения, содержащего ссылки на VCL-модули, последние можно переместить в подкаталог каталога lib, и они не попадут в путь поиска. Это приведет лишь к появлению ошибки «Unit not found». В табл. 5.1 представлено сравнение имен «визуальных» модулей VCL и CLX (сюда не попали модули, предназначенные для работы с базами данных и редко используемые модули). Таблица 5.1. Имена модулей-эквивалентов VCL и CLX VCL CLX ActnList Buttons Clipbrd ComCtrls Con sts Controls Dialogs ExtCtrls Forms Graphics Grids ImgList Menus Printers Search StdCtrls QActnList QButtons QClipbrd QComCtrls QConsts QControls QDialogs QExtCtrls QForms QGraphics QGrids QlmgList QMenus QPrinters QSearch QStdCtrls Вы также должны преобразовать ссылки на модули Windows и Messages в ссыл- ки на модуль Qt. Некоторые структуры данных ОС Windows теперь перемещены в модуль Types (см. главу 3), поэтому его необходимо добавить в CLX-программы. 0201
202 Глава 5. Визуальные элементы управления Обратите внимание, что модуль QTypes не является CLX-версией модуля Types биб- лиотеки VCL; они совершенно не связаны. ВНИМАНИЕ --------------------------------------------------------------- Внимательно просмотрите все выражения uses1 Если откомпилировать проект, содержащий CLX- форму, без обновления текста файла проекта, оставив ссылку на модуль Forms библиотеки VCL, то ваша программа запустится, но тут же остановится. Поскольку невозможно будет создать VCL-фор- му, программа сразу будет завершена. В других случаях попытка создать CLX-форму в VCL-прило- жении вызовет ошибки времени выполнения. И, наконец, IDE Delphi может добавить неверную ссылку на библиотеку в выражениях uses; в этом случае вы получите одно выражение uses, которое для обеих библиотек ссылается на тот же самый модуль, но только вторая из них будет действи- тельной. Это практически не защищает программу от компиляции, но впоследствии вы не сможете запустить ее. В качестве пособия по конвертированию я предлагаю несколько собственных программ. Я написал простое средство «замены» модулей, назвав его VclToClx. Под- робности об этой программе можно найти в приложении А. TControl и производные классы В главе 4 мы рассмотрели базовые классы библиотеки Delphi, особенно остано- вившись на классе TComponent. Одним наиболее важным подклассом этого класса является TControl, который относится к визуальным компонентам. Этот базовый класс доступен как в CLX, так и в VCL. Он определяет общие концепции, такие как местоположение и размер элемента управления, родительский элемент управ- ления, на котором он размещен, и т. д. Хотя для действительной реализации необ- ходимо сослаться на два подкласса. В VCL ими являются TWinControl и TGraphic- Control; в CLX — TWidgetControl и TGraphicControl. Вот их ключевые особенности: Элементы управления на основе Windows («оконные» элементы) Визуальные компоненты, основанные на использовании окна операционной сис- темы. В VCL TWinControl имеет «оконный» обработчик, являющийся числом, ссыла- ющимся на внутреннюю структуру Windows. В CLX TWidgetControl имеет Qt-обра- ботчик, который ссылается на внутренний Qt-объект. С точки зрения пользователя «оконные» элементы управления могут получать фокус, а некоторые из них также могут содержать другие элементы управления. Это большая группа компонентов библиотеки Delphi. Далее элементы управления можно разделить на две группы: оболочки собственных элементов управления Windows или Qt, и пользователь- ские элементы управления, которые, как правило, унаследованы от TCustomControL Графические элементы управления («неоконные» элементы) Визуальные компоненты, не основанные на использовании окна операционной системы. Следовательно, эти компоненты не имеют обработчика, не могут полу- чать фокус и не могут содержать другие элементы управления. Они унаследованы от TGraphicControl и «прорисовываются» элементом-предком, посылающим собы- тия, связанные с мышью, и прочие события. Примерами «неоконных» элементов являются компоненты Label и SpeedButton. К этой группе относится лишь несколь- ко компонентов, которые были критичны для минимизации использования сис- 0202
TControl и производные классы 203 темных ресурсов на ранних этапах существования Delphi (в 16-разрядной версии Windows). Использование графических элементов управления для сохранения ресурсов Windows по-прежнему полезно в ОС Win9x/Me, которые снизили сис- темные ограничения, но не полностью избавились от них (в отличие от Windows NT/2000). Parent и элементы управления Свойство Parent элемента управления указывает, какой иной элемент управления отвечает за вывод первого. Если вы поместили компонент на форме в конструкто- ре форм (Form Designer), именно эта форма станет как владельцем (owner), так и предком (parent) нового элемента управления. Но если вы поместите компонент на элемент Panel, ScrollBox или любой другой компонент-контейнер, то именно он станет его предком, даже если его владельцем по-прежнему будет форма. При создании элемента управления в ходе выполнения обязательно необходи- мо определить владельца (в качестве параметра конструктора Create) и установить свойство Parent. В противном случае этот элемент управления останется невиди- мым. Как и свойство Owner, свойство Parent является инверсным. Массив Controls со- держит список всех элементов управления, для которых текущий элемент являет- ся предком. Они нумеруются от 0 до Controlcount - 1. Для работы со всеми элемен- тами, размещенными на текущем элементе управления, можно сканировать это свойство, используя рекурсивный метод. Свойства положения и размера Некоторые свойства, определенные в TControl, являются общими для всех элемен- тов управления и относятся к их размеру и положению. Положение элемента уп- равления определяется свойствами Left и Тор, а его размер — свойствами Height и Width. Технически все компоненты имеют определенное местоположение, по- скольку в ходе разработки вы повторно открываете существующую форму и хоти- те видеть значки невизуальных компонентов именно там, где вы их поместили. Это местоположение хранится в файле формы. ПРИМЕЧАНИЕ------------------------------------------------------- При изменении любого из свойств положения или размера вам необходимо однократно вызвать метод SetBounds. Поэтому каждый раз, когда одновременно необходимо изменить два и более из этих свойств, непосредственный вызов SetBounds ускорит выполнение программы. Другой метод, BoundsRect, возвращает прямоугольник, связанный с элементом управления, соответствующий зна- чениям свойств Left, Top, Height и Width. Важность положения компонента заключается в том, что как и любые коорди- наты, они определяются относительно клиентской области родительского компо- нента (свойство Parent). Для формы клиентской областью является поверхность в пределах ее границ и верхнего заголовка (исключая сами границы). Было бы очень Неудобно работать с координатами экрана, хотя некоторые готовые к использова- нию методы осуществляют преобразование координат формы в координаты экра- на и наоборот. 0203
204 Глава 5. Визуальные элементы управления Обратите внимание, что координаты элементов управления всегда связаны с родительским элементом управления, например, формой или другим компонен- том-контейнером. При помещении на форму панели, а затем кнопки на эту панель, координаты кнопки будут выражены относительно панели, а не формы, содержа- щей панель. В этом случае родительским компонентом кнопки является панель. Свойства Activation и Visibility Для активизации и изменения видимости компонента могут использоваться два основных свойства. Простейшим является свойство Enabled. Если компонент за- прещен (когда Enabled установлен в False), то, как правило, это состояние будет указано пользователю визуально. Во время разработки свойство «запрещен» не всегда оказывает какой-либо визуальный эффект, но в ходе выполнения «запре- щенные» компоненты (недоступные) выводятся серым цветом. Для более радикального подхода можно полностью спрятать компонент либо с помощью соответствующего метода Hide, либо установив свойство Visible в False. Однако необходимо знать, что прочтение значения свойства Visible не сможет точ- но сказать, действительно ли компонент виден. Если контейнер компонента неви- ден, то даже если свойство Visible компонента установлено в True, вы не сможете увидеть его. По этой причине для определения действительной видимости для пользователя компонента необходимо опрашивать значение доступного в ходе выполнения, имеющего атрибут только для чтения свойства Showing, то есть про- верять видимость всех его предков. Шрифты Свойства Color и Font довольно часто используются для настройки пользователь- ского интерфейса компонента. С цветом связан ряд свойств. Само свойство Color обычно относится к цвету фона компонента. Кроме того, существуют свойства Color для шрифтов и других графических элементов. Многие компоненты также имеют свойства ParentColor и ParentFont, указывающие, будет ли элемент управления ис- пользовать те же цвета, что и родительский компонент, которым обычно является форма. Эти свойства можно использовать для изменения шрифта каждого элемента на форме, устанавливая только свойство Font самой формы. При установке шрифта либо вводом значения атрибутов свойства в окне инс- пектора объектов, либо с помощью стандартного диалогового окна выбора шриф- тов можно выбрать один из установленных в системе шрифтов. Тот факт, что Delphi дает возможность выбрать любой из шрифтов, установленных в системе, имеет как преимущества, так и недостатки. Основным преимуществом является возмож- ность выбора любого из «красивых» шрифтов. А недостатком — возможное отсут- ствие этого шрифта на компьютере заказчика, на который производится установ- ка программы. Если программа использует недоступный шрифт, Windows выберет какой-ни- будь подходящий шрифт. Аккуратно отформатированный вывод программы мо- жет быть полностью нарушен в результате замены шрифта, поэтому необходимо полагаться только на стандартные шрифты ОС Windows (MS Sans Serif, System, Arial, Times New Roman и т. д.). 0204
TControl и производные классы 205 Цвета Существуют различные способы установки значения цвета. Тип этого свойства — TColor не является типом класса, он является целым типом. Для свойств этого типа значение можно выбрать из предопределенного списка именованных констант или непосредственным вводом значения. К константам цвета относятся dBlue, dSilver, dWhite, dGreen, dRed и множество других (включая появившиеся в Delphi 6 dMoney- Green, clSkyBlue, clCream и clMedGray). В качестве альтернативы для указания состоя- ния элементов можно использовать один из цветов, используемых системой. На- боры цветов в VCL и CLX различны. VCL включает предопределенные цвета Windows, такие как фоновый цвет окна (clWindow), цвет текста выделенного пункта меню (clHighlightText), цвет заголовка активного окна (clActiveCaption) и цвет кнопки (clBtnFace). CLX включает и другой несовместимый набор системных цветов, включая clBackground, который является стандартным цветом формы; dBase, используемый строками редактирования и другими визуальными элементами управления, dActiveForeground — основной цвет активных элементов управления, и dDisabledBase — цвет фона для «запрещенных» (отключенных) текстовых элементов управления. Все упоминаемые здесь цвето- вые константы можно найти в справочной системе VCL и CLX в разделе «TColor type». Другой особенностью является возможность вместо предопределенных значе- ний указать значение TColor в виде числа (4-байтное шестнадцатеричное значение). При этом младшие три байта представляют интенсивность RGB-цвета для синего, зеленого и красного цветов соответственно. Например, значение SOOFFOOOO соот- ветствует чистому синему цвету, $OOOOFFOO — зеленому, $OOOOOOFF — красному, $00000000 — черному, a S00FFFFFF — белому. Указанием промежуточных значе- ний можно получить до 16 миллионов оттенков. Вместо непосредственного указания числового значения можно воспользовать- ся функцией RGB ОС Windows, имеющей три параметра (в диапазоне 0..255). Пер- вый указывает количество красного цвета, второй — зеленого, а последний — синего. Использование функции RGB с тремя параметрами вместо одной шестнадцатерич- ной константы делает восприятие программы более простым. RGB — это функция почти Windows API, она определена в относящемся к Windows модуле и не явля- ется функцией Delphi, но такой функции нет в Windows API. Существует макрос языка С, имеющий такое же имя и предназначение; приветствуется его включение в модуль Windows. В CLX нет функции RGB, поэтому я написал собственную: function RGB (red. green, blue Byte) Cardinal. begin Result - blue + green * 256 + red * 256 * 256. end; Старший байт типа TColor указывает палитру, которая будет использована для поиска ближайшего подходящего цвета, но палитры — это отдельный вопрос, ко- торый здесь мы рассматривать не будем. (Специализированные программы обра- ботки изображений также используют этот байт для представления сведений о прозрачности каждого выводимого на экран элемента.) Обратите внимание, что независимо от совпадения палитры и цветов Windows иногда заменяет произвольный цвет ближайшим доступным цветом, по крайней 0205
206 Глава 5. Визуальные элементы управления мере в видеорежимах, использующих палитры. Это всегда имеет место при работе с шрифтами, линиями и т. д. В других случая Windows использует технологию сглаживания (dithering) для имитации требуемого цвета посредством сжатой мас- ки пикселов доступных цветов. При использовании 16-цветных (VGA) адаптеров или при высоком разрешении вместо ожидаемого цвета могут проявиться стран- ные узоры из пикселов различных цветов. Класс TWinControl (VCL) Большинство элементов пользовательского интерфейса в Windows представлены окнами. С точки зрения пользователя окно — это часть экрана, окруженная грани- цами, имеющая заголовок и системное меню. Но с технической точки зрения окно — это составляющая внутренней системной таблицы (зачастую соответствующая элементу, видимому на экране), имеющая соответствующий код. Большинство из окон играют роль элемента управления; другие являются временными, создавае- мыми системой (например, раскрывающиеся контекстные меню). Кроме того, имеются создаваемые приложениями окна, которые остаются скрытыми от пользо- вателя и используются только как средство получения сообщений (например, не- блокируемые сокеты используют окна для связи с системой). Общим для всех окон является тот факт, что они стали известны благодаря ОС Windows, и их поведение определяется функциями; каждый раз при происхожде- нии в системе какого-либо события соответствующему окну посылается уведоми- тельное сообщение, которое откликается выполнением определенного программного кода. Каждое окно системы имеет связанную с ним функцию (обычно называе- мую процедурой окна), которая обрабатывает различные сообщения, поступающие в это окно. В Delphi любой класс TWinControl может заменить (override) метод WndProc или определить новое значение свойства WindowProc. Однако интересные сообщения Windows лучше отслеживать с помощью специального обработчика сообщений. А еще лучше дать VCL преобразовать сообщения низшего уровня в события. Ко- роче говоря, Delphi позволяет работать на высоком уровне (что упрощает разра- ботку приложений), но при необходимости позволяет перейти на низший уровень. Обратите также внимание, что создание экземпляра класса на основе TWi nControl автоматически создает соответствующий обработчик. Delphi использует техноло- гию «ленивой» инициализации: элементы управления низкого уровня создаются только по мере необходимости, как правило, при обращении метода к свойству Handle. Метод Get этого свойства вызывает HandleNeeded, который вызывает Create- Handle и т. д., в конечном счете доходя до CreateWnd, CreateParams и CreateWindowHandle (эта последовательность сложная; нет необходимости знать ее более подробно). С другой стороны, можно оставить существующий (возможно, невидимый) эле- мент управления в памяти, но для сбережения системных ресурсов уничтожить его оконный обработчик. Класс TWidgetControl (CLX) В CLX каждый класс TWidgetControl имеет внутренний Qt-объект, ссылка на кото- рый осуществляется с помощью свойства Handle. Это свойство имеет такое же имя, как и соответствующее свойство Windows, но они полностью отличаются. 0206
Набор компонентов 207 TWidgetControl обычно владеет соответствующим Qt/C++-o6beicTOM. Класс CLX использует отложенное создание (внутренний объект не создается до тех пор, пока не потребуется). Класс CLX освобождает внутренний объект сразу после его унич- тожения. Однако существует возможность создать элемент управления окном вок- руг существующего Qt-объекта: в этом случае CLX-объект не будет владеть Qt- объектом и не сможет уничтожить его. Это поведение определяется свойством OwnHandle. Если быть более точным, каждый VisualCLX-компонент имеет два связанных с ним объекта C++: Qt Handle и Qt Hook, которые получают системные события. При существующей структуре Qt Hook должен быть С++-объектом, который в от- ношении обработчиков событий элементов управления Delphi действует как по- средник. Метод HookEvents связывает объект hook с элементом управления CLX. В отличие от Windows, библиотека Qt определяет два различных типа со- бытий: О события — это преобразование ввода пользователя или событий системы (на- жатие клавиши, движение мыши, раскраска); О сигналы — внутренние события компонента (соответствующие внутренним или абстрактным операциям VCL (например, OnClick и OnChange). Однако события CLX-компонента осуществляют слияние событий и сигналов. К характерным для элемента управления CLX событиям относятся OnMouseDown, OnMouseMove, OnKeyDown, OnChange, OnPaint и многие другие, точно также, какв VCL (в которой большинство событий запускается сообщениями Windows). СОВЕТ ------------------------------------------------------------------ Опытные программисты могут заметить, что CLX имеет редко используемый метод EventHandler, более-менее соответствующий методу WndProc VCL-типа TWinControl. Набор компонентов Итак, вы хотите написать Delphi-приложение. Вы открываете новый Delphi-npo- ект и сталкиваетесь с большим выбором компонентов. Проблема состоит в том, что для каждой операции существует множество альтернативных вариантов вы- полнения. Например, список значений можно представить с помощью элементов: список, поле со списком, группа переключателей, таблица строк или даже дерева, если значения связаны иерархическим порядком. Что лучше использовать? Сложно сказать. Есть много соображений в зависимости от того, что, в конечном итоге, вы хотите получить от приложения. На основании этого я предусмотрел сконцентри- рованную сводку альтернативных параметров для некоторых общих задач. СОВЕТ---------------------------------------------------—----- Для некоторых элементов управления, описанных в последующих разделах, в Delphi имеется точно такая же версия элемента, но предназначенная для работы с базами данных, имеющая приставку DB. Как вы увидите в главе 13, DB-версия элемента управления обычно выполняет ту же роль, что и «стандартный» эквивалент, но свойства и способы его использования весьма различны. Напри- мер, в элементе «строка редактирования» (Edit) используется свойство Text, в то время как в DBEdit происходит обращение к Value (значению) поля связанного объекта. 0207
208 Глава 5. Визуальные элементы управления Компоненты ввода текста Хотя форма или компонент могут непосредственно обрабатывать ввод клавиату- ры с помощью события On KeyPress, так обычно не делается. Windows предоставля- ет готовые к использованию элементы управления, которые могут использоваться для получения строки ввода и даже построения простого текстового редактора. В этой области Delphi имеет ряд несколько отличающихся компонентов. Компонент Edit Компонент Edit (строка редактирования) позволяет пользователю вводить одну строку текста. Вывод одной строки также может осуществляться с помощью эле- ментов управления Label или StaticText, однако эти компоненты обычно исполь- зуются только для представления фиксированного текста или генерированного программой вывода, но не для ввода. В CLX есть также собственный элемент уп- равления LCD digit, который может использоваться для вывода чисел. Для ссылки на представляемый текст компонент Edit использует свойство Text, в то время когда другие элементы — свойство Caption. Единственное условие, нала- гаемое на ввод пользователя, — количество символов. Если необходимо обязать пользователя использовать только допустимые символы, то можно обработать со- бытие OnKeyPress строки редактирования. Например, можно написать метод, кото- рый проверяет, является ли символ числом или клавишей Backspace (имеющей числовое значение 8). Если нет, то заменяете значение клавиши на нулевой сим- вол (#0) для того, чтобы он не обрабатывался элементом Edit, и вывести предупре- дительный звуковой сигнал: procedure TForml.EditlKeyPressl Sender: TObject, var Key Char). begin // проверить, является пи клавиша цифровой или клавишей backspace if not (Key in ['O'. '9'. #8]) then begin Key := #0: Beep: end: end: СОВЕТ-------------------------------------------------------------------- Незначительным отличием CLX является то, что элемент управления Edit не имеет встроенного механизма Undo. Еще одним отличием является то, что свойство PasswordChar заменено свойством EchoMode. Не производится определение выводимого символа, но вместо этого будет либо продуб- лирован вводимый текст, либо выводиться символ «звездочка» (*). Элемент управления LabeledEdit В Delphi 6 появился прекрасный элемент управления LabeledEdit, который явля- ется элементом Edit с присоединенным к нему элементом Label (Надпись). Эта надпись представлена как свойство составного элемента, производного от TCustom- Edit. Этот компонент очень удобен, поскольку он позволяет сократить число ком- понентов на форме, сближая их и обеспечивая более последовательное размеще- ние всех надписей приложения. Свойство EditLabel имеет подкомпонент, имею- 0208
Набор компонентов 209 щий обычные свойства и события. Еще два свойства — LabelPosition и LabelSpacing — позволяют настраивать относительное положение двух элементов. СОВЕТ------------------------------------------------------------------------------ Этот компонент был добавлен в модуль ExtCtrls для демонстрации использования подкомпонентов в инспекторе объектов. Мы рассмотрим разработку этих компонентов в главе 9. Обратите также внимание, что эти компоненты не доступны в CLX. Компонент MaskEdit Для дальнейшей настройки ввода в строку редактирования можно использовать компонент MaskEdit. Он имеет свойство EditMask, являющееся срочным значением для каждого символа, определяющим, что он должен быть либо строчной, либо прописной буквой, либо числом, а также прочие ограничения. Для настройки это- го свойства может использоваться редактор: Input Mask Editor - ItyttMatk ,|гдачо»ободТ OurtifB glares: S' $<w»Litei»rCh«act«s Extension Social Security Short Zp Code LongZp Code Date Long Time Short Time 15450 555-55 5555 90504 90504-0000 06/27/94 09 0515PM 1345 f I Саляй' H**? - j Этот редактор позволяет не только вводить маску, но попросит указать сим- вол-«заполнитель», а также решить, будут ли сохраняться существующие в маске литералы в окончательной строке. Например, вы можете указать использование круглых скобок для кода города телефонного номера либо в качестве подсказки, либо сохранить их в строку, содержащую окончательный номер. Эти два пункта в Input Mask Editor соответствуют двум последним полям маски (разделенным точ- кой с запятой). ПРИМЕЧАНИЕ--------------------------------------------------------------- Щелчок на кнопке Masks позволяет выбрать предопределенные маски ввода для различных стран. Компоненты Мето и Rich Ed it Рассмотренные до этого элементы работали с одной строкой. В отличие от них компонент МЕМО может содержать несколько строк текста, но (на платформах Win95/98) по-прежнему имеет ограничение 16-разрядного текста Windows (32 кБайт) и позволяет иметь на весь текст лишь один шрифт. Текст можно обра- батывать строка за строкой (с помощь списка строк Lines) или обращаться одно- временно ко всему тексту (с помощью свойства Text). Если требуется работать с большим текстом, иметь возможность изменять Шрифт и выравнивать абзацы, то необходимо использовать VCL-компонент Rich- Edit — стандартный элемент Win32, основанный на использовании RTF-формата. Лример полноценного редактора, использующего компонент Rich Edit, можно найти 0209
210 Глава 5. Визуальные элементы управления в составе программ, поставляемых вместе с Delphi. (Этот пример так и называется: RichEdit.) ВНИМАНИЕ --------------------------------------------------------------- Элемент RichEdit является одним из распространенных элементов Delphi, который недоступен в CXL и Kylix. Самая последняя версия Qt имеет собственный аналогичный элемент, поэтому эти элемен- ты, возможно, будут поддерживаться в последующих версиях CLX. Компонент RichEdit имеет свойство DefAttributes, указывающее заданные по умол- чанию стили, и свойство SelAttributes, указывающее текущие настройки стиля. Эти два свойства не имеют типа TFont, но они совместимы с шрифтами, поэтому для копирования их значения можно использовать метод Assign: procedure TForml ButtonlClick(Sender: TObject); begin if RichEditl.SeiLength > 0 then begin FontDialogl.Font.Assign (RichEditl.DefAttributes); if FontDialogl.Execute then RichEditl.Sei Attributes.Assign (FontDialogl.Font); end; end; Элемент управления Textviewer (CLX) В CLX и Qt нет RichEdit, но они предоставляют полнофункциональную возмож- ность просмотра HTML без его редактирования. Средство просмотра HTML встро- ено в два элемента управления: одностраничный элемент TextViewer и элемент TextBrowser с активными ссылками. iA’HtmtEdit <hl>Test Html</h1> <p>Testtext<b>*wthbdd</b><br> \ f-. <p>andmofetestonanewlr«, folowed by a table <p> / <tabie bocder-1> * < tr> < td> cell 1 < /td> < td> cel b< Ad> < td> cek Ad> < td> cek Ad> < /1 r> J. <trxtd>cell 2<AdXtd>cel b<AdXtd>cekAdXtd>cel<AdxAf> '‘ <trxtd>ce!3<Ad><td>cel b<AdXtd>cekAdxtd>cel<AdxAf> ' < tr> < td> cel 4<Ad> < td> cel tx Adx td> cek Ad> <td> cek Ad> < Лг> ' <trxtd>ce!5<Adxtd>celb<Adxtd>cekAdxtd>finalcelkAdx/h> </table> , J <p>and Inaly a "deed" hyperlink, <a hcel»"http //www marcocantu com"> marcocantu com </a> Test Html Test text with ЬоИ and more test on a new ine, folowed by a table Рис. 5.3. Пример HtmlEdit: во время выполнения при вводе HTML-текста вы тут же получаете предварительный просмотр 0210
Набор компонентов 211 В качестве простого примера я добавил МЕМО и TextBrowser в CLX-форму и связал их таким образом, что все вводимое в МЕМО тут же выводилось в режи- ме просмотра. Этот пример я назвал HtmlEdit не потому, что он является редакто- ром HTML, а потому, что это самый простой известный мне способ предваритель- ного просмотра HTML непосредственно в программе (рис. 5.3). ВНИМАНИЕ------------------------------------------------------------------ Изначально этот пример построен с помощью Kylix в Linux. Для его перемещения на Windows и Delphi потребовалось лишь скопировать файлы и повторно откомпилировать. Выбор параметров Два стандартных элемента управления Windows позволяют пользователю выбрать различные параметры. Еще два элемента — сгруппировать наборы параметров. Компоненты CheckBox и RadioButton Первый стандартный элемент выбора вариантов — check box (флажок), который соответствует одному параметру, выбираемому независимо от состояния других флажков. Установка свойства AUowGrayed переключателя позволяет отображать три различных состояния (выбранный, невыбранный и недоступный), которые изме- няются при щелчке пользователя на переключателе. Второй тип элемента управления — radio button (переключатель), который со- ответствует исключительному выбору. Два переключателя на одной форме или внутри одного контейнера «группа переключателей» не могут быть выбраны од- новременно: один из них всегда должен быть выбран (во время разработки вы, как программист, отвечаете за выбор одного из переключателей). Компонент GroupBox Для помещения нескольких переключателей может использоваться элемент уп- равления GroupBox (группирующий контейнер), соединяющий их функционально и визуально. Для создания группы переключателей просто поместите на форме компонент GroupBox и затем добавьте в него переключатели: Обработка переключателей может выполняться индивидуально, но проще пе- ремещаться по массиву элементов (см. главу 4), владельцем которого является GroupBox. Вот небольшой фрагмент, используемый для получения текста группы, выбранной переключателем: var I: Integer: Text: string: 0211
212 Глава 5. Визуальные элементы управления for I .= О to GroupBoxl.Controlcount - 1 do if (GroupBoxl.Controls[I] as TRadioButton).Checked then Text := TRadioButton(GroupBoxl.Controls[I]) Caption: Компонент RadioGroup Delphi имеет подобный компонент, который специально предназначен для пере- ключателей: компонент RadioGroup (группа переключателей). RadioGroup — это груп- па с уже размещенными в ней переключателями. Разница заключатся в том, что внутренние переключатели автоматически управляются контейнером. Использо- вание группы переключателей обычно проще, чем использование группирующего контейнера, поскольку различные пункты являются частью списка. Вот как мож- но получить текст выбранного пункта: Text := RadioGroupl Items [RadioGroupl.Itemindex]: Еще одним преимуществом компонента RadioGroup является возможность ав- томатического выравнивания переключателей в одной и более колонках (в соот- ветствии со значением свойства Columns), а также возможность добавления новых вариантов в ходе выполнения путем добавления строк в список Items. Добавление новых флажков в группирующий контейнер будет крайне сложным. Списки Когда имеется множество возможных вариантов выбора, переключатели уже не подойдут. Во избежание загромождения пользовательского интерфейса, как пра- вило, допустимым является использование 5-6 переключателей. Когда требуется выбрать из большего числа вариантов, необходимо использовать список или дру- гие элементы, выводящие списки пунктов и позволяющие пользователю выбрать один из них. Компонент ListBox Выбор пункта в списке с использованием свойств Items и Itemindex представлено при рассмотрении элемента RadioGroup. Если вам требуется обращаться к тексту выбранного пункта списка, можно написать небольшую функцию-оболочку: function SelText (List- TListBox): string: var nltem: Integer: begin nltem := List.Itemindex: if nltem >= 0 then Result := List Items [nltem] el se Result := '’: end: Другая важная особенность заключается в том, что при использовании компо- нента ListBox (список) имеется возможность разрешить выбор отдельного пункта (как в группе переключателей) или выбор множества пунктов (как в группе флаж- ков). Это определяется свойством МuItiSelect. В Windows и Delphi допускается два типа вариантов: многократное выделение (multiple selection) и расширенное выделе- ние (extended selection). В первом случае пользователь выбирает множество вари- антов. последовательно щелкая на них. Во втором случае пользователь может ис- 0212
Набор компонентов 213 пользовать клавиши Shift и Ctrl для выделения множества последовательных и не- последовательных пунктов соответственно. Два альтернативных режима опреде- ляются состоянием свойства ExtendedSetect. Для списка, допускающего множество выделений, программа с помощью свой- ства SetCount может получать информацию о числе выбранных пунктов. А сами пункты — посредством массива Selected. Это массив логических значений, имею- щих номера, соответствующие пунктам списка. Например, для слияния всех выб- ранных пунктов в одну строку можно просканировать массив Selected следующим образом: var Sei Items: string; nltem: Integer; begin Sei Items for nltem := 0 to ListBoxl.Items.Count - 1 do if ListBoxl Selected [nltem] then Sei Items := Sei Items + ListBoxl.Iterns[nltem] + ' '. В отличие от VCL, в CLX можно настроить ListBox на использование фиксиро- ванного числа строк и столбцов с помощью свойств Columns, Row, ColumnLayout и RowLayout. Из них в ListBox библиотеки VCL имеется только свойство Columns. Компонент ComboBox Списки занимают много места на экране и предлагают фиксированный набор зна- чений, т. е. пользователь может только выбрать из них существующий пункт и не может ввести какое-либо значение, не указанное ранее программистом. Для решения этих проблем можно использовать элемент управления ComboBox (комбинированный список), который сочетает в себе строку редактирования и рас- крывающийся список. Поведение ComboBox в основном определяется значением его свойства Style: О стиль csDropDown определяет типичное поле со списком, которое допускает не- посредственное редактирование и по запросу отображает список; О стиль csDropDownList определяет поле со списком, которое не допускает редак- тирования (но использует сочетание клавиш для выбора пункта); О стиль csSimple определяет поле со списком, которое всегда выводит список. Обратите внимание, что обращение к тексту выбранного значения ComboBox проще, чем в списке, за счет использования свойства Text. Полезная и известная Уловка комбинированного списка заключается в добавлении нового элемента в общий список после ввода текста и нажатия клавиши Enter. Представленный ниже метод сначала проверяет, нажал ли пользователь эту клавишу (ожидая символ с Числовым значением (ASCII) 13). После этого он проверяет, что текст в списке Не пустой и не существует в списке (его позиция в списке менее 0): B^oeedure TForml.ComboBoxlKeyPress! * Sender: TObject: var Key Char): eegin если пользователь нажал Enter 'iT Key = Chr (13) then with Sender as TComboBox do 0213
214 Глава 5. Визуальные элементы управления If (Text <> ") and (Items.IndexOf (Text) < 0) then Items Add (Text); end; ПРИМЕЧАНИЕ--------------------------------------------------------------------- В CLX комбинированный список может автоматически добавлять введенный в строке редактирова- ния текст в раскрывающийся список после нажатия пользователем клавиши Enter. Кроме того, не- которые события запускаются несколько по-другому. Начиная с Delphi 6, имеются два новых события комбинированного списка. Событие On Closed р соответствует закрытию раскрывающегося списка и логически дополняет ранее существовавшее событие OnDropDown. Событие OnSelect запуска- ется только тогда, когда пользователь выберет что-либо в раскрывающемся спис- ке, а не введет. Еще одной приятной особенностью является свойство AutoComplete. При его установке компонент ComboBox (а также ListBox) автоматически находит строку, наиболее соответствующую вводимому пользователем, предлагая заключительную часть текста. Ядро этой функции также доступно в CLX и реализовано методом TCustomListBox.KeyPress. Компонент CheckListBox Другое расширение элемента-списка представлено компонентом CheckListBox — списком, в котором каждый пункт имеет флажок: Пользователь может выбрать в списке отдельный пункт, а может щелкнуть на флажке для изменения его состояния. Это делает компонент CheckListBox очень удобным для многократных выделений или для выделения состояния независи- мых пунктов (в виде ряда флажков). Для проверки текущего состояния каждого пункта можно использовать свой- ства-массивы Checked и State (последний используется, если флажок имеет три со- стояния). В Delphi 5 появилось свойство Item Enabled, которое может использовать разрешения или запрещения каждого пункта списка. Вы можете использовать про- цедуру CheckListBox примера DragList, представленную далее в этой главе. ПРИМЕЧАНИЕ-------------------------------------------------------------- Большинство элементов-списков поддерживают общую и важную особенность: каждый пункт спис- ка связан с 32-разрядным значением, на что указывает тип TObject. Это значение может использо- ваться для каждого пункта как ярлык и очень полезно для хранения дополнительной информации о каждом пункте. Этот подход связан с отличительной чертой собственного элемента-списка Windows, предоставляющего 4 дополнительных байта для каждого пункта списка. Использование этой осо- бенности будет рассмотрено далее в этой главе (пример ODList). 0214
Набор компонентов 215 Расширенные комбинированные списки: ComboBoxEx и ColorBox ComboBoxEx (где Ex — сокращение от extended (расширенный)) является обо- лочкой нового элемента управления Win32, позволяющей рядом с пунктом спис- ка выводить изображение. Необходимо к комбинированному списку присоединить список изображений, а затем для каждого пункта выбрать индекс изображения. Эффект от этого изменения заключается в том, что простой список строк Items заменяется более сложной коллекцией — свойством ItemsEx. Элемент ComboBoxEx используется в примере RefList2, рассматриваемом в главе 7. ПРИМЕЧАНИЕ---------------------------------------------------- В Delphi 7 компонент ComboBoxEx имеет новое свойство AutoCompleteOptions, позволяющее комби- нированному списку реагировать на нажатие сочетаний клавиш. ColorBox — это версия комбинированного списка, специально предназначенная для выбора цветов. Для выбора группы представляемых цветов (стандартные, рас- ширенные, системные цвета и т. д.) можно использовать свойство Style. Компоненты Listview и TreeView Если вам требуется более сложный список, можно использовать обобщающий эле- мент управления ListView, который позволит сделать интерфейс приложения очень современным. Этот компонент более сложен для использования. Другими вариан- тами представления списка значений является обобщающий элемент управления TreeView, который представляет пункты в иерархической привязке, и элемент уп- равления StringGrid, который представляет множество элементов в одной строке. Ссылки на фактические примеры использования этих компонентов можно найти в Приложении В. При использовании общих элементов управления в приложении пользователи будут знать, как взаимодействовать с ними, и они расценят пользовательский ин- терфейс вашей программы как современный. TreeView и ListView — два ключевых компонента Проводника Windows, который знаком многим пользователям даже больше, чем традиционные элементы управления Windows. В CLX имеется эле- мент управления IconView, который практически аналогичен ListView библиотеки VCL. ВНИМАНИЕ----------------------------------------------------------- Элемент ListView библиотеки CLX не имеет вариантов стилей small/large icon (крупные значки/мел- кие значки), как его Windows-напарник, а компаньон IconView — имеет. Компонент ValueListEditor Delphi-приложения довольно часто используют структуру «имя-значение» изна- чально предлагаемые списками строк (см. главу 4). В Delphi 6 появилась версия Компонента StringGrid (технически — наследника класса TCustomDrawGrid), спе- циально связанная с этим списком строк. ValueListEditor имеет два столбца, в кото- рых можно вывести и позволить пользователю отредактировать содержание спис- ка строк с парами «имя-значение» (рис. 5.4). Список строк указывается в свойстве brings этого элемента управления. 0215
216 Глава 5. Визуальные элементы управления Рис. 5.4. Пример NameValues использует компонент ValueListEditor, который показывает пары «имя-значение» или «ключ-значение» списка строк, также представляемое в виде открытого МЕМО Мощность этого элемента управления определяется тем, что с помощью свой- ства-массива Item Props, используемого только во время выполнения, вы можете настраивать параметры редактирования каждой позиции сетки или для каждого ключевого значения. Для каждого пункта можно указать: О атрибут «только для чтения»; О максимальное количество символов; О маску редактирования (в конечном счете, требуемую событию OnGetEditMask); О пункты в раскрывающемся списке (в событии OnGetPickList), как продемонст- рировано в первом пункте примера; О вывод кнопки, открывающей диалоговое окно редактирования (в событии Оп- EditButtonClick). Нет необходимости говорить, что это поведение похоже на сетку строк элемен- та DBGrid и поведение инспектора объектов. Свойство Item Props должно устанавливаться во время выполнения, путем со- здания объекта класса TItem Prop и присвоения его индексу или ключу списка строк. Для того чтобы для каждой строки использовался стандартный редактор, можно присвоить один объект свойств пункта несколько раз. В этом примере совместно используемый редактор устанавливает маску редактирования: допускается до 3 чи- сел: procedure TForml.FormCreate(Sender: TObject); var г- integer: 0216
Набор компонентов 217 begin SharedltemProp := TItemProp Create (ValueListEditorl): SharedltemProp EditMask = '999:0: SharedltemProp.EditStyle = esEllipsis: FirstltemProp := TItemProp.Create (ValueListEditorl): for I .= 1 to 10 do FirstItemProp.PickList.AdddntToStr (I)): Memol Lines : = ValueListEditorl.Strings; ValueListEditorl ItemProps [0] •= FirstltemProp: for I = 1 to ValueListEditorl.Strings.Count - 1 do ValueListEditorl ItemProps [I] := SharedltemProp. end: Можно повторить этот же программный код в случае изменения количества строк, например, за счет добавления новых элементов в МЕМО и копирования их в список значений. procedure TForml.VaiueListEditorlStringsChange(Sender: TObject); var I: Integer: begin ValueListEditorl.ItemProps [0] := FirstltemProp: for I := 1 to ValueListEditorl Strings.Count - 1 do if not Assigned (ValueListEditorl ItemProps [I]) then ValueListEditorl.ItemProps [I] SharedltemProp; end; СОВЕТ-------------------------------------------------------------------------------------- Повторное присвоение того же редактора дважды может вызвать проблемы, поэтому я присвоил редактор только тем строкам, которые его еще не имеют. Другое свойство, KeyOptions, позволяет дать пользователю возможность редак- тирования ключей (или имен), добавлять пункты, удалять существующие и дуб- лировать имена в первой части строки. Достаточно странно, но нет возможности добавить новый ключ, пока не будет активизирован параметр редактирования, что усложняет предоставление пользователю возможности добавления пунктов, за- щищая имена базовых пунктов. Диапазоны И, наконец, вы можете использовать несколько компонентов для выбора значений из диапазона. Диапазоны могут использоваться для числового ввода и для выбора элемента из списка. Компонент ScrollBar Автономный компонент ScrollBar (полоса прокрутки) — оригинальный компонент этой группы, но сам по себе он используется редко. Полосы прокрутки обычно связаны с другими компонентами, например, списками или MEMO-полями, либо непосредственно с формами. В любом случае полоса прокрутки может рассматри- ваться как часть поверхности другого компонента. Например, форма с полосой Прокрутки является формой, имеющей область, подобную полосе прокрутки, 0217
218 Глава 5. Визуальные элементы управления нарисованной у ее границы; она управляется специальным стилем формы окна Windows. Под «подобной» я понимаю тот факт, что она технически не является отдельным окном типа ScrollBar. Эти «ложные» полосы прокрутки обычно управ- ляются в Delphi с помощью двух специальных свойств формы и другими поме- щенными на нее компонентами; это свойства VertScroLLBar и HorzScrollBar. Компоненты TrackBar и ProgressBar Компонент ScroIlBar редко используется явно, особенно по сравнению с компо- нентом TrackBar, появившимся в Windows 95, который используется для предос- тавления пользователю возможности выбрать значение в диапазоне. Среди общих компонентов Win32 существует и элемент управления ProgressBar, позволяющий программе вывести значение в диапазоне, показывая ход выполнения длительной операции. Эти два компонента представлены здесь: Компонент UpDown Еще одним «связанным» компонентом является компонент UpDown, который обыч- но подключен к строке редактирования, что позволяет пользователю как ввести значение вручную, так и увеличить/уменьшить его значение с помощью малень- ких кнопок со стрелками. Для объединения двух компонентов вы устанавливаете свойство Associate компонента UpDown. Ничего не препятствует использованию компонента UpDown как отдельного элемента управления, выводя текущее значе- ние в элемент «надпись» или другим образом. СОВЕТ------------------------------------------------------------ CLX не имеет элемента управления UpDown, но предлагает элемент SpinEdit, объединяющий Edit и UpDown в единый элемент управления. Компонент PageScroller Win32^eMeHT PageScroLLer — это контейнер, позволяющий выполнять прокрутку внутреннего элемента управления. Например, если поместить панель инструмен- тов в PageScroller и эта панель инструментов по размерам больше, чем доступная область, то PageScroller выведет сбоку две маленькие стрелки. Щелчки на этих стрел- ках приводят к прокрутке внутренней области. Этот компонент может использоваться как полоса прокрутки, но он также мо- жет частично заменить элемент ScrollBox. Компонент ScrollBox Элемент управления ScrollBox представляет область формы, которая может про- кручиваться независимо от остальной поверхности. По этой причине ScrollBox имеет две полосы прокрутки для перемещения встроенных компонентов. Также как и на „о Q^miiRnv мп-1-ил ппмастить лгпггие компоненты. На самом деле ScrollBox — 0218
Набор компонентов 219 это и есть панель с полосами прокрутки для перемещения ее внутренней поверх- ности — интерфейсный элемент, используемый во многих Windows-приложени- ях. Когда на вашей форме имеется много элементов управления, панель инстру- ментов и строка состояния, для центральной области формы можно использовать ScrollBox, оставив панель инструментов и строку состояния за пределами области прокрутки. Понадеявшись на полосы прокрутки формы, вы можете лишить пользо- вателя возможности видеть одновременно панель инструментов и строку состоя- ния (очень странная ситуация). Команды Последняя категория компонентов не так отчетлива, как предыдущие, и относит- ся она к командам. Базовый компонент этой группы — TButton (или на жаргоне Windows — «кнопка нажатия»). Более часто, чем отдельные кнопки, Delphi-npo- граммисты используют кнопки (объекты TToolButton) на панели инструментов (в ранних версиях Delphi они использовали кнопки быстрого доступа с панеля- ми). Помимо кнопок и аналогичных элементов управления другой ключевой ме- тодикой выполнения команд является использование пунктов меню, части рас- крывающегося меню, присоединенные к основному меню формы, или локальные контекстные меню, активизируемые правой кнопкой мыши. Команды, связанные с меню или панелью инструментов, делятся на катего- рии в зависимости от их предназначения и ответной реакции в отношении пользователя: О команды — пункты меню или кнопки, используемые для выполнения опреде- ленного действия; О «определители» состояния — пункты меню или кнопки, используемые для переключения параметра в положения включен/выключен, изменяющие состо- яние конкретного элемента. Пункты меню этих команд для указания активно- сти обычно имеют отметку в виде «галочки» (вы можете автоматически уп- равлять этим поведением с помощью свойства AutoCheck). Кнопки обычно выводятся как «нажатая» (элемент управления ToolButton имеет свойство Down); О переключатели — пункты меню, выводящие маркер абзаца (bullet) и группи- руются для представления различных вариантов, подобно переключателям. Для получения пунктов переключателей установите свойство RadioItem в True и ус- тановите свойство Grouplndex для альтернативных пунктов меню в то же самое значение. Подобным образом вы можете группировать взаимно исключающие кнопки панели инструментов. ° «открыватели диалоговых окон» — пункты, которые приводят к открытию диалоговых окон. Они обычно отмечаются многоточием (...) в конце текста. Команды и действия Как вы увидите в главе 6, современные Delphi-приложения для обработки меню и ^Оманд панелей инструментов стремятся использовать компонент ActionList или расширение ActionManager. В двух словах, вы определяете ряд объектов-дей- |№вий и связываете каждый из них с кнопкой панели инструментов и/или пунктом 0219
220 Глава 5. Визуальные элементы управления меню. Можно определить выполнение команды в одном месте и также обновить пользовательский интерфейс указанием действия; соответствующие визуальные элементы управления будут автоматически отображать состояние объекта-дей- ствия. Конструктор меню Если в вашем приложении необходимо показать простое меню, можно поместить на форму компонент MainMenu или PopupMenu и дважды щелкнуть на нем для за- пуска конструктора меню (Menu Designer) (рис. 5.5). С его помощью можно доба- вить новые пункты меню, задавая им свойство Caption, а с помощью символа «де- фис» (-) — разделять заголовки пунктов меню. Рис. 5.5. Конструктор меню в действии Для каждого добавляемого пункта меню Delphi создает компонент. Для имено- вания компонентов Delphi использует введенный вами заголовок и добавляет но- мер (так Орел становится Openl). После удаления пробелов и прочих специальных символов из заголовков, если больше ничего не осталось, Delphi в качестве имени берет букву N и к ней добавляет число. Таким образом, разделители пунктов меню называются N1,N2ht. д. Зная, что будет делать Delphi по умолчанию, вы сначала можете отредактировать имя, что просто необходимо, если вы хотите получить практичную схему именования компонентов. ВНИМАНИЕ-------------------------------------------------------------------- Не используйте свойство Break, применяемое для разбиения раскрывающегося меню на несколько столбцов. Значение mbMenuBarBreak указывает, что данный пункт будет выведен на второй или последующей строке; значение mbMenuBreak означает, что данный пункт будет добавлен ко второ- му или последующему столбцу раскрывающегося меню. Для получения меню, имеющего современный внешний вид, вы можете доба- вить в программу элемент управления ImageList (список изображений), содержа- щий последовательность битовых изображений, и подсоединить этот список к меню с помощью свойства Images. После этого можно определить изображение для каж- дого пункта, установив соответствующее значение его свойства Imageindex. Опре- деление изображений для меню очень удобное: с помощью свойства SubMenuimages можно связать список изображений с любым раскрывающимся меню (и даже с оппелеленным пунктом меню). Имея небольшой список изображений для каж- 0220
Набор компонентов 221 дого раскрывающегося меню вместо одного большого списка изображений для всего меню, мы получаем большую «настраиваемость» приложения в ходе выполнения. ПРИМЕЧАНИЕ----------------------------------------------------------------- Создание пунктов меню в ходе выполнения настолько распространено, что Delphi предоставляет в модуле Menus ряд готовых к использованию функций. Имена этих глобальных функций не требу- ют описания: NewMenu, NewPopupMenu, NewSubMenu, Newltem и NewLine. Контекстные меню и событие OnContextPopup Компонент PopupMenu обычно отображается при щелчке пользователя правой кноп- кой мыши по компоненту, который использует данное контекстное меню как зна- чение свойства PopupMenu. Однако помимо подсоединения контекстного (всплы- вающего) меню к компоненту посредством соответствующего свойства, его можно вызывать с помощью метода Popup, требующего в качестве параметров положения в координатах экрана. Соответствующие значения могут быть получены преобразо- ванием локальной точки в точку экрана с помощью метода CLientToScreen локального компонента, который используется в следующем фрагменте программного кода: procedure TForml.Label3MouseDown(Sender. TObject, Button- TMouseButton; Shift- TShiftState; X, Y; Integer), var ScreenPoint. TPoint, begin // если выполняются некоторые условия . if Button = mbRight then begin ScreenPoint •= Label3 CllentToScreen (Point (X. Y)), PopupMenul Popup (ScreenPoint.X. ScreenPoint Y) end, end: Альтернативный вариант заключается в использовании события OnContextMenu. Это событие, впервые появившееся в Delphi 5, запускается при щелчке пользова- теля правой кнопкой мыши точно на компоненте, что я и проверил с помощью выражения if Button = mbRight. Преимущество состоит в том, что то же самое собы- тие также запускается в качестве реакции на нажатие сочетания Shift+FlO. На не- которых клавиатурах это сочетание реализовано в виде отдельной клавиши «кон- текстное меню». Это событие может использоваться для запуска контекстного меню минимальным программным кодом: Procedure TFormPopup.LabellContextPopup(Sender- TObject.1 MousePos: TPoint: var Handled: Boolean): var ScreenPoint. TPoint: begin // добавить динамические пункты PopupMenu2.Items.Add (NewLine): PopupMenu2. Items. Add (Newltem (TimeToStr (Now). 0. False, True, nil, 0. 11 вывести контекстное меню ScreenPoint : = CllentToScreen (MousePos), PopupMenu2.Popup (ScreenPoint X. ScreenPoint Y), Handled := True. // удалить динамические пункты PopupMenu2.Items [4].Free. PopupMenu2.Items [3].Free: 0221
222 Глава 5. Визуальные элементы управления Этот пример добавляет некоторый динамизм в контекстное меню, добавляя временный пункт при выводе меню. Этот результат не имеет практической ценно- сти, но иллюстрирует, что если необходимо вывести открытое контекстное меню, вы может использовать свойство PopupMenu данного элемента управления или од- ного из его предков. Обработка события OnContextMenu имеет смысл только в тех случаях, когда требуется дополнительная обработка. Параметр Handled предварительно сброшен в False, поэтому если вы ничего не сделаете с этим обработчиком события, произойдет нормальная обработка кон- текстного меню. Если в обработчике события будет сделано что-либо, заменяю- щее нормальную обработку контекстного меню (допустим, открытие диалогового или пользовательского меню, как в данном случае), вы должны установить пара- метр Handled в True, и система прекратит обработку сообщения. Устанавливать Handled в True приходится редко, поскольку для динамического создания или на- стройки контекстного меню обычно обрабатывается событие OnContextRopup, а за- тем вы можете позволить стандартному обработчику показать это меню. Обработчик события OnContextPopup не ограничен лишь выводом контекстного меню. Он может выполнять любую другую операцию, как, например, непосред- ственный вывод диалогового окна. Вот пример изменения цвета элемента управ- ления, выполняемого при щелчке на нем правой кнопкой мыши: procedure TFormPopup Label2ContextPopup(Sender: TObject: MousePos- TPoint, var Handled: Boolean). begin ColorDialogl.Color := Label2.Color: if ColorDialogl.Execute then Label2.Color •= ColorDialogl.Color; Handled = True: end: Все фрагменты кода из этого раздела можно найти в примерах CustPop для VCL и QCustPop для CLX. Технологии, связанные с элементами управления После общего обзора наиболее известных элементов управления Delphi часть книги будет посвящена рассмотрению общих стержневых технологий, не связанных с определенным компонентом. Мы коснемся фокуса ввода, привязки элементов уп- равления, использования компонента-разделителя и вывода «всплывающих» под- сказок. Конечно же, эти вопросы не охватывают все, что можно сделать с элемен- тами управления, но они дают отправную точку изучения и применения наиболее известных общих технологий. Обработка фокуса ввода С помощью свойств TabStop и TabOrder, имеющихся у большинства элементов уп- равления, можно указать порядок, в котором элементы будут получать фокус при использовании пользователем клавиши Tab. Вместо ручной установки свойства Tab 0222
Технологии, связанные с элементами управления 223 для каждого элемента формы можно использовать контекстное меню конструкто- ра форм для активизации диалогового окна Edit Tab Order (рис. 5.6). 7'Fdit T<»h Order EdrtLastName TEdit EditPasswoid TEdit StatusBail TStatusBar Рис. 5.6. Диалоговое окно Edit lab Order Помимо представленных здесь основных установок важно знать, что каждый раз, когда компонент получает или теряет фокус ввода, он получает соответ- ствующее событие OnEnter или OnExit. Это позволяет подстроить порядок опера- ций пользователя. Некоторые из этих технологий продемонстрированы в примере InFocus, создающем обычное окно ввода пароля. Эта форма имеет три строки ре- дактирования с надписями, указывающими их предназначение (рис. 5.7). Внизу окна имеется область состояния с указаниями для пользователя. Каждый пункт необходимо вводить в строгой последовательности. Рис. 5.7. Пример InFocus Для вывода сведений о состоянии я использовал компонент StatusBar с одной областью вывода (полученной установкой свойства SimplePanel в True). Вот сводка свойств данного примера. Обратите внимание, что символ & в надписях указывает ♦горячую» клавишу, а также на подключение этих надписей к соответствующим строкам редактирования (с помощью свойства FocusControl): ebject FocusForm: TFocusForm ActiveControl = EditFirstName Caption = 'InFocus' object Label 1- TLabel Caption = '&First name' FocusControl = EditFirstName 0223
224 Глава 5. Визуальные элементы управления end object EditFirstName: TEdit OnEnter = Global Enter OnExit = EditFirstNameExit end object Label 2: TLabel Caption = '&Last name' FocusControl = EditLastName end object EditLastName: TEdit OnEnter = Global Enter end object Label 3: TLabel Caption = ’^Password’ FocusControl = EditPassword end object EditPassword: TEdit PasswordChar = OnEnter = Global Enter end object StatusBarl: TStatusBar SimplePanel = True end end Программа проста и выполняет только две операции. Первая заключается в идентификации в строке состояния элемента Edit, имеющего фокус. Это выпол- няется обработкой события OnEnter с помощью одного общего обработчика, что позволяет избежать повторов программного кода. В данном примере вместо хра- нения дополнительной информации для каждой строки редактирования я прове- ряю каждый элемент формы для определения, какая надпись подключена к теку- щей строке редактирования (указанной параметром Sender): procedure TFocusForm.GlobalEnter(Sender: TObject); var I: Integer: begin for I := 0 to Control Count - 1 do // если элемент является надписью if (Controls [I] is TLabel) and Ни эта надпись подключена к текущей строке редактирования (TLabel(ControlsEU).FocusControl = Sender) then // скопировать текст, отключив начальный символ & StatusBarl.SimpleText := 'Enter ' + Copy (TLabel(Controls[I]).Caption. 2. 1000); end; Второй обработчик события формы относится к событию OnExit первой строки редактирования. Если этот элемент управления оставлен пустым, то переход фо- куса ввода будет отменен и возвращен обратно перед выводом сообщения. Эти методы также ожидают получения входного значения, автоматически заполняют вторую строку редактирования и перемещают фокус непосредственно на третью строку: procedure TFocusForm.EditFirstNameExit(Sender: TObject): begin if Fdit.F1 rstName Text = " then 0224
Технологии, связанные с элементами управления 225 begin // не позволить пользователю перейти Ech tFirstName.SetFocus: MessageDlg (.'First name is required', mtError. [mbOKJ. 0); end else if EditFirstName.Text = 'Admin' then begin 11 заполнить вторую строку и перейти к третьей EditLastName.Text := 'Admin'; EditPassword.SetFocus: end; end: ПРИМЕЧАНИЕ------------------------------------------------------------------------------- CLX-версия этого примера имеет тот же программный код и доступна в виде программы QInFocus. Привязка элементов управления Для того чтобы вы могли построить приятный, удобный пользовательский интер- фейс с использованием элементов управления, самостоятельно подстраивающих- ся под размер формы, Delphi с помощью свойства Anchors позволяет определять относительное расположение элементов управления. До того как такая возмож- ность появилась в Delphi 4, каждый элемент управления, размещаемый на форме, имел координаты относительно верхнего и левого края формы, если только не выполнялось выравнивание по нижней или правой части формы. Выравнивание — хорошая функция, но не для всех элементов, особенно не для кнопок. За счет использования привязки можно установить привязку элемента относи- тельно любой из сторон формы. Например, для того чтобы привязать кнопку к нижнему правому углу формы, поместите кнопку на требуемое место и устано- вите у его свойства Anchors значение [akRight, akBottom], При изменении размеров формы расстояние кнопки от сторон привязки останется фиксированным. Други- ми словами, если вы установите эти две привязки и удалите привязки по умолча- нию, то кнопка останется в правом нижнем углу. С другой стороны, если помещаете большой компонент (например, Мето или ListBox) в центре формы, то можно установить привязку ко всем четырем сторо- нам. При этом элемент управления будет вести себя как выровненный, увеличива- ясь или уменьшаясь в размерах в соответствии с размерами формы, но между ком- понентом и формой будет оставаться некоторый зазор. ПРИМЕЧАНИЕ--------------------------------------------------------- Привязки (Anchors), как и ограничения (Constraints), работают как во время разработки, так и во время выполнения. Для получения максимальной выгоды от этой функции их необходимо устанав- ливать как можно более простыми. В качестве примера обоих подходов вы можете поэкспериментировать с прило- жением Anchors, имеющем две кнопки в правом нижнем углу и список в середине. Как видно на рис. 5.8, элементы автоматически уменьшают свой размер при изме- нении размеров формы. Для того чтобы эта форма работала правильно, необходи- мо также установить свойство Constraints, иначе если форма станет очень малень- кой, элемент управления может перекрыть ее или пропасть. 0225
226 Глава 5. Визуальные элементы управления Рис. 5.8. Элементы управления примера Anchors перемещаются и изменяют размер автоматически при изменении пользователем размера формы. Для этого не требуется дополнительного программного кода, только соответствующие значения свойства Anchors Обратите внимание, что при удалении всех привязок или двух противополож- ных привязок (например, левой и правой) изменение размера приведет к тому, что элемент станет плавающим в форме. Элемент сохраняет свой текущий размер, а система добавляет или удаляет некоторое число пискелов с каждой из его сто- рон. Такая привязка может быть определена как «центрирование», поскольку если изначально компонент находился в середине формы, то он и останется в этом по- ложении. Если следует отцентрировать элемент, необходимо использовать проти- воположные привязки, что при увеличении формы пользователем приведет к уве- личению размеров элемента. В только что представленном случае увеличение формы оставит элемент неизменного размера в центре формы. Использование компонента Splitter В Delphi существует несколько способов реализации технологии разбиения фор- мы, но самый простой из них — использовать компонент Splitter, расположенный на вкладке Additional панели компонентов. Для эффективного использования эле- мента «разделитель» он должен использоваться совместно со свойством Constraints того элемента, к которому относится. Как можно увидеть в примере Splitl, эта ме- тодика позволяет определить максимальное и минимальное положение раздели- теля на форме. Для создания этого примера просто поместите на форму компо- нент ListBox, затем добавьте компонент Splitter, второй ListBox, еще один Splitter и, наконец, третий ListBox. Форма также имеет простую панель инструментов, осно- ванную на компоненте Panel. Простым перемещением этих двух разделителей можно предоставить вашей форме полноценную возможность перемещения и изменения размеров размещен- ных на ней элементов управления во время выполнения. Свойства Width, Beveled и Color компонента «разделитель» определяют его внешний вид, а в примере Splitl для их изменения можно воспользоваться элементами панели инструментов. Дру- гим значимым свойством является MinSize, которое определяет минимальный раз- мер компонентов формы. В ходе работы с разделителем (рис. 5.9) конечное поло- жение разделителя указывает линия, но ее нельзя перетащить за определенный 0226
Технологии, связанные с элементами управления 227 предел. Поведение программы SpLitl не позволяет элементам становиться слиш- ком маленькими. Альтернативная технология заключается в установке нового свой- ства AutoSnap разделителя в True. Это свойство позволяет разделителю спрятать элемент управления, если его размер выходит за ограничение MinSize. Split (wrth the Spfcttrr component} i Dog Cat Hen Monkey Cow Bull Hare Sheep Lizard Ant Shrimp Bug Bee Рис. 5.9. Компонент «разделитель» примера Splitl определяет минимальный размер каждого элемента управления на форме, даже если он не соседствует с разделителем Я предлагаю вам поэкспериментировать с программой Splitl для того, чтобы полностью понять, как он влияет на расположенные рядом и прочие элементы формы. Даже если вы установите свойство MinSize, пользователь сможет сокра- тить размер всей формы программы до минимума, скрыв при этом некоторые эле- менты-списки. Если вы протестируете версию Split2, вы отметите более разумное поведение программы. В ней я установил некоторые значения свойств Constraints элементов ListBox: object LlstBoxl: TListBox Constraints.MaxHeight = 400 Constraints.MinHeight = 200 Constraints.MinWidth = 150 Ограничения размеров применяются только в ходе изменений размеров, по- этому для удовлетворительной работы программы необходимо установить свой- ство ResizeStyle двух разделителей в rsUpdate. Это значение указывает, что положе- ние элемента управления обновляется при каждом перемещении разделителя, а не только по завершении операции. Если вместо него выбрать rsLine или новое значе- ние rsPattern, то разделитель просто нарисует линию в требуемом месте, проверяя свойство MinSize, а не ограничения элементов управления. ПРИМЕЧАНИЕ----------------------------------------------------------------------— Ири установке свойства AutoSnap компонента Splitter в True разделитель полностью спрячет сосед- ний элемент управления, когда размер этого элемента станет меньше минимальной установки для Чрмпонента Splitter. 0227
228 Глава 5. Визуальные элементы управления Горизонтальное разбиение Компонент Splitter помимо вертикального также может использоваться и для горизонтального разбиения. Как обычно, вы помещаете компонент на форму, вы- равниваете его по верхнему краю, а затем помещаете на форму разделитель. По умолчанию разделитель выравнивается по левому краю. Выберите значение alTop свойства Align, и все готово! Форму с горизонтальным разделителем можно уви- деть в примере SplitH. Эта программа имеет два компонента МЕМО, в которые мож- но загрузить файл, а также разделитель, определенный как: object Splitterl: TSplitter Cursor = crVSplit Align - alTop OnMoved - SplitterlMoved end Программа имеет строку состояния, отслеживающую текущую высоту двух компонентов МЕМО. Она обрабатывает событие OnMoved разделителя (единствен- ное событие этого компонента) и обновляет текст в строке состояния. При каждом изменении размера формы выполняется такой программный код: procedure TForml.SplitterlMoved(Sender: TObject); begin StatusBarl. Panel s[0].Text Format (.'Upper Memo: %d - Lower Memo: %d'. [MemoUp.Height, MemoDown.Height]); end: Клавиши быстрого вызова Начиная с Delphi 5 нет необходимости вводить символ & в свойстве Caption пункта меню. Это производится автоматически, если вы его упустите. Автоматическая система назначения клавиш быстрого вызова Delphi также может распознать на- значение клавиши, конфликтующей с уже существующими. Это не означает, что вы должны полностью отказаться от указания клавиш быстрого вызова с помощью символа &, поскольку автоматическая система использует лишь первую доступ- ную букву и не соблюдает стандарты. Вы можете найти более удобную клавишу, чем автоматическая система. Эта функция управляется свойством AutoHotkeys, доступной в главном меню компонента, в каждом раскрывающемся меню и в пунктах меню. В главном меню это свойство по умолчанию имеет значение maAutomatic; в раскрывающемся меню и в пунктах меню — maParent, поэтому значение, установленное для главного меню компонента, автоматически будет использовано во всех подменю, если только вы не укажете значение maAutomatic или maManual. Движущей силой этой системы является метод RethinkHotkeys класса TMenuItem и его напарник InternalRethinkHotkeys. Также существует метод RethinkLines, кото- рый проверяет, не имеет ли раскрывающееся меню два последовательных разде- лителя, а также не начинается ли и не заканчивается разделителем. Во всех этих случаях разделитель автоматически удаляется. Одна из причин, по которой в Delphi включена эта функция, заключается в под- держке переводов. Когда вам необходимо перевести меню приложения на другой язык, она является очень удобной, если только вы не сталкивались с клавишами быстрого вызова или, по крайней мере, не хотите беспокоиться о том, что два пун- 0228
Технологии, связанные с элементами управления 229 кта меню будут конфликтовать. Наличие системы, которая может автоматически разрешать подобные проблемы, определенно является преимуществом. Еще од- ной мотивацией была IDE Delphi. С учетом всех динамически загружаемых паке- тов, устанавливающих новые пункты в главном меню IDE или контекстных меню, и с учетом прочих пакетов, загружаемых в различных версиях продукта, становит- ся почти невозможным получить «бесконфликтные» клавиши быстрого вызова для каждого меню. Вот почему этот механизм не является мастером, выполняю- щим статический анализ в ходе разработки; он предназначен для решения реаль- ной проблемы управления меню, динамически создаваемыми в ходе выполнения. ВНИМАНИЕ-------------------------------------------------------------- Эта функция, безусловно, удобна, но поскольку по умолчанию она является активной, она может привести к разрушению существующего программного кода. Я был вынужден изменить два приме- ра данной главы при переходе от выпуска для Delphi 4 к выпуску для Delphi 5 лишь для того, чтобы избежать ошибки периода выполнения, вызванной этим изменением. Проблема заключается в том, что я использовал заголовки в программном коде, а дополнительные символы & испортили мой код. Изменение очень простое: все, что надо сделать, — установить свойство AutoHotkeys компонента main menu в maManual. Использование всплывающих подсказок Еще одним общим элементом в панели инструментов является контекстная под- сказка, также называемая всплывающей подсказкой. Это текст, который кратко опи- сывает кнопку, находящуюся под курсором. Он обычно выводится в желтом окош- ке, появляющемся рядом с курсором мыши, который «задержался» над кнопкой некоторое время. Для добавления подсказки для группы кнопок или компонентов просто установите свойство ShowHints родительского элемента управления в True и введите какой-либо текст в свойство Hint каждого элемента. Можно разрешить подсказки для всех компонентов формы или для всех кнопок панели инструмен- тов или любой панели. Если необходимо, чтобы подсказки появлялись над большим числом эле- ментов управления, то можно использовать некоторые методы и события объек- та Application. Этот глобальный объект имеет помимо прочих следующие свой- ства: Свойство Определяет HintColor HintPause HintHidePause HintShortPause Цвет фона окна подсказки Как долго курсор должен «задержаться» над элементом до появления подсказки Длительность вывода подсказки Как долго система должна ожидать вывода подсказки, если уже выведена другая подсказка Например, программа может позволить пользователю настраивать цвет фона подсказки путем указания определенного цвета с помощью следующего программ- ного кода: ColorDialog.Color : = Application.HintColor; Col orDialog.Execute then Application.HintColor : = ColorDialog.Color: 0229
230 Глава 5. Визуальные элементы управления В качестве альтернативы цвет подсказки можно изменить обработкой свойства OnShowHint объекта Application. Этот обработчик может изменять цвет подсказок различных элементов. Событие OnShowHint используется в примере CustHint, рас- сматриваемом в следующем разделе. Настройка подсказок Точно так же, как вы можете добавлять подсказки к панели инструментов прило- жения, можно добавлять подсказки к формам или к компонентам формы. Для круп- ного элемента управления подсказка появится около курсора мыши. В некоторых случаях важно знать, что программа сама может свободно настроить, каким обра- зом будут выведены подсказки. Единственное, что можно сделать, — изменить зна- чение свойств объекта Application. Для получения больших полномочий в отноше- нии подсказок их можно и далее модифицировать, назначив обработчик события OnShowHint приложения. Необходимо либо вручную присоединить это событие, либо, что лучше, добавить в форму компонент ApplicationEvents и обрабатывать его событие OnShowHint. Обработчик события имеет ряд интересных параметров, таких как строка с тек- стом подсказки, флажок логического типа, свидетельствующий об активизации подсказки, и структуры THintlnfo с последующей информацией, включая сам эле- мент управления, позицию подсказки и ее цвет. Параметры передаются ссылке, поэтому имеется возможность изменить их и значения структуры THintlnfo; на- Вфимер, можно изменять положение окна подсказки перед его отображением. Именно это я и сделал в примере CustHint, выводящем подсказку для надписи в центре ее области. procedure TForml.ShowHint (var HintStr: string; var CanShow: Boolean: var Hintinfo: THintlnfo); begin with Hintinfo do // если элемент является надписью, показать подсказку в середине if HintControl = Label 1 then HintPos : = HintControl.CllentToScreen (Point ( HintControl.Width div 2. HintControl.Height div 2)): end: Этот код уточняет середину основного элемента (Hintinfo.HintControl), а затем преобразует его координаты в координаты экрана, применяя к элементу метод ClientToScreen. Вы можете и дальше доработать этот пример. Пусть элемент формы ListBox имеет несколько довольно больших текстовых пунктов, поэтому было бы желательно вывести весь текст в подсказке при помещении над этим пунктом указателя мыши. Настройка отдельной подсказки для списка этого сделать, конечно же, не сможет. Хорошим решением является настройка системы подсказок на динамическое соответствие подсказки тексту пункта списка, оказавшемуся под курсором. Вам также надо указать системе, к какой области принадлежит подсказка, чтобы при переходе к новой строке выводилась новая подсказка. Этого можно добиться на- стройкой поля CursorRect записи Thintlnfo, которая указывает область компонента, над которой перемещается курсор, не отключая подсказку. Когда курсор перемес- тится за пределы этой области, Delphi спрячет окно подсказки. Вот фрагмент кода, который я добавил к методу ShowHint: 0230
Технологии, связанные с элементами управления 231 else if Hintcontrol = ListBoxl then begin nltem .= ListBoxl ItemAtPos! Point (CursorPos.x, CursorPos.Y). True); if nltem >= 0 then begin // установить строку подсказки HintStr := ListBoxl Items[nltem], // определить область действия подсказки CursorRect := ListBoxl.ItemRect(nltem): // показать над пунктом HintPos := HintControl.ClientToScreen (Point! 0. ListBoxl.ItemHeight * (nltem - ListBoxl.TopIndex))): end else CanShow := False. end: Окончательный эффект заключается в том, что каждая строка списка будет иметь свою подсказку (рис. 5.10). Положение подсказки вычисляется таким обра- зом, чтобы она закрывала текст текущего пункта, выходя за границы списка. Рис. 5.10. Элемент ListBox примера CustHint показывает различные подсказки в зависимости от того, над каким пунктом находится указатель мыши Собственные элементы управления и стили В Windows система обычно отвечает за вывод (изображение) кнопок, списков, строк редактирования, пунктов меню и подобных элементов. По существу эти элементы знают, как себя изобразить. Однако в качестве альтернативы система позволяет Нарисовать эти элементы их владельцу, обычно форме. Эта технология, доступная Для кнопок, списков, комбинированных списков и пунктов меню, называется изоб- ражение владельцем (owner-draw). В VCL эта ситуация еще более сложна. Компоненты заботятся о собственном изображении (как класс TBitBtn кнопки с изображением) и, возможно, активизи- ровании соответствующих событий. Система посылает запрос на прорисовку вла- дельцу (обычно форме), а форма пересылает событие обратно соответствующему элементу управления, запуская его обработчики событий. В CLX некоторые из элементов управления, такие как ListBoxes и Combo Boxes, действуют аналогично Изображению в Windows, а вот меню — нет. Собственный подход Qt заключается ^использовании стилей для определения графического поведения всех элементов ^правления системы, определенного приложения или данного элемента управле- ния. Стили будут рассмотрены далее в этом разделе. 0231
232 Глава 5. Визуальные элементы управления СОВЕТ---------------------------------------------------------------------------- Большинство из общих УУт32-элементов имеют поддержку технологии изображения владельцем, обычно называемой настраиваемым рисованием. Можно полностью настроить внешний вид ListView, TreeView, TabControl, PageControl, HeaderControl, StatusBar или ToolBar. Элементы управления ToolBar, ListView и TreeView также поддерживают расширенное настраиваемое рисование и в большей сте- пени настраиваемые возможности, представленные компанией Microsoft в последних версиях биб- лиотеки общих компонентов Win32. Недостаток изображения владельцем заключается в том, что при изменении в будущем стиля пользовательского интерфейса Windows (а это обязательно про- изойдет), элементы управления, использующие механизм изображения владельцем и являющиеся удачными при текущих стилях интерфейса пользователя, будут выглядеть устаревшими и неумест- ными. Поскольку создается настраиваемый интерфейс пользователя, то необходимо обеспечить возможность самостоятельного обновления. И наоборот, если используется стандартный вывод эле- мента управления, то приложения должны автоматически приспособиться к новой версии этого элемента управления. Собственные пункты меню VCL, по сравнению с традиционным подходом в API Windows, делает графиче- скую разработку пунктов меню значительно проще: вы устанавливаете свойство OwnerDraw компонента «пункт меню» в True и обрабатываете его события ОпМеа- sureltem и OnDrawItem. В событии OnMeasureltem можно определить размер пунктов меню. Этот обработчик события активизируется после каждого пункта меню при раскрытом меню и имеет два параметра-ссылки, которые можно изменить: Width и Height. С помощью события OnDrawItem можно вывести действительное изобра- жение. Обработчик события активизируется каждый раз, когда перерисовывается данный пункт. Это происходит, когда Windows первый раз выводит пункты, и каж- дый раз при изменении состояния; например, при перемещении указателя мыши над пунктами меню, при котором эти пункты выделяются. Для изображения пунктов меню вы должны учесть все возможности, включая раскрашивание выделенных пунктов определенным цветом, прорисовку «галоч- ки» и т. д. К счастью, событие Delphi передает в обработчик Canvas, где он должен быть изображен, выходной прямоугольник и состояние пунктов (выбран или нет). В примере ODMenu я осуществил обработку цвета выделения, но опустил прочие дополнительные аспекты (вывод «галочки», например). Установлено свойство OwnerDraw меню и написаны обработчики для некоторых пунктов меню. Для того чтобы написать один обработчик для каждого события трех пунктов меню, меняю- щих цвет, я в обработчике события OnCreate формы присвоил их свойству Тад зна- чение цвета. Это сделало обработчик события OnClick пунктов совершенно простым: procedure TForml.ColorClick(Sender: TObject). begin ShapeDemo.Brush.Col or := (Sender as TComponent).Tag end: Обработчик события OnMeasureltem не зависит от конкретного пункта, но исполь- зует фиксированные значения (отличающиеся от обработчиков других раскрыва- ющихся меню). Наиболее важной частью программного кода являются обработ- чики событий OnDrawItem. Для раскраски прямоугольника пункта определенным цветом используется значение tag (рис. 5.11). Однако перед этим необходимо за- полнить фон пункта меню (прямоугольную область, передаваемую в качестве па- раметра) стандартным цветом меню (clMenu) или цветом выбранного пункта меню (clHighlight): 0232
Технологии, связанные с элементами управления 233 procedure TForml ColorDrawItem(Sender. TObject: ACanvas: TCanvas: ARect: TRect: Selected: Boolean): begin // установить цвет фона и прорисовать его if Selected then ACanvas Brush.Color •= clHighlight el se ACanvas.Brush Color = clMenu: ACanvas.Fi11Rect (ARect). // показать цвет ACanvas.Brush Color := (Sender as TComponent) Tag; InflateRect (ARect. -5. -5): ACanvas.Rectangle (ARect.Left, ARect.Top. ARect.Right, ARect Bottom), end: Рис. 5.11. Изображаемое владельцем меню примера ODMenu Три обработчика для данного события раскрывающегося меню Shape совершен- но разные, хотя используют аналогичный программный код: procedure TForml.EllipselDraw!tem(Sender: TObject, ACanvas. TCanvas; ARect: TRect. Selected: Boolean). begin 11 установить цвет фона и прорисовать его if Selected then ACanvas.Brush.Color := clHighlight else ACanvas.Brush.Color := clMenu: ACanvas.FillRect (ARect): 11 нарисовать эллипс ACanvas.Brush.Color :-= clWhite. InflateRect (ARect. -5. -5); ACanvas.Ell ipse (ARect.Left. ARect.Top. ARect.Right. ARect Bottom); end; СОВЕТ ---------------------------------------------------------------------------------- Для подстройки ко все возрастающему числу стилей пользовательского интерфейса Windows 2000 Delphi для меню имеет событие OnAdvancedDrawItem. Список цветов J^K у только что рассмотренных меню, у списков также имеется возможность соб- Двенной прорисовки. Это значит, что программа может раскрашивать отдельные 0233
234 Глава 5, Визуальные элементы управления пункты списков. Точно такая же возможность имеется и у комбинированных спис- ков. Эта функция также доступна в CLX. Для создания списка с собственной про- рисовкой необходимо установить свойство Style в IbOwnerDrawFixed или IbOwner- DrawVariable. Первое значение указывает, что высота пунктов списка определяется свойством Item Height, и эта высота будет одинакова для всех пунктов. Второй стиль собственной прорисовки указывает, что список будет с пунктами различной высо- ты; в этом случае этот компонент будет запускаться событием OnMeasureltem каж- дого пункта, запрашивая у программы их высоту. В примере ODList (и его второй версии QODList) я использовал первый, более простой подход. Этот пример хранит сведения о цвете совместно с пунктами меню и затем прорисовывает пункты, используя эти цвета (вместо использования одно- го цвета для всего списка). DFM- или XFM-файл каждой формы, включая данную, имеет атрибут TextHeight, указывающий число пикселов, требуемых для представления текста. Это значе- ние должно использоваться в свойстве ItemHeight списка. Альтернативным реше- нием является вычисление этого значения в ходе выполнения, что позволит не беспокоиться о соответствующей установке высоты, если в ходе разработки вы измените шрифт. СОВЕТ------------------------------------------------------------------------- Сейчас мы рассмотрели TextHeight как атрибут формы, а не как свойство. Он не является свойством, а лишь локальным значением формы. Если TextHeight — не свойство, то вы можете спросить, как Delphi сохраняет его в DFM-файле? Все дело в том, что весь механизм поточной передачи Delphi основан на свойствах, плюс специальные клоны свойств, создаваемые методом DefineProperties. Поскольку TextHeight не является свойством, хотя перечислен в описании фор- мы, вы не можете обращаться к нему непосредственно. Изучая программный код VCL, я установил, что это значение рассчитывается вызовом частного метода фор- мы: GetTextHeight. Поскольку он является частным, вызвать его невозможно. Вмес- то этого можно скопировать его код (который очень прост) в метод FormCreate фор- мы, выбрав шрифт списка: Canvas.Font :» ListBoxl.Font; ListBoxl.ItemHeight Canvas.TextHeight!’O'): После этого добавьте несколько пунктов в список. Поскольку это список цве- тов, вы можете добавить наименования цветов в свойство Items списка и соответ- ствующие значения в хранилище данных Objects, связанное с каждым пунктом списка. Вместо раздельного добавления двух значений я написал процедуру, до- бавляющую новые пункты в список: procedure TODListForm.AddColors (Colors: array of TColor): var I: Integer; begin for I := Low (Colors) to High (Colors) do ListBoxl.Iterns.AddObject (ColorToString (ColorsUJ). TObject(Colors[I])) .• end; Этот метод использует параметр типа «открытый массив», т. е. массив с нео- пределенным числом элементов одинакового типа. Для каждого пункта, передава- емого в качестве параметра, посредством вызова метода AddObject в список добав- 0234
Элементы управления ListView и TreeView 235 строки, соответствующей цвету, вызывается функция Delphi ColorToString. Она воз- вращает строку, содержащую либо наименование соответствующей цветовой кон- станты, если таковая имеется, либо шестнадцатеричное представление цвета. Дан- ные цвета добавляются в список после приведения его значения к типу TObject (четырехбайтная ссылка), которая необходима методу AddObject. ПРИМЕЧАНИЕ-------------------------------------------------------------- Помимо ColorToString, которая преобразует значение цвета в соответствующую строку с идентифи- катором или шестнадцатеричное значение, имеется функция StringToColor, преобразующая соот- ветствующим образом отформатированную строку в цвет. В примере ODList этот метод вызывается обработчиком события OnCreate фор- мы (после того как установлена высота пунктов): AddColors ([clRed, clBlue, clYellow, clGreen, clFuchsla. clLIme. clPurple, clGray, RGB (213. 23. 123), RGB (0. 0. 0). clAqua. clNavy. clOllve. clTeal]): Для компиляции CLX-версии этого программного кода я добавил функцию RGB, рассмотренную ранее в разделе «Цвета». Программный код, используемый для изображения пунктов, не особенно сложен. Необходимо просто извлечь цвет, свя- занный с пунктом, установить его в качестве цвета шрифта, а затем вывести текст: procedure T0DL1stForm.ListBoxlDrawItem(Control: TWinControl: Index: Integer; Rect: TRect: State: TOwnerDrawState); begin with Control as TLlstbox do begin // стереть Canvas.Fi11Rect(Rect); // нарисовать пункт Canvas.Font.Col or := TColor (Items.Objects [Index]): Canvas.TextOut(Rect.Left, Rect.Top. Listboxl.Items[Index]); end: end; Система уже устанавливает соответствующий цвет фона, поэтому выбранный пункт выводится верно, даже безо всякого дополнительного программного кода с вашей стороны. Более того, программа позволяет добавлять новые пункты при двойном щелчке на списке: procedure TODListForm.LlstBoxlDblClick(Sender: TObject): begin if ColorDialogl.Execute then AddColors ([ColorDi alogl.Col or]); end; При попытке использовать эту возможность необходимо обратить внимание, что некоторые из добавляемых цветов возвращаются в виде имен цветов (одной из Цветовых констант Delphi), в то время как другие — преобразованы в шестнадца- теричные значения. Элементы управления ListView и TreeView 3 предшествующем разделе «Набор компонентов» мы рассмотрели различные Визуальные компоненты, которые можно использовать для вывода списков значе- 0235
236 Глава 5. Визуальные элементы управления ний. Стандартные компоненты «список» и «комбинированный список» являются очень известными, но зачастую они замещаются более мощными элементами List- View и TreeView. Эти элементы входят в состав общих элементов Win32 (вкладка Win32 в палитре компонентов) и хранятся в библиотеке ComCtl32.DLL. Подобные элементы также доступны и в Qt и VisualCLX, как в Windows, так и в Linux. Графический список ссылок При использовании компонента ListView вы можете использовать битовые изоб- ражения как для указания состояния элемента (например, выбранный), так и для графического описания содержания пункта. Для подключения изображений к списку или дереву необходимо ссылаться на компонент ImageList, который мы уже использовали для хранения изображений меню. ListView может иметь три списка изображений: для крупных значков (свой- ство Largelmages), для мелких значков (свойство Smalllmages) и для индикации со- стояния пункта (свойство Statelmages). В примере RefList я установил первые два свойства, используя два различных компонента ImageList. Каждый пункт ListView имеет свойство Imageindex, указывающее на изображе- ние. Для нормальной работы этой методики элементы в двух компонентах «списки изображений» должны следовать в одинаковом порядке. При наличии фиксиро- ванного списка изображений в него с помощью ListView Item Editor (подключен- ного к свойству Items) можно добавлять пункты. В этом редакторе можно опреде- лять пункты и подпункты. Подпункты выводятся только в режиме развернутого просмотра (при установке в vsReport свойства ViewStyle) и при подключении к свой- ству Columns набора заголовков: ВНИМАНИЕ----------------------------------------------------------------- Элемент ListView в CLX не имеет крупных/мелких значков. В библиотеке Qt этот тип вывода досту- пен из другого компонента — IconView. В примере RefList (простой список ссылок на книги, журналы, компакт-диски и веб-сайты) пункты хранятся в файле, поскольку пользователь программы мо- жет редактировать содержание списка, которое автоматически сохраняется при вы- ходе из программы. Таким образом, изменения, внесенные пользователем, стано- вятся постоянными. Сохранение и загрузка содержания ListView не очень простая задача, поскольку тип TListltems не имеет автоматического механизма сохранения данных. В качестве иного подхода я копировал данные в список строк, используя 0236
Элементы управления ListView и TreeView 237 собственный формат. Список строк может сохраняться в файл и повторно загру- жаться всего одной командой. Формат файла очень простой. Для каждого пункта списка программа сохраня- ет в одной строке заголовок, в следующей строке — индекс изображения (с пре- фиксом @), а в последующих строках — подпункты с табулированным отступом: procedure TForml.FormDestroy(Sender- TObject): var I, J: Integer: List: TStringList; begin // сохранить пункты List := TStringList.Create: try for I := 0 to ListViewl.Items.Count - 1 do begin // сохранить заголовок List.Add (ListVi ewl.Items[I].Caption); // сохранить индекс List.Add ('§' + IntToStr (ListViewl. ItemsEU. Imageindex)); // сохранить подпункты(с отступом) for J := 0 to ListViewl.ItemsEU.SubItems.Count - 1 do List.Add (#9 + ListViewl ItemsEU.SubItems [J]): end, List.SaveToFile (ExtractFilePath (Application.ExeName) + 'Items.txt'): finally List.Free; end: end: После этого пункты можно загрузить в методе FormCreate: procedure TForml.FormCreate(Sender. TObject); var List: TStringList; Newltem: TListltem: I: Integer: begin // остановить выдачу предупреждающего сообщения Newltem :- nil: // загрузить пункты ListViewl.Items.Clear; List := TStringList.Create: try List.LoadFromFile ( ExtractFilePath (Application.ExeName) + 'Items.txt"). for I -.= 0 to List.Count - 1 do if List EUEU “ #9 then Newltem.SubItems.Add (Trim (List [I])) else if List EUEU = then Newltem.Imageindex := StrToIntDef (List EUE2J. 0) el se begin // новый пункт Newltem .— ListViewl.Items.Add: Newltem.Caption : = List QI]: end: 0237
238 Глава 5. Визуальные элементы управления finally List.Free: end; end: Данная программа имеет меню, с помощью которого можно выбрать один из различных вариантов представления элемента ListView, а также добавить флажки в пункты, как в элементе CheckListBox. Некоторые из вариантов можно увидеть на рис 5.12. Рис. 5.12. Различные примеры вывода компонента ListView в программе RefList, получаемые изменением свойства ViewStyle Еще одна важная особенность, которая является общей для подробного пред- ставления или отчета, позволяет сортировать пункты в одном из столбцов. В VCL для реализации этой технологии требуется выполнить три операции. Сначала не- обходимо установить свойство SortType элемента ListView в stBoth или stData. При этом ListView будет осуществлять сортировку не по заголовкам, а путем вызова со- бытия ОпСотраге для каждого из двух пунктов, которые необходимо сравнить. Далее, поскольку необходимо сортировать по каждому столбцу подробного представления, требуется также обработать событие OnColumnClick (которое про- исходит при щелчке пользователя на заголовке столбца детализированного про- смотра, но только в том случае, если свойство ShowColumnHeaders установлено в True). При каждом щелчке на столбце программа сохраняет номер столбца в частном поле nSortCol класса формы: procedure TForml.LtstVnewlColumnClзck(Sender. TObject; Column: TListColumn); begin nSortCol := Column.Index. ListViewl.AlphaSort: end; И в качестве третьего шага код сортировки использует либо заголовок, либо 0238
Элементы управления ListView и TreeView 239 procedure TForml.ListViewlComparetSender: TObject; Iteml. Item2: TListltem; Data: Integer, var Compare. Integer). begin if nSortCol = 0 then Compare = CompareStr (Iteml.Caption. Item2 Caption) else Compare := CompareStr (Iteml.SubItems [nSortCol - 1]. Item2.SubItems [nSortCol - 1]); end; В CLX-версии этой программы (названной QRef List) нет необходимости выпол- нять только что рассмотренные действия. Элемент управления уже сам имеет возможность соответствующей сортировки при щелчке на заголовке. Вы получае- те множество автоматически отсортированных столбцов (в возрастающем или в убывающем порядке). И последняя функция, которую я добавил в программу, связана с мышью. При щелчке на пункте левой кнопкой программа Ref Li st показывает описание выбран- ного пункта. При правом щелчке на выбранном пункте он переводится в режим редактирования и может быть изменен пользователем (не забывайте, что все измене- ния будут автоматически сохранены при завершении программы). Вот программ- ный код обоих операций в обработчике события OnMouseDown элемента ListView: procedure TForml.ListViewlMouseDown(Sender: TObject: Button: TMouseButton: Shift: TShiftState; X. Y: Integer); var strDescr: string-. I: Integer; begin // если имеется выбранный пункт if ListViewl.Selected <> nil then if Button = mbLeft then begin // создать и показать описание strDescr := ListViewl.Columns [0] Caption + #9 + ListViewl.Selected.Caption + #13; for I ;= 1 to ListViewl.Selected.SubItems.Count do strDescr := strDescr + ListViewl.Columns [I] Caption + #9 + ListViewl.Selected.SubItems [1-1] + #13; ShowMessage (strDescr); end else if Button = mbRight then // редактировать заголовок Listvi ewl.Seiected.EditCaption; end; Хотя здесь и не полный набор функций, этот пример показывает некоторые потенциальные возможности ListView. Кроме того, я активизировал функцию «го- рячего отслеживания» (HotTrack), которая позволяет выделять и подчеркивать .пункт списка, находящийся под указателем мыши. Наиболее важные свойства ListView можно просмотреть и в текстовом варианте: object ListViewl; TListView Align = alClient Columns = < item Caption = 'Reference' 0239
240 Глава 5. Визуальные элементы управления Width = 230 end item Caption = 'Author' Width = 180 end item Caption = 'Country' Width = 80 end> Font.Height = -13 Font.Name = 'MS Sans Serif' Font.Style = [fsBold] FullDrag = True HideSelection = False HotTrack = True HotTrackStyles = [htHandPoint. htUnderlineHot] SortType = stBoth ViewStyle = vsList OnColumnClick = ListViewlColumnClick OnCompare = ListViewlCompare OnMouseDown = ListViewlMouseDown end Эта программа интересна еще и тем, что в дальнейшем она будет расширена добавлением диалоговых окон (см. главу 9). Для построения CLX-версии (QRefList) пришлось использовать только один из списков изображений и отключить меню крупных/мелких значков, поскольку ListView имеет только два стиля представления: список и отчет. Крупные/мелкие значки доступны в другом элементе управления, называемом IconView. Как ранее упоминалось, здесь уже имеется поддержка сортировки, что позволяет избежать написания большей части программного кода примера. Дерево данных А теперь, после рассмотрения примера на основе ListView, изучим элемент управ- ления TreeView. Этот элемент имеет гибкий и мощный пользовательский интер- фейс (с поддержкой возможности редактирования и перетаскивания составляю- щих). Он также является стандартным, поскольку является пользовательским интерфейсом Проводника Windows. У него имеются свойства и различные спосо- бы настройки изображений в каждой строке или для каждого типа строк. 0240
Элементы управления ListView и TreeView 241 Для определения структуры узлов TreeView во время разработки вы можете ис- пользовать редактор TreeView Items Editor (см. рис. на предыдущей странице). Однако в этом случае я решил загрузить данные TreeView во время запуска та- ким же способом, как и в предыдущем примере. Свойство Items компонента TreeView имеет множество функций-членов, кото- рые могут использоваться для изменения иерархии строк. Например, двухуровне- вое дерево строится следующим образом: var Node: TTreeNode; begin Node = TreeViewl.Items.Add (nil. 'First level'): TreeViewl.Items AddChild (Node. 'Second level'): С помощью методов Add и AddChiLd можно построить сложную структуру не- посредственно в ходе выполнения программы. Для загрузки информации во вре- мя выполнения снова можно использовать String List, загрузить текстовой файл со сведениями и проанализировать его. Однако поскольку компонент TreeView имеет метод LoadFromFile, в примерах DragTree и QDragTree используется следующий простой код: procedure TForml.FormCreate(Sender: TObject); begin TreeViewl LoadFromFile (ExtractFilePath (Application ExeName) + ’TreeText.txt'). end; Метод LoadFromFile загружает данные в список строк и проверяет уровень каж- дого пункта, просматривая количество предшествующих ему символов табуляции. (Если интересно, можете посмотреть метод TTreeStrings.GetBufStart, который можно найти в исходном программном коде модуля ComCtrls VCL). Для примера исполь- зования TreeView я подготовил данные, представляющие собой штатную структуру многонациональной компании (рис. 5.13). fir- DragTree !-] US Headquarters № Board of Directors Г-} Marketing Steve Rubens E3 Sales S3 US Offices Id European Offices !tl Paris London H Mian ® Frankfurt Andrea branch Moscow Far East Токю Рис. 5.13. Пример DragTree после загрузки данных и раскрытия ветвей Вместо последовательного раскрытия пунктов-узлов можно воспользоваться меню этой программы File ► Expand All, которое вызывает метод FullExpand элемента TreeView, или выполнить команду (с соответствующим деревом в корневом пункте): 0241
242 Глава 5. Визуальные элементы управления TreeViewl.Items [0].Expand(True); Помимо загрузки данных программа при завершении работы сохраняет дан- ные. В ней также имеется несколько пунктов меню, позволяющих настраивать шрифт элемента TreeView и ряд других настроек. Особенностью программы явля- ется реализация поддержки «перетаскивания» пунктов и целиком поддерева. Я установил свойство DragMode этого компонента в dmAutomatic и написал обра- ботчики событий для OnDragOver и OnDragDrop. В первом из двух обработчиков программа проверяет, что пользователь не пы- тается перетащить пункт в дочерний пункт (который будет перемещаться совместно с самим пунктом, вызывая бесконечную рекурсию): procedure TForml,TreeV1ewlDrag0ver(Sender. Source: TObject: X. Y: Integer: State: TDragState; var Accept: Boolean): var TargetNode. SourceNode: TTreeNode; begin TargetNode := TreeViewl.GetNodeAt (X. Y); // принять перетаскивание от себя if (Source - Sender) and (TargetNode о nil) then begin Accept True: // определить исходный и конечный пункт SourceNode TreeViewl.Selected: // look up the target parent chain while (TargetNode.Parent о nil) and (TargetNode <> SourceNode) do TargetNode := TargetNode.Parent: // еспи исходный найден if TargetNode = SourceNode then // запретить перетаскивание в дочерний пункт Accept False: end else Accept := False: end; Действие этого программного кода заключается в том, что пользователь может «перетаскивать» один пункт TreeView в другой (за исключением случаев, когда это запрещено). Написание программного кода по перемещению пунктов значитель- но проще, поскольку элемент управления TreeView предоставляет поддержку этой операции в виде метода MoveTo класса TTreeNode. procedure TForml.TreeViewlDragDrop(Sender. Source: TObject: X. Y: Integer); var TargetNode. SourceNode: TTreeNode; begin TargetNode : = TreeViewl.GetNodeAt (X. Y): if TargetNode <> nil then begin SourceNode :» TreeViewl.Selected: SourceNode.MoveTo (TargetNode. naAddChildFirst); TargetNode.Expand (False); TreeViewl.Selected TargetNode; end; end: 0242
Элементы управления ListView и TreeView 243 СОВЕТ--------------------------------------------------------------------------- Совместно с Delphi поставляется интересный пример, использующий TreeView (он расположен в подкаталоге CustomDraw). Перемещаемая версия DragTree Поскольку я использовал эту программу при демонстрации перемещения, я пост- роил версию, которую можно откомпилировать как в «родное» VCL-приложение Delphi, так и в CLX-приложение Kylix. Она сильно отличается от прочих упомя- нутых в этой книге программ, включая предыдущую версию этого же примера, который может быть перенесен в Kylix с помощью VisualCLX. Иногда выполне- ние того же действия другим образом может быть поучительным. Первое, что надо сделать, — посредством условной компиляции обеспечить использование двух наборов выражений uses. Модуль примера PortableDragTree на- чинается так: unit TreeForm: interface uses SysUtils. Classes. {UFDEF LINUX} Qt. Libc, QGraphics. QControls. QForms. QDialogs. QStdCtrls. QComCtrls. QMenus. QTypes. QGrids: (tENDIF) (UFDEF MSWINDOWS} Windows. Graphics. Controls. Forms. Dialogs. StdCtrls. ComCtrls. Menus. Grids; {iENDIF} . Для подключения соответствующего файла ресурсов формы в начале раздела реализации (implementation) используется аналогичная директива условной ком- пиляции: (UFDEF LINUX} {$R *.xfm) {iENDIF} (UFDEF MSWINDOWS} {iR *.dfm} (iENDIF} Я опустил некоторые специфичные для Windows возможности, поэтому вся разница заключается в методе FormCreate. Программа загружает файл данных из папки пользователя, назначенной ему по умолчанию, которая не совпадает с пап- кой программы. В зависимости от операционной системы каталогом пользователя является либо домашний каталог (и скрытый файл, начинающийся с точки), либо специальная область Мои документы (Му Documents) (доступная с помощью спе- циального вызова API): Pcocedure TForml.FormCreate(Sender; TObject); ver . path: string: •*91n (UFDEF LINUX} filename := GetEnvironmentVariablet’HOME') + 0243
244 Глава 5. Визуальные элементы управления '/.TreeText.txt'; {$ELSE} SetLength (path. 100); ShGetSpecial FolderPath (Handle. PChar(path). CSIDL_PERSONAL. False); path ;= PChar (path), // fix string length filename ;= path + '\TreeText.txt': {$ENDIF} TreeViewl.LoadFromFi1e (f11 ename). end; Настройка узлов дерева В Delphi 6 появилось несколько функций, связанных с TreeView: многократные выделения (см. свойства MultiSelect и MultiSelectStyle и массив Selections), улучшен- ная сортировка и несколько событий. Однако наиболее важным улучшением яв- ляется предоставление программисту возможности определить класс узловых (в представлении дерева) пунктов. Наличие настраиваемых узловых пунктов пред- полагает возможность подключения данных к узлам простым, объектно-ориенти- рованным способом. Для поддержки этой технологии имеется новый метод AddNode класса TTreeltems и специальное событие OnCreateNodesClass. В обработчике этого события можно вернуть класс создаваемого объекта, который должен наследоваться от TTreeNode. Это важная технология, поэтому для ее подробного рассмотрения я построил пример Custom Nodes. Этот пример сконцентрирован не на реальном случае, но ох- ватывает сложную ситуацию, в которой два различных класса узлов дерева исхо- дят один от другого. Базовый класс добавляет свойство ExtraCode, отображенное на виртуальные методы, и этот подкласс заменяет один из этих методов. Для базово- го класса функция GetExtraCode просто возвращает значение; для производного класса это значение умножается на значение родительского узла. Вот эти классы: type TMyNode = class (TTreeNode) private FExtraCode; Integer; protected procedure SetExtraCodefconst Value- Integer), virtual; function GetExtraCode; Integer; virtual; public property ExtraCode: Integer read GetExtraCode write SetExtraCode: end; TMySubNode = class (TMyNode) protected function GetExtraCode: Integer: override: end; function TMySubNode.GetExtraCode- Integer: begin Result := fExtraCode * (Parent as TMyNode) ExtraCode; end; При наличии настраиваемых классов узлов дерева программа создает три пун- „ „лплтиопозипои прпвпгп ТИПЯ Л ЛЯ V.17TOR ПРПВЛГЛ VnOBHB И ВТОПОЙ КЛАСС — ДЛЯ 0244
Элементы управления ListView и TreeView 245 остальных узлов. Поскольку существует только один обработчик OnCreateNodeClass, программа использует ссылку класса, хранимую в частном поле формы (Current- NodeClass типа TTree Node Class): procedure TForml TreeViewlCreateNodeClass(Sender: TCustomTreeView; var NodeCIass TTreeNodeClass); begin NodeCIass := CurrentNodeClass: end: Программа устанавливает эту ссылку класса перед созданием узла каждого типа. Например, подобным образом: var MyNode: TMyNode: begin CurrentNodeClass := TMyNode: MyNode = TreeViewl.Items.AddChild (nil. ’item’ + IntToStr (nValue)) as TMyNode: MyNode.ExtraCode := nValue: После того как создано все дерево, при выборе пользователем пункта можно преобразовать его тип в TMyNode и обратиться к дополнительным свойствам (но лишь к методам и данным): procedure TForml.TreeViewlCTick(Sender: TObject): var MyNode: TMyNode; begin MyNode := TreeViewl Selected as TMyNode: Label 1.Caption : = MyNode.Text + ' [' + MyNode.ClassName + '] = ' + IntToStr (MyNode.ExtraCode). end: CustomNodes item2 Ж valuel ЭС vakje2 +; vakje3 vdue4 rtem3 item4 vakjel £ value2 В valuel vakeel :+• SB value? rtem5 5 valuel Рис. 5.14. Пример CustomNodes благодаря событию OnCreateNodes имеет дерево с узлами-объектами, основанными на различных настраиваемых классах Этот программный код используется в примере CustomNodes для вывода описа- 1я выбранного узла в надписи (рис. 5.14). Обратите внимание, что при выбо- пункта в дереве его значение умножено на значение родительского узла. 0245
246 Глава 5. Визуальные элементы управления Существуют более простые способы достижения того же результата, но наличие дерева с пунктами-объектами, созданными из различных классов иерархии, обес- печивает объектно-ориентированную структуру, на основе которой можно пост- роить более сложный программный код. Что далее? В этой главе мы изучили основы библиотек Delphi, используемых для построения пользовательских интерфейсов: «родная» для Windows VCL-библиотека и CLX- библиотека на основе Qt. Был рассмотрен класс TControl, его свойства и наиболее важные производные от него классы. Мы исследовали некоторые основные компоненты, представленные в обеих библиотеках. Эти компоненты соответствуют стандартным элементам управления Windows и общим элементам, без которых не обойдется ни одно приложение. Вы также видели, как создаются главное и контекстное меню, как добавить дополни- тельные изображения в эти элементы управления. Следующим шагом будет более подробное изучение элементов пользователь- ского интерфейса, рассмотрение списков действий (Action lists) и Action Manager, а также построение простых, но полноценных примеров. С этим вы столкнетесь в главе 6, а глава 7 посвящена формам. 0246
6 Создание интерфейса пользователя В главе 5 рассматривались базовые положения класса TControl и производных от него классов библиотек VCL и VisualCLX. Был представлен краткий обзор основ- ных элементов управления, используемых для создания интерфейса пользователя (компоненты редактирования, списки, селекторы диапазонов и т. д.). В данной главе рассматриваются другие элементы управления, определяющие общий вид формы, такие как PageControl и TabControl. Затем будут представлены панели инструментов и строки состояния, в том числе некоторые дополнительные возможности. Все это даст основу для понимания остальной части главы, в которой освещаются дей- ствия и архитектура Action Manager. Современные приложения Windows обычно имеют несколько способов пода- чи команд, в том числе с помощью пунктов меню, кнопок панели инструментов, контекстных меню и т. д. Для выделения рабочих команд, подаваемых с помощью многочисленных представлений в интерфейсе пользователя, Delphi использует концепцию действий (actions). В современных версиях Delphi эта архитектура была расширена, чтобы сделать процесс строительства пользовательского интерфейса на основе действий полностью видимым. Сейчас пользователи могут настраивать свой интерфейс также легко, как и во многих профессиональных программах. Кроме того, Delphi 7 добавил к поддерживающим архитектуру Action Manager визуаль- ным элементам управления усовершенствованный и более современный интер- фейс пользователя, который поддерживает возможности интуитивного понимания (look-and-feel) ХР. В Windows ХР можно создавать приложения, адаптирующиеся к активной теме благодаря большому количеству новых внутренних процедур VCL. ’ В данной главе рассмотрены следующие темы: О многостраничные формы; О страницы и вкладки; ° панели инструментов и строки состояния; О темы и стили; ° Действия и списки действий; ° предопределенные действия в Delphi; D компоненты панели управления и панели CoolBar; ? стыкуемые панели инструментов и другие элементы управления; архитектура Action Manager. 0247
248 Глава 6. Создание интерфейса пользователя Многостраничные формы При необходимости отобразить в диалоговом окне или форме большое количе- ство информации и элементов управления можно использовать несколько стра- ниц. Пользователь выбирает одну из доступных страниц с помощью вкладок. Для создания многостраничного приложения в Delphi используются два элемента уп- равления: О Компонент PageControl содержит вкладки, которые находятся с одной сторо- ны, и несколько похожих на панели страниц, занимающих остальную его часть. Каждой вкладке соответствует одна страница, поэтому для получения нужного результата элементы просто размещаются на каждой странице в ходе разработ- ки или во время работы. О Компонент TabControl включает только часть с вкладками, не предлагая стра- ниц для хранения информации. В данном случае можно использовать один или несколько компонентов для имитации операции смены страницы (page change) или разместить во вкладках различные формы для имитации страниц. Третий связанный с ними класс, TabSheet, представляет одну страницу Раде- Control. Он не является автономным компонентом и недоступен в Component Palette. TabSheet создается в ходе разработки или во время работы с помощью контекстно- го меню PageControl СОВЕТ----------------------------------------------------------- Delphi до сих пор включает (на вкладке Component Palette Win 3.1) представленные в 32-разрядных версиях компоненты Notebook, TabSet и TabbedNotebook (начиная с Delphi 2). Для любых других целей компоненты PageControl и TabControl с инкапсулированными общими элементами управления Win32 предлагают более современный пользовательский интерфейс. В 32-разрядных версиях Delphi компонент TabbedNotebook снова был реализован с помощью PageControl Win32 для уменьшения размера кода и обновления внешнего вида. Компоненты PageControl и TabSheet Как обычно, вместо дублирования списка свойств и методов компонента PageControl справочной системы создан пример, расширяющий возможности этого элемента управления и позволяющий изменять его поведение во время работы. Пример Pages включает PageControl с тремя страницами. Структура PageControl и другие ключе- вые компоненты показаны в листинге 6.1. Листинг 6.1. Ключевые части DFM-файла примера Pages object Forml TForml Bordericons = [biSystemMenu. bi Minimize] BorderStyle = bsSingle Caption = 'Pages Test' OnCreate = FormCreate object PageControl1• TPageControl ActivePage = TabSheetl Align = alClient HotTrack = True 0248
Компоненты PageControl и TabSheet 249 Images - ImageListl MultiLine = True object TabSheetl: TTabSheet n = 'Pages’ object Label 3: TLabel object ListBoxl: TListBox end object TabSheet2: TTabSheet Caption = 'Tabs Size' ImageIndex = 1 object Label 1: TLabel // другие элементы управления end object TabSheet3: TTabSheet Caption = 'Tabs Text' Imageindex = 2 object Memol: TMemo = [akLeft. akTop. akRight, akBottom] = MemolChange end object BitBtnChange: TBitBtn = [akTop. akRight] '&Change' end end end object BitBtnPrevious: TBitBtn Anchors = [akRight. akBottom] Caption = '&Previous' OnClick = BitBtnPreviousClick end object BitBtnNext- TBitBtn Anchors = [akRight. akBottom] Caption = '&Next' OnClick = BitBtnNextClick end object ImageListl- TImageList Bitmap = {...} end end Учтите, что вкладки связаны с bitmap-изображениями, которые обеспечиваются элементом управления Im age List, и что некоторые элементы управления использу- ют свойство Anchors для того, чтобы оставаться на фиксированном расстоянии от правой или нижней границы формы. Даже если форма не поддерживает измене- ние размеров (из-за сложности настройки с помощью такого большого числа эле- ментов управления), эти положения можно изменить, если вкладки отображают- ся в несколько линий (просто увеличить длину заголовков) или с левой стороны формы. Каждый объект TabSheet имеет собственное свойство Caption, отображаемое как вкладка страницы. При разработке для создания новых страниц и для перехода между страницами используется контекстное меню. Это меню компонента Page- Control показано вместе с первой страницей (рис. 6.1), на которой расположено окно списка и небольшой заголовок, а две кнопки являются общими с другими страни- цами. 0249
250 Глава 6. Создание интерфейса пользователя Рис. 6.1. Первая страница компонента PageControl примера Pages с контекстным меню Если поместить на странице компонент, он будет доступен только на данной странице. Как разместить один и тот же компонент (в данном случае две кнопки bitmap-изображений) на каждой странице, не дублируя его? Просто поместите этот компонент на форму вне PageControl (или перед выравниванием его по клиентской области), азатем передвиньте его перед страницами с помощью управляющей ко- манды Bring То Front из контекстного меню формы. Две размещенные нами на каж- дой странице кнопки используются для перехода назад и вперед между страница- ми и являются альтернативой вкладкам. Вот код, связанный с одной из них: procedure TForml.BitBtnNextClick(Sender: TObject): begin PageControll SelectNextPage (True): end: Другая кнопка вызывает ту же процедуру, используя в качестве параметра для выбора предыдущей страницы значение False. Учтите, что при этом не нужно про- верять, на какой странице вы находитесь, поскольку метод SelectNextPage рассмат- ривает последнюю страницу как предшествующую первой, и вы перейдете как раз от одной к другой. Вернемся к первой странице. На ней имеется окно списка, которое во время работы будет содержать имена вкладок. Если щелкнуть на пункте этого окна спис- ка, текущая страница поменяется. Это третий способ изменять страницы (кроме вкладок и кнопок Next (Вперед) и Previous (Назад). Окно списка заполнено с по- мощью метода FormCreate, который связан с событием OnCreate этой формы и копи- рует заголовок каждой страницы (свойство Раде хранит список объектов TabSheet): for I := 0 to PageControll.PageCount - 1 do ListBoxl Items.Add (PageControll Pages.Caption): При щелчке на пункте списка выбирается соответствующая страница: procedure TForml.ListBoxlClтck(Sender: TObject); begin 0250
Компоненты PageControl и TabSheet 251 PageControll.ActivePage PageControl1.Pages [ListBoxl.Itemindex]: end; На второй странице имеются два окна редактирования (связанные с двумя ком- понентами UpDown), два флажка и два переключателя (рис. 6.2). Пользователь мо- жет ввести цифру (или выбрать ее, щелкнув на кнопках вверх и вниз или нажав клавиши со стрелками Up (Вверх) или Down (Вниз), пока выведено соответствую- щее окно редактирования), пометить или снять флажки и выбрать положение пе- реключателей, затем щелкнуть на кнопке Apply (Применить), чтобы выполнить изменения: procedure TForml.BitBtnApplyClick(Sender: TObject): begin // задать ширину, высоту и число линий вкладки PageControl1.TabWidth :- StrToInt (EditWidth.Text); PageControll.TabHeight :» StrToInt (EditHeight.Text): PageControl1,Multi Line ;= CheckBoxMultiLine.Checked; // показать или спрятать последнюю вкладку TabSheet3.TabVisiЫe .= CheckBoxVisible.Checked; // задать положение вкладки if RadioButtonl.Checked then PageControll.TabPosition := tpTop el se PageControl1 TabPosition := tpLeft; end; Pages Test ............ ........... Цо’ ” ... J ' : A' r w ' * F lea Рис. 6.2. Вторая страница примера используется для изменения размера и положения вкладок. Здесь вкладка слева от элемента управления страницы С помощью этого кода можно изменить ширину и высоту каждой вкладки (по- мните, что 0 означает автоматическое вычисление размера исходя из простран- ства, необходимого для каждой строки). Имеется возможность выбрать либо несколько линий вкладок, либо две небольших стрелки для прокрутки области вкладок, а также можно передвинуть вкладки к левой стороне окна. Данный эле- мент управления позволяет разместить вкладки внизу или справа, но данная про- грамма воспрещает это, поскольку это может серьезно затруднить размещение дру- гих элементов управления. Кроме того, можно скрыть последнюю вкладку на PageControl, которая соответ- ствует компоненту TabSheet3. Если скрыть одну из вкладок, установив для ее свой- 0251
252 Глава 6. Создание интерфейса пользователя ства TabVisible значение False, она будет недоступна с помощью кнопок Next (Впе- ред) и Previous (Назад), основанных на методе SelectNextPage. Вместо этого следует использовать функцию FindNextPage, которая выберет эту страницу, даже если вкладка не станет видимой. Вызов метода FindNextPage показан в приведенной ниже новой версии обработчика события OnClick кнопки Next (Вперед): procedure TForml.BitBtnNextClick(Sender: TObject): begin PageControll.ActivePage PageControl1.FindNextPage ( PageControll.ActivePage. True. False): end: Последняя страница имеет компонент memo с именами страниц (добавленный в методе FormCreate). Вы можете редактировать имена страниц, щелкнув на кнопке Change (Изменить) для изменения текста вкладки, но только если количество строк соответствует количеству вкладок: procedure TForml.ВтtBtnChangeClтck(Sender: TObject); var I: Integer; begin if Memol Lines Count <> PageControl1.PageCount then MessageDlg ('One line per tab. please'. mtError. [mbOK], 0) else for I := 0 to PageControl1.PageCount -1 do PageControl1.Pages [I].Caption := Memol.Lines [I]: BitBtnChange.Enabled := False: end: Наконец, последняя кнопка Add Page (Добавить страницу) позволяет добавлять к элементу управления страницы новый бланк вкладки, хотя программа не добав- ляет к нему никаких компонентов. Объект (пустого) бланка вкладки создается с по- мощью элемента управления страницы в качестве его владельца, но не функцио- нирует, пока не будет установлено свойство PageControl. Перед этим, однако, необходимо сделать новый бланк вкладки видимым. Вот код: procedure TForml.ВтtBtnAddClтck(Sender. TObject): var strCaption: string: NewTabSheet: TTabSheet; begin strCaption .= ‘New Tab'-. if InputQuery ('New Tab'. 'Tab Caption'. strCaption) then begin // добавить новую пустую страницу к элементу управления NewTabSheet := TTabSheet Create (PageControl1); NewTabSheet Visible .= True; NewTabSheet.Caption : = strCaption: NewTabSheet PageControl = PageControl1; PageControll.ActivePage = NewTabSheet. // добавить ее к обоим спискам Memol.Lines Add (strCaption), ListBoxl.Items.Add (strCaption): end: end: 0252
Компоненты PageControl и TabSheet 253 ПРИМЕЧАНИЕ------------------------------------------------------------- При написании формы на основе PageControl помните, что первой отобразится при выполнении программы та страница, на которой вы были перед компиляцией кода. Например, если вы работа- ете на третьей странице, а затем компилируете код и запускаете программу, она будет начинаться с этой страницы. Обычно эта проблема решается с помощью добавления строки кода в метод Form- Create для настройки PageControl или ноутбука на первую страницу. При этом текущая страница В ходе разработки не определяет начальную страницу при выполнении. Средство просмотра изображений Image Viewer с «собственными» вкладками Кроме того, можно использовать TabControl и описанный в последнем примере ди- намический подход в более сложных (и более простых) случаях. Каждый раз при необходимости иметь несколько страниц с одинаковым типом содержимого, вмес- то тиражирования элементов управления в каждую страницу можно использовать TabControl и изменять его содержимое при выборе новой вкладки. Это проделано мной в примере многостраничного средства просмотра bitmap-изображений Bmp- Viewer. Изображение, открывающееся в TabControl этой формы, выровненной по всей клиентской области, зависит от выбора во вкладке над ним (рис. 6.3). Рис. 6.3. Интерфейс средства просмотра bitmap-изображений в примере BmpViewer. Обратите внимание на «собственные» вкладки Вначале компонент TabControl пуст. После выбора File Open пользователь выби- рает в диалоговом окне File Open разные файлы и во вкладку добавляется (свой- ство Tabs компонента TabControll) массив строк с именами файлов (свойство Files компонента OpenDialogl): Procedure TFormBmpViewer OpenlCl1ck(Sender: TObject); begin if OpenDialogl.Execute then begin TabControll.Tabs AddStrings (OpenDialogl.Files): TabControll Tabindex .= 0: 0253
TabControlIChange (TabControll): end: end: ВНИМАНИЕ ------------------------------------------------------------------------- Свойство Tabs компонента TabControl в CLX — это совокупность, в то время как в VCL — это просто список строк. После вывода на экран новых вкладок следует обновить изображение, чтобы оно соответствовало первой вкладке. Для этого программа вызывает метод, свя- занный с событием OnChange компонента TabControl, который загружает файл, со- ответствующий текущей вкладке в компоненте изображения: procedure TFormBmpViewer.TabControllChange(Sender: TObject); begin Imagel.Picture.LoadFromFile (TabControl1.Tabs [TabControl1.Tabindex]); end: Пример не действует, если выбран файл без bitmap-изображения. Программа предупредит пользователя стандартным исключением, проигнорирует файл и про- должит свое выполнение. Программа позволяет также вставить bitmap-изображение в буфер обмена (без немедленного получения его, а только добавляя вкладку, которая при выборе бу- дет выполнять действительную операцию вставки) и копировать в него текущее bitmap-изображение. В Delphi буфер обмена поддерживается с помощью глобаль- ного объекта Clipboard, определенного в модуле ClipBrd. Для копирования или встав- ки bitmap-изображений используется метод Assign классов TCliрboard и TBitmap. При выборе команды Edit Paste в примере к набору вкладок добавится вкладка Clipboard (если она уже не существовала). Затем номер новой вкладки используется для из- менения активной вкладки: procedure TFormBmpViewer.PastelClick(Sender: TObject): var TabNum: Integer: begin // попытаться локализовать страницу TabNum := TabControl 1.Tabs.IndexOf (’Clipboard”); if TabNum < 0 then // создать новую страницу для буфера обмена TabNum := TabControll.Tabs.Add (.'Clipboard'); 11 перейти к странице буфера обмена и принудительно перерисовать TabControll.Tabindex := TabNum: TabControlIChange (Self); end; Чтобы вычислить возможное присутствие вкладки Clipboard (Буфер обмена), код метода TabControllChange становится: procedure TFormBmpViewer.TabControllChange(Sender: TObject); var TabText: string: begin Imagel.Visible : = True: TabText := TabControll.Tabs [TabControll.Tabindex]; if TabText <> 'Clipboard' then // загрузить файл, указанный во вкладке 0254
Imagel.Picture.LoadFromFi 1 e (TabText) else {if the tab is 'Clipboard' and a bitmap is available in the clipboard} if Clipboard.HasFormat (cf_Bitmap) then Imagel.Picture.Assign (Clipboard) else begin // в противном случае удалить вкладку буфера обмена TabControl1.Tabs.Delete (TabControl1.Tabindex); if TabControl1.Tabs.Count = 0 then Imagel.Visible := False: end: Эта программа вставляет bitmap-изображение из буфера обмена при каждом из- менении вкладки. Программа каждый раз сохраняет только одно изображение и не имеет возможности сохранять bitmap-изображение буфера обмена. Однако если со- держание буфера обмена изменяется и формат bitmap-изображения становится не- доступен, вкладка Clipboard (Буфер обмена) автоматически удаляется (см. предыду- щий листинг). Если вкладок больше нет, компонент Image становится скрытым. Изображение также можно удалить с помощью любой из двух команд меню: Cut (Вырезать) или Delete (Удалить). Cut (Вырезать) удаляет вкладку после копи- рования bitmap-изображения в буфер обмена. На практике метод CutlClick только вызывает методы CopylClick и DeletelClick. Метод CopylClick выполняет копирова- ние текущего изображения в буфер обмена; a DeletelClick просто удаляет текущую вкладку. Вот их код: procedure TFormBmpViewer.CopylClick(Sender: TObject): begin Clipboard.Assign (Imagel.Picture.Graphic): end: procedure TFormBmpV1ewer.DeletelC11ck(Sender: TObject); begin with TabControl1 do begin if Tabindex >= 0 then Tabs.Delete (Tabindex); if Tabs.Count = 0 then Imagel.Visible := False: end; end: Одной из особенностей этого примера является то, что для свойства OwnerDraw элемента управления TabControl установлено значение True. Это значит, что эле- мент управления не будет рисовать вкладку (которая будет пустой в ходе разра- ботки), а сделает это с помощью приложения, вызвав событие OnDrawTab. В своем коде программа выводит на экран центрированный по вертикали текст, применяя API-функцию DrawText. Отображаемый текст — это не весь путь файла, а только его имя. Таким образом, если текст имеет значение не None, программа читает bitmap- изображение, к которому обращается вкладка, и рисует его уменьшенную версию в самой вкладке. Для этого программа использует объект TabBmp типа TBitmap, который создается и уничтожается вместе с формой. Программа также использует константу BmpSide для правильного позиционирования bitmap-изображения и текста: 0255
procedure TFormBmpViewer.TabControlIDrawTabtControl: TCustomTabControl: Tabindex: Integer; const Rect: TRect: Active: Boolean); var TabText: string; OutRect; TRect: begin TabText := TabControl1.Tabs [Tabindex]; OutRect := Rect; InflateRect (OutRect. -3. -3); OutRect.Left ;- OutRect.Left + BmpSide + 3; DrawText (Control.Canvas.Handle. PChar (ExtractFIleName (TabText)). Length (ExtractFIleName (TabText)). OutRect. dt_Left or dt_SingleLine or dt_VCenter); if TabText = 'Clipboard' then if Clipboard.HasFormat (cf_Bitmap) then TabBmp.Assign (Clipboard) else TabBmp.Freelmage else TabBmp.LoadFromFile (TabText): OutRect.Left ;- OutRect.Left - BmpSide - 3; OutRect.Right OutRect.Left + BmpSide; Control.Canvas.StretchDraw (OutRect. TabBmp); end; Кроме того, программа поддерживает печать текущего bitmap-изображения после вывода предварительного просмотра формы страницы, в которой можно за- дать нужный масштаб. Эта дополнительная функция программы подробно не рассматривается, но данный код имеется в программе и его можно изучить. Пользовательский интерфейс мастера При использовании TabControl без страниц можно использовать PageControl без вкладок. Рассмотрим разработку интерфейса пользователя для мастера. При ра- боте с мастером пользователю с помощью последовательности действий даются указания, по одному экрану на каждый шаг, и при каждом действии обычно пред- лагается выбрать, выполнять ли следующее действие или вернуться к предыдуще- му для исправления введенных данных. Вместо вкладок, которые можно выби- рать в любом порядке, мастера обычно предлагают кнопки Next (Далее) и Back (Назад) для перехода к страницам. Это будет достаточно простой пример, главная цель которого — предложить некоторые рекомендации. Пример назван WizardUI. Для начала нужно создать серию страниц в PageControl и указать свойству TabVisible каждой TabSheet значение False (а для свойства Visible оставить значение True). Начиная с Delphi 5 вкладки можно скрыть в ходе разработки. В этом случае для перехода к другой странице вместо вкладок следует использовать контекстное меню элемента управления страницы, поле со списком Object Inspector или Object Tree View. Но почему не сделать вкладки видимыми в период проектирования? Можно разместить элементы управления на страницах и затем дополнительные элементы управления — перед страницами (как это было сделано в примере), и со- ответствующие изменения их положения при выполнении не будут видны. Мож- но также удалить из вкладки неиспользуемые заголовки; они занимают простран- ство в памяти и ресурсах приложения. 0256
На первой странице помещено изображение и элемент управления фаска (bevel control) с одной стороны, и текст, флажок и две кнопки с другой стороны. Факти- чески кнопка Next (Далее) находится внутри страницы, а кнопка Back (Назад) — вне ее (и является общей для всех страниц). Первая страница в период проектиро- вания показана на рис. 6.4. Следующие страницы выглядят аналогично и имеют надпись, флажки и кнопки с правой стороны и пустую правую сторону. Рис. 6.4. Первая страница примера WizardUI в ходе разработки При щелчке на кнопке Next (Далее) на первой странице программа проверяет состояние флажка и выбирает следующую страницу. Для этого можно написать следующий программный код: procedure TForml.btnNextlClick(Sender: TObject): begin BtnBack.Enabled := True; if Checkinprise.Checked then PageControll.ActivePage : = TabSheet2 else PageControll.ActivePage : = TabSheet3; // переместить изображение и фаску Bevel 1.Parent : = PageControll.ActivePage; Imagel.Parent PageControll.ActivePage; end; После размещения общей кнопки Back (Назад) программа меняет активную страницу и затем переносит графическую часть на новую страницу. Поскольку нужно повторить этот код для каждой кнопки, я поместил его в метод, после того как добавил несколько дополнительных возможностей. Вот код: procedure TForml.btnNextlClick(Sender: TObject): begin if Check Inprise.Checked then MoveTo (TabSheet2) else MoveTo (TabSheet3): end; procedure TForml.MoveTo(TabSheet: TTabSheet); begin // добавить к списку последнюю страницу 0257
BackPages.Add (PageControl1.ActivePage): BtnBack.Enabled = True: 11 сменить страницу PageControll.ActivePage := TabSheet. 11 переместить изображение и фаску Bevel 1.Parent .= PageControll.ActivePage: Imagel.Parent := PageControll.ActivePage: end: Кроме уже объясненного программного кода, метод MoveTo добавляет после- днюю страницу (ту, которая была перед сменой) к списку посещенных страниц, который действует как стек. Объект BackPages класса TList создается при запуске программы, а последняя страница всегда добавляется к концу. При щелчке на кноп- ке Back (Назад), которая не зависит от страницы, программа извлекает последнюю страницу из списка, удаляет ее запись и переходит к этой странице: procedure TForml.btnBackClick(Sender: TObject); var LastPage: TTabSheet: begin // получить последнюю страницу и перейти на нее LastPage := TTabSheet (BackPages [BackPages.Count - 1]): PageControll.ActivePage :- LastPage; 11 удалить последнюю страницу из списка BackPages.Delete (BackPages.Count - 1): Il в конце отключить кнопку back BtnBack Enabled := not (BackPages.Count = 0); 11 переместить изображение и фаску Bevel 1.Parent := PageControll.ActivePage. Imagel.Parent := PageControll.ActivePage; end: С помощью этого кода пользователь может вернуться назад на несколько стра- ниц (пока список не очистится) к точке, в которой вы отключили кнопку Back (На- зад). Вам придется столкнуться с трудностью: перейдя от конкретной страницы, вы знаете, какая станица «следующая» и «предыдущая», но не знаете, с какой стра- ницы вы пришли, поскольку может быть несколько путей перехода к странице. Только отследив движения с помощью списка, вы можете надежно вернуться на- зад. Остальная часть программного кода, которая показывает некоторые веб-адре- са, очень проста. Вы можете повторно использовать навигационную структуру этого примера в собственных программах, изменив только графическую часть и содер- жание страниц. Поскольку большинство надписей программ показывает НТТР- адреса, пользователь может щелкнуть на надписи, чтобы открыть браузер по умол- чанию, показывающий данную страницу. Это выполняется путем извлечения из надписи HTTP-адреса и вызова функции SheLLExecute: procedure TForml.Label LinkClick(Sender: TObject); var Caption, StrUrl: string; begin Caption := (Sender as TLabel).Caption; StrUrl := Copy (Caption, Pos ('http:ll'. Caption). 1000): ShellExecute (Handle, 'open'. PChar (StrUrl), ", ", sw Show): end; 0258
Этот метод подключается к событию OnClick нескольких надписей на форме, которые были преобразованы в связи (links) (путем установки Cursor в виде руки). Вот одна из надписей: object Label 2 TLabel Cursor = crHandPoint Caption = 'Maw site. http.Hwww borland com' OnClick = LabelLinkClick end Элемент управления ToolBar Для создания панели инструментов в Delphi имеется специальный компонент, инкапсулирующий соответствующий обычный элемент управления Win32 или соответствующий элемент управления окном Qt в VisualCLX. Этот компонент предлагает панель инструментов со своими кнопками и обладает многими расши- ренными возможностями. Он помещается на форму и затем его редактор (контек- стное меню вызывается щелчком правой кнопки) используется для создания кно- пок и разделителей. Панель инструментов наполнена объектами класса TTooLButton. Эти объекты имеют фундаментальное свойство Style, которое определяет их поведение: О Стиль tbsButton означает стандартную кнопку. О Стиль tbsCheck означает кнопку с поведением флажка или с поведением пере- ключателя, если эта кнопка объединена в блок с другими (определяется нали- чием разделителей). О Стиль tbsDropDown означает кнопку с раскрывающимся списком (один из ви- дов поля со списком). Раскрывающуюся часть можно легко реализовать в Delphi, привязав элемент управления PopupMenu к свойству DropdownMenu данного эле- мента. О Стили tbsSeparator и tbsDivider означают разделители различными вертикаль- ными линиями (в зависимости свойства Flat панели инструментов). Для создания графической панели инструментов можно добавить к форме ком- понент ImageList, загрузить в него несколько bitmap-изображений и затем связать ImageList со свойством Images панели инструментов. По умолчанию изображения будут назначаться кнопкам в порядке их появления, но это поведение можно лег- ко изменить, установив свойство Imageindex каждой кнопки панели инструмен- тов. Следующие компоненты ImageLists можно подготовить для специальных ус- ловий и назначить их свойствам Disabledlmages и Hotlmages панели инструментов. Первая группа применяется для отключенных кнопок; вторая — для кнопки, на которую в настоящее время наведена мышь. СОВЕТ------------------------------------------------------------------ В неочевидных приложениях панели инструментов обычно создаются с помощью ActionList (списка Действий) или новейшей архитектуры Action Manager, рассматриваемой далее в этой главе. В этом случае кнопкам панели инструментов придается определенное поведение, поскольку их свойства и события управляются компонентами действия. Кроме того, вы можете закончить операцию с по- мощью панели инструментов конкретного класса TActionToolBar. 0259
Пример RichBar В качестве примера использования панели инструментов я создал приложение RichBar, имеющее компонент RichEdit, выполняемый с помощью панели инструмен- тов. Программа имеет кнопки для загрузки и сохранения файлов, для копирова- ния и вставки операций и изменения некоторых атрибутов текущего шрифта. Подробно возможности элемента управления RichEdit рассматриваться не бу- дут. Изучим только те, которые характерны для панели инструментов, используе- мой в данном примере (рис. 6.5). У этой панели инструментов имеются кнопки, разделители и даже раскрывающееся меню и два поля со списком (см. в следую- щем разделе). Ан imrvrfMi tfon th* featates of (he RtfhKei e/ampi*, Й; vhaptsi L ei (he bftok “Mastering Delphi Written end copyrighted by Marra Ceaiu. This document explains how do you create a simple editor based on the RichEdit control, using Delphi 6 The program has a toolbar and implements a number of features, including a complete scheme for opening and saving the text hies, discussed in this document In fact, we want to be able to ask the user to save any modified file before opening a new one, to avoid losing any changes Sounds hke a professional application, doesn't it? File Operations The most complex part of this program is implementing the commands of the File pull-down menu- New. Open, Save, and Save As In each case, we need to track whether the current file has changed, Рис. 6.5. Пример панели инструментов RichBar. Обратите внимание на раскрывающееся меню Различные кнопки реализуют функции, включая открытие и сохранение тек- стовых файлов — программа просит пользователя сохранить любой измененный файл перед открытием нового, чтобы избежать потери любых изменений. Обраба- тывающая файлы часть программы достаточно сложная, но ее следует изучить, поскольку большинство работающих с файлами приложений будет использовать аналогичный код. Подробнее см. в файле RichBar File Operations.rtf, который содер- жит исходный код для этого примера и который можно открыть с помощью самой программы RichBar. Кроме операций с файлами, программа поддерживает операции копирования и вставки и управление шрифтом. Операции копирования и вставки не требуют взаимодействия с VCL-объектом Clipboard, поскольку компонент может выполнить их с помощью простых команд, наподобие следующих: RichEditCutToCLipboard; RichEdit. CopyToCLipboard; RichEdit. PasteFromCLipboard; RichEdit.Undo; Сложнее понять, когда следует включать эти операции (и соответствующие кнопки). Кнопки Сору (Копировать) и Cut (Вырезать) можно включить, когда выб- ран какой-либо текст в событии OnSelectionChange элемента управления RichEdit: 0260
procedure TFormRichNote.RichEditSeiectionChange(Sender: TObject); begin tbtnCut.Enabled — RichEdit.Sei Length > 0; tbtnCopy.Enabled := tbtnCut.Enabled: end. Операция Copy (Копировать) не может быть определена действием пользовате- ля, поскольку она зависит от содержимого буфера обмена, на который также вли- яют другие приложения. Один из способов — использовать таймер содержимого буфера обмена и время от времени проверять содержимое самого буфера. Но луч- ше использовать событие Onldle объекта Application (или компонента Application- Events). Поскольку элемент управления RichEdit поддерживает многие форматы буфера обмена, программный код не может просто просмотреть их, а должен за- просить сам компонент, используя низкоуровневую функцию, которая не выво- дится наружу с помощью элемента управления Delphi: procedure TFormRichNote.ApplicationEventslIdle(Sender: TObject: var Done: Boolean): begin // обновить кнопки панели управления tbtnPaste.Enabled := RichEdit.Perform (em_CanPaste. 0. 0) <> 0: end: Основное управление шрифтом выполняется с помощью кнопок Bold (полу- жирный) и Italic (курсив), имеющих сходный код. Кнопка Bold переключает соот- ветствующий атрибут выбранного текста (или изменяет стиль в текущем положе- нии редактирования): procedure TFormRichNote.BoldExecute(Sender: TObject): begin with RichEdit.SeiAttributes do if fsBold in Style then Style := Style - [fsBold] else Style := Style + [fsBold]: end: Напоминаем, что текущее состояние кнопки определяется текущим выбором, поэтому в метод RichEditSelectionChange необходимо добавить следующую строку: tbtnBold.Down := fsBold in RichEdit.SelAttributes.Style: Меню и поле co списком в панели инструментов Кроме серий кнопок в примере RichBar имеется раскрывающееся меню и пара по- лей со списком, общая функция для многих обычных предложений. Раскрываю- щая список кнопка позволяет выбрать размер шрифта, а поля со списком позволя- ют быстро выбрать семейство шрифтов и его цвет. Второе поле создано с помощью элемента управления ColorBox. Кнопка Size связана с компонентом PopupMenu (именуемым SizeMenu) при по- мощи свойства DropdownMenu. Пользователь может щелкнуть на кнопке, запустив ее событие OnClick обычным способом, или, выбрав раскрывающую стрелку, от- крыть всплывающее меню (рис. 6.5) и выбрать один из его пунктов. В данном слу- чае имеется три разных размера шрифта для одного определения меню: object SizeMenu: TPopupMenu object Small 1: TMenuItem 0261
Tag - 10 Caption - 'Small' OnClick - SetFontSIze end object Mediuml: TMenuItem Tag - 16 Caption = 'Medium' OnClick = SetFontSIze end object Largel: TMenuItem Tag = 32 Caption = 'Large' OnClick = SetFontSIze end end Каждый пункт меню имеет тег, показывающий действительный размер шриф- та, который активизируется общим обработчиком событий: procedure TFormR1chNote.SetFontS1ze(Sender: TObject): begin RichEdlt.SelAttrlbutes.Size (Sender as TMenuItem).Tag: end; Элемент управления панели инструментов — полнофункциональный контей- нер элемента управления, поэтому можно взять окно редактирования, поле со спис- ком и другие элементы управления и разместить их непосредственно внутри пане- ли инструментов. Поле со списком в панели инструментов инициализируется в методе FormCreate, который извлекает экранные шрифты, доступные в данной си- стеме: ComboFont.Items := Screen.Fonts: ComboFont.Itemindex := ComboFont.Items.IndexOf (RichEdlt.Font.Name) Поле co списком сначала выводит на экран имя используемого в элементе уп- равления Rich Edit шрифта по умолчанию, который установлен в ходе разработки. Это значение заново вычисляется при каждом изменении текущего выбора при помощи шрифта выбранного текста и текущего цвета в ColorBox: procedure TFormRlchNote.R1chEd1tSelect1onChange(Sender: TObject); begin ComboFont.Itemindex ;- ComboFont.Items.IndexOf (RichEdlt.Sei Attributes.Name): ColorBoxl.Selected := RichEdlt.SelAttrlbutes.Color; end; При выборе в поле со списком нового шрифта происходит обратное действие. Текст текущего пункта поля со списком присваивается в качестве имени шрифта для любого текста, выбранного в элементе управления Rich Edit: RichEdlt.SelAttrlbutes.Name := ComboFont.Text; Выбор цвета в ColorBox активизирует аналогичный код. Простая строка состояния Создать строку состояния даже проще, чем панель инструментов. Delphi включает специальный компонент StatusBar, основанный на соответствующем обычном эле- менте управления Windows (похожий элемент управления доступен в VisualCLX). 0262
Этот компонент может использоваться почти как панель, если для его свойства SimplePanel установлено значение True. В данном случае для вывода текста можно использовать свойство SimpleText. Реальное преимущество этого компонента за- ключается в том, что он позволяет определять число субпанелей с помощью запус- ка редактора его свойства Panels. (Редактор этого свойства также можно вывести на экран, дважды щелкнув на элементе управления строки состояния или выпол- нив те же операции с помощью Object TreeView). У каждой субпанели есть свои гра- фические атрибуты, которые можно настроить с помощью Object Inspector. Другая функция компонента строки состояния — участок «захвата для изменения размера» («size grip»), добавляемый к нижнему правому углу строки, которая используется для изменения размера. Это обычный элемент интерфейса пользователя Windows, им можно частично управлять с помощью свойства SizeGrip (он автоматически от- ключается, если размер формы не может быть изменен). Строка состояния используется для множества целей, чаще всего для отобра- жения информации о текущем выбранном пункте меню или о статусе программы: положении курсора в графическом приложении, текущей строке текста в тексто- вом процессоре, состоянии блокированных клавиш, времени и дате и т. д. Для ото- бражения информации на панели используется свойство Text, обычно в выраже- ниях наподобие следующего: StatusBarl.Panels[lj.Text := 'message': В примере RichBar создана строка состояния с тремя панелями (для командных подсказок), состояние клавиши Caps Lock и текущее положение редактирования. Компонент StatusBar данного примера имеет четыре панели — вам необходимо оп- ределить четвертую, чтобы разграничить область третьей панели. Последняя па- нель всегда достаточно большая для того, чтобы покрыть оставшуюся поверхность строки состояния. ПРИМЕЧАНИЕ ------------------------------------------------------------------ Более подробно программа RichBar рассмотрена в RTF-файле в исходном коде примера. Учтите, что поскольку указанные подсказки должны отображаться в первой панели строки состояния, я мог упростить программный код с помощью свойства AutoHint. Я предпочел показать более подробный код, чтобы вы смогли настроить его сами. Панели не являются независимыми компонентами, поэтому доступ к ним нельзя получить по имени, только по положению в предшествующем фрагменте кода. Рекомендуется улучшить читаемость программы, определив постоянную для каж- дой используемой панели, и затем использовать эти постоянные при обращении к панелям. Вот мой пример кода: const sbpMessage = 0: sbpCaps = 1: sbpPosition = 2: В первой панели строки состояния я хочу вывести на экран сообщение с под- сказкой для кнопки панели инструментов. Это будет реализовано в программе Путем обработки события OnHint, для чего снова используется компонент АррЬ- cationEvents и копируется текущее состояние свойства Hint данного приложения в строку состояния: 0263
procedure TFormRichNote.ApplIcationEventslHint (Sender: TObject): begin StatusBarl.PanelstsbpMessage] Text : = Application.Hint: end. По умолчанию этот код отображает в строке состояния текст всплывающих подсказок, которые не создаются для пунктов меню. Свойство Hint можно исполь- зовать для того, чтобы задать разные строки для двух случаев, написав строку, раз- деленную на две части разделителем в виде вертикальной черты (|). Например, в качестве значения свойства Hint можно ввести: 'Wew|Create a new document' Первая часть строки (New) используется всплывающей подсказкой, а вторая часть (Create a new document) — строкой состояния. См. пример на рис. 6.6. An uitioduuio» to the teaiufes of the ftfchSai example, in Chapter 6 of th* "Mastering Delphi Written and copyrighted by Marco Cautu. Ths document explains how do you create a simple editor based on die RichEdit control using Delphi 6 The program has a toolbar and implements a number of features, including a complete scheme for opening and saving die text files, discussed in ths document In fact, we want to be able to ask die user to save any modified file before opening a new one, to avoid losing any changes Sounds like a professional application, doesn't it? File Operations The most complex part of this program is tmplemenimg the commands of die File pull-down menu- New. Open, Save, and Save As In each case, we need to track whether die current file has changed, > -- C1- k — -k—tj *1----- -r— *U- Л1 ~k Jy : <1 Рис. 6.6. Строка состояния примера RichBar отображает более подробное описание, чем всплывающая подсказка ПРИМЕЧАНИЕ-------------------------------------------------------------- Если подсказка элемента управления состоит из двух строк, можно использовать методы GetShortHint и GetLongHint для извлечения первой (короткой) и второй (длинной) подстрок из строки, которую вы передаете в качестве параметра, обычно являющегося значением свойства Hint Вторая панель отображает состояние клавиши Caps Lock, получаемое в резуль- тате вызова API-функции GetKeyState, которая возвращает число состояния. Если указано число младшего разряда (то есть если число нечетное), тогда клавиша на- жата. Я решил проверить это состояние, когда приложение не работает, поэтому тест выполняется при каждом нажатии клавиши, а также при достижении сообще- нием окна (в случае изменения пользователем этой установки при работе с другой программой). Я добавил к обработчику ApplicationEventslldle вызов пользователь- ского метода CheckCapslock, реализованный следующим образом: procedure TFormRichNote.CheckCapslock: begin if Odd (GetKeyState (VK_CAPITAL)) then StatusBarl.PanelslsbpCaps].Text := 'CAPS’ 0264
else StatusBarl.PanelsCsbpCapsJ.Text end. В заключение программа использует третью панель для отображения текущего положения курсора (измеряемого в строках и символах на строке) при каждом изменении выбора. Поскольку значения CaretPos отсчитываются от нуля (то есть, левый верхний угол — строка 0, символ 0), я добавил по одному к каждому значе- нию, чтобы сделать их более приемлемыми для случайного пользователя: procedure TFormRichNote.RichEditSelectionChange(Sender: TObject); begin // обновить положение в строке состояния StatusBar.Panels[sbpPosition].Text •= Format (.'Xd/Xd'. [RichEdit CaretPos.Y + 1. RichEdit.CaretPos.X + 1]); end; Темы и стили В прошлом основанные на использовании GUI операционные системы диктовали все элементы интерфейса пользователя для выполняемых в них программ. В на- стоящее время Linux разрешает пользователям настраивать внешний вид и воз- можности как главных окон приложений, так и элементов управления пользова- тельского интерфейса, например, кнопок. Аналогичная идея (часто используется термин skin (кожа, оболочка)) использовалась в многочисленных программах с таким размахом, что ее начала интегрировать даже Microsoft (сначала в програм- мах и затем во всей операционной системе целиком). Стили CLX Как уже упоминалось, в Linux (в XWindow, если быть более точными) пользова- тель, как правило, может выбрать стиль интерфейса элементов управления. Такой подход полностью поддерживается библиотекой Qt и встроенной поверх нее сис- темой KDE. Qt предлагает несколько основных стилей, как в интуитивно понят- ном интерфейсе Windows, стиль Motif, так и другие. Кроме того, пользователь может установить новые стили в систему и сделать их доступными для приложений. СОВЕТ----------------------------------------------------------------------—------- Рассматриваемые стили относятся к пользовательскому интерфейсу элементов управления, а не к формам и их границам. Они могут быть настроены в системах Linux, но технически являются отдельным элементом интерфейса Поскольку эта технология встроена в Qt, она также доступна в Windows-вер- сии этой библиотеки; CLX делает ее доступной для разработчиков Delphi, поэто- му Приложение может иметь интуитивно понятный интерфейс Motif в операцион- ной системе Microsoft. Глобальный объект CLX Application имеет свойство Style Для установки пользовательского стиля или стиля по умолчанию, показываемой: подсвойством Defaultstyle. Например, можно выбрать интерфейс Motif с помощьк следующего кода: 0265
Application Style Defaultstyle = dsMotif: В добавленной мной программе StylesDemo среди различных примеров элемен- тов управления есть окно списка с именами стилей по умолчанию, как показано в перечислении TDefaultStyle, и следующий код для его события OnDblClick: procedure TForml ListBoxlDblClick(Sender TObject). begin Application Style Defaultstyle - TDefaultStyle (ListBoxl Itemindex). end: Дважды щелкнув на окне списка, можно изменить текущий стиль приложения и сразу же увидеть его на экране (рис. 6.7). dsWmdows dsMotifPlus dsCDE dsQtSGI dsPlalnum dsSystemDefauft Рис. 6.7. Windows-приложение StylesDemo с редким размещением компонентов интерфейса Motif Темы Windows ХР В Windows ХР Microsoft представил новую, отдельную версию общей библиотеки элементов управления. Старая библиотека также доступна, и при выполнении в ХР программа может выбрать для использования любую из этих двух библиотек. Глав- ное отличие новой общей библиотеки элементов управления заключается в том, что она не имеет фиксированного механизма визуализации, основывается на ме- ханизме реализации тем ХР и делегирует пользовательский интерфейс элемен- тов управления текущей теме. В Delphi 7 VCL полностью поддерживает темы благодаря большому объему внутреннего кода и библиотеке управления темами, разработанной Майком Лиш- ке (Mike Lischke). Некоторые из этих новых функций визуализации используют- ся визуальными элементами управления архитектуры Action Manager, независи- мо от операционной системы. Однако полная поддержка тем доступна только в операционной системе, которая имеет такую возможность, — в настоящий мо- мент, Windows ХР. Даже в ХР приложения Delphi применяют обычный подход по умолчанию. Для поддержки тем ХР необходимо включить в программу файл манифеста (manifest file). Это можно сделать несколькими способами: 0266
О Поместите файл манифеста в папку, где находится приложение. Это XML-файл, указывающий на зависимости программы. Он имеет такое же имя, как и испол- няемая программа с дополнительным расширением .manifest в конце (напри- мер MyProgram.exe.manifest). Пример такого файла приведен в листинге 6.2. О Добавьте такую же информацию в файл ресурсов, скомпилированный внутри приложения. Следует написать файл ресурсов, включающий файл манифеста. В Delphi 7 VCL имеет двоичный файл ресурсов WindowsXP.res, получаемый в ре- зультате перекомпиляции файла WindowsXP.rc, доступного среди исходных фай- лов VCL. Этот исходный файл содержит файл sample.manifest, который также можно найти среди исходных файлов VCL. О Используйте компонент XpManifest, который добавлен в Delphi 7 с помощью Borland для дальнейшего упрощения этой задачи. Если перетащить этот ком- понент в форму программы, Delphi автоматически включит свой модуль ХРМап, который импортирует упомянутый ранее файл ресурсов VCL. ВНИМАНИЕ ------------------------------------------------------------------- При удалении из приложения компонента XpManifest также необходимо вручную удалить модуль ХРМап из оператора uses — Delphi этого не сделает. Если вы этого не сделаете, даже без компонен- та XpManifest программа по-прежнему будет привязана в файле манифеста ресурсов. Модуль все равно будет использоваться (очень удивительно, почему Borland выбирает создание компонента вместо того, чтобы просто предложить модуль или соответствующий файл ресурсов; причем этот компонент вообще не задокументирован). Листинг 6.2. Пример файла манифеста (pages.exe.manifest) <?xml version=”l 0" encoding="UTF-8" standalone="yes"?> «assembly xmlns="urn:schemas-microsoft-com:asm.vl" mamfestVersion-"1.0"> «assemblyidentity version="1.0 0.0" processorArchitecture="X86” name="Pages exe" type="win32" /> «description>Mastering Delphi Demo«/description> «dependency» «dependentAs sembly> «assemblyldentity type="win32" name="Microsoft.Windows.Common-Controls” version="6.0 0.0" processorArchitecture="X86" publ1cKeyToke n="6595b64144cc fId f“ language»"*" /> «/dependentAssembly» «/dependency» «/assembly» В качестве примера файл манифеста из листинга 6.2 добавлен в папку примера Pages, который был рассмотрен в начале этой главы. Выполняя его в Windows ХР со стандартной темой ХР, вы получите следующий результат (рис. 6.8). Можно сравнить его с рис. 6.1 и 6.2, которые отображают эту же программу в Windows 2000. 0267
Рис. 6.8. Пример Pages использует текущую тему Windows ХР, поскольку содержит файл манифеста (сравните с рис. 6.1) Компонент ActionList Архитектура событий Delphi очень открытая: можно написать отдельный обра- ботчик события и соединить его с событиями OnClick кнопки панели инструментов и меню. Этот обработчик также можно подключить к различным кнопкам и пунк- там меню, поскольку он использует параметр Sender для обращения к объекту, ко- торый запускается данным событием. Немного труднее синхронизировать состоя- ние кнопок панели инструментов и пунктов меню. Если пункт меню и кнопка панели инструментов включают одинаковый параметр, при каждом его включе- нии необходимо устанавливать флажок для пункта меню и переводить кнопку в нажатое положение. Для решения этой проблемы Delphi имеет архитектуру обработки событий на основе действий. Действие (action) (или команда) означает операцию, которую необходимо выполнить при щелчке на пункте меню или кнопке и которая опреде- ляет состояние всех элементов, связанных с этим действием. Соединение действия с пользовательским интерфейсом связанных элементов управления является очень важным и не должно недооцениваться, поскольку именно здесь реализуются пре- имущества данной архитектуры. Архитектура обработки событий включает множество элементов. Главная роль принадлежит объектам действия. Объект действия, как и многие другие компо- ненты, имеет имя и другие свойства, применяемые к связанным элементам управ- ления (именуемым клиенты действия (action clients)). Это свойства: Caption, графическое представление (Imageindex), состояние (Checked, Enabled и Visible) и об- ратная связь с пользователем (Hint и HelpContext). Кроме того, существуют свой- ство Shortcut и список Secondaryshortcuts, свойство AutoCheck для действий, имею- щих два состояния, свойства поддержки справки и свойство Category, используемое для включения действий в логические группы. Базовый класс для всех объектов действий — TBasicAction, указывающий основ- ное абстрактное поведение действия, без какой-либо конкретной привязки или 0268
коррекции (даже к пунктам меню или элементам управления). Производный класс TContainedAction представляет свойства и методы, которые позволяют действиям отобразиться в списке действий или диспетчере действий. Вторичный производ- ный класс TCustomAction представляет поддержку для тех свойств и методов пунк- тов меню и элементов управления, которые связаны с объектами действия. Нако- нец, существует готовый к применению производный класс TAction. Каждый объект действия соединен с одним или несколькими клиентскими объектами посредством объекта ActionLink. Несколько элементов управления, воз- можно, разных типов, могут иметь один общий объект действия, указанный в их свойстве Action. Технически объекты ActionLink поддерживают двунаправленное соединение между объектом клиента и действием. Объект ActionLink необходим, поскольку соединение работает в двух направлениях. Операция на объекте (на- пример, щелчок) передается на объект действия и затем в виде вызова — на его событие OnExecute; обновление состояния объекта действия отражается на соеди- ненных клиентских элементах управления. Другими словами, один или несколько клиентских элементов управления могут создать компонент ActionLink, который регистрирует себя с объектом действия. Не следует устанавливать свойства клиентских элементов управления, кото- рые вы связываете с действием, поскольку это действие переопределит значения свойства клиентских элементов управления. Поэтому обычно сначала следует на- писать действия, а затем создавать пункты меню и кнопки, которые вы хотите с ними связать. Учтите также, что если у действия нет обработчика OnExecute, эле- мент управления клиента автоматически отключается (или становится серым), если для свойства DisablelfNoHandler не будет установлено значение False. Связанные с действиями клиентские элементы управления — это обычно пун- кты меню и различные типы кнопок (кнопки включения, флажки, переключатели, кнопки быстрого вызова, кнопки панели инструментов и т. п.), но можно созда- вать новые компоненты, подключаемые к данной архитектуре. Разработчики ком- понентов могут даже определить новые действия, что будет сделано в главе 9, и но- вые объекты действия компоновки. Кроме клиентского элемента управления, некоторые действия могут иметь це- левой компонент. К конкретному целевому компоненту добавляются некоторые предопределенные действия. Другие действия автоматически ищут целевой ком- понент в форме, поддерживающей данное действие, начиная с активного элемента управления. Наконец, объекты действия содержатся в компоненте ActionList или Action- Manager, единственном классе базовой архитектуры, который отображается в па- литре компонента. Список действий получает исполнительные действия, которые не обрабатываются конкретными объектами действия, запускаемыми On Execute- Action. Даже если список действий не обрабатывает действие, Delphi вызывает событие OnExecuteAction объекта Application. У компонента ActionList имеется специальный редактор, который используется для создания нескольких дей- ствий (рис 6.9). В редакторе действия отображаются в группы в соответствии с их свойством Category. Простая установка нового значения для этой категории дает редактору Указание представить новую категорию. Данные категории в основном представ- ляют собой логические группы, однако в некоторых случаях группа действий может 0269
функционировать только на определенном типе целевого компонента. Можно оп- ределить категорию для каждого раскрывающегося меню или сгруппировать их некоторыми другими логическими способами. Рис. 6.9. Редактор компонента ActionList со списком предопределенных действий Предопределенные действия в Delphi С помощью списка действий и редактора Action Manager можно создать новое дей- ствие или выбрать одно из существующих и зарегистрированных в системе. Пос- ледние перечислены во вторичном диалоговом окне (рис. 6.9). Существует множе- ство предопределенных действий, которые можно разделить на логические группы: о Действия с файлами включают: open (открыть), save as (сохранить как), open with (открыть с помощью), run (выполнить), print setup (печать) и exit (закрыть). о Действия редактирования показаны в следующем примере. Включают: cut (вы- резать), сору (копировать), paste (вставить), select all (выделить все), undo (от- менить) и delete (удалить). о Действия RichEdit дополняют действия редактирования для элементов управ- ления RichEdit и включают стили bold (полужирный), italic (курсив), underline (подчеркнутый), strikeout (зачеркнутый), bullets (маркеры абзаца) и различные действия выравнивания. о Действия окна MDI рассмотрены в главе 8, где изучается многодокументный интерфейс. Включают все наиболее общие операции MDI: arrange (упорядо- чить), cascade (каскадом), close (закрыть), tile (horizontally или vertically) (раз- местить в виде мозаики (по горизонтали или вертикали)) и minimize all (свер- нуть все). 0270
О Действия с наборами данных связаны с таблицами и запросами БД и будут рассмотрены в главе 13. Существует множество действий, включающих все ос- новные операции с наборами данных. Delphi 7 добавляет к действиям с набора- ми данных группу действий, специально приспособленных к компоненту Client? DataSet, включая apply (применить), revert (вернуться к предшествующему состоянию) и undo (отменить). Более подробно эти действия рассматриваются в главах 13 и 14. О Действия справки разрешают активизировать страницу оглавления или пред- метный указатель файла справки, присоединенного к приложению. О Действия поиска включают: find (найти), find first (найти первое вхождение), find next (найти далее) и replace (заменить). О Действия вкладки и управления страницей включают: переход к предыдущей странице и к следующей странице. О Действия с диалогами активизируют диалоги color (цвет), font (шрифт), open (открыть), save (сохранить) и print (печать). О Действия со списками включают: clear (очистить), сору (копировать), move (пе- реместить), delete (удалить) и select all (выделить все). Эти действия разреша- ют взаимодействовать с элементами управления списком. Другая группа дей- ствий, включая статический список, виртуальный список и некоторые классы поддержки, позволяет определять списки, которые можно подключить к ин- терфейсу пользователя. Подробнее эта тема рассматривается в разделе «Исполь- зование действий со списками» в конце этой главы. О Действия с Интернетом включают просмотр URL, загрузку URL и отправку почты. О Действия с инструментами настройки включают только диалог для настройки панелей действий. Кроме обработки события On Execute и отражения изменения состояния действия в клиентских элементах управления, действие может обрабатывать событие On- Update, которое активизируется, когда приложение не выполняется. Это дает воз- можность проверить состояние приложения или системы и соответствующим об- разом изменить пользовательский интерфейс элементов управления. Например, стандартное действие PasteEdit включает клиентские элементы управления, толь- ко если буфер обмена содержит какой-либо текст. Практическое применение действий Рассмотрим пример с использованием этой важной функции Delphi. Программа называется Actions и демонстрирует некоторые возможности архитектуры действий. Он создан путем размещения нового компонента Action List в его форме и добавле- ния трех стандартных действий редактирования и нескольких пользовательских. Форма также имеет панель с несколькими кнопками быстрого вызова, главное меню и элемент управления Мето (автоматическая цель действий редактирования). Листинг 6.3 — это список действий, извлеченный из DFM-файла. 0271
Листинг 6.3. Действия примера Actions object Actionlistl: TActionList Images - ImageLlstl object ActionCopy: TEditCopy Category - 'Edit' Caption - '&Copy‘ Shortcut • <CtrI+C> end object ActionCut: TEditCut Category = 'Edit' Caption = 'Cu&t' Shortcut = <Ctrl+X> end object ActionPaste: TEditPaste Category = 'Edit' Caption = '&Paste' Shortcut = <Ctrl+V> end object ActionNew: TAction Category = 'File' Caption = '&New' Shortcut - <Ctrl+N> OnExecute = ActionNewExecute end object ActionExit: TAction Category - 'File' Caption = 'E&xit' Shortcut - <Alt+F4> OnExecute = ActionExitExecute end object NoAction: TAction Category = 'Test' Caption = '&No Action' end object ActionCount: TAction Category = 'Test' Caption = '&Count Chars' OnExecute = ActionCountExecute Onllpdate = ActionCountUpdate end object ActionBold: TAction Category = 'Edit' AutoCheck = True Caption - '&Bold' Shortcut = <Ctrl+B> OnExecute = ActionBoldExecute end object ActionEnable: TAction Category = 'Test' Caption = '&Enable NoAction' OnExecute = ActionEnableExecute end object Actionsender: TAction Category = 'Test' Caption = 'Test &Sender' OnExecute - ActionSenderExecute end end 0272
СОВЕТ------------------------------------------------------------------------ Клавиши быстрого выбора команд сохраняются в DFM-файле с помощью виртуальных номеров кла- виш, которые также включают значения для клавиш Ctrl и Alt. В этом и других листингах книги я заменил эти номера буквенными символами, заключив их в угловые скобки. Все эти действия связаны с пунктами компонента MainMenu, а некоторые из них — также с кнопками элемента управления панели инструментов. Учтите, что изображения, выбранные в элементе управления ActionList, влияют на действия только в редакторе (рис. 6.10). Для отображения изображений ImageList в пунктах меню и кнопках панели инструментов нужно также выбрать список изображений в MainMenu и в компонентах панели инструментов. / Editing .ActtonltsU х| (No Category) - t J^ActionCopy Q^ActionCut ActionPaste C) AcfonNew [] ActionExit NoAction Я ActionCount AA ActionB old ActonEnable ActionSender Рис. 6.10. Редактор ActionList примера Actions У трех указанных предопределенных действий для меню Edit (Редактирование) нет присвоенных обработчиков, но эти специальные объекты имеют внутренний код для выполнения соответствующего действия на активном элементе управле- ния редактирования или компоненте memo. Эти действия также включаются или выключаются в зависимости от содержимого буфера обмена и наличия выбранно- го текста в активном элементе управления редактирования. Большинство других действий имеют пользовательский код, за исключением объекта NoAction; поскольку он не имеет кода, соединенные с этой командой пункт меню и кнопка отключены, даже если для свойства Enabled этого действия указано значение True. Я добавил к примеру и к меню Test другое действие, которое включает пункт Меню, соединенный с объектом NoAction: procedure TForml. Act юпЕпаЫ eExecute (Sender: TObject); begin NoAction.DisablelfNoHandler False; NoAction.Enabled True; ActionEnable.Enabled False: end; Установка для Enabled значения True дает результат только на короткое время, пока не будет установлено свойство Disable-IfNoHandler, как указывалось в преды- дущем разделе. Выполнив это, вы отключаете текущее действие, поскольку нет Необходимости снова выполнять ту же команду. 0273
Это отличается от действия, которое можно включить, например, пункт меня редактирования Bold и соответствующая кнопка быстрого вызова. Вот код действж Bold (свойство AutoCheck которого имеет значение True, поэтому нет необходимо- сти изменять состояние свойства Checked в коде): / procedure TForml.ActionBoldExecute(Sender- TObject). begin with Memol Font do if fsBold in Style then Style := Style - [fsBold] el se Style .= Style + [fsBold]: end. У объекта ActionCount очень простой код, но он показывает обработчик OnUpdate; если элемент управления memo пуст, он автоматически отключается. Такой же эффект можно получить, обработав само событие OnChange элемента управления memo, но в целом не всегда возможно определить состояние элемента управления с помощью простой обработки одного из его событий. Вот код для двух обработчи- ков этого действия: procedure TForml ActionCountExecute(Sender: TObject): begin ShowMessage (’Characters: ’ + IntToStr (Length (Memol.Text))): end: procedure TForml.ActionCountUpdate(Sender: TObject): begin ActionCount.Enabled .= Memol.Text <> ”: end. Наконец, я добавил специальное действие для проверки объекта-отправителя обработчика события и получения некоторой другой системной информации. Кроме указания имени и класса объекта, я добавил код, дающий доступ к объекту списка действий. Я сделал это вручную, чтобы показать, как получить доступ к информации: procedure TForml ActionSenderExecute(Sender TObject): begin Memol Lines.Add (.'Sender class: ' + Sender.ClassName): Memol.Lines.Add (‘Sender name: ' + (Sender as TComponent).Name). Memol.Lines Add ('Category: ' + (Sender as TAction) Category): Memol Lines.Add ( 'Action list name: ' + (Sender as TAction).ActionList Name); end. Просмотрите вывод этого кода и пользовательский интерфейс примера (рис. 6.11). Учтите, что Sender — это не выбранный вами пункт меню, даже если обработ- чик события соединен с ним. Запускающий это событие объект Sender — это дей- ствие, которое перехватывает операцию пользователя. Наконец, помните, что можно также написать обработчики для событий само- го объекта ActionList, играющие роль глобальных обработчиков для всех действий списка и для глобального объекта Application, который запускается для всех дей- ствий приложения. Перед вызовом события OnExecute указанного действия Delphi активизирует событие OnExecute ActionList и событие OnActionExecute глобального объекта Application. Эти события могут оценить действие, выполнить какой-либо общий код и затем прекратить выполнение (с помощью параметра Handled) или позволить ему перейти к следующему уровню. 0274
Рис. 6.11. Пример Actions с подробным описанием отправителя события OnExecute объекта действия Если отсутствует обработчик события, назначенный для ответа на действие на уровне списка действий, приложения или действия, то приложение пытается иден- тифицировать целевой объект, к которому можно применить это действие. СОВЕТ----------------------------------------------------------------------- При выполнении действие ищет элемент управления, выполняющий роль цели действия, просмат- ривая элемент управления, активную форму и другие элементы управления на форме. Например, действия редактирования обращаются к текущему активному элементу управления (если они на- следуются из TCustomEdit), а элементы управления наборами данных ищут наборы данных, соеди- ненные с источником данных информационного элемента управления, имеющего фокусировку на ввод. Другие действия используют иные способы поиска целевого компонента, но общая идея под- держивается большинством стандартных действий Панель инструментов и компонент ActionList редактора В главе 5 был создан пример RichBar для иллюстрации создания редактора с па- нелью инструментов и строкой состояния. Конечно, следует также добавить к форме строку меню, но это создаст некоторые проблемы синхронизации состояния кно- пок панели инструментов и пунктов меню. Для решения этой проблемы рекомен- дуется использовать действия, как это было сделано в примере MdEditl, рассмот- ренном в данном разделе. Указанное приложение основано на компоненте ActionList, которое включает действия для обработки файлов и поддержки буфера обмена с кодом, аналогич- ным версии RichBar. Выбор цвета и типа шрифта также выполняется с помощью полей со списками, поэтому действия здесь не применяются — то же самое отно- сится и к раскрывающемуся меню кнопки Size (Размер). Это меню, однако, имеет несколько дополнительных команд, в том числе для подсчета символов и измене- ния цвета фона. Эти команды основаны на действиях, это же относится и к трем новым кнопкам выравнивания абзаца (и командам меню). 0275
Одно из главных отличий новой версии заключается в том, что код никогда нё обращается к состоянию кнопок панели инструментов, но в конечном итоге изме- няет состояние действий. В других случаях я использовал события OnUpdate. Hi- пример, метод RichEditSelectionChange не обновляет состояние кнопки Bold, кото- рая соединена с действием с помощью следующего обработчика OnUpdate: procedure TFormRichNote.acBoldUpdate(Sender; TObject); begin acBold.Checked := fsBold in RichEdit.SeiAttributes.Style; end; Аналогичным образом обработчики события OnUpdate доступны для большин- ства действий, включающих операции подсчета (доступна только при наличии тек- ста в элементе управления RichEdit), операцию Save (Сохранить) (доступна, только если текст был изменен) и операции Cut (Вырезать) и Сору (Копировать) (доступ- ны, только если выбран текст): procedure TFormRichNote.acCountcharsUpdate(Sender: TObject); begin acCountChars.Enabled ;= RichEdit.GetTextLen > 0; end; procedure TFormRichNote.acSaveUpdate(Sender: TObject); begin acSave.Enabled := Modified; end: procedure TFormRichNote.acCutUpdate(Sender: TObject); begin acCut.Enabled := RichEdit.SelLength > 0; acCopy.Enabled := acCut.Enabled; end; В старом примере состояние кнопки Paste (Вставить) было обновлено в собы- тии Onldle объекта Application. При использовании действий это можно преобразо- вать в другой обработчик OnUpdate (подробнее об этом коде см. в главе 5): procedure TForfflRichNote.acPasteUpdate(Sender: TObject); begin acPaste.Enabled SendMessage (RichEdit.Handle, em_CanPaste, 0. 0) <> 0; end; Три кнопки выравнивания абзаца и соответствующие пункты меню действуют как переключатели: они взаимно исключают друг друга и один из трех параметров всегда выбран. Поэтому параметр Grouplndex действий равен 1, свойство RadioItem соответствующих пунктов меню имеет значение True, свойство Grouped трех кно- пок панели инструментов — True и свойство AllowAllUp — False. (Визуально они так- же заключены между двумя разделителями.) Такие установки необходимы, поскольку в соответствии с текущим стилем про- грамма может присвоить действию свойство Checked, что исключает непосредствен- ную отмену проверки для двух других действий. Этот код является частью собы- тия OnUpdate списка действий, поскольку применяется к нескольким действиям: procedure TFormRichNote.ActionListUpdate(Action: TBasicAction; var Handled: Boolean); begin // проверить правильное выравнивание абзаца 0276
case RichEdlt.Paragraph.Alignment of taLeftJustify: acLeftAligned.Checked := True; taRightJustify: acRightAligned.Checked := True; taCenter: acCentered.Checked := True: end; // проверяет состояние блокировки клавиш CheckCapslock: end; В результате если выбрана одна из этих кнопок, то для определения надлежа- щего выравнивания общий обработчик событий использует значение Тад, установ- ленное для соответствующего значения перечисления TAlignment: procedure TFormRichNote.ChangeAlignment(Sender: TObject): begin RichEdit.Paragraph.Alignment := TAlignment ((Sender as TAction).Tag); end; Контейнеры панели инструментов Большинство современных приложений имеют несколько панелей инструментов, как правило, размещенных в специальном контейнере, Microsoft Internet Explorer, различные стандартные коммерческие программы и Delphi IDE — все используют этот общий подход, по-разному, однако, реализуя его. Delphi имеет два готовых к использованию контейнера панели инструментов: О Компонент CoolBar — общий элемент управления Win32, представленный Inter- net Explorer и используемый некоторыми приложениями Microsoft, О Компонент ControlBar полностью основан на VCL и не зависит от внешних биб- лиотек. Оба компонента могут включать элементы управления панели инструментов и некоторые дополнительные элементы, например, поля со списками и другие эле- менты управления. Панель инструментов может также заменить меню приложе- ния. Поскольку компонент CoolBar редко используется в приложениях Delphi, он рассмотрен во врезке «Действительно „крутая” панель инструментов»; а в следую- щих разделах упор сделан на изучении компонента Delphi ControlBar. Действительно «крутая» панель инструментов Общий элемент управления Win32 CoolBar, главным образом, представляет собой коллекцию объектов TcoolBand, которые можно активизировать с по- мощью редактора свойства Bands, доступного в пунктах меню редактора ком- понента или с помощью Object TreeView, Компонент CoolBar настраивается несколькими способами. Можно установить в качестве фона bitmap-изоб- ражение, добавить несколько полос в коллекцию Bands и затем назначить каждой полосе существующий компонент или контейнер компонентов. Можно использовать любой оконный элемент управления (неграфические элементы управления), но только некоторые из них будут отображены дол- жным образом. Например, для установки в качестве фона CoolBar bitmap- — — ----- ---- ...... , „ ,— — . продолжение 0277
Действительно «крутая» панель инструментов (продолжение) изображения необходимо использовать частично прозрачные элементы уп- равления. Обычным используемым в CooLBar компонентом является панель инструментов, но применяются и поля со списками, окна редактирования и анимационные элементы управления. Можно разместить по одной полосе на каждой строке или все полосы на одной строке. Каждая будет использо- вать часть доступного пространства и будет автоматически увеличиваться при щелчке на ее заголовке. Этот компонент легче использовать, чем объяс- нять его. Попробуйте сделать это сами или откройте пример CoolDemo: ТеюЯмг J9 Ptoy^ouM ' ffflConior RflM S«o|?oi Ойг««тяии»гпиггт ~ Jp This i» the text C#veiyhure ifyou compart» jt will» the really cod toolbar at the top of this f< '• m. Форма примера CoolDemo содержит компонент TCoolBar с четырьмя поло- сами, по две на каждой линии. Первая полоса включает набор панели инст- рументов предыдущего примера, с добавлением ImageList для выделенных изображений. Вторая имеет окно редактирования, используемое для уста- новки шрифта текста; третья содержит компонент ColorGrid, применяемый для выбора цвета шрифта и цвета фона. Последняя полоса включает эле- мент управления поле со списком доступных шрифтов. Интерфейс пользователя компонента CooLBar приятен, и Microsoft исполь- зует его в своих приложениях, но альтернативные варианты, например, ком- понент ControlBar, предлагают аналогичный беспроблемный интерфейс поль- зователя. Элемент управления Windows CooLBar имел множество разных и несовместимых версий, поскольку Microsoft выпускал различные версии общей библиотеки элемента управления с разными версиями Internet Explorer. Некоторые из этих версий «ломали» существующие программы, созданные с помощью Delphi — достаточная причина для того, чтобы не ис- пользовать ее в настоящее время, даже если она стала стабильнее. ControlBar ControlBar — это контейнер элементов управления. Он создается путем размеще- ния в нем других элементов управления, как и в случае с панелью (здесь нет спис- ка Bands). Каждый размещенный в панели элемент управления (даже отдельная кнопка) получает собственную область перетаскивания или «граббер* (grabber) (небольшая панель с двумя вертикальными линиями слева от элемента управле- ния): 0278
По этой причине в целом следует избегать размещения конкретных кнопок внут- ри ControlBar, рекомендуется добавлять контейнеры с включенными в них кнопка- ми. Вместо панели следует использовать один элемент управления панели инст- рументов для каждого раздела ControlBar. Пример MdEdit2 — другой вариант демонстрационной версии, разработанной ранее в этой главе для изучения компонента ActionList. Я сгруппировал кнопки в трех панелях инструментов (вместо одной) и оставил два поля со списками как автоном- ные элементы управления. Все эти компоненты находятся внутри ControlBar, поэтому пользователь может размещать их во время выполнения программы (рис. 6.12). Рис. 6.12. Пример MdEdit2 во время выполнения, пока пользователь изменяет расположение элементов панели инструментов в панели элементов управления Следующий фрагмент DFM-листинга примера Md Edit2 показывает, как разные панели инструментов и элементы управления внедряются в компонент ControlBar: object Control Bari: TControlBar Align = alTop AutoSize = True ShowHint = True PopupMenu = BarMenu object ToolBarFile: TToolBar Flat = True Images = Images Wrapable = False object ToolButtonl: TToolButton Action = acNew 0279
end // еще кнопки... end object ToolBarEdit: TToolBar... object ToolBarFont: TToolBar.. object ToolBarMenu: TToolBar AutoSize = True Flat = True Menu = MainMenu end object ComboFont: TComboBox Hint = 'Font Family' Style - csDropDownList OnClick = ComboFontClick end object ColorBoxl: TColorBox. . end Чтобы получить стандартный результат, необходимо отключить границы эле- ментов управления панели инструментов и установить для них однородный стиль. Установить одинаковый размер для всех элементов управления, чтобы одна или две строки элементов были одной высоты, не так легко, как кажется на первый взгляд. Некоторые элементы управления имеют автоматическое определение раз- меров или различные ограничения. В частности, чтобы поле со списком было та- кой же высоты, как и панели инструментов, следует точно настроить тип и размер его шрифта. От изменения размеров самого элемента управления эффекта не будет. Компонент ControlBar также имеет контекстное меню, которое позволяет пока- зать или скрыть каждый из элементов управления, находящийся в нем в настоя- щее время. Вместо написания специального кода для этого примера я реализовал более общее (с возможностью повторного использования) решение. Контекстное меню ВагМепи в ходе разработки пустое и заполняется с запуском программы: procedure TFormRichNote.FormCreate(Sender: TObject); var I: Integer: mltem: TMenuItem; begin , // заполнить меню панели элементов управления for I := 0 to Control Bar.Control Count - 1 do begin mltem TMenuItem.Create (Self): mltem.Caption := ControlBar Controls [I].Name; mltem.Tag := Integer (ControlBar.Controls [I]); mltem.OnClick := BarMenuClick; BarMenu.Items.Add (mltem); end: Процедура BarMenuClick — это отдельный обработчик события, используемый всеми пунктами меню; он использует свойство Тад пункта меню Sender для обраще- ния к элементу ControlBar, связанному с этим пунктом в методе FormCreate: procedure TFormRichNote.BarMenuClick(Sender: TObject). var aCtrl: TControl; begin 0280
aCtrl TControl ((Sender as TComponent).Tag); aCtrl.Visible not aCtrl.Visible: end; В итоге событие OnPopup этого меню используется для обновления флажка вы- бора пунктов меню: procedure TFormRichNote.BarMenuPopup(Sender: TObject); var I: Integer; begin // обновить флажки выбора пунктов меню for I := 0 to BarMenu.Items.Count - 1 do BarMenu.Items [I].Checked ;= TControl (BarMenu.Items [I].Tag).Visible; end; Меню в Control Bar Если посмотреть на интерфейс пользователя приложения MdEdit2 (рис. 6.12), вы обратите внимание, что меню формы появляется внутри панели инструментов, размещенной в панели элементов управления, ниже заголовка приложения. Необ- ходимо только установить свойство Menu панели инструментов. Также нужно уда- лить главное меню из свойства Menu формы (сохраняя на форме компонент Main- Menu), чтобы на экране не было двух копий меню. Поддержка стыковки в Delphi Другая доступная в Delphi функция — поддержка стыкуемых (dockable) панелей инструментов и элементов управления. Другими словами, можно создать панель инструментов и передвинуть ее к любой стороне формы или даже в свободное ме- сто на экране, отстыковав ее. Однако правильная настройка программы для полу- чения нужного эффекта достаточно сложна. Поддержка стыковки в Delphi связана с контейнерными элементами управле- ния, а не только с формами. Панель, ControlBar и другие контейнеры (технически любой элемент управления, извлеченный из TWinControl) может быть настроен как цель стыковки путем включения его свойства DockSite. Этим контейнерам также можно установить свойство AutoSize, чтобы они отображались только в том случае, если содержат элемент управления. Чтобы перетащить элемент управления (объект любого производного от Tcontrol класса) на место стыковки, просто установите для его свойства DragKind значение dkDock и для свойства DragMode — dmAutomatic. Таким способом можно перетащить элемент управления из его текущего положения в новый стыковочный контейнер. Для отстыковки компонента и его перемещения в специальную форму для его свой- ства RoatingDockSiteClass устанавливается значение TCustomDockForm (для исполь- зования предопределенной автономной формы с небольшим заголовком). Все операции по стыковке и отстыковке отслеживаются с помощью специаль- ного события перетаскиваемого компонента (OnStartDock и OnEndDock) и компонен- та, Получающего пристыкованный элемент управления (OnDockOver и OnDockDrop). Эти события стыковки очень похожи на события перетаскивания в ранних верси- ях Delphi. Для выполнения операций стыковки в коде и для изучения состояния стыко- вочного контейнера можно использовать команды. Каждый элемент управления 0281
может быть перемещен на другое место с помощью методов Dock, ManualDock и ManualFloat. У контейнера есть свойство DockClientCount, показывающее число при- стыкованных элементов управления, и свойство DockClients, которое представляет собой перечень этих элементов управления. Кроме того, если для свойства UseDockManager стыковочного контейнера уста- новлено значение True, имеется возможность использовать свойство DockManager, реализующее интерфейс IDockManager. Этот интерфейс имеет много функций, при- меняемых для настройки поведения стыковочного контейнера, включая поддерж- ку непрерывного изменения его состояния. Очевидно, что поддержка стыковки в Delphi основана на большом количестве свойств, событий и методов и имеет больше возможностей, чем было рассмотрено. В следующем примере показаны основные функции, которые нам понадобятся. СОВЕТ-------------------------------------------------------------------------- В настоящее время поддержка стыковки в VisualCLX на любой платформе невозможна. Стыкуемые панели инструментов в ControlBars Уже рассмотренный пример MdEdit2 включает поддержку стыковки. Программа имеет вторую панель ControlBar внизу формы, которая принимает перетягивание одной из панелей инструментов в панель ControlBar наверх. Поскольку для обоих контейнеров панели инструментов свойство AutoSize имеет значение True, они ав- томатически удаляются, если хост не содержит элементов управления. Кроме того, для свойств AutoDrag и AutoDock обеих панелей ControlBar установлено значение True. Я вынужден был разместить нижнюю панель ControlBar внутри панели вместе с эле- ментом управления RichEdit. Без этого панель ControlBar, будучи активизирована и ав- томатически изменив размеры, удерживается ниже строки состояния, что является неправильным поведением. В данном примере ControlBar — только элемент управле- ния панели, выровненный по низу, поэтому возможной путаницы не возникает. Чтобы позволить пользователям перетащить панели инструментов из исход- ного контейнера, нужно еще раз (как указывалось ранее) установить для их свойств DragKind значение dkDock и для свойств DragMode — значение dmAutomatic. Суще- ствует два исключения — панель инструментов меню, которую я решил размес- тить рядом с обычным положением строки меню, и элемент управления ColorBox, поскольку в отличие от поля со списком для этого компонента не предлагаются свойства DragMode и DragKind. В методе FormCreate данного примера вы найдете код для активизации стыковки компонента, основанный на «protected hack» (см. гла- ву 2). Поле со списком Fonts может быть перетянуто, но я не хочу разрешать пользо- вателю пристыковывать его в нижней панели элементов управления. Для реали- зации этого ограничения использован обработчик события OnDockOver панели элементов управления путем принятия операции стыковки только для панелей инструментов: procedure TFormRichNote.ControlBarLowerDockOverlSender. TObject: Source- TDragDockObject: X. Y: Integer: State. TDragState: var Accept: Boolean): begin • Accept := Source.Control is TToolbar: end: 0282
ВНИМАНИЕ--------------------------------—--------------------------------- Перетягивание панели инструментов непосредственно из верхней панели элементов управления в нижнюю не будет выполнено. Панель элементов не изменит размер для размещения панели инст- рументов во время операции перетягивания, как это происходит при перетягивании панели инстру- ментов сначала в плавающую форму и затем в нижнюю панель элементов управления. Это проблема VCL, которую очень трудно решить. Из следующего примера видно, что MdEdit3 функционирует как предполагалось, даже если он имеет такой же код: он использует различные компоненты с разными кодами поддержки VCL! При перемещении одной из панелей инструментов за пределы любого контей- нера Delphi автоматически создает плавающую форму. Попытка вернуть ее назад, закрыв плавающую форму, будет неудачной, поскольку плавающая форма удаля- ется вместе с панелью инструментов, которую она содержит. Однако для показа этой скрытой панели инструментов можно использовать контекстное меню самой верхней панели ControlBar, также присоединенной к другой ControlBar. Созданная Delphi для размещения отстыкованных элементов управления пла- вающая форма имеет узкий заголовок, так называемый заголовок панели инстру- ментов (toolbar caption), в котором по умолчанию нет текста. Поэтому я добавил код к событию OnEndDock каждого стыкуемого элемента управления для настрой- ки заголовка вновь созданной формы, к которой пристыкован данный элемент управления. Во избежание пользовательской структуры данных для этой инфор- мации использован текст свойства Hint для данных элементов управления (кото- рый в основном не применяется) с целью обеспечения подходящего заголовка: procedure TFormRichNote.EndDock(Sender. Target: TObject: X. Y: Integer); begin if Target is TCustomForm then TCustOTiForm(Target).Caption := GetShortHint((Sender as TControl).Hint); end. Пример приведен в программе MdEdit2 (рис. 6.13). Рис. 6.13. Пример MdEdit2 разрешает пристыковать панели инструментов (но не меню) сверху или снизу формы или оставить их плавающими Дополнение к примеру (которое я не сделал) может добавить области стыков- ки к двум сторонам формы. Чтобы повернуть панели инструментов из горизон- тального положения в вертикальное, может потребоваться рутинная программа. Для этого после отключения автоматической установки размера необходимо пе- реключить свойства Left и Тор каждой кнопки. 0283
284 Управление операциями стыковки Delphi предлагает множество событий и методов, обеспечивающих управление операциями стыковки, включая диспетчер стыковки Для знакомства с некоторы- ми из этих возможностей изучите пример DockTest, который представляет собой тест для операций стыковки (рис 6 14) Программа обрабатывает события OnDockOver и OnDockDrop панели размещения стыковочных элементов для вывода на экран сообщений для пользователя, напри- мер, количество пристыкованных в настоящее время элементов управления. procedure TForml PanelIDockDropCSender TObject Source TDragDockObject X V Integer) begin Caption = Docked ' + IntToStr (Panel 1 DockClientCount) end Рис. 6.14. Пример DockTest с тремя элементами управления, пристыкованными к главной форме Аналогичным образом программа обрабатывает события стыковки главной формы Элементы управления имеют контекстное меню, вызываемое для опера- ций стыковки и отстыковки в коде (без обычного перетягивания мышью) с по- мощью следующего кода procedure TForml menuFloatPanelClick(Sender TObject) begin Panel 2 Manual Fl oat (Rect (100 100 200 300)) end procedure TForml FloatinglClick(Sender TObject) var aCtrl TControl begin aCtrl = Sender as TControl // включить плавающее состояние 0284
285 if aCtrl Floating then aCtrl ManualDock (Panell ml alBottom) else aCtrl ManualFloat (Rect (100 100 200 300)) end Чтобы при запуске программа выполнялась правильно, следует пристыковать элементы управления к главной панели в исходном коде, в противном случае ре- зультат непредсказуем Как ни странно, для нормальной работы программы необ- ходимо добавить элементы управления к диспетчеру стыковки и также пристыко- вать их к панели (одна операция автоматически не запускает другую) // пристыковать memo Memol Dock(Panell Rect (0 0 100 100)) Panell DockManager Insertcontrol(Memol alTop Panell) 11 пристыковать список ListBoxl Dock(Panell Rect (0 100 100 100)) Panell DockManager InsertControl(ListBoxl alLeft Panell) // dock pane 12 Panel2 Dock(Panell Rect (100 0 100 100)) Panell DockManager InsertControl(Panel2 alBottom Panell) Последняя функция примера, возможно, самая интересная и наиболее слож- ная для правильной реализации При каждом закрытии программы сохраняется стыковочное состояние панели с помощью поддержки диспетчера стыковки При повторном открытии программа снова применяет информацию стыковки, восста- навливая предыдущую конфигурацию окна Ниже приведен код для сохранения и загрузки procedure TForml FormDestroy(Sender TDbject) var FileStr TFileStream begin if Panell DockClientCount > 0 then begin FileStr = TFileStream Create (DockFileName fmCreate or fmOpenWrite) try Panell DockManager SaveToStream (FileStr) finally FileStr Free end end else II удалить файл DeleteFile (DockFileName) end procedure TForml FormCreate(Sender TObject) var FileStr TFileStream begin // код инициализации выше II перезагрузить настройки DockFileName = ExtractFilePath (Application Exename) + dock dck if FileExists (DockFileName) then begin FileStr = TFileStream Create (DockFileName fmOpenRead) 0285
try Panel 1.DockManager.LoadFromStream (Fi1 eStr): finally FileStr.Free: end: end: Panel 1.DockManager.ResetBounds (True): end: Данный код прекрасно работает, пока в исходном состоянии все элементы уп- равления пристыкованы. Если при сохранении программы один элемент управле- ния плавающий, вы не увидите его, если перезагрузите настройки. Однако посколь- ку код инициализации вставлен раньше, элемент управления будет в любом случае пристыкован к панели и появится при перетаскивании других элементов управле- ния. Без сомнения, это запутанная ситуация. Поэтому после загрузки настроек я добавил этот дополнительный код: for 1 : = Panell.DockClientCount - 1 downto 0 do begin aCtrl := Panell.DockClients[i]: Panel 1.DockManager.GetControlBounds(aCtrl. aRect): if (aRect.Bottom - aRect.Top <= 0) then begin aCtrl.ManualFloat (aCtrl.ClientRect); Panel 1.DockManager.RemoveControl(aCtrl): end: end: Полный листинг содержит много кода с комментариями, который я использо- вал при разработке этой программы; его можно использовать для понимания про- исходящего (которое зачастую отличается от того, что предполагалось). Короче говоря, элементы управления, у которых нет набора размеров в диспетчере стыковки (единственный способ, которым можно вычислить, что они не пристыкованы), ото- бражаются в плавающем окне и удаляются из списка диспетчера стыковки. Если взглянуть на полный код обработчика события OnCreate, вы увидите мно- го сложного кода, необходимого для получения ясного поведения. К программе стыковки можно добавить больше функций, но для этого во избежание конфлик- тов следует удалить другие функции. Добавление пользовательской стыковочной формы нарушает функции диспетчера стыковки. Автоматическое выравнивание перестает нормально работать с кодом диспетчера стыковки для восстановления положения. Рекомендуется изучить поведение этой программы, усовершенство- вав ее для поддержки предпочитаемого вами типа пользовательского интерфейса. СОВЕТ------------------------------------------------------------------------------------ Помните, что хотя стыковочные панели делают вид приложения более приятным, некоторые пользо- ватели испытывают неудобство потому, что их панели инструментов могут исчезать или находиться не в том положении, как обычно. Не злоупотребляйте возможностями стыковки, иначе некоторые неопытные пользователи могут потеряться. Стыковка к PageControl Другая интересная функция элементов управления страницей — их специальная поддержка стыковки. При стыковке нового элемента управления к PageControl для 0286
его размещения автоматически добавляется новая страница, что можно легко уви- деть в среде Delphi. Для этого установите PageControl как хост стыковки и активи- зируйте стыковку для клиентских элементов управления. Лучше всего применять эту технологию, если имеются вторичные формы для использования в качестве хостов. Более того, если нужно переместить PageControl целиком в плавающее окно и затем пристыковать его назад, необходима панель стыковки в главной форме. Это было сделано в примере DockPage, который имеет главную форму со следу- ющими настройками: object Forml: TForml ption = 'Docking Pages' ject Panel 1: TPanel n - al Left Site = True useDown = PanelIMouseDown ct PageControl1: TPageControl ActivePage = TabSheetl Align = alClient DockSite = True DragKind = dkDock object TabSheetl: TTabSheet Caption = 'List' object ListBoxl: TListBox Align = alClient end end end end object Splitterl: TSplitter Cursor = crHSplit end object Memol: TMemo Align = alClient end end Учтите, что свойство Use DockManager панели имеет значение True и на PageControl постоянно размещается страница с окном списка, поэтому при удалении всех стра- ниц код, используемый для автоматической установки размера стыковочных кон- тейнеров, может вызвать проблемы. Программа имеет две другие формы с аналогичными настройками (хотя на них размещаются разные элементы управления): object Form2: TForm2 Caption = ‘Small Editor' DragKind = dkDock DragMode = dmAutomatic object Memol: TMemo Align = alClient end end Можно перетащить эти формы на элемент управления страницей для добавле- ния к ней новых страниц с заголовками, соответствующими заголовкам форм. Можно также отстыковать каждый из этих элементов управления и даже PageControl Целиком. Программа не поддерживает автоматическое перетягивание, что делает 0287
невозможным переключение страниц; вместо этого функция активизируется при щелчке на области PageControl, где нет вкладок, то есть на основной панели: procedure TForml.PanellMouseDown(Sender: TObject; Button: TMouseButton: Shift: TShiftState: X. Y: Integer): begin PageControll.BeglnDrag (False. 10): end: Это поведение можно тестировать, запустив пример DockPage (рис. 6.15). Учти- те, что при удалении PageControl из главной формы непосредственно пристыко- вать к панели другие формы нельзя, это обеспечивается специальным кодом в про- грамме (просто потому, что иногда поведение может быть некорректным). Рис. 6.15. Главная форма примера DockPage после стыковки формы с левой стороны элемента управления страницей Архитектура ActionManager Вы видели, что действия и компонент ActionManager могут играть главную роль в развитии приложений Delphi, поскольку они допускают более лучшее отделе- ние интерфейса пользователя от абсолютного кода приложения. Сейчас пользова- тельский интерфейс может легко быть изменен без серьезного влияния на код. От- рицательный момент в таком подходе — это большой объем работы программиста. Для создания нового пункта меню необходимо сначала добавить соответствующее действие, затем перейти в меню, добавить пункт меню и соединить его с действием. Для решения этой проблемы^ обеспечения разработчиков и конечных пользо- вателей рядом расширенных возможностей Delphi 6 представил новую архитекту- ру, основанную на компоненте ActionManager, который значительно расширяет роль действий. ActionManager включает коллекцию действий, а также коллекцию пане- лей инструментов и привязанных к ним меню. Разработка этих панелей инстру- ментов и меню — полностью визуальный процесс: вы перетягиваете действия из 0288
специального редактора компонента ActionManager на панели инструментов для доступа к нужным кнопкам. Более того, можно разрешить конечным пользовате- лям вашей программы выполнять те же операции, изменяя компоновку их пане- лей инструментов и меню с помощью предоставленных вами действий. Иначе говоря, применение данной архитектуры позволяет создавать приложе- ния с современным интерфейсом, настроенным под конкретного пользователя. Меню показывает только недавно созданные пункты (как во многих программах Microsoft), допускает анимацию и многое другое. Данная архитектура сконцентрирована вокруг компонента ActionManager, но также включает некоторые другие компоненты, найденные в конце дополнитель- ной страницы палитры: О Компонент ActionManager заменяет ActionList (но может использовать один или несколько существующих ActionLists). О Элемент управления ActionMainMenuBar — это панель инструментов, используе- мая для отображения меню приложения на основе действий компонента Action- Manager. О Элемент управления ActionToolBar — это панель инструментов, применяемая для размещения кнопок на основе действий компонента ActionManager. О Компонент CustomizeDlg содержит диалоговое окно, используемое для настрой- ки пользовательского интерфейса приложения на основе действий компонента ActionManager. О Компонент PopupActionBarEx — это дополнительный компонент, который следует использовать, чтобы всплывающие меню отображались в интерфейсе пользо- вателя как главные меню. Этот компонент не поставляется с Delphi 7, но может быть загружен отдельно. СОВЕТ-------------------------------------------------------------------------- Компонент PopupActionBarEx (также имеет название ActionPopupMenu) можно найти в веб-хранили- ще Borland CodeCentral (номер 18870). В приложении содержится более подробная информация о сайте автора компонента (homepages.borland.com/strefethen); он является членом группы разра- ботчиков Delphi по Borland; компонент имеется на сайте, но официально не поддерживается. Создание простой демо-версии Поскольку данная архитектура большей частью визуальна, демо-версия более по- лезна, чем общие рассуждения (хотя печатная книга не лучший способ обсуждать визуальные серии операций). Для создания примера программы на основе данной архитектуры перетащите компонент ActionManager на форму и дважды щелкните на нем, чтобы открыть его редактор компонента (рис. 6.16). Учтите, что этот редак- тор не модальный, поэтому его можно держать открытым, выполняя другие дей- ствия в Delphi. Это же диалоговое окно выводится компонентом CustomizeDlg, но с некоторыми ограниченными функциями (например, недоступно добавление но- вых действий). Редактор имеет три страницы: ° Первая страница предлагает список визуальных контейнеров действий (панелей инструментов или меню). Новые панели инструментов добавляются щелчком 0289
7- Editingfonnl.AttionManageri Todbm Adfchs | (^tant | 3 ((AJAcbons) Category (No Category] МММ Fie Format Help Search Tods (Al Actions) емв^йи- ХЙ» lifcfc*.' 85 в«* Ык>£| hem eiJwt й»»вЛ^Н|»1*»«><11«Ь» --------- . , P Н^)йй»мк*фш«1^ж«йГ lJSssssssslJ м л V.M Ч Ш V. V , M •., ».-. Г-Ич#^!, • ' p Sb»(|»®Sefa« P ShowAwtollwnlb* r^J^eiwitaw.^Std» ri Рис. 6.16. Три страницы диалогового окна редактора ActionManager на кнопке New. Для добавления новых меню необходимо добавить соответству- ющий компонент в форму, затем открыть коллекцию ActionBars в ActionManager, выбрать панель действий или добавить новую и привязать к ней меню с по- мощью свойства ActionBar. С помощью этих же действий новая панель инстру- ментов связывается с данной архитектурой во время работы программы. О Вторая страница редактора ActionManager очень похожа на редактор ActionList,. обеспечивая способ добавления новых стандартных или пользовательских дей* ствий, распределения их по категориям и изменения их порядка. Хорошей функ- цией этой страницы является то, что можно перетащить категорию или отдель- ное действие из нее и вставить в элемент управления панели действий. ЕслЯ^ перетащить категорию в меню, вы получите раскрывающееся меню со всемЯ' пунктами категории; если перетащить ее в панель инструментов, каждое действий категории получит кнопку на панели инструментов. Если перетащить на панел1| 0290
инструментов отдельное действие, вы получите соответствующую кнопку; если перетащить его в меню, получите прямую команду меню, чего следует избегать. О Последняя страница редактора ActionManager позволяет вам (и дополнительно конечному пользователю) активизировать вывод на экран недавно использо- ванных пунктов меню и изменять некоторые визуальные параметры панелей инструментов. Программа-пример AcManTest использует некоторые стандартные действия и элемент управления RichEdit для демонстрации применения данной архитекту- ры (я не написал никакого пользовательского кода для улучшения работы дей- ствий, поскольку хотел сосредоточиться только на диспетчере действий для этого примера). Можно поэкспериментировать с ним в ходе разработки или во вре- мя его выполнения. Для этого щелкните на кнопке Customize (Настроить) и по- смотрите, что пользователь может сделать для настройки этого приложения (рис. 6.17). Рис. 6.17. с помощью компонента CustomizeDIg можно разрешить пользователю настраивать панели инструментов и меню приложения путем перетягивания пунктов из диалогового окна или перемещения их по панелям действий В данной программе можно запретить пользователю выполнение каких-либо операций с действиями. Любой конкретный элемент интерфейса пользователя (объект TActionClient) имеет свойство ChangedAllowed, которое можно использовать Для запрещения операций изменения, перемещения и удаления. Любой клиент- ский контейнер действий (визуальные панели) имеет параметр, запрещающий его скрытие (свойство ALlowHiding по умолчанию имеет значение True). Каждая кол- лекция Items в ActionBar имеет параметр Customizable, который можно отключить Для воспрещения всем пользователям делать изменения в панели целиком. 0291
СОВЕТ-------------------------------------------------------------------------------: Когда я упоминаю ActionBar, я имею в виду не визуальные панели инструментов, включающие эле- менты действий, а пункты коллекции ActionBars компонента ActionManager, у которого в свою очередь есть коллекция Items. Для уяснения этой структуры рекомендуется взглянуть на дерево, отобража- емое Object TreeView для компонента ActionManager. Каждый элемент коллекции ТАсйопВаг имеет подключенный визуальный компонент TCustomActionBar, а не наоборот (поэтому, например, ука* занное свойство Customizable недоступно при запуске путем выбора визуальной панели инструмент тов). Из-за совпадения этих двух имен приходится разбираться, к чему обращается справка Delphi. Чтобы сделать настройки пользователя постоянными, я присоединил файл (с именем settings) к свойству FileName компонента ActionManager. При назначений этого свойства следует ввести имя файла, который предполагаете использовать; при запуске программы ActionManager создаст этот файл. Устойчивость обеспечи- вается потоковой передачей каждого ActionClientltem, связанного с диспетчером дей- ствий. Поскольку эти клиентские элементы действий основаны на настройках пользователя и сохраняют информацию о состоянии, один файл собирает и пользой вательские изменения интерфейса, и данные о применении. Поскольку Delphi сохраняет настройки пользователя и информацию о состоя- нии в предложенном вами файле, можно обеспечить поддержку вашей программы для нескольких пользователей на одном компьютере. Следует просто использо- вать файл настроек для каждого из них (в виртуальных папках MyDocuments или MySettings) и подключить его к диспетчеру действий при запуске программы (с по- мощью текущего пользователя компьютера или какого-либо регистрационного имени). Другой способ — сохранить эти настройки в сети, чтобы при переходе пользователя на другой компьютер текущие персональные настройки перемеща- лись вместе с ним. В данной программе я решил сохранить настройки в файле, который находится в той же папке, что и программа, назначив свойству FileName ActionManager отно- сительный путь (имя файла). Компонент получит полное имя файла с папкой про- граммы и будет легко находить файл для загрузки. Однако среди других данных файл включает собственное имя файла с абсолютным путем. Поэтому при сохра- нении файла операция может обратиться к старому пути. Это предотвратит копи- рование данной программы с ее настройками в другую папку (например, как в демо- версии AcManTest). После загрузки файла свойство FileName можно установить заново. Кроме того, можно задать имя файла во время работы программы в собы- тии OnCreate формы. В данном случае необходимо также перезагрузить файл, по- скольку он был назначен после создания и инициализации компонента Action- Manager и ActionBars. Однако можно принудительно изменить имя файла после его загрузки, выполнив следующее: procedure TForml.FormCreatetSender: TObject): begin ActlonManagerl.FileName ExtractFIlePath (Applicatlon.ExeName) + 'settings': ActionManagerl.LoadFromFi1e(Acti onManagerl. Fi 1 eName); // заново задать настройки имени файла после его загрузки (относительный путь) < ActionManagerl.FileName ' ExtractFIlePath (Applicatlon.ExeName) + ’settings’: end; 0292
Редко используемые пункты меню Поскольку файл с настройками пользователя доступен, ActionManager сохраняет в нем предпочтения пользователя, а также использует его для отслеживания дея- тельности пользователя. Это позволяет системе удалить пункты меню, которые не использовались в течение некоторого времени, сделав их доступными в расши- ренном меню с помощью того же принятого в Microsoft интерфейса пользователя (см. пример на рис. 6.18). Рис. 6.18. ActionManager отключает редко используемые пункты меню, которые можно просмотреть, выбрав команду расширения меню ActionManager не только показывает редко используемые пункты меню: он по- зволяет очень точно настроить это поведение. В каждой панели действий имеется свойство SessionCount, которое отслеживает количество запусков приложения. Каж- дый ActionCLientltem имеет свойство LastSession, а свойство UsageCount использует- ся для отслеживания операций пользователя. Кстати, учтите, что пользователь может сбросить всю эту динамичную информацию с помощью кнопки Reset Usage Data в диалоге настройки. Система определяет число сеансов, когда действие не применялось, путем вычисления разницы между количеством раз, когда приложе- ние выполнялось (SessionCount), и последним сеансом, в течение которого действие использовалось (LastSession). Значение UsageCount применяется для определения в PrioritySchedule, сколько сеансов указанный пункт не использовался, прежде чем был удален. Другими словами PrioritySchedule связывает каждый случай использо- вания с числом неиспользованных (unused) сеансов. Изменив Priorityschedule, мож- но задать, как быстро пункты будут удаляться, если они не используются. Можно также запретить в системе запуск конкретных действий или групп дей- ствий. Свойство Items для ActionBars ActionManager имеет свойство HideUnused, ко- торое можно включить для запрещения этой функции для всего меню. Чтобы кон- кретный элемент всегда был видимым, независимо от реального использования, можно установить для его свойства UsageCount значение -1. Однако настройки пользователя могут переопределять это значение. Для лучшего объяснения действия данной системы, к примеру AcManTest, до- бавлено пользовательское действие (ActionShowStatus). Действие имеет следующий код, который сохраняет текущие настройки диспетчера действий в потоке памяти, 0293
преобразует поток в текст и отображает его в memo (подробнее о перемещении данг ных потоком см. главу 4): procedure TForml ActionShowStatusExecute(Sender TObject) var memStr memStr2 TMemoryStream begin memStr = TMemoryStream Create try memStr2 = TMemoryStream Create try ActionManagerl SaveToStream(memStr) memStr Position = 0 ObjectBinaryToText(memStr memStr2) memStr2 Position - 0 RichEditl Lines LoadFromStream(memStr2) finally memStr2 Free end finally memStr Free end end Результат — текстовая версия файла settings автоматически обновляется при каждом выполнении программы. Вот небольшая часть файла, включающая под- робные сведения об одном из раскрывающихся меню и масса комментариев' item // Файл, раскрывающийся из панели действий главного меню Items = < item Action = Forml FileOpenl LastSession = 19 // использовался в последнем сеансе UsageCount = 4 // использовался четыре раза end item Action = Forml FileSaveAsl // никогда не использовался end item Action = Forml FilePrintSetupl LastSession — 7 // использовался некоторое время назад UsageCount = 1 // только раз end item Action = Forml FileRunl // никогда не использовался end item Action = Forml FileExitl // никогда не использовался end> Caption = ’&File' LastSession * 19 UsageCount = 5 // общая сумма использования пунктов end Перенос существующей программы Если такая архитектура полезна, чтобы воспользоваться ее преимуществами, воз- можно, понадобится переделать большинство ваших приложений. Однако если 0294
действия уже использовались (с компонентом ActionList), такое преобразование будет гораздо проще. ActionManager имеет собственный набор действий, но может использовать действия из другого ActionManager или ActionList Свойство Action- Manager Li n ked Action Li sts — это коллекция других контейнеров действий (Action Lists или ActionManagers), которые могут быть сопоставлены с текущим ActionManager. Сопоставление всех различных групп действий полезно, поскольку позволяет пользователю настроить весь пользовательский интерфейс с помощью одного ди- алогового окна. Если подключить внешние действия и открыть редактор ActionManager, в стра- нице Actions вы увидите поле со списком, включающее текущий ActionManager плюс другие связанные с ним контейнеры действий. Можно выбрать один из этих кон- тейнеров для просмотра набора его действий и изменения их свойств. Параметр All Action данного поля со списком позволяет работать со всеми действиями из раз- ных контейнеров одновременно; однако, как уже отмечалось, при загрузке он вы- бирается, но не всегда работает. Выберите его еще раз, чтобы увидеть все действия. В качестве примера переноса существующего приложения в этой главе созда- ние программы расширено в примере MdEdit3 Этот пример использует тот же, что и предыдущая версия, список действий, привязанный к ActionManager и имеющий дополнительный параметр настройки, который позволяет пользователям перестра- ивать интерфейс В отличие от более ранней программы AcManDemo пример MdEdit3 использует ControlBar как контейнер для панелей действий (меню, трех панелей инструментов и обычных полей со списками) и полностью поддерживает перетя- гивание их вне контейнера в качестве плавающих панелей и вставку в располо- женную ниже панель ControlBar Чтобы выполнить это, необходимо было слегка изменить исходный код для обращения к новым классам для контейнеров (TCustomActionToolBar вместоTToolBar) в методе ControlBarLowerDockOver Я также обнаружил, что событие On End Dock ком- понента ActionToolBar передается как параметр для пустой цели, если система со- здает плавающую форму для размещения элемента управления, поэтому мне с оп- ределенным трудом удалось дать этой форме новый пользовательский заголовок (см. метод End Dock формы). Использование действий списка Еще ряд примеров использования этой архитектуры приведен в главах, посвящен- ных MDI и программированию баз данных (главы 8 и 13, например). Сейчас я хо- чу добавить дополнительный пример, показывающий, как использовать достаточ- но сложную группу стандартных действий: действия списка. Действия списка состоят из двух разных групп. Некоторые из них (например, Move (Переместить), С°РУ (Копировать), Delete (Удалить), Clear (Очистить) и Select All (Выбрать все)) — обычные действия, которые выполняются с окнами списков или другими списка- ми В то же время VirtualListAction и StaticListAction определяют действия, обеспе- чивающие список пунктов, которые отображаются в панели инструментов в виде поля со списком Демо-версия ListActions выделяет обе группы действий списка; ее ActionManager включает пять действий, отображаемых в двух отдельных панелях инструментов. Вот все эти действия (часть DFM-файла компонента, связанная с панелью дей- ствий)- 0295
object ActionManagerl: TActionManager ActionBars.SessionCount * 1 ActionBars - <...> object StaticListActionl: TStaticListAction Caption = 'Numbers' Items.CaseSensitive = False Items SortType = stNone Items = < item Caption = 'one' end item Caption = 'two' end OnltemSelected = ListActionltemSelected end object VirtualListActionl: TVirtualListAction Caption = 'Items' OnGetltem = VirtualListActionlGetltem OnGetltemCount = VirtualListActionlGetltemCount OnltemSelected = ListActionltemSelected end object ListControlCopySelectionl: TListControlCopySelection Caption = 'Copy' Destination = ListBox2 ListControl - ListBoxl end object ListControlDeleteSelectionl: TListControlDeleteSelection Caption = 'Delete' end object ListControlMoveSelection2: TListControlMoveSelection Caption = 'Move' Destination = ListBox2 ListControl = ListBoxl end end Программа списков в ее форме включает также два окна, которые используют- ся как цели действия. Действия Сору и Move связаны с этими двумя окнами спис- ков своими свойствами ListControl и Destination. Действие Delete автоматически ра- ботает с окном списка, имея фокусировку на ввод. StaticListActioп определяет серию альтернативных элементов в своей коллекции Items. Это не простой список строк, поскольку любой пункт также имеет Imageindex, позволяющий добавлять графические элементы к элементу управления, отобра- жающему список. Можно, конечно, с помощью программных средств добавить к этому списку дополнительные пункты. Однако если список достаточно динамич- ный, также используется VirtualListAction. Это действие не определяет список элемен- тов, но содержит два события, которые применяются для обеспечения строк и изо- бражений для списка: OnGetltemCount позволяет указывать количество элементов для отображения, a OnGetltem вызывается затем для каждого конкретного пункта. В демо-версии ListActions при создании списка, который можно увидеть в ак- тивном поле со списком, VirtualListAction использует следующие обработчики со- бытия для его определения (рис. 6.19): 0296
procedure TForml.VIrtualListActionlGetltemCount(Sender: TCustomLlstAcfion: var Count: Integer); begin Count :- 100; end: procedure TForml.Virtual ListActionlGetItem(Sender: TCustomListAction; const Index: Integer: var Value: String, var Imageindex: Integer: var Data: Pointer); begin Value := 'Item' + IntToStr (Index); end; Рис. 6.19. Приложение ListActions с панелью инструментов, на которой размещены статичный список и виртуальный список СОВЕТ---------------------------------------------------------------------------------------- Я предполагал, что виртуальные элементы действий требуются только тогда, когда их нужно ото- бразить, образуя, таким образом, виртуальный список. На самом деле все элементы создаются сразу же. В этом можно убедиться, включив код с комментариями в метод VirtualListActionlGetltem (отсутствующий в предыдущем листинге), который добавляется к каждому элементу, когда необхо- дима его строка. Статичный и виртуальный списки имеют событие OnltemSelected. В общем об- работчике события я написал следующий код для добавления текущего элемента в первое окно списка формы: procedure TForml.ListActionItemSelected(Sender: TCustomListAction: Control: TControl): begin ListBoxl.Items.Add ((Control as TCustomActionCombol.SelText); end; В данном случае отправителем является пользовательский список действий, но свойство Itemindex этого списка не обновлено и не включает выбранный эле- мент. Однако, обратившись к визуальному элементу управления, который отобра- жает этот список, можно получить значение выбранного пункта. 0297
Что далее? В этой главе было рассмотрено использование действий, списка действий и архи- тектур Action Manager. Очевидно, что это очень мощная архитектура, позволяющая отделить пользовательский интерфейс от программного кода вашего приложения, использующего и обращающегося к действиям, а не к связанным с ними пунктам меню или кнопкам панели инструментов. Нынешнее расширение этой архитекту- ры позволяет пользователям ваших программ иметь множество элементов управ- ления и Превращает ваши приложения в конечные программы без особых усилий с вашей стороны. Подобная архитектура также очень удобна для разработки пользо- вательского интерфейса программ, независимо от того, предоставляется ли эта возможность пользователям. Также были рассмотрены некоторые технологии разработки интерфейса пользо- вателя, такие как стыкуемые панели инструментов и другие элементы управле- ния. Эту главу можно считать первым шагом на пути создания профессиональных приложений. Ниже будут рассмотрены дальнейшие шаги; но полученных знаний уже достаточно для того, чтобы сделать ваши программы похожими на самые попу- лярные приложения Windows, что может быть очень важным для ваших клиентов. Сейчас, когда элементы главной формы вашей программы настроены надлежа- щим образом, можно изучить добавление вторичных форм и диалоговых окон. Это, наряду с общим представлением о формах, — тема главы 7. В главе 8 будет рас- смотрена общая структура приложения Delphi. 0298
Работа с формами Теперь вы умеете применять визуальные компоненты Delphi при создании пользо- вательского интерфейса для своих приложений. Рассмотрим другой главный эле- мент Delphi — формы, который уже неоднократно нами использовался, но подроб- но не изучался. В данной главе представлены некоторые свойства и стили форм, установка их размеров и расположение, а также масштабирование и прокрутка. Также будут рассмотрены приложения с несколькими формами, использование диалоговых окон (пользовательских и предопределенных), рамки и наследование визуальных форм. Кроме того, мы уделим внимание вводу данных в форму с клавиатуры и мыши. В данной главе освещаются следующие темы: О стили форм, стили границ и значки границ; О ввод с мыши и клавиатуры; О прямое раскрашивание форм и спецэффекты; О расположение, масштабирование и прокрутка форм; О создание и закрытие форм; О модальные и немодальные диалоговые окна и формы; О динамическое создание вторичных форм; О предопределенные диалоговые окна; О создание окна-заставки. Класс TForm Формы в Delphi определяются классом TForm, который содержится в модуле VCL Forms. Конечно, в VisualCLX теперь имеется второе определение форм. Хотя в этой главе главным образом рассматривается класс VCL, внимание также будет уделе- но и особенностям кросс-платформенной версии CLX. Класс TForm является частью иерархии оконных элементов управления, кото- рая начинается с класса TWinControl (или TWidgetControl). TForm выводится из по- чти завершенного TCustomForm, который, в свою очередь, выводится из TScrolling- WinControl (или TScrollingWidget). Поскольку формы обладают всеми функциями своих многочисленных базовых классов, они включают большое количество мето- дов, свойств и событий. Поэтому вместо их перечисления в этой главе будут пред- 0299
ставлены некоторые интересные методы, относящиеся к формам. Сначала будет изучен метод, позволяющий не определять форму программы в ходе разработки с помощью непосредственного использования класса TForm, а затем рассмотрены несколько интересных свойств этого класса. Особое внимание будет обращено на некоторые различия между формами VCL и CLX. Для большинства примеров создана CLX-версия, поэтому можно сразу же начинать экспериментировать с формами и диалоговыми окнами в CLX и в VCL. Как и в предыдущих главах, CLX-версия каждого примера имеет префикс в виде буквы Q. Использование простых форм На практике разработчики Delphi обычно создают формы в ходе разработки, что предполагает выведение нового класса из базового класса и визуальное построе- ние содержания формы. Но для отображения формы не обязательно создавать класс-потомок класса TForm, особенно если это касается простой формы. Предположим необходимо вывести на экран длинное сообщение (на основе строки) для пользователя. При этом вы не хотите использовать простое предопре- деленное окно сообщения, потому что оно будет выглядеть слишком большим и не предусматривает полосы прокрутки. Можно создать форму с компонентом memo и отобразить в нем строку. Ничто не препятствует созданию такой формы обыч- ным визуальным способом, но это нужно будет сделать с помощью кода, особенно если необходима большая степень гибкости. В неординарных в некотором смысле примерах DynaForm и QDynaForm (приве- денных в исходном коде этой книги) нет формы, определенной в ходе разработки, но имеется модуль с такой функцией: procedure ShowStringForm (str: string): var form: TForm. begin Application.CreateForm (TForm. form): form.caption := 'DynaForm'; form.Position := poScreenCenter; with TMemo.Create (form) do begin Parent := form: Align .= alClient: Scroll bars : = ssVertical; Readonly := True: Color := form.Col or: Borderstyle := bsNone; Wordwrap True: Text := str; end: form.Show: end; Я вынужден был создать форму путем вызова метода CreateForm глобального объекта Application (функция, обязательная для приложений Delphi и рассматри- ваемая в главе 8); этот способ отличается от того, который динамически выполня- ет код при обычной работе с Form Designer. Написать этот код сложнее, но он по- 0300
зволяет добиться большей гибкости, поскольку любой параметр может зависеть от внешних настроек. Предыдущая функция ShowStringForm не выполняется с помощью события дру- гой формы, поскольку в этой программе нет обычных форм. Вместо этого исход- ный код проекта изменен мной следующим образом: program DynaForm; uses Forms. DynaMemo in 'DynaMemo.pas': {$R *.RES} var str: string: begin str := Randomize: while Length (str) < 2000 do str := str + Char (32 + Random (74)); ShowStringForm (str); Application.Run; end. В результате выполнения программы DynaForm получается странного вида фор- ма, заполненная случайными символами (рис. 7.1). Сама по себе она не представ- ляет практической пользы, но подчеркивает идею. Рис. 7.1. Динамическая форма полностью создана примером DynaForm во время выполнения (создание в ходе разработки не поддерживается) ПРИМЕЧАНИЕ--------------------------------------------------------------— Еще одно преимущество этого подхода по сравнению с использованием DFM-файлов для создания Форм в ходе разработки состоит в том, что в этом случае внешнему программисту гораздо сложнее захватить информацию о структуре приложения. В главе 5 рассматривалось, как можно извлекать DFM-файл из текущего исполняемого файла Delphi; то же самое можно легко сделать с любым компилируемым в Delphi исполняемым файлом, исходный код которого отсутствует. Если нужно наряду с заданными по умолчанию значениями свойств сохранить для себя определенный набор используемых вами компонентов (например, в специальной форме), возможно, следует написать Дополнительный код. 0301
Стиль формы Свойство FormStyLe позволяет выбирать между нормальной формой (fsNormal) и ок- нами, которые составляют приложение многодокументного интерфейса (М DI). Во втором случае используется стиль fsMDIForm для родительского окна MDI (для рамочного окна приложения MDI) и стиль fsMDIChild для дочернего окна MDI. Подробнее развитие приложения MDI рассмотрено в главе 8. Четвертый параметр — стиль fsStayOnTop, который определяет, должна ли фор- ма всегда располагаться поверх всех других окон, за исключением тех, которые также являются окнами «поверх всех». Для создания самой верхней формы (формы, окно которой всегда располагает- ся поверх остальных окон) необходимо только установить свойство FormStyle, как указывалось ранее. Данное свойство, в зависимости от вида формы, к которой оно применяется, имеет две различные функции: О Главная форма этого приложения будет оставаться поверх окна любого друго- го приложения (если последние не будут иметь такой же стиль «поверх всех»). Иногда такое поведение создает достаточно неприятный визуальный эффект, поэтому целесообразно применять его только для специальных предупрежда- ющих программ. О Вторичная форма будет оставаться поверх любой другой формы приложения, к которому она относится. Это не касается окон других приложений. Такой спо- соб часто применяется для плавающих панелей инструментов и других форм, которые должны располагаться поверх главного окна. ВНИМАНИЕ--------------------------------------------------------------- Если указанное свойство применяется к вторичной форме в VCL, только эта форма остается поверх других форм в этом же приложении. В CLX даже вторичная форма будет сохраняться перед любой другой формой системы с оконным интерфейсом, чего обычно рекомендуется избегать. Стиль границы Другое важное свойство формы — BorderStyle. Оно относится к визуальному элемен- ту формы, но оказывает гораздо более сильное влияние на поведение окна (рис. 7.2). В ходе разработки форма всегда отображается, используя заданное по умолча- нию значение bsSizeable свойства BorderStyle. Это значение соответствует стилю Windows толстая рамка. Если вокруг главного окна имеется толстая рамка, пере- таскивая ее границу, пользователь может изменять его размеры. Данное состоя- ние можно определить по специальным курсорам изменения размеров (в виде дву- сторонней стрелки), которые появляются при наведении курсора мыши на эту толстую границу окна. При выборе второго важного параметра для этого свойства (bsDialog) форма использует в качестве своей границы обычную рамку диалогового окна (толстую рамку, которая не позволяет изменять размеры). Обратите также внимание, что при выборе значения bsDialog форма становится диалоговым окном. Это приводит к определенным изменениям: отличаются, например, пункты системного меню, а форма игнорирует некоторые элементы свойства набора Bordericons. 0302
Рис. 7.2. Образцы форм с различными стилями границы, созданные примером Borders ВНИМАНИЕ------------------------------------------------------------------- Установка в ходе разработки свойства BorderStyle не приводит к какому-либо видимому результату. Некоторые свойства компонента нельзя применить в ходе разработки, потому что они делают не- возможным работу на компоненте при создании программы. Например, как с помощью мыши изме- нить размеры формы, преобразованной в диалоговое окно? Тем не менее при выполнении приложения форма будет иметь требуемую границу. Для свойства BorderStyle можно назначить четыре значения: О bsSingle создает главное окно с неизменяемыми размерами. Это значение при- меняется во многих играх и приложениях, основанных на использовании окон с элементами управления (например, форм для ввода данных), просто потому что изменять размеры этих форм нецелесообразно. Увеличение формы для про- смотра пустых полей или уменьшение ее размера, чтобы некоторые компоненты были не так явно видны, не имеет особого смысла для пользователей програм- мы (хотя автоматические полосы прокрутки Delphi частично решают после- днюю проблему). О bsNone используется только в исключительных случаях и внутри других форм. Приложений с главным окном без границы или заголовка не существует (воз- можно, за исключением приведенного в книге по программированию примера, демонстрирующего отсутствие смысла в таком окне). О bsToolWindow и bsSizeToolWin относятся к специальному расширенному стилю Win32 ws_ex_ToolWindow. Этот стиль превращает окно в плавающую панель ин- струментов с небольшим шрифтом заголовка и кнопкой закрытия. Этот стиль нельзя использовать для главного окна приложения. ВНИМАНИЕ ------------------------------------------------------------------ В CLX перечень свойств BorderStyle использует несколько отличающиеся значения с префиксом fbs (form border style, стиль границы формы): fbsSingle, fbsDialog и т. д. Для проверки эффекта и поведения различных значений свойства BorderStyle я написал программу Borders, также существующую в CLX-версии (Qborders). Ее 0303
вывод уже был приведен на рис. 7.2. Однако рекомендуется запустить этот пример и поэкспериментировать с ним некоторое время, чтобы понять все различия в фор- мах. Главная форма этой программы содержит только группу переключателей и кнопку. Вторичная форма не имеет компонентов, и для ее свойства Position уста- новлено значение poDefaultPosOnly, которое влияет на начальное положение вто- ричной формы, создаваемой при щелчке на этой кнопке (свойство Position рассмот- рено далее в этой главе). Код программы достаточно прост. При щелчке на кнопке динамически созда- ется новая форма, которая зависит от выбора в группе переключателей: procedure TForml.BtnNewFormC1ick(Sender: TObject): var NewForm: TForm2: begin NewForm := TForm2.Create (Application): * NewForm.BorderStyle := TFormBorderStyle (BorderRadioGroup.Itemindex): NewForm.Caption := BorderRadioGroup.Items[BorderRadioGroup.Itemindex]: NewForm.Show; end: Этот код использует следующую хитрость: он отображает номер выбранного элемента в перечне TFormBorderStyle. Данный метод срабатывает, поскольку пере- ключателям указан тот же порядок, что и для значений перечня TFormBorderStyle. Метод BtnNewFormClick в таком случае копирует текст переключателя в заголовок вторичной формы. Эта программа обращается к вторичной форме TFormZ, опреде- ленной во вторичном модуле программы, сохраненном как Second.pas. Поэтому для компиляции данного примера к разделу implementation модуля главной формы сле- дует добавить следующие строки: uses Second: ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- При каждом обращении к другому модулю программы при возможности поместите соответствую- щий оператор в части uses вместо части interface. Это ускорит процесс компиляции, приведет к более чистому коду (поскольку включенные вами модули отделены от включенных Delphi) и пре- дотвратит ошибки компиляции циклического модуля. Для обращения к другим файлам текущего проекта можно также выбрать команду меню File ► Use Unit (Файл ► Использовать модуль). Значки границы Другой важный элемент формы — присутствие значков на его границе. По умол- чанию окно имеет маленький значок, подключенный к системному меню, свора- чивающую кнопку, разворачивающую кнопку и закрывающую кнопку. С помощью свойства Bordericons, имеющего четыре возможных значения (biSystemMenu, biMini- mize, biMaximize и biHelp), можно задавать различные параметры. СОВЕТ------------------------------------------------------------- Значок границы biHelp включает справку «What's this? (Что это такое?)». При включении этого стиля и исключении biMinimize и biMaximize в области заголовка формы появляется значок вопроса. Если щелкнуть на этом значке вопроса и затем на компоненте внутри формы (но не на самой форме!), Delphi активизирует справку для данного объекта (во всплывающем окне в Windows 9х или в постоянном окне WinHelp в Windows 2000/ХР). Это поведение продемонстрировано в примере BIcons, который включает простой файл справки со страницей, связанной со свойством HelpContext кнопки в центре формы. 0304
Пример BIcons демонстрирует поведение формы с различными значками гра- ницы и порядок изменения этого свойства во время выполнения. Форма примера очень проста: она включает только раскрывающееся меню с четырьмя пунктами, по одному для каждого из возможных элементов набора значков границы. Я напи- сал для этих четырех команд единый метод, который читает метки на пунктах меню для определения значения свойства Bordericons. Поэтому этот код является также хорошим упражнением при работе с наборами: procedure TForml.SetIcons(Sender: TObject): var Borlco: TBorderlcons; begin (Sender as TMenuItem).Checked :- not (Sender as TMenuItem).Checked; if SystemMenul.Checked then Borlco := [biSystemMenu] el se Borlco := []; if MaximizeBoxl.Checked then Include (Borlco. biMaximize); if MinimizeBoxl.Checked then Include (Borlco, biMinimize): if Helpl.Checked then Include (Borlco. biHelp): Bordericons := Borlco; end: При выполнении примера BIcons можно легко устанавливать и удалять различ- ные визуальные элементы границы формы. При этом вы сразу увидите, что неко- торые из этих элементов тесно связаны: при удалении системного меню исчезают все значки границы; при удалении сворачивающей или раскрывающей кнопки она становится затененной; если удалить обе эти кнопки, они исчезают. Заметьте так- же, что в двух последних случаях автоматически блокируются соответствующие элементы системного меню. Это стандартное поведение для любого приложения Windows. Если раскрывающая и сворачивающая кнопки отключены, можно акти- визировать кнопку Help. Фактически в Windows 2000 кнопка Help появится (но не будет работать), если отключена только одна из этих кнопок. В качестве ярлыка для получения этого эффекта можно щелкнуть кнопкой внутри формы. Кроме того, можно щелкнуть на этой кнопке после щелчка на значке меню Help, чтобы про- смотреть сообщение справки (рис. 7.3). В качестве дополнительной функции про- грамма также отображает в заголовке время вызова справки с помощью обработки события формы OnHelp. Это видно на рисунке. Рис. 7.3. Пример BIcons 0305
ВНИМАНИЕ-------------------------------------------------------------------- В созданном в CLX-версии примере QBIcons ошибка в библиотеке запрещает изменять значки гра- ницы во время выполнения. Различные настройки в ходе разработки полностью работоспособны, поэтому для просмотра любого эффекта программу необходимо изменять перед ее запуском. Во время выполнения программа ничего не делает! Установка некоторых стилей окна Стиль границы и значки границы обозначены двумя различными свойствами Delphi, которые используются для установки начального значения соответствую- щих элементов интерфейса пользователя. Помимо изменения интерфейса пользо- вателя, эти свойства влияют на поведение окна. Важно знать, что в VCL (и очевид- но, что в CLX этого нет) эти связанные с границей свойства и свойство FormStyle прежде всего соответствуют различным параметрам настройки в стиле и расши- ренном стиле окна. Эти два термина отражают два параметра функции интерфей- са API CreateWindowEx, которые Delphi использует для создания форм. Важно это понимать, поскольку Delphi позволяет свободно изменять эти два параметра, перекрывая виртуальный метод CreateParams: public procedure CreateParams (var Params: TCreateParams): override; Это единственный способ использовать некоторые специфические стили окна, которые недоступны непосредственно через свойства формы. Подробно о списке стилей и расширенных стилей окна можно узнать в разделах «CreateWindow» и «CreateWindowEx» справки интерфейса API. Учтите, что интерфейс API Win32 имеет стили для этих функций, включая связанные с окнами инструментов. Для демонстрации данного подхода написан пример NoTitle, который позволя- ет создать программу с пользовательским заголовком. Сначала путем установки соответствующих стилей необходимо удалить стандартный заголовок, сохранив рамку изменения размеров: procedure TForml.CreateParams (var Params: TCreateParams). begin inherited CreateParams (Params); Params.Style := (Params.Style or ws_Popup) and not ws_Caption; end; No T ills .a. . ................ Рис. 7.4. В примере NoTitle отсутствует реальный заголовок, а ложный создан с помощью надписи 0306
Для удаления заголовка следует изменить наложенный стиль на всплывающий стиль; в противном случае заголовок просто приклеится. Чтобы добавить пользо- вательский заголовок, я поместил надпись, выровненную по верхней границе фор- мы, и маленькую кнопку в дальнем конце (рис. 7.4). Чтобы сделать поддельный заголовок, необходимо сообщить системе, что опе- рация мыши на этой области соответствует операции мыши на заголовке. Это мож- но сделать, перехватив сообщение Windows wm_NCHitTest, которое часто посылает- ся для определения положения мыши. Когда нажатие приходится на клиентскую область и на надпись, путем установки соответствующего результата можно симу- лировать, что мышь находится на заголовке: procedure TForml.WMNCHitTest (var Msg. TWMNCHitTest). // сообщение wri_NcHitTest begin inherited: if (Msg.Result - htClient) and (Msg.YPos < Labell.Height + Top + GetSystemMetrics (sm_cyFrame)) then Msg.Result := htCaption. end: Используемая в этом листинге функция интерфейса API GetSystemMetrics за- прашивает операционную систему о вертикальной толщине (су) в пикселах грани- цы окна с заголовком, размеры которого не изменяются. Этот запрос необходимо делать каждый раз (и не кэшировать результат), потому что пользователи могут настраивать большинство этих элементов с помощью страницы Appearance (Вид) параметров рабочего стола (в Панели управления) и других настроек Windows. Маленькая кнопка запрашивает метод Close в обработчике события OnClick. Кноп- ка сохраняет свое положение, даже если размеры окна изменены с помощью зна- чения [akTop,akRight] для свойства Anchors. Форма также имеет ограничения по размеру, чтобы пользователь не мог сделать ее слишком маленькой. Это подробно рассматривается в разделе «Ограничения форма» далее в этой главе. Прямой ввод данных в форму Перейдем к очень важной теме: ввод данных в форму. Если вы решили ограничен- но использовать компоненты, писать сложные программы можно, осуществляя ввод с мыши и клавиатуры. В этой главе эта тема будет только представлена. Контроль ввода с клавиатуры Вообще, формы не обрабатывают прямой ввод с клавиатуры. Для ввода текста форма должна включать компонент редактирования или один из других компо- нентов ввода. Если вы хотите работать с сочетаниями клавиш, можете использо- вать те, которые подключены к меню (возможно использование скрытого всплы- вающего меню). В других случаях для конкретной цели может потребоваться специфическая обработка ввода с клавиатуры. Для этого можно включить свойство формы Key- Preview. Тогда даже при наличии некоторых элементов управления вводом собы- тие формы OnKeyPress всегда будет активизироваться для любой символь- 0307
ной операции ввода (система и сочетания клавиш исключены). В этом случае ввод с клавиатуры достигнет компонента назначения, если не останавливать его в фор- ме, установив символьное значение равным нулю (не символ 0, а значение 0 набо- ра символов, символ управления, обозначенный как #0). Созданный для демонстрации такого подхода пример KPreview имеет форму без специальных свойств (даже без KeyPreview), группу переключателей с четырьмя параметрами и несколько окон редактирования (рис. 7.5). По умолчанию программа ничего особенного не делает, за исключением случаев, когда различные переклю- чатели используются для включения предварительного просмотра клавиши: procedure TForml.RadioPreviewClick(Sender: TObject); begin KeyPreview :- RadioPreview.Itemindex <> 0: end; Теперь вы начинаете получать события On KeyPress и можете выполнить одно из трех действий, предлагаемых тремя специальными кнопками в группе переключа- телей. Действие зависит от значения свойства Itemindex компонента группы пере- ключателей. Вот почему обработчик события основан на операторе case: procedure TForml.FormKeyPress(Sender: TObject; var Key; Char): begin case RadioPreview.Itemindex of 7' Key Preview Pre’new Options: fi $01» A'F C Enter-T«b r TypeinCapKon ' * jEdilT (елз c Skipvoweb Рис. 7.5. Программа KPreview в ходе разработки В первом случае если значение параметра Key равно #13, что соответствует кла- више ENTER, вы отключаете операцию (установив значение Key равным нулю) и за- тем имитируете активизацию клавиши табуляции. Это можно сделать многими способами, но выбранный мною метод весьма специфический. Я посылаю форме сообщение CM_DIALOGKEY, отправляя код для клавиши табуляции (VK_TAB): 1 // Enter = Tab if Key = #13 then begin Key := #0; Perform (CMJlialogKey, VK_TAB. 0); end. 0308
СОВЕТ—"—----------------------— —----------------------------------- Сообщение CM_DialogKey является внутренним, недокументированным сообщением Delphi. Имеет- ся несколько таких сообщений, и представляется интересным создавать для них расширенные ком- поненты и использовать их для специального кодирования, но в Borland они никогда не описывались. Подробнее об этом см. в главе 9. Учтите также, что этот стиль написания программных кодов на основе сообщений недоступен в CLX. Для ввода текста в заголовок формы программа добавляет символ к текущему значению свойству Caption. Есть два особенных случая. При нажатии клавиши Backspace удаляется последний символ строки (копируя в Caption все символы те- кущего Caption, кроме последнего). При нажатии клавиши ENTER программа оста- навливает операцию, сбрасывая свойство Itemindex элемента управления группы переключателей. Вот код: 2 : // ввести текст в заголовок begin if Key = #8 then // backspace: удалить последний символ Caption := Copy (Caption, 1, Length (Caption) - 1) else if Key = #13 then // enter: остановить операцию RadioPreview.Itemindex = 0 else // что-либо другое: добавить символ Caption := Caption + Key; Key := #0: end; Кроме того, при выборе последнего положения переключателя код проверяет, является ли символ гласной буквой (путем проверки включением в постоянный «набор гласных»), В этом случае символ пропускается полностью: 3; // пропустить гласные if UpCase(Key) in ['А'. 'Е'. ’Г, 'О'. 'U'] then Key := #0; Ввод с помощью мыши При щелчке одной из кнопок мыши на форме (или на компоненте) Windows посы- лает сообщения приложения. Delphi определяет события, используемые для на- писания кода, отвечающего на эти сообщения. Два основных события — это Оп- MouseDown, получаемое при нажатии кнопки мыши, и OnMouseUp, получаемое при отпускании кнопки. Другое фундаментальное системное сообщение связано с дви- жением мыши: OnMouseMove. Хотя понять значение этих трех сообщений достаточ- но просто — вниз, вверх и перемещение — нужно разобраться, как они связаны с событием OnClick, которое нами уже использовалось. Используемое для компонентов событие OnClick доступно также и для формы. Его основное значение заключается в том, что левая кнопка мыши нажимается и отпускается на том же окне или компоненте. Однако между этими двумя дей- ствиями курсор может быть перемещен вне области окна или компонента при удер- живаемой в нажатом положении левой кнопки мыши. Другое различие между событиями OnMouseXX и OnClick состоит в том, что пос- леднее относится только к левой кнопке мыши. Большинство работающих с Win- dows типов мыши имеет две кнопки мыши, а некоторые — даже три. Обычно эти кнопки именуются как левая кнопка мыши (для выбора), правая кнопка мыши (для Доступа к меню быстрого запуска) и средняя кнопка мыши (редко используется). 0309
Сейчас новейшие мыши имеют колесо-кнопку вместо средней кнопки; пользова- тели обычно используют колесо для прокрутки (вызывая событие OnMouseWheel), но его можно также нажимать (генерируя события OnMouseWheelDown и OnMouseWheelUp). События колеса мыши автоматически преобразуются в события прокрутки. Работа в Windows без мыши Пользователь должен уметь использовать любое приложение Windows без мыши. Это правило программирования Windows. Конечно, работать с при- ложением с помощью мыши легче, но к этому не следует принуждать. У неко- торых пользователей мыши может не быть, например, у путешественников с маленькой портативной ЭВМ и отсутствием места, у рабочих на производстве и банковских служащих с множеством других внешних устройств вокруг них. Есть и другая причина для поддержки клавиатуры: использование мыши может замедлять работу. Если пользователь — квалифицированный набор- щик текста «со слепой печатью», он не будет использовать мышь, чтобы пе- ретащить слово в тексте. Он применит сочетания клавиш для его копирова- ния и вставки без отрыва рук от клавиатуры. Поэтому всегда нужно устанавливать подходящий порядок вкладок для компонентов формы. Не забывайте добавлять клавиши для кнопок и пунк- ты меню для выбора клавиатуры, используйте сочетания клавиш для ко- манд меню и т. д. Параметры событий мыши Все события мыши низшего уровня имеют такие же параметры: обычный пара- метр Sender, параметр Button, указывающий, которая из трех кнопок мыши была нажата (mbRight, mbLeft или mbCenter), параметр Shift, указывающий, какая из свя- занных с мышью виртуальных клавиш (модификаторы изменения состояния Alt, Ctrl и Shift плюс три кнопки мыши) была нажата, когда произошло событие; и коор- динаты х и у положения мыши в координатах клиентской области текущего окна, procedure TForml.FormMouseDowntSender: TObject; Button: TMouseButton: Shift: TShiftState; X. Y: Integer); begin end; if Button = mbLeft then Canvas.Ell ipse (X-10, Y-10, X+10, Y+10); СОВЕТ------------------------------------------------------- Для рисования на форме используется специальное свойство Canvas. Объект TCanvas имеет две отличительные особенности: он включает коллекцию инструментов рисования (типа пера, кисти и шрифта) и обеспечивает несколько методов рисования, использующих текущие инструменты. Код рисования в этом примере неправилен, потому что изображение на экране непостоянно; переме- щение другого окна на текущее очистит его вывод. Следующий пример демонстрирует подход Windows «сохрани-и-рисуй» Перетягивание и рисование с помощью мыши Для демонстрации некоторых рассмотренных ранее методов мыши я создал при- мер с использованием формы без каких-либо компонентов. Программа называет- 0310
ся MouseOne в версии VCL и QMouseOne в версии CLX. Она отображает текущее положение мыши в значение свойства Caption формы: procedure 7MouseForm.FormMouseMove(Sender: TObject: Shift: TShiftState: X. Y: Integer); begin // отобразить положение мыши в заголовке Caption -.= Format ('Mouse in x-Xd. y-Xd', [X. Y]); end: Эту особенность программы можно использовать для лучшего понимания ра- боты мыши. Запустите программу (эту простую версию или полную) и измените размеры окон на рабочем столе так, чтобы форма программы MouseOne или QMouse- One была позади другого окна и неактивна, но заголовок был виден. Теперь пере- местите мышь выше формы и увидите, что координаты изменяются. Это поведе- ние означает, что событие OnMouseMove посылается приложению, даже если его окно неактивно, и подтверждает то, что сообщения мыши всегда ориентированы на окно под мышью. Единственное исключение — операция захвата данных мыши, рассмотренная в том же самом примере. Кроме отображения положения в заголовке окна пример MouseOne/QMouseOne может отслеживать движения мыши, закрашивая маленькие пикселы на форме, если пользователь удерживает в нажатом состоянии клавишу SHIFT (напомню, что этот прямой код закрашивания производит непостоянный вывод): procedure TMouseForm.FormMouseliove(Sender: TObject; Shift: TShiftState; X. Y: Integer): begin // отобразить положение мыши в заголовке Caption := Format ('Mouse in x=Xd. y=Xd'. [X. Y]). if ssShift in Shift then // пометить точки желтым Canvas.Pixels [X. Y] :- clYellow; end: ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- Класс TCanvas библиотеки CLX для Kylix 1 и Delphi б не включал массив Pixels. Вместо этого можно было вызвать метод DrawPoint после установки надлежащего цвета для пера, как это было сделано в примере QMouseOne. В Kylix 2 и Delphi 7 снова представлено свойство массива Pixels. Наибольший интерес в этом примере представляет прямая поддержка перетас- кивания мышью. Вопреки предположениям, Windows не имеет системной поддер- жки перетаскивания, которое реализовано в VCL посредством событий мыши низ- шего уровня и операций (в главе 6 рассматривался пример перетаскивания от одного элемента управления к другому). В VCL формы не поддерживают опера- ции перетаскивания, поэтому здесь нужно использовать низкоуровневый подход. Цель этого примера состоит в том, чтобы с помощью операции перетаскивания нарисовать прямоугольник, предлагая пользователям визуальные представления о выполняемой операции. Основа перетаскивания достаточно проста. Программа получает последователь- ность сообщений о нажатии кнопки, перемещении мыши и освобождении кнопки. Перетаскивание начинается при нажатии кнопки, хотя реальные действия проис- ходят только тогда, когда пользователь перемещает мышь (не отпуская кнопку) и при завершении перетаскивания (когда прибывает сообщение об освобождении 0311
кнопки). Проблема состоит в том, что такой подход ненадежен. Окно обычно по- лучает события мыши только при нахождении мыши над его клиентской областью; поэтому если пользователь нажимает кнопку мыши, перемещает мышь на другое окно и затем отпускает кнопку, сообщение освобождения кнопки получит второе окно. Есть два решения этой проблемы. Одно (используемое достаточно редко) — отсечение мыши. Используя функцию API-интерфейса Windows (ClipCursor), можно принудить мышь не покидать определенную область экрана. При попытке переме- стить ее вне указанной области она натыкается на невидимый барьер. Второе бо- лее принятое решение состоит в том, чтобы зафиксировать мышь. При фиксирова- нии мыши в окне весь последующий ввод мыши будет отправляться данному окну. Этот подход использован для примера MouseOne/QMouseOne. Код примера построен вокруг трех методов: FormMouseDown, FormMouseMove и FormMouseUp. Щелчок левой кнопкой мыши на форме начинает процесс, уста- навливая булеву область формы fDragging (которая указывает, что перетаскивание действует в двух других методах). Этот метод также использует переменную TRect, которая отслеживает начальное и текущее положение перетаскивания. Вот код: procedure TMouseForm.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X. Y: Integer); begin if Button = mbLeft then begin fDragging ;= True; Mouse.Capture := Handle; fRect.Left := X; fRect.Top := Y; fRect.BottomRight := fRect.TopLeft; dragStart := fRect.TopLeft; Canvas.DrawFocusRect (fRect): end: end; Важное действие этого метода — запрос к функции интерфейса API SetCapture, получаемый путем установки свойства Capture глобального объекта Mouse. Теперь даже если пользователь перемещает мышь вне клиентской области, форма все равно получает все связанные с мышью сообщения. Это поведение можно увидеть, пере- мещая мышь к левому верхнему углу экрана: в заголовке программа показывает отрицательные координаты. ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- Глобальный объект Mouse позволяет получать глобальную информацию о мыши, например, о ее наличии, типе и текущей позиции, а также устанавливать некоторые ее глобальные функции. Этот глобальный объект скрывает несколько функций API, делая код более простым и компактным. В VCL свойство Capture имеет тип Handle, в то время как в CLX оно имеет тип TControl (объект компонента, который фиксирует мышь). Так, включенный в этот раздел код станет Mouse.Capture := self, как показано в примере QMouseOne. Когда перетаскивание активно и пользователь перемещает мышь, программа рисует пунктирный прямоугольник, соответствующий положению мыши. Програм- ма дважды вызывает метод DrawFocusRect. При первом вызове этого метода он уда- ляет текущее изображение благодаря тому, что два последовательных запроса 0312
к DrawFocusRect сбрасывают первоначальную ситуацию. После обновления поло- жения прямоугольника программа вызывает метод второй раз: procedure TMouseForm.FormMouseMove(Sender: TObject: Shift: TShiftState: X. Y: Integer): begin // отобразить положение мыши в заголовке Caption := Format ('Mouse in x=%d. y=Zd'. [X. Y]): if fDragging then begin // удалить и снова нарисовать перетаскиваемый прямоугольник Canvas.DrawFocusRect (fRect): If X > dragStart.X then fRect.Right := X else fRect.Left := X; if Y > dragStart.Y then fRect.Bottom ;= Y el se fRect.Top := Y: Canvas.DrawFocusRect (fRect): end el se if sShift in Shift then // пометить точки желтым Canvas.Pixels [X. Y] := clYellow: end: В Windows 2000 (и других версиях) функция DrawFocusRect не рисует прямо- угольники с отрицательным размером, поскольку код программы был зафиксиро- ван (как было показано выше) путем сравнивания текущего положения с началь- ным положением перетаскивания, сохраненного в точке dragStart. При отпускании кнопки мыши программа заканчивает операцию перетаскивания, сбрасывая свой- ство Capture объекта Mouse (который внутренне вызывает ReleaseCapture) и уста- навливая для области fDragging значение False: procedure TMouseForm.FormMouseUp(Sender: TObject: Button: TMouseButton: Shift: TShiftState; X. Y: Integer): begin if fDragging then begin Mouse.Capture := 0; // вызывает ReleaseCapture fDragging := False; Invalidate: end; end; Конечный вызов Invalidate включает операцию рисования и выполняет следу- ющий обработчик события OnPaint: procedure TMouseForm.FormPaint(Sender: TDbject): begin Canvas.Rectangle (fRect.Left. fRect.Top. fRect.Right, fRect.Bottom); end; Это делает вывод данных формы постоянным, даже если скрыть ее за другой формой. На рис. 7.6 показана предыдущая версия прямоугольника и операция пе- ретаскивания в действии. 0313
Рис. 7.6. Во время операции перетаскивания пример MouseOne использует пунктирную линию для показа конечной области прямоугольника Рисование в формах Почему для создания нужного вывода необходимо обрабатывать событие OnPaint, и почему нельзя рисовать непосредственно на холсте формы? Это зависит от за- данного по умолчанию поведения Windows. Поскольку вы рисуете в окне, Windows не сохраняет конечное изображение. При закрытии окна его содержание обычно теряется. Подобное поведение объясняется экономией памяти. Windows предполагает, что в конце длительной работы «дешевле» нарисовать экран заново с помощью кода, чем занимать системную память сохранением экранного состояния окна. Это классический компромисс «память против ЦП». Цветное bitmap-изображение с разрешением 600x800 при 256 цветах требует приблизительно 480 Кб памяти. Увеличивая число цветов или количество пикселов, можно легко достичь 4 Мб памяти для разрешения 1280x1024 при 16 миллионах цветов. Если планируется иметь постоянный вывод для разрабатываемых приложений, можно использовать два метода. Обычная практика заключается в сохранении достаточного количества данных о выводе, чтобы обеспечить возможность его воспроизведения при запросе системой необходимой операции рисования. Альтер- нативный подход состоит в сохранении вывода формы при создании bitmap-изоб- ражения. Это осуществляется путем размещения на форме компонента Image и ри- совании изображения на холсте этого компонента. Первый метод, рисование, представляет собой обычный подход к обработке вывода в большинстве работающих с окнами систем, за исключением особых ори- ентированных на графику программ, которые целиком сохраняют изображение формы в точечный рисунок. Подход, предназначенный для реализации рисования, имеет очень подробное название: store and paint (сохранить и нарисовать). Когда пользователь щелкает кнопкой мыши или выполняет любую другую операцию, нужно сохранить положение и другие элементы; тогда в методе рисования эта ин- формация будет использована для создания соответствующего изображения. 0314
Такой подход позволяет приложению заново перерисовать всю его поверхность при любом из возможных условий. Вы сможете правильно воссоздать вывод, если обеспечите метод перерисовывания содержания формы и автоматически вызове- те его, когда часть формы скрыта и требует перерисовывания. Поскольку этот подход осуществляется в два этапа, необходимо выполнить эти две операции последовательно, запросив систему перерисовать окно, не дожида- ясь, пока система запросит операцию перерисовывания. Можно использовать не- сколько методов запуска перерисовывания: Invalidate, Update, Repaint и Refresh. Два первых соответствуют функциям API Windows, а два последних были представле- ны Delphi: О Метод Invalidate сообщает Windows, что должна быть перерисована вся повер- хность формы. Очень важно помнить, что Invalidate не предписывает немедлен- ное выполнение операции перерисовывания. Windows сохраняет запрос и от- вечает на него только после полного выполнения текущей процедуры (пока не вызывается Application.ProcessMessages или Update) и когда в системе нет других событий, ожидающих выполнения. Windows преднамеренно задерживает опе- рацию рисования, поскольку это одна из наиболее долгих операций. Иногда, учитывая такую задержку, нарисовать форму становится возможным только после того, как произойдут многочисленные изменения, аннулирующие мно- гократные последовательные запросы к (медленному) методу рисования. О Метод Update запрашивает, чтобы Windows обновила содержимое формы, не- медленно перерисовав ее. Однако эта операция будет выполнена только при условии наличия недопустимой области. Это произойдет только в случае вы- зова метода Invalidate или в результате операции пользователя. Если недопус- тимой области нет, запрос к Update будет безрезультатен. Поэтому обычно зап- рос к Update поступает только после запроса к Invalidate, выполняемого двумя методами Delphi Repaint и Refresh. о Метод Repaint последовательно вызывает Invalidate и Update. В результате не- медленно активизируется событие OnPaint. Немного отличающаяся версия это- го метода с именем Refresh по умолчанию вызывает Repaint. Фактически суще- ствует два метода для одной операции, что связано с периодом Delphi 1, когда они различались очень тонко. Для запроса формы относительно операции перерисовывания обычно необхо- димо вызвать Invalidate, следуя стандартному подходу Windows. Это особенно важ- но при частом вызове операции, поскольку если Windows требует для обновления экрана слишком много времени, несколько запросов о перерисовывании могут быть объединены в одно простое действие перерисовывания. Сообщение Wm_Paint в Windows имеет низкий приоритет; если просьба о перерисовывании отложена, но имеются другие ожидающие сообщения, то последние будут обработаны рань- ше, чем система выполнит действие рисования. С другой стороны, при многократных вызовах Repaint экран каждый раз дол- жен быть перерисован до того, как Windows сможет обработать другие сообщения. Поскольку операции рисования в вычислительном отношении очень интенсивны, ваше приложение может стать менее отзывчивым. Иногда, однако, нужно, чтобы приложение перерисовало поверхность как можно быстрее. В этих редких случаях используется вызов Repaint. 0315
СОВЕТ------------------------------------------------------------------------------------ Еще одна важная особенность состоит в том, что для ускорения операции рисования Windows пере- рисовывает только так называемую область обновления. Поэтому если сделать часть окна нефунк- ционирующей, будет перерисована только указанная область. Для этого можно использовать функции InvalidateRect и InvalidateRegion. Но данная особенность имеет две стороны: с одной — это мощный метод, позволяющий повысить скорость и уменьшить мерцание, вызванное частым перерисовыва- нием, а с другой — он также может выдавать неправильный вывод. Типичная проблема возникает только тогда, когда некоторые из областей, на которые действуют пользовательские операции, изменены, в то время как другие остаются в прежнем состоянии, даже если система выполняет исходный код, предусматривающий их обновление. Если операция перерисовывания попадает вне области обновления, система игнорирует ее, как будто это произошло вне видимой области окна. Необычные методы: Alpha Blending, Color Key и Animate API Одна из последних связанных с формами функций Delphi — поддержка новых интерфейсов API Windows, которые влияют на способ отображения форм (эта функция Windows 2000/ХР недоступна в Qt/CLX). Alpha blending (альфа-сопря- жение) позволяет сливать содержание формы с тем, что находится позади нее на экране, — эти функциональные возможности применяются достаточно редко, по крайней мере в коммерческом приложении. Этот метод более интересен, когда применяется к bitmap-изображению (с новыми функциями API AlphaBlend и Alpha- DIBBlend), а не к форме. В любом случае путем установки для свойства формы AlphaBlend значения True и определению свойству AlphaBlendValue значения меньше 255 благодаря прозрачности можно будет видеть то, что находится за формой. Чем меньше значение AlphaBlend Value, тем сильнее исчезает форма. Пример альфа-со- пряжения приведен на рис. 7.7, взятом из примера ColorKeyHole. Рис. 7.7. Вывод примера ColorKeyHole, демонстрирующий влияние новых свойств TransparentColor и AlphaBlend и интерфейс API с анимационным окном Другая необычная особенность Delphi — булево свойство TransparentColor, по- зволяющее указывать прозрачный цвет, который будет заменен фоном, создавая своего рода дыру в форме. Фактический прозрачный цвет обозначен свойством TransparentColorValue (рис. 7.7). 0316
Наконец, можно использовать родной метод Windows анимационный экран (animated display), который непосредственно не поддерживается Delphi (кроме экранных подсказок). Например, вместо запроса метода формы Show можно напи- сать Form3.Hide: AnimateWindow (Form3.Handle. 2000. AW_BLEND); Form3.Show: Чтобы форма вела себя правильно, следует вызывать метод Show в конце. Ана- логичный анимационный эффект можно получить путем изменения свойства Alpha- BlendValue в цикле. Также для управления переносом формы в представление мо- жет использоваться интерфейс API AnimateWindow, начиная с центра (с флажком AW_CENTER) или от одной из ее сторон (AW_HOR_POSITIVE, AW_HOR_NEGATIVE, AW_ VER_POSITIVE или AW_VER_NEGATIVE), как это обычно делается для слайдов. Эту же функцию можно применять к работающим с окнами элементам управ- ления, получая эффект постепенного появления fade-in effect вместо обычного прямого появления. Я сильно сомневаюсь в целесообразности очень большой бес- полезной нагрузки анимации на ЦП, но полагаю, что, будучи примененной долж- ным образом и в правильной программе, она улучшает интерфейс пользователя. Положение, размер, прокрутка и масштабирование При запуске программы после разработки формы вы ожидаете, что она отобразит- ся именно так, как была подготовлена. Однако у пользователя вашего приложения может быть экран с другой разрешающей способностью или он хотел бы изменить размеры формы (если это возможно, в зависимости от стиля границы), что в ко- нечном счете влияет на интерфейс пользователя. Ранее (главным образом, в главе 7) уже рассматривались некоторые методы, связанные с элементами управления, например, выравнивание и якоря. Ниже обратимся к элементам, связанным с фор- мой в целом. Кроме различий в системе пользователя, существует много причин изменить для этой области значения Delphi по умолчанию. Например, нужно запустить две копии программы и при этом избежать отображения всех форм в одном месте экрана. Положение формы Задать положение формы можно с помощью нескольких свойств. Начальное по- ложение формы в Delphi определяет свойство Position. Значение по умолчанию poDesigned указывает, что форма появится там, где вы ее разработали и где исполь- зовали свойства формы, касающиеся ее положения (Left и Тор) и размера (Width и Height). Некоторые другие параметры (poDefault, poDefaultPosOnly и poDefaultSizeOnly) зависят от особенностей операционной системы. С помощью специального флаж- ка Windows может задавать положение и/или устанавливать размеры новых окон при размещении их каскадом. Таким образом, установленные в ходе разработки 0317
касающиеся положения и размера свойства будут игнорироваться, но если пользо- ватель запустит приложение дважды, наложения окон не будет. Заданные по умол- чанию положения игнорируются, если форма имеет стиль границы с диалогом. Значение poScreenCenter отображает форму в центре экрана с заданным в ходе раз- работки размером. Это обычный параметр настройки для диалоговых окон и дру- гих вторичных форм. Другое свойство, которое влияет на начальный размер и положение окна, — его состояние. Свойство WindowState используется в ходе разработки для отображе- ния при запуске развернутого или свернутого окна. Это свойство имеет только три возможных значения: wsNormal, wsMinimized и wsMaximized. Если установить свер- нутое состояние окна, при запуске форма будет отображена в Панели задач Win- dows. Для главной формы приложения это свойство может быть установлено ав- томатически путем определения соответствующих атрибутов в значке быстрого вызова приложения. Конечно, во время выполнения также можно разворачивать или сворачивать окно путем установки для свойства WindowState значения wsMaximized или wsNormal. Установка значения wsMinimized, однако, создаст свернутое окно, которое будет размещено поверх Панели задач, а не в ней. Это ожидаемое действие для вторич- ной формы, а не для главной! Простое решение этой проблемы состоит в вызове метода Minimize объекта Application. Кроме того, для восстановления формы в клас- се Tapplication имеется метод Restore, хотя чаще всего пользователь выполняет эту операцию с помощью команды системного меню Restore. Пристыковывание к экрану (в Delphi 7) В Delphi 7 у форм есть два новых свойства: О Булево свойство ScreenSnap определяет, должна ли форма быть пристыкована к области экрана, когда расположена близко к одной из его границ. о Целое число SnapBuffer определяет расстояние от границ, которое расценивает- ся как близко. Пользователям будет удобно пристыковывать формы к стороне экрана и использовать преимущества всей экранной поверхности; это особенно удобно для приложений с несколькими одновременно отображаемыми форма- ми. Не задавайте слишком большое значение для свойства SnapBuffer (разме- ром с экран), иначе система запутается! Размер формы и ее клиентская область Есть два способа задать размер формы в ходе разработки: путем установки значе- ния свойств Width и Height или перетаскивания ее границ. Если форма имеет гра- ницу, позволяющую изменять ее размеры, это можно сделать во время выполне- ния (создав событие OnResize, позволяющее выполнять пользовательские действия Для адаптирования интерфейса пользователя к новому размеру формы). Однако если посмотреть на свойства формы в исходном коде или в интерак- тивной справке, можно заметить, что два свойства относятся к его ширине и два — к высоте. Height и Width связаны с размером формы, включая границы; ClientHeight и Clientwidth — с размером внутренней области формы, исключая границы, заголо- вок, полосы прокрутки (если есть) и строку меню. Клиентская область формы — 0318
это поверхность, которая используется для размещения компонентов на форме, создания вывода и получения пользовательского ввода. Учтите, что в CLX даже Height и Width относятся к размеру внутренней области формы. Поскольку вы заинтересованы в наличии некоторой доступной области для ваших компонентов, зачастую целесообразно задать клиентский размер формы вместо глобального. Поскольку при установке одного из двух клиентских свойств соответственно изменяется соответствующее свойство формы. ПРИМЕЧАНИЕ-------------------------------------------------------------- В Windows также можно создавать вывод и получать ввод из неклиентской области формы, то есть из ее границы. Рисование границы и получение ввода, когда вы щелкаете на ней, довольно слож- ные вопросы. Просмотрите в файле Справки описание таких сообщений Windows, как wm_NCRaint, wm_NCCalcSize и wm_NCHitTest, и ряда неклиентских сообщений, связанных с вводом мыши, напри- мер, wm_NCLButtonDown. Сложность этого подхода заключается в объединении вашего кода с за- данным по умолчанию поведением Windows. Ограничения форм Если выбрать для формы границу, позволяющую изменять ее размер, пользовате- ли обычно изменяют размеры формы по своему усмотрению и, как правило, раз- вертывают ее на полный экран. Windows сообщает, что размер формы изменился с помощью сообщения wm_Size, которое генерируется событием OnResize. Это собы- тие происходит после того, как размер формы уже был изменен. Еще одно измене- ние размера в этом случае (если пользователь уменьшил или увеличил форму слиш- ком сильно) было бы бессмысленно. Для этого более подходит предупредительное решение проблемы. Delphi предусматривает специальное свойство для форм и также для всех эле- ментов управления: свойство Constraints. Установка для подсвойств данного свой- ства соответствующих максимальных и минимальных значений создает форму, размеры которой не будут выходить за эти пределы. Вот пример: object Forml TForml Constraints MaxHeight = 300 Constraints MaxWidth = 300 Constraints MinHeight = 150 Constraints MinWidth = 150 end Учтите, что при установке свойство Constraints начинает действовать немедлен- но даже в ходе разработки, изменяя размер формы, если она располагается вне разрешенной области. Кроме того, Delphi использует ограничения максимального размера для раз- вернутых окон, вызывая ненужный эффект. Поэтому следует вообще отключать разворачивающую кнопку окна, которое имеет максимальный размер. В некото- рых случаях существует необходимость развернутого окна с ограниченным разме- ром — поведение главного окна Delphi. Для изменения ограничения во время выполнения можно также использовать два специальных события OnCanResize и OnConstrainedResize. Первое может также использоваться для отключения изме- нения размеров формы или управления в конкретных обстоятельствах. 0319
Прокрутка формы Все необходимые для создания простого приложения компоненты могут быть раз- мещены в одной форме. С развитием приложения возникает необходимость вста- вить дополнительные компоненты, увеличить размер формы или добавить новые формы. Для уменьшения занимаемого компонентами пространства можно доба- вить возможность изменения их размеров во время выполнения, разделив форму на различные области. Для увеличения размеров формы можно использовать по- лосы прокрутки, чтобы позволить пользователю перемещаться в форме, превыша- ющей размеры экрана (или, по крайней мере, ее видимую на экране часть). Добавить полосу прокрутки к форме достаточно просто. Фактически, при разме- щении нескольких компонентов в большой форме и уменьшении затем ее размера ничего делать не нужно — полоса прокрутки будет добавлена к форме автоматиче- ски, если не изменять значение свойства AutoScroll по умолчанию, которое равно True. Кроме AutoScroll формы имеют свойства HorzScrollBar и VertScrollBar, применяе- мые для установки нескольких свойств для двух связанных с формой объектов TFormScrollBar. Свойство Visible показывает, имеется ли полоса прокрутки; свойство Position определяет начальное состояние указательного курсора прокрутки; свой- ство Increment определяет эффект щелчка одной из стрелок на конце полосы про- крутки. Однако самое важное свойство — Range. Свойство Range полосы прокрутки определяет виртуальный размер формы, а не диапазон значений полосы прокрутки. Предположим, вам нужна форма, на кото- рой необходимо разместить несколько компонентов, и поэтому ее ширина должна составлять 1000 пикселов. С помощью этого значения можно задать «виртуальный диапазон» формы, изменяя свойство Range горизонтальной полосы прокрутки. Значение свойства Position полосы прокрутки находится в диапазоне от 0 до 1000 минус текущий размер области клиента. Например, если ширина клиентской области формы равна 300 пикселов, чтобы увидеть дальний конец формы (тысяч- ный пиксел), можно прокрутить 700 пикселов. Пример проверки прокрутки Чтобы продемонстрировать этот особый случай, я создал пример Scrolll с шири- ной виртуальной формы 1000 пикселов. Для горизонтальной полосы прокрутки задан диапазон 1000: object Forml: TForml HorzScrollBar.Range = 1000 VertScrollBar.Range = 305 AutoScroll = False OnResize = FormResize Форма примера заполнена списками без смысловой нагрузки, и тот же самый диапазон полосы прокрутки можно получить, разместив самый правый список так, чтобы его положение (Left) плюс его размер (Width) было равно 1000. Интересно, что в примере имеется окно панели инструментов, показывающее состояние формы и ее горизонтальной полосы прокрутки. В данный момент фор- ма имеет четыре надписи: две с фиксированным текстом и две с выводом. Кроме того, вторичная форма (с именем Status) имеет стиль границы bsToolWindow и рас- полагается поверх всех окон. Следует также установить для ее свойства Visible зна- чение True, чтобы ее окно автоматически выводилось на экран при запуске: 0320
object Status: TStatus Bordericons = [biSystemMenu] BorderStyle = bsToolWindow FormStyle = fsStayOnTop Visible = True object Label 1: TLabel... В этой программе содержится немного кода. Его цель состоит в том, чтобы об- новить значения в панели инструментов при каждом изменении или прокрутке формы (рис. 7.8). Первая часть чрезвычайно проста. Можно обработать событие формы On Resize и копировать пару значений в две надписи. Надписи — это часть другой формы, поэтому перед ними нужно добавить префикс с именем экземпля- ра формы Status: procedure TForml.FormResizeCSender: TObject): begin Status Label3.Caption := IntToStr(ClientWidth); Status.Label4.Caption := IntToStrCHorzScrollBar.Position), end; При желании изменять вывод при каждом прокручивании содержания формы нельзя использовать обработчик событий Delphi, потому что у форм нет события OnScroll (хотя оно есть у компонентов автономной полосы прокрутки). Это собы- тие исключено, потому что формы Delphi автоматически активно обрабатывают полосы прокрутки. В Windows, наоборот, полосы прокрутки — элементы чрезвы- чайно низкого уровня, требующие объемного кодирования. Обработка события прокрутки применяется только в особых случаях, например, когда нужно точно отследить выполненные пользователем операции прокрутки. Вот код, который нужно написать. Сначала добавьте к классу декларацию ме- тода и свяжите с сообщением горизонтальной прокрутки Windows (wm_HScroll); затем напишите код для этой процедуры, который является почти таким же, как Для уже рассмотренного метода FormResize: 0321
public procedure WMHScroll (var Scroll Data. TWMScroll); message wm_HScrol 1; procedure TForml.WMHScroll (var Scroll Data TWMScroll). begin inherited: Status.Label3.Caption IntToStr(ClientWidth): Status.Label4.Caption : = IntToStr(HorzScrollBar Position), end; Важно добавить к inherited запрос, активизирующий метод, связанный с тем же сообщением в базовом классе формы. Ключевое слово inherited в обработчиках сообщения Windows вызывает метод перекрываемого вами базового класса, кото- рый связан с соответствующим сообщением Windows (даже при другом имени процедуры). Без этого запроса поведение формы по умолчанию не будет иметь прокрутки; то есть форма вообще не будет прокручиваться. СОВЕТ------------------------------------------------------------------------------------ Поскольку в CLX нельзя обрабатывать сообщения прокрутки низкого уровня, то кажется, нет како- го-либо простого способа создать программу, подобную Scrolll. Это не очень важно в реальных приложениях, потому что система прокрутки автоматическая и можно подключаться к библиотеке CLX на более низком уровне Автоматическая прокрутка Свойство Range полосы прокрутки может казаться странным, пока не начать по- следовательно его использовать. При этом становятся понятными преимущества подхода «виртуальный диапазон». Полоса прокрутки автоматически удаляется из формы, если клиентская область формы достаточна для размещения ее виртуаль- ного размера; при уменьшении размера формы полоса прокрутки добавляется снова. Эта функция особенно полезна, если для свойства формы AutoScroll задано зна- чение True. В этом случае крайние положения самого правого и наиболее низкого элементов управления автоматически копируются в свойства Range двух полос прокрутки формы. Автоматическая прокрутка хорошо работает в Delphi. В преды- дущем примере виртуальный размер формы мог быть установлен на правую гра- ницу последнего списка. Это было определено следующими атрибутами: object ListBox6: TListBox Left = B32 Width = 145 end Таким образом, горизонтальный виртуальный размер формы был бы равен 977 (сумма двух предшествовавших значений). Это число автоматически копируется в поле Range свойства формы HorzScrollBar, пока вы не измените его вручную, что- бы иметь большую форму (как это было сделано для примера Scrolll путем уста- новки для него значения 1000, чтобы оставить некоторое пространство между пос- ледним списком и границей формы). Это значение можно увидеть в Инспекторе объектов или я предлагаю вам провести следующий тест: запустите программу, ус- тановите размеры формы по желанию и переместите указатель прокрутки в самую правую позицию. При добавлении размера формы и позиции указателя всегда бу- дет получаться 1000, значение виртуальной координаты самого правого пиксела формы, независимо от ее размера. 0322
Прокрутка и координаты формы Вы только что видели, что формы могут автоматически прокручивать свои компо- ненты. Но что происходит, если закрашивать непосредственно на поверхности формы? Возникают некоторые проблемы, но решить их нетрудно. Предположим, что нужно нарисовать линии на виртуальной поверхности формы (рис. 7.9). По- скольку, вероятно, у вас нет монитора, способного обеспечить разрешение в 2000 пикселов на каждой оси, можно создать форму меньшего размера, добавлять две полосы прокрутки и установить для них свойство Range, как это сделано в примере Scroll2. Рис. 7.9. Рисование линий на виртуальной поверхности формы При рисовании линий с помощью виртуальных координат формы изображе- ние будет выводиться на экран неправильно. В методе отклика OnPaint следует вычислить виртуальные координаты самостоятельно. К счастью, это очень просто, поскольку известно, что виртуальные координаты XI и Y1 левого верхнего угла кли- ентской области соответствуют текущим положениям этих двух полос прокрутки: procedure TForml FormPai nt(Sender: TObject). var XI. Yl: Integer, begin XI := HorzScrollBar.Position: Yl .= VertScrollBar.Position: // нарисовать желтую линию Canvas.Pen.Width .= 30: 0323
Canvas.Pen.Col or cl Yellow: Canvas.MoveTo (30-X1, 30-Yl): Canvas.LineTo (1970-X1. 1970-Yl): // и так далее . . . Вместо вычисления соответствующей координаты для каждой операции выво- да рекомендуется вызвать API SetWindowOrgEx, чтобы переместить начало коорди- нат Canvas. При этом ваш код рисования обратится непосредственно к виртуаль- ным координатам, но будет отображен на экране должным образом: procedure TForm2.FormPaint(Sender: TObject): begin SetWindowOrgEx (Canvas.Handle. HorzScrollbar.Position. VertScrollbar.Position, nil): // нарисовать желтую пинию Canvas.Pen.Width := 30: Canvas.Pen.Col or := cl Yellow; Canvas.MoveTo (30. 30): Canvas.LineTo (1970. 1970): // и так далее . . . Эта версия программы содержится в исходном коде книги. Попытайтесь, ис- пользуя программу и комментируя вызов SetWindowOrgEx, увидеть, что происхо- дит, если не использовать виртуальные координаты: обнаружится, что вывод про- граммы неправилен — он не будет прокручиваться, и изображение будет всегда оставаться в одном и том же положении, независимо от операций прокручивания. Заметьте также, что Qt/CLX-версия программы QScroll2 не использует виртуаль- ные координаты, а просто вычитает позиции прокрутки из каждой жестко запрог- раммированной координаты. Масштабирование форм При создании формы с многочисленными компонентами можно выбрать границу с фиксированным размером или разрешить пользователю изменять размеры фор- мы и автоматически добавлять полосы прокрутки, чтобы получить доступ к ком- понентам, попадающим вне видимой части формы. Это также возможно, если пользователь вашего приложения имеет драйвер дисплея с намного меньшим чис- лом пикселов, чем ваш. Вместо уменьшения размера формы и прокрутки содержания можно одновре- менно уменьшить размер каждого из компонентов. Это автоматически происхо- дит, если у пользователя системный шрифт с другим соотношением пикселов на дюйм, чем тот, который использовался вами при разработке. Для решения этих проблем Delphi имеет несколько хороших функциональных возможностей масш- табирования, но они не совсем интуитивно понятны. Метод формы ScaleBy позволяет масштабировать форму и каждый из ее компо- нентов. Свойства PixelsPerlnch и Scaled позволяют Delphi автоматически изменять размеры приложения, если оно запущено с другим размером системного шрифта, часто из-за отличающейся разрешающей способности экрана. В обоих случаях для того, чтобы форма соответствовала масштабу ее окна, убедитесь, что для свойства AutoScroll также установлено значение False. В противном случае содержание фор- 0324
мы будет масштабироваться, а сама граница формы нет. Эти два подхода рассмат- риваются в следующих двух разделах. СОВЕТ-------------------------------------------------------------------------- Масштабирование формы рассчитывается на основе различия между высотой шрифта во время выполнения и высотой шрифта в ходе разработки. Масштабирование обеспечивает, что средстве редактирования и другие элементы управления будут достаточно большими, чтобы отобразить и> текст с помощью пользовательских предпочтений шрифта без отсечения текста. Как станет ясне ниже, форма также масштабируется, но самое главное — это сделать удобными для прочтения средства редактирования и другие средства управления. Ручное масштабирование формы В любое время, когда возникнет необходимость масштабировать форму, включая ее компоненты, можно использовать метод ScaleBy, который имеет два целых пара- метра — множитель и делитель в виде дроби. Например, следующий оператор уменьшает размер текущей формы до трех четвертых ее первоначального размера ScaleBy (3. 4): Этот же результат может быть получен путем ScaleBy (75. 100): При масштабировании формы все пропорции сохраняются, но если выйти за некоторые пределы, могут быть слегка изменены пропорции текстовых строк. Про- блема состоит в том, что в Windows размещение компонентов и установка их раз- меров выполняется только в целых пикселах, учитывая, что масштабирование по- чти всегда использует умножение на дробные числа. Поэтому любая дробная часть первоначального компонента или после изменения размера будет усечена. Для демонстрации масштабирования формы вручную в ответ на запрос пользо- вателя я создал простой пример Scale (или QScale). Форма приложения имеет две кнопки, надпись, окно редактирования и связанный с ним (с помощью свойства Associate) элемент управления UpDown (Вверх-вниз). Используя этот параметр на- стройки, пользователь может ввести цифры в окне редактирования или щелкнуть на двух маленьких стрелках, чтобы увеличивать или уменьшать значение (на ве- личину, обозначенную свойством Increment). Чтобы извлечь входное значение можно использовать свойство Text окна редактирования или Position элемента уп- равления UpDown. При щелчке на кнопке Do Scale (Выполнить масштабирование) текущее входное значение используется для определения процента масштабиро- вания формы: procedure TForml.SealeButtonClick(Sender. TObject). begin AmountScaled : = UpDownl.Position: ScaleBy (AmountScaled. 100): UpDownl.Height := Editl.Height: Seal eButton.Enabled := False: RestoreButton.Enabled := True: end: Данный метод сохраняет текущее входное значение в частном поле AmountScaled этой формы и включает кнопку Restore (Восстановить), выключая кнопку, кото- рая была нажата. Позже, при щелчке на кнопке Restore выполняется обратное мас- штабирование. При необходимости восстановить форму до того, как произойдет 0325
другая операция масштабирования, я устраняю суммирование ошибок округле- ния. Я также добавил линию, чтобы задать компоненту UpDown такую же высоту, как и для присоединенного окна редактирования. Это предупреждает небольшое различие между этими двумя значениями из-за проблем масштабирования эле- мента управления UpDown. СОВЕТ--------------------------------------------------------------------- Если вы хотите правильно масштабировать текст формы, включая заголовки компонентов, элемен- ты списков и т. д., следует использовать исключительно шрифты TrueType. Системный шрифт (MS Sans Serif) масштабирует не очень хорошо. Проблема шрифта имеет большое значение, потому что размер многих компонентов зависит от высоты текстов их заголовков, и если заголовок масштаби- руется неправильно, компонент может должным образом не работать. Поэтому в примере Scale использован шрифт Arial. Аналогичная методика масштабирования также действует в CLX, как видно при выполнении примера QScale. Единственное действительное различие заклю- чается в том, что я заменил компонент UpDown (и связанное окно редактирования) элементом управления Spin Edit, потому что прежний недоступен в Qt. Автоматическое масштабирование формы Можно использовать Delphi в своих интересах. При запуске Delphi запрашивает у системы конфигурацию дисплея и сохраняет это значение в свойстве Pixels PerIn ch объекта Screen, доступного в любом приложении специального глобального объек- та VCL. Вопреки названию, PixelsPerlnch не имеет никакого отношения к разрешающей способности экрана (фактически доступной в Screen.Height и Screen.Width). При изменении разрешающей способности вашего экрана с 640x480 до 800x600 и 1024x768 или даже до 1600x1280 обнаружится, что во всех случаях Windows сообщает об одном значении PixelsPerlnch, если не изменить системный шрифт. PixelsPerlnch дей- ствительно обращается к пиксельной разрешающей способности экрана, для кото- рой был разработан установленный в настоящее время системный шрифт. Когда пользователь изменяет масштаб системного шрифта, чтобы сделать меню и дру- гой текст более читаемым, он предполагает, что все приложения будут учитывать эти параметры настройки. Приложение, которое не отражает пользовательские предпочтения рабочего стола, выделяется из ряда и, в исключительных случаях, может быть даже визуально непригодно для тех, кто пользуется очень большими шрифтами и высококонтрастными цветовыми схемами. Наиболее общепринятые значения PixelsPerlnch — 96 (маленькие шрифты) и 120 (большие шрифты), но возможны и другие значения. Новейшие версии Win- dows разрешают пользователю устанавливать произвольный масштаб для размера системного шрифта. В ходе разработки значение экрана PixelsPerlnch, которое име- ет свойство только для чтения, копируется во все формы приложения. Затем Delphi использует значение PixelsPerlnch (если значение свойства Scaled равно True) для изменения размеров формы при запуске приложения. Как уже упоминалось, автоматическое масштабирование и масштабирование, выполняемое методом ScaleBy, действуют на компонентах, изменяя размер шриф- та. Размер каждого элемента управления зависит от используемого им шрифта. При автоматическом масштабировании значение свойства формы PixelsPerlnch (зна- 0326
чение в ходе разработки) сравнивается с текущим системным значением (обозна- ченным соответствующим свойством объекта Screen), и результат используется для изменения шрифта компонентов на форме. Для повышения точности данного кода конечная высота текста сравнивается с высотой, заданной в ходе разработки, и его размер корректируется, если указанные величины не совпадают. Благодаря автоматической поддержке Delphi, приложение, запускаемое на си- стеме с другим размером системного шрифта, автоматически масштабируется без специального кода. Элементы редактирования приложения будут иметь правиль- ный размер для отображения текста с предпочтительным для пользователя разме- ром шрифта, и форма будет иметь правильный размер для включения этих эле- ментов управления. Хотя при автоматическом масштабировании в некоторых особых случаях возникают проблемы, хорошие результаты можно получить при соблюдении следующих правил: О Установите значение свойства форм Scaled равным True (значение по умолчанию). О Используйте только шрифты TrueType. О Используйте на компьютере для разработки форм маленькие шрифты Windows (96 точек на дюйм). О Установите значение свойства AutoScroll, равным False, если нужно масштаби- ровать форму, а не только элементы управления внутри нее (значение по умол- чанию для AutoScroll равно True, поэтому не забудьте выполнить этот пункт). О Задайте местоположение формы вблизи верхнего левого угла или в центре эк- рана (с помощью значения poScreenCenter) во избежание выхода формы за рам- ки экрана. Создание и закрытие форм До сих пор я игнорировал тему создания формы. Известно, что при создании фор- мы вы получаете событие OnCreate и можете изменять или проверять некоторые свойства или поля начальной формы. Ответственный за создание формы оператор находится в исходном файле проекта: begin Application Initialize; Application CreateFormlTForml. Forml): Application Run: end Для пропуска автоматического создания формы можно изменять этот код или использовать страницу Forms (Формы) диалогового окна Project Options (рис. 7.10). В этом диалоговом окне предлагается выбрать, должна ли форма создаваться ав- томатически. Если автоматическое создание отключить, код инициализации про- екта становится следующим: begin Арр!1 cations.Initialize: Application.Run: end 0327
Если теперь запустить эту программу, ничего не произойдет. Она немедленно завершается, поскольку не создается главное окно. Вызов метода приложения CreateForm создает новый экземпляр класса формы, передаваемый как первый па- раметр, и присваивает его переменной, передаваемой как второй параметр. Кое-что происходит неявно. При вызове CreateForm, если в настоящее время глав- ной формы нет, текущей форме присваивается свойство приложения MainForm. Поэтому форма, обозначенная в диалоговом окне (рис. 7.10) как Main Form (Глав- ная форма), соответствует первому вызову метода приложения CreateForm (то есть когда при запуске создается несколько форм). То же самое сохраняется для закрытия приложения. Закрытие главной формы завершает приложение, независимо от других форм. При необходимости выпол- нить эту операцию из программного кода вызовите метод главной формы Close, как в некоторых предыдущих примерах. Рис. 7.10. Страница Forms диалогового окна Delphi Project Options События создания формы Независимо от ручного или автоматического режима при создании формы можно перехватить множество событий. События создания формы запускаются в следу- ющем порядке: 1. OnCreate указывает, что идет создание формы. 2. OnShow показывает, что форма выводится на экран. Кроме главных форм, это событие происходит после установки для свойства формы Visible значения True или вызова метода Show или ShowModal. Это событие запускается снова, если форма скрыта и затем снова выводится на экран. 3. OnActivate указывает, что данная форма становится активной формой в прило- жении. Данное событие запускается при каждом переходе от другой формы приложения к текущей. 0328
4. ДрУгие события, включая OnResfze и OnPaint, показывают операции, всегда вы- полняемые при запуске, а затем многократно повторяемые. СОВЕТ------------——--------------------------------------------------------------- В Qt событие OnResize не будет запускаться, как это происходит в Windows при создании формы. Чтобы при переходе от Delphi к Kylix сделать код более компактным, CLX моделирует это событие, хотя более логично было бы точно настроить VCL, чтобы избежать такого неясного поведения (комментарий в исходном коде CLX описывает эту ситуацию). У каждого события есть своя специальная роль, не считая инициализации фор- мы, за исключением OnCreate, которое вызывается только один раз при создании формы. Однако существует альтернативный подход к добавлению к форме кода ини- циализации: перекрытие конструктора. Обычно это выполняется следующим об- разом: constructor TForml.CreatelAOwner: TComponent): begin inherited Create (AOwner): // дополнительный код инициализации end; До вызова метода Create базового класса свойства формы не загружаются и внут- ренние компоненты недоступны. Поэтому обычный подход включает: сначала вы- зов конструктора базового класса и затем — выполнение пользовательских опера- ций. СОВЕТ----------------------------------------------------------------------------- До версии 3 Delphi использовал другой порядок создания, который обращался к свойству совмести- мости VCL OldCreateOrder. Если для этого свойства по умолчанию задано значение False, весь код конструктора формы выполняется перед кодом обработчика события OnCreate (который запускает- ся специальным методом AfterConstruction). Если вместо этого включить старый порядок создания, вызов конструктора inherited приводит к вызову обработчика события OnCreate. Можно изучить поведение примера CreateOrd, используя эти два значения свойства OldCreateOrder. Закрытие формы При закрытии формы с помощью метода Close или обычных средств системного меню (Alt+F4 системного меню или кнопки Close (Закрыть)), вызывается событие OnCloseQuery. В этом событии можно запросить пользователя подтвердить действие, особенно при наличии несохраненных данных в форме. Вот пример возможного кода: procedure TForml.FormCloseQuery(Sender: TObject: var CanClose: Boolean); begin if MessageDlg ('Are you sure you want to exit?', mtConfirmatwn. [mbYes. mbNo], 0) = mrNo then CanClose False: end; Если OnCloseQuery указывает, что форма все же должна быть закрыта, вызывает- ся событие OnClose. Третий шаг должен вызвать событие OnDestroy, которое явля- ется обратным событию OnCreate и обычно используется для открепления связан- ных с формой объектов и освобождения соответствующей памяти. 0329
СОВЕТ----------------------------------------------------------------------------------j- Чтобы быть более точным, метод BefdreDestruction порождает событие OnDestroy до вызова де- структора Destroy. Так происходит, если не установить для свойства OldCreateOrder значение True, в последнем случае Delphi использует другой порядок закрытия. Итак, что представляет собой промежуточное событие OnClose? Этот метод пре- доставляет другую возможность избежать закрытия приложения, кроме того, можно указать альтернативные «действия закрытия». Метод имеет параметр Action, кото- рому можно присвоить следующие значения: О CaNone. Не разрешается закрыть форму. Это соответствует указанию для пара- метра CanClose метода OnCloseQuery значения False. О caHide. Форма не закрывается, а только становится скрытой. Применяется, если в приложении имеются другие формы; в противном случае программа завер- шает работу. Является значением по умолчанию для вторичных форм, именно поэтому я вынужден был обработать событие OnClose в предыдущем примере для закрытия вторичных форм. О caFree. Форма закрывается, освобождая память, и приложение в итоге закрыва- ется, если это была главная форма. Это действие по умолчанию для главной формы и его следует использовать при динамическом создании нескольких форм (если нужно удалить окна и уничтожить соответствующий объект Delphi при закрытии формы). О caMinimize. Форма не закрывается, а только сворачивается. Это — действие по умолчанию для дочерних форм MDI. СОВЕТ--------------------------------------------------------------------- При выключении Windows активизируется событие OnCloseQuery и программа может использовать его для остановки процесса отключения. В данном случае событие OnClose не вызывается даже в том случае, если OnCloseQuery устанавливает для параметра CanClose значение True Диалоговые окна и другие вторичные формы При написании программы диалоговое окно отличается от какой-либо вторичной формы только границей, значками границы и другими такими же настраиваемы- ми элементами пользовательского интерфейса. Пользователи связывают с диалоговым окном концепцию модального окна (modal window), окна, которое находится в фокусе и должно быть закрыто до того, как пользователь сможет вернуться к главному окну. Это верно для окон сообщения и, как правило, также для диалоговых окон. Однако диалоговые окна также могут быть немодальными (modeless). Мнение, что диалоговые окна — это только модальные формы, не совсем точно. В Delphi (как и в Windows) могут быть немодальные диалоговые окна и модаль- ные формы. Следует учитывать два разных элемента: граница формы и ее пользо- вательский интерфейс определяют, будет ли форма выглядеть как диалоговое окно; использование двух различных методов (Show и ShowModal) для отображения вто- ричной формы определяет ее поведение (немодальное или модальное). 0330
добавление в программу второй формы Чтобы добавить вторую форму к приложению, щелкните на кнопке New Form (Со- здать форму) на панели инструментов Delphi или используйте команду меню фор- мы File ► New (Файл ► Создать). В качестве альтернативы можно выбрать File ► New ► Other (Файл ► Создать ► Другие документы), перейти на страницу Forms (Фор- мы) или Dialogs (Диалоги) и выбрать один из доступных шаблонов формы или мастеров создания форм. Если в проекте имеется две формы, для перехода по ним в ходе разработки можно использовать кнопку View Form или View Unit на панели инструментов Delphi. Вы можете также определить главную форму и формы, которые должны автоматиче- ски создаваться при запуске с помощью страницы Forms диалогового окна Project Options (Параметры проекта). Эта информация отражена в исходном коде файла проекта. ПРИМЕЧАНИЕ--------------------------------------------------—---- Вторичные формы автоматически создаются в файле исходного кода проекта в зависимости от состояния флажка Auto Create Forms (Автоматическое создание форм) на странице Designer (Проек- тировщик) диалогового окна Environment Options (Параметры среды). Хотя автоматическое созда- ние является самым простым и наиболее надежным подходом для неопытных разработчиков и быстрых проектов, рекомендуется снять этот флажок для любого серьезного развития проекта. Если ваше приложение содержит сотни форм, они не должны все создаваться при запуске прило- жения. Создавайте экземпляры вторичных форм, когда в них возникнет необходимость, и освобож- дайтесь от них, когда они сделаны. После создания вторичной формы можно установить для ее свойства Visible значение True, и обе формы будут отображаться при запуске программы. Обычно вторичные формы приложения оставляют «невидимыми» и затем выводят на эк- ран путем вызова метода Show (или задавая во время выполнения свойство Visible). При использовании функции Show вторая форма будет отображена как немодаль- ная, поэтому перейти к первой форме можно в то время, когда вторая еще является видимой. Закрыть вторую форму можно с помощью системного меню или щелк- нув на кнопке или пункте меню, который вызывает метод Close. Как было показа- но, по умолчанию действие закрытия (см. событие OnClosfi) для вторичной формы должно просто скрыть ее, поэтому вторичная форма при закрытии не уничтожает- ся. Она сохраняется в памяти (не всегда лучшее решение) и доступна при необхо- димости. Создание вторичной формы во время выполнения Если вы не создаете все формы при запуске программы, следует проверить, суще- ствует ли форма, и создать ее при необходимости. Самый простой случай, когда вы хотите создать несколько копий одной и той же формы во время выполнения. В примере MultiWin/QMu ItiWin это сделано с помощью следующего кода: with TForm3 Create (Application) do Show. При каждом щелчке на кнопке создается новая копия формы. Заметьте, что я не использую глобальную переменную Form3, потому что нет смысла присваивать этой переменной новое значение при каждом создании объекта новой формы. 0331
Важно, однако, не обращаться к глобальной переменной Form3 в коде формы или/в других частях приложения. Переменная Form3 будет неизменно указывать на nil. В таком случае необходимо удалить ее из модуля, чтобы избежать какой-либо Не- точности. ПРИМЕЧАНИЕ------------------------------------------------------------------— В коде формы, которая может иметь несколько экземпляров, ни в коем случае нельзя явно обра- щаться к форме, используя глобальную переменную, которую загружает для этого Delphi. Предпо- ложим, например, что в коде для TForm3 вы обращаетесь к Form3.Caption. При создании второго объекта того же типа (класс TForm3) выражение Form3.Caption будет обращаться к заголовку объекта формы, связанного с переменной Form3, которая не может быть текущим объектом, выполняющим код. Чтобы избежать этой проблемы, для указания заголовка текущего объекта формы обратитесь к свойству Caption в методе формы, и используйте ключевое слово Self, если необходима конкрет- ная ссылка на объект текущей формы. Во избежание проблем при создании нескольких копий фор- мы я предлагаю удалять глобальный объект формы из интерфейсной части модуля, объявляющего форму. Эта глобальная переменная требуется только для автоматического создания формы. При динамическом создании нескольких копий формы не забывайте уничто- жать все объекты формы после их закрытия путем обработки следующего собы- тия: procedure TForm3.FormClose(Sender: TObject. var Action- TCloseAction). begin Action : = caFree; end: Если этого не делать, память будет значительно нагружена, поскольку все со- зданные формы (окна и объекты Delphi) становятся скрытыми, но остаются в па- мяти. Создание одного экземпляра вторичной формы Давайте рассмотрим динамическое создание формы в программе, которая способ- на сделать только одну копию формы за прогон. Создание модальной формы дос- таточно просто, поскольку диалоговое окно может быть уничтожено при закры- тии с помощью следующего кода: var Modal: TForm4: begin Modal := TForm4.Create (Application): try Modal.ShowModal. finally Modal.Free: end: Поскольку запрос ShowModal может вызвать исключение, необходимо написать его в блоке try, за которым следует блок finally, чтобы убедиться, что объект будет освобожден. Обычно этот блок также включает код, инициализирующий диалого- вое окно перед его отображением, и код, извлекающий установленные пользовате- лем перед разрушением формы значения. Конечные значения имеют свойство толь- ко для чтения, если результат функции ShowModal — mrOK, как будет видно из следующего примера. 0332
Немного сложнее вывести на экран только одну копию немодальной формы. Необходимо создать такую форму, если ее еще нет, и затем показать ее: if not Assigned (Form2) then Form2 := TForm2.Create (Application): Form2.Show; С помощью этого кода форма создается при первой необходимости в ней и за- тем сохраняется в памяти, оставаясь видимой на экране или скрытой от просмот- ра. Чтобы избежать излишней нагрузки на память и системные ресурсы, нужно уничтожить вторичную форму после ее закрытия. Это можно сделать, написав об- работчик для события OnClose: procedure TForm2.FormClose(Sender: TObject: var Action: TCloseAction); begin Action := caFree: // внимание: установить указатель на нуль!! Form2 := nil: end; Учтите, что после уничтожения формы глобальная переменная Form2 установ- лена на nil, что противоречит правилу, определенному ранее для форм с несколь- кими экземплярами, но поскольку перед нами форма с одним экземпляром, это — прямо противоположный случай. Без указанного кода закрытие формы приведет к уничтожению ее объекта, но переменная Form2 все еще будет обращаться к пер- воначальному местоположению в памяти. При этом при попытке повторно ото- бразить форму с помощью показанного ранее метода btnSi ngleClick, проверка if not Assigned() пройдет успешно, поскольку она проверяет, имеет ли переменная Form2 значение nil. Код не сможет создать новый объект, и метод Show (вызванный для несуществующего объекта) приведет к ошибке системной памяти. В качестве эксперимента можно создать данную ошибку, удалив последнюю строку предыдущего листинга. Как вы видели, решение состоит в установке для объекта Form2 значения nil при уничтожении объекта для того, чтобы правильно написанный код «увидел», что новая форма должна быть создана до его использо- вания. Напомню, что экспериментирование с примером MultiWin/QMultiWin дока- зывает полезность проверки различных условий (изображения экрана из этого примера отсутствуют, поскольку отображаемые им формы полностью пусты, за исключением главной формы, на которой имеется три кнопки). СОВЕТ---------------------------------------------------------------------------- Установка для переменной формы значения nil имеет смысл и срабатывает, если в любой данный момент присутствует только один экземпляр формы. Для создания нескольких копий формы необ- ходимо применять другие методы, чтобы отследить их. Также имейте в виду, что в этом случае нельзя использовать процедуру FreeAndNil, поскольку отсутствует возможность вызова Free для Form2 — нельзя уничтожить форму до того, как закончилось выполнение ее обработчиков событий. Создание диалогового окна Ранее в этой главе утверждалось, что диалоговое окно не очень отличается от дру- гих форм. Чтобы создать вместо формы диалоговое окно, нужно только выбрать 0333
для свойства BorderStyle значение bsDialog. После этого простого изменения интер- фейс формы становится подобным интерфейсу диалогового окна, без системного значка и сворачивающих и разворачивающих окон. Естественно, такая форма имеет типичную толстую границу диалогового окна, которая не позволяет изменять его размеры. / После создания формы диалогового окна его можно вывести на экран в ^йде модального или немодального окна с помощью двух обычных методов отображе- ния (Show и ShowModal). Модальные диалоговые окна, однако, используются чаще, чем немодальные. Для форм — все наоборот; модальных форм, как правило, нужно избегать, потому что пользователь не ожидает их. Диалоговое окно примера RefList В главе 5 была рассмотрена программа Ref Li st/Q Ref Li st, которая использовала эле- мент управления ListView для вывода на экран ссылок к книгам, журналам, веб- сайтам и т. д. В версии RefList2 (и ее варианте QRefList2 для CLX) к этой основной версии добавлено диалоговое окно, которое используется в двух разных случаях: для добавления новых элементов в список и редактирования существующих эле- ментов. ВНИМАНИЕ -------------------------------------------------------- В CLX-компоненте ListView присутствует проблема. Если активизировать флажки и затем отключить их, изображения исчезнут. Это поведение примера QRefList из главы 5. Для обхода этой ошибки в версии QRefList2 добавлен код, который повторно назначает свойство Imageindex каждого элемента. Единственная очень интересная функция этой формы в данном VCL-приме- ре — использование компонента ComboBoxEx, который придан к тому же ImageList, который используется элементом управления ListView главной формы. Применяе- мые для выбора типа ссылки раскрывающиеся элементы списка включают тексто- вое описание и соответствующее изображение. Как уже упоминалось, данное диалоговое окно используется в двух различных случаях. Первый — при выборе пользователем в меню File ► Add Items (Файл ► До- бавить элементы): procedure TForml.AddltemslClтck(Sender: TObject); var Newltem: TListltem: begin FormItem.Caption := 'Newltem'-. FormItem.Clear: if FormItem.ShowModal = mrOK then begin Newltem := ListViewl.Items.Add; Newltem.Caption .= FormItem.EditReference.Text; Newltem.Imageindex := FormItem.ComboType.Itemindex: Newltem.SubItems.Add (FormItem.EditAuthor.Text): Newltem.SubItems.Add (FormItem.EditCountry.Text): end: end: Кроме установки соответствующего заголовка для формы, эта процедура ини- циализирует диалоговое окно, поскольку вводится новое значение. При щелчке на 0334
кнопке ОК программа добавляет новый элемент к представлению списка и уста- навливает все его значения. Для очистки окон редактирования диалога программа вызывает пользовательский метод Clear, который сбрасывает текст каждого эле- мента управления окна редактирования: procedure TFormltem.Clear: var I: Integer: begin // очистить все окна редактирования for I := 0 to ControlCount - 1 do if Controls [I] is TEdit then TEdit (Controls[I]).Text := end: Редактирование существующего элемента требует немного другого подхода. Сначала текущее значение переносится в диалоговое окно до его отображения. Затем при щелчке на кнопке ОК программа вместо создания нового изменяет теку- щий элемент списка. Вот код: procedure TForml.ListViewlDblClick(Sender: TDbject); begin if ListViewl.Selected <> nil then begin И инициализация диалога FormItem.Caption ;= 'Edit Item'; FormItem.EditReference.Text := ListViewl.Selected.Caption; FormItem.ComboT.ype.Itemindex : = ListViewl.Selected.Imageindex; FormItem.EditAuthor.Text ;= ListViewl.Selected.SubItems [0]; Formitem.EditCountry.Text := ListViewl.Selected.SubItems [1J; // показать его if Formitem.ShowModal = mrDK then begin // прочитать новое значение ListViewl.Selected.Caption ;= Formitem.EditReference.Text; ListViewl.Selected.Imageindex ;= Formitem.ComboType.Itemindex: ListViewl.Selected.Subitems [0] := Formitem.EditAuthor.Text: ListViewl.Selected.Subitems [1] := Formitem.EditCountry.Text: end; end; end; Результат работы этого кода показан на рис. 7.11. Заметьте, что код, применяе- мый для чтения значения нового элемента или измененного элемента, аналогичен. В общем, следует попробовать избежать такого дублированного кода и по возмож- ности разместить общие операторы кода в методе, добавленном к диалоговому окну. В таком случае метод мог бы получить в качестве параметра объект TListltem и ско- пировать в него нужные значения. СОВЕТ------------------------------------------------------------------------------------- Какие внутренние события происходят при щелчке на кнопке ОК или CANCEL в диалоговом окне? Модальное диалоговое окно закрывается путем установки его свойства ModalResult, при этом воз- вРащается значение данного свойства. Возвращаемое значение может быть показано с помощью Установки свойства кнопки ModalResult. При щелчке на кнопке ее значение ModalResult копируется в Форму, что приводит к закрытию формы и возвращению этого значения как результата функции ShowModal. 0335
Рис. 7.11. Диалоговое окно примера RefListZ в режиме редактирования. Заметьте, что используется графический компонент ComboBoxEx Немодальное диалоговое окно Второй пример диалоговых окон показывает более сложное модальное диалого- вое окно, которое использует стандартный подход (также как и немодальное диа- логовое окно). Главная форма примера DlgApply (и идентичной демо-версии Qdlg- Apply на основе CLX) имеет пять надписей с именами (см. рис. 7.12 и исходный код примера). Рис. 7.12. Три формы (главная форма и два диалоговых окна) примера DlgApply во время выполнения При щелчке на имени его цвет изменяется на красный; если щелкнуть на нем два раза, программа отображает модальное диалоговое окно со списком имен для выбора. При щелчке на кнопке Style открывается немодальное диалоговое окно, позволяющее изменить стиль шрифта надписей главной формы. Пять надписей на главной форме связаны с двумя методами: для события OnClick и для события OnDoubleClick. Первый метод закрашивает красным последнюю надпись, на кото- рой щелкнул пользователь, сбрасывая все другие к черному (для их свойства Тад установлено значение 1 в качестве группового индекса). Заметьте, что этот же ме- тод связан со всеми надписями: procedure TForml.LabelClick(Sender: TObject); var I: Integer; begin for I ;= 0 to Componentcount - 1 do if (Components[I] is TLabel) and (Components [ I ]. Tag - 1) then TLabel <Components[I]).Font.Color := clBlack; // установить красный цвет для выбранной надписи 0336
, (Sender as TLabel).Font.Color := cl Red: end; Второй метод (обычный для всех надписей) — обработчик события On DoubleClick. Метод LabelDoubleClick выбирает свойство Caption текущей надписи (обозначенной параметром Sender) в списке диалога и затем отображает модальное диалоговое окно. При щелчке на кнопке ОК для закрытия диалогового окна при выбранном элементе списка данный выбор копируется назад в заголовок надписи: procedure TForml.LabelDoubleCiтck(Sender: TObject): begin with ListDial.Listboxl do begin 11 выбрать текущее имя в списке Itemindex = Items.IndexOf (Sender as TLabel).Caption); // показать модальное диалоговое окно, проверив возвращенное значение if (ListDial.ShowModal = mrOk) and (Itemindex >= 0) then // копировать выбранный элемент в надпись (Sender as TLabel).Caption : = Items [Itemindex]: end: end: ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Обратите внимание, что весь используемый для настройки модального диалогового окна код содер- жится в методе LabelDoubleClick главной формы. У формы этого диалогового окна нет добавленного кода. Немодальное диалоговое окно, наоборот, имеет большое количество присоеди- ненного кода. Главная форма отображает диалоговое окно при щелчке на кнопке Style (заметьте, что заголовок кнопки имеет в конце три точки, указывающие на то, что она ведет к диалоговому окну) путем Вызова его метода Show (рис. 7.12). Две кнопки Apply (Применить) и Close (Закрыть) заменяют кнопки ОК и Cancel (Отмена) в немодальном диалоговом окне (эти кнопки можно быстро получить, выбрав значение ЬкОК или bkCancel для свойства Kind и затем отредактировав Capti- on). Иногда можно обнаружить кнопку Cancel, которая работает как кнопка Close, но кнопка ОК в немодальном диалоговом Окне обычно не имеет значения. Вместо этого одна или несколько кнопок могли бы выполнять определенные действия на главном окне, например, Apply (Применить), Change Style (Изменить стиль), Replace (Заменить), Delete (Удалить) и т. д. При щелчке на одном из флажков в этом немодальном диалоговом окне внизу соответственно изменяется стиль текста образца надписи. Это выполняется путем Установки или снятия определенного флажка, который указывает стиль, как это сделано в следующем обработчике события OnClick: procedure TStyleDial.ItalicCheckBoxClickfSender: TObject): begin if ItalicCheckBox.Checked then Label Sample.Font.Style := LabelSample.Font.Style + [fsltalic] el se Label Sample. Font. Style .-= LabelSample. Font. Style - [fsltalic]; end; При щелчке на кнопке Apply (Применить) программа копирует стиль образца надписи во все надписи формы, а не рассматривает значения флажков: 0337
procedure TStyleDial.ApplyBitBtnClicktSender: TObject); begin Forml Label 1.Font.Style ;= Label Sample.Font.Style; Forml.Label 2.Font.Style := Label Sample.Font Style: В качестве альтернативы вместо прямого обращения к каждой надписи ее мЬж- но найти путем вызова метода формы FindComponent, отправив как параметр имя надписи и затем преобразовав результат в тип TLabel. Преимущество этого подхода заключается в том, что имена различных надписей можно создавать с помощью цикла for: ' procedure TStyleDial.ApplyBitBtnClickfSender TObject); ' var I. Integer: begin for I = 1 to 5 do (Forml.FindComponent ('Label' + IntToStr (I)) as TLabel).Font Style := Labelsample Font.Style: end: ПРИМЕЧАНИЕ-------------------------------------------------------------------------------- Метод ApplyBitBtnClick также можно написать, сканируя массив Controls в цикле, как это было сде- лано в других примерах. Для демонстрации другого метода здесь я решил использовать метод FindComponent. В данный момент версия кода работает медленнее из-за большого количества выполняемых операций, но разница не будет очень заметна, потому что скорость достаточно высока. Конечно, этот подход более гибок; если добавить новую над- пись, нужно только установить более высокий предел цикла for, обеспечив, чтобы все надписи имели последовательные номера. Обратите внимание на то, что при щелчке на кнопке Apply (Применить) диало- говое окно не закрывается — такой эффект имеет только кнопка Close (Закрыть). Учтите также, что этому диалоговому окну не нужен код инициализации, поскольку форма не уничтожена, и ее компоненты сохраняют свое состояние при каждом ото- бражении этого диалогового окна. Заметьте, что в CLX-версии программы QDlgApply диалог является модальным, даже если он вызывается методом Show. Предопределенные диалоговые окна Кроме создания собственных диалоговых окон Delphi позволяет использовать не- которые виды диалоговых окон по умолчанию. Некоторые из них предопределены Windows; другие — простые диалоговые окна (типа окон сообщения), отображае- мые стандартной программой Delphi. Палитра компонентов Delphi содержит стра- ницу компонентов диалогового окна. Каждое из этих диалоговых окон (известных как общие диалоги Windows) определено в системной библиотеке ComDlg32. DLL. Общие диалоги Windows Вы, вероятно, уже знакомы с некоторыми из этих диалоговых окон, поскольку они использовались в предыдущих примерах. Главное — нужно поместить соответ- 0338
ствующий компонент на форму, установить некоторые ее свойства, запустить диа- логовое окно (с помощью метода Execute, возвращающего булево значение) и об- наружить свойства, которые были установлены при его запуске. Для помощи в экспериментах с этими диалоговыми окнами я создал программу CommDlgTest. Я выделю некоторые ключевые и неочевидные функции общих диалоговых окон и предоставлю возможность подробно изучить исходный код примера: О Компонент Open Dialog может быть настроен путем установки различных филь- тров расширения файлов с помощью свойства Filter, которое имеет удобный редактор. Значение этого свойства может быть непосредственно присвоено с использованием строки, например, Text File (*.txt)|*.txt. Другая полезная фун- кция позволяет диалогу проверить, соответствует ли расширение выбранного файла расширению по умолчанию, проверяя после выполнения диалога фла- жок ofExtensionDifferent свойства Options. Наконец, этот диалог позволяет вы- брать многие параметры, если установить его опцию ofAllowMultiSelect. В дан- ном случае — путем просмотра свойства списка строки Files. О Компонент SaveDialog используется аналогичными способами и имеет такие же свойства, хотя, конечно, отсутствует возможность выбора нескольких файлов. О Компоненты 0 pen Picture Di a log и SavePictureDialog обеспечивают похожие возмож- ности, но имеют пользовательскую форму, позволяющую предварительно про- смотреть изображение. Эти компоненты целесообразно использовать только для открытия или сохранения графических файлов. О Компонент FontDialog может использоваться для показа и выбора всех типов шрифтов, шрифтов, используемых на экране и выбранном принтере (режим WYSIWYG (наглядного отображения) или только шрифтов TrueType). Мож- но показать или скрыть часть, связанную со специальными эффектами, и полу- чать другие различные версии с помощью настройки его свойства Options. Так- же можно активизировать кнопку Apply (Применить), обеспечивая обработчик события OnApply и используя параметр fdAp ply Button. Диалоговое окно Font (Шрифт) с кнопкой Apply (Применить) (рис. 7.13) ведет себя почти так же, как немодальное диалоговое окно (но не является таковым). О Компонент ColorDialog использует различные параметры для показа с самого начала полностью открытого диалога или для воспрещения его полного откры- тия. Эти параметры cdFuLLOpen или cdPreventFullOpen являются значениями свой- ства Options. О Диалоговые окна Find и Replace — действительно немодальные диалоги, и реа- лизовывать функциональные возможности поиска и замены необходимо само- стоятельно, как частично сделано мной в примере CommDlgTest. Пользователь- ский код связан с кнопками этих двух диалоговых окон, обеспечивая события OnFind и OnReplace. СОВЕТ-------------------------------------------------------------—---------- Qt предлагает похожий набор предопределенных диалоговых окон, но набор параметров, как пра- вило, более ограничен. Для экспериментов с этими параметрами настройки предлагается версия примера QCommDIg. Программа CLX имеет меньше пунктов меню, потому что некоторые опции недоступны; имеются и другие минимальные изменения в исходном коде. 0339
Font Рис. 7.13. Параметры выбора диалогового окна Font с кнопкой Apply Окна сообщений Окна сообщений и окна для ввода Delphi представляют собой другой набор пре- допределенных диалоговых окон. Для отображения простых диалоговых окон ис- пользуется ряд процедур и функций Delphi: О Функция MessageDlg показывает настраиваемое окно сообщения с одной или несколькими кнопками и, как правило, bitmap-изображение. Функция Messa- geDlgPos аналогична функции MessageDlg, но окно сообщения выводится на эк- ран в конкретном положении, а не в центре экрана (если не используется поло- жение -1,-1, чтобы заставить его появиться в центре экрана). О Процедура ShowMessage выводит на экран более простое окно сообщения с на- званием приложения в качестве заголовка и кнопкой ОК. Процедура ShowMessa- gePos делает то же самое, но следует указать положение окна сообщения. Про- цедура ShowMessageFmt — это вариант ShowMessage, которая имеет такие же параметры, как и функция Format. Она соответствует вызову Format внутри зап- роса к ShowMessage. О Метод MessageBox объекта Application позволяет определить сообщение и заго- ловок, а также различные кнопки и возможности. Это — простая и прямая ин- капсуляция функции MessageBox API-интерфейса Windows, который передает в качестве главного параметра окна дескриптор объекта Application. Этот де- скриптор необходим, чтобы заставить окно сообщения вести себя как модаль- ное окно. О Функция In putBox просит пользователя ввести строку. Вы обеспечиваете заго- ловок, запрос и строку по умолчанию. Функция InputQuery также просит ввести строку. Единственное отличие между этими функциями — в синтаксисе. Фун- кция InputQuery использует возвращаемое булево значение, которое указывает, щелкнул пользователь на кнопке ОК или на кнопке Cancel. 0340
Для демонстрации нескольких доступных в Delphi окон сообщения я написал другой образец программы с аналогичным подходом к предшествующему приме- ру CommDlgTest. До щелчка на кнопке для отображения окна сообщения в примере MBParade имеется возможность выбора большого числа элементов (переключате- ли, флажки, окна редактирования и элементы управления редактированием с из- меняемым числовым полем). В похожем примере QMbParade отсутствует только кнопка Help, которая недоступна в окнах сообщения CLX. Окна About и окна-заставки Приложения обычно имеют окно About (О программе), в котором можно размес- тить информацию о версии программного продукта, уведомление об авторских правах и т. д. Самый простой способ создать окно About — использовать функцию MessageDlg. С помощью этого метода можно показать только ограниченный объем текста и нельзя — специальную графику. Поэтому обычно для создания окна About используется диалоговое окно, на- пример, один из шаблонов Delphi по умолчанию. В этом окне можно добавить код для отображения системной информации, например, о версии Windows или необ- ходимом объеме свободной памяти, или какой-либо пользовательской информа- ции, например, зарегистрированного имени пользователя. Создание окна-заставки Другой стандартный метод отображает начальное окно до появления главной фор- мы приложения. Вывод какого-либо изображения на экран во время загрузки про- граммы дает хороший визуальный эффект и делает приложение более приятным. Иногда в качестве такого отображается окно About. Для примера полезности окна- заставки я создал программу, выводящую на экран список с простыми числами. Простые числа вычисляются при запуске программы с помощью цикла for, вы- полняемого в диапазоне от 1 до 30 000; числа отображаются, как только форма ста- новится видимой. Поскольку я (намеренно) использовал медленную функцию для вычисления простых чисел, выполнение этого кода инициализации длится неко- торое время. Числа добавляются к списку, закрывающему всю клиентскую область формы, что позволяет отображать многочисленные столбцы (рис. 7.14). Существует три версии программы Splash (плюс три соответствующие CLX- версии). Как видно из примера SplashO, проблема состоит в том, что в методе Form- Create начальная операция занимает много времени. Для отображения главной формы при запуске программы требуется несколько секунд. Если ваш компьютер работает очень быстро или очень медленно, можно изменить верхний предел цик- ла for в методе FormCreate, чтобы замедлить или ускорить работу программы. Данная программа имеет простое диалоговое окно с компонентом изображе- ния, заголовком и растровой кнопкой, размещенными на панели, занимающей всю поверхность окна About. Эта форма отображается при выборе Help ► About (Справ- ка ► О программе). Если нужно отобразить это окно About во время старта про- граммы, следует выполнить примеры Splash 1 и Splash2, которые показывают окно- заставку с помощью двух различных методов. 0341
Рис.7.14. Главная форма примера Splash с окном-заставкой (версия Splash?) Сначала я добавил метод к классу TAboutBox. Этот метод, называемый MakeSplash, изменяет некоторые свойства формы, чтобы сделать ее пригодной для формы-за- ставки. Главным образом, он удаляет границу и заголовок, скрывает кнопку ОК, делает границу панели толстой (для замены границы формы) и затем показывает форму, немедленно перерисовывая ее: procedure TAboutBox.MakeSplash; begin Borderstyle : = bsNone; BitBtnl.Visible False; Panel 1 BorderWidth := 3; Show; Update; end; Этот метод вызывается после создания формы в файле проекта примера Splashl. Код выполняется перед созданием других форм (в этом случае только главной формы), и затем перед запуском приложения окно-заставка удаляется. Эти опера- ции происходят в рамках блока try/finally. Вот исходный код главного блока файла проекта для примера Splash2: var SplashAbout; TAboutBox; begin Application.Initialize; // создать и показать окно-заставку SplashAbout := TAboutBox.Create (Application); try SplashAbout.MakeSplash; 0342
// стандартный код.. . Арр!1 cation.CreateForm(TForml. Forml): // избавиться от формы заставки SplashAbout.Close: finally SplashAbout.Free: end: Application.Run; end. Такой подход нужен, только если для создания главной формы вашего прило- жения, выполнения кода его запуска (как в этом случае) или открытия таблиц базы данных требуется значительное время. Заметьте, что окно-заставка — это первая созданная форма, но поскольку программа не использует метод CreateForm объекта Application, она не становится главной формой приложения. Иначе закрытие окна- заставки завершило бы программу! Альтернативный подход состоит в том, чтобы сохранить форму заставки на эк- ране несколько дольше и использовать таймер для ее удаления. Эта методика осу- ществлена в примере Splash2. Кроме того, данный пример использует другой под- ход для создания формы заставки: форма создается не в исходном коде примера, а в самом начале метода FormCreate главной формы. procedure TForml.FormCreate(Sender: TObject): var I: Integer; SplashAbout- TAboutBox; begin // создать и показать форму заставки SplashAbout ;» TAboutBox.Create (Application); Spla shAbout.MakeSplash; // замедлить код (опущен)... // уничтожить всплывающую форму, со временем SplashAbout.Timed.Enabled :» True: end; Таймер включается непосредственно перед завершением метода. После исте- чения интервала (в приведенном примере — 3 секунды) активизируется событие OnTimer и форма заставки обрабатывает его путем самозакрытия и самоуничтоже- ния, вызывая события Close и затем Release. СОВЕТ---------------------------------------------------------------------------------- Метод формы Release аналогичен методу объектов Free, но уничтожение формы откладывается до завершения выполнения всех обработчиков событий. Использование Free внутри формы приводит к затруднению доступа, поскольку запустивший обработчик события внутренний код может снова Необходимо запомнить еще один момент. Главная форма будет отображаться позже и сверху формы окна-заставки, если не сделать ее формой поверх всех. По- этому в примере Splash2 я добавил одну строку к методу MakeSplash окна About: FormStyle := fsStayOnTop; 0343
Что далее? В этой главе мы исследовали некоторые важные свойства формы. Теперь вы знае- те, как работать с размером и положением формы, как получить ввод с мыши и на- рисовать форму. Вы больше узнали о диалоговых окнах, модальных формах, пре- допределенных диалогах, всплывающих экранах и многих других методах, включая интересный эффект альфа-сопряжения. Понимание деталей работы с формами чрезвычайно важно для правильного использования Delphi, особенно для созда- ния сложных приложений (если, конечно, вы создаете услуги или веб-приложе- ния без интерфейса пользователя). В главе 8 мы продолжим исследовать всю структуру приложения Delphi, обра- тив особое внимание на роли двух глобальных объектов: Application и Screen. Так- же рассмотрим развитие интерфейс MDI для более полного изучения расширен- ных возможностей форм, например, визуального наследования формы. Кроме того, будут обсуждены фреймы, которые являются сходными с формами контейнерами визуальных компонентов. В этой главе я также кратко представил непосредственное рисование и исполь- зование классаTCanvas. Подробнее о графике в формах Delphi рассказывается в до- полнительной главе «Графика в Delphi», которая упоминается Приложении В. 0344
ЧАСТЬ II Объектно- ориентированные архитектуры Delphi В этой части: ♦ Глава 8. Архитектура Delphi-приложений ♦ Глава 9. Создание Delphi-компонентов ♦ Глава 10. Библиотеки и пакеты ♦ Глава 11. Моделирование и ООП-программирование ♦ Глава 12. От СОМ к СОМ+ 0345
8 Архитектура Delphi- приложений Хотя с начала книги мы построили уже несколько Delphi-приложений, но струк- туру и архитектуру приложений, построенных на библиотеках классов, мы не рас- сматривали. Например, мы совершенно не касались объекта Application, техноло- гий отслеживания создаваемых форм, потока системных сообщений и прочих элементов. В главе 7 мы изучили, как построить приложение с несколькими формами и ди- алоговыми окнами. Однако не рассмотрели, как эти формы взаимосвязаны друг с другом, как совместно использовать одинаковые характеристики различных форм и как оперировать несколькими подобными формами одинаковым способом. Основная цель этой главы — рассмотреть эти темы. Глава охватывает как базо- вые, так и дополнительные технологии, включая визуальное наследование, исполь- зование фреймов, MDI-разработки, а также использование интерфейсов для по- строения сложной иерархической структуры классов форм. В этой главе рассматриваются следующие вопросы: О глобальные объекты Application и Screen; О сообщения и многозадачность в Windows; О фоновая обработка и многопоточность; О нахождение предыдущего экземпляра приложения; О MDI-приложения; О визуальное наследование форм; О фреймы; О базовые формы и интерфейсы; О диспетчер памяти Delphi. Объект Application Я уже упоминал объект Application во многих случаях, но поскольку эта глава по- священа структуре Delphi-приложений, пора разобраться с деталями этого объек- та и соответствующего ему класса. Application — это глобальный объект класса "[Application, определенный в модуле Forms и создаваемый в модуле Controls. Класс "[Application — это компонент, но его нельзя использовать в ходе разработки. Не- 0346
Объект Application 347 которые из его свойств могут автоматически устанавливаться с помощью страни- цы Application (Приложение) диалогового окна Project Options (Параметры проек- та); другие свойства — с помощью программного кода. Для обработки событий среда Delphi содержит удобный компонент Application- Events. Помимо того, что он позволяет назначать обработчики во время разработ- ки, еще одно преимущество этого компонента заключается в том, что он допускает наличие нескольких обработчиков. Если поместить экземпляры компонента Appli- cation Events в две различные формы, то каждый из них сможет обрабатывать одно и то же событие, и оба обработчика события будут исполнены. Другими слова- ми, множество компонентов Application Events могут составлять цепочку обра- ботчиков. Ряд событий масштаба приложения, включая OnActivate, OnDeactivate, OnMinimize и On Restore, позволяют отслеживать состояние приложения. Другие события пере- направляются приложению посредством использования воспринимающих их эле- ментов управления, например, On Action Execute, On Actionllpdate, OnHelp, OnHint, On- ShortCut и OnShowHint. И, наконец, существует обработчик глобального исключения OnException, который использовался нами в главе 2, событие Onldle, используемое для выполнения обработки в фоновом режиме, и событие OnMessage, которое за- пускается при отправке сообщения какому-либо окну или оконному элементу уп- равления приложения. Несмотря на то что класс объекта Application наследуется непосредственно от класса TCom ponent, этот объект имеет связанное с ним окно. Окно приложения спря- тано от просмотра, но представлено в панели задач Windows. Вот почему Delphi называет окно Forml, а значок в панели задач — Projectl. Окно, относящееся к объекту Application (то есть окно приложения), предназ- начено для того, чтобы содержать все окна этого приложения. Тот факт, что все формы верхнего уровня данной программы имеют невидимое окно-владелец, яв- ляется фундаментальным при активизации приложения. Когда окно вашего при- ложения находится позади окон других программ, щелчок на окне вашего прило- жения приведет к переходу всех окон этого приложения на передний план. Другими словами, невидимое окно приложения используется для связи различных форм приложения. (Окно приложения не является скрытым, поскольку это бы влияло на его поведение; просто оно имеет нулевую высоту и нулевую ширину, поэтому его и не видно.) ПРИМЕЧАНИЕ--------------------------------------------------------------- В Windows, операции Minimize (Свернуть) и Maximize (Восстановить) по умолчанию связаны с сис- темными звуками и анимационными эффектами. Приложения, построенные в Delphi, также по умол- чанию связаны с этими звуками и эффектами. При создании нового, пустого приложения среда Delphi генерирует файл про- екта, который содержит следующий программный код: begin Application.Initial ize; Application.CreateForm(TForml. Forml); Application.Run: end. Как вы могли заметить, это — стандартный программный код. Объект Application Может создавать формы, устанавливая первую из них как Main Form (главная фор- 0347
348 Глава 8. Архитектура Delphi-приложений ма) (одно из свойств Application), и закрывать все приложение при уничтожении главной формы. Исполнение программы заключается в методе Run, который для обработки сообщений системы внедряет системный цикл. Этот цикл продолжает- ся до тех пор, пока не будет закрыто главное окно приложения (первое созданное окно). ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Как мы видели на примере окна-заставки в главе 7, главная форма не обязательно должна быть первой создаваемой формой, но первой, которая создается вызовом Application.CreateForm. Цикл сообщений Windows, внедренный в метод Run, доставляет системные со- общения соответствующему окну приложения. Цикл сообщения обязательно тре- буется любому Windows-приложению, но его не надо писать в Delphi, поскольку объект Application предоставляет этот цикл по умолчанию. Помимо выполнения основной роли объект Application управляет и другими интересными функциями: О подсказки (рассматривались в конце главы 5); О справочная система, включающая возможность определения типа программы просмотра справки (в данной книге не рассматривается); О активизация, минимизация и восстановление приложения; О обработчик глобальных исключений (см. пример ErrorLog в главе 2); О общие сведения о приложении, включая MainForm, имя исполняемого файла и путь (ExeName), значок (Icon) и заголовок (Title), выводимые в панели задач Windows, а также при сканировании запущенных приложений с помощью кла- виш Alt+Tab. ПРИМЕЧАНИЕ----------------------------------------------------------------- Во избежание разночтений между двумя заголовками можно в ходе разработки изменить имя приложения. В этом случае заголовок главной формы изменится в ходе выполнения, а его можно скопировать в заголовок приложения следующим программным кодом: Application.Title := Forml.Caption. В большинстве приложений вам не стоит беспокоиться об окне приложения, лишь об установке его заголовка, определении значка и обработки некоторых со- бытий. Однако можно выполнить ряд простых операций. Установка свойства ShowMainForm в False в исходном коде проекта определяет, что при запуске главная форма выводиться не будет. Внутри программы для обращения к главной форме можно воспользоваться свойством MainForm объекта Application. Вывод окна приложения Нет лучше доказательства существования окна у объекта Application, чем его вы- вод на экран (с помощью примера ShowApp). Нет необходимости показывать его, необходимо лишь изменить его размер и установить парочку атрибутов окна, та- ких как наличие заголовка и границы. Эти операции можно выполнить с помощью API-функций Windows в отношении окна, указанного свойством Handle объекта Application: procedure TForml.ButtonlC11ck(Sender: TObject): 0348
Объект Application 349 var OldStyle: Integer: begin // добавить в окно приложения границу и заголовок OldStyle := GetWindowLong (Application.Handle, gw1_Sty1e); SetWindowLong (Application.Handle, gwl_Style. OldStyle or ws_TMckFrame or ws_Caption); // установить размер окна приложения SetWindowPos (Application.Handle. 0, 0, 0. 200, 100, swp_NoMove or swp_NoZOrder); end; API-функции GetWindowLong и SetWindowLong обращаются к системной инфор- мации, связанной с окном. В данном случае для чтения и записи стилей окна (гра- ница, заголовок, системное меню, значки границы и т. п.) можно воспользоваться параметром gwl_Style. Этот код получает текущий стиль и добавляет (с помощью выражения or) в форму стандартную границу и заголовок. Конечно же, вам вряд ли придется делать что-либо подобное в своих програм- мах, но знание того, что приложение имеет собственное окно, является важным аспектом понимания стандартной структуры Delphi-приложений и предоставляет возможность его модификации в случае необходимости. Активизация приложений и форм Для того чтобы показать, как осуществляется активизация, я написал простой для понимания пример ActivApp. Этот пример имеет две формы. На каждой из них име- ется компонент Label (LabelForm), используемый для вывода состояния формы. Для представления сведений о состоянии программа использует текст и цвет. Вот об- работчики событий On Activate и On Deactivate первой формы: procedure TForml.FormActivate(Sender: TObject): begin LabelForm.Caption := 'FortnZ Active': Label Form.Col or := clRed: end: procedure TForml.FormDeactivate(Sender: TObject): begin Label Form.Caption := 'Form2 Not Active'; Label Form.Col or := clBtnFace: end; Вторая форма имеет аналогичный компонент Label и аналогичные обработчики. Основная форма также представляет и состояние всего приложения. Для обра- ботки событий On Deactivate и On Activate объекта Application она использует компо- нент Application Events. Эти два обработчика подобны представленным выше. Един- ственное отличие заключается в том, что они изменяют текст и цвет во второй надписи формы, а один из них выдает еще и звуковой сигнал. Если запустить эту программу, то вы увидите, является ли приложение актив- ным, и если «да», то какая из форм является активной. Просматривая результат (рис. 8.1) и слушая звуковой сигнал, можно понять, как Delphi запускает каждое из событий активизации. Запустите программу и поэкспериментируйте с ней для того, чтобы полностью разобраться в ее работе. Позже мы рассмотрим и другие события, связанные с активизацией форм. 0349
350 Глава 8. Архитектура Delphi-приложений Рис. 8.1. Пример ActivApp показывает, является ли приложение активным и какая из форм приложения активна Отслеживание форм с объектом Screen Мы уже рассмотрели несколько свойств и событий объекта Application. Интерес- ные глобальные сведения о приложении также доступны через объект Screen, ко- торый основан на классе TScreen. Этот объект содержит информацию о системном дисплее (размер экрана и шрифты), а также о текущем наборе форм запущенного приложения. Например, размер экрана и список шрифтов можно увидеть, написав: Label 1 Caption := IntToStr (Screen.Width) + 'x' + IntToStr (Screen.Height): ListBoxl.Items := Screen.Fonts; TScreen также позволяет узнать количество и разрешение дисплеев в системе с несколькими мониторами. Но сейчас мы разберемся со списком форм, содержа- щимся в свойстве Forms объекта Screen, «самую верхнюю» форму, на которую ука- зывает свойство ActiveForm, и связанное с ним событие OnActiveFormChange. Обратите внимание, что формы, на которые ссылается объект Screen, — это формы при- ложения, а не системы. Все перечисленные особенности продемонстрированы в примере Screen, обслу- живающем список текущих форм. Этот список обновляется каждый раз, когда со- здается новая форма, удаляется существующая форма или изменяется активная форма. Для работы примера необходимо с помощью кнопки New создавать вторич- ные формы: procedure TMainForm.NewButtonClick(Sender: TObject): var NewForm: TSecondForm; begin // создать новую форму, установить ее заголовок и запустить ее NewForm = TSecondForm.Create (Self): Inc (nForms): NewForm.Caption := 'Second ' + IntToStr (nForms): NewForm Show: end; Обратите внимание, что необходимо отключить автоматическое создание вто- ричной формы (с помощь страницы Forms (Формы) диалогового окна Project Options (Параметры проекта)). Одним из ключевых разделов программы является обра- ботчик события OnCreate формы, заполняющий список при первом включении и подключающийся к обработчику события OnActiveFormChange: procedure TMainForm.FormCreate(Sender- TObject): begin FillFormsList (Self): 0350
Объект Application 351 // установить счетчик вторичных форм в О nForms := 0; // настроить обработчик события на объект screen Screen.OnActiveFormChange := Fi11FormsList; end; Программный код, используемый для заполнения списка Forms, размещается во второй процедуре (FillFormsList), которая также устанавливается как обработ- чик события OnActiveFormChange объекта Screen: procedure TMainForm.Fill FormsLi st (Sender: TObject); var I: Integer; begin // пропустить код этапа уничтожения phase if Assigned (FormsListBox) then begin FormsLabel.Caption ;= ’Forms: ' + IntToStr (Screen.FormCount); FormsListBox.Clear; // записать имя класса и заголовок формы в элемент list box for I := 0 to Screen.FormCount - 1 do FormsListBox.Items Add (Screen.FormsEI].ClassName Screen.Forms!I].Caption); ActiveLabel.Caption = 'Active Form : ' + Screen.ActiveForm.Caption; end; end; ВНИМАНИЕ ---------------------------------------------------------------------------------- Очень важно не допустить выполнения этого программного кода при уничтоженной основной форме. В качестве альтернативной проверки: установлен ли список в nil, необходимо убедиться в отсут- ствии значения csDestroying в Componentstate формы. Другой подход заключается в удалении об- работчика события OnActiveFormChange перед выходом из приложения, то есть следует обработать событие OnClose главной формы и присвоить Screen.OnActiveFormChange значение nil. Метод FillFormsList заполняет список и устанавливает значение двух надписей, расположенных над ним, выводящих количество форм и наименование активной формы. При щелчке на кнопке New программа создает экземпляр вторичной фор- мы, назначает ей новый заголовок и выводит ее. Список Forms обновляется автома- тически, поскольку имеется обработчик события OnActiveFormChange. На рис. 8.2 представлен результат работы программы после создания нескольких вторичных окон. Рис. 8.2. Результат работы примера Screen с несколькими вторичными формами 0351
352 Глава 8. Архитектура Delphi-приложений Каждая вторичная форма имеет кнопку Close, при щелчке на которой эта форма удаляется. Программа обрабатывает событие OnClose, устанавливая параметр Action в caFree, поэтому при закрытии форма уничтожается. Этот программный код за- крывает форму, но не обновляет список окон. Система сначала перемещает фокус на другое окно, запускает событие, обновляющее список, и уничтожает старую форму только после выполнения этой операции. Сначала я хотел проводить обновление списка окон путем введения задержки отправки определяемого пользователем Windows-сообщения. Поскольку отправ- ленное сообщение помещается в очередь и обрабатывается не сразу, при его отправке в последний момент жизни вторичной формы главная форма будет по- лучать его после уничтожения другой формы. Уловка заключается в отправке со- общения в обработчике события On Destroy вторичной формы. Для выполнения этого необходимо ссылаться на объект Main Form посредством добавления инструкции uses в раздел реализации данного модуля. Я отправлял сообщение wm_User, которое обрабатывается специальным методом message главной формы: public procedure ChiIdClosed (var Message TMessage); message wmJJser; procedure TMainForm.ChildClosed (var Message TMessage). begin FillFormsList (Self), end. Проблема заключается в том, что если вы закроете главное окно до закрытия вторичных форм, главная форма останется, но ее программный код выполняться не будет. Во избежание иной системной ошибки (Access Violation Fault) необхо- димо посылать сообщение только тогда, когда главная форма не закрыта. Но как определить, закрыта ли эта форма? Один из способов заключается в добавлении в класс TMainForm флага и изменении его значения при закрытии главной формы, что позволит проверять этот флаг из программного кода вторичного окна. А вот второе решение — как хорошо, что VCL предоставляет подобную воз- можность с помощью упоминаемого ранее свойства Componentstate и его флага csDestroying. С его использованием можно написать следующий код: procedure TSecondForm FormDestroy(Sender TObject). begin if not (csDestroying in MainForm Componentstate) then PostMessage (MainForm Handle. wmJJser. 0. 0), end; При использовании такого кода список всегда будет показывать верное коли- чество форм приложения. После реализации этого подхода я обнаружил альтернативный и более подхо- дящий для Delphi вариант. «Обходной маневр» основан на том, что каждый раз, когда уничтожается компонент, он сообщает об этом событии своему владельцу, вызывая метод Notification, определенный в классе TComponent. Поскольку вторич- ные формы принадлежат главной форме (это определяется в программном коде метода NewButtonClick), вы можете перекрыть этот метод и упростить программ- ный код (см. папку Screen2): procedure TMainForm Notification(AComponent TComponent: Operation TOperation), begin inherited Notification(AComponent. Operation): 0352
От событий к потокам 353 if (Operation = opRemove) and Showing and (AComponent is TForm) then FillFormsList. end: СОВЕТ----------------------------------------------------------------------------------------- Если бы вторичные формы не принадлежали главной форме, то можно было бы воспользоваться методом FreeNotification для того, чтобы вторичные формы уведомляли главную форму о своем уничтожении. FreeNotification получает в качестве параметра компонент для того, чтобы уведо- мить, когда текущий компонент будет уничтожен. Эффект заключается в вызове метода Notification, который поступает от компонента, отличного от «владеемых» компонентов. FreeNotification обычно используется разработчиками компонентов для обеспечения безопасной связи между компонента- ми на различных формах или модулях данных. Последняя функциональная возможность, которую я добавил в обе версии про- граммы, довольно проста: при щелчке на пункте списка с помощью метода Bring- ToFro nt активизируется соответствующая форма. Если вы щелкнете на списке, когда главная форма неактивна, сначала активизируется главная форма, и список пере- упорядочивается, что в конечном счете может привести к выбору совсем не той формы, которую вы ожидали. Если поэкспериментировать с программой, то вы вскоре поймете, что я имел в виду. Достоинством этой программы является при- мер того, с чем вы можете столкнуться при динамическом обновлении информа- ции и позволить пользователю работать с ней. От событий к потокам Для того чтобы понять внутреннюю работу Windows-приложения, давайте затра- тим некоторое время на рассмотрение поддержки многозадачности в этой опера- ционной системе. Вам также необходимо понять роль таймеров (и компонента Timer), фоновой (или idle) обработки, а также роль метода ProcessMessages глобаль- ного объекта Application. Короче говоря, необходимо более глубоко вникнуть в управляемую событиями структуру Windows и ее поддержку многозадачности. Поскольку эта книга посвя- щена Delphi-программированию, я не буду рассматривать этот вопрос во всех тон- костях, а предоставлю обобщенный обзор, в основном предназначенный для тех читателей, которые не имеют опыта программирования API Windows. Программирование на основе событий Базовая идея программирования на основе событий заключается в том, что поток Управления приложения определяют особые события. Большую часть времени программа проводит в ожидании этих событий и предоставляет код отклика на пих. Например, когда пользователь щелкает одну из кнопок мыши, происходит событие. Сообщение, описывающее это событие, направляется в окно, которое в Данный момент находится под указателем мыши. Программный код данного окна, отвечающий на события, получает сообщение и обрабатывает его, реагируя соот- ветствующим образом. После окончания отклика на событие он возвращается в режим ожидания или «простоя» (idle). Как ясно из этого объяснения, события происходят последовательно; каждое событие обрабатывается только после того, как завершена обработка предыдуще- 0353
354 Глава 8. Архитектура Delphi-приложений го. Когда программа выполняет код обработки события (то есть когда оно не ожи- дает события) другие события этого приложения вынуждены ожидать в очереди сообщений, зарезервированной для данного приложения (если только приложе- ние не использует многопоточную обработку). Как только приложение откликну- лось на сообщение и возвратилось в состояние ожидания, оно становится после- дним в списке программ, ожидающих обработки дополнительных сообщений. В каждой версии Win32 (9х, NT, Me и 2000) через фиксированный интервал вре- мени система прерывает текущее приложение и немедленно предоставляет управ- ление следующей программе данного списка. Первая программа будет продолже- на после того, как будет включено каждое из приложений. Этот процесс называется упреждающая многозадачность (preemptive multitasking). Приложение, выполняющее в обработчиках событий длительные операции, не обеспечивает верную работу системы (поскольку другие процессы имеют свой временной интервал (квант времени) центрального процессора), обычно оно даже не в состоянии верно восстановить свое окно — очень неприятный эффект. Если вы не сталкивались с этой проблемой, попробуйте: напишите цикл, требующий больших временных затрат, выполняемый при щелчке кнопки, и попытайтесь пе- реместить форму или поместить на нее другое окно. Эффект будет действительно раздражительный. А теперь попробуйте добавить в цикл Application. ProcessMessages; вы увидите, что выполнение операций стало еще более медленным, но форма об- новляется очень быстро. В качестве примера использования Application.ProcessMessages внутри трудоем- кого цикла (и отсутствии этого вызова) вы можете обратиться к примеру BackTask. Вот программный код, использующий этот подход (не обращайте внимания на саму технику вычисления суммы данного набора простых чисел): procedure TForml Button2Click(Sender TObject). var I Tot Integer. begin Tot =0. for I =1 to Max do begin if IsPrime (I) then Tot = Tot + I. ProgressBarl Position = I * 100 div Max. Application ProcessMessages. end ShowMessage (IntToStr (Tot)) end ПРИМЕЧАНИЕ----------------------------------------------------------------------------- Существует и вторая альтернатива вызову ProcessMessages: функция HandleMessage. Имеется два отличия: HandleMessage при каждом вызове обрабатывает одно сообщение, в то время как Pro- cessMessages продолжает обрабатывать сообщения из очереди; HandleMessage также активизирует обработку во время простоя, например в ходе вызовов обновления действий. Если приложение ответило на событие и ожидает своей очереди обработки со- общения, оно не имеет возможности получить управление до тех пор, пока не по- лучит другое сообщение (если только оно не использует многопоточность). По этой причине используется таймер (Timer): системный компонент, который всякий раз посылает сообщение вашему приложению через определенный временной интер- 0354
От событий к потокам 355 вал. Использование таймера — единственный способ заставить приложение время от времени автоматически выполнять какие-либо операции, даже при отсутствии пользователя или если он не использует эту программу (то есть не порождает ни- каких событий). Одно последнее примечание: при рассмотрении событий учтите, что входные сообщения (генерируемые мышью или клавиатурой) составляют лишь малый про- цент потока сообщений Windows-приложения. Большинство сообщений — это внутренние системные сообщения или сообщения, которыми обмениваются раз- личные окна или элементы управления. Даже знакомые входные операции, например, щелчок на кнопке мыши, может привести к огромному количеству со- общений, преобладающее большинство которых будут составлять внутренние Win- dows-сообщения. Это можно проверить с помощью утилиты WinSight, поставляе- мой вместе с Delphi. Выберите в ней просмотр Message Trace и отметьте сообщения для всех окон. Щелкните на кнопке Start, после чего выполните ряд простых дей- ствий с мышью. За несколько секунд вы увидите сотни сообщений. Доставка Windows-сообщений Перед рассмотрением нескорых реальных примеров давайте рассмотрим еще один ключевой элемент обработки сообщений. Windows имеет два варианта отправки сообщения в окно: API-функция PostMessage Помещает сообщение в очередь сообщений приложения. Сообщение будет обра- ботано только тогда, когда приложение будет иметь возможность обратиться к оче- реди сообщений (то есть когда получит от системы право управления), и после того как будут обработаны ранее поступившие сообщения. Это называется асинх- ронным вызовом, поскольку вы не знаете, когда сообщение будет получено. API-функция SendMessage Сразу же выполняет код обработки сообщения. SendMessage минует очередь сооб- щений приложения и отправляет сообщение непосредственно в целевое окно или элемент управления. Это называется синхронным вызовом. Эта функция даже имеет возвращаемое значение, которое поступает обратно в код обработки сооб- щения. Вызов SendMessage ничем не отличается от вызова любого метода или фун- кции программы. Разница между этими двумя вариантами отправки сообщений аналогична от- правке сообщения письмом (которое может попасть к адресату через неопределен- ный срок) и отправкой факса (который немедленно поступает получателю). Хотя редко возникает необходимость использования этих низкоуровневых вызовов API в Delphi, это описание поможет вам определиться, какой из них необходим. Фоновая обработка и многозадачность Предположим, что вам необходимо реализовать трудоемкий алгоритм. Если реали- зовать этот алгоритм в качестве реакции на определенное событие, ваше приложение полностью остановится на время выполнения этого алгоритма. Для того чтобы пользователь представлял, что обработка продолжается, можно изменить указа- тель мыши на «песочные часы» или вывести индикатор выполнения (progress bar), Но это нельзя назвать дружественным пользователю решением. Win32 позволяет 0355
356 Глава 8. Архитектура Delphi-приложений другим программам продолжать свое выполнение, но эта программа покажется «замороженной»; она не будет обновлять даже свой собственный пользовательс- кий интерфейс, если потребуется перерисовка. В ходе реализации алгоритма при- ложение будет не способно получать и обрабатывать любые иные сообщения, вклю- чая сообщения раскраски (paint). Простейшим решением этой проблемы является вызов рассмотренных ранее методов ProcessMessages и HandleMessage. Проблема этого подхода заключается в том, что пользователь может повторно щелкнуть на кнопке или повторно нажать соче- тание клавиш, запускающее алгоритм. Для исключения этой возможности необ- ходимо отключить соответствующую кнопку и команду и вывести курсор «песоч- ные часы» (который технически не предотвращает событие «щелчок мыши», но предполагает, что пользователь перед выполнением какой-либо иной операции будет ожидать). Для обеспечения некоторых низкоприоритетных фоновых вычислений можно разбить алгоритм на более малые части и выполнять каждую из них по очереди, позволяя приложению отвечать на сообщения между обработкой этих частей. Для разбивки алгоритма по временным интервалам можно использовать таймер. Хотя таймеры могут использоваться для реализации некоторых форм фоновой обра- ботки, это далеко не самое удачное решение. Лучше каждый шаг программы вы- полнять тогда, когда объект Application получает событие Onldle. Разница между вызовом метода ProcessMessages и использованием события Onldle заключается в том, что вызов метода ProcessMessages предоставляет коду больше процессорного времени. Вызов этого метода позволяет программе в ходе выпол- нения длительной операции осуществлять и другие действия; использование Onldle позволяет программе выполнять фоновые задачи, когда нет запросов от поль- зователя. Многопоточность Delphi Если требуется выполнить операции в фоновом режиме или любую обработку, не связанную напрямую с пользовательским интерфейсом, можно воспользоваться технически более корректным подходом: внутри основного процесса порождать подпроцесс в виде отдельного потока выполнения. Программирование многопо- точности может показаться неясным вопросом. Но на самом деле это не так сложно, даже если вы подходите к нему с осторожностью. Необходимо знать по меньшей мере основы многопоточности, поскольку в мире сокетов и Интернет-программи- рования практически ничего невозможно сделать без потоков. RTL-библиотека Delphi предоставляет класс TThread, позволяющий создавать потоки и управлять ими. Этот класс никогда не используется непосредственно, поскольку он является абстрактным классом, то есть классом с виртуальными аб- страктными методами. Для использования потоков всегда создаются подклассы класса TThread и применяются возможности базового класса. Класс TThread имеет конструктор с единственным параметром (CreateSuspended), определяющий, будет ли поток запускаться немедленно либо будет отложен на потом. Если объект-поток стартует непосредственно либо возобновляется, он за- пускает метод Execute и ожидает его выполнения. Класс предоставляет защищен- ный интерфейс, включающий два ключевых метода для подклассов-потоков: procedure Execute; virtual: abstract; procedure Synchronize(Method: TThreadMethod); 0356
От событий к потокам 357 Метод Execute, объявленный в качестве виртуальной абстрактной процедуры, должен быть переопределен в каждом поточном классе. Он содержит основной код потока — код, который при использовании системной функции обычно поме- щается в поточную функцию (threadfunction). Метод Synchronize предназначен для предотвращения параллельного доступа к VCL-компонентам. VCL-код выполняется внутри основного потока программы, поэтому во избежание проблемы повторного входа (ошибки от повторного входа в функцию до завершения выполнения предыдущего вызова) и параллельного до- ступа к совместно используемым ресурсам необходимо синхронизировать доступ к VCL. Единственным параметром метода Synchronize является метод, не воспри- нимающий никаких параметров; обычно метод того же поточного класса. Поскольку этому методу невозможно передать параметры, он является общим для хранения некоторых значений в данных объекта-потока в методе Execute и использования этих данных в синхронизированных методах. СОВЕТ---------------------------------------------------------------------------- В Delphi 7 имеется две новые версии метода Synchronize, позволяющие синхронизировать метод основного потока без вызова его из объекта-потока. Новые перегруженные методы Synchronize и StaticSynchronize являются методами класса TThread и в качестве параметра требуют поток. Еще один способ избежать конфликта заключается в использовании техноло- гий синхронизации, предлагаемых операционной системой. Модуль SyncObjs оп- ределяет ряд VCL-классов, часть из которых является объектами синхронизации низкого уровня, например событиями (в классе TEvent и TSingieEvent) и критиче- скими разделами (в классе TCriticalSection). (Не путайте события синхронизации с событиями Delphi. Эти две концепции совершенно не связаны.) Пример организации поточной обработки В качестве примера потока мы снова можем обратиться к программе BackTask. Этот пример порождает вторичный поток для вычисления суммы простых чисел. Класс- поток имеет обычный метод Execute, начальное значение, передаваемое в public- свойство Мах, и два внутренних значения (FTotaL и FPosition), используемых для синхронизации выходных значений в методах ShowTotai и Update Progress. Ниже пред- ставлено полное объявление класса для пользовательского объекта-потока: type TPrlmeAdder = class(TThread) private FMax. FTotal, FPosition: Integer; protected procedure Execute; override: procedure ShowTotal: procedure UpdateProgress; public property Max; Integer read FMax write FMax; end; Метод Execute очень похож на программный код, относящийся к кнопкам в при- мере BackTask, рассмотренном ранее. Единственное отличие содержится в заклю- чительном вызове Synchronize: Procedure TPrimeAdder.Execute; var 0357
358 Глава 8. Архитектура Delphi-приложений I. Tot Integer. begin Tot =0. for I =1 to FMax do begin if IsPrime (I) then Tot = Tot + I, if I mod (fMax div 100) = 0 then begin FPosition = I * 100 div fMax; Synchrom ze (UpdateProgress), end, FTotal -Tot. Synchronize(ShowTotal). end; procedure TPnmeAdder ShowTotal, begin ShowMessage ('Thread ' + IntToStr (FTotal)). end; procedure TPnmeAdder UpdateProgress. begin Forml.ProgressBarl Position = fPosition, end: Объект-поток создается при щелчке на кнопке и автоматически уничтожается, как только завершается выполнение метода Execute: procedure TForml Button3Click(Sender TObject). var AdderThread TPnmeAdder begin AdderThread = TPnmeAdder Create (True). AdderThread Max = Max AdderThread FreeOnTerminate = True. AdderThread Resume end: Вместо установки максимального числа с использованием свойства было бы лучше передавать это значение в качестве дополнительного параметра пользова- тельского конструктора; я не стал делать этого, постаравшись сконцентрироваться на примере использования потока. Дополнительные примеры использования по- токов вы увидите в других главах книги, особенно в главе 19, в которой рассматри- вается использование сокетов. Проверка существования экземпляра приложения Одна из форм многозадачности заключается в исполнении двух и более экземпля- ров одного и того же приложения. Любое приложение обычно может запускаться пользователем несколько раз, и для того чтобы отключить это стандартное поведе- ние и обеспечить существование лишь одного экземпляра, возникает необходи- мость проверить, что один экземпляр уже выполняется. В этом разделе представ- 0358
Проверка существования экземпляра приложения 359 лено несколько вариантов реализации такой проверки, что позволяет рассмотреть несколько интересных технологий Windows-программирования. Поиск копии главного окна Для того чтобы найти копию главного окна существующего экземпляра, исполь- зуется API-функция FindWindow, в которую передается имя класса-окна (имя, ис- пользуемое для регистрации в системе типа окна формы, или WNDCLASS) и заголо- вок окна, которое вы ищете. В Delphi-приложении имя класса-окна WNDCLASS точно такое же, как и имя класса формы на языке Object Pascal (например, TForml). Ре- зультатом функции FindWindow будет либо дескриптор окна, либо ноль (если соот- ветствующее окно не найдено). Основной программный код Delphi-приложения должен быть написан таким образом, чтобы он выполнялся, только если результатом функции FindWindow яв- ляется ноль: var Hwnd THandle. begin Hwnd = FindWindow {'TForml'. ml). if Hwnd = 0 then begin Application Initialize, Application CreateForm(TForml. Forml). Application Run. end else SetForegroundWindow (Hwnd) end Для активизации окна предыдущего экземпляра приложения можно использо- вать функцию SetForegroundWindow, которая работает для окон, которыми владеют другие процессы. Этот вызов имеет эффект только в том случае, если окно, переда- ваемое в качестве параметра, не свернуто. Когда главная форма Delphi-приложе- ния свернута (minimize), оно является скрытым. По этой причине код активиза- ции не имеет эффекта. К сожалению, если вы запускаете программу, использующую вызов только что показанной функции FindWindow из IDE Delphi, окно с этим заголовком и класс могут уже существовать: форма времени разработки. Следовательно, программа не сможет запуститься даже один раз. Однако она запустится, если вы закроете форму и соответствующий ей файл исходного программного кода (закрытие фор- мы лишь прячет окно) или если вы закроете проект и запустите программу с помощью Проводника Windows. Также учтите, что при наличии формы с именем Forml, скорее всего, она не будет работать как ожидалось, поскольку множество Delphi-приложений могут иметь форму с таким же именем. Это будет исправлено в последующих версиях программного кода. Использование мьютексов Полностью отличный подход заключается в использовании мьютекса (mutex), то есть объекта взаимоисключения. Это — типичный для Win32 подход, обычно ис- пользуемый для синхронизации потоков. Здесь мы будем использовать мьютекс 0359
360 Глава 8. Архитектура Delphi-приложений для синхронизации двух различных приложений или, если быть более точным, двух экземпляров одного и того же приложения. После того как приложение создало мьютекс с данным именем, с помощью API- функции Windows WaitForSingLeObject оно может проверить, не принадлежит ли этот объект другому приложению. Если мьютекс не имеет владельца, то приложение, вызвавшее эту функцию, становится его владельцем. Если мьютекс уже кому-либо принадлежит, приложение ожидает истечения тайм-аута (второй параметр функ- ции). После чего возвращает код ошибки. Для реализации этой технологии можно использовать следующий программ- ный код проекта: var hMutex THandle. begin HMutex = CreateMutex (ml. False. ' OneCopyMutex ’). if WaitForSingleObject (hMutex 0) <> wait_TimeOut then begin Application Initialize. Application CreateForm(TForml. Forml) Application Run. end; end. Если запустить этот пример дважды, то вы увидите, что он создает новую, вре- менную копию приложения (в панели задач появляется значок), а затем, по исте- чении тайм-аута, уничтожает его. Такой подход непременно более надежен, чем предыдущий, но у него имеется и недостаток: как запустить существующий экзем- пляр приложения? Вам по-прежнему необходимо найти его форму, для чего мож- но использовать более удачную методику. Поиск в списке окон При поиске определенного главного окна системы вы можете воспользоваться API- функцией EnumWindows. Функция перечисления в Windows является странной, поскольку в качестве параметра она обычно требует другую функцию. Функции перечисления (подсчета) требуют указатель на функцию (зачастую описываемую как функция обратного вызова). Эта функция применяется в отношении каждого элемента списка (в данном случае — списка главных окон), до тех пор, пока не до- стигнет его конца или вернет False. Вот функция перечисления из примера ОпеСору: function EnumWndProc (hwnd THandle. Param Cardinal) Bool stdcall; var ClassName, WinModuleName string. Wininstance THandle begin Result = True. SetLength (ClassName 100). GetClassName (hwnd. PChar (ClassName) Length (ClassName)): ClassName = PChar (ClassName). if ClassName = TForml ClassName then begin // получить имя модуля целевого окна SetLength (WinModuleName. 200). Wininstance = GetWindowLong (hwnd. GWL_HINSTANCE). GetModuleFl 1 eName (Wininstance. 0360
Проверка существования экземпляра приложения 361 PChar (WinModuleName). Length (WinModuleName)), WinModuleName = PChar(WinModuleName). // корректировать длину // сравнить имена модулей if WinModuleName = ModuleName then begin FoundWnd - Hwnd, Result = False. // остановить подсчет end. end. end; Данная функция, вызываемая для каждого «недочернего» окна системы, про- веряет имя класса каждого окна в поиске имени класса TForml. Когда она находит окно с такой строкой в имени класса, для извлечения имени исполняемого файла приложения, владеющего соответствующей формой, она использует метод Get- ModuleFilename. Если имя модуля соответствует текущей программе (которое было извлечено предыдущим программным кодом), вы можете быть уверены, что на- шли предыдущий экземпляр той же программы. Вот как можно вызывать функ- цию перечисления: var FoundWnd THandle. ModuleName string. begin if WaitForSingleObject (hMutex. 0) <> wait_TimeOut then else begin // получить имя текущего модуля SetLength (ModuleName 200). GetModuleFileName (HInstance. PChar (ModuleName). Length (ModuleName)). ModuleName = PChar (ModuleName). // скорректировать длину // найти окно предыдущего экземпляра EnumWindows (@EnumWndProc 0) Обработка определяемых пользователем Windows-сообщен и й Я уже упоминал, что вызов SetForegroundWindow не работает, если главная форма программы свернута. Теперь мы можем решить эту проблему. Мы можем запро- сить форму другого приложения (в нашем случае — предыдущего экземпляра той же самой программы), восстановив ее главную форму посредством отправки поль- зовательского Windows-сообщения. После этого можно проверить, была ли свер- иута форма, и отправить новое сообщение старому окну. Вот программный код (в примере ОпеСору он следует за последним фрагментом, представленным в пре- дыдущем разделе): FoundWnd о о then begin // временно показать окно if not IsWindowVisiЫe (FoundWnd) then PostMessage (FoundWnd wm_App 0 0). SetForegroundWindow (FoundWnd). end, 0361
362 Глава 8. Архитектура Delphi-приложений API-функция PostMessage отправляет сообщение в очередь сообщений прило- жения, владеющего окном-адресатом. Для обработки этого сообщения в программ- ный код формы можно добавить специальную функцию: public procedure WMApp (var msg: TMessage): message wm_ApP: procedure TForml.WMApp (var msg: TMessage); begin Application.Restore: end: СОВЕТ-------------------------------------------------------------------------------- Программа использует сообщение wm_App, а не wmJJser. Последнее сообщение используется не- которыми системными окнами, поэтому нет гарантии, что это сообщение не пошлют система или другие приложения. Вот почему компания Microsoft ввела wm_User для сообщений, которые пред- назначены для взаимодействия приложений. Создание MDI-приложений Многодокументный интерфейс (Multiple Document Interface, MDI) — это извест- ный подход к структуре приложения. MDI-приложение является совокупностью нескольких форм, открываемых внутри одного окна главной формы. При исполь- зовании Блокнота Windows можно открыть только один текстовой документ, поскольку Блокнот не является MDI-приложением. Но в любимом текстовом про- цессоре вы, вероятно, имеете возможность открыть несколько различных докумен- тов, каждый в своем дочернем окне, поскольку этот текстовой процессор является MDI-приложением. Все эти окна обычно содержатся в окне фрейма или окне при- ложения. СОВЕТ-------------------------------------------------- Важно, что компания Microsoft отходит от MDI-модели, на которой делалась ставка со времен Win- dows 3. Даже последние версии пакета Office стремятся использовать специальное главное окно для каждого документа: классический SDI-подход (Интерфейс единственного документа) (Single Document Interface). Тем не менее MDI не отмирает и иногда может быть полезной архитектурой, что можно отметить на примерах браузеров, подобных Opera и Mozilla. MDI в Windows: технический обзор MDI-структура автоматически предоставляет программистам несколько преиму- ществ. Например, Windows обрабатывает список дочерних окон в одном из меню MDI-приложения, а специальные методы Delphi активизируют соответствие фун- кционального назначения MDI, упорядочивая дочерние окна каскадно или в виде черепицы. Вот техническая структура MDI-приложения в Windows: О главное окно приложения действует как фрейм или контейнер; О специальное окно, известное как MDI-клиент, покрывает всю клиентскую об- ласть окна фрейма. MDI-клиент является одним из предопределенных компо- нентов Windows (так же, как строка редактирования или список). Окно MDI- 0362
Фрейм и дочерние окна в Delphi 363 клиента не имеет каких-либо интерфейсных элементов, но всегда является ви- димым. Вы можете изменить стандартный системный цвет рабочей области MDI (называемой фоном приложения) на странице Appearance (Оформление) диа- логового окна Display Properties (Свойства экрана) Windows; О множество дочерних окон одного или нескольких видов. Эти окна помещаются не во фрейм окна; они определены как дочерние по отношению к окну MDI- клиента, которое в свою очередь является дочерним по отношению к окну фрейма. Фрейм и дочерние окна в Delphi Среда Delphi значительно облегчает разработку MDI-приложений, даже без ис- пользования шаблона MDI Application, доступного в Delphi (см. страницу Applications в диалоговом окне, открываемом командой File ► New ► Other). Необходимо лишь создать по меньшей мере две формы и одной из них установить свойство FormStyle в fsMDIForm, а другой то же свойство — в fsMDlChild. Установите эти два свойства в простой программе и запустите ее, и вы увидите две вложенные формы стан- дартного MDI-стиля. Однако обычно дочерняя форма не создается при запуске, а вы должны обеспе- чить способ создания одного и более дочерних окон. Этого можно добиться добав- лением меню с пунктом New (Новый) и написанием следующего программного кода: var ChlldForm: TChildForm: begin ChlldForm :- TChildForm.Create (Application): ChlldForm.Show; Еще одной важной особенностью является добавление раскрывающегося меню окон, которое используется как значение свойства WindowMenu формы. Это меню будет автоматически перечислять все доступные дочерние окна. Конечно же, вы можете выбрать любое другое имя этого пункта меню, но стандартным является Window (Окно). Для правильной работы программы к каждому имени дочернего окна можно при его создании добавлять номер: procedure TMainForm.NewlCl1ck(Sender: TObject): var ChlldForm: TChildForm: begin WindowMenu := Windowl: Inc (Counter): ChlldForm := TChildForm.Create (Self): ChlldForm.Caption := ChlldForm.Caption + ' ’ + IntToStr (Counter); ChlldForm.Show: end; Можно также открыть дочерние окна, свернуть или восстановить каждое из них, закрыть и использовать раскрывающееся меню Window для перехода от одно- го к другому. А теперь представьте, что вы хотите закрыть часть окон, чтобы ис- 0363
364 Глава 8. Архитектура Delphi-приложений ключить загромождение клиентской области программы. Щелкните на квадрати- ках-значках Close (Закрыть) дочерних окон и они лишь свернутся! В чем дело? Не забывайте, что когда окно закрывается, оно обычно прячется от просмотра. В Delphi закрытая форма остается существовать, хотя и невидимо. В случае дочерних окон их сокрытие не работает, поскольку MDI-меню Window и список окон по-прежне- му будут содержать все существующие дочерние окна, даже если они скрыты. По этой причине при попытке закрыть дочерние окна MDI Delphi лишь сворачивает их. Для решения этой проблемы необходимо при закрытии удалить дочерние окна, установив ссылочный параметр Action события OnClose в caFree. Построение полного меню Window Первая задача заключается в определении наилучшей структуры меню данного примера. Обычное раскрывающееся меню Window имеет, по меньшей мере, три пун- кта: Cascade (Уложить каскадом), Tile (Уложить черепицей) и Arrange Icons (Упо- рядочить все). Для обработки этих команд меню можно воспользоваться рядом предопределенных методов класса TForm, которые могут использоваться только в MDI-фреймах. Метод Cascade Упорядочивает дочерние MDI-окна каскадом. Окна перекрывают друг друга. Пред- ставленные значками дочерние окна проще упорядочивать (см. Arrangelcons). Метод Tile Укладывает «черепицей» открытые MDI-окна; дочерние формы упорядочивают- ся так, что они не перекрываются. По умолчанию выполняется «укладывание» по горизонтали, хотя если имеется несколько дочерних окон, то их можно упорядо- чить в несколько столбцов. Поведение по умолчанию может быть изменено с по- мощью свойства TileMode (которое может принимать значения tbHorizontal или tb Vertical). Процедура Arrangelcons Выполняет упорядочивание всех окон, представленных значками. Открытые фор- мы не перемещаются. Лучше вместо вызова этих методов поместить на форму ActionList и добавить в него последовательность предопределенных MDI-действий. Связанными клас- сами являются TWindowArrange, TWindowCascade, TWindowClose, TWindowTileHorizontal, TwindowTileVertical и TwindowMinimizeAll. Подключенные пункты меню будут выпол- нять соответствующие действия и становиться недоступными, если дочерних окон нет. Пример MdiDemo, который мы рассмотрим далее, помимо прочего демонстри- рует использование MDI-действий. Помимо перечисленных в Delphi существует несколько других тесно связан- ных с MDI методов и свойств. Свойство ActiveM DIChild Свойство времени выполнения, только для чтения. Относится к форме MDI-фрей- ма, который содержит активное дочернее окно. Пользователь сможет изменить это значение, выбрав новое дочернее окно, а программа может изменить его с помощью процедур Next и Previous, которые активизируют предыдущее или следующее от- носительно текущего окно. 0364
фрейм и дочерние окна в Delphi 365 Свойство ClientHandle Содержит Windows-дескриптор окна MDI-клиента, которое охватывает клиент- скую область главной формы. Свойство MDIChildren Массив дочерних окон, предназначенный для использования совместно со свой- ством М DIChi Id Со u nt для циклического перехода по всем дочерним окнам. Это свой- ство может быть полезным для поиска определенного дочернего окна или для ма- нипулирования с каждым из них. Обратите внимание, что внутренний порядок дочерних окон противоположен порядку активизации. Это значит, что последнее выбранное дочернее окно является активным (первое во внутреннем списке), предпоследнее окно во внутреннем пред- ставлении является вторым, а первое дочернее окно — последним. Этот порядок оп- ределяет, как окна будут упорядочиваться на экране. Первое окно в списке будет рас- положено над всеми остальными, в то время как последнее — ниже всех и, вероятно, будет скрытым. Можно представить ось Z, идущую от экрана в вашу сторону. Актив- ное окно имеет большее значение Z-координаты и, следовательно, закрывает другие окна. По этой причине схема упорядочивания Windows известна как z-порядок. СОВЕТ------------------------------------------------------------------- Начиная с Delphi 7 меню Window (Окно) может использоваться совместно с элементами управления ActionManager и ActionMainMenuBar, содержащими это меню. Эти элементы управления имеют спе- циальное свойство WindowMenu, в котором должно быть указано меню, содержащее список дочер- них MDI-окон. Пример MdiDemo Для демонстрации большинства особенностей простого MDI-приложения я раз- работал пример MdiDemo. Он представляет собой полнофункциональный тексто- вой редактор, поскольку каждое дочернее окно содержит компонент МЕМО, а сам редактор может открывать и сохранять текстовые файлы. Дочерняя форма имеет свойство Modified, указывающее, изменялся ли текст в МЕМО (оно устанавливается в True обработчиком события OnChange этого элемента). Modified устанавливается в False пользовательскими методами Save и Load и проверяется при закрытии фор- мы (предлагая пользователю сохранить файл). Главная форма программы основана на компоненте ActionList. Его действия до- ступны через ряд пунктов меню и панель инструментов (рис. 8.3) Подробности настройки ActionList можно посмотреть в исходном программном коде этого при- мера, а я остановлюсь лишь на действиях пользователя. Самые простые действия связаны с объектом Action Font, который имеет оба об- работчика: как OnExecute (который использует компонент FontDialog), так и OnUpdate (который отключает действие, а, следовательно, и связанный пункт меню и кноп- ку панели инструментов при отсутствии дочерних форм): procedure TMainForm.ActionFontExecuteCSender: TObject); begin if FontDialogl.Execute then (ActiveMDIChild as TChildForm).Memol.Font := FontDialogl.Font: end; 0365
366 Глава 8. Архитектура Delphi-приложений procedure TMainForm ActionFontUpdate(Sender: TObject); begin ActionFont.Enabled MDIChi1dCount > 0; end; «•Mdiotmanuindtieotet e жМ to t mtru «nd • iwfeef •cIM to < menu «nd * 1оЫЬ« j »ot«dio«m«nw«nd«iodb«f ’• •olod to « mnu «nd «iodb« J •otod to • menu «nd «I oofe* itelod to « m«nw «nd • loeb«t wetodle«m«nw«nd«loolb«f L wclod io «menu «nd «looteM Рис. 8.3. Программа MdIDemo использует последовательность предопределенных действий Delphi, подключенных к меню и панели инструментов Действие с именем New создает дочернюю форму и устанавливает имя файла по умолчанию. Действие Орел перед загрузкой файла вызывает метод ActionNew- Excecute: procedure TMainForm.ActionNewExecute(Sender: TObject); var ChlldForm: TChildForm; begin Inc (Counter); ChlldForm := TChildForm.Create (Self). ChlldForm.Caption • = Lowercase (ExtractFilePath (Application.Exename)) + 'text' + IntToStr (Counter) + '.txt', ChlldForm Show, end; procedure TMainForm.ActionOpenExecute(Sender TObject). begin if OpenOialogl Execute then begin ActionNewExecute (Self). (ActiveMOIChild as TChildForm).Load (OpenOialogl.FileName); end. end; Загрузка файла выполняется с помощью метода Load формы. Точно так же ме- тод дочерней формы Save используется для действий Save и Save As. Обратите вни- мание на обработчик Onllpdate действия Save, который разрешает действие только в том случае, если текст в компоненте МЕМО был изменен: 0366
MDI-приложения с различными дочерними окнами 367 procedure TMainForm.ActwnSaveAsExecutetSender: TObject): begin // предложить текущее имя файла SaveDlalogl.FileName :- ActiveMDIChild.Caption: if SaveDialogl.Execute then begin // изменить имя файла и сохранить ActiveMDIChild.Caption :- SaveDialogl.FileName: (ActiveMDIChlId as TChlldForm).Save; end; end; procedure TMainForm,Act1onSaveUpdate(Sender: TObject); begin ActlonSave.Enabled (MDIChildCount > 0) and (ActiveMDIChild as TChlldForm).Modified: end; procedure TMainForm.ActionSaveExecute(Sender' TObject): begin (ActiveMDIChild as TChlldForm).Save. end; MDI-приложения с различными дочерними окнами В сложных MDI-приложениях распространенным является наличие дочерних окон различных типов (то есть основанных на различных формах). Для того чтобы за- острить ваше внимание на некоторых проблемах, с которыми вы можете столк- нуться при таком подходе, я построил пример MdiMulti. Этот пример имеет дочер- ние формы двух различных типов: на первом содержится круг, выводимый в точке последнего щелчка мыши, а на втором — отскакивающий квадрат. Главная форма также имеет пользовательский фон, получаемый повторным размещением одного изображения. Дочерние формы и слияние меню Первый тип дочерней формы выводит круг в точке, в которой пользователь щелк- нул на любой из кнопок мыши. На рис. 8.4 представлен результат работы програм- мы MdiMulti. В ней имеется меню Circle (Круг), которое позволяет пользователю изменять цвет поверхности круга, а также цвет и толщину границы. Интересно, что для программирования дочерней формы вам не требуется наличия других форм или главного окна. Вы просто пишете программный код для формы, и все! Внима- ния требуют только меню двух форм. Если вы подготовили главное меню для дочерней формы, то при активизации Дочерней формы оно будет заменено главным меню окна-фрейма: дочернее MDI- окно не может иметь собственного меню. Но этот факт не должен беспокоить вас, Поскольку это стандартное поведение MDI-приложений. Тем не менее существует возможность вывести меню дочернего окна в панели меню окна-фрейма или, что 0367
368 Глава 8. Архитектура Delphi-приложений даже лучше, выполнить слияние панели главного меню и формы дочернего окна. Например, в этой программе меню дочерней формы может быть помещено между пунктами File и Window раскрывающегося меню фрейма. Этого можно добиться, используя следующие значения свойства Grouplndex: О Раскрывающееся меню File, главная форма: 1. О Раскрывающееся меню Circle, дочерняя форма: 2. О Раскрывающееся меню Window, главная форма: 3. Рис. 8.4. Результат работы программы MdiMulti — дочернее окно с построением кругов При использовании этих настроек индексов групп меню в панели меню окна- фрейма будет присутствовать два или три раскрывающихся меню. При загрузке она будет иметь два раскрывающихся пункта. При создании дочерних окон меню станет три; а после закрытия (уничтожения) последнего дочернего окна — меню Circle пропадет. Вы можете поэкспериментировать с этой программой, чтобы про- верить это поведение. Дочерняя форма второго типа представляет двигающееся изображение. Квад- рат (компонент Shape) перемещается по клиентской области формы через фикси- рованные интервалы времени (используя компонент Timer), отскакивая от границ формы, изменяя направление. Процесс поворота определяется довольно сложным алгоритмом, который мы здесь рассматривать не будем; суть примера в том, чтобы показать вам, как будет осуществлено слияние меню при открытии в MDI-фрейме дочерних окон различных типов. Главная форма А теперь давайте интегрируем две дочерние формы в MDI-приложение. Раскры- вающееся меню File имеет два отдельных пункта New, предназначенные для созда- ния дочерних окон различных типов. Программный код использует один счетчик дочерних окон. В качестве альтернативы можно использовать два различных счет- чика различных типов. Меню Window использует предопределенные MDI-действия. Как только форма определенного типа появится на экране, произойдет слия- ние ее панели меню с главным меню. При выборе дочерней формы любого из ти- пов соответственным образом изменится и главная панель меню. После закрытия всех дочерних окон меню главной формы вернется в исходное состояние. Благода- 0368
MDI-приложения с различными дочерними окнами 369 ря использованию индексов групп меню среда Delphi все выполняет автоматиче- ски (рис. 8.5). Рис. 8.5. Панель меню приложения MdiMulti автоматически отражает наличие различных дочерних окон. Сравните с рис. 8.4 Я добавил в меню еще несколько пунктов, позволяющих закрыть любое дочер- нее окно и вывести о нем статистическую информацию. Метод, связанный с ко- мандой Count, сканирует свойство-массив MDIChildren и считает количество окон каждого типа (с помощью RTTI-оператора is): for I .= О to MDIChnIdCount - 1 do if MDIChildren is TBounceChi1dForm then Inc (NBounce) else Inc (NCircle): Перехват сообщений, посланных клиентскому MDI-окну Пример программы также включает поддержку «черепичного» упорядочивания фонового изображения. Битовое изображение берется из компонента Image и «про- рисовывается» на форме с помощью обработчика Windows-сообщения wm_Era- seBkgnd. Проблема заключается в том, что нельзя просто подключить программ- ный код к главной форме, поскольку ее поверхность закрывает отдельное окно (MDI-клиент). Так как же обработать его сообщения, если для этого окна нет соответствую- щей Delphi-формы? Приходится прибегать к низкоуровневой Windows-техноло- гии, известной как «перехват сообщений окна» (subclassing). Его основная идея заключается в замене процедуры, получающей все сообщения данного окна, но- вой. Это достигается с помощью API-функции SetWindowLong и предоставления адреса этой процедуры в памяти (указатель функции). СОВЕТ----------------------------------------------------------- «Оконная» процедура — это функция, которая получает все сообщения окна. Каждое окно должно иметь такую процедуру, и она должна быть единственной. Даже Delphi-формы имеют процедуру окна; хотя это скрывается в системе, вызывается виртуальная функция WndProc, которую тоже можно использовать. Однако VCL имеет предопределенный обработчик для этих сообщений, кото- рый пересылает их методам обработки сообщений формы. Вам необходимо лишь явно обработать «оконную» процедуру, а затем работать с не-Delphi окнами, как в данном случае. 0369
370 Глава 8. Архитектура Delphi-приложений Пока нет причины изменять стандартное поведение этого системного окна, можно просто сохранить оригинальную процедуру и вызывать ее для выполнения стандартной обработки. Два указателя на функции ссылаются на две процедуры (старую и новую), хранимые в двух локальных полях формы: private OldWinProc. NewWInProc: Pointer: procedure NewW1nProcedure (var Msg: TMessage); Эта форма также имеет метод, используемый как новая процедура окна; про- граммный код будет использоваться для «раскраски» фона окна. Поскольку это метод, а не обычная процедура окна, то для добавления префикса в метод и воз- можности использования его в системе в качестве как функции программа должна вызывать метод MakeObjectlnstance. Все эти доводы можно представить в двух слож- ных выражениях: procedure TMainForm.FormCreate(Sender: TObject); begin NewWInProc :- MakeObjectlnstance (NewW1nProcedure): OldWinProc :- Pointer (SetWlndowLong (CHentHandle, gwl_WndProc, Cardinal (NewWInProc))); OutCanvas TCanvas.Create; end: Устанавливаемая оконная процедура вызывает стандартную процедуру. Далее, если поступившим сообщением является wm_EraseBkgnd и изображение непустое, то оно выводится на экран несколько раз с помощью метода Draw временного объек- та canvas (полотно). Этот объект создается при запуске программы (см. предыду- щий программный код) и подключается к обработчику, передаваемому сообщени- ем в качестве параметра wParam. При таком подходе нет необходимости создавать новый объект TCanvas для каждой операции прорисовки, экономя таким образом немного времени в часто повторяющемся действии. Вот программный код, даю- щий результат, представленный на рис. 8.5: procedure TMainForm.NewWInProcedure (var Msg: TMessage): var BmpWidth, BmpHelght: Integer: I. J: Integer: begin // сначала обработка no умолчанию Msg.Result := Cal 1WindowProc (OldWinProc. ClientHandle, Msg.Msg, Msg.wParam. Msg.iParam); // перерисовка фона if Msg.Msg = wm_EraseBkgnd then begin BmpWidth := MainForm.Imagel.Width: BmpHeight := MainForm.Imagel.Height: if (BmpWidth <> 0) and (BmpHeight <> 0) then begin OutCanvas.Handle := Msg.wParam: for I := 0 to MainForm.CllentWidth div BmpWidth do for J := 0 to MainForm.CllentHeight div BmpHeight do OutCanvas.Draw (I * BmpWidth. J * BmpHeight. MainForm.Imagel.Picture.Graphic); end: end: end: 0370
Визуальное наследование форм 371 Визуальное наследование форм Когда необходимо создать две и более схожие формы (при необходимости — с раз- личными обработчиками), можно воспользоваться динамическими технологиями, прятать и создавать новые компоненты во время выполнения, изменять обработ- чики событий и использовать выражения if или case. Либо воспользоваться такой объектно-ориентированной технологией, как визуальное наследование форм, Крат- ко говоря: вместо создания форм, основанных на классе TForm, можно выполнить наследование от существующей формы, добавив новые компоненты или изменив свойства существующих компонентов. В чем же преимущество наследования форм? Оно в основном определяется типом создаваемого приложения, Если програм- ма имеет множество форм, многие из которых схожи между собой или просто содержат одинаковые элементы, то общие компоненты и общие обработчики со- бытий можно поместить в базовую форму, а в подклассах определить особое пове- дение. Например, если подготовить стандартную родительскую форму с панелью инструментов, логотипом, стандартным программным кодом изменения размера и закрытия, а также обработчики некоторых Windows-сообщений, в дальнейшем можно использовать ее в качестве родительского класса для остальных форм при- ложения. Визуальное наследование форм может использоваться для настройки прило- жений под различных клиентов без дублирования программного кода источника и кода определения форм. Клиентская форма наследуется от стандартной формы. Учтите, что основное преимущество визуального наследования заключается в том, что при последующем изменении оригинальной формы все унаследованные от нее формы будут обновлены автоматически. Во всех объектно-ориентированных язы- ках программирования существует это известное преимущество наследования. Но существует и полезный побочный эффект: полиморфизм. В базовую форму мож- но добавить виртуальный метод, а в унаследованной форме перекрыть этот метод. После этого можно обращаться к обеим формам и вызывать соответствующий ме- тод для каждой из них. СОВЕТ---------------------------------------------------------— Delphi включает и другую функциональную особенность, похожую на визуальное наследование форм: фреймы. В обоих случаях во время разработки можно работать с двумя версиями формы/ фрейма. Однако при использовании наследования определяются два различных класса (родитель и наследник), а при использовании фреймов вы работаете с классом и его экземпляром. Боле подроб- но фреймы будут рассмотрены далее в этой главе. Наследование от базовой формы Привила, руководящие визуальным наследованием форм, очень просты, если вы четко представляете саму идею наследования. В общем случае форма-подкласс имеет те же компоненты, что и родительская форма, а также ряд новых компонен- тов. Невозможно удалить компоненты базового класса, хотя (если это визуальный элемент управления) его можно сделать невидимым. Важно, чтобы вы можете лег- ко изменять свойства унаследованных компонентов. Обратите внимание, что при изменении свойства в унаследованной форме лю- бые изменения этого свойства в базовой форме не будут иметь никакого эффекта. 0371
372 Глава 8. Архитектура Delphi-приложений Однако изменение других свойств компонента повлияет на унаследованную вер- сию. С помощью команды Revert to Inherited локального меню инспектора можно выполнить повторную синхронизацию двух значений. Того же эффекта можно достичь установкой в свойствах одинакового значения и последующей повторной компиляцией программного кода. После изменения нескольких свойств выпол- нить повторную синхронизацию на основе базовой версии можно с помощью той же команды Revert to Inherited в локальном меню компонента. Помимо наследования компонентов новая форма наследует все методы базо- вой формы, включая обработчики событий. В унаследованной форме можно пере- крывать существующие обработчики, а также добавлять новые. Для описания того, как работают унаследованные формы, я построил простой пример — VFI. Для его создания необходимо создать новый проект и в его главную форму добавить четыре кнопки. После этого выберите File ► New ► Other и в диало- говом окне New Items выберите страницу с именем проекта (рис. 8.6). Рис. 8.6. Диалоговое окно New Items позволяет создавать унаследованную форму Здесь можно выбрать производную форму. Новая форма имеет те же четыре кнопки. Вот начало текстового описания новой формы: inherited Form2 TForm2 Caption = 'Form2' end А вот начало объявления класса, в котором вместо обычного класса TForm ис- пользуется класс базовой формы: type TForm2 = class(TForml) private { Private declarations } public { Public declarations } end: Обратите внимание на наличие в текстовом описании ключевого слова inherited; а также на то, что форма уже имеет несколько компонентов благодаря тому, что они определены в форме базового класса. Если вы переместите форму и добавите заголовки для одной из кнопок, соответствующим образом изменится текстовое описание: 0372
Визуальное наследование форм 373 inherited Form2: TForm2 Left = 313 Top = 202 Caption = 'Form?' inherited Button2: TButton Caption = 'Beep... ' end end Перечислены только свойства с отличающимися значениями (удалением этих свойств из текстового описания унаследованной формы можно вернуться к значе- ниям, имеющимся в базовой форме). Я изменил заголовок кнопок (рис. 8.7). Sto*.. ‘I W* | Bl j Kfrfews. | № {Вверг, | Рис. 8.7. Две формы примера VFI во время выполнения Каждая из кнопок первой формы имеет обработчик OnClick с простым программ- ным кодом. Первая кнопка показывает унаследованную форму вызовом ее метода Show; вторая и третья кнопки вызывают процедуру Веер, а последняя кнопка выво- дит сообщение. В унаследованной форме сначала необходимо удалить кнопку Show, поскольку вторичная форма уже видима. Однако невозможно удалить компонент с унасле- дованной формы. Альтернативное решение заключается в установке свойства Visible компонента в False — кнопка по-прежнему остается там, но она будет невидима (см. рис. 8.7). Остальные три кнопки будут видимы, но будут иметь другие обра- ботчики. Если вы выберете событие OnClick кнопки на унаследованной форме (двой- ным щелчком на ней), вы получите пустой метод, который практически не отлича- ется от стандартного, но содержит ключевое слово inherited. Это слово указывает на вызов соответствующего обработчика события базовой формы. Обратите вни- мание, что это ключевое слово всегда добавляется средой Delphi, даже если в ро- дительском классе этот обработчик не определен (и это правильно, поскольку он может быть определен позднее) либо если компонент не представлен в родитель- ском классе (что, мне кажется, не совсем верно). Выполнить программный код ба- зовой формы и другие операции довольно просто: procedure TForm2.Button2ClickfSender: TObject). begin inherited; ShowMessage ('Hi'). end; Это не единственный вариант. Можно еще написать новый обработчик собы- тия и не выполнять код базового класса, как это сделано в отношении третьей кноп- ки примера VFI. Для этого просто удалите ключевое слово inherited. Так же можно вызывать метод базового класса после выполнения определен- ного пользовательского кода, вызывать его при выполнении определенного усло- вия или вызывать обработчик другого события базового класса, как это сделано Для четвертой кнопки: 0373
374 Глава 8. Архитектура Delphi-приложений procedure TForm2.Button4Click(Sender: TObject); begin inherited Button3Click (Sender): inherited. end: Вероятно, вы не будете часто наследовать иной обработчик, но знать об этой возможности вы должны. Конечно же, можно рассматривать каждый метод базо- вого класса как метод вашей формы и свободно его вызывать. Этот пример допол- нительно позволяет изучить ряд возможностей визуального наследования форм, но для рассмотрения их реальной мощности требуются более сложные реальные примеры, рассмотреть которые не позволяет объем данной книги. Далее я хочу показать вам визуальный полиморфизм форм. СОВЕТ--------------------------------------------------------------------------------- Визуальное наследование форм не совсем верно работает с коллекциями: на наследованной форме невозможно развернуть свойство-коллекцию компонента. Это ограничение практически не позво- ляет использовать такие компоненты, как Toolbars или ListViews. Конечно же, эти компоненты можно использовать в родительских или в унаследованных формах, но вы не сможете раскрыть содержа- щиеся в них элементы, поскольку они хранятся в виде коллекции. Решение этой проблемы заключа- ется в предотвращении назначения этих коллекций в ходе разработки, а воспользоваться для этого можно технологиями времени выполнения. Вы по-прежнему будете пользоваться наследованием форм, но потеряете визуальную часть этого процесса. Если попытаться использовать компонент Action Manager, вы столкнетесь с тем, что даже не сможете его наследовать с исходной формы. Компания Borland отключила эту возможность, поскольку она может привести к слишком большим проблемам. Полиморфные формы Если в форму добавить обработчик события, а затем изменить его в унаследован- ной форме, то обратиться к этим двум методам с помощью общей переменной ба- зового класса будет невозможно, поскольку обработчик события по умолчанию использует статическое связывание. Смущает? Вот пример, который рассчитан на опытных Delphi-программистов. Предположим, вы хотите построить программу, имеющую форму просмотра бито- вых изображений и в той же программе — форму просмотра текстов. Обе формы имеют похожие элементы, подобную панель инструментов, подобное меню, ком- понент Open Dialog и совершенно различные компоненты для представления дан- ных. Допустим, вы решили построить форму базового класса, содержащую общие элементы, и унаследовать их в двух формах. Во время разработки у вас будет три формы (рис. 8.8). Основная форма содержит панель инструментов с несколькими кнопками (ре- альные панели инструментов имеют ряд проблем с визуальным наследованием форм), меню и компонент OpenDialog. Две унаследованные формы имеют очень незначительные отличия, но в них добавлен новый компонент: либо для просмот- ра изображений (TImage), либо для просмотра текста (ТМето). Они также изменя- ют настройки компонента OpenDialog, чтобы те соответствовали различным ти- пам файлов. Основная форма содержит общий программный код. Кнопка Close и меню File ► Close вызывают метод Close формы. Команда Help ► About выводит простое окно с сообщением. Кнопка Load базовой формы имеет лишь вызов ShowMessage, выво- дящий сообщение об ошибке. Команда File ► Load вызывает другой метод: 0374
Визуальное наследование форм 375 procedure TViewerForm.LoadlClicktSender: TObject); begin LoadFile; end; Рис. 8.8. Форма базового класса и две унаследованные формы примера PoliForm во время разработки Этот метод определен в классе TViewerForm как виртуальный абстрактный ме- тод (так как этот класс в базовой форме является абстрактным). Поскольку это абстрактный метод, его необходимо повторно объявить (или перекрыть) в унаследо- ванных формах. Программный код метода LoadFile использует компонент OpenOialogl для запроса выбранного пользователем файла и загружает его в компонент Image: procedure TImageViewerForm.LoadFile; begin if OpenOialogl.Execute then Imagel.Picture.LoadFromFi 1e (OpenDia 1ogl.Filename); end; Второй унаследованный класс имеет подобный программный код, загружаю- щий текст в компонент МЕМО. Проект имеет еще одну форму — главная форма с дву- мя кнопками, которые осуществляют загрузку файлов в каждую из форм просмотра. Главная форма является единственной формой, созданной проектом при запуске. Общая форма просмотра никогда не создается: она является лишь базовым клас- сом, содержащим общий код и компоненты двух подклассов. Формы двух под- классов создаются обработчиком события OnCreate главной формы: procedure TMainForm.FormCreatetSender: TObject); var I; Integer: begin FormList [1] .= TTextViewerForm.Create (Application); FormList [2] .= TImageViewerForm.Create (Application); for I ;=• 1 to 2 do FormList[I].Show; end; FormList — это полиморфный массив общих объектов TViewerForm, объявленных в классе TMainForm. Обратите внимание, что для выполнения этого объявления в классе в инструкцию uses интерфейсного раздела главной формы необходимо Добавить модуль Viewer (а не определенные формы). Массив форм используется Для загрузки нового файла в каждую форму средства просмотра при щелчке на 0375
376 Глава 8. Архитектура Delphi-приложений одной из двух кнопок. Обработчики событий OnClick этих двух кнопок используют различные подходы: // ReloadButtonlClick for I := 1 to 2 do FormList [I]. ButtonLoadClick (Self); // ReloadButton2C11ck for I := 1 to 2 do FormList [I].LoadFi1e; Вторая кнопка вызывает виртуальный метод, и он работает без проблем. Пер- вая кнопка вызывает обработчик события и всегда «доходит» до общего класса TFormView (выводящего сообщение об ошибке его метода ButtonLoadClick). Это про- исходит ввиду того, что этот метод статический, а не виртуальный. Для того чтобы этот подход работал, можно объявить метод ButtonLoadClick клас- са TFormView как виртуальный и перекрывать его в каждом наследуемом классе формы, как это делается для любых виртуальных методов: type TViewerForm = class(TForm) procedure ButtonLoadClick(Sender: TObject): virtual: publi c procedure LoadFile: virtual; abstract; end: - type TImageViewerForm - class(TViewerForm) procedure ButtonLoadClick(Sender: TObject): override; public procedure LoadFile: override; end; Эта уловка действительно работает, хотя она ни разу не упоминается в доку- ментации Delphi. Возможность использования виртуальных обработчиков собы- тий — именно то, что я подразумеваю под понятием визуальный полиморфизм форм. Другими словами (более техническим языком) это можно выразить как «воз- можность присвоения виртуального метода свойству события, которое будет полу- чать адрес метода, соответствующего экземпляру, доступному в ходе выполнения». Понятие фреймов В главе 1 фреймы были рассмотрены лишь вкратце. Было показано, что имеется возможность создать новый фрейм, поместить на него компоненты, написать об- работчики событий для этих компонентов, а затем добавить фрейм в форму. Ина- че говоря, фрейм очень похож на форму, но он определяет лишь часть окна, а не все окно. Интересным элементом является возможность создания в ходе разработки множества экземпляров одного фрейма, причем класс и его экземпляр модифици- руются одновременно. Следовательно, фреймы являются эффективным средством создания настраиваемых составных элементов управления на этапе разработки, то есть это чем-то напоминает визуальное средство построения компонентов. При использовании в ходе разработки визуального наследования форм можно работать как с базовой формой, так и с производной формой, и любые изменения 0376
Понятие фреймов 377 в базовой форме будут немедленно распространяться на производные (если толь- ко вы не перекрыли их методы и события). При работе с фреймами вы работаете с классами (как обычно в Delphi), но тут же во время разработки имеется возмож- ность настраивать один и более экземпляров класса. При работе с формами на эта- пе разработки невозможно изменить свойство класса TForml для определенного экземпляра этой формы. При работе с фреймами — можно. После того как вы осознаете, что во время разработки вы работаете с классом или одним из его экземпляров, больше ничего о фреймах понимать и не надо. В действительности фреймы очень полезны в том случае, когда необходимо ис- пользовать одну и ту же группу компонентов в различных формах одного прило- жения. В этом случае в ходе разработки можно настроить каждый экземпляр. Это также можно сделать с помощью шаблонов компонентов, но последние основаны на концепции «копирования и вставки» компонентов и их программного кода. Вы не смогли бы изменить исходное определение шаблона и увидеть получаемый эф- фект. При использовании фреймов (и, несколько иным способом, визуального на- следования форм), изменения оригинальной версии (класса) сразу отражаются на копии (экземпляры). Давайте на примере Frames2 рассмотрим несколько моментов, связанных с фрей- мами. Эта программа имеет фрейм, содержащий список, строку редактирования и три кнопки с программным кодом обработки этих компонентов. Фрейм также использует рамку (bevel), выровненную по клиентской области, поскольку фрей- мы не имеют границ. И, разумеется, фрейм имеет соответствующий класс, кото- рый подобен классу формы: type TFrameList = class(TFrame) ListBox: TListBox; Edit: TEdit; btnAdd: TButton; btnRemove: TButton; btnClear: TButton: Bevel: TBevel; procedure btnAddClick(Sender: TObject); procedure btnRemoveClick(Sender: TObject): procedure btnClearClick(Sender: TObject): private { Private declarations } publi c { Public declarations } end; От класса формы его отличает то, что фрейм можно добавлять в форму. В при- мере я использовал два экземпляра фрейма (рис. 8.9) и несколько изменил их по- ведение. Первый экземпляр фрейма имеет отсортированный список. При измене- нии свойства компонента фрейма содержащий его DFM-файл будет содержать список всех изменений, также как это происходит при визуальном наследовании форм: object FormFrames: TFormFrames Caption = 'Frames2‘ inline FrameListl: TFrameList Left - 8 Top - 8 inherited ListBox: TListBox 0377
378 Глава 8. Архитектура Delphi-приложений Sorted =• True end end Inline FrameList2: TFrameLlst Left = 232 Top = 8 Inherited btnClear: TButton OnClick = FrameList2btnClearClick end end end Рис. 8.9. Фрейм и два его экземпляра в ходе разработки (пример Frames2) Как видно из листинга, DFM-файл формы вмещает фреймы с использованием специфичного для DFM ключевого слова inline. Однако для ссылок на модифици- рованные компоненты фрейма используется ключевое слово inherited, хотя обыч- но это понятие используется в расширенном значении. А в данном случае inherited не относится к базовому классу, от которого осуществляется наследование, а к клас- су, от которого осуществляется создание экземпляра (или наследование) объекта. Это была хорошая идея: использовать существующую возможность визуального наследования формы и применения ее в новом контексте. Это позволяет исполь- зовать команду Revert to Inherited инспектора объектов или формы для отмены из- менений и возврата к значениям свойств по умолчанию. Обратите также внимание на то, что оставшиеся без изменения компоненты класса фрейма не представлены в DFM-файле формы, использующей данный фрейм; форма может иметь два фрейма с различными именами, но компоненты двух фреймов могут иметь одинаковые имена. Эти компоненты не принадлежат данной форме, их владельцем является фрейм. Это подразумевает, что форма дол- жна обращаться к этим компонентам через фрейм (см. программный код кнопок, копирующих пункты из одного списка в другой): procedure TFormFrames.btnLeftClicklSender: TObject): begin FrameListl.ListBox.Items.AddStrings (FrameList2.ListBox.Items); end: И, наконец, помимо изменения свойств любого экземпляра фрейма можно из- менить программный код любого из их обработчиков событий. При двойном щел- чке на кнопках фреймов при работе с формой (а не в отдельном фрейме) Delphi crpHpnunveT cneavioniHtt поогоаммный код: 0378
Понятие фреймов 379 procedure TFormFrames FrameList2btnClearClrck(Sender: TObject); begin F rameList2.btnClea rClick(Sender): end: При использовании визуального наследования форм эта строка кода, автома- тически добавляемая Delphi, соответствует вызову унаследованного обработчика события базового класса. Однако в данном случае для обеспечения стандартного поведения фрейма необходимо вызвать обработчик события и применить его к определенному экземпляру — к самому объекту «фрейм». Текущая форма не со- держит этот обработчик события и ничего не знает о нем. Оставите ли вы этот вызов на месте или удалите его — зависит от необходимого эффекта. ПРИМЕЧАНИЕ-------------------------------------------------------------------------- Обратите внимание, что поскольку обработчик события содержит какой-то код, оставление его в том виде, в котором его сгенерировала среда Delphi, и сохранение формы обычно не приводит к его удалению: он не пустой! Для того чтобы система автоматически не удалила пустой обработчик события, необходимо хотя бы добавить комментарий. Фреймы и страницы Когда диалоговое окно имеет множество вкладок (страниц), заполненных элемен- тами управления, программный код, относящийся к форме, становится очень слож- ным ввиду того, что все элементы управления и методы объявляются в одной фор- ме. Кроме того, создание этих компонентов (и их инициализация) могут привести к задержке вывода диалогового окна на экран. По сравнению с формами фреймы не сокращают время создания и инициализации. Совсем наоборот, только увели- чивают, поскольку загрузка фреймов более сложна для поточной системы, чем за- грузка простых компонентов. Однако используя фреймы, можно загружать лишь видимые страницы многостраничных диалоговых окон, сокращая начальное вре- мя загрузки — именно то, что замечает пользователь. Фреймы могут решать и обе эти проблемы. Можно просто разделить программ- ный код одной сложной формы, создав по одному фрейму на страницу. Форма будет содержать все фреймы и элемент управления PageControl. Такой подход бо- лее практичен, сфокусирован на модулях и облегчает повторное использование одной страницы в различных диалоговых окнах или приложениях. Повторное ис- пользование страницы элемента PageControl без использования фрейма или вне- дренной формы довольно сложное. (Альтернативный подход рассмотрен во встав- ке «Формы на страницах»). Для демонстрации этого подхода я разработал пример FramePag. Он содержит несколько фреймов, помещенных на три страницы элемента PageControl (рис. 8.10), что видно в дереве Object TreeView. Все фреймы выровнены по клиентской области и используют всю поверхность содержащей их вкладки (страницы). Две страницы имеют одинаковые фреймы, но два экземпляра этого фрейма имеют некоторые отличия. Фрейм Frame3 имеет список, заполняемый из текстового файла при за- Фузке, а также кнопки для изменения пунктов списка и сохранения его в файл. Имя файла представлено в надписи (элемент label), поэтому в ходе разработки Можно легко выбрать файл для фрейма, изменив свойство Caption этой надписи. Возможность использования множества экземпляров фрейма является одной из причин появления этой технологии, а возможность настройки фрейма во время 0379
380 Глава 8. Архитектура Delphi-приложений разработки сделала ее очень важной. Поскольку добавление свойств во фрейм и его доступность во время разработки требует настройки и более сложного про- граммного кода, то для хранения настраиваемых значений удобней использовать какой-либо компонент. Имеется возможность спрятать компоненты (в нашем при- мере — надпись), если они не относятся к пользовательскому интерфейсу. Рис. 8.10. Каждая страница примера FramePag содержит фрейм, разделяя, таким образом, программный код сложной формы на более доступные фрагменты В данном примере необходимо выполнить загрузку файла при создании экзем- пляра фрейма. Поскольку фреймы не имеют события OnCreate, наилучшим реше- нием, вероятно, будет перекрытие метода CreateWnd. Написание пользовательско- го конструктора не сработает, поскольку он выполняется слишком рано — до того, как станет доступен текст определенной надписи (элемента label). В методе Create- Wnd первый список загружается из файла. СОВЕТ-------------------------------------------------------------------------- Когда задается вопрос о проблемах пропуска обработчика события OnCreate для фрейма, члены Borland R&D утверждают, что они не могут запустить его в соответствии с сообщением wm_Create, поскольку оно происходит с формами. Создание окна фрейма (что верно для большинства элемен- тов управления) откладывается из соображений сохранения производительности. Еще больше про- блем возникает в случае наследования форм, содержащих фреймы, поэтому для того, чтобы избежать этих проблем, эта функция отключена — программисты самостоятельно могут написать подходя- щий код. Формы в страницах Хотя в ходе разработки для определения страниц элемента PageControl мож- но использовать фреймы, в ходе выполнения я обычно использую другие формы. Этот подход оставляет гибкость наличия страниц, определенных в различных модулях (и DFM-файлах), но в то же время позволяет использо- вать эти формы как отдельные окна. После того как у вас появилась главная форма с элементом управления page и одна и более вторичных форм для их вывода, все, что вам необходимо сделать, — написать представленный ниже программный код, создающий вторичные формы и помещающий их на страницы: 0380
Понятие фреймов 381 var Form: TForm; Sheet: TTabSheet; begin // создать таблицу с элементом управления page Sheet := TTabSheet.Create(PageControll); Sheet.PageControl := PageControll; // создать форму и поместить ее в таблицу Form :- TForm2.Create (Application); Form.BorderStyle :- bsNone; Form.Align := alClient; Form.Parent :- Sheet; Form. Visible ;- True; // активизировать и установить заголовок PageControll.ActivePage :- Sheet; Sheet.Caption ;- Form.Caption; end; Этот программный код можно найти в примере Form Раде, но это не все, что может эта программа. В качестве приложения см. пример RW Blocks в гла- ве 14. Множество фреймов без страниц Еще одним подходом, избавляющим от необходимости создания всех страниц вме- сте с содержащими их формами, заключается в оставлении элемента PageControl незаполненным и создании фреймов только по мере вывода страниц. Когда на стра- ницах PageControl имеется несколько фреймов, окна для этих фреймов создаются только при их первоначальном выводе на экран, в чем можно убедиться, устано- вив точку останова (breakpoint) в программном коде предыдущего примера. В качестве более радикального похода можно вовсе избавиться от элементов управления PageControl и использовать TabControl. При этом вкладка не подклю- чена к блокноту с вкладками (или страницам) и может выводить за раз только один набор информации. По этой причине необходимо создать текущий фрейм, либо уничтожить предыдущий, либо спрятать его, установив свойство Visible в False, либо вызвать метод BringToFront нового фрейма. Хотя может показаться, что необ- ходимо выполнить большой объем работ, в больших приложениях эта технология вполне оправданна, поскольку сокращает использование ресурсов и памяти. Для демонстрации этого подхода я построил пример FrameTab, который похож на предыдущий, но основан на TabControl и динамически создаваемых фреймах. Основная форма, видимая в ходе выполнения (рис. 8.11), имеет TabControl с одной страницей для каждого фрейма: object Forml: TForml Caption = 'Frame Pages' OnCreate = FormCreate object Buttonl: TButton... object Button2: TButton... object Tab: TTabControl 0381
382 Глава 8. Архитектура Delphi-приложений Anchors - [akLeft. akTop. akRight, akBottom] Tabs.Strings - ( 'Frame?' ’Frame3' ) OnChange = TabChange end end Я присвоил каждой вкладке заголовок, соответствующий имени фрейма, посколь- ку буду использовать эту информацию для создания новых страниц. После создания формы или при смене пользователем активной вкладки программа получает заголо- вок текущей вкладки и передает его в пользовательский метод ShowFrame. Программ- ный код этого метода проверяет, существует ли запрашиваемый фрейм (имена фрей- мов в этом примере соответствуют Delphi-стандарту и представляют собой имя класса с добавлением числа), а затем переносит его на передний план. Если фрейм не суще- ствует, то метод использует имя фрейма для нахождения связанного с ним имени класса, создает объект этого класса и присваивает ему ряд свойств. Код активно ис- пользует ссылки класса и технологии динамического создания: Рис. 8.11. Первая страница примера FrameTab в ходе выполнения. Фрейм на вкладке создан в ходе выполнения type TFrameClass = class of TFrame: procedure TForml.ShowFrame(FrameName: string): var Frame: TFrame: FrameClass: TFrameClass. begin Frame :- FindComponent (FrameName + ’1") as TFrame. if not Assigned (Frame) then begin FrameClass := TFrameClass (FindClass CT' + FrameName)): Frame .=• Framed ass. Create (Self): Frame.Parent : = Tab: Frame.Visible : = True: Frame.Name := FrameName + ‘Г: end: Frame.BringToFront: end; Для того чтобы этот программный код работал, не забудьте в разделе инициа- лизации каждого модуля, определяющего фрейм, добавить вызов к RegisterCLass. 0382
Базовые формы и экземпляры 383 Базовые формы и экземпляры Вы уже видели, что при необходимости наличия в приложении двух одинаковых форм для производства одной формы от другой или обоих от общего предка мож- но воспользоваться визуальным наследованием форм. Преимущество визуально- го наследования форм заключается в наследовании DFM-определения. Однако это не всегда требуется. Иногда необходимо, чтобы несколько форм придерживались общего поведе- ния или отвечали на одинаковые команды, не имя каких-либо общих компонентов или элементов пользовательского интерфейса. В этом случае более рациональным является использование визуального наследования форм с использованием базо- вой формы, не содержащей дополнительных компонентов. Я предпочитал бы оп- ределить свой собственный класс формы, унаследованный от TForm, а затем вруч- ную отредактировать объявления классов форм для наследования именно от этого класса, а не от стандартного. Если необходимо лишь определить общие методы или перекрыть соответствующим образом виртуальные методы класса TForm, то лучше определить пользовательский класс формы. Использование класса базовой формы Подтверждением моих слов является пример Formlntf. Он также представляет ис- пользование интерфейсов для форм. В новом модуле SaveStatusForm я определил следующий класс формы (без соответствующего DFM-файла. Вместо использо- вания команды New ► Form создайте новый модуль и введите в него этот программ- ный код): type TSaveStatusForm = class (TForm) protected procedure DoCreate: override; procedure DoDestroy. override; end. Два перекрытых метода вызываются одновременно с обработчиком события, поэтому к ним можно присоединить дополнительный программный код (позво- лив определить обработчик события обычным образом). В этих двух методах про- изводится загрузка или сохранение положения формы в INI-файле приложения, в разделе, отмеченном заголовком формы. Вот код этих двух методов: procedure TSaveStatusForm.DoCreate. var Im: TImFile. begin inherited. Ini •= TImFile Create (ExtractFIleName (Application.ExeName)): Left = Im ReadInteger(Caption, 'Left'. Left). Top .= Im ReadInteger(Caption, 'Top', Top). Width = Im.ReadInteger(Caption. 'Hidth'. Width); Height := Im.ReadInteger(Caption. 'Height'. Height); Im .Free: end; Procedure TSaveStatusForm DoDestroy. 0383
384 Глава 8. Архитектура Delphi-приложений var Ini: TIniFile; begin Ini := TIniFile.Create (ExtractFileName (Application.ExeName)): Ini.WriteInteger(Caption, 'Left'. Left): Ini.WriteInteger(Caption. 'Top'. Top): Ini.WriteInteger(Caption, 'Width', Width); Ini,WriteInteger(Caption. 'Height'. Height): Ini.Free: inherited: end; Опять же, это простой характер поведения ваших форм, но здесь можно опре- делить и сложный класс. Для использования его в качестве базового класса для создаваемых форм дайте Delphi создать форму обычным образом (без наследова- ния), а затем обновите объявление формы: type TFormBitmap = class(TSaveStatusForm) Imagel: TImage: OpenPictureDialogl: TOpenPictureDialog: Простая на взгляд технология является очень мощной, поскольку все, что не- обходимо сделать, — это изменить определение формы вашего приложения, отсы- лая его к базовому классу. Если даже это вам кажется неудобным из-за того, что вы хотели бы иметь возможность изменить базовый класс в любом месте, можно при- бегнуть к дополнительной уловке: «вставляемым» (interposer) классам. Реестр и INI-файлы в Delphi Для сохранения сведений о состоянии приложения в целях его восстанов- ления при следующем запуске можно использовать явную поддержку, пре- доставляемую Windows. Старый стандарт — INI-файлы — по-прежнему остается предпочтительным способом сохранения данных приложения. Аль- тернативой является использование реестра, который достаточно популя- рен. Для использования обоих вариантов Delphi предоставляет готовые к использованию классы. Класс TIniFile Для INI-файлов Delphi имеет класс TIniFile. После создания объекта этого класса и подключения его к файлу можно читать и записывать в него ин- формацию. Для создания объекта необходимо вызвать конструктор, пере- дав в него имя файла: var IniFile: TIniFile; begin IniFile := TIniFile.Create ('myprogram.ini'): Существует два варианта размещения INI-файла. Только что представ- ленный код сохраняет файл в каталоге ОС Windows или пользовательской папке настроек ОС Windows 2000. Для хранения данных совместно с при- ложением (а не с настройками текущего пользователя (папка Documents and Settings)), конструктору необходимо указать полный путь к файлу. 0384
Базовые формы и экземпляры 385 INI-файлы делятся на разделы, каждый из которых обозначается име- нем, указанным в квадратных скобках. Каждый раздел может содержать мно- жество пунктов трех возможных типов: строками, целыми числами и логи- ческими значениями. Класс TIniFile имеет три метода чтения, по одному для каждого типа данных: ReadBool, Readinteger и Readstring. Также существует и три соответствующих метода записи: WriteBool, Writeinteger и WriteString. Ос- тальные методы позволяют читать и стирать целые разделы. В Read-мето- дах также можно указать значение по умолчанию, которое будет использо- ваться, если в INI-файле отсутствует соответствующий элемент. Обратите внимание, что Delphi использует INI-файлы довольно часто, но они замаскированы с помощью других расширений. Например, файлы рабочего стола (*.dsk) и параметров (*.dof) по структуре являются INI-фай- лами. Классы TRegistry и TReglniFile Реестр — это иерархическая база данных сведений о компьютере, конфигу- рации программного обеспечения и пользовательских предпочтений. Для обращения к реестру Windows имеет набор API-функций. Обычно необхо- димо открыть ключ (или каталог) и работать с подключами (подкаталога- ми) и значениями (пунктами), но для этого необходимо четко представлять структуру и особенности реестра. Delphi обеспечивает два варианта использования реестра. Класс TRegistry обеспечивает общую инкапсуляцию API реестра, а класс TReglniFile предос- тавляет интерфейс классаПшТйе, но данные сохраняет в реестре. Этот класс является естественным выбором для обеспечения перехода с версии на ос- нове INI на версию, использующую реестр. При создании объекта класса TReglniFile ваши данные в конечном итоге относятся к текущему пользовате- лю, поэтому обычно используется подобный конструктор: IniFile TReglniFile.Create ('Software\MyCompany\MyProgram'); За счет использования предоставляемых VCL классов TIniFile и TReglniFile можно переходить от одной модели хранения данных к другой. Вы не должны слишком интенсивно использовать реестр, поскольку наличие централизо- ванного хранилища настроек каждого приложения является архитектурной ошибкой. Даже компания Microsoft признает этот факт (но не признавая это ошибкой), советуя в Windows 2000 Compatibility Requirements (Требо- вания совместимости Windows 2000) больше не использовать реестр для настроек приложений, а вместо этого вернуться к использованию INI-фай- лов, размещаемых в каталоге Documents and Settings текущего пользователя (немногие программисты знают об этом). Дополнительная уловка: «вставляемые» классы В отличие от VCL-компонентов Delphi, которые должны иметь уникальные име- на, в общем случае Delphi-классы должны быть уникальны только в пределах со- держащих их модулей. Таким образом, в двух различных модулях могут быть оп- ределены классы с одинаковыми именами. На первый взгляд такая технология Может показаться причудливой, но она может оказаться весьма полезной. Напри- 0385
386 Глава 8. Архитектура Delphi-приложений мер, компания Borland использует этот подход для обеспечения совместимости между классами VCL и VisualCLX. В них обоих имеется класс TForm. Один опреде- лен в модуле Forms, а другой — в QForms. СОВЕТ---------------------------------------------------------------------- Эта технология боле стара, чем сами классы CLX/VCL. Например, модули служб и компонентов панели управления определяли собственный объект TApplication, который совершенно не относится к классу TAppiication, используемому VCL визуальных GUI-приложений и определенному в модуле Forms. Я встречал упоминание о технологии «вставляемых» классов в журнале The Delphi Magazine. Она рекомендует заменять стандартные имена классов Delphi на собственные версии, имеющие те же имена классов. Таким образом, с помощью конструктора форм Delphi в ходе разработки можно обращаться к стандартным Delphi-компонентам, но при выполнении использовать собственные классы. Идея проста. В модуле SaveStatusForm можно определить новый класс формы: type TForm = class (Forms.TForm) protected procedure DoCreate: override: procedure DoDestroy: override: end. Этот класс назван TForm и он унаследован от класса TForm модуля Forms (для того, чтобы избежать рекурсивного определения, последняя ссылка является при- нудительной). В оставшейся части программы не надо изменять определение класса формы, а просто добавить модуль, определяющий «вставляемый» класс (в данном случае — SaveStatusForm) в выражении uses после модуля, определяющего Delphi- класс. Порядок следования модулей в выражении uses является важным, что яв- ляется причиной критики этой технологии ввиду сложности понимания происхо- дящего. Должен согласиться: иногда «вставляемый» класс очень удобен (в большей степени для компонентов, а не для форм), но его использование делает программу менее читаемой, а иногда и сложной для отладки. Использование интерфейсов Другая технология, которая более сложна, но является значительно более мощ- ной, чем определение общего базового класса формы, заключается в создании форм, которые реализуют специальные интерфейсы. Допускается наличие форм, реали- зующих один или несколько этих интерфейсов, запрос у каждой формы реализуе- мых ею интерфейсов и вызов поддерживаемых методов. В качестве примера (доступного в программе Formlntf, которую мы начали рас- сматривать в предыдущем разделе) я определил простой интерфейс для загрузки и выгрузки: type IFormOperations = interface I’{DACFDB76-0703-4A40-A951-10D140B4A2A0} '] procedure Load: procedure Save: end: Каждая форма дополнительно может реализовать этот интерфейс, например, как это сделано в классе TFormBitmap: 0386
Диспетчер памяти Delphi 387 type TFormBitmap = class(TForm. IFormOperations) Imagel: TImage: OpenPictureDialogl: TOpenPictureDialog: SavePictureDialogl: TSavePictureDialog; public procedure Load: procedure Save; end: Программный код примера включает методы Load и Save, использующие стан- дартные диалоговые окна загрузки и сохранения изображений. (В этом примере форма также унаследована от класса TSaveStatusForm.) Когда приложение имеет одну и более форм, реализующих интерфейсы, данный интерфейсный метод можно при- менить ко всем поддерживающим его формам: procedure TFormMain.btnLoadClick(Sender: TObject); var i: Integer; iFormOp: IFormOperations; begin for i := 0 to Screen.FormCount - 1 do if Supports (Screen.Forms [i], IFormOperations, iFormOp) then 1 FormOp.Load; end; Представьте коммерческое приложение, в котором можно синхронизировать все формы с данными определенной компании или определенного коммерческого собы- тия. И представьте, что в отличие от наследования, вы можете иметь несколько форм, реализующих множество интерфейсов в любых сочетаниях. Вот почему использова- ние такой архитектуры может значительно повысить сложность Delphi-приложения, делая его более гибким и более простым в подстройке под изменения реализации. Диспетчер памяти Delphi Завершим эту главу разделом, посвященным управлению памятью. Этот вопрос является очень сложным и, вероятно, более важным, чем вся глава. Но здесь я лишь затрону его и предложу несколько указаний для последующих экспериментов. Для более подробного анализа памяти можно обратиться к множеству средств-надстро- ек Delphi, предназначенных для контроля и управления выделением памяти, вклю- чая MemCheck, MemProof, MemorySleuth, Code Watch и AQTime. Delphi имеет диспетчер памяти, к которому можно обратиться с помощью фун- кций GetMemoiyManager и SetMemoiyManager модуля System. Эти функции позволя- ют извлечь текущую запись диспетчера памяти или заменить его пользователь- ским диспетчером памяти. Запись диспетчера памяти — это набор из трех функций, Предназначенных для выделения, освобождения и перераспределения памяти: type TMemoryManager = record GetMem: function(Size: Integer): Pointer: FreeMem: function(P: Pointer): Integer: ReallocMem: functionCP: Pointer: Size: Integer)- Pointer: end. 0387
388 Глава 8. Архитектура Delphi-приложений Очень важно знать, как при создании объекта вызываются зти функции, поскольку их можно перехватить на двух различных этапах. Как только вы вызы- ваете конструктор, Delphi вызывает виртуальную функцию класса Newlnstance, оп- ределенную в TObject. Ввиду того, что это — виртуальная функция, имеется воз- можность, перекрыв ее, модифицировать диспетчер памяти определенного класса. Однако для выделения памяти Newlnstance обычно прибегает к вызову функции GetMem активного диспетчера памяти, предоставляя второй вариант изменения стандартного поведения. Если нет особой нужды, не стоит вмешиваться в работу диспетчера памяти, из- меняя работу механизма выделения памяти. Такая необходимость возникает при желании проверки правильной работы «распределителя памяти», то есть провер- ки отсутствия «утечки» памяти в программе. Например, можно перекрыть методы Newlnstance и Freelnstance для возможности подсчета количества создаваемых и уничтожаемых объектов этого класса и проверки, что в итоге получается ноль. Более простая технология заключается в выполнении такой же проверки в от- ношении объектов, размещаемых общим диспетчером памяти. В ранних версиях Delphi это требовало написания специального программного кода, но сейчас в дис- петчере памяти открыты две глобальные переменные (AllocMemCou nt и AllocMemSize), помогающие определить, что происходит в системе. СОВЕТ------------------------------------------------------------------------ Более подробные сведения о внутренней работе диспетчера памяти можно получить с помощью функции GetHeapStatus. Она доступна только в Windows, поскольку предоставляет информацию о состоянии «распределителя памяти». В Linux RTL использует системный «распределитель», а не доступный «распределитель памяти». Самый простой способ определить, что ваша программа верно оперирует с па- мятью, заключается в проверке, возвращает ли AllocMemCount значение 0. Пробле- ма заключается в определении момента выполнения этой проверки. Программа начинается с выполнения раздела инициализации своих модулей, которые обыч- но распределяют память, освобожденную соответствующими разделами финали- зации (finalization). Для обеспечения того, что ваш программный код будет выпол- нен действительно в самом конце, его необходимо написать в разделе финализации модуля и поместить его в самое начало списка модулей в файле исходного кода проекта. Такой модуль представлен в листинге 8.1. Это модуль SimpleMemTest при- мера ObjsLeft, представляющий простую форму с кнопкой для вывода текущего счетчика выделений памяти и кнопкой для создания «утечки памяти» (которая обнаруживается при завершении программы). Листинг 8.1. Простой модуль для проверки «утечки памяти» из примера ObjsLeft unit SimpleMemTest: interface implementation uses Windows; var msg: string: 0388
Что далее? 389 initialization finalization if Al 1ocMemCount > 0 then begin Str (Al 1ocMemCount. msg): msg := msg + ' heap blocks left’; MessageBox (0. PChar(msg). 'Memory Leak'. MB_OK); end; end. ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- При написании программного кода, выполняющего проверки, обеспечивающего вспомогательные средства или изменяющего диспетчер памяти, необходимо избегать использования функций верх- него уровня, поскольку они могут сказываться на работе диспетчера памяти. Например, в модуль SimpleMemTest я не мог включить модуль SysUtils, поскольку он занимается выделением памяти. Вместо стандартного в Delphi преобразования IntToStr мне пришлось прибегать к традиционной функции Str языка Turbo Pascal. Эта программа удобна, но в действительности не позволяет понять, что идет не так. Для этой цели имеются мощные средства сторонних производителей (некото- рые из которых предлагают бесплатные ознакомительные версии). Кроме того, можно воспользоваться моим диспетчером памяти, отслеживающим выделение памяти (описан в приложении А). Что далее? После подробного рассмотрения форм в предыдущих главах мы сосредоточились на архитектуре приложений, рассмотрев, как работает Delphi-объект Application и как можно построить приложение с множеством форм. В частности, мы рассмотрели MDI, визуальное наследование форм и фреймы. Ближе к концу главы мы изучили настраиваемые архитектуры с наследованием форм и интерфейсами. А теперь мы можем перейти к следующему ключевому эле- менту нестандартных Delphi-приложений: построению пользовательских компо- нентов, которые могут использоваться в ваших программах. На эту тему можно написать целую книгу, поэтому описание не будет всеобъемлющим, но вам будет предоставлен всесторонний обзор этого вопроса. Еще один элемент, имеющий отношение к архитектуре Delphi-приложений, связан с пакетами, который я представлю как технологию, относящуюся к компо- нентам, но реально эта технология имеет более широкий спектр применений. Вы можете внедрить программный код крупных приложений во множество пакетов, содержащих формы и прочие модули. Разработка программ, основанных на мно- жестве исполняемых файлов, библиотек и пакетов, рассмотрена в главе 10. После этого мы перейдем к программированию баз данных, еще одному важно- му элементу среды разработки компании Borland и основному направлению рабо- ты большого числа программистов. 0389
9 Создание Delphi- компонентов Большинство Delphi-программистов, вероятно, хорошо знакомы с использовани- ем существующих компонентов, но иногда возникает необходимость создать соб- ственные компоненты или перенастроить существующие. Одним из наиболее ин- тересных аспектов языка Delphi является возможность создания компонентов, что по уровню сложности практически не отличается от написания программы. И хотя данная книга посвящена программированию приложений, а не разработке средств Delphi, в данной главе мы рассмотрим создание компонентов и вкратце затронем надстройки Delphi, такие как свойства компонентов и редакторы компонентов. Эта глава содержит краткое введение в разработку компонентов, а также ряд при- меров. Для представления сложных компонентов у нас нет места, но затрагиваемые здесь моменты являются базисом, являющимся неплохой отправной точкой. СОВЕТ----------------------------------------------------------------------- Создание компонентов, способных извлекать данные из баз данных, рассмотрено в главе 17. Данная глава охватывает следующие вопросы: О расширение библиотеки Delphi; О создание пакетов; О состав компонентов; О использование свойств интерфейсного типа; О определение пользовательских событий; О настройка существующих компонентов; О использование свойств-массивов; О помещение в компонент диалогового окна; О коллекции свойств; О создание редакторов свойств и редакторов компонентов. Расширение библиотеки Delphi Компоненты Delphi являются классами, а библиотека визуальных компонентов (VCL) — коллекцией всех классов, определенных в Delphi компонентов. Написа- нием новых компонентов в пакете и их установкой можно расширить VCL. Эти 0390
расширение библиотеки Delphi 391 новые классы будут производными от одного из существующих классов, относя- щихся к компонентам, или от исходного класса TComponent, добавляя к наследуе- мым возможностям новые. Новый компонент может исходить от существующего компонента или от аб- страктного класса компонента — класса, не имеющего готового к использованию компонента. Иерархическая структура VCL содержит множество этих промежу- точных классов (как правило, имеющих в имени префикс TCustom), позволяющих выбрать для нового компонента поведение по умолчанию и изменить его свойства. Пакеты компонентов Компоненты добавляются в пакеты компонентов. Каждый компонент обычно яв- ляется динамически подключаемой библиотекой (DLL) имеющей расширение BPL (сокращение от Borland Package Library). Пакеты бывают двух типов: пакеты времени разработки (design-time package), используемые IDE Delphi, и пакеты времени выполнения (run-time package), вы- борочно используемые приложениями. Тип пакета определяет параметр design- only package (пакет только для разработки) или run-only package (пакет только для выполнения). При попытке установить пакет IDE проверяет флаг пакета и реша- ет, позволить ли пользователю установить этот пакет или он должен быть добав- лен в список пакетов времени выполнения. Поскольку имеются два не взаимоиск- лючающих флага, каждый с двумя возможными состояниями, то существует четыре различных типа пакетов: два основных и два специальных: О пакеты «только для разработки» могут устанавливаться в среду Delphi. Эти пакеты обычно содержат части компонента, используемые в ходе разработки: свойства и код регистрации. Они зачастую содержат и сами компоненты, хотя это не самый профессиональный подход. Программный код пакетов компонен- тов «только для разработки» обычно статически скомпонован в исполняемый файл, используя соответствующие DCU-файлы (Delphi Compiled Unit) Delphi. Однако не забывайте, что существует техническая возможность использовать пакет «только для разработки» в качестве пакета для выполнения; О пакеты «только для выполнения», используемые Delphi-приложениями в ходе выполнения. Они не могут устанавливаться в среду Delphi, но автоматически добавляются в список пакетов периода выполнения, когда они требуются уста- новленным пакетом «только для разработки». Пакеты «только для выполне- ния» обычно содержат программный код классов компонентов, но не имеют поддержки в ходе разработки (это сделано для минимизации размера библио- теки компонента, поставляемой вместе с исполняемым файлом). Пакеты «толь- ко для выполнения» очень важны, поскольку они могут свободно распростра- няться совместно с приложением, но другие Delphi-программисты не смогут установить их в среду для создания новых программ; ° открытые пакеты компонентов (не имеющие флагов ни «только для разработ- ки», ни «только для выполнения») не могут быть автоматически установлены или добавлены в список пакетов времени выполнения. Этот вариант может от- носиться к пакетам утилит, используемых другими пакетами, но такие случаи крайне редки; 0391
392 Глава 9. Создание Delphi-компонентов О пакеты с установленными двумя флагами могут быть автоматически установ- лены в среду и добавлены в список пакетов времени выполнения. Обычно эти пакеты содержат компоненты, требующие незначительной (или вовсе не тре- бующей) поддержки времени разработки (не говоря уже о небольшом коде ре- гистрации компонента). ПРИМЕЧАНИЕ------------------------------------------------------------ Имена файлов собственных пакетов Delphi, предназначенных «только для разработки», начинают- ся с DCL (например, DCLSTD60.BPL); имена файлов «только для выполнения» начинаются с VCL (например, VCL60.BPL). При желании можно придерживаться этого подхода. В главе 1 мы рассматривали влияние пакетов на размер исполняемого файла программы. А теперь мы сосредоточимся на построении пакетов, поскольку это обязательный шаг в создании или установке компонентов Delphi. При компиляции пакета времени выполнения создаются как DLL с откомпи- лированным кодом (BPL-файл), так и файл, содержащий только идентификаци- онную информацию (DCP-файл) без машинного кода. Компилятор Delphi исполь- зует последний файл для получения идентификационной информации о модулях, входящих в пакет, не прибегая к обращению к файлу модулей (DCU), содержаще- му как идентификационную информацию, так и машинный код. Это сокращает время компиляции и позволяет распространять только пакеты, без предваритель- но откомпилированных файлов модулей. Файлы модулей по-прежнему требуют- ся для статической компоновки компонентов в приложении. Распространение пред- варительно откомпилированных DCU-файлов (или исходного программного кода) может быть рациональной в зависимости от типа разработанных компонентов. После того как мы рассмотрим общие руководящие принципы и построим компо- нент, вы увидите, как создается пакет. СОВЕТ-------------------------------------------------------------------- DLL — это исполняемые файлы, содержащие совокупности функций и классов, которые в ходе выполнения могут использоваться приложением или другой DLL. Их преимущество заключается в том, что если одна библиотека используется множеством приложений, на диске и в памяти доста- точно одной копии этой DLL, а размер каждого исполняемого файла может быть значительно сокра- щен. То же самое происходит и с пакетами Delphi. Боле подробно DLL рассматриваются в главе 10. Правила создания компонентов Написание компонентов подчиняется некоторым общим правилам. Подробное описание большинства из них можно найти в справочном файле Delphi Component Writer’s Guide, являющемся обязательным для прочтения разработчиками Delphi- компонентов. Вот моя собственная сводка правил: О внимательно изучите сам язык Delphi. Особенно важными концепциями явля- ются наследование, перекрытие методов (override) и перегрузка (overload), раз- личия между public- и published-разделами класса, а также определение свойств и событий. Если вы недостаточно знакомы с языком Delphi или базовыми кон- цепциями VCL, вы можете обратиться к общему описанию языка и библиоте- ки, представленной в первой части этой книги, особенно в главах 2 и 4; О изучите структуру иерархии классов VCL и держите под рукой схему классов; 0392
Расширение библиотеки Delphi 393 О придерживайтесь стандартных соглашений именования Delphi. Их соблюде- ние облегчает другим программистам взаимодействие с разработанными вами компонентами и их последующее расширение; О старайтесь сохранить компонент простым, подобным другим компонентам, и избегайте зависимостей. Эти три правила означают, что программист, исполь- зующий ваши компоненты, должен общаться с ними так же просто, как и со стандартными компонентами. Там, где возможно, используйте имена свойств, методов и событий, подобные стандартным. Если пользователям не придется изучать сложные правила использования компонента (то есть зависимости меж- ду методами и свойствами будут ограничены) и они смогут обращаться к свой- ствам посредством понятных имен, они будут счастливы; О используйте исключения. Если что-либо идет неверно, компонент сможет выз- вать исключение. При выделении любого типа ресурсов их необходимо защи- тить блоком try/finalLy или вызовом соответствующего деструктора; О при завершении создания компонента добавьте в него битовое изображение, которое будет использоваться для представления его в палитре компонентов. Если вы планируете, что ваш компонент будет использоваться другими людь- ми, создайте к нему файл справки; О будьте готовы к ручному написанию программного кода и забудьте о графиче- ских возможностях Delphi. Написание компонентов обычно подразумевает со- здание программного кода без визуальной поддержки (хотя функция Class Com- pletion может ускорить разработку классов). Исключением из этого правила является использование фреймов (для визуального написания компонентов). СОВЕТ----------------------------------------------------------------------- Кроме того, для создания или ускорения разработки компонентов можно воспользоваться предос- тавляемыми сторонними производителями средствами написания компонентов. Самым мощным ин- струментом является Class Developer Kit (CDK) компании Eagle Software (www.eagle-software.com). Базовые классы компонентов Создание нового компонента, как правило, начинается с существующего компо- нента или одного из базовых классов VCL. В любом случае компонент будет при- надлежать одной из трех категорий (представленных в главе 4), что определяется базовыми классами иерархии компонентов: О TWinControl — это родительский класс любого компонента, основанного на ис- пользовании окон. Компоненты, произведенные от этого класса, могут полу- чать фокус ввода и Windows-сообщения системы. При вызове API-функций также можно использовать их Windows-обработчики. При создании совершенно нового элемента управления он наследуется от производного класса TCustom- Control, который имеет ряд дополнительных полезных возможностей (особен- но касающихся «раскраски» элемента управления); О TGraphicControl — родительский класс визуальных компонентов, не имеющих Windows-обработчика (который экономит некоторые Windows-ресурсы). Эти компоненты не могут получать фокус ввода и непосредственно отвечать на со- общения Windows. При создании совершенно нового компонента он наел еду- 0393
394 Глава 9. Создание Delphi-компонентов ется непосредственно от этого класса (имеющего набор возможностей, очень подобный возможностям класса TCustomControl); О TComponent — родительский класс всех компонентов (включая элементы управ- ления) и может использоваться как непосредственный класс-предок для неви- зуальных компонентов. В остальной части главы мы построим несколько компонентов с использовани- ем различных родительских классов и рассмотрим разницу между ними. Давайте начнем с компонентов, наследуемых от существующих компонентов или классов, находящихся на нижнем уровне иерархии, а затем перейдем к примерам, исполь- зующим классы, унаследованные непосредственно от только что упомянутых клас- сов-предков. Создание вашего первого компонента Построение компонента является очень важной областью работы Delphi-програм- мистов. Всегда, когда возникает необходимость в одинаковом поведении в двух различных местах приложения или в двух различных приложениях, в класс мож- но добавить совместно используемый код, а лучше — в компонент. В этом разделе я представлю пару компонентов, чтобы вы представили требуе- мую последовательность действий. Кроме того, я покажу некоторые моменты, ко- торые необходимо реализовать для настройки существующего компонента при ограниченном объеме программного кода. Fonts Combo Box Многие приложения имеют панель инструментов с комбинированным списком, используемым для выбора шрифта. Если приходится часто использовать этот спи- сок, почему бы его не включить в компонент? Это займет менее одной минуты. Сначала закройте все активные проекты среды Delphi и запустите мастер Compo- nent Wizard либо выбором Component ► New Component, либо File ► New ► Other, что приведет к открытию Object Repository (хранилище объектов). А затем на страни- це New выберите Component. Для Component Wizard требуются следующие сведения: 0394
Создание вашего первого компонента 395 О имя типа-предка: класс компонента, от которого осуществляется наследование. В нашем случае укажите TComboBox; О имя класса нового компонента. Укажите TMdFontCombo; О страница палитры компонентов, на которой должен быть представлен новый компонент. Она может быть как новой, так и существующей страницей. Со- здайте новую страницу с именем MD. О имя файла модуля, в который будет помещен исходный программный код но- вого компонента; введите MDFontCombo; О текущий путь поиска (который должен установиться автоматически). Щелкните на кнопке ОК, и Component Wizard сгенерирует файл исходного кода, содержащий каркас компонента (см. листинг 9.1). Кнопка Install может использо- ваться для немедленного добавления компонента в пакет. Давайте сначала рас- смотрим программный код, а затем перейдем к его установке. Листинг 9.1. Класс TMDFontCombo, сгенерированный Component Wizard unit MdFontCombo; interface uses Windows, Messages. SysUtils, Classes. Graphics. Controls. Forms. Dialogs. StdCtrls: type TMdFontCombo = class (TComboBox) private I Private declarations } protected { Protected declarations } public { Public declarations } publi shed { Published declarations } end. procedure Register: implementation procedure Register; begin RegisterComponents(W. [TMdFontCombo]); end. end. Одним из ключевых элементов этого листинга является определение класса, который начинается с указания родительского класса. Еще одним значимым раз- делом является процедура Register. Как можно отметить, Component Wizard выпол- няет лишь незначительную работу. 0395
396 Глава 9. Создание Delphi-компонентов ВНИМАНИЕ---------------------------------------------------------------- Наименование процедуры Register должно быть написано с прописной буквы R. Это требование исходит от необходимости согласования с C++Builder (язык C++ чувствителен к регистру иденти- фикаторов). ПРИМЕЧАНИЕ------------------------------------------------------------------- При создании компонентов придерживайтесь соглашений именования. Все компоненты, устанавли- ваемые в Delphi, должны иметь неповторяющиеся имена классов. По этой причине многие разработ- чики Delphi-компонентов прибегают к добавлению к именам собственных компонентов 2-3-буквенного префикса. Для идентификации компонентов, созданных в данной книге, я сделал то же самое, добавив Md (сокращение от английского названия книги: Mastering Delphi). Преимущество такого подхода заключается в том, что вы можете установить мой компонент TMdFontCombo, даже если уже установлен компонент с именем TFontCombo. Обратите внимание, что имена модулей также должны быть уникальными для всех компонентов, установленных в системе, поэтому я использовал тот же префикс. Это все, что надо для создания компонента. Конечно же, этот пример не содер- жит никакого кода. Вам лишь необходимо при запуске скопировать все системные шрифты в свойство Items комбинированного списка. Для этого в определении клас- са попробуем перекрыть метод Create, добавив выражение Items := Screen.Fonts. Од- нако это неверный подход. Проблема заключается в том, что вы не сможете обра- титься к свойству Items списка до тех пор, пока доступен оконный обработчик компонента; компонент не может иметь оконный обработчик до тех пор, пока уста- новлено его свойство Parent; это свойство устанавливается не в конструкторе, а позже. Ввиду этого вместо назначения новых строк в конструкторе Create необходимо выполнить эту операцию в процедуре CreateWnd, вызываемой после создания ком- понента для создания оконного элемента управления; его свойство Parent установ- лено. А его оконный обработчик доступен. Опять же, вы реализуете стандартное поведение, а затем можете написать пользовательский программный код. Я мог бы пропустить конструктор Create и написать весь код в CreateWnd, но я решил исполь- зовать оба стартовых метода для демонстрации отличия между ними. Вот объяв- ление класса компонента: type TMdFontCombo = class (TComboBox) private FChangeFormFont Boolean. procedure SetChangeFormFont(const Value Boolean) public constructor Create (AOwner TComponent) override. procedure CreateWnd override. procedure Change override published property Style default csDropDownList. property Items stored False property ChangeFormFont Boolean read FChangeFormFont write SetChangeFormFont default True end А вот исходный код двух методов, выполняемых при запуске: constructor TMdFontCombo Create (AOwner TComponent). begin inherited Create (AOwner) 0396
Создание вашего первого компонента 397 Style .= csDropDownList. FChangeformFont .» True, end. \ \ procedure TMdFontCombo CreateWnd begin inherited CreateWnd. Items Assign (Screen Fonts) 11 захватить шрифт no умолчанию формы-владельца if FChangeFormFont and Assigned (Owner) and (Owner is TForm) then Itemindex = Items IndexOf ((Owner as TForm) Font Name) end. Обратите внимание, что помимо присвоения свойству Style компонента нового значения в методе Create, вы переопределяете свойство, определяя значение с клю- чевым словом default. Выполнение обеих операций обязательно, поскольку добав- ление default в свойство не оказывает непосредственного влияния на исходное значение свойства. Зачем после этого необходимо указать значение свойства по умолчанию? Потому что свойства, имеющие значение, равное значению по умол- чанию, не включаются в поток определения формы (и они не представлены в тек- стовом описании формы, в DFM-файле). Ключевое слово default сообщает коду поточного преобразования, что значение этого свойства будет устанавливать код инициализации компонента. ПРИМЕЧАНИЕ ----------------------------------------------------------------------------- Для сокращения размера DFM-файла, а особенно размера исполняемого файла, для pubhshed-свой- ства важно указать значение по умолчанию. Другое переопределенное свойство, Items, устанавливается как свойство, кото- рое вообще не должно сохранятся в DFM-файл, независимо от текущего значения. Это поведение определяется директивой stored, после которой стоит False. Компо- нент и его окно при запуске программы будут заново созданы, поэтому не имеет смысла сохранять в файл сведения, которые впоследствии будут отвергнуты (за- менены новым списком шрифтов). СОВЕТ---------------------------------------------------------------------- Копирование шрифтов в пункты комбинированного списка можно выполнить в программном коде метода CreateWnd только во время выполнения с помощью такого выражения, как if not (csDesignmg в Componentstate). Для первого создаваемого компонента это очень важно, но описанный мной более четкий метод обеспечивает ясную иллюстрацию базовой процедуры. Третье свойство, ChangeFormFont, не наследуется, а представляется самим ком- понентом. Оно используется для определения, будет ли выбранный в данный мо- мент в комбинированном списке шрифт определять шрифт формы, содержащей компонент. Это свойство объявлено со значением по умолчанию, которое устанав- ливается в конструкторе Свойство ChangeFormFont используется в программном коде метода CreateWnd (представленного ранее) для установки начального выделе- ния в зависимости от шрифта формы, содержащей этот компонент Обычно она указана в свойстве Owner компонента, хотя в поиске формы компонента я также 0397
398 Глава 9. Создание Delphi-компонентов мог пройтись по дереву Parent Этот код не является идеальным, но проверка Assigned и is обеспечивает некоторую дополнительную безопасность. Свойство ChangeFormFont и аналогичная проверка с использованием If играют ключевую роль в методе Changed, который в базовом классе запускает событие OnChange. Перекрытие этого метода обеспечивает стандартное поведение (которое можно отключить, изменив значение свойства), и также разрешает выполнение события OnChange таким образом, что пользователи этого класса могут полностью изменить его поведение. Заключительный метод, SetChangeFormFont, изменен та- ким образом, что теперь он обновляет шрифт формы только в том случае, когда свойство включено. Вот полный программный код: procedure TMdFontCombo Change begin // присвоить шрифт фюрне-впадепьцу if FChangeFormFont and Assigned (Owner) and (Owner is TForm) then TForm (Owner) Font Name = Text. inherited end procedure TMdFontCombo SetChangeFormFontCconst Value Boolean), begin FChangeFormFont = Value // обновить шрифт if FChangeFormFont then Change end Создание пакета Теперь вы должны установить компонент в среду с помощью пакета. В нашем при- мере вы можете либо создать новый пакет, либо использовать существующий. В любом случае выберите команду Component ► Install Component. Появившееся диалоговое окно имеет страницу, позволяющую установить компонент в существу- ющий пакет, а также страницу, позволяющую создать новый пакет В последнем случае введите имя файла и описание нового пакета. Щелчок на кнопке ОК приве- дет к открытию редактора Package Editor, состоящего из двух частей (рис. 9.1): О список Contains содержит компоненты, включенные в данный пакет (или, если быть более точным, модули, определяющие эти компоненты); О список Requires содержит пакеты, требуемые данным пакетом. Ваш пакет обыч- но требует пакеты rtl и vcl (основой пакет библиотеки времени выполнения и ос- новной пакет VCL), но также может потребоваться и vcldb-пакет (который со- держит большинство классов, связанных с базами данных), если компоненты нового пакета выполняют какие-либо операции с базами данных СОВЕТ----------------------------------------------------------------------— Начиная с Delphi 6, имена пакетов не зависят от версии, даже если откомпилированный пакет имеет номер версии в имени файла. Как это достигается технически, более подробно рассмотрено в раз- деле «Изменение имени проекта и библиотек» в главе 10. 0398
Создание вашего первого компонента 399 Package - testdpk Add Remove i Inst; lito_________ H £j Contains Path H Г..1 Requires McFontCornbo pa$| E \books\md7code\09\Mdpack S vcldcp Рис. 9.1. Package Editor Добавьте компонент в новый, только что определенный пакет, а затем отком- пилируйте и установите пакет (с помощью двух кнопок панели инструментов окна Package Editor); новый компонент немедленно появится на странице МО палитры компонентов. Процедура Register файла модуля компонента сообщила Delphi, куда необходимо установить новый компонент По умолчанию будет использовано би- товое изображение родительского класса, если вы не предоставили собственное изображение (мы сделаем это в последующих примерах). Обратите также внима- ние, что при перемещении указателя мыши над новым компонентом Delphi выво- дит подсказку с именем класса без начальной буквы Т Что такое пакет? Package Editor генерирует исходный программный код проекта пакета специаль- ный тип DLL, производимой Delphi Проект пакета сохраняется в файл с расшире- нием DPK (сокращение от Delphi Package), выводимом при нажатии клавиши F12 в редакторе пакетов. Обычный проект выглядит следующим образом. package MdPack {SR * RES} {SAL IGN ON} {SBOOLEVAL OFF} {SDEBUGINFD ON} {SDESCRJPHON Mastering Delphi Package'} {SIMPLIC1TBUILD ON} requires vcl contains MdFontBox in ’MdFontBox pas'. end Как можно заметить, для пакетов Delphi использует специальные ключевые слова. Первое из них, package (подобно ключевому слову library, рассматриваемо- му в следующей главе), определяет проект нового пакета Далее следует список 0399
400 Глава 9. Создание Delphi-компонентов всех параметров компилятора, некоторые из которых я упустил. Как правило, пара- метры Delphi-проекта хранятся в отдельном файле; но в отличие от этой практики пакеты содержат данные параметры непосредственно в собственном исходном тек- сте. Среди параметров компилятора имеется директива DESCRIPTION, используемая для доступности описания проекта в среде Delphi. После установки нового пакета это описание появится на странице Packages диалогового окна Project Options (эту стра- ницу также можно активизировать, выбрав пункт Component ► Install Packages). ' Project Options for dclwr7tLbpt Dseetaies/Condiiionals ] Description VsswWa I Packs;- Cornpta | Compilar Messages j - gAit otiwt t? RebuWas Deeded lie Рис. 9.2. Project Options для пакетов Помимо общих директив, таких как DESCRIPTION, существуют другие директи- вы, характерные только для пакетов (рис. 9.2). Основные из них доступны с по- мощью кнопки Options редактора Package Editor. После списка параметров следу- ют ключевые слова requires и contains, которые перечисляют пункты, визуально выводимые на двух страницах редактора Package Editor. Опять же, requires (требу- ет) представляет список пакетов, требуемых текущим пакетом, a contains (содер- жит) — список модулей, устанавливаемых данным пакетом. Давайте рассмотрим техническую сторону построения пакета. Помимо DPK- файла с исходным кодом, Delphi генерирует BPL-файл с динамически подключа- емой версией пакета и DCP-файл с идентификационной информацией. На самом деле этот DCP-файл является сводкой идентификационной информации DCU- файлов модулей, содержащихся в пакете. Во время разработки Delphi требует как BPL-, так и DCP-файлы, поскольку BPL-файл содержит код компонентов и идентификационную информацию, тре- буемую для реализации технологии code insight. При динамической привязке ком- понента (с помощью пакета времени выполнения) DCP-файл также будет исполь- зован компоновщиком, а BPL-файл должен поставляться совместно с основным исполняемым файлом приложения. Если вместо этого вы скомпонуете пакет ста- тически, то компоновщик обратится к DCU-файлам, и вы сможете распростра- нять только конечный исполняемый файл. 0400
Создание вашего первого компонента 401 Ввиду этого вы, как разработчик компонентов, должны в общем случае рас- пространять (предоставлять заказчику) по меньшей мере BPL-файл, DCP-файл и DCU-файл модулей, входящих в пакет и все соответствующие DFM-файлы плюс файл справки. В качестве дополнения также можно предоставить файлы исходно- го программного кода модулей пакета (PAS-файлы) и собственно пакета (DPK- файл). ВНИМАНИЕ --------------------------------------------------------------- По умолчанию Delphi помещает все откомпилированные файлы пакета (BPL и DCP) не в ту, папку, в которой находится исходный код пакета, а в каталог \Projects\BPL. Это облегчает их нахождение IDE и такое размещение не создает практически никаких проблем. Однако при необходимости ском- пилировать проект с использованием компонентов, объявленных в этих пакетах, Delphi может по- жаловаться, что не может найти соответствующие DCU-файлы, которые хранятся в каталоге, в котором находится исходный код пакета. Эту проблему можно решить, указав эту папку в пути библиотек (указываемом в Environment Options (параметры среды), относящиеся ко всем проектам) или указав ее в пути поиска текущего проекта (в Project Options). При использовании первого вари- анта размещение различных компонентов и пакетов в отдельном каталоге может сократить время доступа. Установка компонентов, созданных в данной главе Создав свой первый пакет, вы можете сразу приступить к его использова- нию. Однако перед этим я должен напомнить, что в пакет MdPack добавлены все компоненты, которые вы построите в этой главе, включая различные версии одного и того же компонента. Я предполагаю, что вы уже установили этот пакет. Наилучшим вариантом было бы скопировать его в ваш каталог, что обеспечит доступность его как в среде Delphi, так и программ, которые построены с его использованием. Я собрал все файлы исходного кода ком- понентов и определения пакета в одном подкаталоге, названном MdPack. При этом среда Delphi (или конкретный проект) при поиске DCU-файлов будет обращаться только к одному каталогу. Как уже упоминалось, все компонен- ты, представленные в этой книге, я мог бы собрать на веб-сайте в одной пап- ке; однако я решил, что для читателей будет более удобным упорядочива- ние примеров по главам. Не забывайте, что если вы компилируете приложение с использованием паке- тов в качестве библиотек времени выполнения, эти библиотеки необходимо также Устанавливать на компьютеры клиента. Если вместо этого откомпилировать про- граммы посредством статической компоновки модулей, содержащих компоненты, то пакеты понадобятся только среде разработки, а не приложениям пользователей. Использование Font Combo Box Давайте создадим новую Delphi-npoграмму, использующую Font Combo Box. В па- литре компонентов выберите новый компонент и добавьте его на новую форму. Появится элемент «комбинированный список», похожий на обычный. Однако если вы откроете редактор свойства Items, вы увидите список шрифтов, установленных на данном компьютере Для построения простого примера я добавил на форму 0401
402 Глава 9. Создание Delphi-компонентов компонент МЕМО и добавил в него текст. Оставив свойство ChangeFormFont вклю- ченным, ничего более писать не надо. В качестве альтернативы я мог бы выключить это свойство и обработать событие OnChange данного компонента подобным образом: Memol Font Name = MdFontCombol Text Целью этой программы является лишь проверка поведения только что создан- ного компонента. Он по-прежнему не очень полезен: того же эффекта можно дос- тичь, добавив несколько строк программного кода, но знакомство с парочкой ком- понентов поможет вам уловить саму идею построения компонентов. Значки в палитре компонентов Перед установкой компонента можно выполнить еще один шаг: определить изоб- ражение для использования его в палитре компонентов. Если этого не сделать, то в палитре будет использовано изображение из родительского класса или, если ро- дительский класс не является установленным компонентом, изображение объекта по умолчанию. Определение нового значка для компонента довольно простое, если вы знаете несколько правил. Его можно создать с помощью графического редакто- ра Image Editor, входящего в состав Delphi, начав новый проект и выбрав в качестве типа проекта Delphi Component Resource (DCR). ПРИМЕЧАНИЕ --------------------------------------------------------- DCR-файлы являются стандартными RES-файлами, но с измененным расширением имени. Если пред- почитаете, их можно создавать с помощью любого редактора ресурсов, включая Borland Resource Workshop, являющегося более мощным средством, чем встроенный графический редактор Delphi. По завершении создания файла ресурсов переименуйте расширение RES-файла в DCR. Файл ресурсов может содержать одно (или несколько) битовых изображений, каждое размером 24x24 пиксела. Единственное важное правило касается именова- ния. В данном случае правило именования является не просто соглашением, а требо- ванием, позволяющим IDE найти изображение для данного класса компонента: О имя ресурса-изображения должно совпадать с именем компонента, вклю- чая начальную Т. В нашем случае имя ресурса-изображения должно быть TMDFONTCOMBO. Имя должно состоять только из прописных букв — это обяза- тельное требование; О если требуется, чтобы Package Editor распознал и включил файл ресурса, имя DCR-файла должно совпадать с именем модуля компонента, в котором опреде- лен компонент. В нашем случае имя файла должно быть MdFontBox.dcr. Если вы подключаете файл ресурса вручную, с помощью директивы $R, то вы можете присвоить ему любое имя, оставить расширение RES и добавить в него множе- ство изображений. Когда изображение для компонента готово, можно с помощью кнопки Install Package Package Editor установить компонент в среду Delphi. После этой операции раздел Contains редактора будет содержать как PAS-файл компонента, так и соот- ветствующий ему DCR-файл. На рис. 9.3 представлены все файлы (включая DCR- файлы) окончательной версии пакета MdPack. Если DCR-инсталляция работает неверно, вы можете вручную добавить в исходный код пакета выражение {$R unitname. dcr}. 0402
Создание составных компонентов 403 Р<м kage - Mtlpack.dpk J в S3 : Add Орйегв Е. Contains MdActiveBln.pa$ '*** MdAnowda MdArrowpas MdCtock da MdDock.pes MdClockFwne MdCotect pas MdFontboxpas MdlnlfTestpas MdLsiAcf pas MdjslDial MduslDial dcr 1 НЯ McFersonaData. > MdSounB dcr MdSounB pas SJ Reqtwes Й ftLdcp 3 vcldcp Е \books\md7code\09\Mdpack Е Kbooks\md7code\09\Mdpack Е \books\md7code\09\Mdpdck Е \books\md7code\09\Mdpdck Е \booksVnd7code\09\Mdpdck Е Kbooks\md7code\09\Mdpack Е \booksVnd7code\09\Mdpack Е \bookAmd7code\09\Mdpack Е \books\md7code\09\Mdpack Е \booksVnd7code\09\Mdpack Е \booksVnd7code\09\Mdpdck Е \booksVnd7code\09\Mdpack Е \books\md7code\09\Mdpack Е \books\md7code\09\Mdpack Е \booksVnd7code\09\Mdpack Е \books\md7code\09\Mdpdck Е \books\md7code\09\Mdpack £ 1S 1 Рис. 9.3. Раздел Contains редактора Package Editor представляет как модули, включенные в пакет, так и файлы ресурсов компонента Создание составных компонентов Компоненты не существуют изолированно. Программисты довольно часто исполь- зуют одни компоненты совместно с другими компонентами, обеспечивая их взаи- модействие через один или несколько обработчиков событий. Альтернативный подход заключается в написании составных компонентов, которые могут инкап- сулировать эти отношения и упростить их обработку. Существует два типа состав- ных компонентов: О Внутренние компоненты создаются и управляются основным компонентом, который может открывать (expose) некоторые из их свойств и событий. О Внешние компоненты подключаются с использованием свойств. Такие состав- ные компоненты автоматизируют взаимодействие двух различных компонен- тов, которые могут находиться как на одной, так и на различных формах. В обоих случаях разработка ведется в соответствии со стандартными правилами. Третий, наименее исследованный вариант, заключается в разработке контей- неров компонентов, взаимодействующих с дочерними элементами управлений. Это более сложная тема и здесь рассматриваться не будет. Внутренние компоненты Следующий компонент представляет собой цифровые часы. Этот пример имеет несколько интересных особенностей. Во-первых, он внедряет один компонент (Timer) в другой компонент; во-вторых, он демонстрирует подход к «живым» дан- ным: вы увидите динамическое поведение (время будет обновляться) даже во время 0403
404 Глава 9. Создание Delphi-компонентов разработки, так же как это происходит с компонентами, способными извлекать информацию из баз данных. СОВЕТ----------------------------------------------------------------------—- Первая особенность стала более значимой после выхода Delphi 6, поскольку теперь инспектор объек- тов позволяет непосредственно открывать свойства подкомпонентов. Как результат— пример, пред- ставленный в этом разделе, был изменен (и упрощен) по сравнению с редакцией этой книги, посвященной Delphi 5. При необходимости я упомяну эти различия. Цифровые часы (часы с цифровым отсчетом) будут осуществлять вывод в тек- стовом режиме. Поэтому я решил унаследовать их от класса TLabel. Это позволит пользователю изменять заголовок надписи, то есть показания часов. Для того что- бы избежать этой проблемы в качестве родительского класса я использовал ком- понент TCustomLabel. Объект TCustomLabel имеет те же возможности, что и объект TLabel, но и еще ряд published-свойств. Другими словами, класс, унаследованный от TCustomLabel, может решать, какие свойства должны быть доступными, а какие остаться скрытыми. СОВЕТ------------------------------------------------------------------------------ Большинство Delphi-компонентов, особенно основанные на Windows, имеют базовый класс TCustom- XXX, реализующий общие функциональные возможности, но открывающий лишь ограниченный набор свойств. Наследование от этих базовых классов является стандартным способом открытия некото- рых из свойств компонента в пользовательской версии. Нельзя спрятать public- или published-свой- ства базового класса, пока вы не спрячете их определением нового свойства с тем же именем в классе-наследнике. В последних версиях Delphi компонент должен определить новое свойство Active, являющееся оболочкой для свойства Enabled компонента Timer. Оболочка свойства подразумевает, что методы get и set этого свойства осуществляют чтение и запись значения «охватываемого» свойства, которое принадлежит внутреннему компоненту (оболочка свойства обычно не имеет собственных данных). В нашем случае программный код выглядит следующим образом: function TMdClock GetActive Boolean begin Result = FTimer Enabled end procedure TMdClock SetActive (Value Boolean). begin FTimer Enabled = Value end Публикация подкомпонентов Начиная c Delphi 6, имеется возможность открывать весь подкомпонент (таймер) в собственном свойстве, которое будет раскрываться инспектором объектов, по- зволяя пользователю устанавливать каждое из подсвойств и даже обрабатывать их события. Вот полное объявление типа для компонента TMdClock с подкомпонентом, объяв- ленным в разделе частных данных, и с открытым в разделе published свойством (в последней стооке): 0404
Создание составных компонентов 405 type TMdClock = class (TCustomLabel) private FTimer TTimer. protected procedure UpdateClock (Sender TObject) publ i c constructor Create (AOwner TComponent) override. published property Align property Al i gnment. property Color. property Font property Parentcolor, property ParentFont. property ParentShowHint, property PopupMenu property ShowHint property Transparent property Visible property Timer TTimer read FTimer end. Свойство Timer является свойством только для чтения, поскольку я не хочу, чтобы пользователь выбирал для этого компонента в инспекторе объектов иное значение (или нарушал работу компонента очисткой значения этого свойства). Разработка набора альтернативно используемых подкомпонентов, конечно же, возможна, но добавление поддержки записи для данного свойства безопасным спо- собом является совершенно нетривиальной задачей (учтите, что пользователи ва- шего компонента не являются экспертами в Delphi-программировании). Поэтому я советую оставить для свойств подкомпонентов атрибут «только для чтения» Для создания компонента Timer необходимо перекрыть конструктор компонен- та Clock. Метод Create вызывает соответствующий метод базового класса и создает объект Timer, устанавливая обработчик для его события OnTimer: Object Inspector |MdOocld Hook Propettie® | Events | PatentFont (true Parents hawHir! T rue PopupMenu ShowHint [False Tao |0 E Timet I Es-aUed I Werva’ L ! Top I Transparent I Visible j Width (All shown |MdClor» ’ True 1000 <0 И 20 Fake True 42 Рис. 9.4. Инспектор объектов может автоматически раскрыть подкомпоненты, покатывая иу свойства, как в случае свойства Timer компонента TMdClock 0405
406 Глава 9. Создание Delphi-компонентов constructor TMdClock.Create (AOwner: TComponent); begin inherited Create (AOwner); // создать внутренний объект timer FTimer = TTimer.Create (Self): FTimer Name .= ‘ClockTimer'; FTimer OnTimer = UpdateClock; FTimer Enabled = True, FTimer SetSubComponent (True): end: Программный код дает объекту имя, используемое для представления его в ин- спекторе объектов (рис. 9.4) и вызывает метод SetSubComponent. В данном случае деструктор не требуется; объект FTimer в качестве владельца имеет TMDClock (что указано в параметре конструктора Create), поэтому он будет уничтожен автомати- чески при уничтожении компонента Clock. СОВЕТ--------------------------------------------------------------------------------- В представленном выше программном коде вызов метода SetSubComponent устанавливает внутрен- ний флаг, сохраняемый в свойстве Componentstyle. Этот флаг (csSubComponent) оказывает влияние на поточную систему, позволяя сохранить подкомпонент и его свойства в DFM-файле. По умолча- нию система поточной передачи игнорирует компоненты, не принадлежащие данной форме. Ключевым фрагментом программного кода компонента является процедура UpdateClock, состоящая лишь из одного выражения: procedure TMdLabelClock UpdateClock (Sender. TObject): begin // установить в заголовке текущее время Caption = TimeToStr (Time); end: Этот метод использует неопубликованное свойство Caption, недоступное для изменения пользователем в инспекторе объектов. Это выражение осуществляет непрерывный вывод времени, поскольку данный метод подключен к событию OnTimer компонента Timer. СОВЕТ--------------------------------------------------------------------------------- События подкомпонентов могут редактироваться в инспекторе объектов, и пользователь может обрабатывать их. При внутренней обработке события, как это было сделано в TMdLabelClock, пользо- ватель может перекрыть это поведение посредством обработки определенного события (в данном случае — OnTimer). В общем случае решение заключается в определении для внутреннего компо- нента производного класса, перекрывающего его виртуальные методы (как метод Timer класса TTimer). В нашем случае эта технология работать не будет, поскольку Delphi активизирует таймер только в том случае, если к нему присоединен обработчик события. Если перекрыть виртуальный метод и не предоставить обработчик события (что было бы верным для подкомпонента), то таймер работать не будет. Внешние компоненты Когда компонент обращается к внешнему компоненту, то он не создает этот компо- нент (вот почему он называется внешним). Это программист, использующий ком- поненты, отдельно создает их обоих (например, «перетаскивая» компонент с па- литры в форму) и соединяет их, используя одно из их свойств. Поэтому можно 0406
Создание составных компонентов 407 сказать, что свойство компонента ссылается на подключаемый извне компонент. Это свойство должно иметь тип класса, унаследованный от TComponent. Для демонстрации я создал невизуальный компонент, который может выво- дить в элемент управления «надпись» сведения о человеке и автоматически об- новлять эти данные. Компонент имеет следующие опубликованные свойства: type TMdPersonalData = class(TComponent) publ1 shed property FirstName: string read FFirstName write SetFirstName: property LastName: string read FLastName write SetLastName: property Age: Integer read FAge write SetAge: property Description: string read GetDescription: property OutLabel: TLabel read FLabel write SetLabel: end. Имеются некоторые основные сведения плюс свойство Description, имеющее атрибут «только для чтения», которое возвращает сразу все сведения. Свойство OutLabel связано с локальным частным полем, названным FLabel. В программном коде компонента посредством этой внутренней ссылки FLabel я использовал вне- шний элемент «надпись»: procedure TMdPersonalData.UpdateLabel; begin if Assigned (FLabel) then FLabel.Caption := Description: end: Этот метод UdpateLabel запускается каждый раз, когда изменяется одно из свойств (рис. 9.5 — в ходе разработки): Object Inspector | MdPeisonalDatal TMcFeiwiaP a-“a PropeJltes | Eye ' ' Ай» .. * .-t-*, RrstName ... лJjggiatasJ........... Name ' iMdRemwaiOatai , BOtfUbat ....iLabeli . Afck. .„„ jaW»». _ , ..... GJAncho® [akteft.akTop] jAutaShe J? Twa t jbdLeftToRigb' j (6) CUor, .. . л! AS shown Рис. 9.5. Во время разработки компонент обращается к внешней надписи Procedure TMdPersonalData.SetFirstName(const Value- string): begin if FFirstName <> Value then begin 0407
408 Глава 9. Создание Ре1рЫ-компоненток FFirstName .= value; UpdateLabel: end. end: Конечно же, нельзя использовать надпись, если она не назначена; отсюда сле- дует необходимость предварительной проверки этого факта. Однако эта проверка не гарантирует, что надпись можно будет использовать после ее уничтожения (как во время разработки, так и во время выполнения). При создании компонента со ссылкой на внешний компонент необходимо в разрабатываемом компоненте (ком- понент с внешней ссылкой) перекрыть метод Notification. Этот метод запускается когда создается или уничтожается родственный компонент (имеющий того же вла- дельца). Рассмотрим случай, когда класс TMdPersonalData получает уведомление (notification) об уничтожении (opRemove) компонента Label: procedure TMdPersonalData Notification! AComponent TComponent. Operation- TOperation). begin inherited; if (AComponent = FLabel) and (Operation = opRemove) then FLabel ,= nil. end. Этого программного кода достаточно, чтобы избежать проблем с компонента- ми в одной и той же форме или конструкторе (например модуле данных), посколь- ку при уничтожении компонента его владелец уведомляет об этом все «свои» ком- поненты (родственников уничтоженного компонента). Однако для учета всех компонентов, подключенных через форму или модуль данных, необходимо выпол- нить еще один шаг. Каждый компонент имеет внутренний список уведомления, состоящий из одного и более компонентов, которых он должен предупредить об уничтожении. Ваш компонент должен добавить себя в список уведомления тех компонентов, к которым он подключен (в данном случае к «надписи»), посред- ством вызова метода FreeNotification. Таким образом, даже если внешняя надпись находится на другой форме, она сообщит компоненту об уничтожении запуском метода Notification (который уже обработан и не нуждается в обновлении): procedure TMdPersonalData SetLabel(const Value. TLabel). begin if FLabel <> Value then begin FLabel = Value. if FLabel <> ml then begin UpdateLabel. FLabel FreeNotification (Self): end: end. end. ПРИМЕЧАНИЕ-----------------------------------------------------------------------------— Для автоматического подключения компонента при добавлении его в ту же форму или конструктор можно также использовать обратное уведомление (opinsert). Я не знаю, почему эта методика так редко используется, но она весьма полезна во многих ситуациях. Она более рациональна для созда- ния специальных редакторов свойств и компонентов для операций времени разработки, чем специ- альный программный код, внедряемый в компоненты. 0408
Создание составных компонентов 409 Обращение к компонентам с помощью интерфейсов При обращении к внешним компонентам вы будете традиционно ограничены иерар- хией. Например, компонент, построенный в предыдущем разделе, может обращать- ся только к объектам класса TLabel или к его производным классам, хотя он мог бы выводить сведения и в другие компоненты. В Delphi 6 добавлена поддержка инте- ресной возможности, которая может провести коренное изменение некоторых об- ластей VCL: компонентные ссылки интерфейсного типа. СОВЕТ--------------------------------------------------------- Эта особенность недостаточно используется в Delphi 7. Вероятно, она появилась слишком поздно, чтобы обновить архитектуру компонентов, относящихся к работе с базами данных на основе интер- фейсов. Все, на что приходится надеяться, зто то, что она будет использована для выражения сложных взаимоотношений между библиотеками в будущем. Если имеются компоненты, поддерживающие данный интерфейс (даже если они не являются частью одной ветви иерархии), вы можете объявить свойство интерфейсного типа и назначить его любому из этих компонентов. Например, пред- положим, что имеется невизуальный компонент, присоединенный для вывода к элементу управления, подобный рассмотренному в предыдущем разделе. Я ис- пользовал традиционный подход и подключил компонент к «надписи», но теперь можно определить интерфейс: type IMdViewer = interface ['{97668600-8E4A-4254-9843-59898FEE6C54}'] procedure View (const str- string). end. Компонент может использовать интерфейс viewer для осуществления вывода в другой элемент управления (любого типа). В листинге 9.2 представлено, как объя- вить компонент, использующий этот интерфейс для обращения к внешнему ком- поненту. Листинг 9.2. Компонент, обращающийся к внешнему компоненту с помощью интерфейса type TMdlntfTest - class(TComponent) private FViewer IViewer. FText string. procedure SetViewer(const Value IViewer). procedure SetTextfconst Value: string): protected procedure NotificationfAComponent TComponent: Operation TOperation). override: publi shed property Viewer: IViewer read FViewer write Setviewer: property Text, string read FText write SetText: end. { TMdlntfTest } Procedure TMdlntfTest NotificationfAComponent TComponent; Operation. TOperation). 0409
410 Глава 9. Создание Delphi-компонентов var Intf: IMdViewer; begin inherited; if (Operation = opRemove) and (Supports (AComponent, IMdViewer. Intf)) and (intf - FVIewer) then begin FVIewer := nil; end; end: procedure TMdIntfTest.SetText(const Value: string); begin FText Value: if Assigned (FVIewer) then FVIewer.View(FText); end; procedure TMdIntfTest.SetV1ewer(const Value: IMdViewer); var i Comp; IInte rfaceComponentRe fe rence; begin if FVIewer <> Value then begin FVIewer := Value; FVIewer.Vlew(FText): if Supports (FVIewer. HnterfaceComponentReference. IComp) then 1Comp.GetComponent.FreeNoti f1cat1on(Seif); end; end; Применение интерфейса по сравнению с традиционным использованием типа класса для обращения к внешнему компоненту влечет два значимых отличия. Во- первых, в методе Notification необходимо извлечь интерфейс из компонента, пере- даваемого в качестве параметра, и сравнить его с уже имеющимся интерфейсом. Во-вторых, для вызова метода FreeNotification необходимо посмотреть, поддержи- вает ли объект, передаваемый в качестве параметра, интерфейс linterfaceCompo- nentReference. Он объявлен в классе TComponent и обеспечивает способ обратного обращения к этому компоненту (GetComponent) и вызова его методов. Без него при- шлось бы добавлять аналогичный метод в пользовательский интерфейс, посколь- ку при извлечении интерфейса из объекта отсутствует автоматический способ об- ратного обращения к объекту. Теперь, когда вы имеете компонент с интерфейсным свойством, его можно при- своить любому компоненту (из любого раздела иерархии VCL) посредством до- бавления в него интерфейса IViewer и реализацией метода View. Вот пример: type TViewerLabel = class (TLabel. IViewer) public procedure V1ew(str: String); end: procedure TViewerLabel.View(const str: String): begin Caption :- str; end: 0410
Создание составных компонентов 411 Построение составных компонентов с фреймами (рамками) Вместо построения составных компонентов в программном коде и последу- ющего ручного ассоциирования их с событием таймера, того же эффекта можно достичь с помощью фреймов. Фреймы делают разработку составных компонентов визуальной операцией, а, следовательно, значительно упроща- ют ее. Можно обеспечить совместное использование фрейма добавлением его в Repository или созданием шаблона с помощью команды Add to Palette контекстного меню фрейма. В качестве альтернативы вы можете обеспечить совместное использова- ние фрейма помещением его в компонент и регистрацией как компонента. Технически эта технология не так сложна: вы добавляете процедуру Register в модуль фрейма, добавляете модуль в пакет и компонуете его. Новый ком- понент/фрейм, как и другие компоненты, появится в панели компонентов. В конструкторе форм нельзя извлечь подкомпоненты с помощью щелчка мыши, но это можно сделать с помощью Object TreeView. Однако любое изме- нение, производимое в этих компонентах во время разработки, будет поте- ряно при выполнении программы или сохранении/загрузки формы, посколь- ку изменения этих подкомпонентов в отличие от стандартных фреймов, помещаемых в форму, не учитываются при образовании потока. Если вас это не устраивает, я нашел разумный способ использования фреймов в пакетах, продемонстрированный с помощью компонента MdFra- medClock (часть примеров данной главы находится на сайте Sybex). Ком- поненты, которыми владеет фрейм, вызовом метода SetSubComponent пре- вращаются в подкомпоненты. Я также открыл внутренние компоненты со свойствами, хотя этот шаг не обязателен (они могут быть выбраны в Object TreeView). Вот объявление компонента и программный код его ме- тодов: type TMdFramedClock = class(TFrame) Label 1: TLabel; Timerl: TTimer; Bevel 1: TBevel: procedure T1merlTimer(Sender: TObject); public constructor Create!AOwner: TComponent); override; publi shed property SubLabel: TLabel read Label 1: property SubTimer: TTimer read Timerl: end constructor TMdFramedClock.Create!AOwner: TComponent); begin inherited; Timerl.SetSubComponent (True): Label 1.SetSubComponent (True); end:procedure TMdFramedClock.TimerlTimer(Sender: TObject); begin Label 1.Caption := TimeToStr (Time); end; ---- продолжение 0411
412 Глава 9. Создание Ре1рЫ-компонентов Построение составных компонентов с фреймами (рамками) {продолжение) В отличие от созданного ранее компонента clock, нет необходимости ус- танавливать свойства таймера или вручную подключать событие таймера к функции-обработчику, поскольку это производится визуально и сохраня- ется в DFM-файл фрейма. Обратите также внимание, что я не открывал ком- понент Bevel (не вызывал для него метод SetSubComponent.) Я делал это лишь для того, чтобы вы могли поэкспериментировать с его неверным поведени- ем: попытайтесь отредактировать его в ходе разработки и убедитесь, что эти изменения будут потеряны, о чем я упоминал ранее. После установки этого фрейма/компонента его можно использовать в лю- бом приложении. В этом случае, как только вы поместите компонент на фор- му, таймер будет обновлять его текущим значением времени Однако по- прежнему можно обработать его событие OnTimer и IDE среды Delphi (поняв, что этот компонент находится во фрейме) создаст метод с ранее определен- ным программным кодом: procedure TForml MdFramedClocklTimerlTimerlSender TObject) begin MdFramedClockl TimerlTimer(Sender) end Как только будет подключен таймер (даже в ходе разработки), «живые» часы остановятся, поскольку оригинальный обработчик события окажется отключенным. Однако после компиляции и запуска программы исходное поведение будет восстановлено (по крайней мере, если вы не удалите пре- дыдущую строку); дополнительный про!раммный код также будет выпол- няться. Это поведение именно то, что вы и ожидали от фреймов Завершен- ную версию использования этого фрейма-компонента можно найти в примере FrameClock. Данный подход все еще очень далек от прямолинейного. Но он значи- тельно лучше, чем предлагаемые в последних версиях Delphi, где фреймы из пакетов непригодны для использования и не стоят затрат. Если вы работаете отдельно или в составе небольшой команды, лучше использовать открытые фреймы, хранящиеся в Repository. В больших организациях или при распро- странении фреймов на большую аудиторию, большинство людей предпоч- тут создавать свои компоненты традиционным способом, не прибегая к ис- пользованию фреймов. Я надеюсь, что компания Borland обеспечит более полную поддержку визуальной разработки упакованных компонентов, ос- нованных на фреймах. Сложный графический компонент В этом разделе я покажу, как создать графический компонент Arrow (стрелка). Подобный компонент можно использовать для указания направления потока ин- формации или действия. Этот компонент достаточно сложный, поэтому вместо 0412
Сложный графический компонент 413 представления готового программного кода я покажу различные этапы его созда- ния. Заключительный вариант компонента я добавил в пакет MdPack, который де- монстрирует несколько важных концепций: О определение новых перечисляемых свойств, основанных на перечисляемых типах данных; О реализация метода Paint компонента, который обеспечивает пользовательский интерфейс и должен быть достаточно общим для удовлетворения возможных значений различных свойств, включая Width и Height. Метод Paint играет в гра- фическом компоненте весьма значимую роль; О использование свойств классов, являющихся производными от класса TPersistent (например, ТРеп и TBrush), а также выдача и обработка внутри компонента отно- сящихся к ним событиям OnChange, событиям создания и уничтожения; О определение пользовательского обработчика события компонента, который реагирует на ввод пользователя (в данном случае, двойной щелчок на стрелке). Это требует непосредственной обработки Windows-сообщений и использова- ния API Windows для графической области. Определение перечисляемого свойства После генерации нового компонента с помощью мастера Component Wizard и выбо- ра в качестве родительского класса TGгаphicControl, можно приступать к настройке компонента. Стрелка может указывать в любом из четырех направлений: вверх, вниз, влево и вправо. Перечисляемый тип указывает доступные варианты: type TMdArrowDir = (adUp adRight adDown adLeft) Этот перечисляемый тип определяет член частных данных компонента — пара- метр процедуры, используемой для его изменения, а также тип соответствующего свойства. Свойство ArrowHeight определяет размер «наконечника» стрелки, а свойство Filled указывает, должна ли выполняться заливка наконечника определенным цветом: type TMdArrow = class (TGraphicControl) private FDirecti on TMdArrowDir FArrowHeight Integer FFilled Boolean. procedure SetDirecti on (Value TMd4ArrowDir). procedure SetArrowHeight (Value Integer). procedure SetFilled (Value Boolean), published property Width default 50. property Height default 20 property Direction TMd4ArrowDir read FDirection write SetDirection default adRight, property ArrowHeight Integer read FArrowHeight write SetArrowHeight default 10 property Filled Boolean read FFilled write SetFilled default False. 0413
414 Глава 9. Создание Delphi-компонентов СОВЕТ----------------------------------------------------------------------—- Графический элемент управления не имеет определенного по умолчанию размера, поэтому при поме- щении его на форму его размер составит один пиксел. Поэтому для свойств Width и Height важно добавить значение по умолчанию и в конструкторе класса установить поля класса в эти значения. Три пользовательских свойства с помощью трех методов Set осуществляют не- посредственное чтение из соответствующих полей, а также их запись. Все они имеют стандартную структуру: procedure TMdArrow SetDirecti on (Value TMdArrowDir). begin if FDirecti on <> Value then begin FDirecti on - Value. ComputePoints Invalidate end end Обратите внимание, что вы просите систему перерисовать компонент (посред- ством вызова Invalidate) только в том случае, если свойство действительно изме- нило свое значение, или после вызова метода ComputePoints, который осуществля- ет вычисление треугольника, составляющего «наконечник». В противном случае этот код пропускается, и производится немедленное завершение выполнения ме- тода. Структура этого программного кода одинакова и вы можете использовать ее для большинства процедур Set свойств. Необходимо также не забыть в конструк- торе компонента установить значения свойств по умолчанию: constructor TMdArrow Create (AOwner TComponent) begin // вызвать конструктор родителя inherited Create (AOwner) 11 установить значения по умолчанию FDirecti on = adRight, Width = 50. Height = 20 FArrowHeight = 10. FFilled = False. Как уже упоминалось, значения по умолчанию, указанные в объявлении свой- ства, используются только для определения, будут ли значения свойства сохране- ны на диск. Конструктор Create определен в public-разделе определения типа ново- го компонента и этот конструктор отмечен ключевым словом override, поскольку он перекрывает виртуальный конструктор класса TComponent. Очень важно не за- быть спецификатор override; в противном случае, когда среда Delphi создает новый компонент этого класса, она вызовет базовый конструктор класса, а не тот конст- руктор, который вы определили в производном классе. Соглашения именования свойств Обратите внимание на использование некоторых соглашений именования свойств, методов доступа и полей при определении компонента Arrow Вот их сводка: 0414
Сложный графический компонент 415 • свойство должно иметь внятное и понятное имя; • при использовании частного поля данных для хранения значения свой- ства это поле должно иметь имя, начинающееся с прописной буквы F (от field) сопровождаемой именем соответствующего свойства, • при использовании процедуры для изменения свойства эта функция в начале должна иметь слово Set, сопровождаемое именем соответствую- щего свойства; • функция, используемая для чтения свойства, должна в начале иметь сло- во Get, сопровождаемое именем соответствующего свойства. Это не требования, а лишь руководящие принципы, позволяющие облег- чить восприятие ваших программ. Они не определяются компилятором. Эти соглашения описаны в книге Delphi Component Writers’ Guide и их придер- живается механизм class completion. Написание метода Paint Рисование стрелки в различных направлениях и с различными стилями требует значительного объема программного кода. Для выполнения пользовательского изображения вы заменяете метод Paint и используете защищенное свойство Canvas. Вместо вычисления положения точек «наконечника» стрелки в программном коде «рисования», который будет исполняться довольно часто, я написал отдель- ную функцию, рассчитывающую область наконечника и хранящую ее в массиве точек, определенном среди частных полей компонента FArrowPoints array [0 3] of TPoint Эти точки определяются private-методом ComputePoints, который вызывается каж- дый раз при изменении свойств компонента. Вот фрагмент его программного кода- procedure TMdArrow ComputePoints var XCenter YCenter Integer begin // вычислить точки «наконечника» стрелки VCenter = (Height - 1) div 2 XCenter = (Width - 1) div 2 case FDirecti on of adUp begin FArrowPoints [0] = Point (0 FArrowHeight) FArrowPoints [1] = Point (XCenter 0) FArrowPoints [2] = Point (Width 1 FArrowHeight) end II и т д для остальных направлений Этот код рассчитывает центр области компонента (делением пополам значе- ний свойств Height и Width), а затем использует этот центр для определения поло- жения наконечника. Помимо изменения направления других свойств, положение Наконечника необходимо обновлять и при изменении размеров компонента. При изменении свойств Left, Top, Width и Height компонента можно перекрыть метод SetBounds компонента, вызываемый VCL- 0415
416 Глава 9. Создание Delphi-компоненток procedure TMdArrow SetBounds(ALeft ATop AWidth AHeight Integer) begin inherited SetBounds (ALeft ATop AWidth AHeight) ComputePoints end После того как компонент знает местоположение наконечника, код его рисова- ния становится более простым. Вот фрагмент метода Paint: procedure TMdArrow Paint var XCenter YCenter Integer begin // вычислить центр YCenter = (Height - 1) div 2 XCenter = (Width - 1) div 2. // нарисовать линию стрелки case FDirecti on of adllp begin Canvas MoveTo (XCenter Height-1), Canvas LineTo (XCenter FArrowHeight) end Пит д для других направлений end // нарисовать точку стрелки в конечном счете заполнив ее if FFilled then Canvas Polygon (FArrowPoints) else Canvas PolyLine (FArrowPoints) end Результат работы примера представлен на рис. 9.6. Рис. 9.6. Вывод компонента Arrow Добавление свойств TPersistent Для того чтобы сделать вывод компонента более удобным, я добавил в него два новых свойства, Реп и Brush, определенных в типе класса (типе данных TPersistent, 0416
Сложный графический компонент 417 определяющем объекты, которые Delphi может автоматически представить в виде потока). Эти свойства более сложны для обработки, поскольку теперь компонент должен сам создавать и уничтожать эти внутренние объекты. Однако сейчас вы можете с помощью свойств импортировать объект, позволяя пользователю непос- редственно изменять внутренние объекты с помощью инспектора объектов. Для обновления компонента при изменении подобъектов необходимо также обраба- тывать его внутреннее свойство OnChange. Вот определение свойства Реп и дру- гие изменения в определении класса компонента (для свойства Brush код ана- логичен): type TMdArrow = class (TGraphicControl) private FPen TPen procedure SetPen (Value TPen) procedure RepaintRequest (Sender TObject) published property Pen TPen read FPen write SetPen end Сначала вы создаете в конструкторе объект и устанавливаете его обработчик события OnChange: constructor TMdArrow Create (AOwner TComponent) begin // создать pen и brush FPen = TPen Create // настроить обработчик для события OnChange FPen OnChange = RepaintRequest end События OnChange запускаются при изменении одного из свойств реп; все, что необходимо сделать, — это запросить у системы перерисовку вашего компонента: procedure TMdArrow RepaintRequest (Sender TObject) begin Invalidate end Для компонента необходимо также добавить деструктор, осуществляющий Уничтожение графического объекта из памяти (и освобождение системных ресур- сов). Все, что должен сделать деструктор, — вызвать метод Free объекта Реп. Свойство, связанное с постоянными объектами, требует специальной обработ- ки: вместо копирования указателя на объект необходимо скопировать внутренние Данные объекта, передаваемые в качестве параметра. Стандартная операция := ко- пирует указатель, поэтому в данном случае необходимо использовать метод Assign: ProcedureTMdArrow SetPen (Value TPen) begin FPen Assign(Value) Invalidate end Многие классы TPersistent имеют метод Assign, который необходимо использо- вать при необходимости обновления данных этих объектов. Теперь же для исполь- зования оеп пои поооисовке необходимо изменить метод Paint, поисвоив перед 0417
418 Глава 9. Создание Delphi-компонентов прорисовкой линии соответствующему свойству Canvas компонента значение внут- реннего объекта (см. результат на рис. 9.7): procedure TMdArrow Paint begin // использовать текущий pen Canvas Pen = FPen Поскольку Canvas для присвоения (Assign) объекта pen использует процедуру- установщик, вы не просто сохраняете ссылку на реп в поле Canvas, а копируете все его данные. Это означает, что можно спокойно уничтожить локальный объект Реп (FPen), и что модификация FPen не повлияет на полотно до тех пор, пока не будет вызван Paint и не будет повторно выполнен представленный выше программный код. .ж I Рис. 9.7. Вывод компонента Arrow толстым карандашом и кистью со штриховкой Определение события Для окончательной подготовки компонента Arrow давайте добавим к нему собы- тие. Практически всегда новый компонент использует события родительского клас- са. Например, в данном компоненте я сделал доступными ряд стандартных собы- тий, объявив их в published-разделе класса’ type TMdArrow = class (TGraphicControl) published property OnClick property OnDragDrop property OnDragOver property OnEndDrag Благодаря этому описанию данные события (изначально объявленные в клас- се-предке) после установки компонента будут доступны в инспекторе объектов. Однако иногда для компонента необходимы специальные (настраиваемые) со- бытия. Для определения нового события сначала необходимо убедиться, что уже существует тип указателя метода, подходящий к данному событию, если нет, не- обходимо определить новый тип события Этот тип является типом указателя ме- 0418
Сложный графический компонент 419 тода (см. главу 5). В обоих случаях необходимо добавить в класс поле типа собы- тия: вот определение, которое я добавил в private-раздел класса TmdArrow: FArrowDblClick TNotifyEvent Я использовал тип TNotifyEvent, который имеет единственный параметр Sender и используется Delphi в большинстве случаев, включая события OnClick и OnDblClick. Используя это поле, я определил опубликованное свойство с прямым доступом к этому полю: property OnArrowDblClick TNotifyEvent read FArrowDblClick write FArrowDblClick (Обратите внимание на использование стандартного соглашения именований, при котором имена событий начинаются с On.) Указатель метода fArrowDblClick ак- тивизирован (выполнением соответствующей функции) внутри специального ди- намического метода ArrowDblClick. Это происходит только в том случае, если обра- ботчик события указан в программе, использующей компонент. procedure TMdArrow ArrowDblClick begin if Assigned (FArrowDblClick) then FArrowDblClick (Self) end ПРИМЕЧАНИЕ------------------------------------------------------------------------ Использование Self в качестве параметра вызова метода обработчика события гарантирует, что при вызове методом его параметра Sender будет действительно происходить обращение к объекту, запустившему это событие, что и требуется. Использование низкоуровневых вызовов API Windows Метод fArrowDblClick определен в разделе protected определения типа, что позволя- ет последующим производным классам как вызывать, так и изменять его. Обычно этот метод вызывае!ся обработчиком сообщения Windows wm_LbuttonDblClk, но только в том случае, если имел место двойной щелчок внутри стрелки. Для про- верки этого условия можно использовать некоторые функции региона (region) API Windows. СОВЕТ----------------------------------------------------------------— Регион — это часть экрана, охватываемая любой фигурой. Например, с помощью трех вершин ука- занного стрелкой треугольника можно построить многоугольный регион. Единственная проблема заключается в том, что для правильного заполнения этой поверхности необходимо в направлении по часовой стрелке определить массив TPoints (подробности этого странного подхода см. в описании CreatePolygonaIRgn в справочной системе API Windows). Именно это я сделал в методе ComputePoints. После того как определен регион, для проверки, попадали точка двойного щел- чка внутрь него, можно использовать API-вызов PtlnRegion. Полный программный код этой процедуры Procedure TMdArrow WMLButtonDblClk ( var Msg TWMLButtonDblClk) // сообщение wm LButtonDblClk var HRegion HRgn begin !I выполнить стандартную обработку 0419
420 Глава 9. Создание Delphi-компонентов inherited. 11 вычислить регион наконечника HRegion = CreatePolygonRgn (FArrowPoints 3. WINDING). try // проверить, попал ли щелчок в данный регион if PtlnRegion (HRegion. Msg XPos. Msg YPos) then ArrowDblClick. finally DeleteObject (HRegion). end, end CLX-версия: вызов родных функций Qt Представленный выше программный код не может быть перенесен в Linux и не учитывает CLX/Qt-версию компонента. Если есть необходимость построить ана- логичный компонент для библиотеки классов CLX, то необходимо заменить API- вызовы Win32 на непосредственные (низкоуровневые) вызовы слоя QT, создав объект класса QRegion: procedure TMdArrow DblClick, var HRegion QRegionH MousePoint TPoint, begin // выполнить стандартную обработку inherited. И вычислить регион наконечника HRegion = QRegion_create (PPointArray(FArrowPoints). True); try // получить текущее положение указателя мыши GetCursorPos (MousePoint). MousePoint = ScreenToCllent(MousePoint) // проверить, попадает ли он в регион if QRegionjcontains(HRegion. PPoint(@MousePoint)) then ArrowDblClick, finally ORegion_destroy(HRegion). end. end. Регистрация категорий свойств Вы уже добавили к данному компоненту ряд свойств и новое событие. Если в инс- пекторе объектов упорядочить свойства по категориям, все новые элементы по- явятся в категории Miscellaneous (Прочие). Конечно же, это далеко от идеала, но вы можете легко зарегистрировать новые свойства в одной из доступных категории. Регистрация свойства (или события) в категории может осуществляться вызо- вом одной из перезагруженных версий функции RegisterPropertylnCategory, определенной в модуле Designlntf. При вызове этой функции указывается имя категории и имя свойства, его тип или имя свойства и компонент, к которому оно принадлежит. Например, для реги- страции события OnArrowDblClick в категории Input и свойства Filled в категории VisualB про- цедуру Register модуля можно добавить следующие строки: uses Designlntf 0420
Сложный графический компонент 421 procedure Register. begin RegisterPropertyInCategory ('Input'. TMdArrow 'OnArrowDblClick'). RegisterPropertyInCategory ('Visual', TMdArrow 'Filled'}, end. Первый параметр — это строчное значение, указывающее имя категории (это проще, чем исходный подход Delphi 5 к использованию классов категорий). Мож- но определить новую категорию простейшим образом, передав его имя в качестве первого параметра функции RegisterPropertylnCategory: RegisterPropertylnCategory ('Arrow', TMdArrow 'Direction'}. RegisterPropertylnCategory ('Arrow', TMdArrow. ‘ArrowHeight ') Создание новой категории для определенных свойств компонента позволяет облегчить пользователю нахождение необходимых характеристик. Обратите вни- мание, что хотя вы полагаетесь на модуль Designlntf, необходимо компилировать модуль, содержащий регистрацию в пакет разработки, а не в пакет времени выпол- нения (требуемый модуль Designlde нельзя распространять). Поэтому я написал программный код в модуле, отдельном от модуля, в котором определяется компо- нент, и добавил новый модуль (MdArrReg) в пакет MdDesPk, все модули которого предназначены только для периода разработки. Такой подход рассмотрен далее, в разделе «Установка редактора свойств». ВНИМАНИЕ ------------------------------------------------------------------------------- Является ли правильным использование категории специальных свойств, по-прежнему остается спорным вопросом. С одной стороны, пользователь компонента сможет легко найти необходимое свойство. Хотя некоторые новые свойства могут не попадать ни в одну из существующих категорий. С другой стороны, категории могут быть излишними. Если в каждом компоненте будет представле- но несколько категорий, то пользователи просто запутаются. Кроме того, существует опасность, что категорий станет столько же, сколько и свойств. Object Inspector □ MdArrowl Properties | Everts | Йдёйоп ЭЛггг-w 1 E (TBrirthJ \ crDelault__________________ I SecT QB3 3 [ Heigh! 161 pdTshowr> //, ₽nc. 9.8. Компонент «стрелка» определяет новую категорию свойств. Обратите внимание, что свойства могут быть повторно представлены в различных разделах (см. свойство Filled) 0421
422 Глава 9. Создание Delphi-компонентов Обратите внимание, что регистрация свойства Filled осуществляется в двух раз- личных категориях. В этом нет ничего странного, поскольку одно и то же свойство может быть представлено множество раз в инспекторе объектов в различных груп- пах (рис. 9.8). Для проверки компонента Arrow я написал программу ArrowDenio, которая позволяет изменять большинство ее свойств во время выполнения. После написания компонента или во время его создания этот тип испытания очень важен. СОВЕТ------------------------------------------------------------------—_ Категория свойств Localizable играет особенную роль, связанную с использованием Интегрирован- ной среды перевода (Integrated Translation Environment, ITE). Если свойство входит в эту категорию, то его значение будет представлено в среде перевода как свойство, которое должно быть переве- дено на другой язык. Настройка элементов управления Windows Один из самых распространенных путей настройки существующих компонентов заключается в добавлении необходимой реакции в их обработчики событий. Каж- дый раз, когда необходимо присоединить один и тот же обработчик события к ком- понентам различных форм, требуется добавление кода события в класс-потомок компонента. Очевидный пример — это строки редактирования, которые воспри- нимают только ввод чисел. Вместо применения обычного обработчика события OnChar к каждой строке редактирования, можно определить новый компонент. Однако этот компонент не будет обрабатывать событие; события предназначе- ны только для пользователей компонента. Вместо этого компонент может либо непосредственно обрабатывать сообщение Windows, либо перекрыть этот метод, который часто называется обработчиком сообщения второго уровня. Ранее исполь- зовалась другая технология, но она приводила к тому, что компонент становится специфичным для платформы Windows. Для создания компонента, который мож- но перенести на CLX и Linux, а в будущем — и на .NET архитектуру, необходимо избежать использования низкоуровневых сообщений Windows, и вместо этого пере- крыть виртуальные методы базового компонента и классов элементов управления. СОВЕТ------------------------------------------------------------ Когда большинство VCL-компонентов обрабатывали Windows-сообщения, они вместо непосредствен- ного исполнения программного кода собственным методом обработки сообщений вызывали обра- ботчики сообщений второго уровня (как правило, динамические методы). Этот подход облегчает настройку компонента в классах-потомках. Обычно обработчик второго уровня будет выполнять свою собственную работу, а затем вызывать любой обработчик события, назначенный пользовате- лем компонента. Поэтому для того, чтобы компонент запускал событие как ожидалось, всегда необ- ходимо вызывать inherited. Помимо обеспечения переносимости на другие платформы есть и другие при- чины, почему перекрытие существующего обработчика второго уровня лучше, чем непосредственная обработка сообщений Windows. Во-первых, эта методика более подходила к объектно-ориентированной перспективе. Вместо дублирования кода реакции на события основного класса и его последующей настройки, производит- ся перекрытие вызова виртуального метода; это именно то, что разработчики VCL 0422
Настройка элементов управления Windows 423 и планировали. Во-вторых, если кому-либо потребуется получить класс-потомок из одного из классов, то вы должны, насколько это возможно, обеспечить простоту настройки, и наименее вероятно, что перекрытие обработчиков второго уровня вызовет ошибки (лишь из-за того, что придется написать меньший объем программ- ного кода). Например, обрабатывая системное сообщение wm_Char, я мог опреде- лить следующий элемент управления Numeric Edit Box: type TMdNumEdlt = class (TCustomEdit) public procedure WmChar (var Msg: TWmChar): message wm_Char: Однако этот код будет более перемещаем, если я заменю метод KeyPress, как это сделано в программном коде следующего компонента. (В последующем примере я должен буду обрабатывать пользовательские сообщения Windows, поскольку не существует соответствующего перекрываемого метода.) Numeric Edit Box Для того чтобы строка редактирования ограничивала воспринимаемые при вводе значения, необходимо лишь перекрыть ее метод KeyPress, вызываемый при получе- нии компонентом Windows-сообщения wm_Char. Вот код класса TmdNumEdit: type TMdNumEdlt = class (TCustomEdit) private FInputError: TNotifyEvent: protected function GetValue: Integer: procedure SetValue (Value: Integer): procedure KeyPress(var Key: Char): override. public constructor Create (Owner: TComponent): override: published property OnlnputError: TNotifyEvent read FInputError write FInputError: property Value: Integer read GetValue write SetValue default 0; property AutoSelect: property AutoSjze: // И Т.Д. ... Этот компонент вместо TEdit наследует TCustomEdit, поэтому он может спрятать свойство Text и представить вместо него свойство целого типа Value. Обратите вни- мание, что для хранения этого значения не создается новое поле, поскольку можно Использовать существующее (но теперь неопубликованное) свойство Text. Для этого осуществляется преобразование числового значения в текстовую строку и обрат- но- Класс TCustomEdit (на самом деле, элемент управления Windows, для которого °и является оболочкой) автоматически выводит в экранном представлении ком- понента сведения из свойства Text: function TMdNumEdlt.GetValue: Integer: begin II установить в 0 в случае ошибки Result :- StrToIntDef (Text. 0): end: 0423
424 Глава 9. Создание Delphi-компонентов procedure TMdNumEdit.SetValue (Value: Integer): begin Text := IntToStr (Value): end: Самым важным методом является переопределение метода KeyPress, который филь- трует все нечисловые символы и в случае ошибки запускает специальное событие: procedure TMdNumEdit.KeyPress (var Msg: TWmChar); begin if not (Key in ['0'.. '9']) and not (Key = #S) then begin Key := #0: // ничего не нажималось if Assigned (FInputError) then FInputError (Self); end el se inherited: end; Этот метод проверяет каждый вводимый пользователем символ, допуская на- жатие лишь числовых клавиш и клавиши Backspace (имеющей ASCII-код 8). При этом пользователь, помимо системных клавиш (стрелки управления курсором и клавиша Del), должен иметь возможность использовать клавишу Backspace, по- этому необходимо проверять и это значение. А теперь поместим этот компонент на форму, введем что-либо в строке редак- тирования и посмотрим на ее поведение. Вероятно, вам захочется добавить к со- бытию OnlnputError еще один метод, осуществляющий «обратную связь» с пользо- вателем при нажатии неверной клавиши. Numeric Edit с разделителями тысяч В качестве дальнейшего расширения этого примера рассмотрим случай, когда пользователь вводит большие числовые значения (хранимые как числа с плаваю- щей точкой, которые по сравнению с целыми числами могут принимать большие значения и иметь дробную часть). Было бы неплохо, если бы при вводе автомати- чески появлялись и обновлялись разделители тысяч: Этого можно достичь перекрытием внутреннего метода Change и соответствую- щим форматированием числа. Существует парочка проблем. Первая заключается в том, что для форматирования числа необходимо иметь строчное значение, содер- жащее число, но текст в строке редактирования не является числовой строкой, рас- познаваемой Delphi, поскольку он имеет разделители тысяч и не может быть не- посредственно преобразован в число. Я написал измененную версию функции StringToFloat, выполняющую необходимое преобразование, назвав ее StringToFloat- Skipping. 0424
Настройка элементов управления Windows 425 Вторая небольшая проблема заключается в том, что при изменении текста в стро- ке редактирования текущее положение курсора будет потеряно. Следовательно, необходимо сохранить исходное положение курсора, переформатировать число, а затем восстановить положение курсора, учитывая добавление разделителя. Все эти рассуждения реализованы в следующем программном коде класса Tmd- ThousandEdit: type TMdThousandEdit = class (TMdNumEdlt) public procedure Change; override; end; function StringToFloatSkipping (s; string): Extended; var si: string; I; Integer; begin 11 удалить нечисловые символы si := for i := 1 to length (s) do if s[i] in ['O’..’9’] then si .= si + s[1]; Result := StrToFloat (si). end; procedure TMdThou s and Ed11.Change; var CursorPos. // исходное положение курсора LengthDiff: Integer. // число новых разделителей^ or -) begin if Assigned (Parent) then begin CursorPos = SelStart; LengthDiff .= Length (Text); Text •= FormatFloat (’#.###’. StringToFloatSkipping (Text)); LengthDiff ;= Length (Text) - LengthDiff; // поставить курсор в требуемое положение SelStart := CursorPos + LengthDiff; end; inherited. end: Кнопка Sound Следующий компонент, TMdSoundButton, воспроизводит один звук при нажатии кнопки и другой — при ее отпускании. Пользователь определяет каждый звук из- менением двух строчных свойств, указывающих на два соответствующих WAV- файла. Вам еще раз необходимо перехватить некоторые системные сообщения (wm_LButtonDown и wm_LButtonUp) и перекрыть их соответствующими обработчи- ками второго уровня. Ниже представлен программный код класса TMdSoundButton с двумя защищенны- ми методами и двумя свойствами строчного типа, идентифицирующими звуковые 0425
426 Глава 9. Создание Delphi-компонентов файлы. Они отображаются на частные поля, поскольку нет необходимости выпол- нять какие-либо операции при изменении этих свойств пользователем: type TMdSoundButton = class(TButton) private FSoundUp. FSoundDown: string; protected procedure MouseDown(Button- TMouseButton: Shift: TShiftState: X. Y: Integer); override; procedure Mousellp(Button TMouseButton; Shift- TShiftState. X. Y: Integer): override; publi shed property SoundUp; string read FSoundUp write FSoundUp.- property SoundDown string read FSoundDown write FSoundDown; end. Вот программный код одного из методов: uses MMSystem; procedure TMdSoundButton MouseDown(Button TMouseButton; Shift: TShiftState; X. Y: Integer): begin inherited MouseDown (Button. Shift. X. Y); PlaySound (PChar (FSoundDown). 0. snd_Async). end. Обратите внимание, что вызов унаследованной версии методов производится до того, как будет сделано что-либо еще. Для большинства обработчиков второго уровня это правильный подход, поскольку он гарантирует, что стандартное пове- дение будет реализовано до пользовательского поведения. Далее обратите внима- ние, что для воспроизведения звука можно вызвать функцию PlaySound API Win32. С помощью этой функции (объявленной в модуле MmSystem) можно воспроизвес- ти как WAV-файлы, так и системные звуки (см. пример SoundB). Вот текстовое описание формы этого примера (из DFM-файла): object MdSoundButtonl TMdSoundButton Caption = 'Press' SoundUp = 'RestoreUp' SoundDown = 'RestoreDoMi' end СОВЕТ------------------------------------------------------------------------------------- Выбор соответствующего значения для звуковых свойств не так прост. Далее в этой главе мы рас- смотрим, как в этот компонент добавить редактор свойств, упрощающий эту операцию. Обработка внутренних сообщений: Active Button Интерфейс Windows постепенно развивается, переходя на новые стандарты (на- пример, выделение компонента при помещении над ним указателя мыши). Delphi обеспечивает аналогичную поддержку в большинстве встроенных компонентов. Копирование этого поведения для кнопки может показаться сложной задачей, но это не так. Разработка компонента может стать значительно проще, если вы знае- 0426
Настройка элементов управления Windows 427 те, какую виртуальную функцию необходимо перекрыть, или какое сообщение перехватить. Следующий компонент, класс TMdActiveButton, демонстрирует эту технологию. Для выполнения задачи самым простым способом он обрабатывает некоторые внут- ренние сообщения Delphi. (Сведения о том, откуда берутся внутренние сообще- ния Delphi, см. в следующем разделе.) Компонент ActiveButton обращается к внут- ренним сообщениям Delphi cm_MouseEnter и cm_MouseExit, которые поступают при входе или выходе курсора мыши из области, соответствующей компоненту: type TMdActiveButton = class (TButton) protected procedure MouseEnter (var Msg. TMessage); message cmjnouseEnter. procedure MouseLeave (var Msg TMessage). message cmjnouseLeave. end. Программный код, который вы напишете для этих двух методов, может делать все, что вы захотите. В данном примере я решил переключать «зажирнение» шрифта кнопки. Эффект перемещения мыши над одним из этих компонентов представлен на рис. 9.9. procedure TMdActiveButton MouseEnter (var Msg. TMessage). begin Font Style .= Font Style + [fsBold], end. procedure TMdActiveButton MouseLeave (var Msg: TMessage): begin Font.Style .= Font Style - [fsBold]: end. Active Button Demo MdAclweBui’orn MdActiveBuitonz MdAchveButton3 Рис. 9.9. Пример использования компонента ActiveButton 0427
428 Глава 9. Создание Delphi-компонентов Можно добавить и другие эффекты, включая увеличение размера шрифта или небольшое увеличение размеров кнопки. Наилучшим эффектом обычно является изменение цвета, но для этого необходимо унаследовать класс TBitBtn (элемент управления TButton имеет фиксированный цвет). Сообщения и уведомления компонента Для создания компонента ActiveButton использовались два внутренних сообще- ния Delphi-компонента (на что указывал их префикс ст). Эти сообщения могут быть достаточно интересными (см. выделения в примере), но они совершенно не документированы компанией Borland. Существует также и вторая группа внут- ренних сообщений Delphi, обозначаемых как уведомления компонента и отличае- мых по их префиксу сп. У меня нет возможности рассмотреть каждое из них или представить подробный анализ; если вы хотите узнать больше, просмотрите ис- ходный код VCL. ВНИМАНИЕ --------------------------------------------------------- Это достаточно сложный вопрос, поэтому не стесняйтесь пропустить этот раздел, если вы новичок в разработке Delphi-компонентов. Сообщения компонентов не документированы в справочной сис- теме Delphi, поэтому я посчитал необходимым лишь перечислить их. Сообщения компонента Delphi-компонент передает сообщения компонента другим компонентам для ука- зания каких-либо изменений в его состоянии, которые могут повлиять на эти ком- поненты. Большинство из этих сообщений начинаются как Windows-сообщения, но некоторые из них являются более сложными, более высокого уровня преобра- зования. Кроме того, компоненты посылают собственные сообщения, а также пе- ресылают сообщения, полученные от Windows. Например, изменение значения свойства или ряда других характеристик одного компонента может привести к ге- нерации сообщения об этом одному и более компонентам. Эти сообщения можно разделить на следующие категории: О сообщения активизации и фокуса ввода посылаются к активизируемому или деактивизируемому компоненту, получающему или теряющему фокус ввода: cm_Activate cm_Deactivate cm_Enter cm_Exit cm_FocusChanged Соответствует событию OnActivate формы и приложения Соответствует событию OnDeactivate Соответствует событию OnEnter Соответствует событию OnExit Посылается, если фокус изменился между компонентами одной и той же формы (далее будет рассмотрен пример использования этого сообщения) cm_GotFocus cm_LostFocus Объявлено, но не используется Объявлено, но не используется О сообщения, направляемые дочерним компонентам при изменении свойства: cm_BiDiMod eChanged cm_BorderChanged cm_ColorChanged cm_Ctl3DChanged cm_CursorChanged cm_IconChanged cm_ShowHintChanged cm_Sho wi ngChanged cm_SysFontChanged cm_Tat>StopChanged 0428
Настройка элементов управления Windows 429 cm_EnabledChanged cm_TextChanged cm_FontChanged cm_VisibleChanged Наблюдение за этими сообщениями может помочь отследить изменения свой- ства. Возможно, в новом компоненте придется отвечать на эти сообщения, но не всегда; О сообщения, относящиеся к свойствам ParentXxx: cm_ParentFontChanged, cm_Parent- ColorChanged, cm_ParentCtl3DChanged, cm_ParentBiDiModeChanged и cm_ParentShow- HintChanged. Они подобны сообщениям предыдущей группы; О уведомления (извещения) об изменениях в системе Windows: cm_SysColorChange, cm_WinIniChange, О cm_TimeChange и cm_FontChange. Обработка этих извещений полезна только в спе- циальных компонентах, которые должны отслеживать системные цвета и шрифты; О сообщения мыши: cm_Drag многократно посылается в ходе операций перетас- кивания. cm_MouseEnter и cm_MouseLeave посылаются элементу управления, когда указатель мыши входит или покидает его поверхность, но они посылаются объектом Application как низкоприоритетные. cm_MouseWheel относится к дей- ствиям с колесом прокрутки мыши. Сообщения приложения: cm_AppKeyDown Посылается к объекту Application для того, чтобы позволить ему определить, соответствует ли данная клавиша «горячей» клавише меню cm_AppSysCommand Соответствует сообщению wm_SysCommand cm_DialogHandle Отправляется в DLL для извлечения значения свойства DialogHandle (используемого некоторыми диалоговыми окнами, построенными не в Delphi) cmJnvokeHelp Отправляется программным кодом в DLL для вызова метода InvokeHelp cm_WindowHook Отправляется в DLL для вызова методов HookMainWindow и UnhookMainWindow Вы редко будете использовать эти сообщения. Существует также сообщение cm_HintShowPause, которое никогда не обрабатывается в VCL; О внутренние сообщения Delphi: cm_CancelMode Прекращает специальные действия, такие как вывод раскрывающегося списка элемента «комбинированный список» cm_ContrOlChange Посылается каждому элементу управления перед добавлением или удалением дочернего элемента управления (обрабатывается рядом общих элементов управления) cm_ControlListChange Посылается каждому элементу управления перед добавлением или удалением дочернего элемента управления (обрабатывается компонентом DBCtrIGrid) cm_DesignHitTest Определяет, должно ли действие мыши поступать в компонент или в конструктор форм cm_HintShow Отправляется в элемент управления сразу перед выводом его подсказки (только если свойство ShowHint имеет значение True) cm_HitTest Отправляется в элемент управления в том случае, когда родительский элемент управления пытается найти дочерний элемент в данном положении указателя мыши (если таковой имеется) cm_MenuChanged Отправляется после операции слияния меню MDI или OLE 0429
430 Глава 9. Создание Delphi-компонентов О сообщения, связанные со специальными клавишами: cm_ChildKey Отправляется в родительский элемент управления для обработки некоторых специальных клавиш (в Delphi это сообщение Обрабатывается компонентами DBCtrIGrid) cm_DialOgChar Отправляется в элемент управления для определения, является ли данная клавиша символом ускоренного вызова cm_DialogKey Обрабатывается модальными формами и элементами управления, которым необходимо выполнить специальные действия cm_lsShortCut В настоящее время не используется (поскольку зачастую просто вызывается IsShortCut), но оно предназначено для определения, известно ли это сочетание и поддерживается ли оно формой посредством события OnShortCut, пунктом меню или действием cm_WantSpecia 1 Key Обрабатывается элементами управления, интерпретирующими нажатие специальных клавиш невизуальным способом (например, использование клавиши Tab для перемещения, как делают некоторые компоненты Grid) О сообщения определенных компонентов: cm_GetDataUnk Используется элементами управления DBCtrIGrid (рассматривается в главе 17) cm_Ta bFontCha nged cm_Button Pressed Используется компонентом TabbedNotebook Используется SpeedButtons для предупреждения других родственных компонентов SpeedButton (для обеспечения характерного поведения переключателей) cm_DeferLayout Используется компонентами DBGrid О сообщения OLE-контейнера: cm_DocWindowActivate, cmJsToolControl, cm_Release, cm_UIActivate и cm_UIDeactivate; О сообщения, относящиеся к стыковке (docking), включая cm_DockClient, cm_Dock- Notification, cmFloat и cm_UndockClient; О сообщения реализации методов, такие как cm_RecreateWnd, вызываемое внутри метода RecreateWnd класса TControl; cm_Invalidate, вызываемое внутри TControl. Invalidate; cm_Changed, вызываемое внутри TControl.Changed; и cm_AUChildrenFlipped вызываемое в методах DoFlipChildren классов TWinControl и TScrollingWinControl. В эту же группу попадают два сообщения действий, относящиеся к спискам: cm_ActionUpdate и cm_ActionExecute. Уведомления компонентов Сообщения уведомления компонентов — это сообщения, отправляемые родитель- ской формой или компонентом своим потомкам. Эти уведомления соответствуют сообщениям, отправляемым Windows окну родительского элемента управления, но логически предназначены для самих элементов управления. Например, взаи- модействие с таким элементом управления, как кнопка, строка редактирования или список, приводит к тому, что Windows отправляет сообщение wm_Command родительскому элементу управления. Когда Deiphi-программа получает такое со- общение, она пересылает его самому элементу управления в качестве уведомле- ния. Элемент управления Delphi может обработать это сообщение и в конечном счете запустить событие. Подобные операции диспетчеризации имеют место и для многих других сообщений. Связь между Windows-сообщениями и сообщениями уведомления компонен- та является настолько сильной, что вы зачастую будете узнавать имя Windows- 0430
Настройка элементов управления Windows 431 сообщения из имени сообщения-уведомления, заменяя начальные сп на wm. Су- ществует несколько групп сообщений уведомлений компонентов: О общие сообщения клавиатуры: cn_Char, cn_KeyUp, cn_KeyDown, cn_SysChar и cn_Sys- KeyDown; О специальные сообщения клавиатуры, используемые только списками со сти- лем WantKeyboardlnput: cn_CharToItem и cn_VkeyToItem; О сообщения, связанные с технологией owner-draw (прорисовка владельцем): cn_CompareItem, cn_DeleteItem, cn_DrawItem и cn_MeasureItem; О сообщения для прокрутки, используемые только элементами scroll bar и track bar: cn_HScroll и cn_Vscroll; О общие сообщения уведомления, используемые большинством элементов управ- ления: cn_Command, cn_Notify и cn_ParentNotify; О сообщения, связанные с цветом элемента управления: cn_CtlColorBtn, cn_CtlColor- Dlg, cn_CtlColorEdit, cn_CtlColorListbox, cn_CtlColorMsgbox, cn_CtlColorScrollbarH cn_Ctl- ColorStatic. Кроме того, для общей под держки элементов управления определены и другие уведомления (в модуле ComCtrls). Пример сообщений компонента В качестве примера использования сообщений компонента я написал программу CMNTest. Она имеет форму с тремя строками редактирования и связанными с ними надписями. Первое сообщение, которое опа обрабатывает, cm_DialogKey, позволяет ей трактовать клавишу Enter как клавишу Tab. Программный код этого метода про- веряет код клавиши Enter и посылает такое же сообщение, но передает код клави- ши vk_Jab. Для остановки последующей обработки клавиши Enter производится установка результата сообщения в 1: procedure TForml CMDialogKey(var Message TCMDialogKey). begin if (Message CharCode = VK_RETURN) then begin Perform (CMJhalogKey. VK_TAB. 0). Message Result = 1. end else inherited. end. Второе сообщение, cm_DialogChar, отслеживает клавиши ускоренного вызова. Эта технология может быть полезной для реализации пользовательских «горячих» клавиш без определения для них дополнительного меню. Обратите внимание, что хотя этот программный код является верным для компонента, в обычном прило- жении этого же можно добиться значительно проще, обрабатывая событие OnShort- Cut формы. В данном случае осуществляется «протоколирование» специальных клавиш в надпись: Procedure TForml CMDialogChar (var Msg TCMOialogChar) begin Label 1 Caption = Label 1 Caption + Char (Msg CharCode). 0431
432 Глава 9. Создание Delphi-компонентов inherited; end: И, наконец, в ответ на изменение фокуса вместо обработки события OnEnter каж- дого из своих компонентов форма обрабатывает сообщение cm_FocusChanged. Опять же, это действие выводит описание компонента, попавшего в фокус: procedure TForml.CmFocusChanged(var Msg: TCmFocusChanged): begin Label5.Caption := 'Focus on ' + Msg.Sender.Name: end; Преимуществом такого подхода является то, что он работает независимо от типа и количества компонентов формы и действует безо всяких специальных усилий с вашей стороны. Опять же, это банальный пример для столь сложного вопроса, но если вы добавите к нему код компонента ActiveButton, то у вас появится несколь- ко причин разобраться в этих недокументированных сообщениях. Временами на- писание такого же программного кода без использования этих сообщений может стать весьма сложной задачей. Диалоговое окно в компоненте Следующий компонент, который мы рассмотрим, коренным образом отличается от того, с чем мы работали до сих пор. После построения компонентов, основан- ных на окнах, и графических компонентов мы рассмотрим, как построить невизу- альный компонент. Основная идея заключается в том, что форма — это тоже компонент. После того как вы создали форму, которая может быть полезной для нескольких проектов, вы можете добавить нее в Object Repository (хранилище объектов) или создать из нее компонент. Второй подход более сложен, но он упрощает использование новой формы, а также позволяет распространять форму без ее исходного кода. В качестве примера я построил компонент, основанный на диалоговом окне, стараясь, насколь- ко это возможно, повторить поведение стандартных диалоговых окон Delphi. Первый шаг в построении диалогового окна в компоненте заключается в напи- сании программного кода самого диалогового окна на основе стандартного подхо- да Delphi. Просто определите новую форму и работайте с ней как обычно. Когда компонент основан на форме, вы почти визуально создаете компонент. Конечно же, после того как диалоговое окно создано, необходимо на его основе невизуаль- ным способом определить компонент. Стандартное диалоговое окно в этом примере основано на списках, поскольку это общепринятая практика, позволяющая пользователю выбрать значение из спис- ка строк. Я изменил это стандартное поведение, а затем использовал его для созда- ния компонента. Форма ListBoxForm имеет список и обычные кнопки ОК и Cancel (см. текстовое описание): object MdListBoxForm: TMdListBoxForm BorderStyle = bsDIalog Caption - 'ListBoxForm' object ListBoxl: TListBox OnDblClick = ListBoxlDblCTick end 0432
Диалоговое окно в компоненте 433 object BitBtnl: TBitBtn Kind = bkOK end object BitBtn2: TBitBtn Kind - bkCancel end end Единственный метод этой формы связан с двойным щелчком на списке. Этот метод закрывает диалоговое окно так же, как если бы пользователь щелкнул на кнопке ОК, устанавливая свойство ModalResult формы в mrOk. После того как форма готова, вы можете приступать к изменению ее исходного кода, добавляя определе- ние компонента и удаляя объявление глобальных переменных формы. СОВЕТ------------------------------------------------------------------------------- Для компонентов, основанных на форме, можно использовать два файла исходного кода на языке Pascal: один — для формы, а другой — для включающего ее компонента. Также имеется возмож- ность поместить как форму, так и компонент в один модуль, как я и сделал в данном примере. Теоретически, лучше определять класс формы в implementation-разделе этого модуля, скрывая, таким образом, его от пользователей компонента. Но на практике это не очень хорошая идея. Для визуального манипулирования формой в конструкторе форм определение класса формы должно быть в interface-разделе модуля. Основная причина такого подхода заключается в том, что помимо прочего, это ограничение минимизирует объем программного кода, который предстоит просмот- реть менеджеру модуля для того, чтобы найти объявление формы — операции, которая выполняет- ся довольно часто для синхронизации визуального представления формы с определением класса формы. Наиболее важной операцией является объявление компонента ТМdListBoxDialog. Этот компонент определяется как невизуальный, поскольку его непосредствен- ным предком является класс TComponent. Компонент имеет одно public-свойство и следующие published-свойства: О Lines — это объект TStrings, обращение к которому производится двумя метода- ми: GetLines и SetLines. Второй метод для копирования новых значений в част- ное поле, соответствующее этому свойству, использует процедуру Assign. Этот внутренний объект инициализируется в конструкторе Create и уничтожается методом Destroy; С> Selected — это целое число, непосредственно обращающееся к соответствующе- му частному полю. Оно хранит номер выбранного элемента списка строк; О Title — это строчное значение, используемое для изменения заголовка диалого- вого окна. Public-свойство Selltem является свойством только для чтения, которое авто- матически извлекает выбранный элемент из списка строк. Обратите внимание, что это свойство не имеет данных и места для их хранения; оно обращается к другим свойствам, обеспечивая виртуальное представление данных: type TMdLlstBoxDiа 1og = class (TComponent) private FLines: TStrings: FSelected: Integer: FTItle: String; function GetSelltem: string: 0433
434 Глава 9. Создание Delphi-компонентов procedure SetLines (Value: TStrings). function GetLines: TStrings; public constructor Create(AOwner: TComponent). override; destructor Destroy; override; function Execute: Boolean; property Sei Item: string read GetSelltem; publ1 shed property Lines- TStrings read GetLines write SetLines: property Selected: Integer read FSelected write FSelected; property Title: string read FTitle write FTitle; end. Большая часть программного кода этого примера содержится в методе Execute, функции, которая возвращает True или False, в зависимости от модального резуль- тата диалогового окна. Это согласуется с большинством одноименных методов стан- дартных диалоговых окон Delphi. Функция Execute динамически создает форму, устанавливает некоторые из его значений, используя свойства компонента, пока- зывает диалоговое окно и, если результат верный, обновляет текущее выделение: function TMdListBoxDialog.Execute: Boolean: var ListBoxForm. TListBoxForm; begin if FLines.Count = 0 then raise EStringListError.Create (’/Vo items in the list"): ListBoxForm := TListBoxForm.Create (Self); try ListBoxForm.ListBoxl.Items := FLines; ListBoxForm.ListBoxl.Itemindex := FSelected: ListBoxForm.Caption .= FTitle; if ListBoxForm ShowModal = mrOk then begin Result ;= True; Selected ListBoxForm.ListBoxl.Itemindex; end else Result := False; finally ListBoxForm.Free: end. end: Обратите внимание, что программный код содержится в блоке try/finally, что при возникновении ошибки в ходе выполнения (при выводе диалогового окна) в любом случае уничтожит форму. Я также добавил исключение, вызывающее ошибку, если список пуст. Эта ошибка заложена в ходе разработки, и использова- ние исключения является хорошей методикой для ее приведения. Остальные ме- тоды компонента довольно просты. Конструктор создает список строк FLines, ко- торый уничтожается деструктором; методы GetLines и SetLines оперируют со списком в целом; а функция GetSelltem (представленная ниже) возвращает текст выбранно- го пункта: function TMdListBoxDialog.GetSelltem: string: begin 0434
диалоговое окно в компоненте 435 if (Selected >= 0) and (Selected < FLines.Count) then Result = FLines [Selected] else Result .= ". end: Конечно же, поскольку вы написали программный код компонента вручную и добавили его в программный код исходной формы, надо не забыть написать про- цедуру Register. После написания процедуры Register компонент готов. Необходимо лишь пре- доставить bitmap-изображение. Для невизуальных компонентов значки очень важ- ны, поскольку они используются не только в палитре компонентов, но и при раз- мещении компонента на форме. Использование невизуального компонента После того как подготовлено битовое изображение и компонент установлен, я под- готовил пример для его проверки. Форма этой тестовой программы имеет кнопку, строку редактирования и компонент MdListDialog. В этой программе пришлось набрать лишь несколько строк программного кода, соответствующих событию OnClick кнопки: procedure TForml ButtonlClick(SenderTObject); begin // выбрать текст, если он соответствует одной из строк MdListDialogl.Selected ;= MdLqstDialogl Lines IndexOf (Editl.Text); // запустить диалог и получить результат if MdListDialogl.Execute then Editl Text •= MdListDialogl Sei Item. end: Вот и весь код, который необходим для запуска диалогового окна, помещенно- го в компонент (рис. 9.10). Как видно, это довольно интересный подход к разра- ботке общих диалоговых окон. |Choose a string one two three four OK Cancel ₽ис. 9.10. Пример ListDialDemo показывает диалоговое окно, внедренное в компонент LIstDial 0435
436 Глава 9. Создание Delphi-компонентов Свойства коллекций Иногда возникает необходимость, чтобы свойство имело не одно значение, а спи- сок значений. В отдельных случаях можно использовать свойства на основе TString- List, но они работают только с текстовыми данными (несмотря на то, что к каждой строке может быть подключен объект). Когда требуется, чтобы свойство содержа- ло массив объектов, большинство VCL-подобных решений используют коллек- ции. Роль коллекций специально заключается в создании свойств, содержащих список значений. Примером Delphi свойства-коллекции является свойство Columns компонента DBGrid и свойство Panels компонента TStatusBar. Коллекция обычно является контейнером объектов данного типа. По этой при- чине для определения коллекции необходимо создать производный класс от клас- са TCollection, а также унаследовать новый класс от класса TCollectionltem. Второй класс определяет объекты, содержащиеся в коллекции; коллекция создается пу- тем передачи в нее класса объектов, которые она будет содержать. Мало того, что класс коллекции манипулирует пунктами коллекции, но он так- же отвечает за создание новых объектов при вызове метода Add. Нельзя создать объект, а затем добавить его в существующую коллекцию. В листинге 9.3 пред- ставлены два класса для пунктов и для коллекции с наиболее подходящим кодом. Листинг 9.3. Классы коллекции и их пункты type TMdMyltem = class (TCollectionltem) private FCode Integer. FText string procedure SetCode(const Value Integer); procedure SetText(const Value string), published property Text string read FText write SetText. property Code Integer read FCode write SetCode end. TMdMyCollection = class (TCollection) private FComp TComponent FCollString string publ ic constructor Create (CollOwner TComponent) function GetOwner TPersistent override. procedure Updatedtem TCollectionltem). override; end { TMdMyCollection } constructor TMdMyCollection Create (CollOwner TComponent); begin inherited Create (TMdMyltem). FComp = CollOwner end function TMdMyCollection GetOwner TPersistent. 0436
Свойства коллекций 437 begin Result = FComp, end. procedure TMyCollection Updatedtem TCollectionltem). var str string. i Integer begin inherited. // обновить все в любом случае . str = ''. for i = О to Count - 1 do begin str = str + (Items [i] as TMyltem) Text. if i < Count - 1 then str = str + end. FCollString = str end Для верного представления в редакторе свойств коллекции, предоставляемом IDE, Delphi-коллекция должна определить метод GetOwner. По этой причине он должен быть связан с содержащим его компонентом (владельцем коллекции, хра- нящемся в поле FComp). Пример коллекции компонента представлен на рис. 9.11. Г"1 Forml MdCollectronl MoreOata 4 0 TMyltem »5 1 • TMyltem 4 2 TMyltem [MdCollectronl MoreData[1] T"' i1 Properties | Events ] Code *0_____________ | Тей Г О TMyltem 2 - Т Му hem 1 • TMpitern A# shown 7 Editing MdCoHectioni.MortOM#' Рис. 9.11. Редактор коллекции, Object TreeView и Object Inspector для пункта коллекции Каждый раз при изменении данных в пункте коллекции его код вызывает ме- тод Changed (передающий True или False для указания, является ли изменение ло- кальным по отношению к пункту или оно относится ко всему набору пунктов кол- лекции). В результате этого запроса класс TCollection вызывает виртуальный метод Update, который получает в качестве параметра отдельный пункт, требующий мо- дернизации, или nil, если все пункты изменены (и когда метод Changed вызывается 0437
438 Глава 9. Создание Delphi-компонентов с True в качестве параметра). Для обновления значений других элементов коллек- ции, непосредственно самой коллекции или целевого компонента этот метод мож- но перекрыть. В этом примере обновляются строки со сводкой данных коллекции, которые были добавлены в коллекцию и которые компонент-хозяин будет представлять как свойство. Использование коллекции в компоненте очень простое. Вы объяв- ляете коллекцию, создаете ее с помощью конструктора и в конце освобождаете ее а также предъявляете ее в качестве свойства: type TCanTest = class(TComponent) private FColl TMyCollection. function GetColIString string. public constructor Create (aOwner TComponent): override. destructor Destroy, override. published property MoreData TMyCollection read FColl write SetMoreData, property CollString string read GetCol1 String. end constructor TCanTest Create(aOwner TComponent). begin inherited FColl = TMyCollection Create (Self). end destructor TCanTest Destroy. begin FColl Free inherited end procedure TCanTest SetMoreData(const Value' TMyCollection). begin FColl Assign (Value). end function TCanTest GetColIString string: begin Result = FColl FColIString. end Обратите внимание, что пункты коллекции сохраняются в поток DFM-файла формы совместно с другими ее компонентами, используя специальные маркеры item и угловые скобки: object MdCollectionl TMdCollection MoreData = < item Text = 'one' Code = 1 end item Text = 'two' Code = 2 0438
Определение действий 439 end item Text = 'three' Code = 3 end> end Определение действий Помимо определения пользовательских компонентов можно определить и зареги- стрировать новые стандартные действия, которые станут доступными в редакторе Action Editor компонента Action List. Создание новых действий является неслож- ной задачей. Необходимо унаследовать класс TAction и перекрыть некоторые из методов базового класса. Требуется перекрыть три метода: О функцию HandlesTarget, сообщающую, хочет ли объект-действие обработать опе- рацию для текущей цели, которой по умолчанию является элемент управле- ния, находящийся в фокусе; О процедуру UpdateTarget, которая может устанавливать пользовательский интер- фейс элемента управления, связанный с действием, в конечном счете отключая действие, если операция в текущий момент недоступна; О необходимо реализовать метод ExecuteTarget для определения выполняемого кода, чтобы пользователь мог выбрать нужное действие. Для показа этого подхода на практике я реализовал три действия cut (выре- зать), сору (копировать) и paste (вставить) для элемента «список» примерно таким же способом, как это делает VCL для строки редактирования (хотя я несколько упростил код). Я написал базовый класс, унаследованный от общего класса TList- ControlAction класса модуля ExtActns. Этот класс, TMdCustomListAction, добавляет об- щий код, совместно используемый всеми специальными действиями, и публикует ряд свойств действий. Три производных класса имеют собственный программный код ExecuteTarget плюс еще немного. Вот эти четыре класса: type TMdCustomListAction = class (TListControlAction) protected function TargetList (Target TObject) TCustomListBox function GetControl (Target TObject) TCustomListControl. public procedure UpdateTarget (Target TObject) override; published property Caption property Enabled property HelpContext property Hint, property Imageindex property ListControl property Shortcut, property SecondaryShortCuts property Visible 0439
440 Глава 9. Создание Delphi-компонентов property OnHint; end; TMdListCutAction = class (TMdCustomListAction) public procedure ExecuteTarget(Target: TObject); override, end; TMdListCopyAction - class (TMdCustomListAction) public procedure ExecuteTarget(Target: TObject); override: end: TMdListPasteAction = class (TMdCustomListAction) public procedure UpdateTarget (Target: TObject): override: procedure ExecuteTarget (Target: TObject): override; end: Метод HandlesTarget (один из трех ключевых методов классов действий) предо- ставляется классом TListControlAction с помощью следующего кода: function TListControlAct:on.HandlesTarget(Target: TObject): Boolean: begin Result ;= ((ListControl <> nil) or (ListControl = nil) and (Target is TCustomLlstControl)) and TCustomLlstControl(Target).Focused; end; Метод UpdateTarget имеет две различные реализации. Реализация по умолча- нию обеспечивается базовым классом и используется действиями сору и cut. Эти действия разрешаются, только если целевой элемент-список имеет, по крайней мере, один активный пункт. Доступность действия paste определяется состоянием буфера обмена: procedure TMdCustomListAction.UpdateTarget (Target: TObject): begin Enabled := (TargetList (Target).Items.Count > 0) and (TargetList (Target).Itemindex >= 0): end; function TMdCustomListAction.TargetList (Target: TObject): TCustomListBox; begin Result :- GetControl (Target) as TCustomListBox: end; function TMdCustomListAction.GetControKTarget: TObject): TCustomLlstControl: begin Result := Target as TCustomLlstControl: end; procedure TMdListPasteAction.UpdateTarget (Target: TObject): begin Enabled := Clipboard.HasFormat (CF_TEXT): end; 0440
Определение действий 441 Функция TargetList использует функцию GetControl класса TListControlAction, ко- торая возвращает либо элемент-список, связанный с действием во время разра- ботки, либо целевой элемент управления (элемент-список с фокусом ввода). И, наконец, три метода ExecuteTarget выполняют соответствующие действия в отношении целевого списка: procedure TMdListCopyAction.ExecuteTarget (Target: TObject); begin with TargetList (Target) do Clipboard.AsText Items [Itemindex]: end; procedure TMdListCutAction.ExecuteTarget(Target: TObject); begin with TargetList (Target) do begin Clipboard.AsText := Items [Itemindex]: Iterns.Delete (Itemindex); end; end; procedure TMdListPasteAction.ExecuteTargettTarget; TObject); begin (TargetList (Target)).Items.Add (Clipboard.AsText); end; После того как вы записали этот программный код в модуль и добавили его в пакет (в нашем случае в пакет MdPack), заключительным шагом является регист- рация новых пользовательских действий в данной категории. Эта категория ука- зывается в качестве первого параметра процедуры RegisterActions; вторым парамет- ром является список регистрируемых классов-действий: procedure Register: begin RegisterActions ('List'. [TMdListCutAction, TMdListCopyAction. TMdListPasteAction]. nil): end: Для проверки работы этих трех действий я написал программу ListTest. Она имеет Два списка плюс панель инструментов, содержащую три кнопки, связанные с дей- ствиями, а также строку редактирования для ввода новых значений. Эта програм- ма позволяет пользователю вырезать, копировать и вставлять пункты списка. Ни- чего особенного, как вы могли подумать, но странный факт заключается в том, что программа не имеет программного кода! ВНИМАНИЕ--------------------------------------------------------------------------------- Для установки значка для действия (и вообще для определения значений свойств по умолчанию) необходимо использовать третий параметр процедуры RegisterActions, который является модулем Данных, содержащим список изображений и список действий с предопределенными значениями. Поскольку действия необходимо регистрировать до того, как вы сможете настроить такой модуль Данных, потребуется двойная регистрация этих действий. Эта проблема весьма сложна, так что я не буду останавливаться на ней, но подробное описание можно найти по адресу http:// www.blong.com/ Conferences/BorCon2002/Actions/2110.htm в разделах «Registering Standard Actions» и «Standard Actions And Data Modules». 0441
442 Глава 9. Создание Delphi-компонентов Создание редакторов свойств Написание компонентов — эффективный способ перестройки Delphi, помогающий разработчикам создавать приложения более быстро и без необходимости тщательно- го изучения низкоуровневых технологий. Среда Delphi также открыта для расши- рений. В частности, можно расширить инспектор объектов, написав пользователь- ский редактор свойств, а конструктор форм — добавлением редакторов компонентов. Совместно с этими технологиями Delphi предлагает для разработчиков средств- надстроек внутренние интерфейсы. Использование этих интерфейсов, известных как OpenTools API, требует углубленного понимания работы среды Delphi и очень хороших познаний множества дополнительных методик, которые не попали в эту книгу (см. приложение А). СОВЕТ----------------------------------------------------------з OpenTools API в Delphi со временем значительно изменяются. Например, модуль Dsgnlntf из Delphi 5 теперь разделен на Designlntf, DesignEditors и другие специализированные модули. Компания Borland также ввела понятие интерфейсов, позволяющих определить набор методов для каждого типа редактора. Однако большинство простых примеров, подобных представленным в этой книге, компилируются практически неизменно, начиная с более ранних версий Delphi. Дополнительную информацию можно получить, изучив исходный программный код в каталоге Delphi \Source\ToolsApi. Обратите также внимание, что с Delphi 6 Update Pack 2 сначала поставлялся с файлом справки, содержащим документацию по OpenTools API. Каждый редактор свойств должен наследоваться от абстрактного класса ТРго- pertyEditor, который определен в модуле DesignEditors и обеспечивает стандартную реализацию интерфейса IProperty. Delphi уже определяет некоторые редакторы свойств для строк (класс TString Property), целых чисел (класс TIntegerPro perty), сим- волов (класс TCharProperty), перечислений (класс TEnumProperty) и наборов (класс TSetProperty), так что ваш редактор свойств можно наследовать от редактора для того типа свойств, с которым вы работаете. В любом пользовательском редакторе свойств необходимо переопределить фун- кцию GetAttributes таким образом, чтобы она возвращала набор значений, указыва- ющих способности редактора. Наиболее важные атрибуты — paValueList и pa Dialog. Атрибут paValueList указывает, что инспектор объектов покажет элемент «комби- нированный список» со списком значений (даже отсортированным, если установ- лен атрибут paSortList) предоставляемый за счет перекрытия метода GetValues. Ат- рибут pa Dialog активизирует кнопку с троеточием в инспекторе объектов, которая выполняет метод Edit редактора. Редактор для свойства Sound Созданная ранее «звуковая» кнопка имеет два связанных со звуком свойства: SoundUp и SoundDown. Они являются строчными, поэтому их можно представить в инспекторе объектов, использующем существующий по умолчанию редактор свойств. Однако они требуют введения пользователем имени системного звука или внешнего файла, что является недружественным по отношению к пользователю и может привести к ошибке ввода. Мы могли написать общий редактор, обрабатывающий имена файлов, но тре- буется также возможность выбора системных звуков. («Системные звуки» — это 0442
Создание редакторов свойств 443 предопределенные наименования звуков, связанных с действиями пользователей, и ассоциированы х помощью компонента Sound панели управления Windows с дей- ствительными звуковыми файлами.) По этой причине я создал более сложный редактор свойств. Мой редактор позволяет пользователю либо выбирать значения цз раскрывающегося списка, либо выводить диалоговое окно для загрузки и про- верки звука (системного звука или из звукового файла). Редактор свойства обес- печивает как метод Edit, так и метод GetValues: type TSoundProperty = class (TStrlngProperty) public function GetAttributes: TPropertyAttributes: override: procedure GetValuestProc: TGetStrProc); override: procedure Edit: override: end: ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Стандартное соглашение Delphi предусматривает именование класса редактора свойств именем, заканчивающимся словом Property, а всех редакторов компонентов — именем, заканчивающимся словом Editor. Функция GetAttributes сочетает атрибуты paValueList (для раскрывающегося спис- ка) и paDialog (для пользовательской строки редактирования), а также сортирует списки и разрешает выбор свойства для нескольких компонентов: function TSoundProperty.GetAttributes: TPropertyAttributes: begin // редактср.отсортированный список, множественное выделение Result := [paDialog. paMultiSelect. paValueList. paSortListJ; end: Метод GetValues вызывает процедуру, которую он получает в качестве парамет- ра, несколько раз: для каждого строчного значения, которое требуется добавить в раскрывающийся список (рис. 9.12): procedure TSoundProperty,GetValues(Proc: TGetStrProc): begin // предоставить список системных звуков Proc ( 'Maximize"): Proc ('Minimize'): Proc (.'MenuCommand'): Proc ('MenuPopup'): Proc ('RestoreDown'); end; Лучше извлечь эти значения из реестра Windows, в котором перечислены все имена. Метод Edit совершенно ясен: он создает и выводит диалоговое окно. Я бы мог вывести окно Open непосредственно, но решил добавить промежуточный шаг, предоставив пользователю возможность прослушать звук. Это похоже на то, что Delphi делает с графическими свойствами: сначала открывается предварительный просмотр, а файл загружается только после того, как подтверждена правильность его выбора. Самым важным шагом является загрузка файла и его проверка перед применением его в качестве значения свойства. Вот программный код метода Edit: Procedure TSoundProperty.Edit; begin 0443
444 Глава 9. Создание Pelphi-компонентоЕ SoundForm = TSoundForm Create (Application), try SoundForm ComboBoxl Text = GetValue // показать диалоговое окно if SoundForm ShowModal = mrOK then SetValue (SoundForm ComboBoxl Text), finally SoundForm Free end. end. Object Inspector MdSoundButtonl ПАКТ jt-Jgur.ir - Properties | Events | IShawHH SoundDown „ SoundUp । TabOtdet T abStop JTag [Top [VisSbk ^Widti Word Jah shown Fate ~~*T MenuCommand '*' lAppGPFault (Maximize MenuCommand MenuPopup Minimize RestoreDown Restored p SystemAsterisk n we"......... Рис. 9.12. Список звуков служит подсказкой для пользователя, который также может ввести значение или дважды щелкнуть на элементе для активизации редактора (см. рис. 9.13). Методы GetValue и SetValue определены базовым классом — редактором строч- ных свойств. Они считывают и записывают значение текущего компонента, кото- рый вы редактируете. В качестве альтернативного варианта можно обратиться к редактируемому компоненту с помощью метода GetCom portent (который требует параметр, указыва- ющий, с каким из выбранных элементов вы работаете; 0 указывает на первый ком- понент). При непосредственном обращении к компоненту также необходимо выз- вать метод Modified объекта Designer (свойство редактора свойств базового класса). В примере нам не нужно вызывать Modified, поскольку метод SetValue базового класса делает это за вас автоматически. Предыдущий метод Edit выводит диалоговое окно — стандартную форму Delphi, построенную, как всегда, визуально и добавленную в пакет, содержащий компо- ненты времени разработки. Эта форма весьма проста; элемент ComboBox выводит значения, возвращенные методом GetValues, а четыре кнопки позволяют открывать файл, проверять звук и закрывать диалоговое окно, соглашаясь с выбранным значением или отменяя его (рис. 9.13). Предоставление раскрывающегося списка значений и диалогового окна для редактирования свойства заставляет инспектор объекта показывать только кнопку со стрелкой, указывающей на наличие раскры- вающегося списка, и опускает кнопку с троеточием, означающую наличие диало- гового окна редактора. В данном случае, также как и с редактором свойства Color, диалоговое окно открывается двойным щелчком на текущем значении или нажа- тием сочетания Ctrl+Enter. 0444
Создание редакторов свойств 445 $ Sound Property Editor SoundFfe |MenuCommand febaad.. | gey jf X | Рис. 9.13. Форма редактора свойства Sound выводит список доступных звуков, а также позволяет загрузить файл и прослушать выбранный звук Первые две кнопки формы имеют метод, связанный с их событием OnClick: procedure TSoundForm btnLoadClick(Sender TObject). begin if OpenDialogl Execute then ComboBoxl Text = OpenOialogl FileName. end. procedure TSoundForm btnPlayClick(Sender TObject) begin PlaySound (PChar (ComboBoxl Text). 0 snd_Async) end. Обратите внимание, что достаточно сложно определить, правильно ли указан звук и доступен ли он. (Можно проверить файл, но с системными звуками воз- можны некоторые проблемы.) Функция PlaySound возвращает код ошибки при вос- произведении, но только если она не может найти системный звук по умолчанию, при невозможности найти тот звук, который вы указали. Если требуемый звук не- доступен, она воспроизведет системный звук по умолчанию и не возвращает код ошибки. PlaySound ищет звук сначала в реестре и если не находит его там, проверя- ет, существует ли указанный звуковой файл. ПРИМЕЧАНИЕ--------------------------------------------------------------------------- Если вы хотите развить этот пример далее, то можете добавить значки в раскрывающийся список, выводимый в инспекторе объектов, если решите, какие изображения присоединить к конкретным звукам. Установка редактора свойств После того как вы напишете программный код, вы можете установить компонент и его редактор свойств в Delphi. Для этого необходимо в процедуру Register модуля Добавить следующий фрагмент: procedure Register begin RegisterPropertyEditor (Typelnfo(string) TMdSoundButton. 'SoundUp'. TSoundProperty). RegisterPropertyEditor (Typelnfo(string). TMdSoundButton 'SoundDown' TSoundProperty) end. Это вызовет регистрацию указанного в последнем параметре редактора, пред- назначенного для использования со свойствами строчного типа (первый параметр), Но только для определенного компонента и для свойства с определенным именем. Эти два последних значения могут быть опущены, чтобы обеспечить большее 0445
446 Глава 9. Создание Ре1рЫ-компонентон количество общих редакторов. Регистрация этого редактора позволяет инспектору объектов показывать список значений и диалоговое окно, вызываемое методом Edit. Чтобы устанавливать этот компонент, можно добавить его файл исходного кода в существующий или новый пакет. Вместо добавления этого и других, рассматри- ваемых в этой главе модулей, в пакет MdPack, я создавал второй пакет, содержащий все надстройки, построенные в данной главе. Пакет назван MdOesPk (сокращение от Mastering Delphi design package). Новшеством этого пакета является лишь то, что я откомпилировал его, используя директиву компилятора {$DESIGNONLY}. Эта директива используется для пометки пакетов, взаимодействующих со средой Delphi (устанавливая компоненты и редакторы), но не требующихся готовым приложе- ниям в ходе выполнения. СОВЕТ----------------------------------------------------------------------- Программный код для всех средств-надстроек помещен в подкаталог MdDesPk, вместе с кодом паке- тов, используемых для их установки. Я не предоставил примеров, демонстрирующих использование этих средств-надстроек (которые нужны только в ходе разработки приложений), поскольку все, что необходимо сделать — это выбрать соответствующие компоненты в среде Delphi и посмотреть, как они себя ведут. Модуль редактора свойства использует модуль SoundB, который определяет компонент TMdSoundButton. По этой причине новый пакет должен ссылаться на существующий пакет. Вот начальный фрагмент его программного кода (в даль- нейшем я добавлю в него другие модули): package MdDesPk; {$R *.RES} {SALIGN ON} {$DESCRIPTION 'Mastering Delphi DesignTime Package'} {$DESIGNDNLY} requi res vcl. MdPack. designide; contains PeSound in 'PeSound.pas'. PeFSound in 'PeFSound.pa s' {SoundForm}; Создание редактора компонента Использование редакторов свойства позволяет разработчику сделать компонент более дружественным. Инспектор объектов представляет одну из ключевых час- тей пользовательского интерфейса среды Delphi, и Delphi-разработчики весьма часто используют его. Однако можно придерживаться второго подхода и опреде- лить, как компонент взаимодействует с Delphi: напишите редактор компонента. Так же как редакторы свойства расширяют инспектор объектов, редакторы ком- понентов расширяют конструктор форм. При щелчке правой кнопкой мыши в пре- 0446
Создание редактора компонента 447 делах формы во время разработки вы видите некоторые стандартные пункты кон- текстного меню плюс пункты, добавленные редактором выбранного компонента. Примеры этих пунктов меню: пункты, активизирующие Menu Designer (конструк- тор меню), Fields Editor (редактор полей), Visual Query Builder (визуальный построи- тель запросов) и другие редакторы среды. Со временем вывод этих специальных редакторов становится стандартным действием компонента, выполняемым при двойном щелчке на нем. Общее использование редакторов компонентов включает добавление окна About box с информацией о разработчике компонента, добавление имени компонента и предоставление определенных мастеров для настройки свойств компонента. В частности, изначальное предназначение заключалось в том, чтобы позволить ма- стеру (или непосредственно программному коду) однократно устанавливать мно- жество свойств вместо их индивидуальной установки. Использование в качестве основы класса TComponentEditor Редактор компонента в общем случае должен наследоваться от класса TCompo- nentEditor, который предоставляет базовую реализацию интерфейса IComponent- Editor. Самыми важными методами этого интерфейса являются: О GetVerbCount, который возвращает количество пунктов меню, добавляемых при выборе компонента в контекстное меню конструктора форм; О GetVerb, который однократно вызывается для каждого нового пункта меню и дол- жен возвращать текст, который будет каждому понятен в контекстном меню; О ExecuteVerb, который вызывается при выборе одного из новых пунктов меню. Номер пункта передается как параметр метода; О Edit, который вызывается при двойном щелчке пользователя на данном компо- ненте в конструкторе форм для активизации действия по умолчанию. Как только вы привыкнете к идее, что «verb» — лишь новый пункт меню с соот- ветствующим действием, имена методов этого интерфейса станут весьма понят- ными. Этот интерфейс намного проще, чем редакторы свойств, которые мы рас- смотрели перед этим. СОВЕТ----------------------------------------------------------------- Подобно редакторам свойства, редакторы компонентов претерпели некоторые изменения в Delphi 5 и 6, и теперь определены в модулях DesignEditors и Designlntf. Редактор компонента для ListDialog Теперь, когда я представил ключевые идеи создания редакторов компонентов, да- вайте рассмотрим пример: редактор для созданного ранее компонента ListDialog. В редакторе компонента хотелось бы вывести диалоговое окно About box, добавить примечание о защите авторских прав меню (неуместное здесь, но обычное для ре- дакторов компонентов) и позволить пользователям выполнить специальное дей- ствие — предварительный просмотр диалогового окна, связанного с компонентом dialog. Я также хочу изменить действие по умолчанию, чтобы показать About box После звукового сигнала (который не очень полезен, но демонстрирует методику). 0447
448 Глава 9. Создание Delphi-компонентов Для реализации этого редактора программа должна перекрыть четыре метода перечисленные в предыдущем разделе: uses Designlntf: type TMdListCompEditor = class (TComponentEditor) function GetVerbCount: Integer: override. function GetVerbCIndex: Integer): string: override: procedure ExecuteVerb(Index. Integer): override: procedure Edit: override: end: Первый метод возвращает количество пунктов меню, добавляемых в контекст- ное меню (в нашем случае — 3). Этот метод вызывается только один раз — перед выводом меню. Второй метод вызывается по одному разу для каждого пункта меню (в нашем случае — трижды): function TMdListCompEditor.GetVerb (Index- Integer)- string; begin case Index of 0- Result := ’ MdListDialog ( Cant )'. 1. Result := ’&About this component.. ': 2: Result := ’&Preview. end: end: Forml ..... tn MdListDialog (©Cantu) . .Mgbw ' • '' About this component,,, J' Preview,,. Edit Control 4 Position Flip Children Tab Order... ; ’ CreationOrder... - • , ? tert?-? Add to Repository... View as Text ’ s 1 Text DFM Рис. 9.14. Пользовательские пункты меню, добавленные редактором компонента в компонент ListDialog Этот программный код добавляет пункты меню в контекстное меню формы (рис. 9.14). Выбор любого из этих пунктов активизирует метод ExecuteVerb редак- тора компонента: 0448
Что далее? 449 procedure TMdListCompEditor.Executeverb (Index: Integer): begin case Index of 0: ; ll нечего не делать 1: MessageDlg ('This is a simple component editor'#13 + 'built by Marco Cantu '#13 + 'for the book "Mastering Delphi"'. mtlnformation. [mbOK], 0). 2: with Component as TMdListDialog do Execute: end: end; Я решил обработать первые два пункта в одной ветви выражения case, хотя можно было пропустить этот программный код для пункта с отметкой авторских прав. Другие команды изменяют вызовы метода Execute редактируемого компо- нента, определенного использованием свойства Component класса TComponentEditor. Зная тип компонента, вы можете легко обратиться к его методам после динами- ческого приведения типов. Последний метод обращается к действию компонента по умолчанию и активи- зируется двойным щелчком на компоненте в конструкторе форм: procedure TMdListCompEditor.Edit: begin // выдать звуковой сигнал и вывести about box Веер: Executeverb (0): end. Регистрация редактора компонента Чтобы сделать редактор доступным в среде Delphi, его необходимо зарегистриро- вать. Опять же, вызов можно добавить в процедуру Register его модуля и вызывать специальную процедуру регистрации редакторов компонентов: procedure Register: begin RegisterComponentEditor (TMdListDialog. TMdListCompEditor): end. Я добавил этот модуль в пакет MdDesPk, который содержит все design-time-pac- ширения, рассмотренные в этой главе. После установки и активизации этого паке- та вы можете создать новый проект, поместить в него компонент ListDialog и по- экспериментировать с ним. Что далее? В этой главе вы увидели, как определить различные типы свойств, как добавлять события и как определять и перекрывать методы компонентов. Вы познакомились с различными примерами компонентов, включая простые изменения существую- щих компонентов, новые графические компоненты, и, в заключительном разделе, пример помещения диалогового окна внутри компонента. При создании этих ком- понентов мы столкнулись с некоторыми новыми нюансами Windows-программи- 0449
450 Глава 9. Создание Delphi-компонентов рования. Вообще, при написании новых Delphi компонентов программисты до- вольно часто прибегают к использованию непосредственно API Windows. Написание компонентов — удобная методика для повторного использования программного обеспечения, но для того чтобы сделать ваши компоненты более простыми в использовании, необходимо в максимально возможной степени ин- тегрировать их в среду Delphi посредством создания редакторов свойств и редак- торов компонентов. Кроме того, можно создать множество других расширений IDE Delphi, включая пользовательские мастера. Я создал множество расширений Delphi некоторые из которых представлены в Приложении А. Глава 10 посвящена DLL-библиотекам Delphi. Мы использовали DLL в преды- дущих главах, а теперь настало время подробного рассмотрения их роли, а также вопросов создания библиотек. Кроме того, далее мы рассмотрим использование пакетов Delphi, которые являются специальным типом DLL. Дальнейшее разви- тие темы создания компонентов можно найти в главе 17, которая посвящена эле- ментам управления, способным извлекать данные из баз данных, и пользователь- ским компонентам dataset. 0450
Библиотеки и пакеты Исполняемые файлы Windows бывают двух видов: программы (EXE) и динамиче- ски подсоединяемые библиотеки (DLL). При написании Delphi-приложения вы обычно создаете программный файл. Однако Delphi-приложения довольно часто осуществляют вызов функций, хранящихся в динамических библиотеках. Каждый раз, когда вы непосредственно вызываете API-функцию Windows, в действитель- ности вы обращаетесь к динамической библиотеке. В среде Delphi создать DLL очень просто. Однако из-за самой природы DLL могут возникать некоторые про- блемы. Написание динамической библиотеки в Windows не всегда так просто, как кажется, поскольку эти библиотеки и вызывающие их программы должны при- держиваться одинаковых соглашений именования, типов параметров и других де- талей. Данная глава охватывает основы создания DLL в аспекте Delphi. Вторая часть главы посвящена специальному типу динамически подключае- мых библиотек — пакетам Delphi. Эти пакеты обеспечивают хорошую альтерна- тиву открытым DLL, хотя немногие Delphi-программисты используют их преиму- щества, ограничиваясь написанием Delphi-компонентов. Здесь мы рассмотрим некоторые подсказки и методики использования пакетов для разбиения больших приложений. В данной главе рассматриваются следующие вопросы: О построение и использование DLL в Delphi; О вызов функций DLL в ходе выполнения программы; О совместное использование данных DLL; ° структура пакета Delphi; ° помещение форм в пакеты. Роль DLL в Windows Перед тем как приступить к разработке DLL в Delphi и других языках программи- рования, я дам краткий технический обзор DDL Windows, выделяя ключевые эле- менты. Мы начнем с рассмотрения динамического связывания, затем посмотрим, Как Windows использует DDL, и закончим рассмотрением некоторых общих пра- вил написания DLL. 0451
452 Глава 10. Библиотеки и пакеты Что такое динамическое связывание? Во-первых, оно важно для полного понимания разницы между статическим и ди- намическим связыванием. Когда процедура непосредственно недоступна в исход- ном файле, компилятор добавляет ее в таблицу внешних идентификаторов. Ко- нечно же, компилятор должен был видеть процедуры и знать их параметры и типы, в противном случае — выдать сообщение об ошибке. После компиляции обычной (статической процедуры), компоновщик выбира- ет откомпилированный код процедуры из откомпилированного модуля Delphi (или статической библиотеки) и добавляет ее в код программы. Результирующий ис- полняемый файл включает весь код программы и используемых модулей. Компо- новщик Delphi достаточно умен, чтобы включать только минимальный объем кода программных модулей, и присоединять только те функции и методы, которые действительно используются. Вот почему он называется «умный компонов- щик» («smart linker»). СОВЕТ----------------------------------------------------------- Известным исключением из этого правила является включение виртуальных методов. Компилятор не может заранее определить, какие виртуальные методы вызовет программа, поэтому он включа- ет их всех. По этой причине, программы и библиотеки с двумя и более виртуальными функциями имеют тенденцию к генерации больших исполняемых файлов. В ходе создания VCL разработчики компании Borland вынуждены были балансировать гибкостью, получаемой от наличия виртуальных методов, и сокращением размеров исполняемого файла, достигаемого ограничением использова- ния виртуальных функций. В случае динамического связывания, которое происходит при вызове DLL-фун- кции программным кодом, компоновщик для установки таблицы импорта испол- няемого файла использует сведения из объявления подпроцедуры external. Когда операционная система Windows загружает исполняемый файл в память, она сна- чала загружает все необходимые DLL, а затем запускает программу. В ходе загруз- ки Windows заполняет таблицу импорта программы адресами DLL-функций в па- мяти. Если по какой-либо причине DLL не найдена или процедура, к которой происходит обращение, в найденной DLL не представлена, программа запущена не будет. Каждый раз, когда программа вызывает внешнюю функцию, для перенаправ- ления вызова к DLL-коду (который располагается в адресном пространстве про- граммы), она использует таблицу импорта. Обратите внимание, что эта схема не смешивает два различных приложения. DLL становится частью выполняемой про- граммы и загружена в то же адресное пространство. Передача всех параметров осу- ществляется через стек приложения (потому что DLL не имеет отдельного стека) или через регистры центрального процессора. Поскольку DLL загружена в адрес- ное пространство приложения, любое выделение памяти, выполняемое DLL, или любые глобальные данные, которые она создает, располагаются в адресном про- странстве основного процесса. Следовательно, данные и указатели на память мо- гут передаваться непосредственно из DLL в программу и наоборот. Кроме того, можно использовать и объектные ссылки, которые могут быть несколько затруД' йены, если EXE и DLL имеют различный класс компиляции (и точно так же ДлЯ этой цели можно использовать пакеты). 0452
роль DLL в Windows 453 Существует и иной подход использования DLL, который даже более динами- чен, чем только что рассмотренный. Можно загрузить DLL в память в ходе выпол- нения программы, отыскать функцию (вы знаете ее имя) и вызвать функцию по имени. Этот подход требует более сложного программного кода и требует некото- рого дополнительного времени на нахождение функции. Однако исполнение фун- кции происходит с той же скоростью, что и выполнение неявно загруженной DLL. Положительным моментом является то, что для запуска программы отпадает не- обходимость иметь доступную DLL. Этот подход будет использован в примере DynaCalL Для чего нужны DLL? После того как вы получили общее представление о том, как работает DDL, мы можем перейти к причинам их использования. Первое преимущество заключается в том, что различные программы используют одни и те же DLL, а последние загру- жаются в память только один раз, что позволяет сберечь память системы. DLL ото- бражаются в частное адресное пространство каждого процесса (каждого запущен- ного приложения), но код загружается в память всего один раз. СОВЕТ---------------------------------------------------------------- Более точным будет сказать, что операционная система попытается загрузить DLL в тот же адрес в адресном пространстве каждого приложения (используя базовый адрес, предпочитаемый DLL). Если этот адрес в виртуальном адресном пространстве конкретного приложения недоступен, образ кода DLL для этого процесса будет перегружен — это дорогостоящая в плане производительности и использования памяти операция, поскольку перегрузка производится для процесса, а не для всей системы. Другая интересная особенность заключается в том, что имеется возможность предоставлять различные версии DLL, заменяя текущую, без необходимости пе- рекомпилирования использующих их приложений. Разумеется, что такой подход будет работать только в том случае, если функции DLL имеют такие же парамет- ры, как и в предыдущей версии. Если DLL имеет новые функции — ничего страш- ного. Проблемы могут возникнуть, только если функция в более старой версии не представлена в новой версии, либо если функция использует несовпадающие объек- тные ссылки, классы, базовые классы или даже версию компилятора. Еще одно преимущество заключается в том, что DLL особенно подходят для сложных приложений. Если имеется большая программа, требующая частых об- новлений и периодического устранения «жучков», то разделение ее на несколько исполняемых файлов или динамических библиотек позволит распространять толь- ко измененные части, а не один большой исполняемый файл. Это особенно рацио- нально для системных библиотек Windows: нет необходимости перекомпилиро- вать свою программу, если компания Microsoft предоставляет обновленные версии системных библиотек (например, в новой версии операционной системы или в виде service pack). Еще одна распространенная технология заключается в использовании динами- ческих библиотек для хранения лишь ресурсов. Можно создать различные DLL, содержащие строчные значения для различных языков, а затем в ходе выполнения Менять их, либо подготовить библиотеку значков и битовых изображений, а затем 0453
454 Глава 10. Библиотеки и пакеты использовать их в различных приложениях. Разработка программ с поддержкой различных языков особенно важна. Delphi осуществляет поддержку использова- ния различных языков посредством его встроенной среды Integrated Translation Environment (ITE). Другим ключевым преимуществом является то, что DLL независимы от языка программирования. Практически все среды программирования Windows включа- ют макроязык (макросы) в приложения конечного пользователя, позволяя про- граммисту вызывать функции, хранящиеся в DLL. Хотя это удобство применимо только в отношении функций. Для совместного использования объектов DLL меж- ду языками программирования необходимо переходить на СОМ-инфраструктуру или архитектуру .NET. Правила для разработчиков DLL Delphi Программисты DLL Delphi должны следовать представленным ниже правилам. Вызываемая внешними программами функция или процедура DLL должна удов- летворять следующим требованиям: О она должна быть перечислена в выражении exports данной DLL. Это сделает ее видимой для внешнего мира; О для того чтобы использовать стандартную технологию передачи параметров Win32, вместо оптимизированной технологии передачи register (в Delphi ис- пользуется по умолчанию), экспортируемые функции должны быть объявле- ны как stdcall. Исключение из этого правила допускается только в том случае, если вы собираетесь использовать эту библиотеку только в Delphi-приложени- ях. Кроме того, конечно же, можно использовать и другие технологии вызова, «понимаемые» другими компиляторам (например, cdecl, используемая по умол- чанию компиляторами языка С); О типы параметров DLL должны быть стандартными типами Windows (главным образом совместимы с С), по крайней мере, если вы хотите иметь возможность использовать свою DLL в других средах разработки. Существуют и дополни- тельные правила экспортирования строчных данных (см. пример FirstDLL); О DLL может использовать глобальные данные, которые не будут совместно ис- пользоваться вызывающими приложениями. Каждый раз, когда приложение загружает DLL, оно сохраняет глобальные данные в собственном адресном про- странстве (см. пример DLLMem); О Delphi-библиотека должна перехватывать все внутренние исключения, за ис- ключением тех случаев, когда вы планируете использовать ее только в Delphi- про граммах. Использование существующих DLL В примерах этой книги вы уже использовали существующие DLL при вызове API' функций Windows. Если вы помните, все API-функции объявлены в системном модуле Windows. Функции объявляются в interface-разделе модуля следующим образом: 0454
Использование существующих DLL 455 function PlayMetaFi1e(DC: HDC; MF: HMETAFILE): BOOL; stdcall: function Pa1ntRgn(DC: HDC: RGN: HRGN): BOOL: stdcall; function PolyPolygon(DC: HDC: var Points: var nPolnts: p4: Integer): BOOL: stdcal1; function PtInReg1on(RGN: HRGN: p2. p3: Integer): BOOL: stdcall: А затем в implementation-разделе вместо предоставления программного кода функции модуль ссылается на внешнее определение DLL: const gd132 = 'gdi32.dll': function PlayMetaFile; external gdl32 name ‘PlayMetaFile'; function PalntRgn: external gd132 name 'PaintRgn': function PolyPolygon: external gd132 name ‘PolyPolygon'; function PtlnRegion; external gd132 name 'PtlnRegion': СОВЕТ-------------------------------------------------------------------------------------- В файле Windows. PAS интенсивно используется директива {$EXTERNALSYM identifier}. Эта директива почти ничего не делает в самой среде Delphi, но она применяется в C++Builder. Этот идентификатор предотвращает появление соответствующего идентификатора Delphi в транслируемом заголовоч- ном файле C++. Это помогает сохранить синхронность идентификаторов Delphi и C++, что позво- ляет использовать программный код обоими языками. Расширенное определение этих функций ссылается на имя используемой ими DLL. Имя библиотеки должно быть указано с расширением .DLL, в противном случае программа не будет работать под Windows NT/2000/XP (хотя она будет работать под Windows 9х). Следующим элементом является имя DLL-функции. Директива пате не является обязательной, если имя Delphi-функциии (или про- цедуры) соответствует имени функции DLL (с учетом регистра). Как было сказано ранее, для вызова функции, находящейся в DLL, необходимо обеспечить ее объявление в разделе interface модуля и внешнее определение в раз- деле implementation. Кроме того, в разделе implementation можно выполнить слия- ние двух определений в одно. После соответствующего определения функции ее можно вызвать точно так же, как и любую другую функцию. ПРИМЕЧАНИЕ----------------------------------------------------------------- В среду Delphi включен перевод на язык Delphi большого числа API Windows (см. каталог Source\Rtl\Win среды Delphi). Дополнительные модули Delphi обращаются к другим API, представленными на сайте www.delphi-jedi.org как часть проекта Delphi Jedi. Использование DLL языка C++ В качестве примера я написал DLL на C++, содержащую ряд обычных функций, Для представления, как вызывать DLL из Delphi-приложения. Я не буду в подроб- ностях рассматривать программный код языка C++ (подобный языку С), но вмес- то этого обращу внимание на вызовы DLL языка C++ из Delphi-приложения. А использование DLL, написанных на языке C++ или С, является в Delphi-npo- траммировании довольно распространенной практикой. Предположим, существует DLL, построенная на С или C++. Как правило име- ется DLL-файл (откомпилированная библиотека), Н-файл (объявление функций ’нутри библиотеки) и LIB-файл (еще одна версия списка экспортируемых функ- 0455
456 Глава 10. Библиотеки и пакеты ций, предназначенного для С/C++ компоновщика). LIB-файл бесполезен в Delphi; DLL-файл используется «как есть», а Н-файл может быть переведен в Delphi-мо- дуль с соответствующими объявлениями. В представленном ниже листинге можно увидеть объявление C++ функций, которые используются для создания библиотеки примера CppDll. Полный исход- ный код и откомпилированная версия этой DLL, а также исходный программный код использующего ее Delphi-приложения размещены в каталоге CppDll. У вас дол- жна иметься возможность выполнить компиляцию этого кода компилятором C++; я использовал Borland C+ + Builder. Вот объявления функций (на языке C++): extern "С" _decl spec (dll export) int WINAPI Double (int n); extern "C" _decl spec (dll export) int WINAPI Triple (int n); __declspec(dllexport) int WINAPI Add (int a. int b); Три функции выполняют ряд основных вычислений над параметрами и воз- вращают результат. Обратите внимание, что все функции определены с модифи- катором WINAPI, который устанавливает соответствующее соглашение вызова па- раметров; им предшествуют объявления________declspec(dllexport), которые делают эти функции доступными извне. Две из этих функций также используют соглашение именования языка С (на что указывает выражение extern "С"), а третья — нет. Эта разница влияет на способ, которым эти функции будут вызываться из Delphi. Внутренние имена первых двух функций соответствуют их именам в файле исходного кода C++. Поскольку в фун- кции Add инструкция extern "С" не использовалась, то компилятор C++ использует корректировку имен (name mangling). Эта технология используется для включе- ния сведений о числе и типе параметров в имя функции, которые требуются языку C++ для реализации перегрузки функции. В результате при использовании ком- пилятора Borland C++ получается смешное имя функции: @Add$qqsii. Это имя необходимо будет использовать для вызова DLL-функции Add в Delphi-програм- ме (что объясняет, почему обычно стараются избегать использования технологии корректировки имен в экспортируемых функциях и объявлять их как extern "С"). Вот объявления этих функций в Delphi-примере CallCpp: function Add (А. В. Integer): Integer, stdcall, external 'CPPDLL.DLL' name '@Addfqqsn'; function Double (N- Integer): Integer, stdcall. external 'CPPDLL.DLL' name 'Double'. function Triple (N Integer): Integer: stdcall: external 'CPPDLL.DLL'; Как можно заметить, необходимо либо предоставить, либо опустить псевдоним внешней функции. Я предоставил его для первой функции (поскольку не было иного варианта: имя экспортируемой DLL-функции @AddSqqsii не является допу- стимым идентификатором Delphi), а также во втором случае, хотя это и не являет- ся необходимым. При соответствии двух имен можно опустить ключевое слово name, как это и сделано в третьей функции. Если вы не уверены в действительных именах функций, экспортируемых DLL, то можно использовать запускаемую из командной строки программу TDump, предоставляемую компанией Borland (она находится в каталоге BIN среды Delphi), используя ключ -ее. 0456
Создание DLL в Delphi 457 Не забывайте в каждое определение добавлять директиву stdcall для того, что- бы вызывающий модуль (приложение) и вызываемый модуль (DLL) использова- ли одинаковое соглашение передачи параметров. Если этого не будет, то в каче- стве получаемых параметров будут использоваться непредсказуемые значения: ошибка, которую очень сложно отследить. СОВЕТ ---------------------------------------------------------------------------- Когда возникает необходимость преобразовать большой заголовочный файл C/C++ в соответствую- щие объявления Delphi, то вместо ручного преобразования можно использовать какое-либо средство, частично автоматизирующее этот процесс. Одним из таких средств является HeadConv, написанное Bob Swart. Это инструментальное средство можно найти на его веб-сайте www.drbob42.coni. Это средство было расширено в проекте Project Jedi под именем DARTH-проект (www.delphi-jedi.org/ team_darth_home). Обратите также внимание, что автоматическая трансляция заголовков из С/ C++ в Delphi невозможна. Язык Delphi более строго типизирован, чем C/C++, поэтому приходится использовать типы более точно. Рис. 10.1. Внешний вид формы примера CallCpp при щелчке на любой кнопке Для использования этой DLL я создал пример с именем CallCpp. Его форма име- ет только кнопки, используемые для вызова функций DLL и ряд визуальных ком- понентов для представления входных и выходных параметров (рис. 10.1). Обрати- те внимание, что для запуска этого приложения DLL должна находиться в том же каталоге, что и проект, в каталоге, указанном путями Delphi, в основном каталоге Windows (\Windows, \WinNT) или в системном каталоге (\Windows\System, \WinNT\ System32). При перемещении исполняемого файла в другой каталог и попытке за- пустить его будет выдано сообщение об ошибке времени выполнения, указываю- щее, что DLL не найдена: с allCpp.exe ~ Unable То Locate DLL т he dynamic fink йжагу CPFDCL.DLL todd not be found b the specified path .............................................................. Создание DLL в Delphi Помимо использования динамических библиотек, написанных в других средах, с помощью среды Delphi можно построить DLL, которые могут использоваться как Helphi-программами, так и в любом другом средстве разработки, поддерживающем 0457
458 Глава 10. Библиотеки и пакеты DLL. Создать DLL в Delphi настолько легко, что это может привести к злоупот- реблению. В общем случае я предлагаю вместо DLL попытаться создать пакет. Как будет показано далее в этой главе, пакеты обычно содержат компоненты, но они могут содержать и открытые некомпонентные классы, позволяющие создавать объектно-ориентированный программный код и эффективно повторно его исполь- зовать. Конечно же, пакеты могут содержать и простые процедуры, константы, пе- ременные и т. д. Как я уже говорил, создание DLL полезно в том случае, если часть программно- го кода приложения довольно часто изменяется. В этом случае обычно можно за- менить DLL, не изменяя основной программы. Точно так же, когда возникает не- обходимость написать программу, обеспечивающую различные возможности для различных групп пользователей, для последних можно устанавливать различные версии DLL. Ваша первая Delphi-DLL В качестве точки отсчета изучения разработки DLL в Delphi я представлю биб- лиотеку, построенную в Delphi. Основной фокус этого примера сконцентрирован на синтаксисе, используемом для определения DLL. В нем также будут продемон- стрированы некоторые соображения, относящиеся к передаче строчных парамет- ров. Для начала выберите File ► New ► Other и на странице New хранилища Object Repository выберите значок DLL. Это приведет к созданию очень простого файла исходного кода, начинающегося с определения: library Project1; Выражение Library указывает, что создан DLL, а не исполняемый файл. Теперь в эту библиотеку можно добавить процедуры и перечислить их в выражении exports: function Triple (N: Integer): Integer: stdcall: begin try Result := N * 3: except Result := -1: end: end: function Oouble (N: Integer): Integer; stdcall: begin try Result := N * 2: except Result := -1: end: end: exports Triple. Double: В этой базовой версии DLL нет необходимости использовать выражение uses; но в общем случае основной файл проекта содержит только выражения uses и exports, в то время как объявления функции помещаются в отдельный модуль. В окончательном исходном коде примера FirstDLL я несколько изменил программ- 0458
Создание DLL в Delphi 459 ный код, добавив вывод сообщения при каждом вызове функции. Этого можно добиться двумя способами: либо использованием модуля Dialogs, либо вызовом функции ShowMessage. При создании приложения требуется компоновка большого объема программ- ного кода VCL. Если статически связывать VCL с DLL, то размер файла получится несколько сотен килобайт. Причина этого в том, что функция ShowMessage выво- дит форму VCL, содержащую элементы управления VCL и использует графиче- ские классы VCL; они, в свою очередь, косвенно обращаются к поточной системе VCL, а также к объектам «приложение» и «экран». В этом случае лучшим вариан- том является просмотр сообщения с помощью непосредственных вызовов API, представленных модулем Windows, и вызовом функции MessageBox, для которой не требуется VCL-код. Это позволяет сделать размер приложения менее 50 кбайт. СОВЕТ-------------------------------------------------------------------- Огромная разница в размере подчеркивает тот факт, что для избежания компиляции VCL-кода во множестве исполняемых файлов не стоит чрезмерно использовать DLL. Размер DLL среды Delphi, разумеется, можно сократить за счет использования пакетов времени выполнения (далее в этой главе). Если запустить тестовую программу CallFrst (см. далее) используя API-версию DLL, ее поведение окажется неверным. Можно несколько раз щелкнуть на кноп- ках, вызывающих функции DLL, не закрывая диалоговые окна, выводимые этой DLL. Это происходит ввиду того, что первый параметр API-вызова MessageBox ра- вен нулю. Вместо этого в качестве первого параметра должен быть дескриптор ос- новной формы программы или формы приложения (именно этих сведений и не хватает в DLL). Перегруженные функции в DLL среды Delphi При создании DLL на языке C++ перегруженные функции для генерации различ- ных имен для каждой функции используют корректировку имен (name mangling). Тип параметров включается прямо в это имя (см. пример CppDll). При создании DLL в Delphi и использовании перегруженных функций (то есть множества функций, использующих одно и то же имя и отмеченных директивой overload) среда Delphi позволяет экспортировать только одну из перегруженных фун- кций с оригинальным именем, указывая список ее параметров в инструкции exports. Если необходимо экспортировать множество перегруженных функций, то в инструк- ции exports следует указать различные имена, для того чтобы различить перегрузку. Эта методика продемонстрирована следующим фрагментом примера FirstDLL: function Triple (С: Char): Integer; stdcall. overload. function Triple (N: Integer): Integer; stdcall: overload: exports Triple (N: Integer). Triple (C: Char) name ’TripleChar': СОВЕТ-------------------------------------------------------------------- Допускается также и обратное: можно импортировать ряд подобных функций из DLL, а в объявле- нии среды Delphi определить их всех как перегруженные функции. Ряд примеров этой методики с°держится в модуле OpenGL.PAS, входящем в Delphi. 0459
460 Глава 10. Библиотеки и пакеты Экспортирование строчных значений из DLL В общем случае функции DLL могут использовать параметры любого типа и возвра- щать значение любого типа. Однако из этого правила существует два исключения: О если планируется вызов DLL из других языков программирования, необходи- мо вместо характерных для Delphi типов данных использовать «родные» типы данных Windows. Например, для представления цветовых значений необходи- мо использовать целые числа или Windows-тип Со to г Ref, а не «родной» для Delphi тип TColor, выполняя соответствующее преобразование (как в примере FormDLL, описанном в следующем разделе). Для совместимости необходимо избегать использования и других типов Delphi, включая объекты (которые не могут ис- пользоваться другими языками) и строчные значения Delphi (которые могут быть заменены на строчные значения типа PChar). Другими словами, каждая среда разработки Windows должна поддерживать основные типы API, и если опираться на них, то вашу DLL можно будет использовать в других средах раз- работки. Кроме того, файловые переменные Delphi (текстовые файлы и двоич- ные файлы записей) не должны передаваться за пределы DLL, но допускается использование дескрипторов файлов Win32; О даже если планируется использовать DLL только в Delphi-приложении, отсут- ствует возможность передавать строчные значения (и динамические массивы) Delphi через границы DLL без ряда предосторожностей. Это вызвано особен- ностью обработки средой Delphi строчных значений в памяти: автоматическом выделении, перераспределении и освобождении. Решение этой проблемы за- ключается во включении системного модуля ShareMem как в DLL, так и в ис- пользующую ее программу. Этот модуль должен быть включен как первый модуль каждого проекта. Более того, необходимо совместно с программой ус- танавливать файл BorlndMM.DLL (сокращение от Borland Memory Manager) в спе- циальную библиотеку. В примере FirstDLL используются обаподхода: однафункция получает и возвра- щает строчные значения Delphi, а другая в качестве параметра получает указатель PChar, который впоследствии заполняется функцией. Первая функция написана как обычно: function OoubleString (S string Separator Char) string stdcall begin try Result = S + Separator + S except Result = '[error]', end end Вторая функция несколько сложней, поскольку строчные значения PChar не имеют простого оператора + и не могут непосредственно сравниваться с символа- ми, перед добавлением в строчное значение должен быть включен разделитель. Вот полный программный код; он использует входной и выходной буферы типа PChar, которые совместимы с любыми средами разработки Windows: function OoublePChar (Bufferin. BufferOut PChar. BufferOutLen Cardinal Separator Char) LongBool stdcall. 0460
Создание DLL в Delphi 461 var SepStr array [0 1] of Char: begin try // если буфер достаточно большой if BufferOutLen > StrLen (Bufferin) *2 + 2 then begin // скопировать входной буфер в выходной StrCopy (BufferOut Bufferin) // встроить строчное значение-разделитель!значение, плюс null-терминатор) SepStr [0] = Separator SepStr [1] = #0 // добавить разделитель StrCat (BufferOut SepStr). // добавить входной буфер еще раз StrCat (BufferOut. Bufferin), Result = True end else // недостаточно места Result = False. except Result = False end. end Вторая версия программного кода более сложна, но первая может использо- ваться только из Delphi. Более того, первая версия требует включения модуля ShareMem и установки файла диспетчера памяти BorlndMM.DLL. Вызов Delphi-DLL Как можно использовать только что созданную библиотеку? Ее можно вызвать из любого другого Delphi-проекта или из другой среды разработки. В качестве при- мера я создал проект CaLLFrst (хранящийся в каталоге FirstDLL). Для обращения к функциям DLL необходимо объявить их как внешние, так же, как и DLL, напи- санные на языке C++. Однако в этот раз можно скопировать и вставить определе- ние функций прямо из исходного программного кода DLL Delphi, добавив инст- рукцию external: function Double (N Integer) Integer. stdcall. external ’FIRSTDLL DLL’. Это объявление подобно объявлению, используемому для вызова DLL языка C++. Однако в данном случае нет проблем с именами функций. После того как они повторно объявлены как внешние (external), функции DLL можно использо- вать точно так же, как локальные функции. Вот два примера вызова функций, от- носящихся к строчным значениям (результат выполнения представлен на рис. 10.2): Procedure TForml BtnDoubleStringClick(Sender TObject) begin // вызвать DLL-функцию непосредственно EditDouble Text = DoubleString (EditSource Text ’ '). end Procedure TForml BtnDoublePCharC1ick(Sender TObject), 0461
462 Глава 10. Библиотеки и пакеты var Buffer, string: begin // создать достаточно большой буфер SetLength (Buffer. 1000): // вызывать DLL-функцию if DoublePChar (PChar (EditSource Text). PChar (Buffer). 1000. '/') then EditDouble Text = Buffer: end; Delphi DLL I Double Triple Рис. 10.2. Результат выполнения примера CallFrst, который использует DLL, построенную в среде Delphi Дополнительные особенности Delphi-DLL Помимо вступительного примера с помощью динамических библиотек в Delphi можно сделать несколько дополнительных вещей. С помощью ряда новых дирек- тив компилятора, влияющих на имя библиотеки, можно вызывать DLL во время выполнения, а в библиотеки можно помещать целые формы. Этому и посвящены последующие разделы. Изменение имени проекта и библиотек Для библиотеки, так же как для стандартного приложения, вы в конечном счете придете к тому, что имя библиотеки будет совпадать с именем файла Delphi-npo- екта. В соответствии с методикой, которая также была введена в Kylix для обеспе- чения совместимости со стандартными соглашениями именования Linux для со- вместно используемых библиотек объектов (Linux-эксивалент DLL-библиотек Windows), в Delphi 6 добавлены специальные директивы компилятора, которые могут использоваться в библиотеках для определения их имен. Некоторые из этих директив главным образом рациональны в мире Linux, а не a Windows, по, тем не менее, они были включены: О SLIBPREFIX используется для добавления чего-либо перед именем библиотеки. Аналогично Linux-методике добавления lib в начале имени библиотек, эта ди- ректива используется в Kylix для добавления bpl в начале имени пакета. Это 0462
Дополнительные особенности Delphi-DLL 463 является необходимым, поскольку Linux для библиотек использует одно рас- ширение (.SO), в то время как в Windows можно использовать различные рас- ширения (подобно тому, как компания Borland использует для пакетов расши- рение .BPL); О SLIBSUFFIX используется для добавления текста после имени библиотеки, но перед расширением. Этот текст может использоваться для уточнения сведений о версии или других вариациях в имени библиотеки; в некоторой степени он может быть полезен и в Windows; О SUBVERSION используется для добавления номера версии после расширения (характерно для Linux, но в Windows этого необходимо избегать). Эти директивы могут быть установлены в IDE с помощью страницы Application (Приложение) диалогового окна Project Options (Параметры проекта) (рис. 10.3). В качестве примера предположим, что используются следующие директивы (ге- нерирующие имя MarcoNameTest60.dll): library NameTest; {SLIBPREFIX 'Marco'} [SLIBSUFFIX '60'} j Prefect Options for firstdl.dlt pireciories/Conditionals ) '' Version Info j Packages Application | Compter j Compiler Messages | linker Jicaton settings Г peiaUj | OK | Cancel | Help Рис, 10.3. Страница Application диалогового окна Project Options имеет раздел Library Name СОВЕТ —----------------------------------------------------------------------------- Пакеты Delphi 6 интенсивно используют директиву $LIBSUFFIX. По этой причине VCL-пакет теперь генерирует VCL.DCP и VCL70.BPL файлы. Преимуществом этого подхода является то, что вам не придется менять раздел requires ваших пакетов для каждой новой версии Delphi. Конечно же, это полезно при перемещении проектов с Delphi 6 на Delphi 7, поскольку предшествующие версии Delphi не обеспечивали этой возможности. При повторном открытии пакетов Delphi 5 вам все еще придет- ся вручную обновлять их исходный код — операция, которую IDE Delphi теперь делает автомати- чески. 0463
464 Глава 10. Библиотеки и пакеты Вызов DLL-функций во время выполнения До сих пор в программном коде мы обращались к функциям, экспортируемым биб- лиотеками, и DLL загружались совместно с программой. Ранее я упоминал, что загрузку DLL можно отложить до того момента, когда она понадобится, поэтому часть программы можно использовать без загрузки DLL. Динамическая загрузка DLL в Windows выполняется вызовом API-функции LoadLibrary, осуществляющей поиск DLL в программной папке, в папках, указан- ных в путях, и в ряде системных каталогов. Если DLL не найдена, Windows выве- дет сообщение об ошибке, которое в некоторых случаях можно пропустить, вызвав Delphi-функцию SafeLoadLibrary. Эта функция имеет тот же эффект, что и вклю- ченный в нее вызов API, но она подавляет стандартное Windows-сообщение об ошибке и является предпочтительным вариантом динамической загрузки библио- теки в Delphi. Если библиотека найдена и загружена (о чем можно узнать по значению, воз- вращаемому функциями LoadLibrary или SafeLoadLibrary), программа может вызвать API-функцию GetProcAddress, осуществляющую поиск таблицы экспорта DLL, про- сматривая имя функции, переданной в качестве параметра. Если GetProcAddress находит соответствие, она возвращает указатель на запрашиваемую процедуру. Теперь можно привести этот указатель в соответствующий тип данных и исполь- зовать его для вызова. Какую бы функцию вы ни использовали, не забудьте после нее вызвать Free Library для того чтобы DLL освободила память. На практике система использует в отно- шении библиотек технологию учета ссылок, освобождающую их, когда каждый запрос загрузки сопровождается запросом на освобождение. Для представления динамической загрузки DLL я написал пример DynaCalL Он использует библиотеку FirstDLL (для того, чтобы программа работала, необходимо скопировать эту DLL из ее исходной папки в каталог, в котором находится пример DynaCaLL). Вместо объявления функций Double и Triple и их непосредственного ис- пользования этот пример достигает того же эффекта за счет усложнения программ- ного кода. Однако преимущество заключается в том, что программа может быть запущена даже тогда, когда DLL недоступна. Кроме того, если в библиотеку добав- лены новые совместимые функции, вам не придется пересматривать исходный код программы и перекомпилировать ее для обращения к новым функциям. Вот ядро программного кода: type TIntFunction = function (I- Integer). Integer: stdcall; const DllName = 'Firstdll.dll procedure TForml.ButtonlClick(Sender: TObject): var HInst: THandle: FPointer- TFarProc, MyFunct TIntFunction: begin HInst := SafeLoadLibrary (DllName). -if UTn<tt > fi thpn 0464
Дополнительные особенности Delphi-DLL 465 try FPointer := GetProcAddress (HInst, PChar (Editl.Text)): if FPointer <> nil then begin MyFunct = TIntFunction (FPointer). SpmEditl. Value •= MyFunct (SpinEditl.Value): end el se ShowMessage (Editl.Text + ' DLL function not found’); finally FreeLibrary (HInst); end else ShowMessage (DllName + ' library not found'); end. ВНИМАНИЕ -------------------------------------------------------------------------------- Поскольку библиотека использует диспетчер памяти компании Borland, динамически загружающая ее программа должна делать то же самое. Поэтому необходимо добавить модуль ShareMem в про- ект примера DynaCall. Как это ни странно, в предыдущих версиях Delphi, если библиотека не ис- пользует строчные значения, это было не так. Предупреждаю, что если пропустить включение модуля, то вы получите сообщение о грубой системной ошибке, которая может даже привести к останову отладчика на этапе вызова FreeLIrbary. Как в Delphi вызвать процедуру, после того как вы получили указатель на нее? Одним из решений является преобразование указателя в процедурный тип и по- следующий вызов процедуры с помощью переменной процедурного типа, как в предыдущем листинге. Обратите внимание, что определяемый процедурный тип должен быть совместим с определением процедуры в DLL. Это «ахиллесова пята» данного подхода: отсутствует проверка действительности типов параметров. В чем преимущество этого подхода? Теоретически он может использоваться для обращения к любой функции любой DLL в любое время. На практике он поле- зен при наличии различных DLL с совместимыми функциями или с одной DLL с несколькими совместимыми функциями, как в нашем случае. Методы Double и Triple можно вызвать, введя их имена в строке редактирования. А теперь, когда кто- либо дает вам DLL с новой функцией, получающей в качестве параметра и возвра- щающей целое значение, ее можно вызвать, введя имя в строке редактирования. Вам даже не придется перекомпилировать приложение. Рассматриваемый программный код позволяет компилятору и компоновщику игнорировать наличие DLL. При загрузке программы DLL загружается не сразу. Можно даже сделать программу более гибкой и позволить пользователю ввести имя используемой DLL. В некоторых случаях это является большим преимуще- ством. Программа может переключать DLL в ходе выполнения. Это то, чего не по- зволяет способ с непосредственным указанием библиотеки. Обратите внимание, что данный подход к загрузке DLL-функций широко распространен в макроязы- ках и используется многими средами визуального программирования. Только система, основанная на использовании компилятора и компоновщика (такая, как Delphi) может непосредственно использовать этот подход, который в общем более надежен и работает несколько быстрей. Мне кажется, косвенная загрузка, реализованная в примере DynaCall, полезна во многих случаях, но она мо- 0465
466 Глава 10. Библиотеки и пакеты жет быть очень мощной. С другой стороны, наибольшую значимость приобретает использование динамической загрузки в пакетах, включающих формы, что вы уви- дите к концу этой главы. Помещение форм Delphi в библиотеку Помимо написания библиотеки с функциями и процедурами имеется возможность поместить в динамическую библиотеку всю форму, построенную в Delphi. Ей мо- жет быть диалоговое окно или любой тип формы. Она может использоваться не только другими Delphi-программами, но и в других средах разработки или макро- языке, способном использовать динамически подсоединяемые библиотеки. После создания нового проекта-библиотеки все, что надо сделать, — это добавить в про- ект одну или несколько форм, а затем написать экспортирующие функции, кото- рые будут создавать и использовать эти формы. Например, функция, активизирующая модальное окно выбора цвета, может выглядеть так: function GetColor (Col: Longlnt): Longlnt; cdecl: var FormScroll: TFormScroll: begin // значение по умолчанию Result := Col; try FormScroll TFormScroll.Create (Application): try // инициализация данных FormScroll.SeiectedColor : = Col; // показать форму if FormScroll.ShowModal = mrOK then Result := FormScroll.SeiectedColor: finally FormScroll.Free: end. except on E: Exception do MessageDlg (.'Error In library: ' + E.Message. mtError, [mbOKJ. 0): end; end: Отличие от кода, который вы обычно пишете, заключается в использовании обработки исключений: о блок try/except защищает всю функцию. Любое исключение, генерируемое этой функцией, будет перехвачено, и появится соответствующее сообщение. Обра- ботка всех возможных исключений выполняется потому, что вызывающее при- ложение может быть написано на любом языке, в частности, на том, который «не умеет» обрабатывать исключения. Даже если вызывающей является Delphi- программа, иногда полезно использовать такой защитный подход; о блок try/finally защищает операции на форме, гарантируя, что объект-форма будет правильно уничтожен, даже если возникнет исключение. Проверкой возвращаемого значения метода ShowModal программа определяет результат функции. Я установил значение по умолчанию перед блоком try, гаран- 0466
Библиотеки в памяти: код и данные 467 тирующее, что он в любом случае будет выполнен (а также для того, чтобы избе- жать появления предупреждения компилятора о том, что результат функции мо- жет быть не определен). Фрагменты этого программного кода можно найти в проектах Form DLL и UseCol, находящихся в каталоге FormDLL. (Существует также файл WORDCALL.TXT, поясняю- щий, как вызвать процедуру с помощью макроса текстового редактора Word). При- мер также показывает, как в DLL добавить немодальное окно, но эти действия могут привести к некоторым проблемам. Немодальное окно и основное окно не синхро- низированы, поскольку DLL имеет свой собственный глобальный объект Application и собственную копию VCL. Такая ситуация может быть частично устранена путем копирования Handle объекта Application приложения в Handle объекта Application библиотеки. Не все проблемы решаются с помощью кода, находящегося в данном примере. Лучшее решение может заключаться в компиляции программы и биб- лиотеки в пакеты Delphi, при этом VCL-код и данные не смогут дублироваться. Но такой подход все еще может привести к сложностям: обычно не рекомендуется совместно использовать Delphi-DLL и пакеты. Так каким будет наилучшее пред- ложение? Для того чтобы формы библиотеки были доступны в других Delphi-npo- граммах, вместо открытых DLL используйте пакеты! Библиотеки в памяти: код и данные Перед переходом к рассмотрению пакетов я хочу остановиться на технической сто- роне динамических библиотек: как они используют память. Давайте начнем с раз- дела программного кода библиотеки, а затем перейдем к ее глобальным данным. Когда операционная система Windows загружает программный код библиотеки, также как и любой модуль кода, она должна выполнить операцию адресной при- вязки (fixup). Эта привязка состоит из корректировки адресов безусловных пере- ходов и вызовов внутренних функций на действительные адреса памяти, куда она загружена. Эффект этой операции заключается в том, что используемые адреса памяти, в которую загружен программный код, зависят от того, куда он был загру- жен. Это не проблема для исполняемых файлов, но может привести к значительным проблемам с библиотеками. Ели два исполняемых файла загружают одну и ту же библиотеку по одному и тому же базовому адресу, то в RAM (физической памяти) машины будет существовать только одна физическая копия программного кода DLL, что приводит к экономии памяти. Если в следующий раз библиотека загру- жается в уже используемый адрес памяти, то она должна быть перераспределена, то есть библиотека должна быть перемещена с последующим выполнением опера- ции адресной привязки. Это, в конечном счете, приведет к существованию в опе- ративной памяти второй физической копии DLL. Для проверки, на какой адрес памяти отображается текущий процесс функции, можно использовать технологию динамической загрузки, основанной на вызове ^Pl-функции GetProcAddress: Procedure TForml.Button3Click(Sender: TObject): var Uni I Tnrt . Т1ЬнЛ1 л . 0467
468 Глава 10. Библиотеки и пакеты begin HDLLInst = SafeLoadLibrary ('d//mem’): Label 1. Caption .= Format ('Address- %p'. [ GetProcAddress (HDLLInst. 'SetData ' УУ) FreeLibrary (HDLLInst); end: Этот код выводит (в надписи) адрес памяти функции внутри адресного про- странства приложения. Если запустить две программы на основе данного кода, они обычно показывают одинаковый адрес. Эта методика демонстрирует, что программ- ный код загружается в память только один раз. Еще одной технологией получения дополнительной информации о том, что происходит, является использование окна Modules среды Delphi, который показы- вает базовый адрес каждой библиотеки, к которой обращается данный модуль, а так- же адрес каждой функции в библиотеке: i Modules Name l BaseAddjess Path j VERSION dll $77820000 C WINDOWS \system32W LZ32 dll $75980000 C WINDOWS \system32\L C0MCTL32dll $71780000 C WINDOWS \system32\c Dllmem.tS $00800000 E: WooksW/codeYI OSDS.. _J dllmem .t? DIIMemU '+j SysConst >1 Syslnit ч; System >1 SysUtils Ll I . . h [SetDeta GetData SetShareData GetShar eData Finalization DIIMemU Finalization dllmem FreeT erminateProcs ImtDnveSpacePtr FreeAndNil Fmahzation SysUtils I Addras $00807838 $00807858 $00807884 $00807894 $00807D14 $00807094' $00807DA4 $00807DAC ' i $00807DC0 $00807DC8 $OO8O7E1O $00807EA8 | $00807F18 2 Важно знать, что базовый адрес DLL — это то, что запрашивается при установ- ке соответствующего параметра. В Delphi этот адрес определяется значением Image Base (База образа) страницы компоновщика диалогового окна Project Options (Па- раметры проекта). В библиотеке DllMem, например, я установил значение в $00800000. Для каждой из библиотек должно быть установлено различное значе- ние, отличное от значения любой системной библиотеки или другой библиотеки (пакета, ActiveX и т. д.). Опять же это можно просмотреть с помощью окна Module отладчика. Хотя установка базового адреса библиотеки не гарантирует уникальность раз- мещения, но это всегда лучше, чем игнорирование данной установки; в этом слу- чае размещение всегда имеет место, но шанс, что два различных исполняемых файла повторно разместят ту же библиотеку в том же самом адресе, невысок. СОВЕТ-----------------------------------------------------------------------— Для изучения любого процесса на любой машине можно использовать Process Explorer с сайта httpj //www.sysinternals.com. Это инструментальное средство имеет параметр настройки, позволяющий выделять повторно размещенные DLL. Проверьте эффект запуска той же программы с ее библиоте- ками на различных операционных системах (Windows 2000, Windows ХР и Windows ME), а также распределение неиспользуемого пространства. Это то, что касается размещения программного кода DLL, а где помещаются глобальные данные? Обычно каждая копия DLL имеет в адресном пространстве DLrOLTDO ТЛТТТОГП ТТ Г\ ТТ ТТ ГЛМГ О TJ ТТ СТ C'OrATZV РпАрТООиииТЛ ЫЛТТТТТ/Л 45UUI.TV (Дпиог/п ИМДОТГЯ йОЗ“ 0468
Библиотеки в памяти: код и данные 469 щожность совместно использовать глобальные данные библиотеки приложения- ми, использующими DLL. Наиболее известная технология совместного использо- вания данных заключается в использовании файлов, отображаемых на память. Я использовал эту методику для DLL, но она также может использоваться для со- вместного использования данных непосредственно приложениями. Этот пример я назвал DLLMem для библиотеки и UseMem — для демонстрацион- ного приложения. DLL-код имеет файл проекта, осуществляющий экспорт четы- рех процедур: library dllmem: uses SysUtils. DllMemU in 'DllMemU.pas'; exports SetData. GetData. GetShareData. SetShareData; end. Действительный программный код находится во втором модуле (DLLMemU.PAS), который содержит программный код этих четырех процедур, осуществляющих чтение или запись двух глобальных ячеек памяти. Эти ячейки содержат целое чис- ло и указатель на целое число. Вот объявление переменных и две Set-процедуры: var PlainData: Integer = 0: // совместно не используется ShareData: 'Integer: // используется совместно procedure SetData (I. Integer): stdcall: begin PlainData = I; end. procedure SetShareData (Г Integer): stdcall: begin ShareData' .= I. end. Совместный доступ к данным с помощью отображаемого на память файла Для данных, не предоставляемых в совместное пользование, ничего не надо де- лать. А для обращения к данным, предоставляемым в совместный доступ, DDL Должна создать отображаемый на память файл, а затем получить указатель на эту область памяти. Эти операции требуют использования двух вызовов API: ° CreateFileMapping требует в качестве параметра имя файла (или SFFFFFFFF — для использования виртуального файла в памяти), ряд атрибутов безопасности и за- щиты, размер данных и внутреннее имя (которое для совместного использова- ния отображенного файла множеством вызывающих приложений должно быть одинаковым); ° MapViewOfFile требует в качестве параметра дескриптор отображаемого на па- мять файла, ряд атрибутов и смещений, а также размер данных (повторно). 0469
470 Глава 10. Библиотеки и пакеты Вот программный код раздела initialization, который выполняется каждый раз, когда DLL загружается в пространство нового процесса (то есть однократно для каждого приложения, использующего эту DLL): var hMapFile; THandle; const VirtualFileName = 'ShareDllData': DataSize = sizeof (Integer); initialization // создать отображаемый на память файл hMapFile .= CreateFileMapping (SFFFFFFFF. nil. Page_ReadWrite. 0. DataSize. VirtualFileName); if hMapFile = 0 then raise Except!on.Create ('Error creating memory-mapped file"); 11 получить указатель на действительные данные ShareData := MapViewOfFile ( hMapFile. File_Map_Write. 0. 0. DataSize); Когда приложение завершается и DLL высвобождается, оно должно освобо- дить указатель на отображаемый файл и отображение файла: finalization UnmapViewOfFile (ShareData): CloseHandle (hMapFile): Форма демонстрационной программы UseMem имеет четыре строки редактиро- вания (две с присоединенными элементами UpDown), пять кнопок и надпись. Первая кнопка сохраняет значение первой строки редактирования в данные DLL получая значение из присоединенного элемента UpDown: SetData (UpDownl Position); Если вы щелкнете на второй кнопке, программа скопирует данные DLL во вто- рую строку редактирования: Edit2 Text := IntToStr(GetData); Третья кнопка используется для вывода адреса памяти функции с исходным кодом, представленным в начале этого раздела. Последние две кнопки в общем имеют такой же программный код, как и первые две, но они вызывают процедуру SetShareData и функцию GetShareData. При запуске двух копий этой программы можно увидеть, что каждая копия имеет собственное значение открытых глобальных данных DLL, в то время как значение совместно используемых данных — общее. Установите различные значения в двух программах, а затем получите их, и вы поймете, о чем я говорю (см. рис. 10.4). ВНИМАНИЕ------------------------------------------------------------------------------- Отображаемый на память файл резервирует минимум 64-килобайтный диапазон адресов и занима- ет физическую память в четырех страницах. Использование примером 4-6айтного целого значения в совместно используемой памяти является весьма дорогостоящим, особенно если такой же подход используется для организации совместного использования множества значений. Если необходимо совместно использовать несколько переменных, то все их следует поместить в одну область совме- стно используемой памяти (обращаться к различным переменным с помощью указателей или пост- роить для всех структуру-запись). 0470
; использование пакетов Delphi 471 Рис. 10.4. Если запустить две копии программы UseMem, то вы увидите, что глобальные данные в их DLL совместно не используются Использование пакетов Delphi В Delphi пакет компонентов является важным типом DLL. Пакеты позволяют объе- динять группу компонентов, а затем связывать эти компоненты либо статически (добавляя их откомпилированный код в исполняемый файл приложения), либо динамически (оставляя код компонента в DLL — пакете времени выполнения, ко- торый необходимо будет устанавливать вместе с программой совместно с другими необходимыми пакетами). В главе 9 мы рассматривали, как создаются пакеты. А те- перь я хочу подчеркнуть некоторые преимущества и недостатки двух видов связы- вания. Необходимо учитывать следующие моменты: О использование пакетов в качестве DLL значительно уменьшает размер испол- няемого файла; О компоновка модулей пакетов в программу позволяет предоставлять для уста- новки только часть программного кода пакета. Размер исполняемого файла приложения плюс размер необходимых DLL-пакетов всегда значительно боль- ше, чем размер статически скомпонованной программы. Компоновщик присо- единят только код, используемый программой, в то время как пакет должен связать все функции и классы, объявленные в разделах interface всех модулей, входящих в пакет; ° при установке нескольких Delphi-приложений, использующих одни и те же па- кеты, в конечном счете придется устанавливать меньший объем кода, посколь- ку пакеты времени выполнения используются совместно. Иначе говоря, раз У пользователя имеются стандартные пакеты времени выполнения Delphi, ему можно поставлять лишь очень маленькие по объему программы; ° если запустить несколько Delphi-приложений, основанных на одних и тех же пакетах, то экономится память; код пакетов времени выполнения для множе- ства Delphi-приложений загружается в память только один раз; ° не переживайте насчет установки большого исполняемого файла. Помните, что для внесения небольших изменений в программу можно использовать различ- 0471
472 Глава 10. Библиотеки и пакеты ные средства создания файла-заплатки (patch file), что позволит распростра- нять только файл, подвергшийся модификации, а не все файлы; о если поместить несколько форм программы в пакеты времени выполнения, их можно использовать одновременно несколькими приложениями. Однако при изменении этих форм придется перекомпилировать и основную программу и повторно устанавливать их у пользователя. Этот сложный вопрос будет рас- смотрен в следующем разделе; О пакет — это коллекция откомпилированных модулей (включающих классы, типы, переменные, процедуры), которые совершенно не отличаются от моду- лей программы. Единственным отличием является процесс создания. Код мо- дулей пакета и модулей основной программы, использующей их, остаются иден- тичными. Вероятно, это одно из ключевых преимуществ пакетов по сравнению с DLL. Контроль версий пакетов Очень важным и зачастую недооцененным элементом является распространение и обновление пакетов. При обновлении DLL можно предоставить новую версию, и использующий ее исполняемый файл будет по-прежнему работать (только если в новой версии не удалены существующие экспортируемые функции или не изме- нены их параметры). Однако когда распространяется Delphi-пакет, при обновлении пакета или при внесении изменений в раздел interface любого модуля пакета придется перекомпи- лировать все приложения, использующие этот пакет. Этот шаг также необходим при добавлении методов или свойств классов, но не требуется при добавлении новых глобальных идентификаторов (или изменении чего-либо, чем не пользует- ся клиентское приложение). Не существует проблем при изменении только разде- ла implementation модулей пакета. DCU-файл имеет метку версии, основанную на временной метке и контрольной сумме, вычисленной на основе интерфейсного раздела модуля. При внесении из- менений в раздел interface каждый модуль, основанный на данном модуле, должен быть перекомпилирован. Компилятор сравнивает временную отметку и конт- рольную сумму модуля предыдущей компиляции с новой временной отметкой и контрольной суммой и решает, должен ли быть перекомпилирован «зависимый» модуль. По этой причине при установке новой версии Delphi, в которой имеются измененные системные модули, необходимо перекомпилировать каждый модуль. В версии Delphi 3 (в которой впервые появились пакеты) компилятор добав- лял в библиотеку пакета дополнительную функцию с именем контрольной суммы пакета, полученной на основе контрольной суммы входящих в него модулей и кон- трольной суммы необходимых ему пакетов. Эта функция контрольной суммы впос- ледствии вызывалась программой, использующей пакет, что приводило к сбою программы при запуске. В Delphi 4 и последующих версиях, вплоть до Delphi 7, были ослаблены огра- ничения времени выполнения для пакетов. (Тем не менее ограничения времени разработки для DCU-файлов остались прежними.) Контрольная сумма пакетов больше не проверяется, поэтому можно непосредственно модифицировать моду 0472
Использование пакетов Delphi 473 ли, входящие в пакет, и устанавливать новую версию пакета, используемую при- ложением. Поскольку обращение к методам производится по имени, нельзя уда- лять любые существующие методы. Нельзя даже изменять их параметры ввиду использования технологии name-mangling, защищающей методы пакета от изме- нений параметров. Удаление ссылок на методы из вызывающей программы приведет к останову этой программы в ходе процесса загрузки. Однако при внесении других измене- ний сбой в программе может произойти неожиданно в ходе выполнения. Напри- мер, если заменить компонент, помещенный на форму, откомпилированную в пакет, подобным компонентом, то вызывающая программа может по-прежнему обратить- ся к компоненту, расположенному в этом месте памяти, хотя сейчас он находится в другом! Если вы решили следовать этому порочному пути изменений интерфейсов мо- дулей пакета без перекомпилирования всех использующих его программ, необходи- мо хотя бы ограничить эти изменения. При добавлении в форму новых свойств или невиртуальных методов вы должны поддерживать полную совместимость с уже существующими программами, использующими этот пакет. Кроме того, добавление полей и виртуальных методов может повлиять на внутреннюю структуру класса, что приводит к проблемам с существующими программами, которые ожидают иное расположение данных класса и таблицы виртуальных методов (VMT). ВНИМАНИЕ ----------------------------------------------------------------- Здесь я имею в виду распространение откомпилированных программ, разделенных на ЕХЕ-файлы и пакеты, а не предоставление компонентов другим Delphi-разработчикам. В последнем случае пра- вила контроля версии более строгие, и необходимо быть особенно осторожным. После всего сказанного я рекомендовал бы никогда не изменять интерфейс любого модуля, экспортируемого пакетом. Для этого можно добавить в пакет мо- дуль с функциями создания формы (как представленные ранее DLL с формами) и использовать их для доступа к другому модулю, определяющему эту форму. Хотя не существует способа спрятать модуль, скомпонованный в пакет, но если вы никогда не будете непосредственно использовать класс, определенный в этом мо- дуле, а использовать его только через другие процедуры, вы получите дополни- тельные удобства в его изменении. Для модификации формы в пакете, не влияя на исходную версию, также можно использовать наследование форм. Самое строгое правило пакетов касается разработчиков компонентов: для дол- госрочного размещения и обслуживания кода в пакетах спланируйте наличие глав- ного выпуска и младших обслуживающих выпусков. Главный выпуск пакета по- требует перекомпиляции из исходного текста всех клиентских программ; файл пакета должен быть переименован в соответствии с номером новой версии, а ин- терфейсные разделы модулей должны быть изменены соответствующим образом. Обслуживающие выпуски пакета для сохранения полной совместимости с суще- ствующими программами и модулями должны быть ограничены изменениями ре- ализации, как это делается компанией Borland в ее Update Packs (пакетах обнов- ления). 0473
474 Глава 10. Библиотеки и пакеты Формы в пакетах В главе 9 мы рассмотрели использование пакетов компонентов в Delphi-приложени- ях. Теперь мы обсудим использование пакетов и DLL для разделения приложения, поэтому я начну с разработки пакетов, содержащих формы. Ранее уже упоминалось, что можно использовать формы, помещенные в DLL, но это ведет к довольно боль- шим проблемам. Если вы создаете как исполняемый файл, так и библиотеку в Delphi, использование пакетов может обеспечить более удачное и беспроблемное решение. На первый взгляд может показаться, что пакеты Delphi — единственный спо- соб распространения компонентов, которые должны быть установлены в среду. Однако, кроме того, эти пакеты могут использоваться как способ структурирова- ния программного кода, но, в отличие от DLL, сохраняющий всю мощь ООП Delphi. Рассмотрим такую ситуацию: пакет — это коллекция откомпилированных моду- лей, и ваша программа использует некоторые из этих модулей. Модули, к которым обращается программа, будут скомпилированы в исполняемый файл, если только вы не укажете среде Delphi поместить их в пакет. Как упоминалось ранее, это одна из основных причин использования пакетов. Для того чтобы приложение было настроено таким образом, что его программ- ный код был разделен между одним и более пакетами и основным исполняемым файлом, все, что необходимо сделать, — это скомпилировать некоторые модули в пакет, а затем настроить параметры основной программы на динамическое подсо- единение этого пакета. Например, я сделал копию обычной формы выбора цвета и переименовал ее модуль РасkScrollF; затем создал новый пакет и добавил в него этот модуль (см. рис. 10.5). Package - PackWithForm.dpk £->i j s£i йй I О T №И* | i NJ* ,T| Compile I Add Remove | insM Opto Fies Contains F В PackS crollF PackS crolF H FormScroll В SJ Requires И vcldcp E \books\md7code\1 OXPeckForm E \books\md7code\10\PackForm E \books\md7code\1 OXPackForm Рис. 10.5. Структура пакета, содержащего форму Перед компиляцией этого пакета необходимо вместо стандартного подкатало- га по умолчанию /Projects/Bpl указать текущую папку в качестве выходного ката- лога. Для этого перейдите на страницу Directories/Conditional диалогового окна Project Options пакета и установите текущий каталог (для краткости — одна точка) в каче- стве Output directory (для BPL и DCP). Скомпилируйте пакет, но не устанавливай- те его в среду Delphi — в этом нет необходимости! Сейчас можно создать обычное приложение и написать стандартный программ- ный код, используемый в программе для вывода подчиненной формы: 0474
формы в пакетах 475 uses PackScrol1F: procedure TForml BtnChangeClick(Sender: TObject): var FormScrol1 TFormScrol1: begin FormScroll := TFormScroll.Create (Application): try // инициализировать данные FormScroll.SeiectedColor = Color: // показать форму if FormScroll ShouModal = mrOK then Color = FormScroll .SeiectedColor: finally FormScroll Free: end. end. procedure TForml.BtnSelectClick(Sender: TObject); var FormScrol1: TFormScrol1. begin FormScroll := TFormScroll.Create (Application): // инициализировать данные и UI FormScroll SeiectedColor .= Color: FormScroll .BitBtnl Caption .= 'Apply'; FormScrol1.BitBtnl OnClick •= FormScroll.ApplyClick; FormScroll.BitBtn2.Kind := bkClose: // показать форму FormScroll Show: end: Одним из преимуществ такого подхода является возможность обращения к форме, откомпилированной в пакет, тем же программным кодом, что и к форме, откомпилированной в эту программу. При компиляции этой программы модуль формы будет включен в программу. Для того чтобы сохранить модуль формы в пакете, необходимо для данного приложения использовать пакеты времени вы- полнения и вручную добавить пакет PackWithForm в список пакетов времени вы- полнения (это не предлагается IDE, поскольку вы не установили пакет в среду Delphi). После выполнения этого шага скомпилируйте программу. Она будет вести себя как обычно, но теперь форма находится в DLL-пакете, и имеется возможность из- менить форму в пакете, перекомпилировать его и запустить приложения. Обрати- те внимание, что для большинства изменений, относящихся к разделу interface модулей пакета (например, добавление в форму компонента или метода), также необходимо перекомпилировать и программу, вызывающую этот пакет. СОВЕТ------------------------------------------------------------------------------------- Пакет и программу, проверяющую это, можно найти в каталоге PackForm исходного кода, относя- щегося к данной главе. Программный код следующего примера располагается в том же каталоге. Пакеты и проекты упоминаются в файле группы проектов (BPG), находящемся в том же каталоге. 0475
476 Глава 10. Библиотеки и пакеты Загрузка пакетов во время выполнения В предыдущем примере я указал, что пакет PackWithForm является пакетом време- ни выполнения. Это означает, что пакет требуется для выполнения программы и так же, как и DLL, загружается при запуске программы. Так же, как DLL, пакеты можно загружать динамически. Окончательная программа получится более удоб- ной, будет запускаться быстрей и использовать меньше памяти. Важным элементом, который необходимо помнить, является тот факт, что вме- сто API-функций Windows Load Li brary/Safe LoadLi brary и FreeLibrary лучше вызывать функции Delphi LoadPackage и UnloadPackage. Функции, предоставляемые Delphi, загружают пакеты и, помимо этого, вызывают соответствующий им код инициа- лизации и завершения (initialization и finalization). Помимо этого важного момента (который легко реализуется, когда вы осведом- лены о нем) программа потребует дополнительного программного кода, поскольку вы не сможете обратиться из основной программы к модулю, содержащему фор- му. Нельзя непосредственно использовать класс формы или обратиться к ее свой- ствам или компонентам, по крайней мере, с помощью стандартного программного кода Delphi. Обе проблемы можно разрешить с помощью ссылок класса, регистра- цией класса и RTTI. Начнем с первого подхода. В модуле формы, входящем в пакет, я добавил сле- дующий код инициализации: initialization Register-Class (TFormScrol 1). При загрузке пакета основная программа может использовать функцию Delphi GetClass для получения ссылки класса зарегистрированного класса, а затем вызы- вать конструктор Create для этой ссылки класса. Для решения второй проблемы я определил свойство SelectedColor формы, вхо- дящей в пакет, опубликованным, что сделало его доступным посредством RTTI. После этого я заменил программный код доступа к этому свойству (FormScroll.Color) на следующий: SetPropValue (FormScroll 'SelectedColor'. Color). Вот программный код, используемый в основной программе, суммирующий все изменения (приложение DynaPackForm), выводящий модальное окно из динамиче- ски загружаемого пакета: procedure TForml BtnChangeClick(Sender TObject). var FormScroll TForm FormClass TFormClass. HandlePack HModule. begin // попытаться загрузить пакет HandlePack = LoadPackage ('PackWithForm bpl"). if HandlePack > 0 then begin FormClass = TFormClass(GetClass (.’TFormScroll")). if Assigned (FormClass) then begin FormScroll = FormClass Create (Application). try 0476
Формы в пакетах 477 // инициализировать данные SetPropValue (FormScroll. 'SelectedColor'. Color). // показать форму if FormScroll ShowModal = mrOK then Color = GetPropValue (FormScroll. 'SelectedColor'): finally FormScroll Free: end, end else ShouMessage ('Form class not found'): UnloadPackage (HandlePack). end else ShowMessage ('Package not found'). end: Обратите внимание, что программа сразу выгружает пакет, как только она ис- пользовала его. Этот шаг не является обязательным. Я мог бы посоветовать вам переместить вызов UnloadPackage в обработчик OnDestroy формы и избежать повтор- ной загрузки пакета после первого раза использования. А теперь можно попробовать запустить программу при недоступном пакете. Вы увидите, что она запустится, только при щелчке на кнопке Change сообщит, что не может найти пакет. В этой программе пе надо использовать пакеты времени вы- полнения для хранения модуля за пределами исполняемого файла, поскольку в программном коде нет обращения к этому модулю. Кроме того, вообще нет необ- ходимости перечислять пакет PackWithForm в списке пакетов времени выполнения, иначе программа включит глобальные переменные VCL (например, объект Appli- cation) и динамически загружаемый пакет будет содержать иную версию, посколь- ку он в любом случае будет обращаться к пакетам VCL. ВНИМАНИЕ ------------------------------------------------------------------------------ При закрытии программы, динамически загружающей пакет, может произойти нарушение доступа. Зачастую это происходит ввиду того, что объект, чей класс определен в пакете, остается в памяти даже после того, как пакет будет выгружен. При выключении программы она может попытаться освободить этот объект вызовом метода Destroy несуществующей VMT и вызвать, таким образом, Ошибку. Я по опыту знаю, что такой тип ошибок очень сложно отследить и исправить. Я предлагаю перед выгрузкой пакета убедиться в уничтожении всех объектов. Использование интерфейсов в пакетах Обращение к классам форм посредством методов и свойств значительно проще, чем использование в тех же целях RTTI. Для создания объемного приложения я обязательно стараюсь использовать интерфейсы и иметь множество форм, каж- дая из которых реализует несколько стандартных интерфейсов, определенных про- граммой. Мой пример не сможет определить «правильность» архитектуры подобно- го типа, поскольку она предназначена для крупных приложений, но я постарался создать программу, показывающую, как эта идея может быть применена на практике. СОВЕТ---------------------------------------------------------- Если вы не очень хорошо знакомы с интерфейсами, то перед изучением данного раздела я предла- гаю обратиться к соответствующему разделу главы 2. 0477
478 Глава 10 Библиотеки и пакеты Для построения проекта IntfPack я использовал три пакета плюс демонстраци- онное приложение Два из этих пакетов (IntfFormPack и IntfFormPack2) определяют альтернативные формы, используемые для выбора цвета Третий пакет (IntfPack) содержит предоставляемый в совместный доступ модуль, используемый обоими пакетами Этот модуль содержит определение интерфейса Я не мог добавить его в оба пакета, поскольку невозможно загрузить два пакета, имеющих то же имя мо- дуля (даже при загрузке в ходе выполнения) Единственным файлом пакета IntfPack является модуль IntfColSel, представлен- ный в листинге 10 1 Он определяет общий интерфейс (в реальном приложении их будет несколько) плюс список зарегистрированных классов, он подобен подходу, используемому в RegisterClass Delphi, но делает доступным весь список, поэтому его легко просмотреть Листинг 10.1. Модуль IntfColSel пакета IntfPack unit IntfColSel interface uses Graphics Contnrs type IColorSelect = interface [ {3F961395 71F6 4822 BD02 3B475FF516D4} ] function Display (Modal Boolean = True) Boolean procedure SetSelColor (Col TColor) function GetSelColor TColor property SelColor TColor read GetSelColor write SetSelColor end procedure RegisterColorSelect (AClass TClass) var ClassesColorSelect TClassList implementation procedure RegisterColorSelect (AClass TClass) begin if ClassesColorSelect IndexOf (AClass) < 0 then ClassesColorSelect Add (AClass) end initialization ClassesColorSelect - TClassList Create finalization ClassesColorSelect Free end После того как интерфейс доступен, можно определить реализующие его фор- мы, как в следующем фрагменте, взятом из IntfFormPack type TFormSimpleColor = class(TForm IColorSelect) private 0478
формы в пакетах 479 procedure SetSelColor (Col TColor) function GetSelColor TColor public function Display (Modal Boolean = True) Boolean Два метода доступа читают и записывают значение цвета из некоторых компо- нентов формы (в данном случае — элемента управления ColorGnd), в то время как метод Display в зависимости от параметра вызывает либо Show, либо ShowModal function TFormSimpleColor Display(Modal Boolean) Boolean begin Result = True // по умолчанию if Modal then Result = (ShouModal = mrOK) else begin BitBtnl Caption = Apply BitBtnl OnClick = ApplyClick BitBtn2 Kind = bkClose Show end, end. Как видно из этого фрагмента, когда форма становится немодальной, кнопка ОК меняет заголовок на Apply (Применить) И, наконец, модуль имеет код регист- рации в разделе initialization, который выполняется при динамической загрузке пакета Reg1sterColorSelect (TFormSimpleCol or) Второй пакет (IntfFormPack2) имеет аналогичную архитектуру, но другую фор- му Можете просмотреть ее в исходном коде (я здесь не буду обсуждать вторую форму) Используя эту архитектуру, можно построить более совершенную и удобную основную программу, основанную на одной форме При создании формы она оп- ределяет список пакетов (HandlesPackages) и загружает их Я жестко привязал пакет в программный код примера, но можно организовать поиск пакета в теку- щем каталоге или использовать файл конфигурации для придания архитектуре большей гибкости После загрузки пакетов программа показывает зарегистриро- ванные классы в списке Вот программный код методов LoadDynaPackage и FormCreate procedure TFormUselntf FormCreate(Sender TObject) var I Integer begin // загрузить все пакеты времени выполнения HandlesPackages = TList Create LoadDynaPackage ( IntfFormPack bpl ) LoadDynaPackage ( IntfForrnPack2 bpl ) // добавить имена классов и выбрать первый for I = о to ClassesColorSelect Count - 1 do IbClasses Items Add (ClassesColorSelect [I] ClassName) IbClasses Itemindex = 0 end Procedure TFormUselntf LoadDynaPackage(PackageName string) 0479
480 Глава 10. Библиотеки и пакеты var Handle: HModule; begin // попытаться загрузить пакет Handle := LoadPackage (PackageName); if Handle > 0 then // добавить в список для последующего удаления HandlesPackages.Add (Ротnter(Handlе)) else ShouMessage (.'Package ' + PackageName + ' not found"); end: Основная причина содержания списка дескрипторов пакетов — обеспечить воз- можность их выгрузки при завершении программы. Эти дескрипторы не нужны для обращения к формам, определенным в этих пакетах; код времени выполнения использует их для создания и вывода форм с помощью соответствующих классов компонентов. Вот фрагмент программного кода, используемый для вывода немо- дальной формы (определяется флажком): var AComponent: TComponent; ColorSelect: IColorSelect: begin AComponent := TComponentClass (ClassesColorSelectILbClasses.Itemindex]).Create (Application): ColorSelect := AComponent as IColorSelect: ColorSelect. Sei Col or := Color: ColorSelect.Display (False); Программа использует функцию Supports для проверки перед использованием интерфейса, что форма действительно поддерживает его (также учитывается мо- дальная версия формы); но его суть по существу представлена в предшествующих четырех выражениях. Между прочим, обратите внимание, что этот код не требует наличия формы. Хорошим упражнением будет добавление в архитектуру пакета с компонентом, инкапсулирующим диалоговое окно выбора цвета или исходящим от него. ВНИМАНИЕ------------------------------------------------------------------------------- Основная программа обращается к модулю, содержащему объявление интерфейса, но не должна присоединять этот файл. Лучше, если она будет использовать пакет времени выполнения, содер- жащий этот модуль, как это делают динамически загружаемые пакеты. Иначе основная программа будет использовать различные копии того же самого кода, включая различные списки глобальных классов. Это именно тот список, который не должен дублироваться в памяти. Структура пакета Вы можете удивиться: возможно ли узнать, связан ли модуль с исполняемым фаи- лом или он является частью пакета времени выполнения. Мало того, что это воз- можно в Delphi, но в этой среде можно полностью исследовать общую структуру приложения. Компонент может использовать недокументированную глобальную переменную ModulelsPackage, объявленную в модуле Syslnit. Эта переменная ни- когда не понадобится, но технически имеется возможность иметь различный 0480
Структура пакета 481 программный код компонента в зависимости от того, находится он в пакете или нет. Представленный ниже программный код извлекает имя пакета времени вы- полнения, содержащего компонент, если таковой имеется: var fPackName: string; begin // получить имя пакета SetLength (fPackName. 100); if ModulelsPackage then begin GetModuleFileName (HInstance. PChar (fPackName), Length (fPackName)); fPackName := PChar (fPackName) // адресная привязка длины строчного значения end else fPackName := 'Not packaged'; Помимо вызова из компонента сведений о пакете (как в предыдущем программ- ном коде), это также можно выполнить функцией GetPackagelnfoTable вызываемой из специальной точки входа библиотек пакета. Эта функция возвращает некото- рые сведения о пакете, которые Delphi хранит как ресурс и включает в DLL-пакет. К счастью, нет необходимости использовать низкоуровневые методы обращения к этим сведениям, потому что для манипулирования ими Delphi предоставляет ряд функций высокого уровня. Для обращения к сведениям пакета можно использовать две функции: О GetPackageDescription возвращает строчное значение, содержащее описание па- кета. Для вызова этой функции необходимо предоставить в качестве единствен- ного параметра имя модуля (библиотеки пакета); О GetPackagelnfo не возвращает явно сведения о пакете. Вместо этого вы передае- те ее функции, которая вызывает ее для каждого элемента внутренней структу- ры данных пакета. Практически GetPackagelnfo будет вызывать вашу функцию для каждой функции, содержащейся в модуле пакета и требуемых пакетов. Кроме того, GetPackagelnfo устанавливает несколько флагов в переменной Integer. Вызовы этих двух функций позволяют получать внутреннюю информацию о пакете, но как узнать, какие пакеты использует приложение? Эти сведения можно получить, осуществив с помощью низкоуровневых функций поиск в исполняемом файле, но Delphi снова облегчает эту задачу, предоставляя упрощенный способ. Функция EnumModules не выполняет непосредственный возврат сведений о моду- лях приложения, но позволяет передать их функции, которая вызывает их для каж- дого модуля приложения, для основного исполняемого файла и для каждого паке- та. на котором основано приложение. Для демонстрации этого подхода я создал программу, которая выводит сведе- пия о модуле и пакетах в компоненте TreeView. Каждый узел первого уровня соот- ветствует модулю; в каждом модуле я создал поддерево, выводящее содержащиеся в Данном модуле пакеты и требуемые пакеты, а также описание пакетов и флаги ком- пилятора (RunOnly и DesignOnly). Результат работы этого примера см. на рис. 10.6. Помимо компонента TreeView в основную форму я добавил несколько дополни- тельных компонентов, но спрятал их от просмотра: DBEdit, Chart и FilterComboBox. ини добавлены лишь для увеличения числа пакетов времени выполнения, исполь- 0481
482 Глава 10. Библиотеки и пакеты Package Information E'\bac4-:.s\rTtd7code\1CsPacHntcAPa'llnio о? Я Contains Packinfo (Main Unit) PackForm Syslmt Requires C WINDOWS\Sy$tem32\vcldb70 bpl +1 Contains Я Requites Description Borland Database Components Run Only C WINDOWS\System32\dbrtl70 bpl C WINDOWS\System32\tee70 bpl +1 Contains >1 Requites Description TeeChart Components Run Only C WINDOWS\System32\vcl70 bpl Я Contains vcl (Main Unit Package Unit) Themes ComCtils Punters Const* Syslmt Graphics Pnrmt Рис. 10.6. Результат работы примера Packinfo с подробностями об используемых им пакетах зуемых этим приложением, помимо повсеместно используемых VCL- и RTL-па- кетов Единственным методом класса формы является FormCreate, который вызы- вает функцию подсчета модулей: procedure TForml FormCreate(Sender TObject) begin EnumModules(ForEachModule ml) end Функция EnumModules получает два параметра: функция обратного вызова (в данном случае — ForEachModule) и указатель на структуру данных, которую бу- дет использовать функция обратного вызова (в данном случае — nil, поскольку этого не требуется). Функция обратного вызова должна получать два параметра: значение HInstance и нетипизированный указатель, а возвращать должна значение логического типа Функция EnumModules для каждого модуля будет по очереди вы- зывать функцию обратного вызова, передавая в качестве первого параметра эк- земпляр дескриптора каждого модуля, а в качестве второго — указатель на струк- туру данных (в данном случае — nil): function ForEachModule (HInstance Longint Data Pointer) Boolean var Flags Integer ModuleName ModuleDesc string ModuleNode TTreeNode begin with Forml TreeViewl Items do begin onnth f Mnrlt il лМато 2ПП) 0482
Структура пакета 483 GetModuleFileName (HInstance PChar (ModuleName) Length (ModuleName)) ModuleName = PChar (ModuleName) // адресная привязка ModuleNode - Add (ml ModuleName) // получить дескриптор и добавить фиксированные узлы ModuleDesc = GetPackageDescription (PChar (ModuleName)), ContNode = AddChild (ModuleNode Contains ) ReqNode = AddChild (ModuleNode Requires") // добавить сведения если модуль является пакетом GetPackagelnfo (HInstance ml Flags ShowInfoProc) if ModuleDesc <> then begin AddChild (ModuleNode 'Description ' + ModuleDesc) if Flags and pfDesignOnly = pfDesignOnly then AddChild (ModuleNode 'Design Only') if Flags and pfRunOnly = pfRunOnly then AddChild (ModuleNode 'Run Only ). end end Result = True end Как видно из представленного программного кода, функция ForEachModule на- чинается с добавления имени модуля в качестве основного узла дерева (вызовом метода Add объекта TreeViewl.Items и передаче nil в качестве первого параметра). После этого она добавляет фиксированные дочерние узлы, хранящиеся в перемен- ных ContNode и ReqNode, объявленных в разделе implementation данного модуля Далее программа вызывает функцию GetPackagelnfo и передает ее функции об- ратного вызова ShowInfoProc для предоставления списка модулей приложения или пакета В конце функции ForEachModule, если модуль оказался пакетом, программа предоставляет дополнительные сведения, такие как описание и флаги компилятора (программа узнает пакет по тому, что его описание не является пустой строкой). Ранее я упоминал о передаче другой функции обратного вызова (ShowInfoProc) в функцию GetPackagelnfo, которая в свою очередь вызывает функцию обратного вызова для каждого содержащегося или требуемого модуля Эта процедура созда- ет строчное значение, описывающее пакет и его основные флаги (добавляемые в круглых скобках), а загем, в зависимости от типа модуля, вставляет эту строку в один из двух узлов (ContNode и ReqNode). Тип модуля можно определить изуче- нием параметра NameType Вот полный программный код второй функции обрат- ного вызова Procedure ShowInfoProc (const Name string NameType TNameType. Flags Byte. Param Pointer) var FlagStr string begin FlagStr = ' if Flags and ufMainllmt <> 0 then FlagStr = FlagStr + Main Unit ' if Flags and ufPackageUnit <> 0 then FlagStr = FlagStr + Package Unit ' if Flags and ufWeakUmt <> 0 then 0483
484 Глава 10. Библиотеки и пакеты FlagStr := FlagStr + 'Weak Unit if FlagStr <> ' ' then FlagStr :='('+ FlagStr + with Forml TreeViewl.Items do case NameType of ntContainsUmt: AddChild (ContNode. Name + FlagStr); ntRequiresPackage: AddChild (ReqNode. Name): end. end: Что далее? В этой главе вы видели, как можно вызвать функции, находящиеся в DLL, и как создавать DLL в среде Delphi. После общего ознакомления с динамическими биб- лиотеками мы перешли к пакетам, охватив, в частности, вопрос помещения в па- кет форм и других классов. Это удобная методика для разделения Delphi-прило- жения на множество исполняемых файлов. При рассмотрении пакетов мы изучили, как можно использовать дополнительные методики включения RTTI и интерфей- сов для получения динамической и гибкой архитектуры приложения. Мы вернемся к вопросам библиотек, открывающих объекты и классы при рас- смотрении СОМ и OLE в главе 12. А сейчас давайте перейдем к изучению вопроса, связанного с архитектурой Delphi-приложений: использование средств моделиро- вания, а также рассмотрим примеры других ООП-технологий. 0484
Л Моделирование и ООП- программирование Когда компания Borland хотела обеспечить UMU-решение разработки для редак- ций Enterprise и Architect версии Delphi 7, она решила связать его с ModelMaker, разработанным голландской компанией ModelMaker Tools (www.ModelMakertools.com). ModelMaker — это высококачественное средство UML-разработки, с возможностью интеграции в IDE Delphi. Но по мере знакомства с ModelMaker в ходе этой главы вы поймете, что ModelMaker — это не только средство составления UML-схем. Мы, ко- нечно, рассмотрим эту возможность, но помимо нее также познакомимся и с мно- жеством других особенностей этого средства. Кроме того, будет представлен кон- цептуальный обзор продукта, который позволяет добиваться еще большего. ModelMaker появился почти вместе со средой Delphi и со временем накопил па- раметры поддержки почти всего языка Delphi, а также множество удобных для программистов функций. Как результат — громадный набор возможностей, кото- рые на первый взгляд могут испугать. Интерфейс пользователя ModelMaker содер- жит более 100 форм и без соответствующей подготовки неподготовленный чело- век может оказаться в замешательстве. Положитесь на меня и скоро вы сможете смело пользоваться ModelMaker. Хотя ModelMaker обычно относят к средству построения UML-схем, я предпо- читаю воспринимать его как настраиваемый, предназначенный для использова- ния в Delphi, расширяемый инструмент составления UML- и CASE-схем полного Никла. Я называю его «Delphi-инструментом», поскольку он предназначен для обработки программного кода Delphi. Например, при работе со свойствами диало- говые окна ModelMaker представляют параметры, которые специфичны для ключе- вых слов Delphi и самой концепции. «Настраиваемым» я называю ModelMaker по- тому, что генерацией программного кода Delphi на основе построенной объектной Модели могут управлять сотни параметров. ModelMaker является «расширяемым», потому что имеющийся API-интерфейс OpenTools обеспечивает создание встраи- ваемых экспертов, расширяющих функциональность продукта. ModelMaker харак- теризуется как «средство полного цикла» ввиду того, что он предлагает функцио- Нальные возможности, используемые на всех этапах стандартного цикла разработ- ки- И, наконец, ModelMaker может рассматриваться как «CASE-средство», поскольку °н автоматически может генерировать ясный, даже с некоторой избыточностью Unified Modeling Language — унифицированный язык моделирования. 0485
486 Глава 11. Моделирование и ООП-программирование программный код, требуемый для объявления классов Delphi, оставляя вам воз- можность лишь предоставить операционный код классов. ПРИМЕЧАНИЕ--------------------------------------------------------— Эта глава написана совместно с Робертом Лии (Robert Leahey), благодаря его глубоким познаниям и богатому опыту работы с ModelMaker. В мире программного обеспечения Роберт Лии является разработчиком, программистом, автором и лектором. Как музыкант он профессионально занимает- ся музыкой более 20 лет и закончил Северо-техасский университет (University of North Texas) в области теории музыки. Через свою компанию Thoughtsmithy (www.thoughtsmithy.com) Роберт предлагает услуги по обучению, консультированию по вопросам коммерческого программного обеспечения, аудиопродукции и крупномасштабной скульптуры из конструктора LEGO. Он живет в северном Те- хасе со своей женой и дочерьми. В этой главе рассматриваются следующие вопросы: О понятие ModelMaker; О моделирование и UML; О особенности генерации кода в ModelMaker; О документация и макросы; О регенерация программного кода из составляющих; О реализация шаблонов. Понятие внутренней модели ModelMaker Перед тем как перейти к рассмотрению поддержки URL в ModelMaker и других осо- бенностей, необходимо чтобы вы обязательно поняли саму концепцию: как Model- Maker управляет программным кодом модели. В отличие от Delphi и других редак- торов, ModelMaker не выполняет непрерывного анализа файла исходного кода для визуального представления его содержания. Рассмотрим Delphi: любая удобная функция IDE, используемая для изменения программного кода, осуществляет не- посредственное изменение содержания файла исходного кода (который для за- крепления этих изменений впоследствии может быть сохранен). В отличие от него, ModelMaker обслуживает внутреннюю модель, представляющую ваши классы, про- граммный код, документацию и т. д., а потом на основании этой модели в дальней- шем генерирует файлы исходного кода. Когда с помощью различных редакторов продукта ModelMaker осуществляется редактирование модели, изменения приме- няются только к внутренней модели, а не к внешним файлам исходного программ- ного кода; по крайней мере, до тех пор, пока вы не скажете ModelMaker сгенериро- вать внешние файлы. Это различие должно вызвать некоторое разочарование. Еще один необходимый для понимания момент заключается в том, что Model- Maker может представить в пользовательском интерфейсе одну модель программ- ного кода множеством различных видов (представлений). Эта модель может про- сматриваться и редактироваться, например, как иерархия классов, или как список модулей, содержащих классы. Члены классов могут сортироваться, фильтровать- ся, группироваться и редактироваться различными способами. Любое количество видов может быть представлено во множестве надстроек, доступных для ModelMaker. 0486
Моделирование и URL 487 Цо наиболее важный для рассмотрения — редактор UML-схем, который сам по себе является еще одним вариантом просмотра модели. При визуализации эле- ментов модели (классов или модулей) в схемах создается визуальное представле- ние элементов модели кода; если со схемы удалить какое-либо обозначение (sym- bol), это не обязательно приводит к удалению элемента из модели — вы просто удаляете его представление в схеме. Еще одно соображение: хотя ModelMaker предлагает множество мастеров и сис- тем автоматизации в области визуализации, этот продукт не будет осуществлять чтение готового кода и волшебным образом создавать привлекательные URL-cxe- мы безо всякого участия с вашей стороны. При импортировании существующего исходного кода и добавления классов в схему вам для придания удобного для ра- боты вида придется самостоятельно упорядочить обозначения в схеме. Моделирование и URL UML (Unified Modeling Language — унифицированный язык моделирования) — это графическая нотация, используемая для изображения анализа и дизайна программ- ных проектов и обеспечивающая его связь с другими проектами. UML — это неза- висимый язык, но он предназначен для описания объектно-ориентированных про- ектов. Как подчеркивают создатели UML, он сам по себе не является методологией; но может быть использован в качестве средства описания, не обращая внимания на то, что вы предпочитаете процесс конструирования. Моя цель посмотреть на UML-схемы с точки зрения Delphi-программиста, ис- пользующего ModelMaker. Углубленное изучение ModelMaker выходит за рамки дан- ной главы. Схемы классов Одной из самых используемых UML-схем, поддерживаемых ModelMaker, является схема класса (class diagram). Схемы классов могут выводить всевозможные отно- шения классов, но в самом простом варианте этот тип схем изображает набор клас- сов или объектов, а также статические отношения между ними. Например, на Рис. 11.1 представлена схема класса, содержащая классы из примера NewDate, пред- ставленного в главе 2. Если при импортировании этих классов в ModelMaker и со- здания собственной схемы вы получите другой результат, вспомните о множестве параметров, про которые я говорил чуть ранее. Визуальным представлением клас- сов управляет множество настроек. Можно открыть файл ModelMaker (МРВ-файл), используемый для получения рис. 11.1 из соответствующей папки исходного кода текущей главы. Рацее я упоминал, что редактор схем ModelMaker — это просто еще один вариант представления внутренней модели. Некоторые обозначения в схеме непосредствен- но отображаются на элементы модели кода, а другие — нет. На низкоуровневой схеме, такой как схема класса, большинство обозначений представляют действи- тельные элементы модели кода. Манипулирование этими обозначениями может привести к изменению программного кода, генерируемого ModelMaker. С другой стороны диапазона, в case-схеме, большинство (если не все) обозначений не имеют 0487
488 Глава 11. Моделирование и ООП-программирование Н» nm <*»« мл - _ - , у 18 Z3 . ZZ Zl J® !& 4 ''ХкГ* - ' % (feMt) '„4 4Ы. £ Ччт ( ®»£Г1Ой!« . '' ШЙпЙЗйй&до! ТИ New Sequence Diagam Ra New Package Diagam E Extended dess ifeaam J ЙЖйй«“«'-.*| »Г» I * Cate TDateTme £S j? £3 GetDay Integei || 9 25 GetMorth Integei » £3 GefYear Integei 913 SetDayfcoretVatue. IntegeL 9 23 SetMonthfconst Vakje Integei) — $ 13 Setfeelcomt Vakje Integer) 9 £$ Ct eate 9 £3 Createfy m d Integer). | £>*«$.. _ <y 23 Deoease(Nunbc<OfDays Integtxl Рис. 11.1. Схема классов в ModelMaker 'Гй W <**и? fi* т<** «ф- '' ' ' ' - ;д ♦}-* ♦ ы<д' г. ;')я ж v- Шт ,ИЙ Х1№йе| 1W ЭДДО* ii>4!4ew»rtato*f г<^ Ut*с»> 5ййк»мЕ4Й#РаВЛте| КЙемН»j БоШйкМйМ | .till New Clas s D tea am "y| | '> ** "R 1» илиаВ^ '*1* у ♦ (WakerWak-Wakl. Э> Uurplmct TJirrperimpL 9 S3 Create > £3 Destroy * ^3 Rut sting, V £3 SetPosf/akie Integei), »J3WA1 sting. 9 M Jianper TJunpedaipi it «н«мхе* Рое Heger; арепаОоп~ Destroy GetPor Irteger, Rut string, SetPos( ) -О-(Jumper OTAtNete Mtiibutet - (Jumpknpt TJumperlmpi. * Juroer TJumcerHct. operation* * Create Destroy ♦ GetPos Irteger ♦ Rin strng, ♦ SetPos( ) • Wrt1 strng, 5?!3S!^H®K3SS ОТ Му Jumper MtiibMe* fjurc*rcit TJumpertmpi Jurper Tjunperknpt, operation* ' Create Destroy atttibut** Pos Irteger «|МГ40М» GetPos Hegar Jj' Рис. 11.2. Схема класса с интерфейсами, классами и предоставлением интерфейса представления в модели кода. С помощью схемы класса в модель можно добав- лять новые классы, интерфейсы, поля, свойства и даже документацию. Более того, с помощью схемы можно изменять наследование класса в модели. В то же время 0488
Моделирование и URL 489 в схему класса можно добавлять ряд обозначений, не имеющих логического пред- ставления в модели кода. Схемы классов в ModelMaker также позволяют создавать программный код для работы с интерфейсами, т. е. работать на высшем уровне абстракции. На рис. 11.2 представлены отношения классов и интерфейсов сложного примера использова- ния интерфейса IntfDemo. Этот пример рассмотрен в электронной книге (см. При- ложение В), а его фрагменты представлены в примерах данной главы. При использовании интерфейсов в схеме классов имеется возможность ука- зать отношения реализации интерфейсов между классами и интерфейсам, и эти реализации будут добавлены в модель кода. Добавление реализации интерфейса в схему проявляется во внешнем виде одной из искусных особенностей ModelMaker: мастере Interface Wizard (рис. 11.3). Активизация Interface Wizard значительно упрощает задачу реализации интер- фейса. Мастер перечисляет методы и свойства класса, необходимые для реализа- ции данного интерфейса (или интерфейсов), он добавит в реализацию класса все не- обходимые члены. Обратите внимание, что для всех методов, добавленных в класс, вам необходимо лишь предоставить значимый программный код. На рис. 11.3 ма- стер оценивает TAthlete для его интерпретации IWalker и Dumper, а также предлагает изменения, которые необходимы для правильной реализации этих интерфейсов. ух у»; а Вилий L& IWaker ® SetPosfValue Integer), SetPos I# Run. strrrg Run * GetPos Integer GetPos strng IWakerWafc — Ppirdpfrlrilegtt L& Uumper * SetPosf^alue Integer), SetPos * GetPos Integer GetPos • " Pos^arf Irite^w rwtwpetf ; Lai Redundant members ♦ Deate Deale ♦ Destroy Destroy 4е Mumplmpt TJumperlmpI; Uumplmpl ♦ Jumper TJcrrpedmpI Jumper ♦ Wakl strrrg, Wald TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete TAthlete P found «wto* ' P ijoOeuid«model Leave unchanged Leave unchanged Is p Leave unchanged J'p ' Leave unchanged &_' I «йвглеииГЛ: '-----'~~t P' Swapped membwir , &^кввйпдая!явЛв, Leave unchanged fa, > ' Iptitjiiiorii tirtrit ’* ^0Kec( sapped members > KxmnAntartnemtei - leave fedundart member ^|prjbfc Л1 Leave redundant member $ ' ' г Leave redundant member & Leave redundant member I Рис. 11.3. Мастер Interface Wizard Схемы последовательности действий Схемы последовательности действий (sequence diagrams) модели взаимодействия объектов выполняются в виде представления объектов и сообщений, передавае- мых им в течение времени. В типичной схеме последовательности действий (по- следовательной схеме) объекты, взаимодействующие с системой, выстраиваются вДоль оси X, а время откладывается сверху вниз по оси Y. Сообщения представля- 1°тся в виде стрелок между объектами. Пример самой обычной схемы последова- 0489
490 Глава 11. Моделирование и ООП-программирование тельности действий можно увидеть на рис. 11.4. Последовательные схемы можно создавать на различных уровнях абстракции, позволяя представлять высокоуров- невые взаимодействия с системой, состоящие всего из нескольких сообщений, либо низкоуровневые взаимодействия со множеством сообщений. И» Шн ОДОвгч» £ИрН /-J» 1 Г Лз Зидо» | ____. И New Ctott Diagram ffij©flflW «New Package Diegam щЕйепйей class Bagram л«»Д» еатива“’<! * f * «• ♦ » & ** 9 (Dale TDaleTme afrEJSetOv integer | 9 3 GctMonth integer Л> 2} GetYear integer » §3 Setbedconst Value Integer/ & 23 SetMorthjcon» Value rrteger) I wSSSetYeartconttVabje 7-1 Ш t % frtadfari freert 4ew "^sqjwteWisq Рис. 11.4. Последовательная схема для обработчика событий примера NewDate Совместно со схемой классов схемы последовательности действий относятся к поддерживаемым ModelMaker UML-схемам, наиболее близко связанным с моделью программного кода В ModelMaker можно создать ряд схем, в которых обозначения схемы не имеют непосредственной связи с моделью кода, но составление последо- вательной схемы напрямую влияет на программный код, поскольку вы моделиру- ете классы и их методы. Например, при создании обозначения для сообщения между объектами имеется возможность выбрать метод из списка методов, принадлежа- щих объекту-получателю; либо можно выбрать и добавить в объект новый метод, а этот новый метод будет добавлен в модель кода. Обратите внимание, что (я уже говорил об этом) ModelMaker не может автома- тически создавать схему последовательности действий из импортируемого про- граммного кода; вам необходимо будет создать ее самому. Использование CASE- и других схем Сначала я познакомил вас с двумя UML-схемами самого низкого уровня, но Model- Maker поддерживает и высокоуровневые схемы, предназначенные для обеспечения перехода от высокоуровневого моделирования взаимодействий пользователя с использованием case-схем к низкоуровневым схемам класса и схемам последо- вательности действий. Наряду с широко используемыми схемами можно исполь- зовать и case-схемы невзирая на то, что их схемные обозначения не имеют никако- го отношения к элементам модели кода. Эти схемы предназначены для моделей, на основе которых предполагается создавать программное обеспечение. Для ис- пользования их в ходе согласования с заказчиками они являются достаточно яс- ными и не требуют объяснений. 0490
Моделирование и URL 491 Простое использование case-схем основывается на существовании субъектов (actors) (пользователей или подсистем приложений) и случаев использования (use cases) (то, что делают субъекты). Один из самых часто задаваемых вопросов, свя- занных с использованием use cases, касается обработки текстов use cases в ModelMaker. Текст для use cases — это просто следующий шаг, определяемый в ходе предвари- тельного анализа. Например, use cases — короткое описание действий, которые может предпринять субъект («Выполнить предварительный просмотр отчета про- даж» или «Изменить размер окна»); текст use case — более длинный и представля- ет более подробное описание. ModelMaker не имеет специальной поддержки длин- ных текстов use cases; необходимо либо использовать условные знаки в схеме, связанной с обозначением use cases, либо связать обозначение use cases в внешним файлом, содержащим текст use cases. Более подробно эти технологии будут рас- смотрены далее в этой главе. Кроме того, ModelMaker поддерживает и следующие UML-схемы: Collaboration Diagrams (Схемы сотрудничества) Схема взаимодействия, очень похожая на схему последовательности действий. Однако они отличаются тем, что сообщения, устанавливаемые с помощью чисел, предпочтительнее, чем сообщения, представленные в масштабе времени Это при- водит к созданию отличающихся представлений схем, где отношения между объек- тами иногда могут быть выражены более четко. State Diagrams (Схемы состояний) Схемы, описывающие поведение системы посредством идентификации всех со- стояний, которые может принять объект в качестве реакции на получаемые сооб- щения. Схема состояний должна перечислять все переходы состояний целевого объекта, указывая исходное и конечное состояние каждого перехода. Activity Diagrams (Схемы деятельности) Схема, содержащая описание потока работы системы. Она особенно удобна для визуализации параллельной обработки. Component and Deployment Diagrams (Схемы компонентов и расположения) Также известны как «схемы реализации». Это схемы, которые позволяют модели- ровать отношения между компонентами (модулями, а фактически — исполняемы- ми файлами, COM-объектами, DLL и т. д.), либо в случае схем размещения — фи- зическими ресурсами, называемым узлами (nodes). Прочие схемы ModelMaker также поддерживает еще три типа схем, которые не попадают под UML- стандарт, но являются очень полезными: Схемы Mind-Map Предложены Тони Базэн (Tony Buzan) в 60-х гг. XX в. Отличный метод «мозговой атаки», изучения вопросов ветвления или быстрой записи, связанной с мышлени- ем. Я зачастую использую пппс1-тар‘-схемы для общего представления данных в ходе презентаций. Букв отображение сознания — Примеч перев 0491
492 Глава И. Моделирование и ООП-программирование Unit Dependency Diagrams (Схемы зависимостей модулей) Довольно часто представляют результаты работы мощного анализатора Unit Depen- dency Analyzer, входящего в состав ModelMaker. Эти схемы представляют ветвящие- ся отношения модулей внутри Delphi-проекта. Robustness Diagrams (Схемы устойчивости) Вероятно, были упущены в UML-спецификации. Эти схемы помогают «залатать брешь» между case-моделированием, относящимся только к интерфейсному пред- ставлению, и схемами последовательности действий, специализированными на реализации. Схемы устойчивости могут помочь группе аналитиков проверить use cases и вникнуть в детали реализации. Общие элементы схем Каждый тип схем, поддерживаемый ModelMaker, содержит обозначения, характер- ные для данного типа схемы, но существуют элементы, являющиеся общими для всех типов. В схемы можно добавлять изображения и фигуры, а также обозна- чения пакетов (контейнеров, в которые можно добавлять другие условные обо- значения). Обозначение Hyperlink позволяет вам добавлять в схему надпись, связанную с другим элементом. На самом деле подавляющее большинство обозначений схем поддерживают эту возможность. Можно привязать другую схему (щелчок на свя- зи приведет к открытию в редакторе связанной схемы), привязать элемент модели кода (будь то классом, модулем, методом, интерфейсом и т. д. — щелчок на связи приведет к открытию соответствующего редактора для связанного элемента) или привязать внешний документ (эта связь откроет связанный документ в соответ- ствующем приложении). Для каждого типа схем доступны три различных типа средств аннотирования. Некоторые документы более подробно объясняют работу этих средств, поэтому здесь вполне достаточно сказать, что имеется возможность добавить отдельное обозначение аннотации. Это обозначение автоматически открывает связанную внутреннюю документацию объекта, кроме того, можно добавить обозначение аннотации, ввести свой текст и связать это обозначение с объектом. При этом внутренняя документация объекта обновляется в соответствии с текстом обо- значения. Обозначение отношений или соединений по умолчанию производится прямы- ми линиями, однако имеется возможность переключить их на линии, изгибающи- еся под прямыми углами (ортогональные линии), выбрав необходимое обозначе- ние и нажав Ctr 1+0. Помимо этого нажатием клавиши Ctrl и щелкнув на линии можно добавить узлы этих обозначений. ModelMaker постарается, насколько это возмож- но, сделать обозначения ортогональными. ModelMaker также предлагает устойчивый набор визуальных стилей обозначе- ний. Можно определить стили шрифта и цвета в соответствии с иерархией и при- менить их к обозначениям схемы, указывая наименования. Более подробно об этом можно прочитать в разделе «Style manager» онлайновой справочной системы. И заключительной общей чертой является возможность иерархического упо- рядочивания списка схем (рис. 11.5). Для систематизации и «изменения родите- 0492
Особенности составления программного кода в ModelMaker 493 Ля» можно добавлять папки. Для этого, удерживая нажатой клавишу Ctrl, необхо- димо перетащить схему на ее нового предка. Classes ] Unit* Diagram» | ЕЭ’Ш ' ?1 X - > Intro !AX SjWefcome r Intro 4?CASE JP^FullCycle - ЙЖ [gi]Class Diagram «UseCase Diagram Activity Diagram State Diagram §~l Implementation Diagram Collaboration Diagram [to] Sequence Diagram Ю Robustness Diagram Mind Map Diagram API Classes r Basics S Basics R Active Model X Active Model 01 3? Active Model 02 (Xi Рис. 11.5. Возможности структуризации вида схем Построение схем ModelMaker включает обширный набор возможностей; после некоторого изучения этого набора вы сможете признать, что он перевернул вам весь процесс разработки. Для Delphi-разработчиков активная двухсторонняя сущ- ность редактора Diagram Editor обеспечивает более динамичный опыт построения схем, чем большинство UML-редакторов, которые лишь обеспечивают генерацию красивых статических картинок. Особенности составления программного кода в ModelMaker Как вы уже, наверное, поняли, ModelMaker является мощнейшим инструментом составления UML-схем. С помощью только этих возможностей можно обеспечить решение задач анализа и проектирования. Но ModelMaker может значительно боль- Ще- Многие разработчики используют ModelMaker как основную среду разработки, что приводит к вытеснению Delphi. Частично это происходит благодаря визуаль- ному виду, используемому ModelMaker для представления задач программирова- ния, автоматизируя многие из повторяющихся частей написания описаний клас- г’гчп — из-за того, что Delphi изо всех сил стремится облегчать 0493
494 Глава 11. Моделирование и ООП-программирование разработку программного кода, что приводит к стиранию грани между областью представления и проблемной областью. Другими словами, проще написать про- граммный код реализации приложения — код, который действительно выполняет необходимую работу, — и поместить его прямо в обработчики событий форм. Обычно это не отвечает объектно-ориентированному дизайну. С другой стороны, ModelMaker облегчает создание и регенерацию объектов проблемной области и от- деляет эти объекты от пользовательского интерфейса. Прежде чем мы подберемся к этим вопросам, сначала мы рассмотрим, как со- трудничают Delphi и ModelMaker. Интеграция Delphi/ModelMaker После установки ModelMaker добавляете меню IDE Delphi соответствующим обра- зом оформленный пункт ModelMaker. j ModeMsher | beb [Main * ^5 Rjt Moderate JC * .ыт-ptoMocePaH” Shft-Crl-hV । Ado *o Model г 1 to Mocel да j in Model Sb- ft -C t! I i C3fiye,'ttoMoce k Согче-t project to Mode Open Model '(tegiasor Options . ei sion Control ^eso.rce String 'Л ssrd i &s Shortcut Л za-c jr< Dependence: Если у вас отсутствует это меню, необходимо настроить в реестре DLL-мастер ModelMaker (см. вставку «Установка новых DLL-мастеров» в конце главы 1). С по- мощью этого меню можно управлять ModelMaker и быстро импортировать программ- ный код в проекты ModelMaker. Большинство пунктов меню доступны только при запущенном ModelMaker После запуска ModelMaker (либо обычным образом, либо с помощью пункта Run ModelMaker) станут доступными и остальные пункты. Меню интеграции имеет несколько раз- личных возможностей переноса программного кода в модель. Пункты Add to Model, Add Files to Model, Convert to Model и Convert Project to Model приведут к импортирова- нию указанных модулей: пункты Add... импортируют модули в загруженную в те- кущий момент модель ModelMaker, а пункты Convert... создают новую модель и им- портируют в нее модули. Лучше всего начинать с Convert Project to Model Когда в Delphi открыт ваш пр°" ект, не забудьте сделать резервную копию программного кода, а затем выберите этот пункт меню. Весь проект будет импортирован в новую модель ModelMaker 0494
Особенности составления программного кода в ModelMaker 495 Где VCL? Импортирование и наследование в ModelMaker При исследовании только что импортированного в ModelMaker программно- го кода можно отметить, что импортируются только модули, являющиеся частью проекта. ModelMaker «не умеет» автоматическим импортировать мо- дули, содержащиеся в разделе uses проекта. Это привело бы к созданию не- оправданно большого количества моделей с лишними импортированными классами. Однако многие из приятных особенностей ModelMaker, относящих- ся к наследованию, требуют, чтобы в кодовой модели существовал класс- предок. (Например, при установке свойства изменения класса-предка будут автоматически распространяться на всех перекрывающих наслед- ников.) К счастью, при импортировании имеется множество настроечных пара- метров. Хотя импортирование может выполняться с помощью интегриро- ванного в Delphi меню ModelMaker (или путем перетаскивания в ModelMaker модулей из проводника Windows), наиболее удобный способ заключается в использовании одной из кнопок Import панели инструментов ModelMaker. Использование преимущества их параметров позволяет импортировать часть VCL в качестве классов-«заполнителей», что позволяет более эффектив- но использовать средства наследования ModelMaker без «раздувания» модели. Кроме того, в меню интеграции имеется пункт Refresh in Model, обеспечиваю- щий повторный импорт текущего модуля. Теперь самое время рассмотреть одно из последствий особенности модели внутреннего кода ModelMaker, о которой я упо- минал ранее. Поскольку ModelMaker работает с собственным внутренним программ- ным кодом, а не с внешними файлами исходного кода (пока вы не сгенерируете эти файлы), можно отметить, что могут редактироваться как модель, так и ваши файлы. Это возможно благодаря тому, что модель не синхронизирована с исход- ными файлами. Когда изменены исходные файлы, а не модель, последнюю можно синхронизировать повторной загрузкой исходных файлов модулей. Но если изме- нены как модель, так и файлы, ситуация становится более запутанной. К счастью, ModelMaker предлагает солидный набор средств обработки проблем потери синх- ронности. Более подробно они рассмотрены в разделе «Представление Difference». Еще одним интересным пунктом меню интеграции является Jump to ModelMaker. При его выборе ModelMaker пытается найти текущее место программного кода в загруженной на его основе модели, активизируя ModelMaker. Хотя ModelMaker может управляться из Delphi, интеграция является двунаправ- ленной. Так же как и меню ModelMaker в IDE Delphi, в ModelMaker существует меню Delphi. В этом меню представлены команды, позволяющие переходить с выбранно- Го в настоящий момент элемента на соответствующее место в файле исходного 11 Рограммного кода, а также команды, «заставляющие» Delphi выполнить синтаксическую Проверку, компиляцию или компоновку. Таким образом можно редактировать, генериро- Вать и компилировать программный код, не выходя из ModelMaker. 0495
496 Глава 11. Моделирование и ООП-программированщ Управление моделью кода А сейчас рассмотрим реальный механизм кодирования с помощью ModelMaker. Бла- годаря объектной природе модели внутреннего кода ModelMaker редактирование элементов модели кода обычно становится более наглядным процессом, чем в Delphi. Например, редактирование свойств класса выполняется посредством ди- алогового окна Property Editor (рис. 11.6). Это является одним из самых лучших примеров автоматизации в ModelMaker. При добавлении нового свойства не нужно беспокоиться о проблемах добавления частных полей состояния, методах read и write или даже объявлении свойства. Все, что необходимо сделать, — это выбрать соответствующие настройки в редакторе, a ModelMaker создаст необходимые члены класса. Выигрыш здесь даже больше, чем от использования функции Class Completion в IDE Delphi. TDate property - Standard | Ad&fanftl j Documertafion j Visuafeata | - - " default arrant * DoutHe Currency .'IS Г? [ХМ ore г л хуре HyProperty П Wore <? ЕЖ с Method Г Custom “Wrie Access' Г tone t7 Method C Custom ~~l Г* ' Г* ' J? ^t&sCode ' pValue Г~ Oser named accessspedfiets ’ T" ? 1“ Stored specifier ’ g|| Default | unspecified I. Сагедагу fe«tendsg”""~~ 3 Рис. 11.6. Property Editor Обратите внимание, что атрибуты свойства, которые обычно приходится вво- дить вручную, представлены в диалоговом режиме различными элементами уп- равления. Спецификации Visibility, Type, Read, Write и т. д. управляются редактором. Преимущество имеется и в области регенерации (не говоря уже об избавлении от выполнения повторяющихся задач ввода). Например, поскольку ModelMaker в сво- ей модели кода управляет свойством как объектом, любое изменение в свойстве 0496
Особенности составления программного кода в ModelMaker 497 (Изменение его типа, например) также приведет к тому, что ModelMaker применит эЮ изменение к любым ссылкам, которые он знает. Если позже возникнет необхо- димость изменить доступ чтения из поля в метод, эти изменения можно произвес- ти в редакторе, a ModelMaker позаботится о добавлении get-метода и изменении объявления свойства. И самое замечательное: если вы решите переименовать свой- ство или переместить его в другой класс, а это свойство владеет собственными чле- нами классами, то и они будут автоматически переименованы или перемещены. Тот же подход используется для каждого типа членов класса; аналогичные ре- дакторы имеются для методов, полей, событий и даже инструкций разрешения методов. Для работы в ModelMaker у разработчика должно быть чувство абстракции. Этот продукт отрывает вас от необходимости продумывания деталей реализации в ходе редактирования членов класса; необходимо лишь обдумывать элементы интерфей- са, в то время как ModelMaker обработает большинство из повторяющихся частей, реализующих этот член. (Не забывайте о необходимости написания кода реализа- ции метода. Это по-прежнему придется делать вам.) Редактор Unit Code Editor ModelMaker включает два редактора: редактор, реализующий методы классов (рас- сматриваемый ранее), и редактор Unit Code Editor (редактор кода модуля), который требует некоторого пояснения. ModelMaker является средством, ориентированным на классы/объекты. Его удобные функциональные возможности относятся к уп- равлению программным кодом уровня касса. Когда он становится программным кодом, не относящимся к классу (объявлением типов, не являющихся классами, объявлением метаклассов, методами модуля, переменными и т. д.), ModelMaker ис- пользует неэлегантный подход. При импортировании модуля все, что может вой- ти в модель кода, обрабатывается, а то, что выходит за эти рамки, помещается в Unit Code Editor. (Зачастую, особенно у новичков, сюда попадает и документация1, не помещенная в реализацию методов, но ModelMaker может также обратно воспроиз- вести документацию. Об этом чуть позже.) Вот пример того, что может оказаться в редакторе Unit Code Editor: unit <!UmtName!>: interface uses SysUtils. Windows. Messages. Classes, Graphics. Controls, Forms, Dialogs. Dates. StdCtrls: type *iWIN:CLASSINTERFACE TDateForm: ID-37: var DateForm: TDateForm: implementation *.DFM} —-------.--------- Особый вид комментариев, осуществляющий более подробное описание. 0497
498 Глава 11. Моделирование и ООП-программирование MMWIN CLASSIMPLEMENTATION TDateForm ID=37, end Хотя этот код выглядит знакомым, он, несомненно, не компилируется. Вы ожи- дали, что механизм генерации кода ModelMaker будет использоваться при расшире- нии или генерации модуля кода. Когда ModelMaker генерирует модуль, он начинает с вершины и приступает к выдаче строк текста, ожидая один из трех элементов: обычный текст, макрос или тег генерации кода В рассматриваемом примере обычный текст (plain text) попадается в первой же строке, unit. ModelMaker выдаст это строчное значение «как есть». Следующим идентификационным знаком в этой строке является макрос: <!UnitName!>. Более подробно мы рассмотрим макросы далее. А сейчас достаточно усвоить, что Model- Maker может выполнять подстановку. В этом случае макрос представляет имя мо- дуля и будет выдан этот текст. И, наконец, вот пример тега генерации кода, следующий непосредственно за ключевым словом type: MMWIN CLASSINTERFACE TDateForm ID=37 Здесь тег сообщает ModelMaker о необходимости в данном мете модуля кода «рас- крыть» интерфейс класса TDateForm. Таким образом, при редактировании кода в Unit Code Editor вы видите гибрид управляемого вами кода и код, обслуживаемый ModelMaker. Будьте внимательны при редактировании этого кода: не распространяйте код, обслуживаемый Model- Maker, за исключением того случая, когда вы четко представляете, что делаете Это аналогично редактированию кода в DPR-файле, обслуживаемом Delphi (при нео- сторожности можно быстро столкнуться с проблемами). Тем не менее это именно то место, где вы могли бы добавить объявления типов, не являющихся классами. Это делается так же, как и в среде Delphi, добавлением объявления типа в раздел type этого модуля: unit <lUmtNamel>, interface uses SysUtils, Windows. Messages. Classes. Graphics. Controls. Forms Dialogs, Dates. StdCtrls, type TmyWeekday - (wdSunday wdMonday, wdTuesday. wdWednesday, wdThursday. wdFriday wdSaturday), MMWIN CLASSINTERFACE TDateForm ID=37 var DateForm TDateForm implementation {$R * DFM} MMWIN CLASSIMPLEMENTATION TDateForm ID=37 end В этом примере ModelMaker выдаст объявление типа точно так же, как вы ввели его (в виде обычного текста), а затем «раскроет» объявление класса TDateForm. 0498
Особенности составления программного кода в ModelMaker 499 В редакторе Unit Code Editor для управления низкоуровневыми методами пред- лагается ряд средств, и они являются большим удобством при наличии объемных модулей процедур библиотечного типа. Однако при использовании ModelMaker мож- но форсировать возможности разбиения, сконцентрировавшись на процедурах. Редактор Method Implementation Code Editor Редактор Method Implementation Code Editor (редактор программного кода реализа- ции методов) (рис. 117) несколько отличается от Unit Code Editor. Этот редактор занимает до2/1 экрана. В данном примере я добавил вымышленное свойство с име- нем MyNewProperty и позволил ModelMaker сгенерировать поле состояния и методы доступа read и write Метод write в редакторе является активным Справа от редак- тора npoi раммного кода можно видеть два интересных окна Расположенное выше «дерево» является «проводником» локального кода: здесь можно управлять ло- кальными переменными и локальными процедурами Ниже располагается Section List (Список разделов) ModelMaker позволяет разбивать программный код реали- зации метода на несколько разделов. Частично — это организационное удобство, но, что более важно, оно позволяет управлять определенными разделами кода Также как ModelMaker может иметь собственные части модели (например, автома- тически сгенерированный метод доступа к свойству), он может также иметь соб- ственные разделы кода в пределах метода. Наиболее часто это происходит при ис- пользовании ModelMaker для генерации программного кода read или write в методе доступа к свойству Обратите внимание, что в этом примере первый, третий и пя- тый разделы имеют пунктирный, красный с белым левый отступ, указывающий на w Urals Diagrams ? Implementation 4 » Unicode Js DiagramEditcn ] Macros JS Patterns j •>? 5- Classes MMAPIExploret оазjajxs ж « Рис. 11.7. ModelMaker с активной вкладкой Implementation j - UMMAPldxExpIo er n [MMAPIExphtei] « Is! TtsDevExMMAPIExploier 1 — <> tsMMAPExploie n [MMAPIExptorei} } LsJ TttCustomMMAPlExptorw 4 ClTt StandardMMAPlExplore t <> tsMMAPIExploferConst <n(MMAPtExplo<«] t$MMAPlExp|oreiTyp»s in [MMAPIExptorer] l-^TtsMMAPIUtls £3 Classes not a signed to un t *44» i SetClass nstiunentedfaClass IMM Л # ЭД ModelLoachng Boolean □ 9» FModeluadrg Всюеап ЭД ModelPartCount ntege Ш 9 FModelPatCount Integer а ® 23 GetModeFa tCount li tege Ъ ЭД MyNewProperty b teg« jCI S* ГМуИеюРюрепу Integer £3 S’ 23 Ge’MytJewPiope ty Intege rgj & 23 SetMyNewPiopertyjconst aVaue I ЭД PiogtessValue nteget 0499
500 Глава 11. Моделирование и ООП-программирование то, что они принадлежат ModelMaker. При генерации этого метода программный код будет выдан в указанном здесь порядке, раздел за разделом. ВНИМАНИЕ ---------------------------------------------------------------- Большим недостатком создания программного кода с помощью окна Implementation является отсут- ствие каких-либо форм функции Code Completion, предлагаемых IDE Delphi Представление Difference Как я уже упоминал ранее, можно легко спровоцировать ситуацию, когда модель не синхронизирована с вашими файлами исходного кода Если редактируются как модель, так и исходные файлы, имеется возможность восстановить файлы на ос- нове модели, перезаписав изменения в исходных файлах. Более того, после этого можно повторно импортировать модули из страха потерять изменения в модели К счастью, ModelMaker представляет собой один из самых надежных инструментов, которые я видел При обнаружении асинхронности модели пора перейти па вклад- ку Difference (Разница) (рис. 11.8) ModelMaker предлагает множество различных способов просмотра отличий Мож- но увидеть стандартное сравнение текстовых файлов, отличия во временных от- метках или даже сравнение двух выделенных классов модели Я предпочитаю про- сматривать структурные отличия (рис 118) ModelMaker временно импортирует исходный фай (представляя ei о как внутреннюю модель кода) и вместо текста срав- нивает импортированный файл с таким же модулем и классами, представленными в модели на уровне объектов и атрибутов В результате производится более быст- рое и более точное сравнение. * 1 Imptemerttf «on | Unit code & Oiagtatn EtkfcH <£» Macrw| Щ Patterns Whence ’ Doeurrentaton t Events^ V О** M ™ Modelunrts modelC’^DevetopmenftModeWal'er Exp«B MMA disk CSOevefoprrent\ModelMakeExpeAsSMk \Development\ModelMaker Experts\MMAPII . . - - - <#>C SDevefopmentSModelMaker Expert$\MMAPII <£>C \Development\ModelMaker ExpertsSMMAPII Э>С \Development\ModelMaker Expert$\MMAPII ^>C\Development\ModelMaker Expert$\MMAPIl Ф>С \Development\ModelMaker Experts\MMAPII О C \Devdopment\ModelMaker Experts^MMAPII - Classes - a TfimMMAPIExploreHestMan ♦ NewModeMember •" NewPropertydnDisk 13 Events 0 modeled procedures S>C \Devetoprnent\ModeNaker Experts\paLink> £)>C \Devetoprnent\Mode*Haker Experts\t$MMPk C \Devetopment\ModeWake/ ExpeftsVsMMPk ®>C \Devetopment\ModeHaker Experts\lsMMPI« @>C.\Devetopment\ModeWaker ExperlsMsMMPIi &>C\Devetoprnent\ModeNaker ExpertsMsMMPIi 'S> C \Devetopment\ModeWaker ExpertsMsMMPIi (9>C\Devetoprnent\ModeNaker ExpertsVsMMPIi C \Development\Mo<WMaker ExpeftsVsMMPk C kDevelopmentSModeiMaker ExpertsVsMMPIi &>C \Deveiopment\ModelMeke Experts^tsMMFli 0500
документация и макросы 501 Обратите внимание на значки в «дереве», которые отмечают различия. Крас- ные угловые скобки (<>) указывают, что как модель, так и файл содержат указан- ный член класса (в данном примере: btnUITestClick), но два различных экземпляра. Отличающийся программный код выводится в MEMO-полях справа. Зеленый плюс (+) сообщает, что указанный член класса существует только в модели и от- сутствует в дисковом файле. Синий минус (-) — член класса существует только на диске, но отсутствует в модели. Имея эту информацию, можно выбрать два вари- анта синхронизации модели. Один из них заключается в использовании возмож- ности повторного импорта выбранного метода (а не всего модуля) из окна Difference. Этот подход дает понять, что очень важно знать, когда модель стала несинхрон- ной, для того чтобы не перекрывать изменения на диске при повторной генерации исходных файлов. К счастью, ModelMaker предлагает несколько гарантий, которые могут предотвратить такую ситуацию. Одной из них является использование Design Critic (см. раздел «Малоизвестные изюминки» далее в этой главе). При активизации этой функции вкладка Message будет предупреждать об изменении дискового файла: ..." •• ... л.... -1- . • • . I .. I . I Ж > Locaton TimaStamp % Event Types View ModelMaker разрешает управление типами событий в представлении Events. Здесь можно редактировать объявления типов событий. Но не забывайте, что новый тип события может существовать во внутренней модели кода ModelMaker и будет отсут- ствовать в файле исходного кода до тех пор, пока вы не добавите в модуль объяв- ление нового типа события. Наипростейший способ управления этим процессом заключается в «перетаскивании» объявления типа события из представления Events в список Unit и помещении пункта в модуль. Документация и макросы ModelMaker может стать очень полезным в поддержке документирования программ- ного обеспечения. Вам необходимо усвоить важную концепцию, предшествующую осуществлению этого действия (к счастью, несложного): понятие «документация» в ModelMaker не приравнивается к понятию «комментарий». Не бойтесь, можно де- лать усложненные комментарии в исходном коде. Для того чтобы заставить Model- Maker выдать (или импортировать) эти комментарии, необходимо выполнить ряд Шагов. Практически каждый элемент модели (класс, модуль, член, обозначение схемы и т. д.) могут иметь документацию, но ввод документации не приведет к ав- томатической выдаче этой документации в исходном программном коде. Этот текст оу дет присоединен к элементу в модели, но чтобы заставить ModelMaker сгенериро- вать комментарий к исходному программному коду, содержащему эту документа- цию, требуется приложить дополнительные усилия. 0501
502 Глава 11. Моделирование и ООП-программирование Документация по сравнению с комментариями Каждый элемент модели программного кода ModelMaker может иметь два типа до- кументации: стандартный, большой блок текста документации, и короткий, строч- ный (рис. 11.9). В контексте ModelMaker эти тексты могут служить многим целям и, как уже отмечалось ранее, вовсе не тождественны комментариям исходного про- граммного кода, хотя на их основе могут быть произведены такие комментарии. Рис. 11.9. Вкладка Documentation идентификатора Class Symbol В этом примере класс имеет оба типа документации. Эта документация может быть представлена в схеме как приложенная аннотация (либо в виде вкладки, либо в виде стандартной документации). Кроме того, можно указать использование обо- их типов или какого-нибудь одного из них в качестве части комментариев файла исходного программного кода. Для этого необходимо использовать мощный мак- рос ModelMaker (рассмотренный в следующем разделе) и изменить некоторых из параметров вашего проекта. Давайте пока не будем беспокоиться о макросе и рас- смотрим только параметры проекта. Обратитесь к диалоговому окну параметров проекта, выбрав пункт Project Options в меню Options (Параметры), а затем перейдите на вкладку Source Doc Generation. Здесь вы найдете много параметров, касающихся генерации комментариев исход- ного программного кода на основе документации ModelMaker. (Подробности СМ- в онлайновой справочной системе ModelMaker.) Для просмотра процесса добавле- ния комментариев в исходный код установите параметр Before Declaration (Перед 0502
Документация и макросы 503 объявлением) в разделе Method Implementation (Реализация метода) в группе In Source Documentation Generation. Теперь любые методы, содержащие документацию, для генерации комментариев исходного программного кода будут использовать стандартный макрос. ModelMaker также может импортировать комментарии от исходного модуля и связывать их с соответствующими элементами модели программного кода. Для этого необходимо указать ваши комментарии с помощью Documentation Import Signature (вкладка Source Doc Import диалогового окна Project Options) и сообщить ModelMaker, какие строки надо импортировать в модель. Таким образом, если реа- лизация метода имеет комментарии, подобные представленным ниже, то вы може- те заставить ModelMaker игнорировать первые пять строк и импортировать только действительный текст комментария: ^*************************************************** TmyClass DoSomething Returns String Visibility Public Ccmnent Это действительный комментарий который мы хотим импортировать в ModelMaker Первые пять строк этого блока комментария не должны импортироваться в модель } При настройке ModelMaker на комментирование исходного кода важно избежать сдвига комментариев (comment creep). Это может произойти при неточном совпа- дении настроек импорта и экспорта строк. Например, если макрос, контролирую- щий исходный комментарий, для импортирования в текст документации берет 6 строк, а настройки импорта допускают 1 олысо 5 строк, то каждый цикл импорта/ генерации будет добавлять в комментарий лишнюю строку. Работа с макросами Макросы представляют одну из ключевых особенностей ModelMaker они просты в изучении, но сложны для освоения. Макрос — это идентификатор, представляю- щий блок текста. Когда ModelMaker сталкивается с макросом, он пытается заменить имя макроса на его текст. Этот процесс вы уже видели в действии в редакторе Unit Code Editor <! U nitName !> во время генерации кода заменяется на действительное имя генерируемого моду- ля. Это пример объектного макроса, который всегда различный, и определяется в зависимости от того, какой модуль генерируется Макрос UnitName является пред- определенным, но результат всегда будет иной, в зависимости от контекста. В состав ModelMaker входит множество предопределенных макросов (их пол- ный список можно найти на с 75 руководства пользователя в файле usermanual620. Pdf, помещенном на компакт-диске Delphi) С помощью вкладки Macros имеется возможность создавать собственные макросы различной сложности (даже вложен- ные). Кроме того, можно перекрывать некоторые расширенные макросы докумен- тирования. Например, если имеется документация реализации метода, но нет мак- роса, то для генерации комментариев ModelMaker будет использовать встроенный макрос Однако если вы объявите и определите макрос с именем MemberlmpDoc, то ModelMaker при генерации комментариев метода будет использовать именно этот Макрос. (Список допускающих перекрытие макросов, используемых для генера- ции комментариев исходного программного кода, можно найти в онлайновой спра- в°чной системе в разделе «Documentation Macros».) 0503
504 Глава 11. Моделирование и ООП-лрограммирование Макросы используются не только во время генерации кода. Допускается ис- пользовать макросы и в ходе ввода в редакторе программного кода. В этом случае можно задать параметпы макроса для того, чтобы при попытке развернуть их вам предлагались различные значения. Эти значения могут вставляться в текст, ис- пользуемый для развертывания макроса. Регенерация программного кода Регенерация (refactoring) программного кода — одно из тех новомодных понятий программирования, которое постоянно находится на слуху, но для разных людей имеет различные значения. Под регенерацией обычно понимается процесс улучше- ния самого' существующего программного код без изменения его внешнего пове- дения. Не существует единого процесса регенерации, которого необходимо жестко придерживаться: это просто задача по усовершенствованию вашего программного кода без слишком детального его разбиения. Существует много литературы, посвященной концепции регенерации, поэтому я просто представлю специфические способы, которыми ModelMaker может помочь вам в регенерации программного кода. Опять же, внутренняя модель программно- го кода ModelMaker играет важную роль: не забывайте, что разработка в ModelMaker — это не просто разработка объектно-ориентированного кода; это также процесс раз- работки, в котором учитывается и расположение объектов. Поскольку все элемен- ты модели кода внутренне хранятся как объекты (ссылающиеся на другие объек- ты), и поскольку модули исходного кода полностью регенерируются на основе этой модели каждый раз, когда вы выбираете функцию генерации кода, любые измене- ния в элементах кода будут немедленно распространяться на классы. Отличным примером снова является свойство класса. Если имеется свойство, названное MyNewProperty с «обслуживающими» его методами чтения/записи (под- держиваемые ModelMaker и называемые GetMyNewProperty и SetMyNewProperty), а вам надо переименовать это свойство в MyProperty, то для этого необходимо выполнить лишь одну операцию: переименовать свойство. ModelMaker позаботится об осталь- ном: методы доступа будут автоматическим переименованы в GetMyProperty и SetMy- Property. Если свойство представлено в схеме, то схема автоматически будет обновлена. Одна предосторожность: автоматически ModelMaker не ищет в коде экзем- пляры MyNewProperty. Это необходимо сделать самостоятельно с помощью глобаль- ного поиска и замены. Это простой пример, но он иллюстрирует, как ModelMaker упрощает задачу регенерации; как только вы переместите или переименуете эле- менты кода, ModelMaker выполнит обработку большинство деталей за вас. А теперь давайте рассмотрим некоторые особенности. Simple Renaming Simple Renaming (Простое переименование). Эта задача достаточно простая, и мы ее уже касались, но ее полезность невозможно переоценить. Изменения имен эле- ментов модели программного кода распространяются ModelMaker посредством мо- дели кода на все экземпляры элементов, о которых он знает. Reparenting Classes Reparenting Classes (изменение родственных связей классов). Этот до смешного простой процесс может быть выполнен несколькими различными способами. Са- 0504
Регенерация программного кода 505 С.СЛИ у JBuilder), МЫЙ распространенный: можно просто перетащить класс в представлении классов (Classes) из одного родительского узла в другой (это можно также выполнить на схеме классов перетаскиванием стрелки обобщения от старого родителя к ново- му) — теперь класс будет иметь нового родителя. При включенном ограничении наследования ModelMaker автоматически обновит унаследованные объекты дочер- них классов для того, чтобы они соответствовали объявлениям нового родителя. В следующий раз, когда будет произведена генерация программного кода, эти из- менения будут автоматически учтены. Moving Classes between Units Moving Classes between Units (перемещение классов между модулями). Также до- статочно легко переместить класс в новый модуль. В представлении модулей (Units) перетащите класс из текущего места в новый модуль. Весь относящийся к этому классу код (объявления, реализации и комментарии) будут восстановлены в но- вом модуле. Moving Members between Classes Moving Members between Classes (Перемещение членов между классами). В кон- тексте регенерации этот процесс известен как «перемещение возможностей (или ответственности) между объектами». Идея проста: по мере разработки может вы- ясниться, что некоторые обязанности (реализуемые как методы класса) более ра- зумно передать в другой класс. Это можно сделать с помощью технологии «drag and drop». В представлении Classes выберите из списка членов желаемый член класса и «перетащите» его в новый класс (для выполнения перемещения вместо копиро- вания удерживайте нажатой клавишу Shift). Converting Members Converting Members (Преобразование членов). Это одна из очень удачных функ- ций регенерации. Щелчок правой кнопкой на члене в списке членов (Member List) выведет контекстное меню, содержащее пункт Con vert То (Преобразовать в). Выбор одного из его подпунктов позволяет преобразовать существующий элемент класса из одного типа в другой. Например, если имеется частное поле FMylnteger, а вы предпочитаете преобразовать его в свойство, ModelMaker автоматически создаст public-свойство с именем Mylnteger, осуществляющее чтение и запись в FMylnteger. Более того, это поле можно преобразовать в метод: он станет частной функцией с именем Mylnteger, возвращающей целое значение. Restricted Inheritance Restricted Inheritance (Ограниченное наследование). В диалоговом окне редакто- ра метода имеется флажок Inheritance Restricted. Когда он установлен, ModelMaker не позволяет изменять большинство атрибутов метода, поскольку они установле- ны в соответствии с реализацией перекрываемого метода класса-предка. При из- менении объявления метода в классе-предке эти изменения автоматически будут применены ко всем потомкам этого класса, в перекрывающих методах которых Установлено ограниченное наследование. вас имеется опыт регенерации (или вы знакомы с последними версиями то данный набор функциональных возможностей может показаться не очень впечатляющим. Однако по сравнению с тем, что имеется в Delphi, это — пре- красная коллекция возможностей. Кроме того, API OpenTools продукта ModelMaker Дают доступ к большинству моделей кода. Если вы не удовлетворены тем, что пред- лагает базовая поставка ModelMaker, вы можете расширить ее самостоятельно. 0505
506 Глава И. Моделирование и ООП-программирование СОВЕТ----------------------------------------------------------------—-------— Помимо прочего, по секрету могу сказать, что я видел бета-версии (во время написания этих строк) будущих выпусков ModelMaker, содержащие набор новых средств. Большинство из них взяты из книги Мартина Фолера (Martin Fowler) и являются очень удачными. Применение шаблонов дизайна ModelMaker вкладывает все свое обаяние в поддержку шаблонов дизайна (Полное описание шаблонов выходит за рамки данной главы, однако если вы не слишком хорошо знакомы с шаблонами, то для краткого ознакомления с ними предвари- тельно прочитайте вставку «Design Patterns 101» ) ModelMaker обеспечивает удоб- ство применения шаблонов одним щелчком мыши В зависимости от выбранного шаблона допускается большое разнообразие действий Некоторые шаблоны перед добавлением про!раммного кода запускают мастера, а другие просто добавляют свои члены в выбранный класс Как я уже упоминал ранее, новые члены принад- лежат ModelMaker и могут быть легко преобразованы в результат Кроме того, если вы решите отказаться от использования шаблона, то ModelMaker удалит все члены, добавленные в данный шаблон В качестве примера давайте рассмотрим шаблон Singleton Предположим, что имеется класс, и вы хотите, чтобы существовал только один экземпляр этого клас- са Вот этот класс type TOneTimeData = class (TObject) private FGlobalCount Integer procedure SetGlobalCount(const Value Integer) public property GlobalCount Integer read FGlobalCount write SetGlobalCount end Шаблон Singleton дает поручение использовать только одну точку входа (фун- кция класса Instance в реализации этого шаблона в ModelMaker) для получения до- ступа к единственному экземпляру класса Если такого экземпляра не существует, он будет создан и предоставлен в качестве результата, в противном случае будет предоставлен существующий экземпляр Поскольку Instance является точкой вхо- да, вы должны запретить использование метода Create в отношении данного клас- са. После того как будет применен шаблон Singleton, ваш класс в ModelMaker при- мет следующий вид type TOneTimeData = class (TObject) private FGlobalCount Integer procedure SetGlobalCount(const Value Integer) protected constructor Createlnstance class function AccessInstance(Request Integer) TOneTimeData. public constructor Create destructor Destroy override class function Instance TOneTimeData class procedure Releaseinstance 0506
Регенерация программного кода 507 property GlobalCount Integer read FGlobalCount write SetGlobalCount. end Я не буду приводить здесь реализации методов; их можно просмотреть либо самостоятельным применением шаблона, либо загрузив исходный код примера PatternDemo ВНИМАНИЕ--------------------------------------------------------------------- Программный код, используемый ModelMaker для реализации шаблона Singleton, основан на инте- ресном использовании констант в методе для имитации данных класса. Этот программный код бу- дет давать сбой при компиляции до тех пор, пока вы не включите параметр компилятора Assignable Typed Constants Delphi, который по умолчанию сброшен. Design Patterns 101 В то время как программисты концентрируются на реализации специаль- ных классов, разработчики больше усилий прилагают к созданию различ- ных классов/объектов, работающих совместно. Хотя сложно дать точное определение процессу разработки программного обеспечения, по существу он заключается в организации всеобщей структуры программы Выполняя поиск различных проблем в конструкторских решениях раз- личных людей, можно обратить внимание на схожесть и общность элемен- тов. Шаблон — это знание таких общих конструкций, выраженное стандар- тизированным образом, и достаточно абстрактное, что позволяет применять его для множества различных ситуаций. Шаблон в большей степени связан с повторным использованием дизайна, а не с повторным использованием программного кода Хотя готовые решения на основе шаблона могут вдох- новить программиста, его реальная суть заключается в дизайне даже если вам придется повторно переписать программный код, начиная с ясного и проверенного дизайна, это позволит сохранить массу времени. Шаблоны не охватывают первичные «строительные кирпичики» (такие, как равномерно распределенная таблица или связанный список) или проблемы, зависящие от конкретной прикладной области (чем занимаются анализаторы шаблонов). Формальный автор механизма шаблонов был не разработчиком про- граммного обеспечения, а архитектором, который обратил внимание на ис- пользование шаблонов в строениях «Каждый шаблон описывает проблему, возникающую в среде снова и снова, а затем описывает ядро решения для этой проблемы таким образом, что это решение можно использовать милли- оны раз, не повторившись ни разу» Основоположниками шаблонов в мире программирования были Эрик Гамма (Erich Gamma), Ричард Хэлм (Richard Helm), Ральф Джонсон (Ralph Johnson) и Джон Влиссайдс (John Vhssides), которые написали книгу Design Patterns. Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995) Авторы часто использовали псевдоним «Gam- ma et al », но более часто именовались как Gang of Four (Банда четырех) или просто GoF. В разговоре эта книга обычно упоминается как «GoF book». В ней описаны идеи шаблонов программирования, указаны точные пути их описания и предоставлен каталог из 23 шаблонов, разделенных на три группы создающие, структурные и поведенческие. Большинство шаблонов продолжение 0507
508 Глава 11. Моделирование и ООП-программирование Design Patterns 101 (продолжение) GoF реализованы на языке C++, а некоторые на языке Smalltalk, хотя они абстрагируются от языка и в той же мере пригодны для Java или Delphi. В ядре структуры шаблона: • имя шаблона (pattern пате) очень важно, с его помощью можно обращать- ся к имени шаблона при общении с другими программистами и разра- ботчиками; • проблема (problem) определяет, когда применяется шаблон; в конечном счете включая контекст и условия; • решение (solution) определяет части элементов и их взаимоотношения. Оно не является реализацией, а лишь абстрактным описанием ответствен- ности и сотрудничества классов. • выводы (consequences) — это результат и компромиссы применения шаб- лона, включая пространственные и временные ограничения. В настоящее время нет других книг, охватывающих вопросы шаблонов с точки зрения Delphi. Однако имеется множество статей в Delphi-журналах (включая Delphi Informant и The Delphi Magazine). Классические шаблоны GoF, совместно с подробным обсуждением шаблонов в документации Model- Maker, послужили источником вдохновения для множества статей (загляни- те в Интернет). Я не всегда согласен с Delphi-реализацией некоторых стандартных шаб- лонов. Фактически, я стремился сконцентрироваться на нижестоящем ди- зайне и том, как он может быть сохранен при переходе от GoF-реализации (как правило, на C++ или Java) к Delphi, и расширить специфические язы- ковые возможности. Другие авторы стремятся к переносу программного кода, который является единственным способом реализации дизайна. Важность изучения шаблонов заключается в том, что они обеспечивают общий язык для общения с другими программистами, а также могут использоваться для изучения более удачных способов применения ОПП-технологий (особенно инкапсуляции и низкоуровневого связывания). В качестве заключительной подсказки примите во внимание, что большинство шаблонов в Delphi реа- лизуются с помощью интерфейсов лучше, чем с помощью классов (что стре- мится сделать ModelMaker, следуя классическому подходу). ModelMaker предлагает реализацию ряда дополнительных шаблонов, включая Visitor, Observer, Wrapper, Mediator и Decorator. Они жестко запрограммированы в Mo- delMaker и применяются строго определенным образом. Некоторые реализации оказались несколько лучше, чем остальные. Это было точкой раздора между неко- торыми разработчиками, и по этой причине (помимо прочих) ModelMaker поддер- живает другое средство применения шаблонов: шаблоны кода (будут рассмотре- ны в следующем разделе). Этот подход обеспечивает создание и настройку со стороны разработчика. Даже не учитывая расширенную поддержку со стороны ModelMaker; они довольно хороши, и предлагают фиксированную, цельную, рабо- тающую, основанную на Delphi реализацию основных проблем. 0508
Регенерация программного кода 509 Шаблоны программного кода Еще одной мощной функциональной возможностью ModelMaker (которая кажется потерянной, скрытой за миллиардами других удобств) являются шаблоны про- граммного кода — технология, которая может использоваться для создания соб- ственной реализации шаблонов дизайна. Шаблоны кода подобны снимку части класса, который может быть применен к другому классу. Другими словами, это коллекция членов класса, сохраненных в шаблоне, который может быть добавлен к другому классу. Кроме того, эти шаблоны могут иметь параметры (как макросы), которые могут относиться к классу. Будет появляться диалоговое окно, требую- щее заполнить некоторые значения, которые впоследствии будут применены к ча- сти шаблона. Одним из примеров является свойство массива. Объявление свойства массива в ModelMaker достаточно простое, но его полная реализация требует нескольких шагов: необходимо иметь не только само свойство массива, но и TList, или наслед- ников, для размещения элементов массива, а также средство, осуществляющее учет хранимых элементов. Даже в этом простом примере потребуется приложить неко- торые усилия. Введите шаблон свойства массива. Откройте в ModelMaker модель (или создайте новую модель и добавьте в нее наследника класса TObject), а также выберите класс, в который вы хотели бы добавить новое свойство массива. Щелк- ните правой кнопкой в списке членов (Member List) и выберите пункт Code Templates (шаблоны кода). Должна появиться плавающая панель инструментов Code Templates (обратите внимание, что точно такая же панель доступна на вкладке Patterns). Щелкните на кнопке Apply Array Property Template (применить шаблон свойства мас- сива) для открытия диалогового окна Code Template Parameters (параметры шаблона кода). Оно содержит список параметров, которые можно указать для применяемо- го шаблона (рис. 11.10). Можно выделить любой пункт в левом столбце и нажать клавишу F2 для редактирования значения этого параметра. Оставьте значения по умолчанию и щелкните на кнопке ОК. Parameter -„.-..г Т Object ItemCount Fl terns _______________ name of array property type of array property Method returning tt items TList Field storing items Рис. 11.10. Диалоговое окно параметров шаблона кода ModelMaker 0509
510 Глава 11. Моделирование и ООП-программирование Теперь в вашем классе появятся следующие члены: private Flterns TList protected function GetltemCount Integer function Get I terns (Index Integer) TObject public property ItemCount Integer read GetltemCount property Items!Index Integer] TObject read Getltems Теперь вы можете посмотреть, насколько удобной может быть эта технология. Прочие общие задачи (такие, как строго типизированные списки) и собственные реа- лизации шаблонов дизайна реализуются довольно просто. Давайте рассмотрим, как. Для создания собственного шаблона кода начнем с существующего класса, ко- торый уже содержит члены, которые вы хотите включить в шаблон. Выберите этот класс, а затем в списке Member List выберите члены, которые вы хотите использо- вать (это могут быть члены любого типа). Щелкните правой кнопкой в Member List и выберите пункт Create Code Template; появится диалоговое окно Save Code Template. Оно похоже на стандартное окно Save As (и вы должны указать, куда сохранить шаблон), но вы также можете уточнить, как должен быть представлен шаблон. Укажите имя шаблона и страницу палитры шаблонов, на которой он должен быть размещен. Обратите внимание на появляющееся при этом сообщение подтверж- дения; при желании можно изменить значок. Новый шаблон теперь доступен в палитре шаблонов, его можно добавлять в лю- бой класс. Для задания параметров шаблона необходимо изменить PAS-файл, ко- торый был создан при сохранении шаблона. Например, вот часть файла АггауРгор_ List.pas шаблона Array Property. unit ArrayProp_List //DEFINEMACRO Items=name of array property I/DEFINEMACRO TObject^type of array property //DEFINEMACRO ItemCount=Method returning # items //DEFINEMACRO Fltems^TList Field storing items TCodeTemplate = class (TObject) private «'Fltemsb TList protected function Get<'ItemCount1> Integer function Get<'Items'>(Index Integer) <'TObject'> public property <'ItemCount'> Integer read Get<'ItemCount'> property <4tems’>[Index Integer] <'TObject'> read Get<' I terns '> end Обратите внимание на строчки, которые начинаются с //DEFINEMACRO. Это то место, где должны быть определены ваши параметры, они будут представлены в диалоговом окне Code Template Parameters, которое вы видели ранее. Каждая строч- ка — это пара имя/значение (Name/Value): элемент слева от знака = является ре- дактируемым значением, а элемент справа — задаваемым вами объяснением пред- назначения этого параметра 0510
Малоизвестные изюминки 511 После предоставления списка параметров они могут быть использованы в шаб- лоне кода в качестве макросов. Обратите внимание на строчки типа: property <'Items!>[Index Integer] «’TObject'> read Get<lItems|>. Когда это свойство будет добавлено в класс как часть шаблона, эти макросы (подобно <!Items!>) будут заменены значением соответствующего параметра. Та- ким образом можно использовать параметры для более глубокой настройки шаб- лонов программного кода. Малоизвестные изюминки И на прощание хотелось бы представить список интересных особенностей, с кото- рыми вам, возможно, захотелось бы познакомиться более близко: Rethink Orthogonal С помощью сочетания клавиш Ctrl+O имеется возможность изменить используе- мые по умолчанию в редакторе схем прямые диагональные линии на вертикаль- ные и горизонтальные ломаные. Также можно заставить ModelMaker попытаться найти кратчайшую ломаную, нажав Shift+Ctrl+O. Visual Styles Manager Этот диспетчер (доступный из контекстного меню представления схемы в пункте Visual style ► Style manager) достоин целого раздела. Не пожалейте некоторого вре- мени, чтобы познакомиться с его работой. Вы можете определить широкое разно- образие иерархически связанных визуальных стилей для обозначений схемы и при- менять их «на лету». Кроме того, не забудьте также щелкнуть на кнопке Use Printing Style в редакторе схем (Diagram Editor) для разрешения вывода непечатаемых сим- волов и просмотреть, как схема будет выглядеть на бумаге. Design Critics Design Critics — это выразительная QA-особенность в ModelMaker. Design Critics (критики дизайна) представляют собой небольшие корректоры, запускаемые в фо- новом режиме и проверяющие ваш программный код. Для их использования убе- дитесь, что включено окно Show Messages (Shift+Ctrl+M), щелкните правой кнопкой на представлении Messages и выберите пункт Show Critics Manager. Я не рекомендую отключать design critic проверки временной отметки, поскольку он может предуп- редить вас в случае, если исходный файл на диске изменен не с помощью ModelMaker. Кроме того, с помощью API OpenTools продукта ModelMaker можно создать и соб- ственные design critic. Creational Wizard Это еще одна замечательная возможность некоторой автоматизации для постоян- нозанятого Delphi-программиста. Мастер Creational Wizard (доступный с помощью кнопки Wizards списка Member List) проверяет модель на члены класса, которые нуж- ны в настоящий момент или свободны, и добавляет их в соответствующий конст- РУктор или деструктор. Кроме того, он выполнит и другую работу и представит Предупреждения; для обращения к онлайновой справочной системе, находясь в ма- СтеРе, нажмите F1. 0511
512 Глава 11. Моделирование и ООП-программирование “ """" 1 - ’ " —. API Open Tools Так же как API Tools среды Delphi, эта функциональная возможность разрешает создание встраиваемых экспертов для ModelMaker. API-интерфейс является надеж- ным и включает доступ к схемам, а также ко всей модели программного кода. Здесь возможности расширения ModelMaker неограничены. Что далее? В этой главе я представил ограниченный обзор возможностей программы Model- Maker, охватывая совершенно не связанные между собой вопросы, такие как UML- схемы, шаблоны, регенерация и документация разработчика. Конечно же, была причина затронуть эту тему: ModelMaker помогает во всем, в то время как сама по себе IDE Delphi обеспечивает незначительную поддержку этих возможностей. Для более полного понимания различных методик обратитесь на веб-сайт и к другой упомянутой здесь документации. Как я говорил, ModelMaker имеет большой объем собственной документации, включая материалы о схемах и шаблонах. По- сетите веб-сайт этого продукта: www.ModelMakertools.com и загрузите то, чего нет в стандартном комплекте установки. Если вас заинтересовали рассмотренные здесь методики или вы не знаете, с че- го начать, обратитесь к онлайновой справочной системе ModelMaker и руководству пользователя (доступному на компакт-диске Delphi 7). Я также рекомендую веб- сайт Thoughtsmithy, на котором Роберт Лии (Robert Leahey) создал набор советов «Getting Started with ModelMaker» (Приступая к работе с ModelMaker). Он нахо- дится по адресу: www.thoughtsmithy.com/mmjump/MMGettingStarted_Intro.html. Следующая глава посвящена архитектуре Delphi-приложений. В ней представ- лен подробный анализ связанных с COM-технологий, доступных в операционной системе Windows и полностью поддерживаемых Delphi. После этого мы перейдем к разделу книги, посвященному программированию баз данных. 0512
От СОМ к COM4- Уже в течение 10 лет, начиная с первого выпуска Windows 3.0, компания Microsoft обещает, что ее операционная система и их API будут основаны на действитель- ных моделях объектов, а не на функциях. В соответствии с этими предположения- ми, Windows 95 (а позже и Windows 2000) должны быть основаны на революцион- ном подходе. Ничего подобного не случилось, но компания Microsoft продолжает продвигать компонентную модель объекта (Component Object Model, СОМ), по- строив на ней ядро Windows 95, подтолкнув интеграцию приложений с помощью СОМ и происходящих от нее технологий (таких, как автоматизация ’), и достигла пика, введя в Windows 2000 технологию СОМ+. Как только выпущено готовое основание, требующееся для высокоуровневого программирования СОМ, компания Microsoft решила переключиться на новую стрежневую технологию, являющуюся частью инициативы .NET. Мне кажется, что СОМ не очень подходила для интеграции мелких объектов, хотя она предоставля- ла очень удачную архитектуру для интеграции целых приложений или крупных объектов. В данной главе вы построите свой первый COM-объект. Я остановлюсь на ос- новных элементах, чтобы вы смогли понять роль этой технологии, не сильно уг- лубляясь в детали. Далее мы рассмотрим автоматизацию и роль библиотек типов, и вы увидите способы работы с типами данных Delphi на серверах автоматизации и у клиентов. В заключительной части главы мы с помощью компонента OleContainer изу- чим использование внедренных объектов, а также порядок разработки элементов управления ActiveX. Кроме того, мы познакомимся с некоторыми неизменными COM-технологиями (MTS и СОМ+) и рядом других дополнительных идей, вклю- чающих поддержку .NET-интеграции, предлагаемую Delphi 7. В данной главе рассматриваются следующие вопросы: ° что такое СОМ; ° COM, GUID и «фабрики классов2»; ° СОМ и интерфейсы Delphi; ° VCL-классы, поддерживающие СОМ; ° создание и использование сервера автоматизации; Далее в этой главе под словом «автоматизация» будет пониматься термин Automation, который из Маркетинговых соображений пришел на смену понятию OLE-автоматизация. Специальный COM-объект, способный создавать объекты некоторого класса. 0513
514 Глава 12. От СОМ к СОМ+ О использование библиотек типов; О компонент Container; О построение ActiveX и ActiveForm; О введение в СОМ+; О СОМ и .NET в Delphi 7. Краткая история OLE и СОМ Некоторая неопределенность в отношении технологии СОМ исходила оттого, что компания Microsoft в течение нескольких лет использовала для нее различные имена (в маркетинговых целях). Все началось с технологии Object Linking and Embedding (OLE) (связывания и внедрения объектов), которая была расширени- ем модели Dynamic Data Exchange (DDE) (динамический обмен данных). Исполь- зование буфера обмена позволяет скопировать исходные данные, а использование DDE позволяет соединять части двух документов. OLE дает возможность копиро- вать данные из серверного приложения в клиентское приложение вместе со сведе- ниями о сервере или ссылки на сведения, хранящиеся в реестре Windows. «Сы- рые» данные могут быть скопированы вместе со ссылкой (внедрение объекта) или остаться в исходном файле (связывание объекта). В настоящее время OLE-доку- менты называются активными документами (active document). Компания Microsoft обновила OLE до OLE2, изменив ее реализацию, сделав ее не просто расширением DDE, а добавив новые понятия, такие как «OLE-автома- тизация» и «элементы управления OLE». Следующим шагом было создание ядра Windows 95, использующего OLE-технологию и интерфейсы, а затем переимено- вание элементов управления OLE (ранее известных как OCX) в «элементы управ- ления ActiveX» за счет изменения спецификации, позволяющей «облегчить» эле- менты управления для обеспечения возможности передачи их по Интернету. На некоторое время компания Microsoft рекламировала элементы ActiveX как «под- ходящие для Интернета», ио эта идея никогда полностью не была принята сооб- ществом разработчиков, во всяком случае, в отношении определения «подходя- щих» для Интернет-разработок. По мере распространения этой технологии и повышения ее значимости для платформы Windows, компания Microsoft изменила наименование обратно на OLE, а затем на СОМ, и, в конце концов, в Windows 2000 — на СОМ+. Эти изменения в наименовании лишь частично связаны с технологическими изменениями, а в ос- новном они определялись маркетинговой политикой. Что же тогда есть СОМ? В общем, Component Object Model — это технология, определяющая стандартный способ связи между клиентским модулем и модулем сервера посредством специального интерфейса. В данном определении под поня- тием «модуль» подразумевается приложение или библиотека (DLL); два модуля могут одновременно выполняться на одном и том же компьютере или на различ ных машинах, соединенных через сеть. В зависимости от роли клиента и сервер3 допускается наличие множества интерфейсов. Для специальных целей можно Д° бавлять новые интерфейсы. Эти интерфейсы реализуются объектами сервер3- 0514
реализация IUnknown 515 Объект сервера обычно реализует несколько интерфейсов, и все объекты сервера имеют несколько общих возможностей, поскольку они должны реализовывать интерфейс IUnknown (который соответствует характерному для среды Delphi ин- терфейсу IInterface, представленному в главе 2). Приятным моментом является то, что Delphi полностью согласована с техно- логией СОМ. С появлением Delphi 3 ее реализация СОМ была более простой и ин- тегрированной в сам язык, чего не было в C++ и в других языках того времени до тех пор, пока программисты группы R&D (занимающиеся разработкой Windows) не признали: «Нам необходимо использовать СОМ так же, как это сделано в Del- phi». Эта простота главным образом обеспечивается включением типов интерфей- сов в сам язык Delphi. (Интерфейсы также используются для интеграции Java и СОМ на платформе Windows.) Как я уже упоминал, предназначение интерфейсов СОМ заключается в обес- печении связи между программными модулями, которые могут быть исполняе- мыми файлами или DLL. Реализация объектов СОМ в библиотеках DLL обычно проще, поскольку Win32-nporpaMMa и используемая ею DLL располагаются в од- ном и том же адресном пространстве оперативной памяти. Это означает, что если программа передает адрес памяти в DLL-библиотеку, то этот адрес остается дей- ствительным. При использовании двух исполняемых файлов СОМ приходится выполнить большой объем работы для обеспечения связи двух приложений. Этот механизм называется маршализацией (marshaling) (который, если быть более точ- ным, требуется и для DLL, если клиент является мультипоточным). Обратите вни- мание, что DLL, реализующая объекты СОМ, описывается как внутрипроцессный сервер (in-process sewer), а сервер, являющийся отдельным исполняемым файлом, называется внепроцессным сервером (out-of-processsewer). Однако когда DLL-биб- лиотеки исполняются на другой машине (DCOM) или в среде головной машины (MTS), то они также являются внепроцессными. Реализация IUnknown Перед тем как мы перейдем непосредственно к рассмотрению примеров создания СОМ, я представлю вам некоторые основы СОМ. Каждый COM-объект должен Реализовать интерфейс IUnknown, дублированный в среде Delphi интерфейсом ^Interface, который предназначен для не-COM использования интерфейсов (см. главу 2). Это — базовый интерфейс, от которого наследуются все остальные ин- терфейсы Delphi, а сама среда Delphi предоставляет парочку различных классов с готовыми к использованию реализациями lUnknown/IInterface, включая TInterfaced- Object и TComObject. Первый может использоваться для создания внутреннего объек- та, не относящегося к СОМ, а второй — для создания объектов, которые могут эк- спортироваться серверами. Как вы увидите позже, ряд других классов наследуются °т TComObject и обеспечивают поддержку для остальных интерфейсов, требуемых ДЛя СеРверов автоматизации и элементов управления ActiveX. Как уже говорилось в главе 2, интерфейс IUnknown имеет три метода: _AddRef, - elease и Queryinterface. Вот определение интерфейса IUnknown (позаимствован- н°е из модуля System): 0515
516 Глава 12. От СОМ к СОМч- type IUnknown = interface ['{00000000-0000-0000-С000-000000000046) '] function QueryInterface!const IID: TGUID: out Obj): Integer: stdcall: function _AddRef: Integer: stdcall; function _Release: Integer; stdcall; end: Методы _AddRef и -Release используются для реализации подсчета ссылок. Ме- тод Querylnterface оперирует сведениями о типах и совместимостью типов объек- тов. СОВЕТ------------------------------------------------------------------------------------- В представленном выше программном коде можно увидеть пример использования параметра out который возвращается из метода обратно в вызывающую программу, но без начального значения переданного программой в метод. Эти параметры были добавлены в язык Delphi для обеспечения поддержки СОМ, но они также могут использоваться и в обычных приложениях. По некоторым соображениям это делает передачу параметров более эффективной (в большинстве случаев ими являются интерфейсы, строчные значения и динамические массивы). Также важно отметить, что хотя определение типов интерфейсов в языке Delphi предназначено для обеспечения совместимо- сти с СОМ, сами интерфейсы Delphi не требуют наличия СОМ. Это уже подчеркивалось в главе 2, в которой мы построили пример на основе интерфейса без поддержки СОМ. Как правило, нет необходимости реализовывать эти методы, поскольку их можно унаследовать от одного из поддерживающих их класса Delphi. Самым важным клас- сом является TComObject, определенный в модуле ComObj. При создании СОМ-сер- вера его обычно наследуют от этого класса. TComObject реализует интерфейс IUnknown (отображая его методы на ObjAddRef, ObjQuery Interface и ObjRelease) и интерфейс ISupportErrorlnfo (посредством метода InterfaceSupports-Errorlnfo). Обратите внимание, что реализация подсчета ссылок для класса TComObject является потоко-защищенной, поскольку она вместо обыч- ных процедур Inc и Dec использует API-функции Interlockedlncrement и Interlocked- Decrement. Как можно ожидать (если вы помните обсуждение подсчета ссылок в главе 2), метод Release класса TInterfacedObject уничтожает объект, когда на него нет ссылок. Класс TComObject делает то же самое. Также не забывайте, что Delphi автоматиче- ски добавляет вызовы учета ссылок в откомпилированный код при использова- нии переменных, относящихся к интерфейсу, включая СОМ-переменные. И, наконец, обратите внимание, что метод Queryinterface выполняет двойную роль: О Queryinterface используется для проверки типов. Программа может задать объек- ту следующие вопросы: вы интересующего меня типа? Вы реализуете интер- фейс и специальные методы, которые я хочу вызвать? Если ответом будет «нет», то программа будет искать другой объект, возможно, запрашивая другой сервер- О если ответом будет «да», то Queryinterface обычно возвращает указатель на объект, используя его выходной ссылочный параметр (Obj). Для понимания роли метода Queryinterface важно учесть, что COM-объект МО жет реализовывать множество интерфейсов, также как это делает класс TComObjeC При вызове Queryinterface, вы с помощью параметра TGUID запрашиваете один из доступных для объекта интерфейс. 0516
реализация IUnknown 517 Помимо класса TComObject среда Delphi содержит ряд других предопределен- ных COM-классов. Вот список самых важных VCL-классов среды Delphi, которые будут использованы в последующих разделах: О TTypedComObject, определенный в модуле ComObj, унаследованный от TComObject и реализующий интерфейс IProvideClassInfo (помимо интерфейсов IUnknown и ISupportErrorlnfo, уже реализованных в базовом классе TComObject); О TAutoObject, определенный в модуле ComObj, унаследованный от TTypedComObject и помимо прочего реализующий интерфейс IDispatch; О TActiveXControl, определенный в модуле AxCtrls, унаследованный от TAutoObject и реализующий ряд интерфейсов (IPersistStreamlnit, IPersistStorage, lOleObject и lOleControl). Глобально уникальные идентификаторы Метод Querylnterface имеет параметр типа TGUID. Этот тип представляет собой уни- кальный идентификатор, используемый для идентификации классов СОМ-объек- тов (в этом случае GUID называется CLSID), интерфейсов (используется поня- тие IID) и ряд других СОМ- и системных компонентов. Если требуется узнать, поддерживает ли объект определенный интерфейс, вы запрашиваете объект, реа- лизует ли он интерфейс, имеющий определенный IID (которые по умолчанию яв- ляются COM-интерфейсами, определенными компанией Microsoft). Другой ID используется для индикации определенного класса или CLSID. Реестр Windows хранит этот CLSID с указанием связанного с ним исполняемого файла или DLL. Идентификатор класса определяется разработчиками СОМ-сервера. Одновременно оба эти идентификатора известны как GUID или глобально уни- кальные идентификаторы. Если каждый разработчик для указания СОМ-сервера использует число, как гарантировать, что эти значения не повторяются? Короткий ответ: никак. Действительный ответ заключается в том, что GUID является на- столько большим числом (16 байт или 128 бит — число из 38 цифр!), что практи- чески невозможно встретить два одинаковых случайных идентификатора с одина- ковым значением. Более того, для создания действительного GUID, отражающего некоторые системные сведения, программисты должны использовать особый API- вызов, называемый CoCreateGuid (непосредственно или через среду разработки). GUID-значения, созданные на машинах, имеющих сетевые карты, гарантиру- ют уникальность, поскольку сетевые карты содержат уникальные серийные номе- ра, являющиеся основой создания GUID. GUID-значения, созданные на машинах с идентификаторами центрального процессора (таких, как Pentium III), также га- рантируют уникальность даже при отсутствии сетевой карты. Даже при отсутствии Уникального аппаратного идентификатора GUID вряд ли когда-нибудь будут по- вторяться. внимание-------------------------------------------------------------— Будьте осторожны и не копируйте GUID из других программ (что может привести к наличию двух Различных COM-объектов, использующих одинаковый GUID). Также нельзя создавать собственный ’’Дентификатор, вводя произвольную последовательность чисел. Во избежание различных проблем ны*МИТе в РеДактРР6 Delphi сочетание Ctrl+Shift+G, и вы получите новый, правильно определен- действительно уникальный GUID. 0517
518 Глава 12. От СОМ к СОМ+ В среде Delphi тип TGUID (определенный в модуле System) — это структура-за- пись, которая является несколько странной, но требуется в ОС Windows. Благода- ря некоторому волшебству компилятора Delphi, обычно помогающему сделать более простыми некоторые объемные и длительные задачи, имеется возможность назначить GUID-значение с помощью шестнадцатеричной нотации, хранимой в виде строчного значения: const Class_ActlveForml. TGUID = '{1AFA6D61-7BB9-11D0-98D0-444553540000}'. Также можно передать интерфейс, идентифицируемый IID, для которого тре- буется GUID, и опять же среда Delphi сможет магически извлечь указываемый IID. Если требуется создать GUID вручную и не в среде Delphi, то можно вызвать API-функцию Windows CoCreateGuid (см. пример NewGuid) (рис. 12.1). Этот пример настолько прост, что я решил не представлять его программный код. jywewGmp {DB538CF1-47ВА-4Е91-В359 4950 2943834F} (26F8C69D ОЕ05 485A-B92F-DBAB6B3BB5A6} (43АА26СА-5350.4486-А5ВВ-Е845663С227С) {067F0C21-6F35 4А8Е -8886-9A25BF880784} {0824C23E-D38B-44F8-A37B-7C86FECCE6C2} {E504F2B8-2980-445B-9E20 8A680EF361F9) {7F335C60-842A-45F8-B323-75368044A5BB} (34208CFA-FF63-4B5F A1B5-8E89A6F7112B} {5C8B87ED-E45B 4966-9449-F7F8AA860774) Рис. 12.1. GUID, сгенерированные примером NewGuid. Значения определяются моим компьютером и временем, когда была запущена программа Для манипулирования GUID среда Delphi предоставляет функцию GUlDToString и обратную ей функцию StringToGUID. Также можно использовать соответствую- щие API-функции Windows, такие как StringFromGuid2, но в этом случае вместо обычного типа необходимо использовать тип WideString. При использовании СОМ необходимо обязательно использовать WideString, если только не использовать функции Delphi, выполняющие автоматическое преобразование. Если необходи- мо обойти Delphi-функции, которые могут напрямую вызывать API-функиии СОМ, то можно использовать тип PWideChar (указатель на массив символов, закан- чивающийся нулем) или выполнять приведение типа WideString в PWideChar (точно так же, как вы приводите строчное значение к типу PChar для низкоуров- невого API вызова Windows). Роль «фабрик классов» При регистрации в реестре GUID COM-объекта можно указать специальную API функцию, создающую этот объект, например, API CreateComObject: function CreateComObject (const Classic TGUID) IUnknown. Эта API-функция будет просматривать реестр в поисках сервера, регистрирУ ющего объект с данным GUID, загрузит его и, если сервер представляет собой DLL, 0518
Первый COM-сервер 519 вызовет метод DLLGetClassObject этой библиотеки. Это та функция, которую может предоставлять и экспортировать любой внутрипроцессный сервер: function DIIGetClassObject (const CLSID. IID: TGUID: var Obj)- HResult; stdcall: Эта API-функция в качестве параметров получает зарегистрированный класс И интерфейс, а возвращает объект в виде параметра-ссылки. Объект возвращается посредством этой функции в виде «фабрики классов» (class factory). Как следует из наименования, «фабрика классов» — это объект, способный со- здавать другие объекты. Каждый сервер может иметь несколько объектов. Сервер «открывает» «фабрику классов» для каждого COM-объекта, который он может создать. Одно из множества преимуществ такого упрощенного подхода Delphi к - разработке СОМ заключается в том, что «фабрику классов» для вас может предо- ставить сама система. Из-за этого в моем примере не пришлось добавлять собствен- ную «фабрику». Однако вызов API CreateComObject не приводит к остановке создания «фабрики класса». После извлечения «фабрики» CreateComObject вызывает метод Createlnstance интерфейса ICLassFactory. Этот метод создает требуемый объект и возвращает его. Если не произошло никаких ошибок, этот объект становится возвращаемым зна- чением API CreateComObject. Настройкой этого механизма (включая «фабрику классов» и вызов DLLGet- ClassObject) Delphi делает создание COM-объектов очень простым. В то же время CreateComObject операционной системы Windows — это лишь простой вызов функ- ции со сложным внутренним поведением. Что хорошо в Delphi, так это то, что мно- жество сложных механизмов СОМ обрабатываются RTL. Давайте рассмотрим подробно, как Delphi делает СОМ проще для освоения. Для каждого стержневого класса COM VCL Delphi также определяет «фабри- ку классов». Классы «фабрики классов» формируют иерархию. К ним относятся: TComObjectFactory, TTypedComObjectFactory, TAutoObjectFactory и TActiveXControlFactory. «Фабрики классов» важны и необходимы каждому COM-серверу. Обычно Delphi- программы используют «фабрики классов» путем создания объекта в разделе ини- циализации модуля, определяющего соответствующий класс серверного объекта. Первый СОМ-сервер Нет лучше способа понять технологию СОМ, чем построить простой СОМ-сер- веР, помещенный в DLL. Библиотека, содержащая COM-объект, индицируется в Delphi как библиотека ActiveX. По этой причине разработку данного проекта не- обходимо начинать с выбора File ► New ► Other, перехода на страницу ActiveX, и вы- бором значка ActiveX Library. Это приводит к созданию файла проекта, который я со- хранил как пример FirstCom. Вот его программный код: library FirstCom. uses ComServ. Sports 0519
520 Глава 12. От СОМ к CQM+ 01IGetClassObject, DllCanUnIoadNow DllRegisterServer 011UnregisterServer. {SR * RES} begin end. Четыре экспортируемые DLL функции требуются для функционального удоб- ства СОМ и используются следующим образом: О DUGetClassObject — для обращения к «фабрике классов»; О DtlCanUnloadNow — для проверки: уничтожил ли сервер все объекты и может ли он быть выгружен из памяти; О DURegisterServer и DllUnregisterServer — для добавления и удаления сведений о сервере в реестр Windows. Обычно не приходится реализовывать эти функции, поскольку Delphi обеспе- чивает их стандартную реализацию в модуле ComServ. По этой причине требуется лишь экспортировать их в программном коде сервера. СОМ-интерфейсы и СОМ-объекты Теперь, когда имеется каркас СОМ-сервера, можно приступать к его дальнейшей разработке. Очередной шаг состоит в написании программного кода интерфейса, который должен реализовываться сервером. Вот программный код простого интерфейса, который необходимо поместить в отдельный модуль (я назвал его Numlntf): type INumber = interface [’{B4131140-7C2F-11D0-9BD0-444553540000}’] function GetValue Integer stdcall procedure SetValue (New Integer) stdcall procedure Increase stdcall. end. После объявления пользовательского интерфейса в сервер можно добавлять объекты. Для этого можно воспользоваться мастером COM Object Wizard (дос- тупным на странице ActiveX, открываемой при выборе File ► New ► Other) (рис. 12.2). Введите имя класса сервера и его описание. Я отключил генерацию библиотеки типов (при использовании которой Delphi 7 отключает поле интерфейса, чего не было в Delphi 6) для исключения введения одновременно слишком большого чис- ла разделов. Также необходимо выбрать модель организации потоков и модель создания экземпляров. Код, сгенерированный мастером, очень прост. Интерфейс содержит определе- ние класса, которое необходимо наполнить методами и данными: type TNumber = class(TComObject INumber) protected {здесь определяются INumber-методы} end 0520
Первый COM-сервер 521 COM Object Wizard £lassMame J instancing: Threading Me 3-- ^Number | Miihple Instance I Apartment D*&cwt«?n fs ample Mlasted OK canc&t j нФ Рис. 12.2. Мастер COM Object Wizard Помимо GUID для сервера (хранимого в константе Class_Number) в разделе ини- циализации модуля также существует код, который использует большинство па- раметров, которые установлены в диалоговом окне мастера: initialization TComObjectFactory CreatelComServer TNumber Class_Nuinber 'Number', 'Number Server’. ciMultiInstance tmApartment) Этот программный код создает объект класса TComObjectFactory, передавая в ка- честве параметра глобальный объект ComServer, ссылку класса на только что опре- деленный класс, GUID класса, имя сервера, описание сервера и выбранные моде- ли организации потоков и создания экземпляров. Глобальный объект ComServer, определенный в модуле ComServ, является дис- петчером «фабрик классов», доступных в библиотеке сервера. Для поиска класса, поддерживающего данный запрос COM-объекта, он использует свой собственный метод ForEachFactory. Как вы уже видели, модуль ComServ реализует функции, кото- рые требуются DLL для того, чтобы быть СОМ-библиотекой. Проверив программный код, сгенерированный мастером, можно закончить его формирование, добавив в класс TNumber методы, требуемые для реализации ин- терфейса INumber, и записать их программный код. Теперь в сервере вы имеете работающий СОМ-объект. Модель организации потоков и модель создания экземпляров При создании COM-сервера требуется выбрать подходящую модель организации потоков и модель создания экземпляров, которые значительно влияют на поведе- ние СОМ-сервера. Модель создания экземпляров влияет в основном на внепроцессные серверы '.любой COM-сервер в отдельном исполняемом файле, а не в DLL) и может при- нимать их значения. Multiple Называет, что когда СОМ-объект потребуется нескольким клиентским прило- жениям тп гкг-трма папугтит несколько экземпляров сервера. 0521
522 Глава 12. От СОМ к СОМ+ Single Указывает, что когда COM-объект потребуется нескольким клиентским прило- жениям, то будет существовать только один экземпляр серверного приложения Для обслуживания запросов он создаст необходимое число внутренних объектов Internal Указывает, что объект может создаваться только внутри сервера. Его не может за- прашивать клиентское приложение (эта настройка соответствует внутрипроцесс- ному серверу). Второе решение связано с поддержкой потоков COM-объектов, которое дей- ствительно только в отношении внутрипроцессных серверов (DLL). Модель фор- мирования потока — это объединенное решение для клиентского и серверного приложений: если обе стороны согласны с определенной моделью, то она и ис- пользуется для соединения. Если согласия найдено не будет, то СОМ по-прежне- му может установить соединение, используя маршалинг (marshaling), что может замедлить выполнение операций. Также учтите, что сервер должен не только пуб- ликовать свою модель формирования потока в реестре (в соответствии с настрой- кой параметра мастера), в программном коде он также должен придерживаться правил модели формирования потока. Вот ключевые моменты различных моде- лей формирования потоков: Single Model Не находит реальной поддержки. Запросы, поступающие на COM-сервер, упоря- дочиваются последовательным образом, поэтому клиент может одновременно вы- полнять только одну операцию. Apartment Model или «Single-Threaded Apartment» Только поток, создавший объект, может вызывать его методы. Это означает, что запросы для каждого объекта сервера последовательно упорядочиваются, но од- новременно и другие объекты того же сервера могут получать запросы. По этой причине объект сервера должен быть особенно внимательным при обращении к глобальным данным сервера (используя критические разделы, мьютексы1 или другие технологии синхронизации). Эта модель формирования потоков обычно используется в Internet Explorer в отношении ActiveX. Free Model или «Multithreaded Apartment» Клиент не имеет ограничений. Это означает, что одновременно один объект может использоваться множеством потоков. По этой причине каждый метод любого объек- та должен защищать себя и нелокальные данные от множества одновременных вызовов. Эта модель формирования потоков более сложная для поддержки серве- ром, чем Single- и Apartment-модели, поскольку даже обращение к принадлежа- щим к объекту экземплярам данных должно выполняться с учетом безопасности потоков. Both Объект сервера поддерживает Apartment- и Free-модели. ' Специальный синхронизирующий объект в межпроцессном взаимодействии, подающий сигнал, есл11 он не захвачен каким-либо потоком. 0522
Первый COM-сервер 523 Neutral Эта модель появилась в Windows 2000 и доступна только под СОМ+. Она указы- вает, что множество клиентов могут одновременно вызывать объект с помощью различных потоков, но СОМ гарантирует, что один и тот же метод не будет вызван одновременно. Сохранение последовательного доступа к данным объекта является обязательным требованием. В технологии СОМ это отражается в Apartment-модели. Инициализация СОМ-объекта Если вы посмотрите на определение класса TComObject, то вы обратите внимание на то, что он имеет невиртуальный конструктор. Фактически он имеет множество конструкторов, каждый из которых вызывается с помощью виртуального метода Initialize. По этой причине для установки свойств СОМ-объекта нет необходимо- сти определять новый конструктор (который никогда не будет вызван). Вместо этого необходимо перекрыть его метод Initialize, как это сделано в классе TNumber. Вот окончательная версия этого класса: type TNumber = cl ass(TComObject. INumber) private fValue. Integer; public function GetValue: Integer; virtual, stdcall, procedure SetValue (New: Integer): virtual: stdcall; procedure Increase, virtual: stdcall: procedure Initialize; override. destructor Destroy; override: end; Как можно заметить, также перекрыт и деструктор этого класса, поскольку я хотел проверить автоматическое уничтожение COM-объектов, осуществляемое Delphi. Тестирование СОМ-сервера Теперь, когда мы закончили создание объекта «COM-сервер», его можно зарегис- трировать и использовать. Откомпилируйте его программный код и выберите ко- манду Run k Register ActiveX Server. Это приводит к регистрации сервера на собствен- ной машине, выражающееся в добавлении изменения в локальный реестр. При распространении этого сервера необходимо установить его на клиентских компьютерах. Для этого можно написать REG-файл (помещающий сведения о сер- вере в реестр). Однако это не является наилучшим подходом, поскольку сервер Уже имеет функцию, которую можно использовать для регистрации сервера. Как вы уже видели, эта функция может быть активизирована средой Delphi либо сле- дующими способами: ° можно передать DLL СОМ-сервера в качестве параметра командной строки программе RegSvr32.exe, находящейся в каталоге \Windows\System; ° можно использовать программу, подобную демонстрационной TRegSvr.exe, поставляемую вместе с Delphi. (Откомпилированная версия находится в ката- логе \Bin, а исходный программный код — в \Demos\ActiveX.) 0523
524 Глава 12. От СОМ к СОМ+ О можно предоставить возможность программе установки вызвать функцию ре- гистрации. После регистрации сервера можно включить клиентскую сторону примера. В данном случае он называется TestCom и хранится в отдельном каталоге. Эта про- грамма загружает DLL сервера посредством COM-механизма благодаря представ- ленным в реестре сведениям сервера, поэтому для клиента не обязательно знать, в каком каталоге находится сервер. Форма, выводимая этой программой, похожа на форму, используемую для про- верки DLL главы 10. В клиентскую программу необходимо включить (include) файл исходного программного кода с интерфейсом, а также повторно объявить GUID СО М-сервера. После запуска программы все кнопки отключены (определено в ходе разработки), и она включает их только после того, как будет создан объект. При этом если в ходе создания одного из объектов возникнет исключение, то кнопки, связанные с этим объектом, останутся недоступными: procedure TForml FormCreate(Sender TObject). begin // создать первый объект Numl = CreateComObject (Class_Number) as INumber. Numl SetValue (SpinEditl Value) Label 1 Caption = 'Wuml ' + IntToStr (Numl GetValue). Buttonl Enabled = True Button2 Enabled = True // создать второй объект Num2 = CreateComObject (Class_Number) as INumber. Label2 Caption = 'Num2 ' + IntToStr (Num2 GetValue): Buttons Enabled = True Button4 Enabled = True. end: Особое внимание обратите на вызов метода CreateComObject и следующее за ним приведение типов as. API-вызов запускает механизм создания COM-объекта. Бо- лее подробно мы его уже рассматривали. Этот же вызов динамически загружает DLL сервера. Возвращаемое значение является объектом типа IUnknown. Перед тем как будет присвоен полям Numl и Num2, которые теперь имеют тип INumber, этот объект должен быть преобразован в соответствующий тип интерфейса. ВНИМАНИЕ----------------------------------------------------------------------——’ Для преобразования интерфейса в определенный тип всегда используйте приведение as, которое для интерфейсов выполняет вызов Queryinterface. В качестве альтернативы можно выполнить не- посредственный вызов Queryinterface или Supports. В отношении интерфейсов приведение as (или соответствующий вызов функции) является единственным способом извлечения одного интерфей- са из другого интерфейса. Приведение интерфейсного указателя в другой интерфейсный указатель непосредственно является ошибкой — никогда так не делайте1 В этой программе также имеется кнопка (в нижней части формы) с обработчи- ком события, который создает новый COM-объект, используемый для получения значения числа, следующего за 100. Чтобы увидеть, зачем я добавил в пример этот метод, щелкните на кнопке в сообщении, выводящем результат. Вы увидите вто- рое сообщение, указывающее, что объект уже уничтожен. Это сообщение свиДе" 0524
Первый COM-сервер 575 тельствует о том, что выход интерфейсной переменной из области видимости при- водит к вызову метода-Release этого объекта, уменьшающего счетчик ссылок объек- та и уничтожающего объект, если счетчик ссылок достигнет нуля. То же самое происходит с другими двумя объектами при завершении програм- мы. Даже если программа не сделает это явно, оба объекта будут действительно уничтожены, что ясно продемонстрирует сообщение, выводимое деструктором Destroy. Это происходит благодаря тому, что они были объявлены как объекты ин- терфейсного типа, и Delphi будет использовать для них подсчет ссылок. Тем не менее если вы захотите уничтожить ссылку СОМ-объекта на интерфейс, восполь- зоваться методом Free будет невозможно (интерфейсы не имеют метода Free), мож- но лишь присвоить переменной-интерфейсу значение nil. Это приведет к удале- нию ссылки и возможному уничтожению объекта. Использование свойств интерфейса В качестве дальнейшего небольшого шага можно расширить пример добавлением в интерфейс INumber свойства. При добавлении свойства в интерфейс вы указыва- ете тип данных, а затем директивы read и write. Свойства могут быть «только для чтения» или «только для записи», но инструкции read и write всегда должны ссы- латься на метод, поскольку не могут содержать ничего кроме методов. Вот обновленный интерфейс, являющийся частью примера PropCom: type INumberProp = interface ['{B36C5800-8E59-11D0-98D0-444553540000}'] function GetValue Integer stdcall, procedure SetValue (New Integer) stdcall property Value Integer read GetValue write SetValue procedure Increase stdcall end Я дал этому интерфейсу новое имя и, что более важно, новый идентификатор интерфейса. Можно было унаследовать новый тип интерфейса от предыдущего, но это не дало бы реальных преимуществ. СОМ сама по себе не поддерживает на- следование, и с точки зрения СОМ все интерфейсы различны, поскольку имеют различные идентификаторы. Не стоит говорить, что в Delphi наследование может использоваться для совершенствования структуры программного кода интерфей- сов и реализующих их серверных объектов. В примере PropCom я обновил объявление серверного класса, добавив ссылку на новый интерфейс и предоставив новый идентификатор серверному объекту. Клиентская программа (TestProp) теперь вместо методов SetValue и GetValue может использовать свойство Value. Вот небольшой фрагмент метода FormCreate: Numl = CreateComObject (Class_NumPropServer) as INumberProp uml Value = SpinEditl Value abel1 Caption = 7tel ’ + IntToStr (Numl Value) Разница между использованием свойств и методов интерфейсов только син- таксическая, поскольку свойства интерфейсов не могут обращаться к частным дан- ным, как это могут делать свойства класса Delphi. Посредством использования свойств можно несколько облегчить восприятие программного кода. 0525
526 Глава 12. От СОМ к СОМ+ Вызов виртуальных методов Вы уже построили несколько COM-примеров, однако по-прежнему можете чув- ствовать некоторое неудобство от идеи программы, вызывающей методы объек- тов, создаваемых в DLL. Как это сделать возможным, если эти методы не экспор- тированы DLL? СО М-сервер (DLL) создает объект и возвращает его вызывающему приложению. При этом DLL создает объект с таблицей виртуальных методов (virtual method table, VMT). Если быть более точным, то объект имеет VMT для своего класса, плюс VMT для каждого реализуемого им интерфейса. Основная программа получает назад переменную-интерфейс с VMT требуемо- го интерфейса. Эта VMT может использоваться для вызова методов и для запроса других интерфейсов, поддерживаемых данным COM-объектом (поскольку метод Queryinterface доступен как часть VMT интерфейса IUnknown). Главная программа не должна знать адресного пространства этих методов, по- скольку его знают объекты, точно так же как они делают с полиморфными вызова- ми. Но технология СОМ более мощная: нет необходимости знать, какой язык про- граммирования использовался для создания объекта, который предоставляет VMT (следуя стандартам, диктуемым СОМ). ПРИМЕЧАНИЕ----------------------------------------------------- COM-совместимые VMT обладают странным эффектом. Имена методов не важны, если их адрес находится в соответствующем месте VMT. Вот почему имеется возможность отобразить метод ин- терфейса на реализующую его функцию. Подводя итог, можно сказать, что СОМ предоставляет независимый от языка двоичный стандарт объектов. Совместно используемые модулями объекты отком- пилированы и их VMT имеют конкретную структуру, определяемую СОМ, а не используемой средой разработки. OLE-автоматизация До сих пор мы видели, что технология СОМ может применяться для обеспечения совместного использования объектов исполняемыми файлами и библиотеками. Однако зачастую пользователям необходимы приложения, которые могут общаться друг с другом. Одним из подходов, которые могут использоваться для достижения этой цели, является автоматизация (ранее называемая OLE Automation). После рассмотрения пары примеров, использующих пользовательские интерфейсы, ос- нованные на библиотеках типов, мы перейдем к разработке контроллеров автома- тизации Word и Excel, рассмотрев, как в эти приложения переносятся сведения из баз данных. СОВЕТ-------------------------------------------------— Имеющаяся в настоящее время документация компании Microsoft вместо термина OLE Automation (OLE-автоматизация) использует понятие Automation (автоматизация), а вместо OLE-документа понятия active document (активный документ) и compound document (составной документ). В этой книге я старался использовать новую терминологию, хотя по-прежнему использовал старую при ставку «OLE» как более точную. 0526
OLE-автоматизация 527 В ОС Windows приложения существуют не изолированно; пользователи, как правило, желают, чтобы они взаимодействовали. Буфер обмена и DDE предлага- ют простой способ взаимодействия между приложениями, поскольку с их помощью пользователи могут копировать и вставлять данные. Однако все больше и больше программ предлагают интерфейс автоматизации, позволяющий управлять ими из других программ. Помимо очевидного преимущества программируемой автома- тизации по сравнению с ручными операциями пользователя, эти интерфейсы пол- ностью нейтральны по отношению к языку программирования, используемому для их написания. Для реализации в Delphi автоматизация очень проста благодаря интенсивной работе, выполняемой компилятором и VCL, защищающими разра- ботчика от этих сложностей. Для поддержки автоматизации Delphi предоставляет мастер и мощный редактор библиотеки типов, а также поддерживает двойные ин- терфейсы. При использовании внутрипроцессной DLL клиентское приложение может использовать сервер и непосредственно вызывать его методы, поскольку они находятся в одном адресном пространстве. При использовании автоматиза- ции ситуация более сложная. Клиент (называемый контроллером) и сервер пред- ставляют собой два совершенно разных приложения, выполняемых в различных адресных пространствах. По этой причине система должна координировать вызо- вы методов, используя сложный механизм передачи параметров, называемый мар- шалингом (marshaling) (более подробно я не могу его рассматривать). Технически поддержка автоматизации в СОМ подразумевает реализацию ин- терфейса IDispatch, объявленного в Delphi в модуле System следующим образом: type IDispatch = Interfacet IUnknown) ['{00020400-0000-0000-COOO-000000000046}'] function GetTypeInfoCount(out Count: Integer): HResult; stdcall: function GetTypeInfo(Index. LocalelD: Integer: out Typeinfo): HResult: stdcall; function GetIDsOfNamestconst IID: TGUID; Names: Pointer; NameCount. LocalelD: Integer: DispIDs: Pointer): HResult; stdcall; function InvoketDispID: Integer; const IID: TGUID: LocalelD: Integer; Flags: Word: var params; VarResult, Exceplnfo, ArgErr: Pointer): HResult; stdcall; end; Первые два метода возвращают сведения о типах; последние два используются Для вызова действительного метода сервера автоматизации. Фактически вызов осуществляется только последним методом, (Invoke) в то время как GetIDsOfNames используется для определения идентификатора координации (требуемого Invoke) на основе имени метода. Все, что необходимо сделать для создания сервера авто- матизации в Delphi, — это определить библиотеку типов и реализовать ее интер- фейс. Все остальное выполняется компилятором и программным кодом VCL (точ- нее частью VCL, изначально называемой DAX-структурой). Роль IDispatch становится более очевидной, если вы учтете наличие трех спосо- бов, которыми контроллер может вызвать методы, «открываемые» сервером авто- матизации: ° он может запросить выполнение метода, передавая его имя в виде строчного значения таким же способом, как и при динамическом вызове DLL. Это — имен- но то, что делает среда Delphi при использовании вариантного типа (см. после- дующее примечание) для вызова сервера автоматизации. Данная методика 0527
528 Глава 12. От СОМ к СОМ+ проста в использовании, но довольно медленная и обеспечивает лишь незначи- тельную проверку типов компилятором. Она подразумевает вызов GetIDsOfNames, сопровождающийся вызовом Invoke; О он может импортировать объявление интерфейса-координатора Delphi (dispin- terface) для объекта на сервере и вызвать его методы более явным способом (ко- ординируя номер, вызываемый непосредственно Invoke как часть Displd каждого метода, известного в ходе компиляции). Эта методика основана на интерфей- сах и позволяет компилятору проверять типы параметров и создавать быстро работающий код, но она требует от программиста больших усилий (а именно, использования библиотеки типов). Кроме того, в конце концов приходится свя- зывать приложение-контроллер с определенной версией сервера; О он может вызывать интерфейс непосредственно, через интерфейс vtable, то есть обращаться к нему как к нормальному COM-объекту. Это работает в большин- стве случаев, поскольку большинство интерфейсов серверов автоматизации обеспечивают двойные интерфейсы (т. е. поддерживают как IDispatch, так и обычный СОМ-иитерфейс). В представленных далее примерах вы увидите все эти методики и сравните их. СОВЕТ------------------------------------------------------------------- Для хранения ссылки на объект автоматизации можно использовать вариантный тип данных. В язы- ке Delphi вариантным является тип, который может хранить любые типы данных, «подстраиваясь» под их значения. Вариантный тип данных помимо значений базовых типов (таких, как Integers, strings, characters и Boolean) может хранить значения интерфейсного типа — IDispatch. Тип вариан- тных данных проверяется в ходе выполнения; вот почему компилятор может компилировать про- граммный код, даже не имея представления о методах сервера автоматизации. Координация вызова автоматизации Наиболее значимым отличием между двумя подходами является то, что второй обычно требует библиотеку типов, т. е. библиотеку, являющуюся фундаментом СОМ. Обычно библиотека типов представляет собой коллекцию информации о типах, которую также можно найти в COM-объекте (не осуществляя поддержки координации). Эта коллекция, как правило, описывает все элементы (объекты, интерфейсы и другие типы информации), которая делается доступной общим COM-сервером или сервером автоматизации. Ключевое отличие между библио- текой типов и другими описаниями этих элементов (таких, как программный код на языке С или Pascal) заключается в том, что библиотека не зависит от языка. Типы элементов определены посредством СОМ как поднабор стандартных эле- ментов языка программирования и могут использоваться любым средством разра- ботки. Зачем нам нужна эта информация? Как упоминалось ранее, при вызове метода объекта автоматизации с использо- ванием вариантного типа компилятор Delphi в ходе компиляции может и не знать об этом методе. Небольшой фрагмент программного кода примера, используюше" го старый интерфейс автоматизации приложения Word, зарегистрированного как Word.Basic, иллюстрирует, как это упростить для программиста: var VarW Variant. 0528
OLE-автоматизация 529 begin VarW = Created eObject ('Hord Basic') VarW Ft 1 eNew VarW Insert ('Mastering Delphi by Marco Cant ') СОВЕТ------------------------------------------------------------------------------------ Как вы увидите далее, последние версии Word по-прежнему регистрируют интерфейс Word.Basic, который соответствует внутреннему языку макросов WordBasic, но помимо него также регистриру- ется новый интерфейс Word.Apphcation, который соответствует языку макросов VBA. Delphi предос- тавляет компоненты, которые упрощают подключение приложений пакета Microsoft Office. Они будут представлены далее. Эти три строчки программного кода запускают Word (если только он уже не запущен), создают новый документ и добавляют в него несколько слов. Результат работы представлен на рис. 12.3. Normal Normal Normal Erwflsrertj (Mastering Delphi by Marco Cantu Hello, Delphians 1Л ' СЫ i Jy-’ Microsoft Wwd 'DotwHcrrlS ’Jj He Sow Insert tod» Wte SBmtow adp Р|»В ДйГ] " | Раде 1 Set 1 111 Рис. 12.3. Документ Word, созданный и заполняемый Delphi-приложением WordTest К сожалению, компилятор Delphi не имеет возможности проверить, существу- ют ли эти методы. Выполнение всех проверок типов во время выполнения крайне рискованно, поскольку в случае даже незначительной ошибки в имени функции вы не получите сообщения об ошибке до тех пор, пока программа не будет запуще- на и не дойдет до выполнения ошибочного кода. Например, если вы наберете VarW.Isnert, компилятор не пожалуется на ошибку в написании, но во время выпол- нения вы получите ошибку. Поскольку Word не сможет распознать имя, он посчи- тает, что такой метод не существует. Хотя интерфейс IDispatch поддерживает только что рассмотренный подход, он также может (что более безопасно для сервера) экспортировать описание его ин- терфейсов и объектов с помощью библиотеки типов. Такая библиотека позже мо- жет быть преобразована специальным средством (таким, как среда Delphi) в опре- деление, написанное на языке, который вы хотите использовать для написания клиентской программы или программы-контроллера (например, на языке Delphi). Это позволяет компилятору проверять, верным ли является программный код, авам — использовать в редакторе Delphi функции Code Completion и Code Parameters. 0529
530 Глава 12. От СОМ к СОМ+ После того как компилятор выполнит эти проверки, для отправки запроса на сервер он может использовать две разные методики. Он может воспользоваться обычной VTable (т. е элементом в объявлении типа интерфейса), либо использо- вать dispinterface (интерфейс координации). Ранее в этой главе мы использовали объявление типов интерфейсов, поэтому оно должно быть вам знакомо. Dispinterface обычно является способом отображения каждого элемента интерфейса на число. После этого вызовы к серверу могут координироваться только посредством числа, взывающим IDipatch.Invoke, без необходимости выполнения дополнительной опе- рации вызова IDispatch.GetIDsOfNames. Можно воспринимать это как промежуточ- ную технологию между координацией посредством имен функций и использова- нием непосредственных обращений к VTable. СОВЕТ------------------------------------------------------------------------__ Обозначение dispinterface является ключевым словом. Dispinterface — автоматически генерируется редактором библиотеки типов для каждого интерфейса. Совместно с dispinterface Delphi использует и другие ключевые слова: dispid — указывает число, ассоциируемое с каждым интерфейсом; readonly и wnteonly — дополнительные спецификаторы свойств. Понятие, используемое для описания возможности связи с сервером двумя раз- личными способами, используя динамичный или статический подход, называется двойные интерфейсы (dual interfaces). При написании COM-контроллера можно выбрать обращение к методам двумя способами: либо с использованием позднего связывания (late binding) и механизма, обеспечиваемого dispinterface, либо с ис- пользованием раннего связывания (early binding) и механизма, основанного на VTables. Важно учесть, что (помимо других моментов) различные технологии замедля- ют или ускоряют выполнение Поиск функции по имени (и выполнение проверки типов в ходе выполнения) — более медленный подход. Использования dispinterface приводит к более быстрому выполнению, а использование непосредственных вы- зовов к VTable — самый быстрый подход. Быстродействие всех этих вариантов можно проверить с помощью примера TlibCli, рассматриваемого далее в этой главе. Создание сервера автоматизации Давайте перейдем к написанию сервера автоматизации. Для создания объекта ав- томатизации можно использовать имеющийся в Delphi мастер Automation Object Wizard Начните с создания нового приложения, откройте File ► New ► Other, перей- дите на страницу ActiveX и выберите Automation Object. От кроется ,..астер Automation Object Wizard (см. рис. на следующей странице) В этом мастере введите имя класса (без начальной Т, поскольку она будет до- бавлена автоматически) и щелкните на кнопке ОК Теперь откроется редактор биб- лиотеки типов. ПРИМЕЧАНИЕ--------------------------------------------------—"' Среда Delphi может генерировать серверы автоматизации, которые также экспортируют события- Установите в мастере соответствующие флажки, и Delphi добавит необходимые элементы в библио- теку типов и в генерируемый программный код. 0530
Создание сервера автоматизации 531 Automation Object Wizard ОДаи Name |Г {nslancing: (MuitipieTnstance Threading yodel [Apartment Options”----------------- Generals Event support code I OK I Caned | Редактор библиотеки типов Редактор библиотеки типов (type-library editor) используется для определения в Delphi библиотеки типов. На рис. 12.4 представлено его окно после добавления в него ряда элементов Редактор позволяет добавлять методы и свойства в только что созданный объект автоматизации или в СОМ-объект, созданный ранее с по- мощью мастера COM Object Wizard. После этого он сгенерирует и библиотеку типов (TLB-файл) и соответствующий исходный программный код на языке Delphi, сохраняемый в модуле, называемом модулем импорта библиотеки типов. TLbOemo г) IFirstServer ES0533S5 Value Value а FirslServef AStSwies | Рдопеьв] Hags | Text 1 Рис. 12.4. Редактор библиотеки типов представляет уточняющие сведения интерфейса Для облегчения работы с редактором библиотеки типов Delphi у меня имеется Два предложения. Первое, и наиболее простое щелкните правой кнопкой на пане- ли инструментов и включите параметр Text Labels (Текстовые надписи); это приве- дет к появ тению надписей на каждой кнопке панели, что облегчает использование Редактора. Второе предложение: перейдите на страницу Type Library (Библиотека типов) диалогового окна Environment Options (Параметры среды) и над переключа- телем IDL language выберите Pascal language Эта настройка определяет нотацию, 0531
532 Глава 12. От СОМ к СОМ+ используемую редактором для представления методов и параметров, и также для редактирования типов параметров методов или типов свойств. Если только вы не используете языки C/C++ для написания СОМ, вы, вероятно, предпочтете мыс- лить понятиями Delphi, а не понятиями IDL. ВНИМАНИЕ------------------------------------------------------------------ В этой части книги я объясню, как взаимодействовать с редактором библиотек типов исходя из указанных настроек, поскольку предоставление описания в понятиях IDL будет более сложным и не столь уместным. Для построения первого примера попробуем добавить свойство и метод в сер- вер с помощью соответствующих кнопок редактора и введением их наименований либо в элемент управления Tree View в левой части окна, либо в строку редакти- рования Name справа. Эти два новых элемента будут добавлены в интерфейс, кото- рый я назвал IFirstServer. На странице Parameters (Параметры) можно определить параметры процедуры. На той же странице для функций можно определить возвращаемый тип. В нашем случае метод ChangeColor не имеет параметров и в Delphi его определение выглядит как: procedure ChangeColor safecall СОВЕТ------------------------------------------------------------------------ Методы, содержащиеся в интерфейсах автоматизации Delphi, обычно используют соглашение вы- зова safecall. Для каждого метода оно обеспечивает оболочку из блока try/except и предоставляет возвращаемое по умолчанию значение, указывающее на успешное выполнение или на ошибку. Кроме того, оно настраивает COM-объект rich error, содержащий сообщение исключения, поэтому наследуемые клиенты (такие, как Delphi-клиенты) могут воссоздать серверное исключение на кли- ентской стороне. Теперь можно добавить свойство в интерфейс, щелкнув на кнопке Property (Свойство) в панели инструментов редактора библиотек типов. Опять же имеется возможность ввести его имя (например, Value) и выбрать тип данных в списке Туре (Тип). Помимо выбора одного из множества уже существующих в списке типов можно непосредственно ввести другие типы, особенно это касается интерфейсов других объектов. Определение свойства Value в примере соответствует следующим элементам Delphi-интерфейса: function Get_Value Integer safecall procedure Set_Value(Value Integer) safecall property Value Integer read Get_Value write Set_Value Щелчок на кнопке Refresh (Обновить) панели инструментов редактора библио- тек типов приводит к генерации (или обновлению) Delphi-модуля, содержащего интерфейс Серверный программный код Теперь можно закрыть редактор библиотек свойств и сохранить изменения. Эта операция приводит к добавлению в проект трех пунктов файла библиотеки типов, соответствующего Delphi-onpeделения и объявления серверного объекта Библио- 0532
Создание сервера автоматизации 533 тека типов подключена к проекту с помощью выражения подключения ресурсов, добавляемого в исходный программный код файла проекта: {$R * TLB} Кроме того, повторно открыть редактор библиотек типов можно с помощью команды View ► Type Library либо выбором соответствующего TBL-файла в обыч- ном диалоговом окне File ► Open. Как уже упоминалось, библиотека типов также конвертируется в определение интерфейса и добавляется в новый Delphi-модуль. Размер этого модуля достаточ- но большой, поэтому я представлю только его ключевые элементы. Самой важной частью является объявление нового интерфейса: type IFirstServer = interface!IDispatch) ['{39855B42-8EFE-11D0-98D0-444553540000} ] procedure ChangeColor safecall function Get_Value Integer safecall procedure Set_Value(Value Integer) safecall property Value Integer read Get_Value write Set_Value end Далее следует dispinterface, который связывает каждый элемент интерфейса IfirstServer с числом: type IFirstServerDisp = dispinterface ['{89855842-8EFE-11D0-98D0-444553540000}'] procedure ChangeColor dispid 1 property Value Integer dispid 2 end Заключительная часть файла содержит класс-«создатель» (creator class), кото- рый используется для создания объекта на сервере (и по этой причине, использу- емого на клиентской стороне приложения, а не на серверной): type CoFirstServer = class class function Create IfirstServer class function CreateRemote(const MachineName string) IfirstServer end Все объявления этого файла (ряд из которых я опустил) могут рассматривать- ся как внутренняя, скрытая поддержка реализации. Для того чтобы создавать прак- тически любые приложения автоматизации, нет необходимости разбираться в них досконально И, наконец, Delphi генерирует файл, содержащий реализацию объекта автома- тизации. Этот модуль добавляется в приложение и является тем модулем, с кото- рым вы будете работать до завершения программы. Модуль объявляет класс сер- йерного объекта, который должен реализовать только что определенный интерфейс: type TFirstServer = class(TAutoObject IFirstServer) protected function Get_Value Integer safecall procedure ChangeColor. safecall procedure Set__Value(Value Integer) safecall end 0533
534 Глава 12. От СОМ к СОМ+ Среда Delphi автоматически предоставляет скелет программного кода этих ме- тодов, поэтому вам надо лишь откомпилировать помещенные в них строки В дан- ном случае три метода обращаются к свойству и двум методам, которые я добавил в форму В общем случае добавлять программный код, относящийся к пользова- тельскому интерфейсу, входящему в класс серверного объекта, не приходится Я сделал это только для того, чтобы обеспечить возможность изменения свойства Value и наблюдать визуальный эффект (вывод значения в строке редактирования) Вот как выглядит эта форма в ходе разработки 7» Type Library Demtr' Регистрация сервера автоматизации Модуль, содержащий серверный объект, имеет еще одно выражение, добавляемое средой Delphi в раздел инициализации initialization TAutoObjectFactory CreatetComServer TFirstServer Class_FirstServer ci MuitiInstance) end СОВЕТ-----------------------------------------------------------------— В данном случае я выбрал возможность наличия множества экземпляров. Различные варианты со- здания экземпляров см. раздел «Модели организации потоков и создания экземпляров» ранее в этой главе Это не сильно отличается от создания фабрик классов, рассмотренных в начале главы Для регистрации всех COM-объектов в ходе загрузки серверного СОМ- приложения модуль ComServer перехватывает системную функцию InitProc Выпол- нение этого программного кода запускается вызовом Application.Initialize, который по умолчанию добавляется средой Delphi в исходный программный код файла проекта любой программы Сведения о сервере можно добавить в реестр Windows просто запуском этого приложения на целевой машине (компьютере, на котором вы хотите установить сервер автоматизации) либо запуском этого приложения из командной строки с указанием параметра /regserver Это можно сделать с помощью команды Start ► Run (Пуск ► Выполнить), либо созданием в проводнике ярлыка, либо запуском про- граммы в Delphi после указания параметра командной строки (с помощью меню Run ► Parameters) Еще один параметр командной строки, /unregserver, предназна- чен для удаления данного сеовеоа из оеестоа 0534
Создание сервера автоматизации 535 Написание клиентской части А теперь, после разработки сервера, можно подготовить клиентскую программу Этот клиент сможет подключиться к серверу либо с помощью вариантных типов, либо с использованием новой библиотеки типов Второй подход может быть реа- лизован либо вручную, либо с использованием Delphi-техпологии создания ком- понентов-оболочек для серверов автоматизации Мы испробуем все варианты Создайте новое приложение (я назвал его TLibCli) и с помощью команды Project ► Import type library (Проект ► Импортировать библиотеку типов) меню IDE импор- тируйте библиотеку типов сервера Эга команда выведет диалоговое окно Import Type Library (рис 12 5), в верхней части которого перечислены зарегистрированные COM-серверы, имеющие библиотеки типов В этот список с помощью кнопки Add и указания соответствующего модуля могут быть добавлены и другие проекты В нижней части окна представлены некоторые подробности о выбранной библио- теке (например, список серверных объектов), а также о модуле импорта библиоте- ки типов, который будет создан при нажатии кнопки Create Unit (или Install) Import Type library *1 I mport Type ЬЫау | Tabular Data Control 1 1 Type Library (Version 1 1) TAPI3 Terminal Manager 1 0 Type Library (Version 1 0] TIFFilter 1 0 Type Library (Version 1 0) TIME (Version 1 0l 3 -TLibDem.o Library f/ersiori.l ..QI... tom (Version 1 0) TopStyle Lite Library (Version 1 0) TopStyle Lite Library (Version 2 0) \boi^$^Tid7cpde\1 АТСгьЬетйР^кЬгйгпа exe Add I fiemove I мм mi н »11 „и 1Д .mum-' чу «. • inift TFirstServer Palette page. ActiveX .........................-_____________________ Unit флата. |C \Progrem Frle$\Borland\Delphi7\lmports |$(0ЕЬРН|,)МаьГ$фСИЕЬРН1)\Веп',$(0ЕЬРН1]\ InslaS 11 J Cancel [ p fieneiite Component Wrapper Рис. 12.5. Диалоговое окно Type Library Import ВНИМАНИЕ ------------------------------------------------------—-------- Be добавляйте библиотеку типов в клиентское приложение, поскольку вы создаете контроллер автоматизации, а не сервер Delphi-проект контроллера не должен включать библиотеку типов сервера, к которому он подключается Имя модуля импорта библиотеки типов определяется средой Delphi на основе библиотеки типов, с добавлением в конце _TLB В нашем случае имя модуля будет TLibdemoLib_TLB Я уже говорил, что одним из элементов этого модуля, также гене- 0535
536 Глава 12. От СОМ к СОМ+ рируемым редактором библиотек типов, является класс создания (creation class). Я уже показывал вам интерфейс этого класса, а вот реализация первой из двух его функций: class function CoFirstServer.Create: IFirstServer. begin Result := CreateComObject(Class_FirstServer) as IFirstServer: end: Ее можно использовать для создания серверного объекта (и, возможно, для за- пуска серверного приложения) на том же компьютере. Как можно заметить, дан- ная функция является «ярлыком» для вызова CreateComObject, который позволяет создавать экземпляр COM-объекта, если вы знаете его GUID. В качестве альтер- нативы можно воспользоваться функцией CreateOleObject, которая в качестве пара- метра требует ProgID, являющийся зарегистрированным именем сервера. Между этими функциями создания имеется и еще одно отличие: CreateComObject возвра- щает объект типа IUnknown, a CreateOleObject — типа IDispatch. Давайте в этом примере используем сокращенную запись CoFirstServer.Create. При создании серверного объекта в качестве возвращаемого значения вы получи- те интерфейс IFirstServer. Его можно использовать либо непосредственно, либо со- хранить в переменной вариантного типа. Вот пример первого подхода: var MyServer- Variant: begin MyServer .= CoFirstServer.Create: MyServer. CiiangeCol or: Программный код, основанный на использовании вариантных типов, не силь- но отличается от первого контроллера, построенного в этой главе (тот, который использовал Microsoft Word). Вот альтернативный код, имеющий тот же эффект: var IMyServer IFirstServer: begin IMyServer := CoFirstServer.Create. IMyServer. CiiangeCol or. Вы уже видели, как можно использовать интерфейс и вариантные типы. А как на счет интерфейса координации? В этом случае можно объявить переменную типа интерфейса координации (dispatch interface): var DMyServer: IFirstServerDisp: После этого ее можно как обычно использовать для вызова методов, присвоив ей объект путем приведения объекта, полученного от класса-«создателя»: DMyServer = CoFirstServer Create as IFirstServerDisp. Интерфейсы, вариантные типы и интерфейсы координации: проверка скорости Как я уже говорил в разделе, представляющем библиотеки типов, одним из отли- чий этих подходов является скорость. Сложно оценить точную производительность каждой технологии, поскольку в ней участвует множество факторов. Чтобы уло- вить идею, в пример TLibCli я добавил простой тест. Программный код этого при- мера представляет собой цикл, который обращается к свойству Value сервера 100 раз- 0536
Создание сервера автоматизации 537 Результаты теста представляются в виде временных интервалов, получаемых вы- зовом API-функции GetTickCount перед и после выполнения цикла. (Кроме того, в Delphi можно использовать собственные временные функции, которые значи- тельно менее точны, либо использовать очень точные временные функции модуля поддержки мультимедийных средств, MMSystem.) С помощью этой программы вы можете приблизительно сравнить производи- тельность, достигаемую при вызове метода на основе интерфейса, вариантных ти- пов и на основе интерфейса координации. Анализируя результаты примера, мож- но отметить, что самыми быстрыми являются интерфейсы, а самыми медленными — вариантные типы. Границы действия объектов автоматизации Еще одним важным элементом, который необходимо учитывать, является граница действия (scope) объектов автоматизации. Вариантные и интерфейсные объекты используют технологии, основанные на подсчете ссылок, поэтому если перемен- ная, связанная с интерфейсным объектом, объявлена в методе локально, то по окон- чании выполнения метода данный объект будет уничтожен, а сервер может завер- шить выполнение (если все объекты, созданные этим сервером, уничтожены). Например, метод с таким программным кодом имеет минимальный полезный эф- фект: procedure ТС1ientFo rm.Ch a ngeCo1о г; var IMyServer: IFirstServer; begin IMyServer := CoFirstServer.Create; IMyServer.ChangeColor; end Если сервер активен, создается копия программы и изменяется цвет, но после этого сервер сразу же закрывается, поскольку объект интерфейсного типа выхо- дит за границы действия. Альтернативный подход я использовал в примере TLibCli, объявляя объект полем формы и создавая COM-объект при запуске, как в этой процедуре: procedure TClientForm FormCreate(Sender TObject). begin IMyServer CoFirstServer.Create, end; В данном примере при запуске программы сразу активизируется серверная программа. При завершении программы поле формы уничтожается и сервер за- крывается. Еще одним альтернативным вариантом является объявление объекта в форме, но создание его только при использовании, как в следующих двух фраг- ментах кода: 11 MyServerBi$: Variant. varType (MyServerBis) = varEmpty then MyServerBis - CoFirstServer.Create; MyServerBis ChangeColor. 11 ItfyServerBis: IFirstServer: not Assigned (IMyServerBis) then 0537
538 Глава 12. От СОМ к СОМ+ IMyServerBts = CoFirstServer Create. IMyServerBis ChangeColor. СОВЕТ------------------------------------------------------------------------------------ При создании тип variant инициализируется в качестве типа varEmpty. Если вместо этого присвоить вариантному типу значение null, то его тип станет varNull. Оба типа varEmpty и varNull представляют вариантные данные без назначенного значения, но при вычислении значений они ведут себя по- разному. VarNull всегда распространяется на выражение (делая его null-выражением), а значение типа varEmpty просто исчезает. Сервер в компоненте При создании клиентской программы для вашего сервера или любого другого серве- ра автоматизации можно использовать более удачный подход: создать для СОМ-сер- вера оболочку в виде Delphi-компонента. Если вы посмотрите на заключительную часть файла TlibdemoLib_TLB, то найдете объявление класса IFirstServer, унаследо- ванного от TOleServer. Это компонент, сгенерированный при импортировании биб- лиотеки, который система регистрирует в процедуре Register модуля. Если добавить этот модуль в пакет, то новый серверный компонент будет до- ступен в палитре компонентов Delphi (по умолчанию — на странице ActiveX). Гене- рация программного кода этого компонента управляется флажком, расположен- ным внизу диалогового окна Import Type Library (см. рис. 12.5). Я создал новый пакет, PackAuto, который можно найти в каталоге с таким же именем. В этот пакет с помощью страницы Directories/Conditionals диалогового окна Project Options (Параметры проекта) я добавил директиву LIVE_SERVER_AT_DESIGN_ TIME. Эта директива разрешает использование дополнительной функциональной возможности, которая недоступна по умолчанию: во время разработки серверный компонент будет иметь дополнительное свойство, которое в качестве подпунктов содержит список всех свойств сервера автоматизации: F' Ж ! Object inspector jFirstSetved 'I- Properties | Events| fAutoConnect j False CormectKind j ckBunrungOrNew Name ErrstServerl Ft e mg leM.achin Servet Value Tag iA# shown" IstSetverPtoperties] 5 ВНИМАНИЕ-----------------------------------------------------------------" Директива LIVE_SERVER_AT_DESIGN_TIME для большинства сложных серверов автоматизации (вклю- чая такие программы, как Word, Excel, PowerPoint и Visio) должна использоваться очень осторожно- Некоторые серверы перед тем, как вы сможете использовать некоторые из их свойств интерфейсов автоматизации, должны находиться в определенном режиме. Поскольку достижение этой особен- ности во время разработки для многих серверов является проблематичным, данная директива по умолчанию неактивна. 0538
Создание сервера автоматизации 539 Как можно увидеть в инспекторе объектов, компонент имеет ряд свойств. Свой- ство AutoConnect указывает, когда активизируется COM-сервер. Если его значение равно True, то серверный объект загружается, как только будет создан компонент- оболочка (как во время выполнения, так и во время разработки). Когда AutoConnect имеет значение False, сервер автоматизации загружается при первом же вызове одного из его методов. Еще одно свойство, ConnectKind, определяет, как осуществ- ляется подключение к серверу. Он может каждый раз начинать новый экземпляр (ckNewInstance), использовать выполняемый экземпляр (значение ckRunningln- stance, которое покажет сообщение об ошибке, если он еще не запущен) или вы- брать текущий экземпляр или запустить новый, если ни один экземпляр не досту- пен (ckRunningOrNew). И, наконец, можно запросить удаленный сервер с помощью ckRemote и присоединить этот сервер непосредственно в программном коде после ручного установления соединения с помощью ckAttachToInterface. СОВЕТ--------------------------------------------------------------------------------------- Для подключения к существующему объекту должна быть зарегистрирована таблица Running Object Table (ROT). Регистрация должна выполняться серверным вызовом API-функции RegisterActiveObject. Конечно же, в определенный момент может быть зарегистрирован только один экземпляр каждого СОМ-сервера. Типы данных СОМ СОМ-координация не поддерживает все доступные в Delphi типы данных. Это факт особенно важен в отношении автоматизации, поскольку клиент и сервер зачастую выполняются в различных адресных пространствах, и система должна перемещать (или выстраивать в определенном порядке (букв, перевод «marshal»)) данные от одной стороны к другой. Также учтите, что COM-интерфейсы должны быть дос- тупны программам, написанным на любом языке. К поддерживаемым СОМ типам данных относятся основные типы, такие как Integer, Smalllnt, Byte, Single, Double, WideString, Variant и WordBool (но не Boolean). Помимо основных типов для таких сложных элементов, как шрифты, списки строк и битовые изображения, с помощью интерфейсов IFontDisp, IStrings и IPictu- reDisp можно использовать COM-типы. В последующих разделах рассмотрены де- тали сервера, предоставляющего клиенту списки строк и шрифты. «Открытие» списков строк и шрифтов Пример ListServ является практической демонстрацией «открытия» с сервера ав- томатизации, написанного в Delphi, таких сложных типов, как список строк и шриф- ты. Я выбрал именно эти два типа, поскольку они оба поддерживаются Delphi. Интерфейс IFontDisp предоставляется ОС Windows и доступен с помощью мо- дуля ActiveX. Delphi-модуль AxCtrls расширяет поддержку этого интерфейса добав- Дением таких методов преобразования, как GetOleFont и SetOleFont. Delphi поддер- живает интерфейс IStrings в модуле StdVCL, а модуль AxCtrls предоставляет функции преобразования для этого типа (а также для третьего типа, который я сейчас ис- пользовать не буду, TPicture). 0539
540 Глава 12. От СОМ к СОМ+ ВНИМАНИЕ--------------------------------------------------------------------- Для запуска данного и подобных ему приложений на клиентском компьютере необходимо устано- вить и зарегистрировать библиотеку StdVCL. На вашем компьютере она уже зарегистрирована в ходе установки Delphi. Методы Set и Get свойств сложных типов копируют информацию из СОМ-ин- терфейсов в локальные данные, а оттуда — в форму, и наоборот. Например, два метода строк выполняют эти операции посредством вызова Delphi-функций GetOleStrings и SetOleStrings. Клиентское приложение, используемое для демонст- рации этих возможностей, называется ListCli. Две программы являются довольно сложными, но я решил оставить их программный код для самостоятельного изуче- ния без приведения здесь всех подробностей, поскольку эта расширенная возмож- ность редко используется Delphi-программистами. Использование программ пакета Office Вы построили как клиентскую, так и серверную часть соединения автоматизации. Если целью является обеспечение взаимодействия построенных приложений, то это — полезная методика, хотя и не единственная. Использование файлов, отобра- жаемых на память, уже рассматривалось в главе 10. (Другой методикой, не пред- ставленной в этой редакции книги, является использование сообщения wm_ CopyData.) Реальная ценность автоматизации заключается в том, что она является стандартной и может использоваться для интеграции Delphi-программ с другими приложениями. Типичным примером является интеграция программы с пакетом Office, например, с Microsoft Word и Microsoft Excel, или даже с автономными при- ложениями, такими как AutoCAD. Интеграция с этими приложениями обеспечивает двойное преимущество: О можно предоставить пользователям возможность работать в той среде, кото- рую они знают, например, генерировать отчеты и служебные записки на основе баз данных в формате, с которым им проще работать; О можно избежать необходимости реализации сложных функций «с нуля», на- пример, написания собственного текстового процессора. Вместо повторного использования компонентов можно просто повторно использовать сложные приложения. Такой подход имеет и несколько недостатков, о которых необходимо упомя- нуть: О пользователь должен иметь приложение, с которым вы хотите интегрировать собственную программу; кроме того, для реализации всех возможностей, ис- пользуемых в вашей программе, это должна быть последняя версия; О вам придется изучить новую архитектуру программирования, как правило, с минимальным количеством имеющейся документации. И хотя вы по-прежне- му используете язык Delphi, создаваемый программный код зависит от типов данных, предоставляемых сервером, а также от конкретной коллекции взаимо- связанных классов, которые довольно сложны для понимания; О в конечном счете вы можете получить программу, которая работает только с определенной версией серверного приложения, особенно если вы попытаетесь 0540
Использование составных документов 541 оптимизировать вызовы использованием интерфейсов, а не вариантных типов данных. В частности, компания Microsoft и не пыталась обеспечить совмести- мость сценариев в основных выпусках Word и других приложений Office. Delphi упрощает использование приложений пакета Office за счет предвари- тельной установки ряда готовых к использованию компонентов, которые служат оболочками для интерфейсов автоматизации этих серверов. Данные компоненты можно найти на странице Servers палитры компонентов. Они устанавливаются по- средством методики, рассмотренной в предыдущем разделе. Реальный плюс осно- ван на создании компонентов, становящихся оболочками существующих серверов автоматизации, а не в доступности предварительно определенных серверных ком- понентов. Обратите также внимание на то, что Office-компоненты представлены в различных версиях в зависимости от версии установленного у вас пакета: устанав- ливаются все компоненты, но в ходе разработки зарегистрирован только один на- бор, указанный при установке Delphi. Эту настройку можно изменить путем уда- ления связанного пакета компонентов и добавлением нового. В данном разделе вы не увидите действительного примера, поскольку очень сложно написать программу, работающую с различными версиями Microsoft Office. Подобный программный код и подсказки можно найти в книге Essential Delphi (см. Приложение С). Использование составных документов Составной документ — это используемое компанией Microsoft название техноло- гии, допускающей редактирование «на месте» документа, входящего в другой доку- мент (например, битового изображения внутри Word-документа). Э га технология произошла от понятия OLE, но в настоящее время ее роль значительно ограниче- на по сравнению с тем, что предполагала компания Microsoft, когда представляла ее в начале 1990-х. Составные документы имеют две функциональные особенно- сти: связывание объектов и внедрение (отсюда и произошло само понятие OLE): О понятие внедрение объекта (embedding) по отношению к составному докумен- ту соответствует «умной» версии операции копирования и вставки, выполняе- мых с помощью буфера обмена. Ключевое отличие заключается в том, что при копировании OLE-объекта из приложения-сервера и вставки его в приложе- ние-контейнер, вы копируете как данные, так и некоторые сведения о данном сервере (его GUID). Это позволяет для редактирования данных активизиро- вать приложение-сервер прямо из контейнера; О понятие связывание объектов (linking) по отношению к составному документу соответствует копированию только ссылки на данные и сведения о сервере. Связанный объект обычно активизируется с помощью буфера обмена и выпол- нения операции Paste Link (вставка ссылки). При редактировании данных в приложении-контейнере происходит изменение оригинальных данных, кото- рые сохраняются в отдельном файле. Ввиду того, что программа-сервер ссылается на весь файл (только часть кото- рого может быть связана с клиентским документом), сервер будет активизирован 0541
542 Глава 12. От СОМ к СОМ+ в отдельном окне и оперировать со всем исходным файлом, а не только со скопи- рованными данными. Однако при использовании внедренного объекта приложе- ние-контейнер может поддержать визуальное (или «на месте») редактирование, что означает возможность модификации объекта с контекстом внутри основного окна контейнера. Окна приложения-сервера и приложения-контейнера, их меню и их панели инструментов автоматически сливаются, позволяя пользователю ра- ботать в одном окне с различными типами объектов, а, следовательно, с несколь- кими различными OLE-серверами, без выхода из окна приложения-контейнера. Еще одно ключевое отличие между внедрением и связыванием заключается в том, что данные встроенного объекта хранятся и обслуживаются приложением- контейнером. Контейнер сохраняет встроенный объект в собственных файлах. В от- личие от него, связанный объект физически находится в отдельном файле, кото- рый обрабатывается исключительно сервером, даже если ссылка относится только к малой части файла. В обоих случаях приложение-контейнер не обязано знать, как обрабатывать объект и его данные (либо даже представлять его) без поддержки сервера. Контейнеры составных документов могут поддерживать СОМ в различной сте- пени. Можно поместить объект в контейнер за счет вставки нового объекта путем выполнения операции вставки, либо вставкой ссылки объекта из буфера обмена, перетаскиванием объекта из другого приложения и т. п. После того как объект по- мещен в контейнер, с ним можно выполнять различные операции, используя дос- тупные на сервере команды (verb) или действия. Обычно команда Edit является действием по умолчанию, т. е. действием, выполняемым при двойном щелчке на объекте. Для других объектов, таких как видео или звуковые клипы, действием по умолчанию является команда Play. Обычно имеется возможность просмотра списка действий, поддерживаемых текущим объектом (правый щелчок на нем). Та же ин- формация доступна во многих программах с помощью меню Edit ► Object, которое от- крывает подменю, в котором перечислены доступные команды текущего объекта. Контейнерный компонент Для создания в Delphi COM-приложения поместите компонент OleContainer на форму, а затем выберите его правой кнопкой для активизации контекстного меню, содержащего команду Insert Object (Вставить объект). При выборе этой команды Delphi выводит стандартное диалоговое окно OLE Insert Object. Это окно позволяет выбрать одно из зарегистрированных в компьютере приложений-серверов. После того как объект вставлен в контейнер, в контекстном меню управления контейнером появятся дополнительные пункты. Они представляют команды из- менения свойств COM-объекта, вставки другого объекта, копирования и удале- ния существующего объекта. В этот список также будут входить команды (дей- ствия) объекта (такие, как Edit, Open или Play). После помещения СОМ-объекта в контейнер будет запушен соответствующий сервер, что позволит вам редактиро- вать новый объект. Как только вы закроете приложение-сервер, Delphi обновит объект в контейнере и выведет его в ходе выполнения на форме разработанного Delphi-приложения. Если просмотреть текстовой вариант формы, содержащей компонент, в кото- рый помещен объект, вы заметите свойство Data, которое содержит данные СОМ- 0542
Использование составных документов 543 объекта. Хотя клиентская программа и хранит данные объекта, она не знает, как обрабатывать и выводить эти данные без помощи соответствующего сервера (ко- торый должен быть доступен на том компьютере, на котором выполняется про- грамма). Это означает, что COM-объект внедрен. Для полной поддержки составных документов программа должна предостав- лять меню и панель инструментов или панель. Эти дополнительные компоненты очень важны, поскольку редактирование «на месте» предполагает слияние пользо- вательского интерфейса клиента и пользовательского интерфейса программы-сер- вера. Когда COM-объект активизируется «на месте», в меню приложения-контей- нера добавляются некоторые раскрывающиеся меню приложения-сервера. Слияние меню обрабатывается в Delphi автоматически. Вам необходимо лишь установить соответствующие индексы пунктов меню контейнера (с помощью свой- ства Grouplndex). Любые пункты меню с нечетным номером индекса заменяются соответствующим элементом активного OLE-объекта. Конкретно, раскрывающи- еся меню File (0) и Window (4) принадлежат приложению-контейнеру Edit (1), View (3) и Help (5) (и группы раскрывающихся меню с этими индексами) — будут заби- раться COM-сервером. Когда COM-объект активен, шестая группа, Object (2), мо- жет использоваться контейнером для вывода прочих раскрывающихся меню групп Edit и View. Программа-пример, которую я написал для демонстрации этих возмож- ностей, позволяет пользователю создавать новый объект вызовом метода InsertOb- jectDialog класса TOleContainer. После того как создан новый объект, его первичную команду можно выпол- нить с помощью метода DoVerb. Программа также выводит небольшую панель ин- струментов, на которой расположены кнопки с изображениями. Я поместил на форму ряд компонентов TWinControl для того, чтобы пользователь мог выделить их и тем самым отключить OleContainer. Для того чтобы сохранить панель инстру- ментов видимой, пока происходит редактирование, необходимо установить свой- ство Locked в True. Эта настройка заставит панель остаться в приложении и не по- зволит заместить ее панелью управления сервера. Для того чтобы показать, что произойдет, если будет использоваться иной под- ход, я добавил в программу вторую панель с рядом кнопок. Поскольку я не уста- навливал их свойство Locked, эта новая панель будет замещаться панелью актив- ного сервера. Когда редактирование «на месте» запускает приложение-сервер, выводящее панель инструментов, оно будет замещать панель инструментов кон- тейнера (см. нижнюю часть рис. 12.6). ВНИМАНИЕ --------------------------------------------------------------- Для сглаженного выполнения всех операций автоматического изменения размера необходимо по- местить компонент OLE Container в компонент Panel и выровнять их обоих по клиентской области Формы. В качестве альтернативы можно создать COM-объект с помощью метода Paste- SpeciaIDialog, вызываемого в обработчике события PasteSpeciallClick моего примера. Другое стандартное диалоговое окно СОМ, являющееся оболочкой Delphi-функ- Чии, показывает свойства объекта; это диалоговое окно активизируется пунктом Object Properties раскрывающегося меню Edit посредством вызова метода Object- PropertiesDialog компонента OleContainer. 0543
544 Глава 12. От СОМ к СОМ+ Рис. 12.6. Вторая панель инструментов примера OleCont (выше) заменена панелью инструментов сервера (ниже) Использование внутреннего объекта В предыдущей программе пользователь определял тип создаваемого программой внутреннего объекта. В этом случае для взаимодействия с внутренним объектом нужно сделать совсем немного. Вместо этого предположим, что необходимо вне- дрить документ Word в Delphi-приложение и затем с помощью программного кода изменить его. Это можно сделать посредством OLE-автоматизации (Automation) (см. пример WordCont). ВНИМАНИЕ----------------------------------------------------- Поскольку пример WordCont включает объект определенного типа (документ Microsoft Word), он не будет работать, если у вас не установлено это приложение. Наличие различных версий также мо- жет привести к различным проблемам (я проверял работу примеров данной главы только с пакетом Office 97). Для других версий, возможно, придется перестроить программу, выполнив ту же после- довательность действий, что и я. В форму примера я добавил компонент OleContainer, установив его свойство AutoActivate в aaManual (что обеспечивает единственный способ взаимодействия — с помощью программного кода) и добавил панель инструментов с парой кнопок. Программный код не требует пояснений, поскольку вы знаете, что внедренный объект представляет собой документ Word. Вот этот пример (результат его рабо- ты представлен на рис. 12.7): procedure TForml Button3Click(Sender- TObject): var Document. Paragraph- Variant. begin // активизировать. если еще не запущен if not (OleContainerl.State = osRunning) then 0544
Введение в элементы управления ActiveX 545 OleContainerl.Run, // получить документ Document .= OleContainerl.OleObject: // добавить параграфы, выбрать последний Document Paragraphs Add: Paragraph = Document Paragraphs Add. // добавить в параграф текст, используя произвольный размер шрифта Paragraph Range Font Size = 10 + Random (20); paragraph Range Text := 'New text (' + IntToStr (Paragraph Range.Font Size) + 'I'#13; end; New text (27) New text (I? t New text (21) New text (29) Рис. 12.7. Пример WordCont демонстрирует использование OLE-автоматизации во встроенном объекте Введение в элементы управления ActiveX Язык Visual Basic компании Microsoft оказался первой средой разработки программ, в которой была введена идея предоставления программных компонентов на всеоб- щий рынок, несмотря на то, что концепция повторного использования программ- ных компонентов уже более стара, чем сам Visual Basic. Она удачно вписывается в теорию объектно-ориентированного программирования (ООП). Первым техни- ческим стандартом, предлагаемым в Visual Basic, был VBX, 16-разрядная специ- фикация, полностью поддержанная Delphi 1. В ходе перехода на 32-разрядную платформу компания Microsoft заменила VBX-стандарт более мощным и более открытым, назвав его «элементы управления ActiveX». СОВЕТ------------------------------------------------------------------------- Элемент управления ActiveX используется для вызова OLE-элементами управления (или OCX). Из- менение имени отражает новую маркетинговую стратегию Microsoft, а не техническую новизну. Неудивительно, что элементы управления ActiveX обычно сохраняются в файлах с расширением •осх. С точки зрения общей перспективы элемент управления Active X не слиш- ком сильно отличаются от элементов управления Windows. Ключевое отличие 0545
546 Глава 12. От СОМ к СОМ+ заключается в интерфейсе элемента управления, т. е. во взаимоотношении между элементом и остальным приложением. Обычные элементы Windows используют интерфейс, основанный на сообщениях; элементы OLE-автоматизации и ActiveX используют свойства, методы и события (как и собственные компоненты Delphi). Используя COM-жаргон, элементы управления ActiveX можно назвать «объек- том „составной документ”», реализованным как внутрипроцессный DLL-сервер, поддерживающий OLE-автоматизацию, визуальное редактирование и активиза- цию „изнутри наружу1”». Совершенно ясно, не правда ли? Давайте подробней рас- смотрим, что означает это отличие. COM-серверы могут быть реализованы тремя способами: О как автономное приложение (например, Microsoft Excel); О как внепроцессный сервер, т. е. исполняемый файл, который не может запус- каться сам по себе, а лишь может быть вызван сервером (например, программа Microsoft Graph и подобные приложения); О как внутрипроцессные серверы, такие как DLL, загружаемые в то же простран- ство памяти, что и использующая их программа. Элементы управления ActiveX могут быть реализованы только в третьем вари- анте, который является самым быстрым; в качестве внутрипроцессного сервера. Более того, элементы управления ActiveX являются серверами OLE-автоматиза- ции. Это означает, что вы можете обращаться к свойствам этих объектов и вызы- вать их методы. Можно увидеть эти элементы в приложении, используя их и вза- имодействуя с ними непосредственно в окне приложения-контейнера. В этом и заключается суть понятия визуальное редактирование или «активизация на мес- те». Элемент управления активизирует один щелчок, а не двойной щелчок, как в OLE-документах, и этот элемент активизируется вне зависимости от его види- мости (вот что означает понятие «активизация „изнутри наружу”») без необходи- мости выполнения двойного щелчка. В элементе управления Active X свойства могут идентифицировать состояния, но они также могут активизировать методы. Свойства могут обращаться к агреги- рованным значения, массивам, подобъектам и т. п. Свойства также могут быть ди- намическими (или «только для чтения», в терминах Delphi). Свойства элементов управления ActiveX можно разделить на следующие группы: основные свойства (stock properties), которые должны быть реализованы в большинстве компонен- тов; свойства окружения (ambient properties), предоставляющие сведения о кон- тейнере (подобно свойствам ParentColor и ParentFont в Delphi); расширенные свой- ства (extended properties), управляемые контейнером, например, положение объекта; заказные свойства (custom properties), которые могут быть совершенно любыми. События и методы точно так же остаются и событиями и методами. Событие связано с щелчком мыши, нажатием клавиши, активизацией компонента или ДРУ' гим определенным действием пользователя. Метод — это функция или процедУ' ра, связанные с данным элементом управления. Между концепциями событий и ме- тодов в элементах управления ActiveX и Delphi значительных отличий нет. 1 Способ активизации внедренного объекта непосредственно нажатием кнопки мыши, при котороМ одновременно активизируется и содержащий его объект. 0546
Введение в элементы управления ActiveX 547 Элементы управления ActiveX и Delphi-компоненты Перед тем как перейти к использованию и написанию элементов управления ActiveX в Delphi давайте рассмотрим некоторые отличия между двумя видами элементов управления. Элементы управления ActiveX основаны на DLL: при их использова- нии необходимо распространять их код (OCX-файл) совместно с использующим их приложением. В среде Delphi код компонентов может быть статически скомпо- нован в исполняемый файл или динамически присоединяться к нему с помощью пакета времени выполнения, поэтому всегда существует выбор: устанавливать один большой файл или множество небольших модулей. Наличие отдельных файлов позволяет совместно использовать находящийся в них код различными приложениями, что обычно и происходит с DLL. Если два приложения используют один и тот же элемент управления (или пакет времени выполнения), на жестком диске необходимо иметь лишь одну его копию или одну копию в оперативной памяти. Однако недостатком в данном случае является то, что если двум программам необходимо использовать две различные версии (или компоновки) элемента управления ActiveX, то могут возникнуть некоторые про- блемы совместимости. Достоинство самодостаточного исполняемого файла заклю- чается в том, что установка практически не вызывает никаких проблем. Недостаток Delphi-компонентов не в том, что этих компонентов меньше, чем элементов управления ActiveX, а в том, что при приобретении Delphi-компонента использовать его можно в среде Delphi или в C++Builder компании Borland. Если вы покупаете элемент управления ActiveX, то его можно использовать во множе- стве сред разработок от различных производителей. Но если вы в основном рабо- таете в Delphi и находите два подобных компонента, основанных на двух разных технологиях, я советую вам приобрести Delphi-компонент, поскольку он лучше подходит для этой среды, а, следовательно, проще в использовании. Кроме того, «родные» Delphi-компоненты, вероятно, будут иметь более подробное описание (с точки зрения Delphi), и вы сможете реализовать преимущества Delphi и ее язы- ковых особенностей, недоступных в общем интерфейсе ActiveX, который тради- ционно основан на С или C++. СОВЕТ-------------------------------------------------------------- В мире .NET эта ситуация изменяется полностью. Вы сможете не только использовать любой сис- темный компонент наиболее удобным образом, но и создавать Delphi-компоненты, доступные для Других языков программирования и средств .NET. Использование элементов управления ActiveX в Delphi Delphi уже имеет ряд предустановленных элементов управления ActiveX. После небольшого пояснения общей работы ActiveX я представлю один пример. Процесс установки в Delphi очень прост: Выберите Component ► Import ActiveX Control в меню Delphi для открытия диало- гового окна Import ActiveX, в котором можно увидеть список ActiveX-библио- Тек, зарегистрированных в операционной системе Windows. 0547
548 Глава 12. От СОМ к СОМ+ 2. Выберите один из них, и Delphi прочитает тип этой библиотеки, выведет спи- сок элементов управления и предположительно назовет имя файла-модуля. 3. Если сведения верны, щелкните на кнопке Create Unit для просмотра файла ис- ходного кода языка Delphi, созданного IDE в качестве оболочки для данного элемента управления ActiveX. 4. Щелкните на кнопке Install для того, чтобы добавить новый модуль в пакет Delphi и в палитру компонентов. Использование элемента WebBrowser Для построения примера я использовал уже установленный в среде Delphi эле- мент ActiveX. В отличие от элементов сторонних производителей, он представлен не на странице ActiveX в палитре компонентов, а на странице Internet. Элемент WebBrowser является оболочкой для ядра Internet Explorer компании Microsoft. Пример WebDemo представляет собой очень ограниченный веб-браузер; он состоит из элемента управления ActiveX, занимающего всю клиентскую область, сверху — панель управления и снизу — строку состояния. Для перехода к определенной веб- странице пользователь может ввести URL в комбинированном списке, выбрать один из посещенных ранее URL (сохраняемых в этом списке) или щелкнуть на кнопке Open File для выбора локального файла (рис. 12.8). : , ао«1Н» Deinta & AppSefwr €♦♦ CuRBA JL InterBase JO.it <i$t сне Login WtfW ЬСГ1Э’Щ СОГГ Linux Team^ource DSP | Login Seal ch S^op Ceve oper SucpoC pjpi^hFO Downleads Book* Cha* Code Centra QiHh у Cen ra Happy Holidays from Borland Hi i m David Intersimone "David I" Vice President of Developer Relations and the developers Santa Claus Welcome to the Borland Developer Network (BDN) From ail of us at Borland Software Corporation to an members of BDN Happy Holidays (Christmas Eid AL Fitter Hanukkah Kwanzaa Santa Lucia Day St Nicholas Day Boxing Day Winter Solstice Festivus ) We also send along a hope for peace on earth and good win toward ail men women and children ' Jsid^cor nd tom * Soapbox David intersimone 14 Dec tf*& Scit-rf John «aster Harxers Cow» Anders Ohisson DotNet Buzz Words Explained - by Alain “Lino ' Tadros This article wilt explain the different buzz words in the DotNet world like CTS CLS VES Managed Code Managed Data Unmanaged Code Assemblies Metadata Manifest Strong Рис. 12.8. Программа WebDemo после выбора страницы хорошо известной Delphi-разработчикам Реализация выбора локального HTML-файла осуществляется программный кодом метода GotoPage: procedure TForml GotoPage(Reqllrl string). begin 0548
Создание элементов управления ActiveX 549 WebBrowserl Navigate (Reqllrl. EmptyParam EmptyParam EmptyParam EmptyParam) end; EmptyParam — это предопределенный тип OleVariant, который может использо- ваться в качестве параметра-ссылки для передачи значения по умолчанию. Его удобно использовать для того, чтобы избежать создания пустой переменной типа OleVariant каждый раз, когда возникает необходимость в подобном параметре. Программа вызывает метод Goto Раде, когда пользователь нажимает на кнопку Орел File или когда он нажимает клавишу Enter, находясь в строке комбинированного списка или при щелчке на кнопке Go (что можно увидеть из исходного программ- ного кода). Программа также обрабатывает четыре события элемента управления WebBrowser. При завершении операции загрузки программа обновляет текст в строке состояния, а также раскрывающийся список элемента combo box: procedure TForml WebBrowserlDownloadComplete(Sender TObject) var Newllrl string begin StatusBarl PanelslO] Text = 'Done'. II добавить URL в элемент combo box Newllrl = WebBrowserl LocationllRL if (Newllrl <> ' ) and (CombollRL Items IndexOf (Newllrl) < 0) then ComboURL Items Add (Newllrl) end; Другими полезными событиями являются OnTitleChange, используемые для об- новления заголовка наименованием HTML-документа, а также событие OnStatus- TextChange, используемое для обновления второй части строки состояния. Этот код обычно дублирует сведения, выводимые в первой части строки состояния преды- дущими двумя обработчиками событий. Создание элементов управления ActiveX Помимо использования существующих в Delphi элементов управления ActiveX можно легко разработать новые, используя одну из двух методик: О можно использовать мастер ActiveX Control Wizard, с помощью которого пре- образовать VCL-элемент в элемент ActiveX. Операция начинается с VCL-ком- понента, который должен происходить от TWinControl (и не должен иметь не- подходящих свойств; в этом случае элемент исключается из комбинированного списка мастера), после чего Delphi создает вокруг него оболочку ActiveX. В ходе этого этапа Delphi добавляет в элемент библиотеку типов. (Создание ActiveX оболочки вокруг Delphi-компонента противоположно действию, выполняемо- му при использовании элемента управления ActiveX в Delphi) ° можно создать ActiveForm, поместить на нее несколько элементов управления и использовать всю форму (без границ) в качестве элемента управления ActiveX Эта методика была введена для построения Интернет-приложений, но она так- же является хорошей альтернативой для создания элементов управления ActiveX, основанных на нескольких элементах Delphi или на Delphi-компонентах, про- исходящих не от TWinControl. 0549
550 Глава 12. От СОМ к CONI+ В любом случае для элемента управления можно дополнительно подготовить страницу свойств для использования ее в виде редактора свойств, позволяющего устанавливать начальные значения свойств элемента в любой среде разработки — альтернатива инспектору объектов Delphi. Поскольку большинство сред разработки обеспечивают лишь ограниченные возможности редактирования, наличие страни- цы свойств является очень важным. Построение элемента управления ActiveX Arrow В качестве примера разработки элемента управления ActiveX я решил воспользо- ваться компонентом Arrow, рассмотренным в главе 9, и включить его в элемент ActiveX. Невозможно использовать компонент непосредственно, поскольку он является графическим элементом управления (подклассом TGraphicControl). Одна- ко включение графического элемента в элемент управления, основанный на ис- пользовании окон, обычно является вполне простой операцией. В этом случае необходимо изменить имя базового класса на TCustomControl (и со- ответственно изменить класс самого элемента управления на TmdWArrow, чтобы избежать столкновения имен) (см. исходный программный код в папке XArrow). После установки этого компонента в Delphi можно приступать к разработке само- го примера. Для создания новой библиотеки ActiveX выберите File ► New ► Other, перейдите на страницу ActiveX, а затем выберите ActiveX Library. Среда Delphi со- здаст «голый скелет» DLL (см. пример в начале главы). Я сохранил эту библиоте- ку как XArrow в одноименном каталоге (как обычно). А теперь пришло время воспользоваться мастером ActiveX Control Wizard, до- ступным на странице ActiveX хранилища Object Repository, открываемого с помощью диалогового окна New: В этом мастере выбирается интересующий класс, настраиваются имена, пред' ставленные в строках редактирования, после чего выполняется щелчок на кнопке ОК. После этого Delphi автоматически создает необходимый программный код эле- мента управления ActiveX. Использование трех флажков, расположенных в нижней части окна ActiveX Control Wizard, не столь очевидно. Если вы хотите сделать этот элемент управления 0550
Создание элементов управления ActiveX 551 лицензируемым, то Delphi включит в код ключ лицензии и предоставит такой же GUID в отдельном файле с расширением LIC. Этот файл необходим для исполь- зования элементом управления в среде разработки без соответствующего лицензи- онного ключа или для использования его на веб-странице. Второй флажок позво- ляет включать в OCX-файл сведения о версии элемента управления ActiveX. Если установлен третий флажок, то мастер ActiveX Control Wizard автоматически до- бавит в создаваемый элемент управления окно About box. Посмотрите на сгенерированный мастером программный код. Ключевым эле- ментом этого мастера является генерация библиотеки типов и, конечно же, соот- ветствующего модуля импорта библиотеки типов с определением интерфейса (dispinterface) и прочих типов и констант. В данном примере файл импорта назван XArrow_TLB.PAS: я предлагаю вам изучить его самостоятельно, для того чтобы по- нять, как Delphi определяет элемент управления ActiveX. Модель содержит GUID элемента, константы для определения значений, соответствующих перечисляемых типов СОМ, используемых свойствами элемента управления Delphi (например, TxMdWArrowDir), и объявление интерфейса IMdWArrowX. В заключительной части модуля импорта находится объявление класса TMdWArrowX. Это класс-потомок клас- са TOLeControL, который может использоваться для установки этого элемента в Delphi (см. в первой части этой главы). Этот класс не нужен для создания элемента уп- равления ActiveX; он необходим лишь для установки данного элемента ActiveX в Delphi. Оставшийся программный код и настраиваемый вами код — это основной мо- дуль, который в примере XArrow назван MdWArrowImpll. Тот модуль содержит объяв- ление сервера элемента управления ActiveX, TMdWArrowX, который наследуется от TActiveXControl и реализует интерфейс IMdWArrowX Перед настройкой этого элемента необходимо разобраться, как он работает. Откомпилируйте ActiveX-библиотеку, а затем зарегистрируйте ее с помощью команды меню Run ► Register ActiveX Server. Теперь можно установить элемент уп- равления ActiveX, как вы недавно делали, за исключением того, что во избежание конфликта имен необходимо было указать другое имя нового класса. При исполь- зовании этого элемента управления он практически не отличается от оригиналь- ного VCL-элемента, но теперь этот компонент может быть установлен в других средах разработки. Добавление новых свойств После того как мы создали элемент управления ActiveX, добавление в него новых свойств, событий или методов (что удивительно) проще, чем те же операции в от- ношении VCL-компонента. Delphi обеспечивает особую визуальную поддержку добавления свойств, методов и событий в элемент управления ActiveX, а не в VCL- злементы. Можно открыть Delphi-модуль с реализацией элемента управления ActiveX и выбрать Edit ► Add То Interface. Альтернативным вариантом является от- крытие этого же диалогового окна с помощью аналогичной команды контекстного Меню редактора Delphi открывает окно Add То Interface (см рис. на следующей стра- нице). В комбинированном списке можно выбирать: новое свойство, метод или собы- тие. В строке редактирования после этого можно ввести объявление нового 0551
552 Глава 12. От СОМ к СОМ+ элемента интерфейса. Если установлен флажок Syntax Helper, то будут появляться подсказки, объясняющие, что необходимо ввести далее, а также отмечаться любые ошибки. В ходе определения нового интерфейсного элемента ActiveX не забывай- те о существующих ограничениях типов данных СОМ. Add То interface Interlace' |Properties/Methods IMdWArrowK J geolsreliaii; IL.... ...... ,. J procedure (unction or property expected! . Ip Syntax Heiser | 0Г- ~| Cancel В примере XArro w я добавил два новых свойства. Поскольку свойства Реп и Brush оригинальных Delphi-компонентов недоступны, я сделал доступным их цвет. Вот примеры того, что необходимо написать в строке редактирования диалогового окна Add То Interface (выполняя дважды): property FtllColor Integer. property PenColor Integer. СОВЕТ----------------------------------------------------------------------------- Поскольку TColor является специфичным Delphi-определением, его использование недопустимо. TColor является поддиапазоном целых значений, который по умолчанию ограничен значением типа integer, поэтому я использовал стандартный тип integer непосредственно. Объявление, которое введено в окне Add То Interface, автоматически добавляет- ся в файл библиотеки типов (TBL), в модуль импорта этой библиотеки и в модуль реализации. Все, что необходимо сделать при завершении, — это заполнить мето- ды Get и Set реализации. Если сейчас установить этот элемент управления ActiveX в Delphi еще раз, то появятся два новых свойства. Единственная проблема, касаю- щаяся этих свойств, заключается в том, что Delphi использует обычный редактор целых значений, что усложняет ввод нового значения цвета вручную. В отличие от этого, программа для создания соответствующего цветового значения может просто использовать функцию RGB. Добавление страницы свойств Как можно предположить, другие среды разработки с вашим компонентом могут выполнить лишь небольшие изменения, поскольку вы не подготовили страницу свойств, то и нет редактора свойств. Страница свойств является основой, позволя- ющей программистам, использующим данный элемент управления, редактировать его атрибуты. Однако добавление страницы свойств значительно сложней, чем добавление элементов в форму. Страница свойств будет интегрироваться с содер- жащей ее средой. Эта страница будет представлена в диалоговом окне свойств ос- новной среды, предоставляющем кнопки OK, Cancel и Apply, а также вкладки других страниц свойств (некоторые из которых будут предоставлены основной средой)- Благодаря тому что поддержка страниц свойств встроена в среду Delphi, созда- ние последних занимает мало времени. Вы открываете проект ActiveX, затем — Ди' алоговое окно New Items, переходите на страницу ActiveX и выбираете Property Page- 0552
Создание элементов управления ActiveX 553 То, что вы получите, практически не отличается от форм — класс TPropertyPagel (создаваемый по умолчанию) наследуется от VCL-класса TPropertyPage, который в свою очередь наследуется от TCustomForm. ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Delphi предоставляет четыре встроенные страницы свойств для цветов, шрифтов, изображений и строчных значений. GUID-идентификаторы этих классов указываются в модуле AxCtrls константами Class_DColorPropPage, Class_DFontPropPage, Class_DpicturePropPage и Class_DStnngPropPage. В страницу свойств, как в обычную форму Delphi, можно добавить элементы управления и написать программный код, позволяющий им взаимодействовать. В примере XArrow в страницу свойств добавлен комбинированный список с допус- тимыми значениями свойства Direction, флажок для свойства Filled и строка редак- тирования с элементом управления UpDown для установки свойства ArrowHeight, а также две области с соответствующими кнопками выбора цветов (рис. 12.9). Vwow properties Direction | adRight (3) Г Flltaf Arrow Height l|ri Рис. 12.9. ActiveX-элемент примера XArrow и его страница свойств в среде Delphi Программный код необходимо добавить лишь для двух кнопок, используемых ДЛЯ изменения цветов в двух областях, представляющих цвет элемента управле- ния ActiveX. Обработчик события OnClick использует компонент ColorDialog обыч- ным способом: procedure TPropertyPagel ButtonPenClick(Sender TObject). begin with ColorDialogl do begin Color = ShapePen Brush Color. if Execute then begin ShapePen Brush Color = Color. Modified. // "разрешить" кнопку Apply ! end. end. end; 0553
554 Глава 12. От СОМ к СОМ+ Важно, что этот программный код вызывает метод Modified класса TPropertyPage, Этот вызов требуется для того, чтобы диалоговое окно страницы свойств «узнало» о внесении изменений, а также для «разрешения» кнопки Apply (Применить). Ког- да пользователь взаимодействует с одним из элементов формы, автоматически производится вызов Modified в отношении метода класса TPropertyPage, который обрабатывает внутреннее сообщение cm_Changed. Как пользователь, вы не должны изменять кнопки этих элементов управления, однако эту строку необходимо до- бавить самостоятельно. ПРИМЕЧАНИЕ-----------------------------------------------------------— Еще один совет относится к заголовку (Caption) формы страницы свойств. Он будет использоваться в диалоговом окне основной среды в качестве заголовка вкладки, соответствующей данной страни- це свойств. Следующий шаг заключается в осуществлении связи элементов управления страницы свойств со свойствами элемента управления ActiveX. Для обеспечения функционирования класс страницы свойств автоматически имеет два метода: Up- dateOleObject и UpdatePropertyPage. Как можно догадаться из их имен, эти методы копируют данные из страницы свойств в элемент управления ActiveX и наоборот (см. программный код примера). Последний шаг заключается в соединении страницы свойств с элементом уп- равления ActiveX. После создания элемента мастер ActiveX Control Wizard авто- матически добавляет объявление метода DefinePropertyPages в модуль реализации. В этом методе можно вызвать метод DefinePropertyPage (в данном случае имя мето- да необычно) для каждой страницы свойств, которую необходимо добавить в эле- мент управления. Параметром этого метода является GUID страницы свойств, который можно найти в соответствующем модуле: procedure TMdWArrowX DefinePropertyPages( DefinePropertyPage TDefinePropertyPage). begi n DefinePropertyPage(Class_PropertyPagel) end: А теперь вы закончили разработку страницы свойств. После повторной компи- ляции и регистрации ActiveX-библиотеки вы можете установить элемент управ- ления ActiveX в любую основную среду разработки (включая Delphi) и посмот- реть, как он выглядит (см. рис. 12.9). ActiveForms Как я уже упоминал, вместо использования для генерации элемента управления ActiveX мастера ActiveX Control Wizard можно использовать ActiveForm, которая является элементом управления ActiveX, основанным на форме, и может содер- жать один и более Delphi-компонентов. Эта технология используется в Visual Basic для создания новых элементов управления и имеет смысл при создании составно- го (сложного) компонента. В примере XClock я поместил на ActiveForm надпись (графический элемент уп- равления, который не может использоваться в качестве точки отсчета в элементе управления ActiveX) и таймер, связав их с небольшим по объему программным 0554
Создание элементов управления ActiveX 555 кодом. Форма, являющаяся, по сути, элементом управления, стала контейнером для других элементов управления, что облегчает создание составных компонен- тов (проще, чем составных компонентов VCL). Для создания такого элемента выберите значок ActiveForm на странице ActiveX, открываемой выбором File ► New. Delphi с помощью диалогового окна мастера ActiveForm Wizard (аналогичного окну ActiveX Control Wizard) уточнит у вас не- которые сведения. Внутренние особенности ActiveForm Прежде чем продолжить рассмотрение примера, давайте подробнее изучим про- граммный код, генерируемый ActiveForm Wizard. Ключевым отличием от обыч- ной формы Delphi является объявление класса новой формы, который наследует- ся от класса TactiveForm, и реализует специальный интерфейс ActiveForm. Код, сгенерированный для класса активной формы, реализует довольно много методов Set и Get, которые изменяют или возвращают соответствующие свойства Delphi- формы; этот программный код также реализует события, которые опять же явля- ются событиями формы. События TForm при создании формы настраиваются на внутренние методы. Например: procedure TAXForml Initialize, begin OnActivate = ActivateEvent. end; Далее каждое событие отображает себя на внешнее событие ActiveX: procedure TAXForml ActivateEventiSender TObject), begin if FEvents <> nil then FEvents OnActivate, end; С учетом этого отображения вам не надо обрабатывать события формы непос- редственно. Вместо этого можно либо добавить программный код в стандартные обработчики или перекрыть TForm-методы, которые в конечном итоге вызывают эти события. Проблемы отображения связаны только с событиями самой формы и не относятся к событиям компонентов формы. События компонентов обрабаты- ваются как обычно. СОВЕТ---------------------------------------------------------------------- Эти проблемы (и их возможные решения) продемонстрированы в примере XForml. Я не буду рас- сматривать его более подробно, а оставлю в качестве примера для самостоятельного изучения. Элемент управления ActiveX XCIock После того как мы рассмотрели основы, давайте вернемся к разработке примера XCIock. И Поместите на форму элементы Timer и Label с крупным шрифтом, отцентриро- ванным текстом, выровненным по клиентской области. 2- Напишите обработчик события для события таймера OnTimer для того, чтобы элемент каждую секунду обновлял выходные показания значением текущего времени: 0555
556 Глава 12. От СОМ к СОМ+ procedure TXClock TimerlTimer(Sender TObject). begin Label 1 Caption = TimeToStr (Time). end; 3. Откомпилируйте эту библиотеку, зарегистрируйте ее и установите в пакет для проверки ее в среде Delphi. Обратите внимание на эффект «вдавленной» границы. Он управляется свой- ством AxBorderStyle активной формы — одним из свойств активных форм, недо- ступных в обычной форме. ActiveX на веб-страницах В предыдущем примере для создания нового элемента управления ActiveX мы использовали технологию ActiveForm. ActiveForm — это элемент управления ActiveX, основанный на форме. Документация компании Borland зачастую предполагает, что ActiveForms должны использоваться на HTML-страницах, но там же могут использоваться и элементы управления ActiveX. Обычно каждый раз, когда вы создаете библиотеку ActiveX, среда Delphi делает доступным пункты меню Project ► Web Deployment Options (Проект ► Параметры размещения в Сети) и Project ► Web Deploy (Проект ► Размещение в Сети). ВНИМАНИЕ ----------------------------------------------------------------- В Delphi 7 эта команды меню доступны только для ActiveForm, что я считаю ошибкой. При недоступ- ности этих пунктов можно воспользоваться следующей уловкой: добавьте ActiveForm в текущую библиотеку ActiveX, что сделает доступным необходимые команды меню, а затем тут же удалите ActiveForm. При этом пункты меню остаются доступными. Проблема заключается в том, что эту операцию необходимо выполнять при каждом повторном открытии проекта, т. е. до тех пор, пока компания Borland не исправит эту ошибку. Первая команда позволяет указать, как и где необходимо разместить необходи- мые файлы. В диалоговом окне можно установить каталог сервера для размеще- ния компонента ActiveX, URL этого каталога, а также каталог сервера для разме- щения HTML-файла (который будет ссылаться на библиотеку ActiveX с помощью предоставленного вами URL). Также можно указать использование сжатого САВ-файла, который будет со- держать OCX-файл и другие вспомогательные файлы, такие как пакеты, что упро- щает и ускоряет доставку приложения к пользователю. Использование сжатого файла означает ускоренную загрузку. Для проекта XClock я создал HTML- и САВ- файлы, расположенные в том же каталоге. Открытый в Internet Explorer HTML- файл представлен на рис. 12.10. Если вы получите только красную отметку X, ука- зывающую на ошибку загрузки элемента управления, то возможны различные объяснения проблемы: Internet Explorer не позволяет загружать элементы управ- ления, он не соответствует уровню безопасности для элементов без подписи, в эле- менте управления отсутствует номер версии и т. д. и т. п. Обратите внимание на раздел HTML-файла, обращающегося к элементу Уп' равления. Можно использовать специальный тег param для настройки свойств дан- ного элемента управления. Например, в HTML-файле элемента управления XArrow я изменил автоматически сгенерированный код HTML-файла (XArrowCust.htm) следующими тремя тегами param: 0556
Введение в СОМ+ 557 ^E:\books\md7code\ii\XClack\XClocktjtbhhn*:?*fcr<woft iFitemettxpkxer | Яв Edfc 'flew Favorites Tods Hrip :' KS| |^E\booI«Vnd7cocfeU2\XciockV<ClockLtbtm £^<jQ inks’*5 =-*— Delphi 7 ActiveX Test Page You should see 5 our Delphi " forms 01 conh ols embedded in the form below 11:58:03 AM Рис. 12.10. Элемент управления XCIock в примере HTML-страницы <object classid="clsid 482B2145-4133-11D3-B9F1-00000100A27B"" codebase-" /XArrow cab" width="350" height="250" align="center" hspace='O" vspace="O"> <param name-'ArrowHeight" value="100"> <param name=”Filled" value="-l"> <param name="FilIColor" value="lllB29"> </object> Хотя это может показаться полезной методикой, важно признать (ограничен- ную) роль ActiveX-формы, помещенной на веб-странице. Она позволяет пользо- вателю загрузить и выполнить настраиваемое Windows-приложение, которое вы- зывает множество вопросов у системы безопасности. Элемент управления ActiveX способен обращаться к системной информации, такой как имя пользователя, струк- тура каталогов и т. д. Я мог бы продолжить, но все и так ясно. Введение в СОМ+ Помимо обычных COM-серверов Delphi позволяет создавать расширенные СОМ- объекты, включая свободные объекты и поддержку транспортов. Первоначально компания Microsoft представила этот тип объектов в Windows NT и 98 с сокраще- нием MTS (Microsoft Transaction Server), а позже, в Windows 2000/ХР, переиме- новала их в СОМ+ (я называю их СОМ+, но подразумеваю как СОМ+, так и MTS). Delphi поддерживает создание как стандартных свободных объектов, так и мо- дулей удаленных данных DataSnap, основанных на свободных объектах. В обоих Случаях разработка начинается с использования одного из мастеров Delphi с 0557
558 Глава 12. От СОМ к СОМ+ помощью диалогового окна New Items и выбора значка Transactional Object на стра- нице ActiveX или значка Transactional Data Module на странице Multitier. Эти объекты необходимо добавлять в проект ActiveX-библиотеки, а не в обычное приложение. Другой значок, СОМ+ Event Object, используется для поддержки событий СОМ+. СОМ+ обеспечивает поддержку во время выполнения служб транзакций баз данных, безопасности, организацию пулов ресурсов, а также общее улучшение на- дежности DCOM-приложений. Среда времени выполнения, управляющая объек- тами, называется СОМ+ components. Эти COM-объекты хранятся во внутрипро- цессном сервере (т. е. в DLL). В то время как другие COM-объекты запускаются непосредственно в клиентском приложении, объекты СОМ+ — средой времени выполнения, в которой установлены СОМ+ библиотеки. Объекты СОМ+ долж- ны поддерживать специальные COM-интерфейсы, начиная с lObjectControl, явля- ющегося базовым интерфейсом (так же как IUnknown — для СОМ-объекта). Перед переходом к множеству технических и низкоуровневых подробностей давайте представим СОМ+ с другой точки зрения: выгодность этого подхода. СОМ+ обеспечивает ряд интересных особенностей, включая: Role-Based Security Role-Based Security (Безопасность на основе роли). Роль, назначаемая клиенту, определяет, имеет ли он право доступа к интерфейсу модуля данных. Reduced Database Resources Reduced Database Resources (Сокращенные ресурсы баз данных). Можно сокра- тить число подключений к базе данных, потому что промежуточный логический уровень осуществляет регистрацию на сервере и использует одно и то же подклю- чение для множества клиентов (хотя одновременно нельзя иметь количество кли- ентов больше, чем указано в лицензии сервера). Database Transactions Database Transactions (Транзакции баз данных). Поддержка транзакций СОМ+ включает операции с множеством баз данных, хотя некоторые не-Microsoft SQL- серверы поддерживают транзакции СОМ+. Создание компонента СОМ+ Начальной точкой создания компонента СОМ+ является создание проекта биб- лиотеки ActiveX. После чего выполняются следующие действия: 1. Выберите новый Transactional Object на странице ActiveX диалогового окна New Items. 2. В появившемся окне (рис. 12.11) введите имя нового компонента (ComPluslObject в примере ComPlusl). 3. Диалоговое окно New Transactional Object позволяет ввести имя класса объекта СОМ+, модели организации поточной обработки (поскольку СОМ+ перево- дит все запросы в последовательную форму, также как будут делать Single пли Apartment) и модель транзакций: • Requires a Transaction (Требует транзакцию) Указывает, что каждый вызов от клиента к серверу будет считаться транзакцией (если только вызываю' щий поддерживает существующий контекст транзакций). 0558
Введение в СОМ+ 559 • Requires a New Transaction (Требует новую транзакцию). Указывает, что каждый вызов будет считаться новой транзакцией. • Supports Transactions (Поддерживает транзакции). Указывает, что клиент должен явно обеспечивать контекст транзакций. • Does Not Support Transaction (He поддерживает транзакции). (Вариант по умолчанию, и который я использую.) Указывает, что модуль удаленных дан- ных не может вовлекаться в какие-либо транзакции. Этот параметр защи- щает объект от активизации, если вызывающий клиент имеет транзакцию. • Ignores Transactions (Игнорирует транзакции.) Указывает, что объекты не участвуют в транзакции, но могут использоваться независимо от того, имеет ли клиент транзакцию. New Transactional Object Confess &атв [CwPluslffiied upportcpdei Рис. 12.11. Диалоговое окно New Transactional Object, используемое для создания объекта СОМ+ 4. При закрытии диалогового окна Delphi добавляет в проект библиотеку типов и модуль реализации, после чего и открывает редактор библиотеки типов, в ко- тором можно определить интерфейс нового COM-объекта. В данном примере Добавьте свойство целого типа Value, метод Increase, имеющий в качестве пара- метра общее число, а также метод AsText, возвращающий WideString с отформа- тированным значением. 5. После завершения редактирования библиотеки (щелчком на кнопке Refresh и закрытием окна) Delphi открывает мастер Implementation File Update Wizard? Если только был установлен параметр Display updates before refreshing страницы Type Library диалогового окна Environment Options. Этот мастер запрашивает у вас подтверждение перед добавлением в класс четырех методов, включая методы get и set свойства. Теперь для ОСМ-объекта можно написать какой-либо про- граммный код, который в моем примере весьма тривиальный. После завершения создания библиотеки ActiveX или COM-библиотеки, кото- рая содержит компонент СОМ+, для установки и настройки СОМ+ компонента мож- но использовать средство администрирования Component Services (представленное 0559
560 Глава 12. От СОМ к СОМ+ в консоли Microsoft Management Console, ММС). Еще лучше для установки СОМ+ компонента воспользоваться командой Run ► Install СОМ+ Object IDE Delphi. В по- явившемся диалоговом окне можно выбрать устанавливаемый компонент (биб- лиотека может содержать множество компонентов) и выбрать СОМ+ приложе- ние, в котором вы хотите установить этот компонент. Instal с ом + objects i Objects ComPkts! Object Mastering Detphi Demo OK | Cancel [ Qeip | Приложение COM+ — это лишь способ сгруппировать СОМ+ компоненты, это не программа или что-либо похожее на нее (почему они называются приложения- ми — мне совсем непонятно). Итак, в диалоговом окне Install СОМ+ Object можно выбрать существующее приложение/группу, выбрать страницу Install Into New Application и ввести имя и описание. В консоли администрирования Component Services я назвал это СОМ+ прило- жение Mastering Delphi Com+ Test (рис. 12.12). Это передовая, которую можно ис- пользовать для точной настройки поведения СОМ+ компонентов, установки их модели активизации (немедленная активизация, создание пула объектов и т. п.), поддержки транзакций, а также необходимых моделей безопасности и взаимной совместимости. Эта консоль также может использоваться для наблюдения за объек- тами и вызовами методов (при долгосрочном выполнении). На рис. 12.12 видно, что активизировано только два объекта ВНИМАНИЕ--------------------------------------------------------- Поскольку вы создали один или несколько объектов, COM-библиотека остается загруженной в СОМ+ среду и некоторые из объектов могут содержаться в кэш-памяти, даже если с ними не соединен ни один клиент. По этой причине обычно невозможно перекомпилировать COM-библиотеку после ее использования, если только вы не использовали ММС для ее выключения или не установили для нее в ММС Transaction Timeout равным 0 секунд. Я создал клиентскую программу для СОМ+ объекта, но она похожа на любой другой COM-клиент. После импортирования библиотеки типов, которая автома- тически регистрируется в ходе установки компонента, я создал переменную ин- терфейсного типа, обращающуюся к нему и обычно вызывающую его методы. Модули данных транзакций Тот же самый уровень функциональных возможностей достигается при создании транзакционного модуля данных (transactional data module) — удаленного модуля 0560
Введение в СОМ+ 561 данных внутри СОМ+ компонента. После создания транзакционного модуля дан- ных можно создать Delphi-приложение DataSnap (см. главу 16). Имеется возмож- ность добавить один или несколько компонентов dataset, один или более провай- деров и экспортировать этих провайдеров. В библиотеку типов модуля данных за счет редактирования библиотеки типов или использования команды Add То Interface можно также добавлять пользовательские методы. fitOw Sew S3 S3 ® £ *** > Й j 0 TfM | I ♦ IIS CMjt Of Process PooM Appkations * & IIS Utilities I - ф Masterng Delphi Corrrt- Test - | ComPlus 1 ComPlus 1 Object I -i 2J interfaces . - IComPluslObject j I r Methods i I 1 Subscriptions I ♦ ....I Rotes | I * System Appbcation I • 1 Ostrtouted Transaction Coordinator < > Ч> В Components t abtecfofr ...................i.fflKfet <Й<опЛи1 ComPlus lObjed 2 2____~ Рис. 12.12. Установленный COM+ компонент в пользовательском СОМ+ приложении В СОМ+ компоненте или транзакционном модуле данных можно использо- вать специальные методы, поддерживающие транзакции. Эти методы технически предоставляются (на нижнем уровне) в интерфейсе lObjectContext, возвращаемом методом GetObjectContext: О SetComplete сообщает СОМ+ среде об объекте, который закончил работу и мо- жет быть деактивирован, что фиксирует транзакцию; О EnableCommit сообщает, что объект еще не закончил работу, но транзакция дол- жна быть зафиксирована; О DisableCommit останавливает операцию фиксации транзакции, даже если метод выполнен, делая невозможным деактивацию объекта между вызовами метода; О SetAbort сообщает, что объект закончил работу и может быть активирован, но транзакция не может быть зафиксирована; О IsInTransaction проверяет, является ли данный объект частью транзакции. Кпрочим методам IContextObjectотносятся Createlnstance, который создает СОМ+ объект в том же контексте и в ходе текущей транзакции; IsCaLLerlnRole, который проверяет, имеет ли «вызывающий» особенную роль «безопасности»; IsSecurity- Enabled (чье имя говорит само за себя). После создания транзакционного модуля данных в библиотеке сервера его можно Установить (см. установку обычного СОМ+ объекта). После установки он становит- ся Доступен напрямую из других приложений и представлен в консоли управления. Важная особенность СОМ+ заключается в том, что с помощью этой среды зна- чительно упрощается настройка поддержки DCOM СОМ+ среда клиентского компьютера может захватывать информацию из СОМ+ среды компьютера-серве- Ра, включая сведения о регистрации СОМ+ объекта, который должен быть досту- Пен для вызова по сети Такая сетевая конфигурация была бы значительно слож- ней, если бы она реализовалась с помощью обычной DCOM, без MTS или СОМ+. 0561
562 Глава 12. От СОМ к CQM+ ПРИМЕЧАНИЕ------------------------------------------------------------— Даже ввиду того, что СОМ+ настройка значительно лучше DCOM-настройки, ее можно реализовать только на компьютерах, на которых установлены последние версии операционной системы Windows. Учитывая, что даже сама компания Microsoft отходит от DCOM-технологии, перед разработкой осно- ванной на ней крупной системы вам сначала необходимо изучить альтернативный вариант, предо- ставляемый набором протоколов SOAP (рассматривается в главе 22). СОМ+ события Клиентское приложение, которое использует обычные COM-объекты и серверы автоматизации, может вызывать методы этих серверов, но это — неэффективный способ проверки, обновил ли сервер данные для клиента. По этой причине клиент может определить COM-объект, реализующий интерфейс обратного вызова, пере- дать этот объект серверу и позволить серверу вызвать его. Традиционные СОМ- события (которые используют интерфейс IConnectionPoint) упрощены с помощью объектов Delphi for Automation, но по-прежнему сложны для обработки. СОМ+ представляет упрощенную модель событий, в которой события явля- ются СОМ+ компонентами, а подключениями управляет окружение СОМ+. При традиционном обратном вызове СОМ серверный объект должен был отслеживать множество клиентов, которых он обязан был уведомлять. Это то, что Delphi не обеспечивает автоматически (программный код события Delphi ограничен одним клиентом). Для поддержки обратных вызовов СОМ множества клиентов необхо- димо добавить программный код, содержащий ссылки на каждого из клиентов. В технологии СОМ+ сервер вызывает единственный интерфейс события, а окру- жение СОМ+ пересылает это событие всем заинтересованным в нем клиентам. Та- ким образом, клиент и сервер оказываются менее связанными, что делает возмож- ным получение клиентом уведомлений от различных серверов безо всяких изме- нений в его программном коде. СОВЕТ------------------------------------------------------------------ Некоторые критикуют Microsoft за то, что она представила только эту модель, поскольку разработ- чикам, работающим в среде Visual Basic, сложно обрабатывать COM-события традиционным спосо- бом. Windows 2000 представляет несколько особенностей, которые специально предназначены для VB-программистов. Для создания события СОМ+ необходимо создать СОМ-библиотеку (ActiveX- библиотеку), а затем воспользоваться мастером СОМ+ Event Object. Получивший- ся в результате работы мастера проект будет содержать библиотеку типов с опре- делением интерфейса, используемого для запуска событий, плюс программный код реализаций-ш/стыа/ек. Сервер, который получает уведомление о событии, обес- печит реализацию интерфейса. Код-пустышка нужен лишь для поддержки систе- мы регистрации СОМ в Delphi. При создании библиотеки MdComEvents я добавил в библиотеку типов один ме- тод с двумя параметрами, получив представленный ниже программный код (в фай- ле определения интерфейса): type IMdlnform - interfaceiIDispatch) 0562
Введение в СОМ+ 563 ['{202D2CC8-8E6C-4E96-9C14-1FAAE3920ECC}'] procedure Informs(Code: Integer; const Message: WideString); safecall: end: Основной модуль включает «пустышку» COM-объекта (обратите внимание, что метод является абстрактным и не имеет реализации) и его фабрику классов для обеспечения самостоятельной регистрации. Теперь необходимо откомпили- ровать библиотеку и установить ее в среде СОМ+: 1. В консоли ММС выберите СОМ+ приложение, перейдите в папку Components и с помощью контекстного меню добавьте в него новый компонент. 2. В мастере COM Component Install Wizard щелкните на кнопке Install New Event Class и выберите только что откомпилированную библиотеку. Определение СОМ+ события будет установлено автоматически. 3. Для проверки того, что это работает, я создал реализацию этого интерфейса события и вызывающего его клиента. Реализация может быть добавлена в дру- гую ActiveX-библиотеку, содержащую обычный COM-объект. В Delphi-масте- ре COM Object Wizard с помощью списка, появляющегося при щелчке на кнопке List, можно выбрать интерфейс для реализации. 4. Результирующая библиотека, которая в моем примере названа EvtSubscriber, «открывает» объект автоматизации: COM-объект, реализующий интерфейс IDispatch (который является обязательным для СОМ+ событий). Объект имеет следующее определение и программный код: type TInformSubscriber = class(TAutoObject. IMdlnform) protected procedure InformsfCode: Integer; const Message: WideString): safecall: end. procedure TInformSubscriber.Informs(Code: Integer; const Message: WideString): begin ShowMessage ('Message <' + IntToStr (Code) + ’>: ' + Message): end: После компиляции этой библиотеки вы можете сначала установить ее в СОМ+ среду, а затем привязать ее к событию. Второй шаг выполняется с помощью консо- ли управления Component Services (Службы компонентов), посредством выбора папки Subscriptions в регистрации события объекта и использовании контекстного меню New ► Subscription. Открывающийся при этом мастер выбирает интерфейс для Реализации (в библиотеке событий СОМ+, вероятно, оно будет лишь единствен- ным событием); далее вы увидите список СОМ+ компонентов, реализующих этот интерфейс. Выбор одного или нескольких из них настраивает привязку подписки, которая будет перечислена в папке Subscriptions. Пример настройки в ходе выпол- нения привязки представлен на рис. 12.13. И, наконец, вы можете сфокусироваться на приложении, запускающем собы- Тие> которое я назвал Publisher (поскольку он публикует сведения обо всех иптере- сУющих COM-объектах). Это самый простой шаг процесса, поскольку он является ооьтчным COM-клиентом, который использует сервер событий. После импорти- 0563
564 Глава 12. От СОМ к СОМ+ Component Services "Jb ggyfe ttelp _____________ Action View ри*з»Ф ©C0 X PM Й ]uJ Tree | H Mastering Delphi Com+ Test 3 £j Components F ComPlusl ComPkislObject - Cj Interfaces '♦« IComPluslObject ' A Subscriptions EvtSubscnber InformSubscnber | S C..1 Interfaces ;+ IMdlnform 3 Li Subscriptions demo subscription f- MdComEvents Mdlnform - Li Interfaces *« IMdlnform I ..1 Subscriptions 1— ♦’ l J Roles (+' System Application »r| Subscript) demo subscription Рис. 12.13. COM+ событие с двумя подписками в консоли управления Component Services рования библиотеки типов СОМ+ события в приложение-«издатель» можно до- бавить следующий код: var Inform IMdlnform. begin Inform = CoMdlnform Create Inform Informs (20. Editl Text). Для сохранения целостности мой пример создает COM-объект в методе Form- Create, но эффект остается тот же. Теперь клиентская программа считает, что она вызывает объект «СОМ+ событие», но на самом деле этот объект (обеспечивае- мый средой СОМ+) вызывает метод для каждого из активных подписчиков. В дан- ном случае вы увидите сообщение: d№o«t Message <2О> Here I ат Чтобы сделать пример более интересным, можно один и тот же сервер подпи- сать к интерфейсу события дважды. Полезный эффект заключается в возможно- сти не прикасаясь к клиентскому программному коду получить два сообщения: по одному для каждого из подписанных серверов. Очевидно, что этот эффект стано- вится интересным, когда имеется множество различных COM-компонентов, уме- ющих обрабатывать данное событие, поскольку вы можете просто включить или отключить их с помощью консоли управления, изменяя СОМ+ среду без измене- ния программного кода. 0564
СОМ и .NET в Delphi 7 565 COM и .NET в Delphi 7 В ходе внедрения новой инфраструктуры .NET компания Microsoft попыталась помочь компаниям, которые продолжают использовать существующие програм- мы. Один из путей миграции предоставляется за счет совместимости .NET-объек- тов с COM-объектами. Существующие COM-объекты можно использовать в NET- приложении, хотя при этом нарушается принцип управляемого и безопасного кода Можно использовать .NET-сборки (.NET-assembhes) из Windows-приложений так, как будто они являются «родными» COM-объектами. Такая функциональная воз- можность имеет место благодаря оболочкам, предоставляемым Microsoft. Попытка компании Borland поддержать возможность взаимодействия СОМ/ .NET в Delphi 7 в основном заключается в том факте, что COM-объекты, ском- пилированные в Delphi, не вызывают проблем для программы-импортера .NET Кроме того, Delphi-модуль импорта библиотеки типов может спокойно работать со сборками .NET как со стандартными СОМ-библиотеками. Сказав это (если вы не сильно занимались СОМ), я должен предостеречь от этого пути. Если вы хотите сделать ставку на технологии Microsoft, то необходимо учитывать, что будущее — за «родными» .NET решениями Если вам не нравятся технологии компании Microsoft или необходимо кросс-платформенное решение, то выбор СОМ по-прежнему будет хуже, чем .NET (в будущем появится .NET струк- тура для других операционных систем). ПРИМЕЧАНИЕ -------------------------------------------------------- Предлагаемые здесь действия также будут работать и в Delphi 6. Delphi 7 лишь добавляет автомати- ческую систему импорта, которая иногда имеет проблемы с программным кодом, сгенерированным компилятором Delphi для .NET Preview. Для демонстрации возможностей импорта .NET я создал .NET-библиотеку с интерфейсом и реализующим его классом. Интерфейс и класс позаимствованы из примера FirstCom, рассматриваемого в начале этой главы. Ниже представлен про- граммный код библиотеки, который должен быть откомпилирован в Delphi для компилятора предварительного просмотра NET. Необходимо обязательно создать объект, или компоновщик удалит из откомпилированной библиотеки {сборки, на жаргоне .NET) практически все: library NetLibrary uses NetNumberClass in 'NetNumberClass pas' {$R * resj begin II создать объект для компоновки всего кода TNumber Create end. Этот код содержится в модуле NetNumberClass, определяющем интерфейс и реа- лизующий его класс type INumber ~ interface 0565
566 Глава 12. От СОМ к СОМ+ function GetValue: Integer: procedure SetValue (New: Integer); procedure Increase; end; TNumber = cl ass(TObject. INumber) private fValue; Integer: public - constructor Create; function GetValue: Integer: procedure SetValue (New: Integer): procedure Increase; end: Обратите внимание, что в отличие от СОМ-сервера, данный интерфейс в соот- ветствии с правилами .NET не требует GUID (хотя он и может иметь его с по- мощью класса Gut dAtt ri bute). Система самостоятельно сгенерирует его. После ком- пиляции этого кода (доступен в каталоге Netlmport, папки программного кода, относящейся к данной главе) в Delphi для .NET Preview (а не для Delphi 7!) необ- ходимо выполнить два действия: запустить утилиту .NET Framework Assembly Registration Utility компании Microsoft (regasm); запустить Type Library Importer компании Borland, (tlibimp). (Теоретически вы могли бы пропустить эти действия и непосредственно воспользоваться диалоговым окном Import Type Library, но для некоторых библиотек использование tLibimp является обязательным.) На практике перейдите в папку, в которую вы откомпилировали библиотеку, и из командной строки наберите две команды, указанные жирным шрифтом (я за- хватил и остальной текст): C-\md7code\NetImport>regasm netlibrary.dll Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.0 Copyright (0 Microsoft Corporation 1998-2001. All rights reserved. Types registered successfully C.\md7code\NetImport>tli bimp netli brary.dl1 Borland TLIBIMP Version 7.0 Copyright (c) 1997, 2002 Borland Software Corporation Type library loaded ... Created E \books\md7code\12\NetImport\mscorlib_TLB.dcr Created E-\books\md7code\12\NetImport\mscorlib_TLB pas Created E \books\md7code\12\NetImport\NetLibrary_TLB dcr Created E-\books\md7code\12\NetImport\NetLibrary_TLB.pas Это действие заключается в создании модуля библиотеки типов проекта и мо- дуля импортируемой библиотеки Microsoft .NET Core Library (mscorlib.dll). Теперь можно создать новое Delphi 7 приложение (стандартную Win32-nporpaMMy) и ис- пользовать .NET-объекты, как будто они являются COM-объектами. Вот фрагмент программного кода примера Netlmport, представленного на рис. 12.14: uses NetLibrary_TLB: procedure TForml.btnAddClick(Sender: TObject); var num: INumber; 0566
Что далее? 567 begin num = CoTNumber Create as INumber: num.Increase: ShowMessage (IntToStr (num.GetValue)): end: Рис. 12.14. Программа Netlmport использует .NET-объект для суммирования чисел Что далее? В этой главе мы рассмотрели приложения COM-технологии компании Microsoft, охватывая автоматизацию, документы, элементы управления и проч. Вы увидели, как среда Delphi осуществляет создание относительно простых серверов автома- тизации, а также клиентов и элементов управления ActiveX. Delphi также позво- ляет создавать компоненты-оболочки для таких серверов автоматизации, как Word и Excel. Мы также познакомились с элементами СОМ+ и кратко рассмотрели ис- пользование ActiveForms в браузере. Я отметил, что это не самый удачный подход для веб-программирования (вопрос, который мы обсудим далее в этой книге). Как упоминалось ранее, если технология СОМ играет важную роль в Windows 2000/ХР, то в последующих версиях операционных систем компании Microsoft эта роль понизится до поддержки инфраструктуры .NET (включая SOAP и XML). Но более подробное рассмотрение поддержки SOAP в Delphi будет представлено в главе 23. 0567
ЧАСТЬ III Механизмы работы с базами данных В этой части: ♦ Глава 13. Встроенная в Delphi архитектура работы с базами данных ♦ Глава 14. Клиент-серверная архитектура с использованием dbExpress ♦ Глава 15. Технология ADO ♦ Глава 16. Многозвенные приложения DataSnap ♦ Глава 17. Разработка компонентов, работающих с данными ♦ Глава 18. Формирование отчетов с использованием Rave 0568
4 Встроенная в Delphi 1*Э архитектура работы с базами данных Встроенная в Delphi поддержка работы с базами данных (БД) является ключевой возможностью этой программной среды. Очень многие программисты большую часть своего рабочего времени тратят на создание кода, работающего с базой дан- ных. Этот код должен быть наиболее надежной частью всего приложения. В дан- ной главе я приведу обзор средств Delphi, предназначенных для программирова- ния баз данных. Прежде всего следует отметить, что вы не найдете здесь дискуссии на тему тео- рии проектирования баз данных. Я предполагаю, что вы уже знакомы с основами этой теории и обладаете опытом проектирования структуры базы данных. Я не собираюсь углубляться в изучение проблем, относящихся к базам данных. Моя цель — помочь вам разобраться, каким образом Delphi поддерживает работу с ба- зами данных. В начале главы рассматриваются альтернативные механизмы доступа к данным, поддерживаемые в среде Delphi. После этого я рассмотрю набор присутствующих в Delphi компонентов, предназначенных для доступа к базам данных. В данной главе основное внимание уделено компоненту TClientDataSet, который обеспе- чивает доступ к локальным данным. Кроме того, здесь приводится обзор класса TDataSet и анализ компонентов TField, а также использование элементов управле- ния, имеющих отношение к базам данных. Рассмотрение вопросов, связанных с доступом к данным в рамках архитектуры «клиент-сервер», начинается с гла- вы 14. В главе 14 мы рассмотрим более сложные вопросы, связанные с доступом к базам данных, в частности использование библиотеки dbExpress, а также компо- ненты InterBase Express. Наконец, хочу отметить, что фактически весь материал, обсуждаемый в данной главе, можно считать кросс-платформенным. В частности, рассматриваемые при- меры можно откомпилировать с использованием CLX для среды Linux. Для этого Необходимо воспользоваться CDS-файлами в соответствующих подкаталогах. В данной главе рассматриваются следующие вопросы: ° входящие в состав Delphi компоненты для работы с базами данных; ° альтернативные способы доступа к базам данных; ° использование элементов управления, имеющих отношение к базам данных; 0569
570 Глава 13. Встроенная в Delphi архитектура работы с базами данных о элемент управления DBGrid; о манипулирование полями таблицы; о приложения баз данных со стандартными элементами управления. Доступ к базе данных: dbExpress, локальные данные и другие альтернативы Самые ранние инкарнации Delphi получили популярность как инструмент разра- ботки приложений, ориентированных на работу с базами данных. Однако в пер- вых версиях Delphi поддерживался единственный способ доступа к базе данных: Borland Database Engine (BDE). Начиная с версии Delphi 3 раздел библиотеки VCL, связанный с доступом к базе данных, был реструктурирован таким образом, чтобы обеспечить поддержку других технологий обращения к базам данных. В настоя- щее время Delphi поддерживает ADO, компоненты InterBase, библиотеку dbExpress, а также BDE. Помимо этого сторонние производители могут разработать для Delphi различные другие механизмы доступа к данным, представленным в самых разно- образных форматах (некоторые из этих форматов не являются доступными для компонентов Borland). СОВЕТ---------------------------2------------------------------------------ В среде Kylix общая картина несколько отличается. Компания Borland решила не переносить уста- ревшую технологию BDE в среду Linux, вместо этого усилия разработчиков Borland направлены на развитие новой библиотеки dbExpress, которая, если сравнивать с BDE, является более легковес- ным решением. Существует также еще одно решение; для простых приложений вы можете ис- пользовать встроенный в Delphi компонент ClientDataSet, который поддерживает возможность сохранять таблицы в локальных файлах, — эту возможность компа- ния Borland обозначает именем MyBase. Имейте в виду, что типичное традицион- ное приложение Delphi, основанное на таблицах Paradox, не может быть перенесе- но в среду Kylix из-за того, что в Kylix не поддерживается BDE. Библиотека dbExpress (DBX) Одним из наиболее значимых нововведений Delphi в последние годы является появление библиотеки dbExpress (DBX). Эта библиотека поддерживается как на платформе Windows, так и в среде Linux. Я употребляю термин библиотека вмес- то термина механизм доступа к базе данных (database engine) не случайно. Дело в том, что dbExpress использует более легковесный подход и фактически не требу- ет выполнения какой-либо конфигурации клиентских компьютеров. Легкость и переносимость — это две ключевые характеристики dbExpress. По- явление dbExpress обусловлено началом развития проекта Kylix. В сравнении с ДРУ' гими механизмами доступа к базам данных библиотека dbExpress чрезвычайно ограничена в своих возможностях. Она позволяет работать только с серверами S<2 (отсутствует поддержка локальных данных), она не поддерживает возможность 0570
доступ к базе данных: dbExpress, локальные данные и другие альтернативы 571 кэширования и обеспечивает только однонаправленный доступ к данным, нако- нец, она позволяет работать только с запросами SQL и не позволяет генерировать выражения SQL, предназначенные для обновления информации в базе данных. Сначала можно подумать, что все эти ограничения делают библиотеку абсо- лютно бесполезной. Однако на самом деле это преимущество — они делают биб- лиотеку интересной. Однонаправленные наборы данных без возможности прямо- го обновления вполне пригодны для формирования отчетов, включая генерацию HTML-страниц, на которых показывается содержимое базы данных. Если же вы намерены построить пользовательский интерфейс, позволяющий модифицировать содержимое базы данных, в составе Delphi присутствуют специально предназна- ченные для этого компоненты (а именно ClientDataSet и Provider), которые обеспе- чивают кэширование и обработку запросов. За счет использования различных ком- понентов для решения разных задач вы обеспечиваете для вашего приложения dbExpress значительно большую гибкость, чем монолитный механизм работы с базой данных (такой как BDE), который делает за вас множество вещей, однако зачастую делает это не так, как вам надо. Библиотека dbExpress позволяет вам разрабатывать приложения, которые (если не принимать во внимание проблемы, связанные с существованием различных диалектов SQL) могут обращаться к различным базам данных без существенных модификаций кода. Поддерживаются следующие серверы SQL: InterBase — соб- ственный сервер компании Borland, Oracle, MySQL (этот сервер популярен в сре- де Linux), Informix, IBM DB2 и Microsoft SQLServer (в Delphi 7). Более подробное описание dbExpress и связанных с этой библиотекой компонентов VCL, а также множество примеров ее использования содержатся в главе 14. Данная глава содер- жит описание основ архитектуры баз данных. СОВЕТ------------------------------------------------------------------- Присутствие в Delphi 7 драйвера dbExpress для Microsoft SQL Server закрывает огромную прореху. База данных Microsoft SQL Server часто используется на платформе Windows, поэтому разработчи- ки, которые нуждаются в решении, легко адаптируемом между несколькими разными серверами баз данных, зачастую вынуждены добавлять в свои продукты поддержку Microsoft SQL Server. Те- перь исчезла еще одна причина иметь дело с BDE. Недавно компания Borland выпустила исправле- ние для драйвера SQL Server dbExpress, поставляемого в составе Delphi 7. Borland Database Engine (BDE) В Delphi по-прежнему присутствует поддержка BDE. Технология BDE позволяет вам обращаться к локальным форматам баз данных (таких как Paradox и dBase), а также серверам SQL, впрочем, как и к другим источникам данных, доступ к кото- рым поддерживается при помощи драйверов ODBC. В ранних версиях Delphi тех- нология BDE являлась стандартной технологией доступа к базам данных, однако в настоящее время компания Borland рассматривает эту технологию как устарев- шую. В частности, это является истиной при использовании BDE для доступа к сер- верам SQL через драйверы SQL Links. Использование BDE для доступа к локаль- ным таблицам по-прежнему официально поддерживается, однако это происходит только потому, что Borland не обеспечивает прямого пути миграции для приложе- ний подобного типа. 0571
572 Глава 13. Встроенная в Delphi архитектура работы с базами данных В некоторых случаях локальную таблицу можно заменить компонентом Client- DataSet (MyBase) — это можно сделать для временных и небольших таблиц. Одна- ко такой подход не работает для более крупных локальных таблиц, так как MyBase подразумевает загрузку всей таблицы целиком в оперативную память даже в слу- чае, если требуется обратиться всего к одной записи этой таблицы. Чтобы решить проблему, предлагается переместить более крупные таблицы на сервер SQL, уста- новленный на клиентском компьютере. Сервер InterBase требует совсем немного дискового пространства и, таким образом, идеально подходит для подобных ситу- аций. Кроме того, благодаря этому вы сможете перенести свое приложение в среду Linux, в которой поддержка BDE отсутствует. Конечно же, если у вас уже есть приложения, основанные на использовании BDE, вы по-прежнему можете использовать BDE. На странице BDE палитры ком- понентов Delphi по-прежнему присутствуют компоненты Table, Query, StoreProc и другие специфичные для BDE компоненты. Однако я настоятельно не рекомен- дую вам использовать технологию BDE в своих новых программах. Поддержка этой технологии фактически полностью прекращена ее производителем. Если ваши программы нуждаются в использовании подобной архитектуры (или вы хотите обеспечить совместимость с устаревшими форматами баз данных), возможно, имеет смысл обратить внимание на аналогичные механизмы, разработанные сторонни- ми производителями, которые можно использовать в качестве замены BDE. ПРИМЕЧАНИЕ-------------------------------------------------------------- Отказ компании Borland от дальнейшего развития BDE является основной причиной, по которой я удалил описание BDE из очередной редакции данной книги. В прошлых изданиях книги в данной главе рассматривались компоненты Table и Query, однако теперь глава полностью переписана. Те- перь в ней рассматривается архитектура Delphi, основанная на использовании компонента Client- DataSet. InterBase Express (IBX) Компания Borland добавила в Delphi еще один набор компонентов для доступа к базе данных InterBase Express. Эти компоненты специально оптимизированы для работы с собственным SQL-сервером компании Borland под названием InterBase. В отличие от dbExpress этот механизм не является адаптируемым для использова- ния совместно с любым типом SQL-сервера. Этот набор компонентов позволяет работать с сервером конкретного типа. Если вы планируете работать только с сер- вером InterBase, благодаря использованию специализированных компонентов вы можете обеспечить более высокую производительность, получить возможность более широкого контроля над сервером, а также возможность конфигурирования и обслуживания сервера прямо из клиентского приложения. ПРИМЕЧАНИЕ--------------------------------------------------------— Использование компонентов InterBase Express является частным случаем использования специфич- ных для конкретной базы данных наборов данных. Подобные специализированные наборы компо- нентов производятся также другими независимыми поставщиками для многих других серверов. (Су- ществуют наборы специализированных компонентов для доступа к Oracle, для работы с локальными и сетевыми файлами dBase, альтернативные наборы компонентов для работы с InterBase, а также многие другие специализированные наборы компонентов.) 0572
доступ к базе данных: dbExpress, локальные данные и другие альтернативы 573 Вы можете рассмотреть возможность использования IBX (или другого совмес- тимого набора компонентов) в случае, если вы уверены в том, что в будущем ис- пользуемая вами база данных не будет изменена, и при этом вы хотите обеспечить наилучшую производительность ценой гибкости и переносимости. Следует при- нимать во внимание, что гибкость и переносимость могут оказаться недостаточ- ными для того, чтобы оправдать подобный подход. Кроме того, если вы использу- ете специализированный набор компонентов, вам придется потратить время на его освоение, если же вы используете универсальный механизм, навыки работы с этим механизмом окажутся полезными во множестве других ситуаций, при ра- боте с множеством других источников данных. MyBase и компонент ClientDataSet Компонент ClientDataSet предназначен для доступа к наборам данных, размещен- ным в оперативной памяти. Эти данные могут быть временно загружены из ло- кального файла, а затем (после обработки) записаны обратно в локальный файл или экспортированы в другой набор данных при помощи компонента Provider. Сле- дует иметь в виду, что данные, хранящиеся в памяти, создаются в процессе работы программы и исчезают сразу же, как только программа завершает работу. Компания Borland указывает, что вы можете использовать компонент Client- DataSet отображенный на файл с именем MyBase для того, чтобы подтвердить, что база данных является локальной. Мне не очень нравится, каким образом марке- тинг компании Borland продвигает эту технологию, однако в любом случае она имеет место быть, и я подробнее расскажу о ней в разделе «MyBase: автономный компонент ClientDataSet». Доступ к данным через провайдера — это стандартный подход для клиент-сер- верных архитектур (об этом будет рассказано в главе 14), а также для многозвен- ных архитектур (о которых рассказывается в главе 16). Компонент ClientDataSet особенно полезен в ситуациях, когда используемые вами компоненты доступа к данным не поддерживают кэширование или поддерживают кэширование в огра- ниченном виде. Это прежде всего относится к библиотеке dbExpress. dbGo для ADO ADO (ActiveX Data Objects) — это высокоуровневый интерфейс, разработанный компанией Microsoft и предназначенный для доступа к базам данных. Интерфейс ADO реализован в рамках технологии Microsoft OLE DB, которая обеспечивает доступ к реляционным и нереляционным базам данных, а также к электронной почте, файловым системам и разного рода специализированным бизнес-объектам. ADO — это технология, возможности которой сравнимы с возможностями BDE: независимость от конкретного сервера базы данных, поддержка локальных баз дан- ных, равно как и серверов SQL, тяжеловесный механизм доступа, а также упро- шенное конфигурирование (простота конфигурации обусловлена тем, что конфи- сурация не является централизованной). Установка ADO не должна (теоретически) создавать особых проблем, так как этот механизм входит в состав последних вер- сий Windows. Однако между различными версиями ADO существует несовмести- мость, поэтому клиентские компьютеры необходимо обновить до той версии ADO, 0573
574 Глава 13. Встроенная в Delphi архитектура работы с базами данных которая была использована при разработке программы. Пакет MDAC (Microsoft Data Access Components) обладает достаточно большим размером, в процессе его установки происходит обновление значительных по размеру разделов операцион- ной системы, иными словами, установку новой версии ADO нельзя назвать про- стой процедурой. Если вы планируете иметь дело с Access или SQL Server, использование ADO будет для вас предпочтительным. Дело в том, что драйверы, разработанные компа- нией Microsoft для своей собственной базы данных, обладают более высоким ка- чеством, чем драйверы других провайдеров OLE DB. Использование встроенной в Delphi поддержки ADO для баз данных Access является очень неплохим реше- нием. Если вы планируете работать с другим SQL-сервером, вначале проверьте наличие хорошего драйвера, в противном случае вы можете столкнуться с сюрп- ризами. ADO — это достаточно мощная технология, однако вы должны научиться работать с ней. ADO располагается между вашей программой и базой данных. Эта система обеспечивает обслуживание вашего приложения, однако в некоторых си- туациях в рамках ADO генерируются не те команды, которые вы ожидаете. С дру- гой стороны, даже не думайте об использовании ADO, если в будущем собирае- тесь перенести свою программу на другую платформу, — эта технология разработана специально для Microsoft Windows и не доступна в среде Linux и в других опера- ционных системах. Короче говоря, используйте ADO только в случае, если планируете работать только в среде Windows, намерены взаимодействовать с Access или другой базой данных компании Microsoft или обнаружили хорошего провайдера OLE DB, с сер- вером базы данных которого вы собираетесь работать (па данный момент к числу таких серверов нельзя отнести InterBase, а также многие другие серверы SQL). Компоненты ADO (часть пакета, который компания Borland называет dbGo) сгруппированы на странице ADO палитры компонентов. Ключевыми компонента- ми являются ADOConnection (для подключения к базам данных), ADOCommand (для выполнения команд SQL), а также ADODataSet (для выполнения запросов, которые возвращают результирующий набор данных). Существует также три компонента, обеспечивающих совместимость, — ADOTabLe, ADOQuery и ADOStoredProc — эти ком- поненты можно использовать для переноса приложений BDE на платформу ADO. Наконец, компонент RDSConnection позволяет вам обращаться к данным в удален- ных многозвенных (multitier) приложениях. ПРИМЕЧАНИЕ-------------------------------------------------------------- Интерфейс ADO и связанные с ним технологии подробно рассматриваются в главе 15. Имейте в виду, что в настоящее время компания Microsoft постепенно меняет традиционную технологию ADO на версию, основанную на технологии .NET, однако новая версия основана на тех же самых базовых концепциях. Таким образом, использование ADO поможет вам перейти к разработке приложении, основанных на .NET (имейте в виду, что компания Borland также планирует перевести dbExpress на платформу .NET). Собственные компоненты наборов данных Существует еще один альтернативный вариант работы с базой данных: вы можете разработать свой собственный набор компонентов или использовать компоненты, разработанные сторонними производителями. Однако имейте в виду, что разра- 0574
My Base: автономный компонент ClientDataSet 575 ботка собственных компонентов наборов данных является одной из самых слож- ных задач программирования в среде Delphi. Подробнее об этом рассказывается в главе 17. Прочитав этот материал, вы также узнаете о внутреннем устройстве класса Т DataSet. MyBase: автономный компонент ClientDataSet Если вы хотите разработать в среде Delphi простейшее приложение для работы с базой данных, самый простой способ основан на использовании компонента Client- DataSet, который отображается на содержимое локального файла. В рамках этого подхода способ отображения отличается от традиционного способа отображения данных на локальный файл. В рамках традиционного способа данные читаются из локального файла по одной записи, возможно, при этом используется второй файл, в котором хранятся индексы. В отличие от этого компонент ClientDataSet отобра- жает в память абсолютно всю содержащуюся в файле таблицу и, возможно, струк- туру «основное/подробности» (master/detail). Когда программа начинает работу, весь файл загружается в память, после выполнения обработки данные записыва- ются в файл целиком. ВНИМАНИЕ ------------------------------------------------------- Теперь вам должно быть понятно, почему вы не можете использовать этот подход в многопользова- тельской среде или в среде, где к одному файлу обращаются одновременно несколько приложений. Если две программы или два экземпляра одной и той же программы загрузят файл ClientDataSet в память, а затем произведут модификацию данных, таблица, которая будет записана на диск по- следней, аннулирует изменения, которые были предварительно выполнены другими программами. Поддержка сохранения содержимого компонента ClientDataSet на локальном диске была добавлена в Delphi несколько лет назад для реализации модели Briefcase (Портфель). В рамках этой модели пользователь может загрузить данные с серве- ра базы данных на клиентский компьютер, сохранить некоторые из них, отклю- читься от сервера (например, взяв с собой в поездку портативный компьютер) и, наконец, заново подключиться к серверу для того, чтобы внести сделанные им из- менения в базу данных. Подключение к существующей локальной базе данных Чтобы отобразить компонент ClientDataSet на локальный файл, вы должны настро- ить свойство FileName этого компонента. Для построения минимальной програм- мы (в нашем примере она называется MyBasel) нам потребуется компонент Client- DataSet, ассоциированный с файлом CDS (в подкаталоге Data папки \Program Files\ Common Files\Borland Shared содержится несколько таких файлов), компонент Data- source (подробнее о нем рассказывается позднее) и элемент управления DBGrid. Необходимо ассоциировать компонент ClientDataSet с источником данных DataSource при помощи свойства DataSet источника данных. Затем необходимо подключить 0575
576 Глава 13. Встроенная в Delphi архитектура работы с базами данных DataSource к элементу управления DBGrid при помощи свойства DataSource элемента DBGrid. Соответствующая структура продемонстрирована в листинге 13.1. После этого включите свойство Active компонента ClientDataSet, и в вашем распоряжении будет программа, отображающая данные из локального файла даже на этапе про- ектирования. Листинг 13.1. DFM-файл для программы MyBasel object Forml TForml ActiveControl = DBGridl Caption - 'MyBasel' OnCreate = FromCreate object DBGridl: TDBGrid DataSource - DataSourcel end object DataSourcel TdataSource DataSet = cds end object cds: TCIlentDataSet FileName = 'C \Program FilesXCommon FilesXBorland Shared\Data\customer cds' end end ^<МуВче1 Kauai Dive Shoppe Urusco DataSmiraeljht Civet 1354 Cayman Divers World Unlimited Tom Sawyer Diving Centre Blue Jack Aqua Center «Й4 VIP Civets Club 1510 Ocean Paradise 1513 Fantastique Aquatica 1551 Marmot Divers Club 1560 The Depth Charge 1563 Blue Sports 1624 Makai SCUBA Club 1645 Action Club 1651 Jamaica SCUBA Centre 1680 Island Finders 1984 Adventure Undersea 211B Blue Sports Club 2135 Frank's Divers Supply |а<Иг! Z ' 4 376 Sugarloaf Hwy P0 8oxZ547 1 Neptune Lane PO Box 541 632-1 Third Frydenhq 23 738 Paddington Lane 32 Main St PO Box 6745 Z32 999 Й12А77АА 872 Queen St 15243 Underwater Fwy 203 12th Ave Box 746 PD Box 8534 PO Box 5451 -F PDBox68 61331 /3 Stone Avenue P0 Box 744 63365 Nez Peice Street 1455 North 44th St |А<Иг2 Suite 1П i Suite 310 Рис. 13.1. Пример локальной таблицы, которая активна на этапе проектирования в среде Delphi IDE Если вы внесете в таблицу изменения и завершите работу приложения, данные будут сохранены в файле автоматически. (Чтобы уменьшить объем записываемых на диск данных, вы можете отключить журнал изменений — об этом рассказывает- ся позже.) Компонент набора данных обладает также методами SaveToFile и Load- Form File, которыми вы можете воспользоваться в своем коде. Я также добавил в программу следующее изменение: я деактивировал Client- DataSet на этапе проектирования для того, чтобы избежать включения всех его даН" ных в файл DFM программы и в откомпилированный исполняемый файл. Я хочу- чтобы данные хранились в отдельном файле. Чтобы добиться этого, на этапе пр0' 0576
MyBase: автономный компонент ClientDataSet 577 ектирования, выполнив тестирование, деактивируйте набор данных и в обработ- чик события OnCreate формы добавьте строку, осуществляющую загрузку файла: procedure TForml.FormCreatelSender TObject), begin cds.Open; end: От динамической библиотеки Midas к модулю MidasLib Чтобы запустить приложение, использующее компонент ClientDataSet, вы должны установить в системе динамическую библиотеку midas.dll. На эту библиотеку ссы- лается файл DSIntf.pas. Код компонента ClientDataSet не является частью VCL, по- этому в комплекте исходного кода библиотеки VCL этот код отсутствует. Для мно- гих разработчиков это является большим разочарованием, так как они привыкли использовать исходный код VCL в процессе отладки своих программ, а также ис- пользовать его в качестве справочника по внутреннему устройству VCL. ВНИМАНИЕ --------------------------------------------------------------- Обратите внимание, что в имени библиотеки midas.dll отсутствует номер версии. Таким образом, если на компьютере установлена более старая версия этой библиотеки, ваша программа, скорее всего, заработает, однако не исключено, что ее поведение будет некорректным. Библиотека Midas — это библиотека, написанная на языке С, однако начиная с версии Delphi 6 ее можно включить внутрь исполняемого файла вашей програм- мы. Для этого в программу необходимо включить модуль MidasLib (специальный модуль DCU, генерируемый компилятором С). В этом случае вы сможете обой- тись без дополнительного DLL-файла. Форматы XML и CDS Компонент ClientDataSet поддерживает два формата файлов: собственный формат CDS и формат, основанный на XML. Упомянутая ранее папка Borland Shared\Demo содержит демонстрационные версии нескольких таблиц в каждом из двух форма- тов. По умолчанию MyBase сохраняет наборы данных в формате XML. Метод SaveToFile принимает параметр, позволяющий вам указать формат, в котором будет выполнена запись. Метод LoadFromFile срабатывает автоматически для обоих фор- матов. Данные в формате XML могут быть доступны для других приложений, поддер- живающих работу с XML, кроме того, файлы в формате XML можно отредактиро- вать при помощи любого текстового редактора. Однако, с другой стороны, формат KML подразумевает преобразование данных из внутреннего формата. Формат CDS °чень близок к внутреннему формату представления данных в оперативной памя- ти, которое используется компонентом ClientDataSet вне зависимости от файлово- Го формата, поэтому формат CDS обеспечивает более высокую производительность. Кроме того, файлы в формате XML основаны на тексте, поэтому они в среднем приблизительно в два раза больше по размеру файлов CDS. 0577
578 Глава 13. Встроенная в Delphi архитектура работы с базами данных СОВЕТ----------------------------------------------------------------------— Когда данные компонента ClientDataSet находятся в памяти, вы можете извлечь XML-представление этих данных, обратившись к свойству XMLData этого компонента. Этот прием продемонстрирован в следующем примере. Создание новой локальной таблицы Компонент ClientDataSet позволяет не только читать данные из локального файла, но и с легкостью создавать в памяти новые таблицы. Для этого необходимо вос- пользоваться свойством FieldDefs этого компонента, при помощи которого вы мо- жете определить структуру таблицы. Определив структуру, вы можете физически создать файл для таблицы. На этапе разработки для этого достаточно воспользо- ваться командой Create DataSet в контекстном меню компонента ClientDataSet в Delphi IDE. На этапе исполнения следует обратиться к методу CreateDataSet компонента ClientDataSet. Далее приведен фрагмент DFM-файла для программы MyBase2, в котором определяется новая локальная таблица: object ClientOataSetl TCIientDataSet FileName = 'mybase2.cds' FieldDefs = < item Name = 'one' Datatype = ftString Size = 20 end item Name = 'two' DataTYpe = ftSmallint end> StoreDefs = True end Обратите внимание на свойство StoreDefs, которое автоматически становится равным True, когда вы редактируете коллекцию определений полей таблицы. По умолчанию набор данных в Delphi загружает метаданные перед открытием. Ло- кальные метаданные используются только в случае, если это определение сохра- нено в DFM-файле (хранение определения полей таблицы в DFM-файле оказы- вается полезным также для кэширования этих метаданных в клиент-сервернои среде). Далее приводится исходный код обработчика события OnCreate для класса фор- мы рассматриваемой нами программы. В этом обработчике выполняется создание набора данных, отключение журнала (об этом позже) и отображение XML-версии изначальных данных в элементе управления Мето: procedure TForml FormCreate(Sender TObject). begin if not FileExists (cds.FileName) then cds CreateDataSet: cds.Open. cds.MergeChangeLog: cds.LogChanges = False, Memol.Lines.Text .= StringReplace ( 0578
MyBase: автономный компонент ClientDataSet 579 Cds.XMLData. '>' + sbneBreak. [rfReplaceAl 1 ]); end: В самом конце метода происходит обращение к функции StringReplace, которая выполняет примитивное форматирование кода XML: после каждого тега XML (точ- нее говоря, после завершающего символа «>») в текст добавляется символ новой строки. На рис. 13.2 показан XML-код таблицы с несколькими записями. Подроб- ная информация о работе с XML в среде Delphi содержится в главе 22. yplyBaseZ I -iDi X] <?xml version="l 0" standalone=''yes"?> <DATAPACKET Version="2 0"> <МЕТАОАТА> <FIELDS> <FIELD c/lrname="one" fieldtype="stnng" WIDTH="20"/> <FIELD attrname="two" heldtype="i2"/> </FIELDS> <PARAMS/> </METADATA> <R0WDATA> <R0W one="one" two="1"/> <R0W one="two" two="2"/> <R0W one="me" two-"10"/> </R0WDATA> </DATAPACKET> Рис. 13.2. Программа MyBase2 отображает содержимое CDS-файла в формате XML. Структура таблицы определяется программой, которая в момент первого запуска создает файл для набора данных Индексация Когда набор данных (ClientDataSet) находится в памяти, вы можете выполнять с ним множество различных операций. Самыми простыми являются индексация, фильтрация и поиск записей. К более сложным операциям относятся группиров- ка, определение агрегатных значений и управление журналом изменений. Внача- ле я расскажу о самых простых операциях, а затем, ближе к концу главы, перейду к рассмотрению более сложных. Чтобы проиндексировать набор данных ClientDataSet, достаточно установить значение свойства IndexFieldNames. Зачастую это выполняется в момент, когда пользователь делает щелчок на заголовке столбца компонента DBGrid (при этом Управление передается обработчику OnTitleClick). Именно так выполняется индек- сация в рассматриваемой программе MyBase2: Procedure TForml OBGridlT-itleClick(Co1umn- Tcolumn): begin cds.IndexFieldNames = Column.Field FieldName. end, В отличие от других локальных баз данных компонент ClientDataSet поддержи- вает динамические индексы подобного типа без необходимости какого-либо до- полнительного конфигурирования базы данных, так как в данном случае индексы Динамически формируются прямо в памяти. 0579
580 Глава 13. Встроенная в Delphi архитектура работы с базами данных СОВЕТ--------------------------------------------------------------------------. Компонент поддерживает также индексы, основанные на поле с вычисляемым значением, точнее говоря, на поле с внутренне вычисляемым значением, доступным только для конкретного набора данных (я подробнее расскажу об этом позднее в данной главе). В отличие от обычных полей с вычисляемыми значениями (значение поля вычисляется каждый раз при обращении к записи), зна- чения, вычисляемые внутренне, вычисляются только один раз, а затем просто хранятся в памяти. По этой причине механизм индексации рассматривает такие поля как обычные. Помимо использования свойства IndexFieldNames для определения нового ин- декса вы можете воспользоваться также свойством IndexDefs. Этот способ позво- ляет вам создать несколько индексов, держать их в памяти и в случае необходимо- сти быстро переключаться с одного на другой. СОВЕТ------------------------------------------------------------------------— Создание дополнительного индекса — это единственный способ получить индекс в порядке умень- шения значений (по умолчанию создается индекс в порядке увеличения значений). Фильтрация Как и любой другой набор данных, компонент ClientDataSet поддерживает свой- ство Filter, которое позволяет игнорировать часть записей, содержащихся в наборе данных, в соответствии с некоторым признаком. Следует иметь в виду, что опера- ция фильтрации выполняется в памяти сразу же после загрузки всех записей; ина- че говоря, фильтрация — это способ представить пользователю меньше данных, чем есть в базе, однако, выполняя фильтрацию, вы вовсе не сокращаете объем па- мяти, необходимый для хранения локального набора данных. Если в рамках клиент-серверной архитектуры в ответ на запрос вы получаете с сервера некоторый объем данных, вы должны постараться сформулировать зап- рос таким образом, чтобы объем результирующих данных не был бы слишком боль- шим. Для этой цели фильтрацию рекомендуется выполнять на стороне сервера. Если речь идет о локальных данных, рекомендуется разделить большое количе- ство записей между несколькими файлами, благодаря этому вы сможете загружать только те из них, которые вам необходимы. По сравнению с другими наборами данных механизм локальной фильтрации, поддерживаемый компонентом ClientDataSet, позволяет использовать значительно более сложные выражения фильтрации. А именно вы можете использовать: О стандартные логические операторы и операторы сравнения, например Population > 100 and Area < 1000; О арифметические операторы, например Population / Area < 10; О строковые функции, например Substring(Last_Name, 1, 2) = 'Са'; О функции даты и времени, например Year(Invoce_Date) = 2002; О другие выражения, включая функцию Like, шаблоны и оператор In. Возможности фильтрации набора данных ClientDataSet полностью документи- рованы в файле электронной справки по VCL. Эта информация располагается на странице Limiting what records appear (отображение лишь некоторого подмножества записей), ссылка на которую располагается на странице с описанием свойства FiIter 0580
MyBase: автономный компонент ClientDataSet 581 класса TClientDataSet. Обратиться к этой странице можно также со страницы содер- жания электронной помощи, для этого необходимо выбрать Developing Database Applications ► Using client datasets ► Limiting what records appear. Переход к заданной записи фильтрация позволяет вам сделать видимыми для пользователя программы лишь некоторое подмножество записей, хранящихся в наборе данных, однако часто вы хотите показать пользователю все записи, но при этом начиная с определенного места таблицы. Иначе говоря, вы хотите переместиться к некоторой интересую- щей пользователя записи. Для этого используется метод Locate. Если ранее вы нико- гда не пользовались этим методом, возможно, его описание в файле электронной справки покажется вам запутанным. Идея состоит в том, что вы должны передать методу список полей, которые вы хотели бы просмотреть, и список значений, по одному для каждого поля. Если вы хотите проанализировать только одно поле, значение передается напрямую, как в следующем примере (строка поиска нахо- дится в компоненте EditName): procedure TForml btnLocateClick(sender TObject). begin if not cds Locate (‘LastName’. EditName Text. []) then MessageDlg ("" + EditName Text + not found', mtError, [mbOk], 0). end Если вы намерены выполнить анализ нескольких полей, вы должны передать массив вариантов (variant array), в котором содержится список интересующих зна- чений. Массив вариантов можно создать из массива констант при помощи функ- ции VarArrayOf или при помощи вызова VarArrayCreate. Вот пример: cds Locate (.'LastName FirstName'. VarArrayOf (['Cook' 'Kevin']), []) Наконец, этот же самый метод можно использовать в случае, если вы знаете только часть интересующего вас значения поля. Для этого необходимо добавить флаг IcPartialKey в параметр Options (третий по порядку) вызова Locate. ПРИМЕЧАНИЕ --------------------------------------------------------------- Использование метода Locate оправдано в случае, если вы имеете дело с локальной таблицей, однако этот метод плохо подходит для клиент-серверных приложений. Если вы работаете с SQL- сервером, использование любого аналогичного способа перемещения к заданной записи на сторо- не клиента приводит к тому, что вначале все данные целиком перемещаются с сервера на клиентский компьютер (в большинстве случаев это плохая идея), а затем выполняется поиск интересующей записи. Вместо этого для поиска записи в базе данных SQL следует использовать фильтрующие выражения SQL. Получив с сервера ограниченный набор данных, вы можете воспользоваться мето- дом Locate для перемещения в рамках этого набора. Например, вначале вы можете запросить у сер- вера все записи, связанные с покупателями, расположенными в заданном городе или области, а затем, загрузив с сервера этот ограниченный набор данных, вы можете воспользоваться методом Locate Для поиска покупателей по имени. Подробнее об этом рассказывается в главе 14, которая посвяще- на разработке клиент-серверных систем. Отмена и точка восстановления По мере того как пользователь модифицирует данные, хранящиеся в компоненте HientDataSet, внесенные им изменения сохраняются в области памяти под назва- 0581
582 Глава 13. Встроенная в Delphi архитектура работы с базами данных нием Delta. Почему информация об изменениях хранится отдельно? Почему нельзя вносить изменения непосредственно в таблицу данных компонента ClientDataSet? Благодаря этому упрощается обработка обновлений базы данных в клиент-сер- верной среде. Благодаря такому подходу для обновления базы данных клиентское приложение отправляет на сервер лишь информацию о внесенных пользователем изменениях (как будет показано в главе 14, для этого используются специальные выражения SQL). Если бы изменения вносились непосредственно в таблицу ClientDataSet, для того чтобы обновить базу данных, пришлось бы передавать на сервер все содержимое таблицы целиком. Благодаря тому что компонент ClientDataSet хранит информацию о каждой из модификаций набора данных, вы можете при желании отменить то или иное изме- нение, удалив из области Delta соответствующую запись. Для отмены последней модификации набора данных компонента ClientDataSet используется метод Undo- LastChange. Совместно с этим методом используется параметр FollowChange, который позволяет вам перейти к месту выполнения отмены, — иными словами, клиент- ский набор данных переместится к записи, которая была восстановлена в резуль- тате отмены. Вот код, который вы можете ассоциировать с кнопкой Undo (Отмена) своего приложения: procedure TForml.ButtonUndoClickCSender: TObject): begin cds.UndoLastChange (True): end: Помимо этого компонент ClientDataSet позволяет установить в области Delta некоторое подобие закладки (точка восстановления состояния набора данных). Иначе говоря, вы запоминаете в специальном свойстве (свойство SavePoint) неко- торую позицию журнала (состояние набора данных) и в дальнейшем можете отме- нить все изменения, последовавшие после этого. Имейте в виду, что при использо- вании подобной закладки можно только удалить записи из журнала, но нельзя восстановить записи, которые существовали в журнале до этого. Свойство SavePoint ссылается на одну из существующих позиций журнала, если вы сохраните теку- щую позицию, затем отмените несколько последних изменений, а затем захо- тите восстановить позицию, сохраненную в свойстве SavePoint, у вас ничего не получится. СОВЕТ-------------------------------------------------------------------------— В Delphi 7 операции Undo (Отменить) компонента ClientDataSet соответствует новое стандартное действие. К другим новым действиям относятся Revert (Вернуться) и Apply (Применить), которые необходимы в случае, если компонент соединен с набором данных, ассоциированным с базой дан- ных. Включение и выключение журнала Хранить журнал изменений имеет смысл только в случае, если вы стоите перед необходимостью передать информацию о сделанных изменениях с клиентского компьютера на сервер базы данных. Если вы работаете с локальными данными, хранящимися в файле MyBase, ведение подобного журнала изменений может ока- заться бесполезным, — журнал будет занимать дополнительную память. Чтобы 0582
Использование элементов управления, работающих с данными 583 отключить журнал, воспользуйтесь свойством LogChanges. Однако имейте в виду, что при этом вы потеряете возможность отмены изменений. Поддерживается также метод MergeChangesLog, при обращении к которому все хранящиеся в журнале изменения вносятся в набор данных, после чего журнал изменений очищается. Эта процедура полезна в случае, если вы хотите хранить журнал изменений только в течение одного рабочего сеанса. При обращении к ме- тоду MergeChangesLog изменения заносятся в набор данных, затем набор данных сохраняется в локальном файле. В противном случае изменения будут сохранены в локальном файле наряду с изначальным набором данных. ПРИМЕЧАНИЕ----------------------------------------------------------------- Программа MyBase? отключает журнал. Однако в порядке эксперимента вы можете убрать соответ- ствующий код, то есть оставить журнал включенным. В этом случае обратите внимание на то, что после редактирования данных размер CDS-файла и длина XML-текста увеличатся (к изначальным данным добавится информация о внесенных пользователем изменениях). Использование элементов управления, работающих с данными После того как вы добавили в программу необходимые компоненты доступа к дан- ным, вы можете приступить к построению пользовательского интерфейса, при помощи которого пользователь сможет просматривать, а возможно, и редактиро- вать данные. Среда разработки Delphi поддерживает множество компонентов, которые по своему внешнему виду и функциям напоминают обычные элементы управления, однако предназначены для работы с наборами данных. Например, ком- понент DBEdit аналогичен обычному компоненту Edit, а компонент DBCheckBox со- ответствует обычному компоненту CheckBox. Все подобные компоненты можно обнаружить на странице Data Controls (элементы управления для работы с данны- ми) палитры компонентов Delphi. Все эти компоненты соединяются с источником данных при помощи свойства DataSource. Некоторые из них ассоциируются с полным набором данных, напри- мер DBGrid и DBNavigator. Другие ссылаются на отдельное поле источника данных, Для чего используется свойство Data Field. Как только вы присвоите значение свой- ству DataSource, редактор свойства Data Field отобразит список доступных значений. Имейте в виду, что все элементы управления, работающие с данными, никоим образом не зависят от используемой вами технологии доступа к данным (подра- зумевается, что компонент доступа к данным является производным от класса TDataSet). Благодаря этому усилия, затраченные на разработку пользовательского интерфейса, не окажутся напрасными даже в случае, если вы примете решение сменить технологию доступа к данным. Однако следует иметь в виду, что некото- рые компоненты просмотра и поиска записей, а также элемент управления DBGrid следует использовать только в отношении ограниченных по размеру наборов дан- ных. В частности, не следует использовать их в отношении полного набора базы Данных в клиент-серверной среде. Подробнее об этом рассказывается в главе 14. 0583
584 Глава 13. Встроенная в Delphi архитектура работы с базами данных Данные в компоненте DBGrid Компонент DBGrid — это элемент управления в виде сетки, в ячейках которой ото- бражаются значения полей таблицы. Компонент DBGrid может отобразить все со- держимое набора данных целиком. Этот элемент поддерживает пролистывание и навигацию, кроме того, пользователь может редактировать содержимое сетки. Компонент DBGrid является расширением других входящих в состав Delphi эле- ментов управления категории Grid. Настройка элемента управления DBGrid выполняется при помощи свойства Options (в котором хранятся различные флаги), а также при помощи содержащей- ся в этом компоненте коллекции Columns (Колонки). Компонент DBGrid позволяет пользователю перемещаться по набору данных при помощи полос прокрутки и вы- полнять все основные действия, которые можно выполнить в отношении набора данных. В частности, пользователь может напрямую редактировать данные, вста- вить в таблицу новую запись в заданной позиции (для этого необходимо нажать на кнопку Insert), добавить новую запись в конец таблицы (переместиться к послед- ней записи и нажать на стрелку «вниз»), а также удалить текущую запись (нажать Ctrl+Del). Свойство Columns — это коллекция, при помощи которой вы можете выбрать поля таблицы, которые вы хотите отобразить в составе DBGrid, а также настроить свойства столбца и заголовка (цвет, шрифт, ширина, выравнивание, заголовок и т. п.) для каждого поля. Помимо этого поддерживаются специальные свойства, такие как ButtonStyle и DropDownRows, при помощи которых можно ассоциировать с отдельными ячейками таблицы специализированные редакторы или ниспадаю- щие списки допустимых значений (перечисленных в свойстве Picklist для столбца). DBNavigator и действия, связанные с набором данных DBNavigator — это набор кнопок, используемых для навигации и выполнения дей- ствий в отношении базы данных. Некоторые из кнопок элемента DBNavigator мож- но отключить, для этого необходимо удалить соответствующие элементы из свой- ства VisibleButtons (это свойство является множеством). СОВЕТ----------------------------------------------------------- Если вы используете стандартные действия, вы можете избежать ассоциирования их с конкретным компонентом DataSource. В этом случае действия будут выполняться в отношении набора данных, соединенного с визуальным элементом управления, который в настоящий момент обладает фоку- сом ввода. В этом случае одна и та же панель инструментов может использоваться для управления разными наборами данных, отображаемыми на форме. В подобной ситуации пользователь может легко запутаться. Кнопки позволяют выполнить базовые действия в отношении ассоциирован- ного с этим элементов набора данных. Вы можете с легкостью заменить их соб- ственной панелью инструментов, в особенности, если вы используете компонент ActionList с заранее предопределенными в среде Delphi действиями для баз дан- ных. В этом случае вы получите всю стандартную функциональность, однако раз- личные кнопки будут доступны только в случае, если соответствующие действия 0584
Использование элементов управления, работающих с данными 585 являются допустимыми. Преимущество использования действий состоит в том, что вы можете отобразить кнопки, расположив их так, как вам хочется, перемешав их с другими кнопками приложения и используя несколько различных клиентс- ких элементов управления, включая главное меню и контекстные меню. Текстовые элементы управления работы с данными Существует несколько компонентов, основанных на тексте: О DBText отображает содержимое поля, значение которого не может быть моди- фицировано пользователем. Это ориентированный на работу с данными ана- лог элемента управления Label. Данный элемент управления может оказать- ся чрезвычайно полезным, однако пользователи могут спутать его с обычной меткой; О DBEdit позволяет пользователю редактировать содержимое поля (изменить его текущее значение) при помощи элемента управления Edit. В некоторых ситуа- циях вы можете отключить возможность редактирования и использовать эле- мент DBEdit в качестве замены элемента DBText (в этом случае пользователь бе- зошибочно поймет, что отображаемые компонентом данные извлечены из базы данных); О DBMemo позволяет пользователю просматривать и модифицировать крупный текстовый фрагмент, хранящийся в поле Мето или BLOB (Binary Large Object). Этот компонент напоминает элемент управления Мето и поддерживает пол- ный набор возможностей редактирования, однако весь текст отображается при помощи одного и того же шрифта. Элементы управления, основанные на списке Компоненты этой категории позволяют пользователю выбрать одно из несколь- ких значений заранее предопределенного списка (благодаря этому снижается ве- роятность ошибки ввода). Для этой цели вы можете воспользоваться множеством разнообразных компонентов: DBListBox, DBComboBox и DBRadioGroup. Любой из этих компонентов хранит список строк в свойстве Items, однако они обладают рядом отличий: О DBListBox позволяет выбрать одну из нескольких строк заранее предопределен- ного набора, однако не поддерживает ввода текста с клавиатуры. Количество допустимых вариантов может быть достаточно большим, однако рекомендует- ся, чтобы количество допустимых вариантов не превышало шести-семи эле- ментов, в противном случае список вариантов будет занимать слишком много места на экране; ° DBComboBox поддерживает как выбор из предопределенного списка, так и ввод с клавиатуры нового значения. Стиль csDropDown этого компонента позволяет пользователю ввести с клавиатуры новое значение вдобавок к уже имеющимся в списке. Помимо этого данный компонент занимает на экране меньше места, так как ниспадающий список появляется на экране только в случае необходимости; 0585
586 Глава 13. Встроенная в Delphi архитектура работы с базами данных О DBRadioGroup соответствует многопозиционному переключателю (допускается выбор только одной позиции). Выбрать можно только один из представленных вариантов. Количество альтернатив не должно быть слишком большим, иначе данный элемент управления будет занимать слишком много места. Отображае- мые в составе компонента значения могут быть значениями, непосредственно добавляемыми в базу данных, однако вместо этого вы можете определить неко- торое удобное для вас соответствие. То есть вы можете поставить в соответ- ствие каждому из положений переключателя DBRadioGroup (понятные для че- ловека строки символов, перечисленные в свойстве Items) некоторое значение (численные или символьные коды, перечисленные в свойстве Values), которое будет добавляться в базу данных при выборе соответствующей позиции переключателя. Например, вы можете определить соответствие между име- нами подразделений компании и численными индексами этих подразделе- ний: object DBRadioGroupl- TDBRadioGroup Caption = 'Department' DataField = 'Department' DataSource = DataSourcel Items Strings = ( 'Sales' 'Accounting' 'Production' 'Management') Values.Strings = ( 'Г '2' '3' '4') end. Компонент DBCheckBox отличается от предыдущих трех компонентов тем, что он предназначен для отображения и изменения значения поля Boolean (логичес- кое поле). Его можно рассматривать как список, состоящий всего из двух позиций (существует также неопределенное положение переключателя для полей со зна- чением null). При помощи свойств ValueChecked и Valuednchecked вы можете опре- делить соответствующие значения, которые будут переданы в базу данных. Пример DbAware Программа DbAware демонстрирует использование элементов управления DBCheck- Box и DBRadioGroup с параметрами, рассмотренными в предыдущем разделе. Этот пример не сложнее, чем те, которые мы рассматривали до этого, однако в нем используется форма с элементами управления, ориентированными на работу с от- дельным полем таблицы (ранее мы использовали сетку, в рамках которой отобра- жалось все содержимое таблицы целиком). Внешний вид формы на этапе проек- тирования показан на рис. 13.3. Как и в примере MyBase2, программа DbAware определяет свою собственную структуру таблицы, используя свойство FieldDefs компонента ClientDataSet. Крат- кое описание полей набора данных приведено в табл. 13.1. 0586
Использование элементов управления, работающих с данными 587 ^Workers {Data Aware Demo) ^ddRahdcm Date Record View | GtdVtew | Last Name jDBEdiH £k^ Kame iDBEdit2 fhanch iDBComboBoxI Рис. 13.3. Форма программы DbAware на этапе проектирования Таблица 13.1. Поля набора данных в примере DbAware Имя Тип данных Размер LastName FirstName Department Branch Senior HireDate ftString 20 ftString 20 FtSmallint ftString 20 ftBoolean ftDate В составе программы присутствует код, который заполняет таблицу случайны- ми значениями. Это несложная и достаточно скучная задача, поэтому я не стану рассматривать здесь соответствующий код. Если вам интересно, вы можете взгля- нуть на исходный код примера DbAware самостоятельно. Использование элементов сопоставления значений из нескольких таблиц Если список значений извлекается из другого набора данных, тогда вместо эле- ментов DBListBox и DBComboBox, как правило, удобнее использовать специализиро- ванные компоненты DBLookupListBox или DBLookupComboBox. Эти компоненты ис- пользуются в ситуациях, в которых для некоторого поля вы хотите отобразить значение, соответствующее записи в другом наборе данных. Например, представьте, что вы разрабатываете форму для работы с таблицей заказов. Таблица заказов часто содержит в себе поле, в котором указывается по- рядковый номер заказчика. Однако пользователям будет неудобно работать с по- рядковыми номерами заказчиков, вместо этого они предпочли бы идентифици- ровать заказчиков по имени. Однако в базе данных информация о заказчиках (в частности, их имена) хранится в отдельной таблице, — благодаря этому сведе- ния о заказчиках не дублируются для заказов, сделанных одним и тем же заказчиком. 0587
588 Глава 13. Встроенная в Delphi архитектура работы с базами данных Таким образом, возникает проблема: в рамках одной формы требуется отобразить сведения о заказе, извлеченные из одной таблицы, и имя заказчика, извлеченное из другой таблицы. Если вы имеете дело с локальной базой данных или неболь- шими по размеру таблицами, проблему можно решить при помощи элемента управления DBLookupComboBox. (Данный способ плохо подходит для клиент-сер- верной архитектуры с большими по размеру таблицами, о чем рассказывается в следующей главе.) Компонент DBLookupComboBox может быть соединен одновременно с двумя ис- точниками данных: один источник содержит данные, а второй содержит отобра- жаемые данные. Я построил стандартную форму с использованием файла orders.cds из каталога демонстрационных файлов Delphi. Форма включает в себя несколько элементов управления DBEdit. Вы должны удалить стандартный компонент DBEdit, подключенный к номеру заказчика (Customer Number или CustNo), и заменить его на компонент DBLookup- ComboBox (рекомендуется также соединить его с компонентом DBText, чтобы пол- ностью понять, что происходит). Итак, компонент DBLookupComboBox (и DBText) соединен с компонентом DataSource для заказа и с полем CustNo. Для того чтобы компонент DBLookupComboBox смог отобразить информацию, извлеченную из дру- гого файла (customer.cds), вы должны создать еще один компонент ClientDataSet, ссылающийся на файл, а также еще один новый источник данных. Чтобы программа могла работать, вы должны настроить несколько свойств ком- понента DBLookupComboBox. Вот список подходящих значений: object DBLookupComboBox 1 TDBLookupCornboBox DataField = 'CustNo' DataSource = DataSourceOrders KeyField = 'CustNo' ListField = 'Company CustNo' ListSource = DataSourceCustomer DropDownWidth = 300 end Lookup Customer CuslHo Blue Jack Aqua Center American SCUBA Supply Aquatic Drama 6312 3984 ShpIaAskkl Blue Spoils Blue Spoils Club Catamaran Dive Club PajmertMethod Visa 1563 2118 3054 ShpToCounhj । Hems! at# | €31 987 00 SHjpTaSp Рис. 13.4. Вывод примера CustLookup с элементом управления DBLookupComboBox, отображающим несколько столбцов в ниспадающем списке Первые два свойства, как обычно, определяют основное соединение. Следую- щие четыре свойства определяют поле, используемое для соединения (KeyField), отображаемую информацию (ListField) и вторичный источник (ListSource). Вместо 0588
Компонент DataSet 589 \ того чтобы указывать имя единственного поля, вы можете указать имена несколь- ких полей, как я и сделал в данном примере. В качестве текста, отображаемого элементом DBLookupComboBox, будет использоваться только первое поле, однако если вы присвоите достаточно большое значение свойству DropDownWidth, ниспа- дающий список будет включать в себя несколько столбцов с данными. Подобный вывод показан на рис. 13.4. СОВЕТ------------------------------------------------------------------------- Если вы сделаете так, что свойство IndexFieldNames компонента ClientDataSet, содержащего инфор- мацию о заказах, будет ссылаться на поле Company, в ниспадающем списке компании будут отобра- жаться в алфавитном порядке, а не в порядке номеров заказчиков. Именно так я поступил в данном примере. Графические элементы управления для работы с данными В состав Delphi входит графический элемент управления для работы с данными: DBImage. Этот компонент является расширением компонента Image, он предназна- чен для отображения картинки, сохраненной в поле BLOB базы данных. Подразу- мевается, что база данных использует графический формат, поддерживаемый ком- понентом Im age, например BMP или JPEG (для этого необходимо добавить модуль JPEG в выражение uses). Если у вас есть таблица, в состав которой входит поле BLOB, в котором хранит- ся изображение в одном из поддерживаемых графических форматов, вы можете без труда подключить его к компоненту DBImage. Если же для того, чтобы отобра- зить картинку на экране, графический формат требует выполнения некоторого преобразования, проще будет использовать стандартный компонент Image и напи- сать дополнительный код таким образом, чтобы картинка менялась каждый раз, когда меняется текущая запись. Прежде чем перейти к подробному обсуждению этой и других важных тем, связанных с данными, давайте подробнее рассмотрим класс TDataSet и классы полей наборов данных. Компонент DataSet Вместо того чтобы обсуждать возможности некоторого конкретного набора дан- ных, я планирую вначале потратить некоторое время на рассмотрение универсальных возможностей класса TDataSet, который является базовым для всех производных классов, осуществляющих доступ к данным. Компонент DataSet обладает сложным строением, однако я не собираюсь перечислять все его возможности — я останов- люсь на рассмотрении только основных элементов. Компонент TDataSet обеспечивает доступ к последовательности записей, кото- рые читаются из некоторого источника данных, хранятся во внутреннем буфере (для повышения производительности) и, возможно, модифицируются пользова- телем (в этом случае изменения записываются обратно в место постоянного хра- нения данных). Это достаточно универсальное описание, которое можно исполь- зовать в отношении самых разных типов данных (даже тех, которые не связаны с базами данных), однако существует несколько уточняющих правил. 0589
590 Глава 13. Встроенная в Delphi архитектура работы с базами данных / О В каждый момент времени может быть только одна активная запись Таким образом, если вы хотите обратиться к данным в нескольких разных записях, вы должны последовательно переместиться к каждой из них, прочитать необходи- мые данные, а затем переместиться к следующей записи и т. д Пример подоб- ной процедуры рассматривается в разделе «Перемещение по набору данных* далее в этой главе О Редактировать можно только одну активную запись: вы не можете модифици- ровать несколько записей одновременно (как это допускается в реляционных базах данных). О Вы можете модифицировать данные в активном буфере только после того, как вы явно сообщите системе о том, что вы намерены выполнить редактирование. Для этого используется команда Edit набора данных Вы также можете исполь- зовать команду Insert для создания новой пустой записи. Обе команды следует завершить при помощи команды Post. В следующих разделах я рассмотрю интересные дополнительные элементы на- бора данных, такие как состояние (и события изменения состояния), навигация и позиции записей, а также роль объектов полей В листинге 13 2 содержится перечень публичных методов класса TDataSet (изначальный исходный код отре- дактирован и комментирован) Этот перечень можно считать краткой сводкой воз- можностей компонента TDataSet Далеко не все эти методы используются в повсед- невной работе, однако я все равно решил включить их в листинг. Листинг 13.2. Публичный интерфейс класса TDataSet (в сокращении) TDataSet = classfTComponent IproviderSupport) public //создание уничтожение открытие и закрытие constructor CreateCAOwner TComponent) override destructor Destroy override procedure Open procedure Close property BeforeOpen TDataSetNotifyEvent read FBeforeOpen write FBeforeOpen property AfterOpen TdataSetNotifyEvent read FafterOpen write FafterOpen property BeforeClose TdataSetNotifyEvent read FbeforeClose write FbeforeClose property AfterClose TdataSetNotifyEvent read FafterClose write FafterClose // информация о состоянии function IsEmpty Boolean property Active Boolean read GetActive write SetActive default False property State TdataSetState read Fstate function ActiveBuffer Pchar property IsUmDirectional Boolean read FislIniDirectional write FisUmDirectional default False function UpdateStatus TupdateStatus virtual property RecordSize Word read GetRecordSize property Objectview Boolean read FobjectView write SetObjectView property RecordCount Integer read GetRecordCount function IsSequenced Boolean virtual function IsLinkedTo(DataSource TdataSource) Boolean 0590
\ Компонент DataSet 591 \ // источник данных (DataSource) property DataSource TDataSource read GetDataSource procedure DisableControls procedure EnableControls function ControlsDisabled Boolean // поля включая BLOB подробности вычисляемые и проч function FieldByName(const FieldName string) TField function FindFieldfconst FieldName string) TField procedure GetFieldList(Li st Tlist const FieldNames string) procedure GetFieldNamesfList Tstrings) virtual // этот метод //виртуальный начиная с Delphi 7 property FieldCount Integer read GetFieldCount property FieldDefs TFieldDefs read FfieldDefs write SetFieldDefs property FieldDefList TFieldDefList read FFieldDefLi st property Fields TFields read FFields property FieldList TFieldList read FFieldList property FieldValues[const FieldName string] Variant read GetFieldValue write SetFieldvalue default property AggField TFields read FAggFields property DataSetField TDataSetField read FdataSetField write SetDataSetField property DefaultFields Boolean read FDefaultFields procedure ClearFields function GetBlobFieldDatafFieldNo Integer var Buffer TblobByteData) Integer virtual function CreateBlobStreamfField TField Mode TblobStreamMode) TStream virtual function GetFieldData(Field TField Buffe1" Pointer) Boolean overload virtual procedure GetDetailDataSetsfList TList) virtual procedure GetDetailLinkFieldsfMasterFields DetailFields TList) virtual procedure GetFieldDataCFieldNo Integer Buffer Pointer) Boolean overload virtual function GetFieldData(Field TField Buffer Pointer NativeFormat Boolean) Boolean overload virtual property AutoCalcFields Boolean read FautoCalcFields write FautoCalcFields default True property OnCalcFields TDataSetNotifyEvent read FonCalcFields write FOnCalcFields // позиция перемещение procedure CheckBnowseMode procedure First procedure Last procedure Next procedure Prior function MoveBy(Distance Integer) Integer property RecNo Integer read GetRecNo write SetRecNo property Bof Boolean read FBOF property Eof Boolean read FEOF procedure CursonPosChanged property BefoneScroll TdataSetNotifyEvent read FBeforeScroll write FBefoneScnoll property AftenScroll TdataSetNotifyEvent read FaftenScnoll write FafterScroll продолжение 0591
592 Глава 13. Встроенная в Delphi архитектура работы с базами данных Листинг 13.2 {продолжение) // закладки (bookmarks) procedure FreeBookmarkfBookmark TBookmark) virtual function GetBookmark TBookmark virtual function BookmarkValidfBookmark TBookmark) Boolean virtual procedure GotoBookmarktBookmark TBookmark) function CompareBookmarksCBookmarkl Bookmark2 TBookmark) Integer virtual property Bookmark TBookmarkStr read GetBookmarkStr write SetBookmarkStr // переход к записи function FindFirst Boolean ---- function FindLast Boolean function FindNext Boolean function FindPrior Boolean property Found Boolean read GetFound function Locatefconst KeyFields string const KeyValues Variant Options TLocateOptions) Boolean virtual function Lookupfconst KeyFields string const KeyValues Variant const ResultFields string) Variant virtual // фильтрация property Filter string read FfilterText write SetFilterText property Filtered Boolean read Ffiltered write SetFiltered default False property FilterOptions TFilterOptions read FfilterOptions write SetFilterOptions default [] property OnFilterRecord TfilterRecordEvent read FonFilterRecord write SetOnFilterRecord 11 обновление синхронизация procedure Refresh property BeforeRefresh TDataSetNotifyEvent read FBeforeRefresh write FBeforeRefresh property AfterRefresh TDataSetNotifyEvent read FafterRefresh write FafterRefresh procedure UpdateCursorPos procedure UpdateRecord function GetCurrentRecordfBuffer Pchar) Boolean virtual procedure ResyncfMode TresyncMode) virtual // редактирование вставка публикация и удаление записей property CanModify Boolean read GetCanModify property Modified Boolean read FModified procedure Append procedure Edit procedure Insert procedure Cancel virtual procedure Delete procedure Post virtual procedure AppendRecordtconst Values array of const) procedure InsertRecordfconst Values array of const) procedure SetFieldsfconst Values array of const) // события связанные с редактированием вставкой публикацией и удалением property Beforeinsert TDataSetNotifyEvent read Fbeforelnsert write Fbeforelnsert property AfterInsert TDataSetNotifyEvent 0592
Компонент DataSet 593 \ read FAfterlnsert write FAfterlnsert property BeforeEdit TDataSetNotifyEvent read FBeforeEdit write FBeforeEdit property AfterEdit TDataSetNotifyEvent read FAfterEdit write FAfterEdit property BeforePost TDataSetNotifyEvent read FBeforePost write FBeforePost property AfterPost TDataSetNotifyEvent read FAfterPost write FafterPost property BeforeCancel TDataSetNotifyEvent read FBeforeCancel write FBeforeCancel property AfterCancel TDataSetNotifyEvent read FAfterCancel write FAfterCancel property BeforeDelete TDataSetNotifyEvent read FBeforeDelete write FBeforeDelete property AfterDelete TDataSetNotifyEvent read FAfterDelete write FAfterDelete property OnDeleteErrOr TDataSetNotifyEvent read FOnDeleteError write FOnDeleteError property OnEditError TDataSetNotifyEvent read FOnEditError write FOnEditError property OnNewRecord TDataSetNotifyEvent read FOnNewRecord write FOnNewRecord property OnPostError TDataSetNotifyEvent read FOnPostError write FOnPostError // вспомогательные утилиты function Trans!atefSrc Dest PChar ToOem Boolean) Integer virtual property Designer TdataSetDesigner read FDesigner property BlockReadSize Integer read FblockReadSize write SetBlockReadSize property SparseArrays Boolean read FSparseArrays write SetSparseArrays end Состояние компонента DataSet Работа с набором данных в среде Delphi может выполняться в одном из несколь- ких режимов, которые называют состояниями (state) Текущее состояние опреде- ляется значением свойства State Этому свойству можно присвоить одно из следу- ющих значений О dsBrowse — набор данных находится в нормальном режиме просмотра Этот ре- жим используется для просмотра данных и сканирования записей, О dsEdit — набор данных находится в режиме редактирования, О dslnsert — в набор данных добавляется новая запись Это может произойти при обращении к методам Insert или Append, при перемещении в последнюю строку элемента управления DBGrid или при использовании соответствующей коман- ды компонента DBNavigator, ° dslnactive — набор данных закрыт, ° dsCalcFields — в рамках набора данных выполняется вычисление значения поля (в ходе обращения к обработчику события OnCaLcFields), ° dsNewValue, dsOldVaLue и dsCurValue — набор данных находится в состоянии об- новления кэша, ° dsFilter — указывает на то, что в отношении набора данных устанавливается фильтр (в ходе обращения к обработчику события OnFilterRecord) 0593
594 Глава 13. Встроенная в Delphi архитектура работы с базами данных В простых примерах переход между этими состояниями осуществляется авто- матически, однако важно понимать, что в момент перехода генерируются некото- рые важные события. В частности, каждый набор данных генерирует событие перед изменением состояния и после изменения состояния. Когда программа запраши- вает операцию Edit, компонент генерирует событие BeforeEdit непосредственно пе- ред переходом в режим редактирования (вы можете остановить эту операцию, сге- нерировав исключение). Сразу же после перехода в режим редактирования набор данных принимает событие AfterEdit. После того как пользователь закончит редак- тирование и отдаст команду сохранить данные в базе (для этого используется ко- манда Post), набор данных генерирует событие BeforePost (это сообщение можно использовать для того, чтобы проверить входные данные перед тем, как добавлять их в базу данных). После того как данные успешно переданы в базу данных, гене- рируется событие AfterPost. Более общий способ слежения за изменением состояния набора данных осно- ван на использовании события OnStateChange компонента DataSource. Например, далее приводится код, который отображает в строке состояния текущий статус набора данных: procedure TForml DataSourcelStateChangelSender TObject) var strStatus string: begin case cds State of dsBrowse strStatus = 'Browse' dsEdit strStatus = 'Edit' dslnsert strStatus = 'Insert' else strStatus = 'Other state'. end StatusBar Panels[0] Text = strStatus end. Код реагирует только на три наиболее часто используемых состояния набора данных: состояние просмотра, состояние редактирования и состояние вставки. Другие специальные случаи, включая неактивное состояние, игнорируются. Поля набора данных Ранее я уже говорил о том, что в любой момент времени в наборе данных некото- рая запись считается текущей, или активной. Эта запись сохраняется в буфере, и вы можете выполнять в ее отношении некоторые действия, применяя для этой цели набор универсальных методов. Однако для доступа к данным, содержащимся в этой записи, вы должны использовать объекты полей записи. Компоненты полей (го- воря точнее, экземпляры классов, производных от базового класса TField) играют фундаментальную роль в любом приложении Delphi, ориентированном на работу с базами данных. Каждый объект поля соответствует одному из полей базы ДаН" ных. Элементы управления для работы с данными напрямую соединяются с объек- тами полей. По умолчанию Delphi автоматически создает компоненты TField на этапе вы- ТТПП1ГО1ГПО пч-> ттгтт^ пппгпямма птгптпп г компонент ттяПппя ттяННЫХ ЭТО 0594
Поля набора данных 595 происходит после прочтения метаданных, ассоциированных с таблицей или зап- росом, на который ссылается набор данных. Компоненты полей хранятся в свой- стве Fields набора данных (это свойство является массивом). Вы можете обращать- ся к ним либо по порядковому номеру (прямое обращение к массиву), либо по имени (при помощи метода FieldByName). Каждое поле может быть использовано для чтения или модификации данных, хранящихся в текущей записи, — для этого применяется свойство Value или специализированные свойства представления дан- ных с указанием типа (например, AsDate, AsString или Aslnteger и т. д.): var strName string. begin strName = Cds FieldslO] AsString strName = Cds FieldByNameCLastName') AsString Свойство Value является свойством типа variant, поэтому обращение к специа- лизированным методам представления значения этого свойства в виде определен- ного типа является несколько более эффективным решением. Компонент набора данных также обладает свойством (типа Variant), позволяющим обращаться к зна- чению поля: это свойство называется FieldValues и является свойством по умолча- нию. Свойство по умолчанию (default property) — это такое свойство, имя которого можно не указывать в исходном коде программы. Иными словами, квадратные скобки ставятся непосредственно рядом с именем набора данных: strName = Cds FieldValues ['LastName']. strNAme = Cds ['LastName ’], Создание компонентов полей в момент открытия набора данных является по- ведением по умолчанию, однако при желании вы можете открыть эти компоненты на этапе проектирования. Для этого используется Fields Editor (редактор полей). Чтобы увидеть редактор полей в действии, сделайте двойной щелчок на наборе данных или откройте контекстное меню набора данных и выберше команду Fields Editor Например, вы можете создать поле для столбца LastName таблицы и после этого обращаться к значению этого поля при помощи методов AsXxx соответствую- щего объекта поля: strName = CdsLastName AsString Помимо того что объекты полей используются для доступа к значению поля, каждый такой объект поддерживает свойства для управления визуализацией и ре- дактированием значения поля. В частности, объект поля позволяет определить Диапазон значений поля, маску редактирования, формат отображения, ограниче- ния, действующие в отношении поля, и т. п. Конечно же, набор свойств определя- ется типом поля, то есть классом, к которому принадлежит объект поля. Если вы создали поле, характеристики которого не меняются, вместо того, чтобы писать код, настраивающий значение свойств в процессе исполнения программы, вы мо- нете задать значения свойств на этапе проектирования. (Свойства объекта поля ^ожно настроить, например, в обработчике события AfterOpen набора данных.) примечание------------------------------------------------------------------------- Несмотря на то что редактор Fields Editor напоминает редакторы коллекций Delphi, следует иметь в ИДУ, что поля не являются частью коллекции. Поля — это компоненты, создаваемые на этапе Проектирования, перечисленные в разделе published класса формы и доступные в ниспадающем J^CKe в верхней части Object Inspector. 0595
596 Глава 13. Встроенная в Delphi архитектура работы с базами данныу Открыв редактор Fields Editor для некоторого набора данных, вы обнаружите что он пуст. Чтобы воспользоваться его возможностями, вы должны открыть его контекстное меню или псевдоузел Fields в древовидной иерархии Object TreeView Команда Add (Добавить) является самой простой операцией, которую можно вы- полнить в редакторе полей. По этой команде выполняется добавление нового поля в список полей набора данных. На рис. 13.5 показано диалоговое окно Add Fields (Добавить поля), в котором перечислены все поля, доступные в таблице. Это поля таблицы базы данных, которые пока что отсутствуют в списке полей редактора. Рис. 13.5. Редактор полей с открытым диалоговым окном Add Fields (Добавить поля) Команда Define (Создать) редактора полей позволяет вам создать новое вычис- ляемое поле, сопоставляемое поле (lookup field) или поле модифицируемого типа. В соответствующем диалоговом окне вы можете ввести понятное пользователю имя поля, которое может содержать пробелы. На основании отображаемого имени поля среда Delphi автоматически генерирует внутреннее имя, то есть имя компо- нента, соответствующего полю, — при желании вы можете его отредактировать. После этого выберите тип данных, хранящихся в поле. Если создаваемое вами поле является вычисляемым или сопоставляемым и не является простой копией суще- ствующего поля с переопределенным типом данных, выберите подходящее поло- жение переключателя. Позднее, в разделах «Добавление вычисляемого поля» и «Со- поставляемые поля», я расскажу о том, как создать вычисляемые и сопоставляемые поля. ПРИМЕЧАНИЕ------------------------------------------------------------- Обратите внимание, что компонент TField обладает двумя разными свойствами: Name и FieldName. Свойство Name — это обычное имя компонента. Свойство FieldName — это либо имя столбца таблице базы данных, либо имя, которое вы присвоили вычисляемому полю. Как правило, в свои стве FieldName хранится имя, более понятное для пользователя, которое может содержать в се символы пробела. По умолчанию значение свойства FieldName компонента TField копируется в сво ство DisplayLabel. Вы можете занести в свойство FieldName любой удобный для вас текст. Ломим прочего значение этого поля используется при поиске поля с использованием метода FieldByNam < который входит в состав класса TDataSet. 0596
Поля набора данных 597 Все поля, которые вы добавили или определили с использованием редактора Fields Editor, могут использоваться элементами управления или отображаться в рамках сетки набора данных. Если поле физической базы данных отсутствует в данном списке, оно не будет доступно для элементов управления. Когда вы рабо- таете с редактором Fields Editor, среда Delphi добавляет объявления доступных полей в класс формы в качестве новых компонентов (это похоже на то, как дизай- нер меню — Menu Designer — добавляет в форму компоненты TMenuItem). Компо- ненты класса TField (точнее говоря, подклассов этого класса) являются полями формы, и вы можете работать с этими компонентами напрямую из кода вашей про- граммы: вы можете обращаться к свойствам этих компонентов (читать и изменять значения этих свойств) в процессе функционирования вашей программы. В редакторе Fields Editor вы также можете перетаскивать поля для того, чтобы изменить порядок их взаимного расположения. Порядок расположения полей на- бора данных играет роль в особенности тогда, когда вы отображаете набор данных в виде сетки. В этом случае порядок столбцов сетки будет определяться порядком, в котором расположены поля в окне редактора Fields Editor. СОВЕТ ------------------------------------------------------------------- Кроме того, вы можете перетаскивать поля из редактора на форму — в результате IDE создаст для вас соответствующие визуальные компоненты. Это весьма полезная возможность, которая позво- лит вам сэкономить массу времени при создании форм, предназначенных для работы с базой дан- ных. Использование объектов полей Прежде чем мы перейдем к рассмотрению примера, хочу коротко рассказать вам об использовании класса TField. Не следует недооценивать важности этого компо- нента: несмотря на то, что зачастую программист не работает с этим компонентом напрямую, его роль в приложениях, работающих с базами данных, фундаменталь- на. Как я уже отметил, даже если вы не создаете специальных объектов этого типа, все равно вы можете обратиться к полям таблицы или запроса, воспользовавшись свойством Fields (которое является массивом объектов данного класса), индекси- руемым свойством FieldValues или методом FieldByName. Как свойство Fields, так и метод FieldByName возвращают объект класса TField, поэтому в некоторых ситу- ациях вам придется использовать оператор as для того, чтобы привести получен- ный результат к правильному типу (например, TFloatField или TdateField). Приведе- ние к типу потребуется в случае, если вы планируете работать со свойствами поля, специфичными для конкретного подкласса класса TField. Программа FieldAcc отображает форму с тремя кнопками на панели инструмен- тов, которые позволяют обращаться к различным свойствам поля во время испол- нения программы. Первая кнопка изменяет формат столбца численности населе- ния (популяции). Чтобы осуществить это, вы должны воспользоваться свойством DisplayFormat, которое является специфичным для класса TFloatField: Procedure TForm2 SpeedButtonlCl1ck(Sender. TObject): (cds.FieldByName (.'Population”) as er^ TFloatField).DisplayFormat := 0597
598 Глава 13. Встроенная в Delphi архитектура работы с базами данных Когда вы настраиваете свойства поля, имеющие отношение к вводу или выводу данных, изменения оказывают влияние на каждую из записей в таблице. Напро- тив, когда вы настраиваете свойства, связанные со значением (value) поля, эти из- менения всегда применяются только в отношении текущего поля. Например, что- бы отобразить численность населения текущей страны в окне сообщения (message box), вы можете написать следующее: procedure TForm2 SpeedButton2Click(Sender: TObject): begin ShowMessage (string (cds [Warne']) +': '+ string (cds ['Population'])); end: Для обращения к значению поля вы можете использовать серию свойств, име- на которых начинаются на As, — эти свойства позволят вам работать со значением текущего поля как со значением некоторого удобного для вас типа (если представить значение с использованием некоторого типа не удается, возникает исключение): AsBoolean Boolean. AsDateTime TDateTime: AsFloat: Double. Aslnteger- Longlnt: AsString: string: AsVariant: Variant: Любое из этих свойств может использоваться как для того, чтобы прочитать значение поля, так и для того, чтобы изменить его. Изменение значения поля воз- можно только в случае, если набор данных находится в режиме редактирования. Альтернативный вариант обращения к значению поля предусматривает использо- вание свойства Value, которое обладает типом variant. Большинство других свойств компонента TField, например Alignment, DisplayLabel, DisplayWidth и Visible, служат для управления пользовательским интерфейсом и используются разнообразными элементами управления, такими как DBGrid. В при- мере FieldAcc, если вы щелкнете на третьей кнопке панели инструментов, изменит- ся выравнивание (Alignment) каждого из полей: procedure TForm2.SpeedButton3Click(Sender: TObject): var I- Integer. begin for I := 0 to cds.FieldCount - 1 do cds Fields[I] Alignment = taCenter, end. Это изменение влияет на внешний вид элементов DBGrid и DBEdit (который раз- мещен на панели инструментов и отображает название текущей выбранной стра- ны). Результат показан на рис. 13.6. Иерархия классов, производных от TField Библиотека VCL содержит множество классов, производных от TField. Среда Delphi автоматически использует один из них в зависимости от типа поля в базе данных, с которой вы работаете. На этапе выполнения создание соответствующих объек- тов происходит в момент открытия таблицы, а на этапе проектирования — во вре мя работы с редактором Fields Editor. Полный список подклассов класса TFiel содержится в табл. 13.2. 0598
Поля набора данных 599 "ft field Acce»s Foimat Show Pup Cental f ............Cuba Name [Capital I Continent |Area | Population i Argentina Buenos Aires South America 2777815 32,300,003 Bolivia La Paz South America 1098575 7,300,000 Brazil Brasilia South America 8511196 150 400,000 Canada Ottawa North America 9976147 26 500 000 Chile Santiago South America 756943 13 200 000 Colomba Bagota South America 1138907 33.000 000 ► Cuba Havana Щ North America 114524 10,600,000 Ecuador Quito South America 455502 10.600,000 El Salvador San Salvador North America 20865 5,300,000 Guyana Georgetown South America 214969 800 000 Jamaica Kingston North America 11424 2,500,000 Mexico Mexico City North America 1967180 88,600,000 Nicaragua Managua North America 139000 3,900 000 Paraguay Asuncion South America 406576 4,660 000 Peru Lima South America 1285215 21,600,000 United States of America Washington North America 9363130 249,200.000 jsi- Рис. 13.6. Интерфейс программы FieldAcc после того, как пользователь щелкнул на кнопках Center и Format Таблица 13.2. Подклассы класса TField Подкласс Базовый класс Описание TADTField TObjectField Поле ADT (Abstract Data Type), соответствующее полю объекта в объектной реляционной базе данных TAggregateField TField Агрегатное поле. Используется в компоненте ClientDataSet, о чем рассказывается в главе 14 TArrayField TObjectField Массив объектов в объектной реляционной базе данных TAutoIncField TIntegerField Положительное целое число, соединенное с полем автоинкремента (специальное поле, которому автоматически присваивается отличающееся значение для каждой новой записи) таблицы Paradox. Имейте в виду, что поля AutoInc таблиц Paradox далеко не всегда работают так, как должны (подробнее об этом — в главе 14) TBCDField TNumericField Вещественные числа с фиксированным количеством знаков после запятой TBinaryField TField Этот класс, как правило, не используется напрямую. Он является базовым для следующих двух классов TBIobField TField Бинарные данные без ограничения на размер (BLOB обозначает Binary Large Object — большой бинарный объект). Теоретический максимальный размер — 2 Гбайт TBo°leanField TField Булевское значение WesField TBinaryField Произвольные данные с большим (до 64 К символов), но фиксированным размером l(-urrency Field TFIoatField Значения валют. Диапазон совпадает с типом данных Real 1 DataSetField TObjectField Объект, который ссылается на отдельную таблицу в объектной реляционной базе данных TDateField TDateTimeField Дата ^ateTimeField TField Дата и время I^oatField TNumericField Числа с плавающей точкой (8 байт) . — продолжение •&' 0599
600 Глава 13. Встроенная в Delphi архитектура работы с базами данных Таблица 13.2 {продолжение) Подкласс Базовый класс Описание TFMTBCDField TNumericField (Новый тип данных в Delphi 6.) True Binary Coded Decimal (TBCD) — настоящее бинарно-закодированное десятичное число. Является противоположностью типа TBCDField, который подразумевает преобразование значений BCD в тип Currency. Данный тип автоматически используется только базами данных dbExpress TGraphicField TBIobField Г рафика произвольной длины TGuild Field TStnngField Данное поле представляет собой COM GUID (Globally Unique Identifier) — часть поддержки ADO TIDispatchField TInterfaceField Данное поле является указателем на СОМ-интерфейс IDispatch (часть поддержки ADO) TlntegerField TNumericField Целые числа из стандартного диапазона целых чисел (32 бит) Tinterfaced Field TField Как правило, этот класс напрямую не используется. Это базовый класс полей, в которых в качестве данных содержатся указатели на интерфейсы (IUnknown) TLargelntField TlntegerField Очень большие целые числа (64 бит) TMemoField TBIobField Текст произвольной длины TNumericField TField Как правило, этот класс напрямую не используется. Это базовый класс для всех цифровых классов полей TObjectField TField Как правило, этот класс напрямую не используется. Это базовый класс для полей, обеспечивающих поддержку объектных реляционных баз данных TReferenceField TObjectField Указатель на объект в объектной реляционной базе данных TSmalllntField TlntegerField Целые числа меньшего размера (16 бит) TSQLTimeStampField TField (Новый тип поля в Delphi 6.) Поддерживает представление даты/времени, используемое драйверами dbExpress TStnngField TField Текстовые данные фиксированной длины (до 8192 байт) TTimeField TDateTimeField Время TVarBytesField TBytesField Произвольные данные длиной до 64 К символов. Этот класс очень похож на базовый класс TBytesField TVariantField TField Данное поле соответствует вариантному типу данных (часть поддержки ADO) TWideStringField TString Field Строка в формате Unicode (16 бит на каждый символ) TWordField TlntegerField Целые положительные числа в диапазоне целых чисел (16 бит без знака) В разных базах данных поддерживаются разные наборы типов полей, в особен- ности, если вы имеете дело с объектными реляционными базами данных. Добавление вычисляемого поля Теперь, когда вы познакомились с объектами TField и получили представление о том, как работать с этими объектами на этапе исполнения программы, я предлагаю раС" смотреть пример создания объекта поля на этапе проектирования с использованием редактора Fields Editor. В этом примере я планирую создать вычисляемое (calcu- 0600
Поля набора данных 601 lated) поле. В наборе данных country.cds для каждой страны указаны площадь и чи- сленность населения. Эти данные можно использовать для вычисления плотности населения. Чтобы построить новый пример под названием Calc, выполните следующее. 1. Добавьте на форму компонент ClientDataSet. 2. Откройте редактор Fields Editor. Щелкните правой кнопкой мыши в окне редак- тора и выберите команду Add Field (Добавить поле). После этого выберите неко- торые из полей таблицы (лично я выбрал все поля). 3. Выберите команду New Field (Создать поле) и введите имя и тип данных (значе- ние нового поля будет вещественным, поэтому следует выбрать Float для того, чтобы получить объект класса TFloatField). Результат показан на рис. 13.7. propet ties------------------------------------------------------------------- Mew®' |PopulationDensity Component: [cdsPopuiationDensity • 1 .......................................... ...................... I Jjipe; [Float jy rFietd type ~ ------------------------------------- ' % Г gate gafcuiajed C lookup C"“U»kup definition--------------------------- Г..................."3 Рис. 13.7. Создание вычисляемого поля в примере Calc ВНИМАНИЕ----------------------------------------------------------------------- Имейте в виду, что когда вы создаете компоненты полей на этапе проектирования, поля базы дан- ных, для которых вы не создали компонентов, останутся недоступными для вас не только на этапе проектирования, но и на этапе исполнения. Если на этапе проектирования вы не создали ни одного компонента поля, в момент открытия таблицы программа автоматически создает объекты полей в соответствии с определением таблицы. Однако если на этапе проектирования вами были созданы какие-либо объекты полей, среда исполнения Delphi будет использовать именно эти компоненты, не добавляя к ним какие-либо дополнительные поля. Конечно же, вы должны добавить в программу код, вычисляющий значение нового поля. Этот код необходимо разместить в обработчике события OnCalcFields компонента ClientDataSet. Вот как может выглядеть самая первая версия этого кода: Procedure TForm2 cdsCalcFiel ds(DataSet TDataSet). begin cdsPopulationDensity Value = cdsPopulation Value I cdsArea Value. end, Достаточно ли этого кода? Совсем нет! Представьте, что вы добавили в табли- цу новую запись и не успели присвоить значения полям площади или численнос- ти населения. Представьте, что вы по ошибке занесли в поле площади значение 0. 0601
602 Глава 13. Встроенная в Delphi архитектура работы с базами данных В любом из этих случаев возникнет исключение, и работа программы будет пре- рвана. Чтобы решить проблему, достаточно внутри обработчика перехватывать ис- ключения и в случае ошибки присваивать вычисляемому полю значение 0: try cdsPopulationDensity Value = cdsPopulation Value I cdsArea Value. except on Exception do cdsPopulationDensity Value = 0, end ПРИМЕЧАНИЕ-------------------------------------------------------------------------— В общем случае значение вычисляемого поля вычисляется для каждой записи и повторно вычисля- ется каждый раз, когда запись загружается во внутренний буфер. При этом генерируется событие OnCalcEvents. По этой причине обработчик данного события должен выполняться как можно быст- рее, кроме того, он не должен менять состояние набора данных, обращаясь к другим записям. Существует более эффективная в отношении времени (но менее эффективная в отношении памяти) версия вычисляемого поля, которая называется внутренне вычисляемым полем (internally calculated field). Значение внутренне вычисляемого поля вычисляется только один раз — при загрузке записи в буфер, — результат сохраняется в памяти для будущих запросов. Однако и эта версия кода не является самой лучшей. Лучше всего внутри обра- ботчика проверять, определено ли значение площади и не равно ли оно нулю. Иначе говоря, если вы знаете о предполагаемых условиях возникновения ошибки, лучше напрямую проверять в программе эти условия и избежать использования исключений: if not cdsArea IsNull and (cdsArea Value <> 0) then cdsPopulationDensity Value = cdsPopulation Value I cdsArea Value else cdsPopulationDensity Value = 0 Код метода cdsCalcFields (в каждой из трех версий) напрямую обращается к не- которым полям Это возможно благодаря тому, что вы воспользовались редакто- ром Fields Editor, который автоматически создал соответствующие определения полей. Определения можно увидеть в следующем фрагменте описания интерфейса формы: type TCalcForm = class(TForm) cds TCIlentDataSet cdsPopulationDensity TFIoatField. cdsArea TFIoatField cdsPopulation TFIoatField cdsName TStringField. cdsCapital TStringField, cdsContinent TStringField procedure cdsCalcFields(DataSet TDataset) Каждый раз, когда вы добавляете или удаляете поля с использованием редак- тора Fields Editor, вы можете немедленно видеть эффект ваших действий в сетке, отображаемой на форме (если, конечно, сетка не обладает собственными объекта- ми колонок — в этом случае вы можете не увидеть никаких изменений). Конечно же, на этапе проектирования вы не увидите каких-либо значений в вычисляемом поле — они становятся видимыми только на этапе исполнения, так как получают- ся в результате работы откомпилированного кода Delphi. 0602
Поля набора данных 603 Так как вы определили компоненты для полей таблицы, вы можете использо- вать их для настройки некоторых визуальных элементов сетки. Например, вы мо- жете настроить формат отображения многозначных десятичных чисел. Допустим, вам хочется, чтобы каждые три разряда десятичного числа отделялись при помо- щи символа-разделителя. Для этого при помощи Object Inspector измените значе- ние свойства Di splay Format некоторых компонентов полей на ###,###,###. Результат станет заметен немедленно на этапе проектирования. ПРИМЕЧАНИЕ----------------------------------------------------------------- При отображении значений Delphi использует региональные параметры Windows (Windows Inter- national Settings). Когда Delphi транслирует численное значение в отображаемый на экране текст, в качестве символа-разделителя используется символ, на который указывает константа Thousand- Separator. Благодаря этому вывод программы будет автоматически адаптирован к текущим регио- нальным параметрам. Например, на компьютерах, обладающих итальянской конфигурацией, вместо символа запятой в качестве разделителя будет использоваться символ точки. Закончив с настройкой компонентов и полей таблицы, я перехожу к конфигу- рированию элемента управления DBGrid. Для настройки этого компонента я ис- пользую редактор свойства Columns. Я присваиваю колонке Population Density атри- бут «только для чтения» и перевожу свойство ButtonStyle в режим cdsEllipsis — в результате для редактирования значения этой колонки будет использоваться спе- циальный редактор. В этом режиме, когда пользователь пытается отредактировать ячейку сетки, на экране появляется маленькая кнопка с многоточием. При щелчке на этой кнопке генерируется событие OnEditButtonClick компонента DBGrid (текст сообщения для удобства переведен на русский язык): procedure TCalcForm DBGridIEditButton(Sender TObject), begin MessageDlg (Format ( 'Плотность населения (Я 2n)’#13 + это численность населения (£ On)'#13 + ' разделенная на площадь (Я On) #13#13 + 'Если вы хотите изменить значение этого поля, '#13 + 'вы должны модифицировать одно из полей Population или Area [cdsPopulationDensity AsFloat cdsPopulation AsFloat cdsArea AsFloat]), mtlnformation [mbOK] 0) end В данном случае я не стал разрабатывать специальный редактор и вместо этого Добавил в обработчик код, отображающий на экране сообщение. Чтобы создать специальный редактор, необходимо добавить в программу вторичную форму, при помощи которой пользователь сможет обеспечить специальный ввод данных. Таб- лица с вычисляемым полем показана на рис. 13.8. Сопоставляемые поля Сопоставляемое (lookup) поле — это альтернатива компонента DBLookupComboBox (о котором рассказывалось ранее в данной главе). Сопоставляемое поле отобража- йся в виде ниспадающего списка в составе сетки DBGrid. Ранее уже отмечалось, что Лля добавления возможности выбора одного из нескольких вариантов в элементе 0603
604 Глава 13. Встроенная в Delphi архитектура работы с базами данных DBGrid используется подсвойство PickList свойства Columns. Однако в ячейке сетки DBGrid могут отображаться не сами значения поля, а соответствующие им значе- ния, извлеченные из другой таблицы базы данных. Для этого необходимо при по- мощи Fields Editor добавить в набор данных сопоставляемое (lookup) поле. 1 Calculated FieM -ef *• | h| |Capa»t jCon#n«>l IpepUatan Iai ea I *] Argent ria Buenos Am as South America 32 300 003 2 777 815 11 63 Boivia La Paz South America 7 300 000 1 096 575 6 64 | Btazi Brasilia South America 150 400 000 8511 196 17 67 1 Canada Ottawa North America 26 500 000 9976147 266 d -J СМе Santiago South America 13 200 000 756 943 17 44 Colombia Bagota South America 33 000 000 1 138 907 28 98 Cuba Havana North America 10 600 000 114524 92 56 Ecuador Quito South America 10 600 000 455 502 23 27 £ El Salvador San Salvador North America 5 300 000 20 865 25401 y| Рис. 13.8. Вывод программы Calc. Обратите внимание на колонку Population Density с вычисляемыми значениями и на кнопку с многоточием для обращения к специализированному редактору поля Для примера я разработал программу FieldLookup, которая обладает сеткой для отображения информации о заказах. Каждому заказу ставится в соответствие чис- ленный код, идентифицирующий сотрудника, которому поручено обслуживание этого заказа. Однако работать непосредственно с численными кодами неудобно, поэтому в составе сетки присутствует сопоставляемое (lookup) поле, в котором отображается имя сотрудника, идентифицируемого численным кодом, хранящимся в записи таблицы заказов. Чтобы реализовать подобную функциональность, я до- бавил в программу еще один компонент ClientDataSet и связал его с файлом employee, cds. После этого я открыл редактор Fields Editor для набора данных заказов и доба- вил в набор данных все поля таблицы orders.cds. Я выбрал поле EmpNo и присвоил его свойству Visible значение false, чтобы это поле не отображалось в составе сетки (вы не можете полностью удалить это поле из набора данных, так как оно исполь- зуется для связи между таблицей заказов и таблицей сотрудников). Теперь необходимо создать сопоставляемое поле. Если вы уже проделали все описанные ранее действия, можете открыть редактор Fields Editor для таблицы за- казов и выбрать команду New Field (Создать поле) На экране появится диалоговое окно создания нового поля. Указанные вами здесь значения повлияют на значение свойства нового компонента TField, который вы добавляете в таблицу. Вот DFM- описание значения этих свойств: object cds2Employee TStringField FieldKind = fkLookup FieldName = Employee' LookupDataSet = cds2 LookupKeyFields = 'EmpNo' LookupResultField = 'LastName' KeyFields = 'EmpNo' Size = 30 Lookup = True end Это все, что нужно для того, чтобы заставить работать ниспадающий список (рис. 13.9), кроме того, вы сможете на этапе проектирования просматривать значе- 0604
Поля набора данных 605 ние поля, ссылающегося на другую таблицу. Обратите внимание, что вам не нуж- но выполнять специальную настройку свойства Columns сетки, так как по умолча- нию к полю добавляется кнопка развертывания списка, а открывающийся список по умолчанию обладает длиной 7 позиций с пролистыванием. Однако это не озна- чает, что вы не можете использовать это свойство для того, чтобы самостоятельно выполнить дополнительную настройку визуальных элементов DBGrid. 1 <| -oj > j и ] j v |g| ChderNo (CustNa ^Employee SaleDate |ShpPate 5 «1003 CN 1351 «1004 CN 2156 «1005 CN 1356 «1006 CN 13B0 «1007 CN 1384 Parker Guckenherner Ichida Steadman Ramanathan * 4/12/1888 5/3/138812 00 00 PM — 4/17/13BB 4/18/198B k 4/20/1988 1/21/1988 1200 OOPt 11/6/1994 11/7/1988 1 200 00 Pt 5/1/1988 5/2/19BB К 5/3/1388 5/4/198B 5/11/198B 5/12/198B 5/Ц/198В 5/12/198B 5/1B/1988 5/19/1988 5/19/1988 5/20/1388 «Ю0В CN 15Ю «1003 CN 1513 «1010 CN 1551 «1011 CN1560 «1012 CN 1563 Papadopoulos a Parker Reeves Stansbury Steadman * ,4 j . 1 Рис. 13.9. Интерфейс программы FieldLookup с ниспадающим списком, отображающим значения, извлеченные из другой таблицы Программа обладает еще одной специфической особенностью. Два компонента ClientDataSet и два компонента DataSource размещены не на форме, а в специальном контейнере для невизуальных компонентов. Этот контейнер называется модулем данных (data module), подробнее о нем рассказывается во врезке «Модуль данных для размещения компонентов доступа к данным». Чтобы создать модуль данных, вы можете воспользоваться меню File ► New (Создать ► Файл). Добавив компонен- ты в модуль, вы можете связать их с элементами управления, расположенными на Других формах, при помощи команды File ► Use Unit (Файл ► Использовать модуль). Модуль данных для размещения компонентов доступа к данным Разрабатывая в среде Delphi приложение, работающее с базой данных, вы можете разместить компоненты доступа к данным и элементы управления данными на одной и той же форме. Этот подход хорош в случае, если речь идет об относительно несложной программе. Однако если речь идет о мно- жестве компонентов, множестве источников данных и множестве элемен- тов управления данными, если все эти объекты будут размещены на одной и той же форме, процесс разработки приложения может существенно услож- ниться. Чтобы решить проблему, среда Delphi позволяет создать модуль дан- ных — контейнер для невизуальных компонентов. На этапе проектирования такой контейнер выглядит так же, как обыч- ная форма, однако на этапе выполнения он не отображается на экране и су- ществует только в оперативной памяти. Класс TDataModule является прямым потомком класса TComponent, таким образом, он никак не связан с концепцией продолжение & 0605
606 Глава 13. Встроенная в Delphi архитектура работы с базами данных Модуль данных для размещения компонентов доступа к данным (продолжение) графического окна Windows. Отсюда также следует, что компонент TData- Module можно переносить в другие операционные системы. В отличие от формы, модуль данных обладает лишь немногими свойствами и событиями. По этой причине его можно рассматривать как контейнеры компонентов и методов. Подобно форме или фрейму, модуль данных обладает дизайнером. Сре- да Delphi создает специальный модуль исходного кода, в котором содержат- ся определения класса модуля данных, а также файл определения формы, в котором перечисляются компоненты модуля данных и значения их свойств. Существует несколько причин, по которым рекомендуется использовать модули данных. Самая простая заключается в том, что набор компонентов, размещенных в модуле данных, можно использовать совместно с несколь- кими разными формами. Этот прием продемонстрирован в начале главы 14. Данная методика работает в комбинации с визуальным связыванием форм — возможностью доступа к компонентам другой формы или модуля данных на этапе проектирования. Для этого используется команда File ► Use Unit (Файл ► Использовать модуль). Вторая причина состоит в том, что модули данных отделяют данные от пользовательского интерфейса, в результате улучшается структура разрабатываемого приложения. В рамках Delphi под- держиваются также версии модулей данных, предназначенные для много- звенных (multitier) приложений (удаленные модули данных) и НТТР-при- ложений, работающих на стороне сервера (модули веб-данных). Обработка значений Null с использованием событий объектов полей Помимо нескольких интересных свойств объекты полей обладают несколькими важными событиями. Событие On Validate может быть использовано для выполне- ния операции расширенной проверки корректности значения поля. Это событие следует использовать в случае, если вы хотите обеспечить выполнение сложного правила соблюдения диапазона и ограничений, накладываемых на значение поля. Это событие генерируется перед тем, как данные записываются в буфер записи, в то время как событие OnChange генерируется вскоре после того, как данные запи- сываются в поле. Два других события — OnGetText и OnSetText — могут быть использованы для того, чтобы настроить вывод поля. Эти события позволяют вам использовать эле- менты управления для работы с данными даже в случае, если представление поля, которое вы хотите отобразить на экране, отличается от представления, которое среда Delphi обеспечивает по умолчанию. Примером использования этих событий является обработка значений null. Сер- вер SQL делает различие между пустым значением поля и значением null. Счита- ется, что вместо того чтобы оставлять поле пустым, корректнее хранить в поле зна 0606
Поля набора данных 607 чение null. Однако Delphi по умолчанию использует пустые значения, кроме того, на экране пустое поле выглядит в точности так же, как и поле со значением null. Подобное поведение вполне допустимо для строковых и численных значений, од- нако оно может оказаться неподходящим для дат. Дело в том, что в большинстве случаев сложно подобрать подходящее значение даты по умолчанию, если же вы удаляете содержимое поля даты, ваш ввод может оказаться некорректным. Программа NullDate для дат, значение которых равно null, отображает специ- альный текст. Если пользователь вводит пустую строку, программа заносит в поле даты значение null. Далее приводится исходный код двух обработчиков событий: procedure TForml cdsShipDateGetText(sender: TField: var Text. String. DisplayText: Boolean): begin if Sender IsNull then Text = '<undefined>‘ else Text .= Sender.AsString: end. procedure TForml cdsShipDateSetText(sender TField: const Text: String): begin if Text = '' then Sender.Clear else Sender.AsString •= Text: end; На рис. 13.10 показан пример вывода программы с неопределенными (или null) значениями для некоторых дат поставки. NullDate* [“vi CutfNs jCN 1984 SateDate [2/1/1995 5-hipfrate |<undehned> | Emptt 0138 «1298 CN 2315 «1300 CN 1384 «1302 CN 1231 81305 CN 1356 81309 CN 3615 81315 CN 1651 81317 a 1350 «1355 tt1860 CN 1984 CN 3052 CN 3053 CN 3615 Isaieoi» 1/9/1995 1/10/1995 1/16/1995 1/20/1995 1/22/1995 1/26/1995 2/1/1995 2/1/1995 2/5/1995 2/4/1996 1/9/1995 1/10/1995 1/16/1995 1/20/1995 1/22/1995 1/26/1995 •• undefined4 2/2/1995 2/5/1995 <undehned> Йий '3 1 Emptt 0011 j Emptt 0028 j Emptt 0052 ! Emptt 0065 Emptt 0094 : Emptt 0121 j | Emptt 0138 Emptt 0071 Emptt 0141 Emptt 0009*4 ? ► Рис. 13.10. Благодаря обработке событий OnGetText и OnSetText для поля даты программа NullDates отображает специальный текст для значений null ВНИМАНИЕ ------------------------------------------------------------------ На обработку null-значений в Delphi 6 и 7 может повлиять изменение порядка обработки значений типа Variant. По сравнению с более ранними версиями в последних версиях Delphi сравнение поля, значение которого равно null, с полем, содержащим другое значение, может привести к другому Результату. Об этом уже рассказывалось в разделе «Модули Variants и VarUtils» главы 3. В этом Разделе вы можете узнать, каким образом при помощи глобальных переменных можно настроить эФфект операций сравнения, выполняемых в отношении значений типа Variant. 0607
608 Глава 13. Встроенная в Delphi архитектура работы с базами данных Навигация внутри набора данных Ранее мы уже говорили о том, что в любом наборе данных существует только одна активная запись. Текущую активную запись можно сменить. Это происходит в ответ на действия пользователя или в соответствии с внутренними командами, которые отдаются набору данных. Для перемещения между записями набора данных мож- но использовать методы класса TDataSet (эти методы можно обнаружить в лис- тинге 13.2, в особенности в разделе с комментарием Позиция, перемещение). В част- ности, можно перейти к следующей записи, вернуться к предыдущей, перескочить вперед или назад на заданное количество записей (метод MoveBy), а также напря- мую переместиться к самой первой или самой последней записи набора данных. Все эти операции доступны в компоненте DBNavigator и являются стандартными действиями (actions) для набора данных. Их смысл несложно понять. Существует пара особенностей, связанных с тем, каким образом осуществляет- ся обработка граничных позиций. Попробуйте открыть любой набор данных с под- ключенным к нему навигатором (DBNavigator). Нажимая кнопку Next (Далее), пе- реместитесь к самой последней записи набора. Обратите внимание, что кнопка остается активной даже тогда, когда курсор указывает на самую последнюю за- пись. Иными словами, вы можете нажать на кнопку и тем самым попытаться вый- ти за границы набора данных. Однако если вы попытаетесь это сделать, кнопка перестанет быть активной, а текущая позиция не изменится. Это происходит по- тому, что проверка конца файла (Eof) дает положительный результат только в слу- чае, когда курсор перемещается в специальную позицию после самой последней записи. Однако если вы нажмете на кнопку Last (Последняя), вы немедленно ока- жетесь на самой последней записи набора. Таким же образом обрабатывается и са- мая первая позиция набора (проверка на Bof). Вскоре будет продемонстрировано, что данный подход весьма удобен: вы просматриваете записи набора одну за дру- гой в направлении от начала к концу набора и обнаруживаете конец файла (Eof) только тогда, когда выполнена обработка самой последней записи в наборе. Помимо последовательного перемещения между соседними записями, а также прыжков в начало и в конец набора может возникнуть необходимость перемес- титься к некоторой конкретной записи в наборе. Некоторые наборы данных для этой цели поддерживают свойство RecordCount и позволяют перескакивать к запи- си, расположенной в заданной позиции, при этом используется свойство RecNo (номер записи). Эти свойства можно использовать только для наборов, которые обладают встроенной поддержкой позиций записей. Любые клиент-серверные ар- хитектуры к этой категории не относятся (исключение составляет ситуация, когда вы извлекаете из серверной базы данных абсолютно все записи и размещаете их в локальном кэше, однако в общем случае такая ситуация является крайне неже- лательной). В главе 14 показывается, что из серверной базы данных извлекаются толь- ко те записи, с которыми хочет работать пользователь, иными словами, локальный набор данных не может следить за позицией той или иной записи в базе данных. Существует два альтернативных способа сослаться на некоторую запись в на- боре данных вне зависимости от его типа. О Вы можете сохранить ссылку на текущую запись, а затем, после перемещении по базе данных, используя ссылку, вернуться обратно к этой записи. Для этого 0608
Навигация внутри набора данных 609 используются закладки (bookmarks), представленные двумя классами: TBookmark и более современным TBookmarkStr. Эта методика описывается в разделе «Ис- пользование закладок». О Вы можете перейти к некоторой записи, удовлетворяющей заданному крите- рию. Для этого используется метод Locate. Данный подход отлично срабатывает даже после того, как вы закроете и заново откроете набор данных, — это связано с тем, что вы обращаетесь к набору данных на логическом, а не на физическом уровне. Итоговая сумма значений столбца таблицы До сего момента в рассматриваемых нами примерах пользователь мог просматри- вать содержимое таблицы базы данных и вручную редактировать данные или до- бавлять новые записи. Теперь давайте рассмотрим, каким образом осуществляет- ся изменение данных из программного кода. Ранее мы уже использовали таблицу employee.cds, в которой содержится информация о сотрудниках некоторой компа- нии. В этой таблице присутствует поле Salary, в котором хранится заработная пла- та для каждого из сотрудников. Менеджер компании может пролистать таблицу и изменить зарплату любого сотрудника. Каким образом можно определить об- щую сумму, которую компания тратит на выплаты всем сотрудникам в качестве зарплаты? Что делать, если менеджер желает увеличить (или уменьшить) зарплату каждого сотрудника на 10 процентов? Для решения этих задач в программу необхо- димо добавить код, который будет читать и модифицировать содержимое таблицы. Рассматриваемая далее программа Total обладает кнопками для вычисления общей суммы зарплаты, а также для изменения зарплаты отдельного сотрудника. Программа демонстрирует также использование стандартных действий (actions), связанных с наборами данных. Чтобы посчитать суммарную зарплату всех сотруд- ников, необходимо просмотреть таблицу и прочитать значение поля cdsSalary для каждой из записей: var Total- Double, begin Total = 0. cds First, while not cds EOF do begin Total = Total + cdsSalary Value. cds Next. end, MessageDlg ('Sum of new salaries is' + Format ('Sm'. [Total]), mtlnformation. [mbOk], 0). end Как видно на рис. 13.11, этот код работает, однако он обладает рядом проблем. Прежде всего, в процессе его исполнения указатель текущей записи перемещается в самую последнюю позицию — при этом текущая позиция, с которой работал пользователь, теряется. Вторая проблема заключается в том, что в процессе испол- нения кода система вынуждена множество раз подряд обновлять пользователь- ский интерфейс, это приводит к замедлению работы. 0609
610 Глава 13. Встроенная в Delphi архитектура работы с базами данных goto I Хаи |юо i! Ьм ' £« Ней last ^Williams '~Jqrxi jnoease 7 Sewtb J^aslHame j Baldwin T’ fjstN»cne |janet phone Ext [2 Sate? I 23300 [information Sum of new safeties Рис. 13.11. Программа Total отображает суммарную зарплату всех сотрудников компании Использование закладок Чтобы решить обе проблемы, необходимо отключить обновление интерфейса, за- помнить текущую позицию в наборе данных, а после выполнения операции вос- становить ее. Для запоминания позиции в таблице используется механизм закладок. Закладка (table bookmark) — это специальная переменная, в которой сохраняется позиция записи в таблице базы данных. Традиционный подход Delphi заключает- ся в использовании переменной типа TBookmark. Такая переменная объявляется и инициализируется следующим образом: var Bookmark: TBookmark: begin Bookmark = cds.GetBookmark. После выполнения метода ActionTotalExecute вы можете восстановить позицию и уничтожить закладку. Для этого используются следующие два выражения (их рекомендуется разместить внутри блока finally, чтобы гарантировать их выполне- ние в случае возникновения ошибки): cds GotoBookmark (Bookmark): cds.FreeBookmark (Bookmark); Существует, однако, альтернативный, более удобный и более современный ме- тод: вы можете воспользоваться свойством Bookmark класса TDataset. Это свойство ссылается на закладку, которая очищается автоматически. (Технически свойство реализовано в виде так называемой непрозрачной строки (opaque string). Время жизни такой структуры регулируется в точности так же, как время жизни обыч- ных строк, однако структура не является строкой, поэтому обращение к ее содер- жимому не имеет смысла.) Вот каким образом можно модифицировать предыду- щий код: var Bookmark: TBookmarkStr: begin Bookmark = cds Bookmark: cds Bookmark : = Bookmark. 0610
Навигация внутри набора данных 611 Чтобы избавиться от другого побочного эффекта рассмотренной ранее програм- мы (в процессе ее работы содержимое таблицы автоматически пролистывается), вы можете временно отключить визуальные элементы управления, связанные с таблицей, а затем, после выполнения операции, включить их вновь. Для этого набор данных поддерживает два метода: DisableControls и EnableControls. Первый ме- тод следует вызвать перед тем, как начнет работу цикл while, а второй — после того как будет восстановлена текущая позиция в таблице. СОВЕТ----------------------------------------------------------------------------- Отключение элементов управления набором данных в процессе выполнения длительных операций не только улучшает пользовательский интерфейс, но и в значительной степени ускоряет выполнение программы. Затраты производительности, связанные с обновлением пользовательского интерфей- са, значительно больше затрат, связанных с обработкой данных и выполнением вычислений. Чтобы убедиться в этом, попробуйте закомментировать обращения к методам DisableControls и EnableControls в примере Total и оцените изменение скорости. Следует иметь в виду, что данный подход скрывает в себе опасность. Опасность исходит от ошибок, которые могут возникнуть во время извлечения данных из базы, в особенности, если данные читаются с сервера через сеть. Если в процессе полу- чения данных возникает какая-либо проблема, генерируется исключение. В этом случае элементы управления остаются в деактивированном состоянии, и програм- ма не может продолжить нормальную работу. Чтобы избежать подобной ситуа- ции, вы можете воспользоваться блоком try/finally. Если вы хотите сделать свою программу на 100 процентов надежной, добавьте в нее два вложенных блока try/ finally. С учетом всех рассмотренных изменений результирующий код выглядит следующим образом: procedure TSearchFrom.ActionTotalExecute(Sendert. TObject): var Bookmark: TBookmarkStr: Total: Double. begin Bookmark := cds.Bookmark: try cds.DisableControls. Total = 0: try cds.First: while not cds.EOF do begin Total = Total + CdsSalary.Value; cds.Next: end: • finally cds EnableControls: end finally cds.Bookmark : = Bookmark. end. MessageDlg (‘Sum of new salaries is ' + Format ('tm'. [Total]), mtlnformation, [mbOK], 0): end: 0611
612 Глава 13. Встроенная в Delphi архитектура работы с базами данных ПРИМЕЧАНИЕ------------------------------------------------------------—. Я написал этот код для того, чтобы продемонстрировать вам пример последовательного просмотра содержимого набора данных. Однако существует альтернативный подход вычисления общей суммы значений полей. Этот подход основан на использовании специального выражения SQL. Запрос SQL возвращает вам результат суммирования значений в столбце. Если вы имеете дело с SQL-сервером эффективнее воспользоваться специальным запросом SQL, так как при этом производительность программы будет существенно выше. Повышение производительности связано с тем, что для вы- числения общей суммы программе не надо будет пересылать все необходимые для вычислений данные с сервера на клиентский компьютер: все необходимые вычисления выполняются на серве- ре, а через сеть передается лишь одно результирующее значение. Если вы используете ClientDataSet существует другая альтернатива: подсчет общей суммы для столбца является одной из возможно- стей, поддерживаемых агрегатами (о них рассказывается ближе к концу главы). В данном разделе я рассмотрел универсальное решение, которое будет работать для любого набора данных. Редактирование столбца таблицы Код изменения зарплаты каждого сотрудника похож на только что рассмотрен- ный нами код. Метод ActionlncreaseExecute тоже сканирует таблицу и вычисляет общую сумму всех зарплат. Однако в нем присутствуют два дополнительных вы- ражения. Когда вы увеличиваете зарплату, вы изменяете данные в таблице. Два ключевых выражения располагаются в теле цикла while: while not cds.EOF do begin cds.Edit: cdsSalary.Value := Round (cdsSalary.Value * SpinEditl.Value) / 100: Total := Total + cdsSalary.Value: cds.Next: end: Первое выражение переводит набор данных в режим редактирования, благода- ря этому изменения полей немедленно попадают в таблицу. Второе выражение вычисляет новую зарплату. Для этого значение старой зарплаты умножается на значение компонента Spin Edit (по умолчанию значение этого компонента равно 105) и делится на 100. В результате по умолчанию получается 5-процентное увеличе- ние зарплаты (следует учитывать, что значения в долларах округляются до ближай- шего целого). Используя эту программу, вы можете изменить зарплаты всех сотруд- ников на любое количество процентов при помощи всего одного щелчка на кнопке. ВНИМАНИЕ--------------------------------------------------------------------— Обратите внимание, что набор данных переходит в режим редактирования каждый раз в начале исполнения тела цикла while. Это происходит потому, что в наборе данных операция редактирова- ния не может быть выполнена в отношении нескольких записей одновременно. Операция редакти- рования завершается либо при помощи вызова Post, либо в момент, когда вы переходите к следующей записи (как в рассмотренном коде). После этого, чтобы изменить следующую запись, вы должны снова перевести набор данных в режим редактирования. Настройка элемента DBGrid Большинство элементов управления, связанных с данными, поддерживают лишь небольшое количество настраиваемых свойств. Исключением является компонент DBGrid, который обладает множеством настраиваемых параметров и является бо- 0612
Настройка элемента DBGrid 613 лее мощным, чем может показаться с первого взгляда. В следующих разделах рас- сматриваются некоторые специальные операции, которые можно выполнить с ис- пользованием элемента управления DBGrid. В первом примере демонстрируется отображение сетки, а во втором — использование возможности множественного выбора. Рисование сетки DBGrid Компонент DBGrid неплохо справляется с задачей отображения содержимого таб- лицы, используя конфигурацию по умолчанию. Однако в некоторых ситуациях вы можете захотеть изменить его обычное поведение. Например, вы можете захо- теть выделить цветом ячейки или записи, удовлетворяющие некоторому крите- рию (например, сотрудников со слишком низкой или слишком высокой зарпла- той). Другой пример: вы можете обеспечить специальный механизм отображения значений специальных полей, таких как BLOB, графические поля и поля Мето. Чтобы полностью видоизменить способ отображения данных в сетке компо- нента DBGrid, вы должны присвоить свойству DefaultDrawing этого компонента зна- чение False и написать код обработчика OnDrawColumnCell. Если вы оставите свой- ство DefaultDrawing равным True, компонент отобразит на экране сетку, используя метод по умолчанию, еще до того, как произойдет обращение к обработчику OnDraw- ColumnCell. В этом случае в рамках обработчика вы можете выполнить дополни- тельный вывод, то есть добавить какие-либо новые элементы к тем, которые уже отображены на экране в рамках вывода, выполненного по умолчанию (конечно же, вы можете нарисовать что-либо поверх имеющейся сетки, однако в этом слу- чае пользователь заметит «моргание» изображения). Существует еще один способ: вы можете вначале изменить некоторые из пара- метров отображения сетки DBGrid (например, изменить шрифт или цвет или су- зить экранную область, в рамках которой осуществляется отображение содержи- мого ячейки), азатем вызвать стандартный обработчик DefaultDrawColumnCell. Если вы изменяете границы вывода, вы можете использовать часть ячейки для своего собственного вывода, азатем заполнить оставшуюся часть стандартным выводом. Именно этот способ реализован в рассматриваемой далее программе DrawData. В этом примере элемент управления DBGrid ассоциирован с классической таб- лицей Biolife, в которой содержится информация о рыбах. Вот описание свойств компонента DBGrid: Object DBGridl: TDBGrib Align = alCllent DataSource = DataSourcel DefaultDrawing = False OnDrawColumnCell = DBGridlDrawColumnCell end Обработчик события OnDrawColumnCell вызывается один раз для каждой ячейки сетки. Этот обработчик принимает несколько параметров, включая прямоуголь- ник, ограничивающий ячейку на экране, индекс отображаемого столбца, сам стол- бец (с полем, выравниванием и другими подсвойствами), а также состояние ячей- ки (State). Вот пример кода, который меняет цвет ячейки на красный в случае, если ячейка удовлетворяет некоторому критерию: 0613
614 Глава 13. Встроенная в Delphi архитектура работы с базами данных procedure TForml DBGridlDrawColumnCel 1 (Sender TObject const Rect TRect DataCol Integer Column TColumn State TGridDrawState). begin // цвет шрифта красный в случае если длина превышает 100 if (Column Field = cdsLengthcm) and (cdsLengthcm Aslnteger > 100) then DBGridl Canvas Font Color = clRed // обращение к методу отображения по умолчанию DBGridl DefaultDrawDataCel1 (Rect Column Field State) end Теперь посмотрим, как отображается значение типа Мето, а также графическое поле Для поля Мето вы можете реализовать обработчики событий OnGetText и OnSetText Если событие OnSetText не равно nil, сетка позволит вам даже отредак- тировать поле Мето Далее приводится код двух обработчиков Я воспользовался функцией Trim для того, чтобы удалить непечатные символы, которые заставляют текст выглядеть пустым в процессе редактирования procedure TForml cdsNotesGetText(Sender TField var Text String DisplayuText Boolean) begin Text = Trim (Sender AsString) end procedure TForml cdsNotesSetText(Sender TField const Text String) begin Sender AsString = Text end Чтобы отобразить в составе сетки графическое изображение, проще всего со- здать объект TBitmap, присвоить ему значение графического поля и отобразить кар- тинку в границах Canvas сетки Однако я воспользовался другим способом Я уда- лил графическое поле из сетки (для этого я присвоил свойству Visible значение False) и добавил изображение рыбы в ячейку, в которой отображается имя рыбы Для этого я добавил в обработчик OnDrawColumnCell следующий дополнительный код var Picture TPicture OutRect TRect PictWidth Integer begin // прямоугольник вывода по умолчанию DutRect = Rect if Column Field - cdsCommon_Name then begin // рисуем изображение Picture = TPicture Create try Picture Assign(cdsGraphic) PictWidth = (Rect Bottom - Rect Top) * 2 OutRect Right = Rect Left + PictWidth DBGridl Canvas StretchDraw (OutRect Picture Graphic) finally Picture Free end 0614
Настройка элемента DBGrid 615 // уменьшаем прямоугольник вывода таким образом // чтобы оставить место для графики OutRect = Rect OutRect Left = OutRect Left + PictWidth end // перекрашивание ячеек в красный при lenght > 100 опущено (см выше) // вызов метода отображения по умолчанию DBGridl DefaultDrawDataCel1 (OutRect Column Field State) Как можно заметить, программа отображает изображение в небольшом прямо- угольнике в левой части ячейки, а затем изменяет прямоугольник вывода таким образом, чтобы имя рыбы выводилось в правой части ячейки После этого проис- ходит обращение к стандартному методу отображения содержимого ячейки Ре- зультат работы кода показан на рис 13 12 ! Draw Date Ittiegug: IfWte» IUr-hta»| 1104*1» Я J 90020 Triggerfish Clown Triggerfish Ballistoides conspiallum 50 19 605 j Й 90030 иямям 4*4 Red Emperor Lutjanus sebae 60 23 622 I J 90050 Wrasse Giant Maori Wrasse Cheilinus undulatus 229 90 15Z J 90070 Angelfish Blue Angelfish Pomacanthus nauarchus 30 11 011“^ J 90000 Cod Lunartail Rockcod Vanola louti 00 31 40 1 90090 Scorpionhsh Firefish Pterois volrtans 30 14 960 90100 Butterflyfish Ornate Butterflyfish Chaetodon Ornatissimus 19 7 4003 J 90110 Shark Swell Shark Cephaloscyihum ventnosum ! 82 40 157 J 90120 Ray ф-'* Bat Ray Myfiobatis cahformca 56 22 047 И 90130 Eel California Moray Gymnothorax mordax 150 59 055 J 90140 Cod r***s-^ Ling cod Ophiodon elongatus 15H 59 055 4U Рис. 13.12. Программа DrawData отображает сетку с графическими изображениями рыб Multiple Selection Grid Name . . . ... jCojtment **'' Argentina Buenos Aires South Ame Bolivia La Paz South Ame Brazil Brasilia SouthAme Chile Santiago South Ame Colombia Bagota South Ame R Cuba ! На vans | North ДгпегИ г Ecuador Quito South Ame 5 El Salvador 5 an Salvador (MorlhAriim^B Guyana Jamaica Mexico Nicaragua Paraguay Georgetown Kingston Mexico City Managua Asuncion South Ame North Amer North Amer North Amer South Ame I GefSofeaed I Canada Cuba El Salvadoi Рис. 13.13. Программа MltGnd обладает элементом управления DBGrid, который поддерживает множественное выделение строк таблицы Поддержка множественного выделения В следующем примере мы добавим в элемент DBGrid возможность множественного вЫделения записей В результате пользователь получит возможность выбрать одно- 0615
616 Глава 13. Встроенная в Delphi архитектура работы с базами данных временно несколько строк таблицы (иначе говоря, несколько записей). Добиться этого не сложно, так как для этого достаточно установить флаг dgMultiSelect в свойстве Options элемента DBGrid. Как только вы это сделаете, пользователь сможет, удержи- вая нажатой клавишу Ctrl, выделить несколько строк таблицы одновременно. Ре- зультат показан на рис. 13.13. В любой момент времени в таблице может существовать только одна активная запись, поэтому ссылки на все выделенные записи хранятся в виде списка закла- док в свойстве SelectedRows. Это свойство обладает типом TBookmarkList. Вы можете узнать количество объектов в списке при помощи свойства Count, а также обра- титься к каждой из закладок при помощи свойства Items, которое является масси- вом. Каждый элемент этого списка — это объект типа TBookmarkStr, то есть указа- тель на одну из ячеек таблицы. Этот указатель можно присвоить свойству Bookmark набора данных. ПРИМЕЧАНИЕ ---------------------------------------------------------------------- Тип TBookmarkStr является строковым только для удобства. Хранящиеся в такой строке данные следует рассматривать как закрытые для прямого доступа. Такая строка называется непрозрачной (opaque string). Значение такого объекта не следует рассматривать как постоянное. Например, его нельзя сохранять в файле для будущего использования. Данные, хранящиеся внутри закладки, могут отличаться для разных драйверов и конфигурации индекса. Кроме того, при добавлении или удалении строк в таблице содержимое класса TBookmarkStr перестает быть корректным. Далее приводится код примера MltGrid, который активируется при помощи кноп- ки Get Selected и копирует значение поля Name для выбранных записей в список, расположенный в правой части формы: procedure TForml ButtonlClick(Sender TObject). var I Integer BookmarkList TBookmarkList Bookmark TBookmarkStr. begin // сохраняем текущую позицию Bookmark = cds Bookmark try // очищаем список, расположенный справа от таблицы ListBoxl Items Clear // получаем перечень выделенных строк сетки BookmarkList = DbGridl SelectedRows for I =0 to BookmarkList Count - 1 do begin // для каждой позиции списка перемещаем курсор таблицы // к соответствующей записи таблицы cds Bookmark = BookmarkListti] // добавляем в список значение поля Name (имя) ListBoxl Items Add (cds FieldsByName ('Name') AsString) end finally // перемещаемся к изначальной записи cds Bookmark = Bookmark. end. end. 0616
работа с базами данных с использованием стандартных элементов управления 617 Перетаскивание в сетку Еще одной интересной возможностью, поддерживаемой элементом управления DBGrid, является механизм перетаскивания (Drag and Drop). Перетаскивание из сетки реализуется совсем не сложно: вы знаете о том, какая запись является теку- щей и какой столбец выбран пользователем. Перетаскивание в сетку — значитель- но более сложная задача. Программа называется DragToGrid. В рамках данной программы сетка соединена с набором данных, в котором содержатся сведения о различных странах мира. Кроме того, в программе используется окно редактирования (ListBox), в котором можно ввести новое значение поля, а также метка (Label), которую можно перетащить на одну из ячеек таблицы для того, чтобы модифицировать соответствующее поле. Проблема в том, как определить целевую ячейку. Код состоит всего из нескольких строчек, однако он несколько запутан и требует объяснений. type TDBGHack = class (TDbGrid) end procedure TFormDrag DBGridlDragDrop(Sender Source TObject X, Y Integer) var gc TGridCoord. begin gc = TDBGHack (GbGridl) MouseCoord (x y) if (gc у > 0) and (gc x > 0) then begin DbGridl DataSource DataSet MoveBy (gc у - TDBGHack(DbGridl) Row), DbGridl DataSource DataSet Edit DBGridl Columns Items [gc X - 1] Field AsString = EdirtDrag Text. end DBGndl SetFocus end Первая операция определяет ячейку, над которой была отпущена кнопка мыши. Обладая координатами х и у курсора мыши, вы можете обратиться к защищенно- му (protected) методу MouseCoord, для того чтобы определить столбец и строку в таблице. Если целевая ячейка располагается не в первом ряду (в котором обычно располагаются заголовки) и не в первом столбце (в котором обычно располагается индикатор), программа перемещает указатель на текущую запись, используя раз- ницу между номером строки (дс.у) и текущей активной строкой (защищенное свой- ство Row сетки). На следующем шаге набор данных переводится в режим редакти- рования, извлекается значение поля из целевого столбца (Columns.Itemsfgc.X - 1]. Field) и изменяется его текст. Работа с базами данных с использованием стандартных элементов управления Элементы управления, специально предназначенные для работы с данными, по- зволяют существенно ускорить разработку приложений Delphi. Однако в случае Иеобходимости вместо специализированных элементов управления вы можете 0617
618 Глава 13. Встроенная в Delphi архитектура работы с базами данных ..... — — . - —. — использовать стандартные элементы управления. Это может оказаться необходи- мым, если вы хотите получить полный контроль над процедурами передачи дан- ных между объектами полей и визуальными элементами управления. На мой взгляд, такая необходимостьдюжет возникнуть в редких случаях. В подавляющем количестве ситуаций достаточно выполнить дополнительную настройку специа- лизированных элементов управления и перехватить все необходимые вам сообще- ния о событиях, генерируемые объектами полей. Однако если вы попробуете ра- ботать без использования специализированных элементов управления, вы сможете лучше изучить поведение Delphi по умолчанию. Для разработки приложения Delphi, не использующего специальные элементы управления работы с данными, можно воспользоваться одним из двух подходов: во-первых, можно имитировать стандартное поведение Delphi при помощи своего собственного кода и, в особых случаях, отклоняться от этого поведения; во-вто- рых, можно напрямую направлять запросы базе данных. Первый подход демонст- рируется в примере NonAware, а второй — в примере SendToDb. Имитация стандартного поведения Delphi Чтобы построить приложение, которое не использует специальных элементов уп- равления работы с данными и ведет себя, как стандартное приложение Delphi, вы можете написать обработчики событий для операций, которые выполняются спе- циальными элементами управления автоматически. Говоря конкретнее, в момент, когда пользователь изменяет содержимое визуальных компонентов, вы должны перевести набор данных в режим редактирования, а когда пользователь завершает работу с элементом управления, переключая фокус на другой элемент, вы должны обновить объекты полей набора данных. СОВЕТ--------------------------------------------------------------— Этот подход может оказаться полезным в случае, если вы собираетесь интегрировать в стандартное приложение элемент управления, не поддерживающий работу с данными. Программа NonAware использует набор кнопок, которые соответствуют некото- рым из кнопок элемента управления DBNavigator. Эти кнопки связаны с пятью спе- циальными действиями (actions). Для данной программы я не могу использовать стандартные действия набора данных, так как они автоматически соединяются с источником данных, ассоциированным с элементом управления, который обла- дает фокусом, — этот механизм не срабатывает в отношении стандартных элемен- тов управления, не предназначенных для работы с данными. Можно было бы со- единить источник данных со свойством DataSource каждого действия, однако в данной программе источник данных не используется. В составе программы присутствует несколько обработчиков событий, которые я не употреблял в предыдущих программах, в которых применялись элементы уп- равления, специально предназначенные для работы с данными. Во-первых, нужно отобразить данные текущей записи в визуальных компонентах (как показано на рис. 13.14). Для этого необходимо написать обработчик события OnAfterScroll набо- ра данных. procedure TForml cdsAfterScrol1(DataSet TDataSet). begin 0618
работа с базами данных с использованием стандартных элементов управления 619 EditName Text = cdsName AsString. EditCapital Text = cdsCapital AsString ComboContinent Text = cdsContinent AsString. EditArea Text = cdsArea AsString EditPopulation Text = cdsPopulation AsString end. Non Awrt tiame gapitaf Continent gopulaton I.. insert Рис. 13.14. Интерфейс программы NonAware в режиме просмотра. Программа вручную извлекает данные из набора каждый раз, когда изменяется текущая запись Обработчик события OnStateChange, ассоциированного с элементом управления, отображает состояние таблицы в строке состояния. Как только пользователь на- чинает набирать текст в одном из окошек редактирования или раскрывает ниспа- дающий список, программа переводит таблицу в режим редактирования: procedure TForml EditKeyPress(Sender TObject var Key Char) begin if not (cds State in [dsEdit dslnsert]) then cds Edit end Этот метод соединен с событием OnKeyPress всех пяти компонентов и выглядит фактически точно так же, как обработчик события OnDropDown раскрывающегося списка. Когда пользователь прекращает работу с одним из элементов управления (переводит фокус на другой элемент), обработчик события OnExit копирует дан- ные в соответствующее поле таблицы. Вот пример кода: procedure TForml EditCapitalExit(Sender TObject). begin if (cds State in [dsEdit dslnsert]) then cdsCapital AsString = EditCapital Text end Операция выполняется только в случае, если таблица находится в режиме ре- дактирования, то есть только если пользователь набирает текст в этом или в дру- гом элементе управления. Подобное поведение нельзя считать идеальным, так как программа осуществляет действия даже в случае, если текст в графе редактирова- ния не меняется. Однако эти действия выполняются достаточно быстро, поэтому Данной оплошностью можно пренебречь. Для самой первой графы редактирова- ния можно проверить текст перед копированием его в набор данных. Если графа пустая, следует генерировать исключение: 0619
620 Глава 13. Встроенная в Delphi архитектура работы с базами данных procedure TForml.EditNameExit(Sender: TObject); begin if (cds.State in [dsEdit. dslnsert]) then if EditName.Text <> " then cdsName.AsString := EditName.Text else begin EditName.SetFocus: raise Exception.Create ( 'Undefined Country"); end; \ end: \ Еще один способ тестирования значения поля — воспользоваться событием BeforePost набора данных. Следует помнить, что в данном примере операция пере- носа данных в базу данных выполняется не при помощи специальной кнопки, а в момент, когда пользователь переходит к новой записи или добавляет в таблицу новую запись: procedure TForml cdsBeforePost(DataSet: TDataSet); begin if cdsArea.Value < 100 then raise Exception.Create ('Area too small"); end: В любом случае, вместо того чтобы генерировать исключение, вы можете вос- пользоваться значением по умолчанию. Однако если поле обладает значением по умолчанию, лучше настроить его заблаговременно, благодаря этому пользователь увидит, какое именно значение будет передано в базу данных. Чтобы реализовать это, можно обеспечить обработку события AfterInsert, которое генерируется сразу же после создания новой записи (или воспользоваться событием OnNewRecord): procedure TForml cdsAfter!nsert(DataSet: TDAtaSet): begin cdsContinent := 'Asia'; end: Пересылка запросов в базу данных Вы можете еще в большей степени реконфигурировать интерфейс вашего прило- жения, если решите отказаться от стандартной последовательности операций ре- дактирования, используемых элементами управления работы с данными. Благо- даря использованию этого подхода вы получите полную свободу, однако при этом вы можете столкнуться с некоторыми побочными эффектами (например, ограни- ченные возможности по обеспечению согласованности параллельных операций — об этом я расскажу в главе 14). В рассматриваемом далее новом примере я заменил первую графу редактиро- вания еще одним ниспадающим списком, а вместо прежних кнопок, связанных с опе- рациями таблицы (которые были аналогичны кнопкам компонента DBNavigator), я разместил на форме две специальных кнопки: одна из них извлекает данные из базы, а вторая обновляет данные в базе. Как и предыдущий пример, данная про- грамма не обладает компонентом DataSource. Соединенный с первой кнопкой метод GetData извлекает из базы поля, соответ- ствующие записи, которые идентифицируются в первом ниспадающем списке: 0620
работа с базами данных с использованием стандартных элементов управления 621 procedure TForml.GetData: begin cds.Locate (’Name'. ComboName.Text. [loCaselnsensitive]); ComboName.Text := cdsName.AsString; EditCapital.Text := cds.Capital.AsString: ComboContinent.Text := cdsContinent.AsString; EditArea.Text := cdsArea.AsString; EditPopulation.Text ;= cdsPopulation.AsString; end; Обращение к этому методу происходит в момент, когда пользователь щелкает на кнопке, выбирает один из пунктов ниспадающего списка или нажимает Enter, когда фокус наведен на окошко ниспадающего списка: procedure TForml.ComboNameClicktSender; TObject); begin GetData: end: procedure TForml ComboNameKeyPresstSender: TObject; var Key; Char); begin if Key = #13 then GetData; end; Чтобы программа работала без сбоев, в самом начале работы ниспадающий спи- сок заполняется именами всех стран в таблице: procedure TForml FormCreatetsender: TObject); begin // заполняем список имен стран cds.Open; while not cds.Eof do begin ComboName.Items Add (cdsName AsString): cds.Nect: end: end: Иными словами, ниспадающий список становится инструментом выбора запи- си в таблице (рис. 13.15). Благодаря этому инструменту программа может обой- тись без кнопок навигации. jfe^Send Го Ddt<rfMse .101 xt 5 end kame Capital Continent PopuLator Chde Colombia Cuba Ecuada El Salvador Guyana Jamaica Mexico S3S Atea |756943 Рис. 13.15. В программе SendToDb вы можете использовать ниспадающий список для выбра записи в таблице 0621
622 Глава 13. Встроенная в Delphi архитектура работы с базами данных Помимо этого пользователь может изменить значения, отображаемые элемен- тами управления, и щелкнуть на кнопке Send (Переслать). В зависимости от того является ли операция обновлением или добавлением новой записи, в методе Send- Data выполняются разные участки кода. Для того чтобы определить тип операции выполняется проверка имени страны (к сожалению, программа примитивная, по- этому если вы ввели неправильное имя страны, вы больше не сможете его моди- фицировать): , procedure TForml.SendData: begin // если в поле имени пустота, генерируется исключение if ComboName.Text = " then raise Exception.Create ('Insert the name'): 11 проверить, содержится ли запись в таблице if cds.Locate ('Name', ComboName Text. [loCaselntensive]) then begin // модифицировать обнаруженную запись cds.Edit; cdsCapital.AsString := EditCapital .Text: cdsContinent.AsString := ComboContinent.Text: cdsArea.AsString := EditArea.Text; cds{Population.AsString := EditPopulation.Text; cds.Post; end. el se begin // вставить новую запись cds.InsertREcord ([ComboName.Text. EditCapital.Text, ComboContinent.Text. EditArea Text, Edit.Population.Text]); // добавить в список ComboName.Items.Add (ComboName.Text) end; Группировка и агрегатные значения Мы уже видели, что компонент ClientDataSet может обладать индексом, при помо- щи которого записи базы данных могут быть отсортированы в порядке, отличаю- щемся от порядка, в котором они располагаются в файле базы данных. Если вы создали индекс, вы можете группировать данные с использованием этого индекса. Группа — это набор последовательно расположенных (в соответствии с индексом) записей, для которых индексируемое поле не изменяется. Например, если вы име- ете дело с таблицей почтовых адресов и индексируете эту таблицу в соответствии с именем штата, все почтовые адреса, расположенные в одном штате, будут при- надлежать к одной группе. Группировка В примере CdsCalcs компонент ClientDataSet извлекает данные из уже знакомой вам таблицы Country, где хранится информация о различных странах. Чтобы выпол- нить группировку, необходимо определить индекс и указать уровень группировки для этого индекса: 0622
Группировка и агрегатные значения 623 object ClientDataSetl: TCIlentDataSet IndexDefs = < item Name = 'ChentDataSetllndexl' Fields = ’Continent' GroupingLevel = 1 end> IndexName = 'ChentDataSetllndexl' Когда группировка активна, вы можете сделать ее очевидной для пользовате- ля, отобразив структуру группировки в сетке DBGrid, как показано на рис. 13.16. Все, что вам нужно сделать — это написать обработчик события OnGetText для поля, в соответствии со значениями которого выполняется группировка (в данном слу- чае поле Continent), и отображать текст только в случае, если запись является пер- вой в группе: procedure TForml ClientDataSetlContinentGetTextlSender. TField: var Text: String. DisplayText Boolean): begin if gbFirst in ClientDataSetl GetGroupState (1) then Text := Sender AsString el se Text ; end; Cdt&iics > North America South America .. ........ Mexico Nicaragua El Salvador Cuba Jamaica United Stales of America Canada Paraguay Uruguay Venezuela Peru Argentina Guyana Ecuador Colombia Chile 6razd Mexico City Managua San Salvador Havana Kingston Washington Ottawa Asuncion Montevideo Caracas Lima Buenos Aires Georgetown Quito 6agola Santiago Brasilia 88.600,000 3,900,000 5,300,000 10,600.000 2,500,000 1,967.180 139,000 20,865 114,524 11 424 9,363.130 249,200 000 9,976.147 406.576 176.140 912,047 1,285.215 2,777,615 214,969 455.502 1,138,907 756.943 8,511.196 150,400 000 26.500,000 4,660,000 3,002,000 19.700.000 21.600.000 32 300,003 800.000 10,600 000 33.000 000 13,200,000 IL jj Are* 17,733.865 Poputeion' 683,162,003 Рис. 13.16. Пример CdsCalc демонстрирует, как при помощи небольшого по объему кода можно заставить элемент управления DBGrid визуально отображать группировку, определенную внутри ClientDataSet Определение агрегатных значений Помимо прочего компонент ClientDataSet поддерживает создание агрегатных зна- чений. Агрегатное значение (aggregate), или просто агрегат, — это значение, кото- рое вычисляется на основании набора записей. Например, сумма или среднее зна- чение поля для всей таблицы или группы записей (группу можно определить при 0623
624 Глава 13. Встроенная в Delphi архитектура работы с базами данных помощи механизма, рассмотренного в предыдущем подразделе). Агрегатное зна- чение обновляется каждый раз, когда изменяется одна из записей. В частности общая стоимость комплексного заказа может меняться по мере того, как пользова- тель добавляет в заказ дополнительные пункты. ПРИМЕЧАНИЕ-------------------------------------------------------------- Обновление агрегатных значений выполняется инкрементно. Иначе говоря, при изменении значе- ния всего одной записи система не выполняет пересчет агрегатного значения с использованием абсолютно всех значений. Вместо этого используется информация о внесенных пользователем из- менениях (дельта). Ранее уже отмечалось, что ClientDataSet хранит такую информацию в специаль- ной области памяти до тех пор, пока не будет выполнено обновление базы данных. Например, чтобы обновить сумму значений полей, компонент ClientDataSet вычитает из агрегатного значения старое значение модифицированного поля и добавляет к нему новое значение. В результате вы- полняются всего два вычисления, несмотря на то что в состав агрегатной группы могут входить тысячи записей. Благодаря такому подходу обновление агрегатных значений происходит почти мгновенно. Существует два способа определения агрегатных значений. Во-первых, вы мо- жете использовать свойство Aggregates компонента ClientDataSet. Это свойство яв- ляется коллекцией. Во-вторых, агрегатное значение можно определить при помо- щи редактора полей Fields Editor. В обоих случаях вы определяете агрегатное выражение, присваиваете ему имя и соединяете его с индексом и уровнем группи- ровки (если, конечно, вы не хотите применить агрегатное значение ко всей табли- це). Вот коллекция Aggregates для программы CdsCalcs: object ClientDataSetl TCIlentDataSet Aggregates = < item Active = True AggregateName = 'Count ' Expression = 'COUNT (NAME)' GroupingLevel = 1 IndexName = 'ChentDataSetlIndexl' Visible = False end item Active = True AggregateName = 'TotalPopulation' Expression = ’SUM (POPULATION)' Visible = False end> AggregatesActive = True Обратите внимание, что в самой последней строке кода содержится команда, активирующая поддержку агрегатов. Имейте в виду, что если вы создадите слиш- ком много агрегатных значений, работа программы может существенно замедлить- ся. В этом случае для увеличения производительности можно отключить поддер- жку агрегатов. Альтернативный подход, как я уже сказал, подразумевает использование ре- дактора Fields Editor. В контекстном меню выберите команду New Field (Создать поле) и установите флажок Aggregate (Агрегат). Вот определение агрегатного поля, object Cl?entDataSetl TCIlentDataSet object CllentDataSetllotalArea TAggregateField FieldName = 'TotalArea' 0624
Инфраструктура Master/Detail 625 Readonly = True Visible = True Active = True DisplayFormat = '###.###.###' Expression = 'SUM(AREA)' Group!ngLevel = 1 IndexName = 'ChentDataSetllndexl' end Агрегатные поля отображаются в редакторе Fields Editor в отдельной группе (как показано на рис. 13.17). Обратите внимание, что в данном случае вы исполь- зуете не просто агрегат, а агрегатное поле, значит, вы можете определить формат отображения, а кроме того, соединить поле с элементом управления, поддержива- ющим работу с данными, например D В Edit (именно так сделано в примере CdsCalcs). Гак как агрегат подключен к группе, как только вы выбираете запись из другой tpynnbi, вывод автоматически обновляется. Кроме того, если вы изменяете дан- ные, значение в графе общей суммы немедленно изменяется. FormtxQ , s L -- 1 F | Р 1 Continent Name Capital Area Population hfotaferea Рис. 13.17. В нижней части окна редактора Fields Editor отображается перечень агрегатных полей Если вы хотите использовать обычные агрегатные значения, вам придется на- писать небольшой дополнительный код, как показано в следующем примере (об- ратите внимание, что свойство Value агрегата обладает типом Variant): procedure TForml ButtonlClicktSender TObject): begin Label 1 Caption = 'Area ' + ClientDataSetlTotalArea DisplayText + #13'Population + FormatFloat ('###.###.###'. ClientDataSetl Aggregates [1] Value) + #13'Number ' + IntToStr (ClientDataSetl Aggregates [0] Value). end. Инфраструктура Master/Detail Зачастую приходится иметь дело с таблицами, записи которых находятся в соот- ношении «одна ко многим». Это означает, что одной записи в главной таблице со- ответствует множество более детальных записей во вторичной таблице. Класси- Ческий пример: таблица счетов и таблица товаров, — каждому из счетов первой 0625
626 Глава 13. Встроенная в Delphi архитектура работы с базами данные таблицы может соответствовать один или более товаров из второй таблицы. Дру- гой пример: таблица заказчиков и таблица заказов — один заказчик может сделать несколько заказов. Подобное соотношение между таблицами называется соотно- шением master/detail, или, по-русски, «основное/подробности». Подобные ситуации являются стандартными при разработке приложений ра- боты с базами данных. Среда Delphi обладает встроенной поддержкой инфраструк- туры master/detail. Класс TDataSet обладает свойством DataSource для настройки главного источника данных. Во вторичном (detail) наборе данных это свойство в комбинации со свойством MasterFields используется для того, чтобы соединить текущую запись главного набора данных. Поддержка Master/Detail с использованием ClientDataSet Программа MastDet использует с наборами данных customer (заказчик) и orders (за- казы). Для каждого набора данных я добавил в программу источник данных (Data- Source), а для вторичного набора данных я сохранил в свойстве DataSource ссылку на источник данных, соединенный с первым набором данных. После этого при по- мощи специального редактора свойства MasterFields я установил соответствие между вторичной таблицей и полем главной таблицы. Все зти операции выполнены с ис- пользованием модуля данных (DataModule). Об использовании модулей данных рассказывалось ранее во врезке «Модуль данных для размещения компонентов до- ступа к данным». Далее приводится полный листинг модуля данных, используемого в програм- ме MastDet (я убрал из листинга не имеющие отношения к делу свойства, связан- ные с позицией компонентов): object DataModulel: TDataModulel OnCreate = DataModulelCreate object dsCust: TDataSource DataSet = cdsCustomers end object dsDrd: TDataSource DataSet = cdsOrders end object cdsOrders- TCIlentDataSet FileName = 'orders.cds' IndexFieldNames = 'CustNo' MasterFields = 'CustNo' MasterSource = dsCust end object cdsCustomers: TCIlentDataSet FileName = 'customer.cds' end end На рис. 13.18 показана форма программы MastDet во время ее функционирова- ния. Я разместил элементы управления, связанные с главной таблицей, в верхней части формы, а сетка, связанная с дополнительной (detail) таблицей, размешена в нижней части формы. Работая с программой, для каждой записи в главной та лице вы немедленно видите список связанных с ней записей из второстепенной таблицы. В данном случае в сетке отображается список всех заказов, размещенных 0626
Обработка ошибок при работе с базой данных 627 заказчиком, информация о котором отображается в верхней части формы. Каж- дый раз, когда вы выбираете нового заказчика, в сетке, расположенной внизу, ото- бражаются заказы, связанные с этим заказчиком. !$* tester Detail - 7* Caw 1072 1005 1280 1059 1080 1305 4/20/1988 12/26/1994 2/24/1989 5/5/1989 1/20/1995 4/12/1989 1/21/198812 0000 PI 12/26/1994 2/25/1989 5/6/1989 1/20/1995 1356 4/11/1989 1356 1356 1356 1356 1356 __________________________atet.......;.......:.......млг omSawyet DtvtngCeniie 16321 Thtfd Ftydenhoi Г 'j504 738-3022 29 110 118 109 45 65 FAX A 1504-798-7772 Qty jchfistiamted Рис. 13.18. Программа MastDet во время функционирования Обработка ошибок при работе с базой данных Еще одним важным аспектом программирования баз данных является обработка специфических для БД ошибок специальным образом. Конечно же, можно исполь- зовать для этой цели механизмы по умолчанию, то есть оставить все как есть. В этом случае каждый раз при возникновении ошибки на экране будет отображаться со- общение Delphi о возникшем исключении. Однако, возможно, вы захотите на- писать код, корректирующий ошибку или отображающий более подробное и ин- формативное сообщение. Существует три подхода к обработке ошибок БД в Delphi. О Сформируйте блок try/except вокруг рискованных операций БД. К сожалению, это невозможно в случае, если операция генерируется в результате взаимодей- ствия со специализированным элементом управления для работы с данными. ° Установите обработчик для события OnExcept глобального объекта Application. О Обеспечьте обработку специальных событий набора данных, связанных с ошиб- ками, например OnPostError, OnEditError, OnDeleteErrorn OnUpdateError. Большинство классов исключений в Delphi отображают на экране сообщение °б ошибке. Однако исключения баз данных часто включают в себя коды ошибок, как универсальные, так и специфичные для конкретного SQL-сервера, служебные сообщения, генерируемые SQL-сервером, и т. п. Компонент ClientDataSet добавля- ет в свой класс исключения EDBClient только код ошибки. Далее я покажу, как вы- полнить обработку этого исключения. Эта демонстрация поможет вам выполнить обработку в других случаях. В качестве примера я построил программу, которая отображает подробную ин- формацию об ошибках внутри компонента Мето (ошибки автоматически генери- 0627
628 Глава 13. Встроенная в Delphi архитектура работы с базами данных руются в момент, когда пользователь щелкает на кнопках программы). Чтобы обес- печить обработку всех ошибок, программа DBError устанавливает обработчик со- бытия On Exception для глобального объекта Application. Обработчик события зано- сит некоторую информацию в элемент Мето, отображая подробности, связанные с возникшей ошибкой: procedure TForml.ApplicationError (Sender: TObject: E: Exception): begin i if E is EDBClient then | begi n | Memol.Lines AddCError.' + (E.Message)); Memol.Lines AddCError Code:' + IntToStr(EDBC1lent (E).ErrorCode)). end; else Memol.Lines AddC'Generic Error-' + (E Message)), end; Что далее? В данной главе были рассмотрены примеры доступа к базе данных из приложений Delphi. Я рассказал вам об использовании базовых компонентов работы с данны- ми, а также о том, как разработать приложение работы с базой данных, используя стандартные элементы управления. Также мы с вами рассмотрели внутреннюю структуру класса TDataSet и принципы работы с объектами полей. Я рассказал о со- бытиях и свойствах, поддерживаемых всеми наборами данных, которые могут ис- пользоваться в любом приложении, работающем с базой данных. В большинстве примеров данной главы компонент ClientDataSet использовался для доступа к ло- кальным данным, однако следует иметь в виду, что этот компонент зачастую ис- пользуется в качестве шлюза для доступа к SQL-серверу (в рамках клиент-сервер- ной архитектуры) или для доступа к данным удаленного приложения (в рамках трехзвенной архитектуры). Я рассказал об использовании вычисляемых полей, сопоставляемых полей, агрегатных значений. Я объяснил, как можно выполнить дополнительную настройку элемента DBGrid и сгруппировать записи в соответствии с индексом. В этой главе я не затрагивал каких-либо вопросов, связанных с функциониро- ванием базы данных, так как эти вопросы зависят от типа базы данных и использу- емого вами сервера. Рассмотрению этих вопросов посвящена глава 14. В главе 14 будут подробно рассмотрены проблемы, связанные с разработкой клиент-сервер- ных приложений работы с базами данных на основе библиотеки dbExpress компа- нии Borland. Я также планирую рассмотреть компоненты InterBase и IBX. В следующих главах мы перейдем к рассмотрению технологии ADO, а также компонентов, предназначенных для реализации трехзвенной архитектуры (техно- логия DataSnap, ранее известная как MIDAS). Мы также изучим разработку эле- ментов управления, специализированных для работы с данными, собственных ком понентов наборов данных, а также рассмотрим технологии формирования отчетов. 0628
*1Л Клиент-серверная I^T архитектура с использованием dbExpress В предыдущей главе мы изучили встроенные в Delphi основные механизмы рабо- ты с базами данных. При этом в качестве базы данных мы использовали локаль- ные файлы (для этого мы использовали механизм MyBase и компонент ClientDataSet). Нами не затрагивались вопросы, связанные с какой-либо конкретной технологией базы данных. В этой главе мы перейдем к рассмотрению баз данных, доступ к ко- торым осуществляется при помощи серверов SQL. Мы подробно остановимся на изучении программирования в среде «клиент-сервер» с использованием BDE и новой технологии dbExpress. В рамках одной главы невозможно достаточно под- робно рассмотреть все связанные с этим вопросы, поэтому я, прежде всего, буду рассматривать все эти проблемы с точки зрения разработчика Delphi. В рассматриваемых примерах я буду использовать базу данных InterBase, так как эта RDBMS1 (Relational DataBase Management System), или, иначе говоря, этот SQL-сервер, входит в комплект поставки Delphi Professional и в более старшие ре- дакции Delphi. Кроме того, сервер InterBase является свободно распространяемым, его разработка ведется в рамках концепции открытого исходного кода (позже бу- дет отмечено, что эти два утверждения относятся не ко всем версиям InterBase). Я буду рассматривать работу с InterBase с точки зрения программирования в сре- де Delphi. Это означает, что я не буду углубляться в описание внутренней архитек- туры этой базы данных. Вы должны иметь в виду, что большая часть материала, представленного в этой главе, относится не только к InterBase, но и к другим сер- верам SQL. Таким образом, даже если вы не планируете работать с InterBase, мате- риал главы все равно будет для вас полезным. В главе рассматриваются следующие вопросы: ° обзор архитектуры «клиент-сервер»; ° элементы дизайна базы данных; ° знакомство с InterBase; Q программирование на стороне сервера: представления (views), сохраненные процедуры (stored procedures) и триггеры (triggers); ° Русском языке иногда используют термин «Реляционная СУБД (Система управления базами дан- ных)». — Примеч. перев. 0629
630 Глава 14. Клиент-серверная архитектура с использованием dbExpress О библиотека dbExpress; О кэширование с использованием компонента ClientDataSet; О компоненты InterBase Express (IBX). Архитектура «клиент-сервер» В прикладных программах, рассматривавшихся в предыдущей главе, специализи- рованные компоненты Delphi использовались для доступа к данным, хранящимся в файлах, расположенных на локальном компьютере, при этом весь файл целиком загружался в память. Это экстремальный подход. В рамках более традиционного подхода в память загружаются лишь некоторые из записей, хранящихся в файле базы данных. Благодаря этому с одним и тем же файлом может работать одновре- менно несколько приложений (подразумевается, что при этом используются ме- ханизмы синхронизации записи). Если данные располагаются на удаленном сервере, копирование всей таблицы в память для обработки является чрезвычайно затратной операцией: она требует огромных затрат времени и пропускной способности. В большинстве ситуаций использовать подобный подход совершенно непрактично. Приведу пример. Пред- ставьте себе, что у вас есть таблица EMPLOYEE (часть демонстрационной базы дан- ных InterBase, включенная в комплект поставки Delphi), в которой содержится информация о сотрудниках некоторой компании. Представьте, что в эту таблицу добавили тысячу записей и расположили ее на удаленном компьютере, выполня- ющем функции файлового сервера. Перед вами стоит задача: определить сотруд- ника с самым высоким окладом. Для этой цели вы можете открыть компонент таблицы bdExpress (EmpTable), выбрать все записи и последовательно перебрать их, проверяя значение поля Salary (Оклад). Этот подход реализован следую- щим кодом: EmpTable Open. EmpTable First, MaxSalary = 0. while not EmpTable Eof do begin if EmpTable FieldByName ('Salary') AsCurrency > MaxSalary then MaxSalary = EmpTable FieldByName ('Salary') AsCurrency. EmpTable Next, end. В результате выполнения этого кода все записи из таблицы, расположенной на удаленном сетевом компьютере, будут перемещены на локальный компьютер — эта операция может потребовать несколько минут. Можно ли ускорить решение задачи? Для этого надо заставить сервер, на котором хранится таблица, самостоя тельно просмотреть все записи, определить запись с наивысшим окладом и вернуть программе лишь окончательный результат: всего одну запись. Подобную обработ, ку информации выполняет SQL-сервер. Вы можете адресовать ему следуюШиИ запрос: select Max(Salary) from Employee 0630
Архитектура «клиент-сервер» 631 ПРИМЕЧАНИЕ-------------------------------------------------------------- Рассмотренные два фрагмента кода являются частью программы GetMax, которая позволяет опре- делить точное время, необходимое для определения наивысшего оклада с использованием каждого из этих подходов. Используя эту программу, вы можете воочию убедиться в том, что клиент-сервер- ная технология обеспечивает существенно более высокую скорость обработки данных. В рассмат- риваемом случае при использовании компонента Table скорость выполнения операции как минимум в 10 раз ниже, чем при использовании SQL-запроса. Такое соотношение соблюдается даже в слу- чае, если база данных InterBase установлена на том же самом компьютере, на котором вы запуска- ете программу GetMax. Если вы хотите хранить большой объем данных на центральном компьютере и при этом избежать копирования данных для обработки на клиентские компью- теры, единственное решение состоит в том, чтобы заставить центральный компью- тер выполнять основную обработку данных и пересылать на клиентские компью- теры лишь ограниченный набор данных, полученных в результате обработки. Эта идея лежит в основе концепции «клиент-сервер». В рамках этой концепции вы используете уже существующую на сервере про- грамму обработки данных (RDBMS) и разрабатываете специальную клиентскую программу, которая подключается к RDBMS, отдает ей команды и принимает от нее данные. Однако в некоторых ситуациях требуется разработать как программу- клиент, так и программу-сервер — такая необходимость возникает, если вы наме- рены реализовать трехзвенную архитектуру. Встроенная в Delphi поддержка трех- звенных программ (соответствующий механизм ранее назывался MIDAS — Middle-tier Distributed Application Services, однако сейчас он получил новое имя DataSnap) рассматривается в главе 16. Переход от использования локальных файлов к использованию SQL-серверов (в английском языке иногда используется термин upsizing) в основном вызван не- обходимостью увеличить производительность баз данных и обеспечить возмож- ность хранения больших объемов данных. В предыдущем примере, если системе RDBMS посылается SQL-запрос на определение максимального оклада, SQL-сер- вер решает эту задачу за очень короткое время и отправляет клиенту только оконча- тельный результат операции, то есть значение максимального оклада. Если система RDBMS работает на достаточно мощном компьютере (например, на мультипроцессорной станции Sun SparcStation), время, необходимое для обработки запроса, будет минимальным. Однако помимо выигрыша в производительности существуют также другие веские аргументы в пользу применения клиент-серверной архитектуры. ° Клиент-серверная архитектура позволяет клиентам работать с большими объ- емами данных. Размер базы данных может достигать нескольких сотен мега- байтов и даже больше — файлы такого размера не всегда приемлемо хранить на локальном жестком диске клиентского компьютера. ° Клиент-серверная архитектура обеспечивает одновременный доступ к данным Для нескольких пользователей. Базы данных, основанные на SQL-сервере, в большинстве случаев поддерживают оптимистичную блокировку (optimistic locking) — подход, который позволяет нескольким пользователям в одно и то же время работать с одними и теми же данными. Решение конфликтов откла- дывается до момента, когда пользователи отправляют SQL-серверу модифи- цированные данные. 0631
632 Глава 14. Клиент-серверная архитектура с использованием dbExpress О Клиент-серверная архитектура обеспечивает целостность данных, контроль над транзакциями, безопасность доступа, контроль доступа, централизованное ре- зервное копирование и т. д. О Клиент-серверная архитектура под держивает программируемость базы данных, то есть возможность запускать на стороне сервера специальный разработанный программистом код (сохраненные процедуры, триггеры, представления таблиц). Благодаря этому снижается сетевой трафик и нагрузка на клиентские компью- теры. Итак, оценив преимущества клиент-серверной архитектуры, мы можем перей- ти к изучению конкретных методик разработки программ в клиент-серверной сре- де. Основная цель состоит в том, чтобы должным образом распределить нагрузку между клиентом и сервером и уменьшить пропускную способность, необходимую для передачи информации между клиентом и сервером. Чтобы достигнуть этой цели, необходимо обеспечить хороший дизайн базы дан- ных. Хороший дизайн базы данных подразумевает формирование эффективной структуры таблиц, а также обеспечение контроля корректности данных (выполне- ние бизнес-правил). Безусловно, контроль корректности данных должен вы- полняться на стороне сервера, так как целостность базы данных — это одно из важнейших условий, за выполнением которого следит RDBMS. Однако контроль корректности данных должен выполняться также на стороне клиента. Благодаря этому улучшается пользовательский интерфейс и экономятся ресурсы: если при вводе дан- ных пользователь допускает ошибку, эта ошибка обнаруживается на стороне клиен- та, в результате ошибочные данные на сервер не передаются, и пользователь получа- ет возможность исправить ошибку, не создавая лишней нагрузки на сеть и на сервер. Элементы дизайна базы данных Эта книга посвящена программированию на Delphi, а не базам данных, однако, приступая к обсуждению программирования клиент-серверных приложений, не- возможно обойти стороной некоторые вопросы, связанные с дизайном современ- ных баз данных. Причина проста: если дизайн вашей базы данных некорректен или запутан, то для доступа к данным вы будете вынуждены писать либо чрезвы- чайно сложные выражения SQL и комплексный код, работающий на стороне сер- вера, либо огромное количество запутанного кода Delphi. Модель Entity-Relation Классический подход к проектированию реляционных баз данных основан на мо- дели Entity-Relation (E-R), название которой можно перевести как «сущность- отношение». Подразумевается, что в базе данных может храниться информация о нескольких сущностях, проще говоря, об объектах нескольких категорий. МеЖ' ду сущностями могут быть установлены отношения. Для хранения информации о каждой сущности используется отдельная таблица, в которой для хранения каж дого элемента данных выделяется отдельное поле. Кроме того, отдельное поле выделяется для формирования отношения типа «один к одному» и «один ко мно 0632
Элементы дизайна базы данных 633 гим» между двумя сущностями (таблицами). Если вы хотите установить отноше- ния типа «многие ко многим», вам придется создать еще одну отдельную таблицу. Приведу пример отношения типа «один к одному». Представьте себе таблицу, в которой содержится информация о курсах лекций в университете. Для каждого курса в таблице существует отдельная запись. Для хранения каждого элемента данных (наименование, описание, аудитория и т. п.) используется отдельное поле, однако помимо этих полей существует еще одно, которое идентифицирует лекто- ра. Информация о лекторе (имя, фамилия, ученая степень, домашний адрес, теле- фон, оклад и т. п.) должна храниться отдельно от информации о курсах лекций, так как эта информация может использоваться отдельно от информации о лекци- ях. Таким образом, мы получаем две таблицы — таблицу курсов лекций и таблицу лекторов. Между таблицами установлено отношение типа «один к одному» (каж- дому курсу лекций ставится в соответствие один лектор). Это отношение опреде- ляется при помощи специального отдельного поля в таблице курсов лекций. Чтобы продемонстрировать отношение типа «один ко многим», мы добавляем в нашу базу данных еще одно понятие — понятие отдельной лекции. Отдельная лекция — это некоторое количество академических часов в некоторый день недели. Каждому курсу лекций может быть поставлено в соответствие несколько отдель- ных лекций. Информация о лекции (время начала, день недели, продолжитель- ность, номер лекционного зала и т. п.) должна храниться отдельно от информации о курсах лекций. Значит, мы должны создать для этой цели отдельную таблицу. Однако на этот раз мы имеем дело с отношением «один ко многим», то есть одному курсу лекций может соответствовать несколько разных лекций. Чтобы установить подобное отношение, мы можем добавить в таблицу отдельных лекций дополнитель- ное поле, указывающее на курс лекций. Таким образом, несколько разных записей в таблице лекций может указывать на одну и ту же запись в таблице курсов лекций. Еще более сложная ситуация возникает в случае, когда мы хотим сохранить в базе данных информацию о том, какие студенты посещают тот или иной курс лекций. Один студент может посещать несколько курсов (количество курсов не фиксировано). На каждой лекции могут присутствовать несколько студентов (ко- личество студентов не фиксировано). Отсюда следует, что мы не можем хранить информацию о соответствии между студентами и курсами ни в таблице студентов, ни в таблице курсов. Такое отношение является отношением «многие ко многим». Чтобы определить это отношение, мы должны создать дополнительную таблицу, В которой будут храниться ссылки на студентов и на курсы лекций. Правила нормализации Классический дизайн реляционной базы данных базируется на правилах норма- лизации. Эти правила придуманы для того, чтобы избежать дублирования данных в базе. Дублирование данных приводит к дополнительным расходам памяти и дис- кового пространства, кроме того, дублирование может стать причиной нарушения соответствий между данными. Представьте, что вам необходимо хранить инфор- мацию о заказах и заказчиках. Один заказчик может сделать несколько заказов. Представьте, что некоторый заказчик сделал пять заказов. Если вы будете хранить Информацию о заказах и заказчиках в одной таблице, для каждого из этих пяти заказов вам придется дублировать информацию о заказчике. Во-первых, для этого Вам Потребуется дополнительное пространство. Во-вторых, если информация 0633
634 Глава 14. Клиент-серверная архитектура с использованием dbExpress о заказчике позже будет изменена, вам придется искать в таблице все записи, име- ющие отношение к этому заказчику, и модифицировать содержащуюся в этих за- писях информацию. Это может привести к ошибкам. Вместо этого вы можете выделить информацию о заказчиках в отдельной таблице, в которой каждому за- казчику будет соответствовать всего одна запись. Помимо прочих сведений в каж- дой такой записи будет указан порядковый номер (численный идентификатор) заказчика. В таблице для каждого заказа вместо полной информации о заказчике будет указан только порядковый номер (идентификатор) заказчика. Всю необхо- димую информацию о заказчике можно будет получить из таблицы заказчиков. Если потребуется изменить сведения о заказчике, модификации подвергнется всего одна запись в таблице заказчиков, все связанные с этим заказчиком записи в таб- лице заказов автоматически будут ссылаться на корректную информацию. Мало того, ссылки на записи таблицы заказчиков могут присутствовать также в других таблицах этой же базы данных. Правила нормализации подразумевают, что для повторяющихся значений должны использоваться сокращенные коды. Например, представьте, что поставка товара мо- жет осуществляться одним из нескольких способов (самолетом, пароходом, поездом, по почте, курьером и т. д.). Каждому из этих способов может соответствовать доста- точно длинное текстовое описание. Вы можете сохранить все эти описания в отдель- ной таблице, а в таблице заказов указывать лишь сокращенный код одного из них. Предыдущим правилом не следует злоупотреблять. Имейте в виду, что если для обработки запроса вам придется объединять слишком большое количество таблиц, общая производительность базы данных снизится. Чтобы избежать этого, вы можете либо в некоторой степени денормализовать базу данных (например, хранить в таблице заказов относительно небольшие описания вариантов поставки товара), либо хранить описания на стороне клиента (внутри клиентского прило- жения) и возложить обязанности по соблюдению соответствий на клиентское при- ложение (такой дизайн базы данных нельзя считать корректным). Последний вари- ант удобно использовать в случае, если все клиентские программы разрабатываются с использованием единой среды разработки (скажем, Delphi). От первичных ключей к идентификаторам объектов OID В реляционной базе данных записи идентифицируются не в соответствии с физи- ческим расположением (как, например, в Paradox или в других локальных базах данных), а с использованием данных, хранящихся внутри каждой записи. Для того чтобы уникально идентифицировать запись в таблице, используется некоторое подмножество полей, которое называется первичным ключом (primary key). Ком- бинация значений полей, входящих в состав первичного ключа, должна быть уни- кальна для каждой записи таблицы. ПРИМЕЧАНИЕ--------------------------------------------------------------- Многие серверы баз данных добавляют в таблицы внутренние идентификаторы записей, однако это делается только для целей внутренней оптимизации. Процесс добавления внутренних идентифик торов не имеет никакого отношения к логическому дизайну реляционной базы данных. В Р^1 SQL-серверах внутренние идентификаторы могут функционировать по-разному, принципы ботки могут изменяться от версии к версии, поэтому, разрабатывая прикладное программное об печение, вы не должны полагаться на значения этих идентификаторов. __. 0634
Элементы дизайна базы данных 635 На ранних этапах развития реляционной теории предполагалось использова- ние логических ключей (logical key). Логический ключ — это одно или несколько полей таблицы, которые можно использовать для уникальной идентификации каж- дой из ее записей. На первый взгляд подобрать такие поля не сложно, например, в качестве логического ключа можно попробовать использовать имя организации. Однако выясняется, что существует достаточно много организаций с одинаковы- ми именами. Мало того, даже если в качестве логического ключа вы будете ис- пользовать комбинацию из имени компании и ее юридического адреса, вы все рав- но не сможете гарантировать уникальность таких комбинаций. Существует еще одна проблема: представьте, что компания меняет имя (это происходит чаще, чем вы думаете, например недавно компания Borland сменила имя на Inprise, а затем снова стала именоваться Borland) или юридический адрес. Если при этом в других таблицах базы присутствуют ссылки на компанию, вам придется менять все эти ссылки, чтобы обеспечить целостность данных. Исходя из всех этих причин, а также из соображений эффективности (исполь- зование в качестве ссылок длинных строковых значений требует расхода значи- тельного пространства во вторичных таблицах, в которых эти ссылки присутству- ют) от использования логических ключей было решено отказаться. Вместо них в настоящее время используются физические ключи и суррогатные ключи. о Физический ключ (Physical Key) — это единственное поле, которое иденти- фицирует запись таблицы гарантированно уникальным образом. Например, каждый гражданин Соединенных Штатов обладает индивидуальным номером социального страхования (Social Security Number), фактически во всех других странах гражданам назначаются индивидуальные идентификаторы, связанные с налогообложением (Tax ID, ИНН). Подобная система государственных иден- тификаторов существует также и для коммерческих организаций. Следует иметь в виду, что подобные идентификаторы действительно можно считать уникаль- ными, однако их формат в разных странах может быть разным (в результате, если ваша компания занимается продажей товаров за рубеж, в ее базе данных возникают связанные с этим проблемы). Кроме того, с течением времени фор- мат идентификатора может измениться (например, в результате появления нового налогового законодательства). Наконец, такие идентификаторы могут быть неэффективными, так как их размер может оказаться достаточно боль- шим (в частности, в Италии для идентификации граждан используется 16-сим- вольный код, в состав которого входят не только числа, но и буквы). ° Суррогатный ключ (Surrogate Key) — номер, идентифицирующий запись. Это может быть код клиента, номер заказа и т. п. Суррогатные ключи очень часто используются при проектировании баз данных. Однако во многих случаях они фактически оказываются логическими идентификаторами. ВНИМАН И Е----------------------------------------------------------------------- Ситуация становится проблематичной в случае, если суррогатный ключ обладает некоторым ос- «сленным значением, а значит, должен подчиняться некоторым специальным правилам. Напри- р< счета, выставляемые заказчикам, должны нумероваться при помощи уникальных порядковых по^еР°в' К0Т0Рые должны следовать друг за другом без пропусков в последовательности. Такое равило достаточно сложно реализовать на практике, ведь когда вы посылаете данные, введенные Льз°вателем, только база данных может определить, каким должен быть следующий порядковый фиМер Счета. Вместе с тем, прежде чем передавать новую запись в базу данных, вы должны иденти- * Пировать эту запись, в противном случае вы не сможете получить ее обратно. Практические -J^^ho Решению этой проблемы рассматриваются в главе 15. 0635
636 Глава 14. Клиент-серверная архитектура с использованием dbExpres Идентификаторы объектов (OID) Идентификаторы объектов OID (Object Identifiers) являются расширением концепции суррогатных ключей. OID — это либо число, либо строка, содер- жащая последовательность цифр. Идентификатор OID добавляется в каж- дую запись каждой таблицы, описывающей некоторую сущность, а также иногда в таблицы, описывающие отношения между сущностями. В отличие от кодов клиентов, порядковых номеров счетов, идентификаторов SSN иден- тификаторы OID генерируются случайным образом: на них не действуют какие-либо упорядочивающие правила, кроме того, идентификаторы OID невидимы для конечного пользователя. Это означает, что вы можете исполь- зовать суррогатные ключи (если в вашей компании используются такие клю- чи) совместно с идентификаторами OID, однако все внешние ссылки на за- писи таблицы будут основаны на идентификаторах OID. Данный подход является частью теоретического фундамента, на котором базируется взаи- модействие между объектными и реляционными базами данных. Приверженцы данного подхода предлагают использовать еще одно пра- вило, в соответствии с которым предлагается использовать идентификато- ры, уникальные в рамках всей системы. Представьте, что у вас есть таблица заказчиков (клиентов компании) и таблица сотрудников компании. По сво- ей сути эти данные не связаны между собой. Зачем в двух этих таблицах для идентификации записей использовать уникальные OID? Причина в том, что в результате вы сможете продавать товары любому сотруднику компании, и при этом вам не потребуется дублировать информацию об этом человеке в таблице заказчиков — вы можете указать OID сотрудника в таблице зака- зов и в таблице счетов. Заказ поступил от человека, идентифицированного при помощи OID. Этот OID может идентифицировать запись в одной из нескольких разных таблиц. В приложениях Delphi вы можете воспользоваться преимуществами этого подхода. Если вы работаете над достаточно крупным проектом, технология OID может оказаться для вас чрезвычайно полезной. Внешние ключи и целостность ссылок Ключ, идентифицирующий запись, вне зависимости от его типа может использо- ваться в качестве внешнего ключа в составе других таблиц — в частности, именно таким образом определяются отношения между таблицами в базе данных. Люоои SQL-сервер обеспечивает проверку корректности подобных ссылок. Иначе гово- ря, вы не сможете добавить в таблицу базы данных ссылку на несуществующую запись другой таблицы. Ограничения, связанные с целостностью ссылок, описы- ваются в процессе создания таблицы. Итак, SQL-сервер запрещает добавлять в базу данных ссылки на несуществую- щие записи. Однако он также предотвращает удаление записи в случае, если суще ствуют внешние ссылки на эту запись. Некоторые SQL-серверы поддерживают еШе более сложную функцию: вместо того, чтобы запретить удаление записи, они вме сте с записью автоматически удаляют из других таблиц базы данных все записи, ссылающиеся на эту запись. 0636
Элементы дизайна базы данных 637 Дополнительные ограничения Помимо уникальности первичных ключей и ограничений, связанных с целостнос- тью ссылок, вы можете определить в рамках SQL-сервера дополнительные огра- ничения, накладываемые на данные. В частности, вы можете сделать так, чтобы в некоторых столбцах (таких как порядковый номер счета или индивидуальный идентификатор налогообложения) можно было хранить только уникальные зна- чения. Вы можете наложить условие уникальности на несколько столбцов, напри- мер постановить, что в одной аудитории в одно и то же время не могут проводить- ся две разные лекции. Простые правила можно выразить в виде ограничений, накладываемых назна- чения некоторого столбца, более сложные правила в большинстве случаев подра- зумевают исполнение сохраненных процедур (stored procedures), которые активи- руются триггерами (скажем, каждый раз, когда данные изменяются или когда данные добавляются в таблицу). Безусловно, об этом можно рассказать значительно больше, однако слишком подробное обсуждение не уместилось бы в данной и без того объемной книге. Моя цель состоит лишь в том, чтобы обеспечить читателей базовыми сведениями, а так- же напомнить о существовании описанных здесь возможностей тем, кто о них и без того знает. ПРИМЕЧАНИЕ ---------------------------------------------------- Более подробно о входящих в состав SQL языках DDL (Data Definition Language) и DML (Data Mani- pulation Language) рассказывается в главе Essential SQL электронной книги, о которой говорится в приложении В. Однонаправленные курсоры В локальных базах данных таблицы хранятся в локальных файлах. Логический порядок записей в таблицах определяется либо физическим порядком их разме- щения в файле, либо при помощи индекса. В отличие от локальных баз данных серверы SQL работают с логическими наборами данных, порядок размещения дан- ных в которых никоим образом не связан с физическим порядком размещения дан- ных. Сервер реляционной базы данных поддерживает работу с данными в соответ- ствии с реляционной моделью: математической моделью, основанной на теории множеств. Важно понимать, что записи (в английском языке вместо термина record — за- пись — иногда используется термин tuple — кортеж) в реляционной базе данных идентифицируются не при помощи позиции или порядкового номера, но исклю- чительно при помощи первичного ключа, который формируется на основе значе- ния одного или нескольких полей. Когда вы получаете с сервера набор записей, сервер добавляет к каждой из них ссылку на следующую запись; благодаря этому вы можете быстро перемещаться от текущей записи к следующей, однако переме- щение в обратном направлении — к предыдущей записи — выполняется чрезвычай- Но медленно. Исходя из этого часто говорят, что RDBMS использует однонаправ- ленный курсор. Подключение такой таблицы или запроса к элементу управления DBGrid практически невозможно, так как пролистывание записей в направлении от к°нца к началу будет выполняться чрезвычайно медленно. 0637
638 Глава 14. Клиент-серверная архитектура с использованием dbExpress Некоторые системы управления базами данных кэшируют данные, извлечен- ные из БД, благодаря этому становится возможным быстрое перемещение в обоих направлениях. В архитектуре Delphi для этой цели можно использовать компо- нент ClientDataSet или другой кэширующий компонент. Этот процесс будет под- робнее рассмотрен позднее, когда мы будем рассматривать библиотеку dbExpress и компонент SQLDataset. ПРИМЕЧАНИЕ---------------------------------------------------------------- В локальных программах компонент DBGrid часто используется для просмотра всего содержимого таблицы, однако в клиент-серверной среде этого следует избегать. Вместо этого следует фильтро- вать данные и отображать в рамках DBGrid только те записи и поля, которые действительно инте- ресуют пользователя. Если вы хотите получить список имен, вы можете вначале извлечь из базы только имена, начинающиеся на букву «А», затем имена, начинающиеся на букву «Б» и т. д., или предложить пользователю ввести первые несколько букв интересующей его фамилии. Итак, при перемещении по направлению к началу таблицы могут возникнуть проблемы. Вы должны также знать, что перемещение к последней записи таблицы — это еще более плохая идея, так как для выполнения этой операции вы будете вы- нуждены извлечь из таблицы абсолютно все записи. Схожая проблема возника- ет при использовании свойства RecordCount набора данных. Это свойство предна- значено для подсчета количества записей в таблице. Если вы попытаетесь посчитать количество записей на стороне клиента, ваша программа будет вынуждена извлечь из базы данных абсолютно все записи одну за другой. По этой причине вертикаль- ная полоса прокрутки для DBGrid не работает в случае, если этот элемент с удален- ной таблицей. Если вы хотите узнать количество записей, отправьте на сервер спе- циальный запрос, и пусть сервер посчитает количество записей самостоятельно. Например, допустим, вас интересуют записи о сотрудниках, чей оклад превы- шает 50 000 долларов, однако, прежде чем извлечь все эти записи из EMPLOYEE базы данных и загрузить их на локальный компьютер, вы хотите предвари- тельно узнать, сколько их будет. Чтобы решить задачу, воспользуйтесь следу- ющим запросом: select count(*) from Employee where Salary > 50000 СОВЕТ----------------------------------------------------------------------------- Поддерживаемое в рамках SQL выражение count(*) является удобным способом вычисления коли- чества записей, которые будут возвращены в ответ на некоторый запрос. Вместо шаблона * вы можете также указать имя конкретного столбца, например count(First_Name). Также вы можете добавить модификаторы distinct или all, которые, соответственно, предписывают подсчитать либо только отличающиеся значения, либо любые значения, не равные null. Знакомство с InterBase Несмотря на то что система InterBase удерживает лишь небольшую долю рынка, она является весьма мощной RDBMS. В данном разделе я расскажу о некоторых ключевых технических возможностях InterBase, не углубляясь при этом в подроО' 0638
Знакомство с InterBase 639 пости (так как эта книга посвящена программированию на Delphi, а не работе с InterBase). К сожалению, в настоящее время опубликовано не так много книг, по- священных InterBase. Большая часть доступного материала содержится либо в до- кументации, прилагаемой к продукту, либо на нескольких веб-узлах, посвящен- ных InterBase (в качестве отправной точки вы можете использовать веб-узлы www. borland.com/interbase и www.ibphoenix.com) С самого начала система InterBase была разработана на основе современной, продуманной и надежной архитектуры. Автор системы, Джим Старки (Jim Starkey), изобрел архитектуру для поддержки совместного доступа и транзакций, которая позволяла выполнять операции в отношении базы данных, не блокируя при этом ее частей. Даже сегодня многие современные хорошо известные системы RDBMS с трудом справляются с подобной задачей. Внутренняя архитектура InterBase на- зывается Multi-Generational Architecture (MGA). Эта архитектура обеспечивает параллельный доступ к данным одновременно нескольких пользователей, при этом пользователь может модифицировать записи базы данных, не влияя на то, что «ви- дят» другие пользователи, работающие с базой в это же самое время. В рамках InterBase используется режим изоляции транзакций (transaction isola- tion mode): пользователь, выполняющий транзакцию, продолжает видеть одни и те же данные не смотря на изменения, которые вносятся другими пользователями. Технически для каждой открытой транзакции сервер базы данных хранит отдель- ную версию записи БД, к которой осуществляется доступ. Безусловно, этот под- ход требует больших расходов памяти, однако в большинстве случаев он позволя- ет избежать физической блокировки таблиц и позволяет минимизировать потери в случае сбоя. Архитектура MGA выполнена в рамках чистой программной моде- ли под названием Repeatable Read (повторяемое чтение), — многие современные хорошо известные SQL-серверы не могут обеспечить поддержку этой модели без потерь производительности. Помимо MGA, лежащей в основе сервера, система InterBase обладает также другими техническими преимуществами: о Для установки InterBase требуется относительно небольшое дисковое простран- ство, благодаря этому данная система идеально подходит для запуска локально на клиентских компьютерах, включая портативные компьютеры. Для установ- ки InterBase в минимальной конфигурации требуется менее 10 Мбайт, кроме того, эта система нуждается в относительно небольшом объеме оперативной памяти для работы. ° InterBase обеспечивает хорошую производительность при работе с большими объемами данных. ° InterBase может работать на множестве программных платформ (включая 32-битные версии Windows, Solaris и Linux), при этом версии этой системы для различных ОС полностью совместимы между собой. Благодаря этому сервер можно легко масштабировать с очень небольших до дорогостоящих высокопро- изводительных компьютеров. ° Хороший послужной список: система InterBase используется в течение 15 лет, при этом в ней обнаружено сравнительно небольшое количество проблем. ° Система InterBase совместима со стандартным ANSI SQL. 0639
640 Глава 14. Клиент-серверная архитектура с использованием dbExpres О InterBase поддерживает программирование на стороне сервера, включая пози ционные триггеры, переключаемые сохраненные процедуры, обновляемые пред ставления, исключения, события, генераторы и многое другое. О Система легко устанавливается и администрируется. Количество проблем с которыми приходится сталкиваться администраторам, минимально. Краткая история InterBase i Система InterBase была написана Джимом Старки (Jim Starkey) для его ком- пании Groton Database Systems (именно поэтому файлы InterBase до сих пор обладают расширением.gds). Позже эта компания была куплена компа- нией Aston-Tate. Еще позднее компания Borland купила компанию Aston- Tate, в результате система InterBase стала собственностью Borland. Вначале Borland напрямую осуществляла поддержку InterBase, однако позже было создано самостоятельное подразделение, специально предназначенное для разработки и сопровождения InterBase. Еще позднее это дочернее предпри- ятие вновь стало частью компании Borland. Начиная с версии Delphi 1 в комплект поставки этого инструмента раз- работки была включена оценочная версия InterBase, благодаря чему зта RDBMS получила распространение среди разработчиков. Несмотря на то что в настоящее время этой системе принадлежит лишь небольшая доля рынка систем RDBMS, база данных InterBase используется несколькими весьма солидными организациями, включая Ericsson, Министерство оборо- ны США, несколькими биржами ценных бумаг, а также домашними бан- ковскими системами. Относительно недавно (в декабре 1999 года) система InterBase 6 была объявлена базой данных с открытым исходным кодом. В июле 2000 года ис- ходный код этой базы данных фактически стал доступен широкой програм- мистской общественности. Однако официальная версия исходного кода InterBase 6 была опубликована компанией Borland в марте 2001 года. Меж- ду этими событиями было объявлено о формировании отдельной компании, которая будет заниматься консультированием и поддержкой базы данных с открытым исходным кодом. Часть бывших разработчиков и менеджеров, работавших над созданием InterBase, покинули компанию Borland и осно- вали новое предприятие IBPhoenix (www.ibphoenix.com), основной целью которого является поддержка пользователей InterBase. В то же самое время независимая группа экспертов InterBase иницииро- вала проект открытого исходного кода с названием Firebird, нацеленный на дальнейшее развитие и расширение InterBase. В настоящее время домаш- няя страница этого проекта располагается по адресу sourceforge.net/projects/ firebird/. Некоторое время на сервере SourceForge размещался также откры- тый исходный код InterBase, опубликованный компанией Borland, однако позднее эта компания объявила о том, что будет поддерживать только соб- ственную внутреннюю версию InterBase, поддержка открытого исходного кода InterBase со стороны Borland была прекращена. Теперь, я надеюсь, об- щая картина стала понятнее читателям. Если вы хотите приобрести версию 0640
Использование IBConsole 641 InterBase с традиционной лицензией (которая обойдется вам намного де- шевле большинства конкурирующих профессиональных SQL-серверов), обращайтесь в компанию Borland. Однако если вы предпочитаете программ- ные продукты с открытым исходным кодом (которые можно приобрести абсолютно бесплатно), воспользуйтесь результатами проекта Firebird (при этом вы можете приобрести профессиональную поддержку от компании IBPhoenix). Использование IBConsole В предыдущих версиях InterBase для прямого взаимодействия с системой вы мог- ли использовать один из двух инструментов: Server Manager (позволяет админис- трировать как локальный, так и удаленный сервер) и Windows Interactive SQL (WISQL). В состав версии 6 включен значительно более мощный интерфейс уп- равления под названием IBConsole. Это приложение Windows (разработанное с использованием Delphi), которое позволяет администрировать, настраивать, тестировать сервер InterBase, а также адресовать ему запросы SQL. IBConsole по- зволяет работать как с локальным, так и с удаленным сервером InterBase. IBConsole — это простая и полнофункциональная система, позволяющая ад- министрировать серверы InterBase и базы данных. Вы можете использовать этот инструмент для подробного изучения структуры базы данных, модификации этой структуры, запроса данных (эта возможность может оказаться полезной для раз- работки запросов, которые вы намерены включить в свою программу), резервного копирования и восстановления базы данных, а также выполнения любых других административных задач. Как можно видеть на рис. 14.1, приложение IBConsole позволяет работать од- новременно с несколькими серверами и расположенными на них базами данных. Все эти ресурсы перечисляются в едином конфигурационном дереве. Используя это приложение, вы можете получить общую информацию о базе данных, а также получить перечень ее элементов (таблиц, доменов, сохраненных процедур, тригге- ров). Для каждого из этих элементов можно получить более детальную информа- цию. Вы можете также создавать новые базы данных и настраивать их, выполнять резервное копирование файлов, обновлять определения, проверять текущее состо- яние базы и выполняемые в настоящий момент операции, получать перечень под- ключенных к базе данных клиентов и т. п. Приложение IBConsole позволяет открывать несколько окон для просмотра Детальной информации. В частности, на рис. 14.2 показано окно с информацией о таблицах. В этом окне отображается перечень ключевых свойств каждой таблицы (столбцы, триггеры, ограничения и индексы), SQL-определение таблицы (мета- данные), разрешения на доступ, содержащиеся в таблице данные, а также инфор- мация о зависимости между таблицами. Аналогичное окно можно открыть для любого другого элемента, определяемого в рамках базы данных. 0641
642 Глава 14. Клиент-серверная архитектура с использованием dbExpress 1 Lft omule p Local Server -] Databases Domans Tables Views Stored Procedures External Functions Generators Exceptions Blob Fiets Roles % IBWintech |+ 'fe WINTECH GDB q 0 Backup fli base If) Server Log © Users ЭЦ wmtech. server an сц lb fx % Disconnect Properties Database Statistics Shutdown Sweep T ransaction Recovery View Metadata Database Restart Drop Database Database Backup Connected Users Restore Database Disconnect from the current deUtese Show database properties Drsptey database stabsbcs Shutdown the database Perform a database sweep Recover fimbo transactions View Database Metadata Restart a database Drop the current database Backup an InterBase database View a fist of users currently connected to the server Restore an InterBase database Рис. 14.1. Приложение IBConsole позволяет управлять несколькими базами данных, которые могут быть расположены на разных компьютерах Properties 1о»; EMPLOYEE ^employee Propert»» | M«adataj Pwmtssws j Data | &^pwdenc«S:| ctSi a® & & ............... ; .......'... emp.no FIRST-NAME LAST-NAME PHONE_EXT HIRE DATE DEPT NO JOB.CODE JOB GRADE JOB COUNTRY SALARY FULL NAME (EMPNOJ SMALUNT (FIRSTNAME) VARCHAR(15) (LASTNAME) VARCHAR(20) VARCHAR(4) TIMESTAMP K (DEPTNO) CHAR(3) k (JOBCODE) VARCHAR(5) (JOBGRADE) SMALUNT (COUNTRYNAME) VARCHAR(15) (SALARY) NUMERIC(15 2) VARCHAR Рис. 14.2. IBConsole позволяет открыть несколько окон, в которых отображается подробная информация для любого из элементов базы данных. В данном случае вы можете видеть информацию о таблице Приложение IBConsole содержит в себе улучшенную версию изначальной про- граммы Windows Interactive SQL (рис 14 3) Вы можете ввести выражение SQL в верхней части окна (к сожалению, при этом программа не оказывает вам ника- кой поддержки) и затем направить этот запрос серверу SQL В результате про' грамма покажет вам данные, план доступа, реализуемый сервером (эксперт может воспользоваться этими сведениями для того, чтобы определить эффективность зап- роса), а кроме того, статистические данные об операции, выполненной сервером. 0642
InterBase: программирование на стороне сервера 643 Г*| Interactive SQL «mpleyee.ftfb Ffo Erft Quay Ttaryac^iB» Windows tide Ф? ’ «5 * & g l<* |» |velect last__nainez hire_date, salary from employee where salary > Ji { 1 И 1 t Вй"сЙёыТ 'jAutoOOLON I 1Ж.Ж Bender tchida Yamamoto Ferrari Gkxi |НЙЗДТ£ 10/8/1992 2/4/1993 7/1/1993 7/12/1993 •8/23/1993 |$А1АЙУ | 212850 6000000 7480000 99000000 390500 Риа 14.3. Окно Interactive SQL программы IBConsole позволяет протестировать SQL-запросы, которые вы намерены включить в состав разрабатываемых вами приложений Delphi На этом я закончу весьма краткое описание программы IBConsole, которая на самом деле является весьма мощным инструментом (и единственным, входящим в комплект поставки Borland InterBase, если не принимать во внимание утилиты командной строки) Однако не следует считать IBConsole наиболее совершенным инструментом в своей категории Существует достаточно много программ адми- нистрирования InterBase, разработанных сторонними производителями Многие из этих программ обладают более широким набором возможностей, однако далеко не все они работают столь же стабильно, как IBConsole, и далеко не все они столь же дружественны и удобны в использовании Некоторые из этих программ явля- ются условно-бесплатными, а другие распространяются совершенно бесплатно Приведу два примера InterBAse Workbench (www.upscene.com) и IB_WISQL (яв- ляется частью продукта InterBase Objects, www.ibobjects.com) InterBase: программирование на стороне сервера В начале данной главы я уже говорил о том, что одной из целей (и одной из про- блем) программирования в среде «клиент-сервер» является корректное разделе- ние нагрузки между компьютерами Когда вы отправляете SQL-запрос (в котором с°Держится SQL-выражение) с клиентского компьютера на сервер, большую часть Работы, связанной с обработкой данных, выполняет сервер При этом вы должны Использовать выражения select таким образом, чтобы набор данных, передаваемых с клиента на сервер, обладал как можно меньшим размером Чем меньше объем Данных, передаваемых между компьютерами, тем ниже нагрузка на сеть. 0643
644 Глава 14. Клиент-серверная архитектура с использованием dbExpress Помимо поддержки DDL (Data Definition Language) и DML (Data Manipulation Language) большинство серверов RDBMS позволяют вам определять процедуры обработки данных непосредственно на стороне сервера. Эти процедуры определя- ются с использованием стандартных команд SQL, кроме того, как правило, сервер поддерживает некоторый набор присущих только ему расширений (в большин- стве случаев код, использующий эти расширения, не является переносимым). Опе- рации обработки данных, выполняемые на стороне сервера, принимают одну из двух форм: сохраненные процедуры и триггеры. Сохраненные процедуры (Stored Procedures) Сохраненная процедура напоминает глобальную функцию модуля Delphi — к ней необходимо напрямую обратиться с клиентского компьютера. Сохраненные про- цедуры используются для определения операций по обработке данных, для груп- пировки последовательностей действий, которые требуется выполнить при воз- никновении различных условий, а также для хранения сложных выражений select. Подобно процедурам Delphi, сохраненные процедуры могут обладать одним или несколькими параметрами. В отличие от процедур Delphi, сохраненные процеду- ры могут возвращать не только одно, но и несколько значений. Вместо того чтобы возвратить всего одно значение, сохраненные процедуры могут вернуть результи- рующий набор данных — результат выполнения внутреннего или специальным образом сформированного выражения select. Далее приводится пример сохраненной процедуры, написанной для InterBase. Эта процедура в качестве параметра принимает дату и вычисляет наибольшую зар- плату среди сотрудников, принятых на работу в этот день: create procedure MaxSalOfTheDay (ofday date) returns (maxsal decimal(8.2)) as begin select max(salary) from employee where hiredate = :ofday into :maxsal; end Обратите внимание на ключевое слово into, которое предписывает серверу со- хранить результат выполнения выражения select в возвращаемом значении maxsal. Чтобы модифицировать или удалить сохраненную процедуру, вы можете исполь- зовать команды alter procedure и drop procedure. Взглянув на текст этой процедуры, вы можете удивиться, в чем ее преимуще- ство по сравнению с обыкновенным аналогичным SQL-запросом? Как в результа- те выполнения данной сохраненной процедуры, так и в результате выполнения аналогичного SQL-запроса вы получите от сервера одни и те же данные. В чем разница? Разница между двумя подходами состоит не в результате, а в скорости выполнения. Сохраненная процедура заранее компилируется и сохраняется на стороне сервера в виде специального внутреннего кода, исполнение которого осу- ществляется значительно быстрее, чем обработка поступившего извне SQL-зап- роса. На этапе компиляции процедуры сервер заранее определяет стратегию- которую он будет использовать в дальнейшем для доступа к данным. Обычный внешний SQL-запрос, напротив, каждый раз компилируется заново. По этой при- 0644
InterBase: программирование на стороне сервера 645 чине сохраненную процедуру можно использовать в качестве замены очень слож- ного SQL-запроса при условии, что этот запрос достаточно часто передается от клиента серверу, а его форма редко меняется. Чтобы активировать сохраненную процедуру из Delphi, достаточно использо- вать следующий SQL-код: select * from MaxSalOfTheDay (’01/01/2003') Триггеры (и генераторы) Триггеры в некоторой степени напоминают события Delphi. Активизация тригге- ра выполняется в момент, когда происходит некоторое событие. Триггеры могут запускать некоторый специальный код или обращаться к сохраненной процедуре. В обоих случаях исполнение происходит полностью на стороне сервера. Триггеры используются для того, чтобы поддерживать целостность данных, — они позволя- ют проверять новые данные более сложными способами, чем это возможно при помощи стандартных ограничений. Кроме того, триггеры автоматизируют обра- ботку побочных эффектов, возникающих при выполнении некоторых операций ввода (например, ведение журнала, в котором сохраняются сведения о предыду- щих изменениях зарплат сотрудников предприятия). Триггер может быть активирован в результате выполнения трех базовых опе- раций обновления данных: insert, update и delete. Когда вы создаете триггер, вы ука- зываете, должен ли он срабатывать перед или после одной из этих операций. В качестве примера триггера рассмотрим генератор, который создает уникаль- ный индекс в таблице. Во многих таблицах уникальные идентификаторы исполь- зуются в качестве первичных ключей. В InterBase поле AutoInc не поддерживается. Клиент, обращающийся к серверу, не может генерировать уникальный идентифи- катор, так как для этого ему потребовалось бы согласовывать этот идентификатор с другими клиентами. Фактически все серверы SQL поддерживают счетчик, зна- чение которого можно использовать для генерации уникальных ID. В InterBase зти автоматические счетчики называются генераторами (generators), а в Oracle — последовательностями (sequences). Вот пример кода InterBase: create generator cust_no_gen: gen_id (cust_no_gen. 1): Функция gen_id извлекает новое уникальное значение генератора, переданно- го в качестве первого параметра. Второй параметр указывает, насколько сле- дует увеличить значение счетчика (в данном случае значение увеличивается иа единицу). Теперь вы можете добавить в таблицу триггер (автоматический обработчик одного из событий, возникающих внутри таблицы). Триггер напоминает обработ- чик одного из событий компонента Table, однако он пишется на языке SQL и ис- полняется на сервере, а не на клиентском компьютере. Вот пример: create trigger set_cust_no for customers before insert position 0 as begin new.cust no = gen id (cust no gen. 1): end “ ” " - 0645
646 Глава 14. Клиент-серверная архитектура с использованием dbExpress Этот триггер определяется для таблицы заказчиков и активируется каждый раз, когда в таблицу вставляется новая запись. Символ new соответствует новой запи- си, вставляемой в таблицу. Параметр position указывает порядок исполнения не- скольких триггеров, соединенных с одним и тем же событием. (Триггеры с мень- шими значениями исполняются в первую очередь.) Внутри тела триггера вы можете писать выражения DML, которые выполняют обновление других таблиц, однако при этом вы должны следить за тем, чтобы не возникло событий, вновь активирующих этот же самый триггер, — в результате этого может возникнуть замкнутый цикл. Позже вы можете модифицировать или отключить триггер при помощи выражений alter trigger или drop trigger. Триггеры срабатывают автоматически для указанных вами событий. Если вы планируете выполнить в отношении базы данных набор команд в пакетном режи- ме, наличие в базе триггера может замедлить этот процесс. Если целостность дан- ных, добавляемых в базу таким образом, уже проверена, вы можете временно отключить триггер и выполнить пакетное обновление. Зачастую пакет команд оформляется в виде сохраненной процедуры, однако в сохраненной процедуре в общем случае нельзя использовать DDL-выражений, подобных тем, которые вы- полняют деактивацию и реактивацию триггера. В подобной ситуации вы можете определить представление (view) базы данных на основе команды select * from table — в результате будет создан псевдоним (alias) таблицы. После этого вы може- те выполнить сохраненную процедуру в отношении таблицы и применить триггер к представлению (представление может быть использовано также клиентской про- граммой). Библиотека dbExpress В настоящее время основным механизмом доступа к серверам SQL в Delphi явля- ется библиотека dbExpress. Как уже отмечалось в главе 13, это не единственный механизм доступа к серверу SQL, однако, безусловно, это основной подход. Биб- лиотека dbExpress, впервые появившаяся в Kylix и Delphi 6, позволяет вам обра- щаться к разнообразным серверам баз данных (InterBase, Oracle, DB2, MySql, Informix и теперь Microsoft SQL Server). В главе 13 я уже рассказывал об отличи- тельных особенностях этой библиотеки и сравнивал ее с другими альтернативами. По этой причине здесь я не буду вдаваться в общие описания, а немедленно перей- ду к рассмотрению технических вопросов. ПРИМЕЧАНИЕ-----------------------------------------------;— Наиболее важным нововведением, появившимся в dbExpress в последней, седьмой версии Delphi, яв- ляется добавление драйвера поддержки Microsoft SQL Server. В отличие от других драйверов dbExpress, этот драйвер обращается к библиотеке производителя сервера не напрямую, а через провайдера Microsoft OLEDB для SQL Server. (Подробнее о провайдере OLEDB я расскажу в главе 15.) Работа с однонаправленными курсорами Основной лозунг dbExpress может звучать так: «извлекать, но не кэшировать»- Основное отличие библиотеки от BDE или ADO заключается в том, что dbExpress 0646
Библиотека dbExpress 647 может только исполнять запросы SQL и получать с сервера результаты в формате однонаправленного курсора. Это означает, что вы можете перемещаться от теку- щей записи к следующей, однако вы не можете вернуться обратно к предыдущей записи набора данных (для этого вам придется заново открыть запрос и с самого начала просмотреть все записи минус одну — это чрезвычайно медленная опера- ция, библиотека dbExpress блокирует ее). Данное ограничение вызвано тем, что библиотека не сохраняет полученные с сервера данные в локальном кэше — она всего лишь передает их от сервера базы данных к вызывающей программе. С использованием однонаправленного курсора связаны серьезные ограниче- ния — помимо проблем навигации вы не можете подключить к набору данных эле- мент управления сетки (grid). Однако существуют задачи, для решения которых однонаправленный курсор вполне подходит. О Однонаправленный набор данных можно использовать для составления док- ладов. Как в случае распечатки доклада на бумаге, так и при оформлении его в виде страницы HTML или трансформации ХМЕвы перемещаетесь от записи к записи — нет никакой нужды возвращаться к предыдущей, уже обработанной записи, кроме того, пользователь фактически никак не взаимодействует с дан- ными. Таким образом, можно сделать вывод, что однонаправленные наборы данных подходят для построения веб-архитектур и многозвенных архитектур. О Однонаправленный набор данных можно использовать для заполнения локаль- ного кэша, например в качестве кэша можно использовать компонент Client- DataSet. Заполнив локальный кэш данными, вы можете подключить к нему лю- бые удобные для вас визуальные компоненты, включая сетку. Вы можете свободно перемещаться по кэшу в любых направлениях и редактировать его содержимое. Более того, вы можете контролировать данные в локальном кэше даже лучше, чем это возможно при использовании BDE и ADO. Важно отметить, что в подобных ситуациях кэш, встроенный в механизм дос- тупа к базе данных (BDE или ADO), является не благом, а помехой, так как он приводит к лишним затратам времени и памяти. Библиотека не тратит лишнюю память на кэширование информации и не затрачивает дополнительное время, свя- занное с копированием данных из кэша и в кэш. За последние два года многие программисты перешли от использования кэширования, основанного на BDE, к кэшированию данных в компоненте ClientDataSet, так как этот компонент обеспе- чивает большую гибкость. Однако при использовании ClientDataSet в комбинации с BDE (или ADO) вы имеете дело с двумя кэшами — это чревато значительными расходами памяти. Еще одно преимущество ClientDataSet состоит в том, что кэш этого компонента поддерживает операции редактирования, обновления, хранящиеся в его кэше, мо- гут быть применены к изначальному серверу базы данных при помощи компонен- та DataSetProvider. Этот компонент генерирует необходимые для этого выражения SQL update и при этом обеспечивает большую гибкость по сравнению с BDE (в этом отношении ADO является более мощным механизмом). В общем и целом, провай- ДеР может использовать компонент ClientDataSet для обновления базы данных, од- нако компоненты dbExpress не позволяют напрямую реализовать это. 0647
648 Глава 14. Клиент-серверная архитектура с использованием dbExpress Платформы и базы данных Основное преимущество dbExpress заключается в том, что эта библиотека под- держивается как в среде Windows, так и в среде Linux. Механизмы BDE и ADO мо- гут использоваться только в среде Windows. Вместе с тем, некоторые компоненты, специально предназначенные для работы с конкретной RDBMS (например, InterBase Express), также поддерживаются в нескольких разных операционных средах. Если вы используете dbExpress, в вашем распоряжении оказывается общая ин- фраструктура, которая не зависит от SQL-сервера, с которым вы планируете рабо- тать. В составе dbExpress присутствуют драйверы для MySQL, InterBase, Oracle, Informix, Microsoft SQL Server и IBM DB2. ПРИМЕЧАНИЕ-----------------------------------------------------------------— Для архитектуры dbExpress можно написать свои собственные драйверы. Подробнее об этом рас- сказывается в документе db Express Draft Specification, опубликованном на веб-узле Borland Community. На момент написания данной книги этот документ располагался по адресу http://commumty.borland. com/article/0,1410,22495,00.html. Кроме того, существуют драйверы, разработанные сторон- ними производителями. Например, существует свободно распространяемый драйвер, который обес- печивает взаимодействие dbExpress и ODBC. Полный список драйверов располагается по адресу http://community.borland.eom/article/0,1410,28371,00.html. Проблемы с версиями драйверов и встроенные модули Технически драйвер dbExpress — это DLL-файл, который вы должны включить в комплект поставки своей программы. Это справедливо как для Delphi 6, так и для Delphi 7. Проблема состоит в том, что имена этих файлов не изменились. По этой причине, если вы установили откомпилированное приложение Delphi 7 на компьютере, на котором установлены драйверы dbExpress из комплекта Delphi 6, ваше приложение сможет начать работу, откроет соединение с сервером, однако при попытке извлечь данные произойдет сбой. На экране вы увидите сообщение SQL Error: Error mapping failed (сбой отображения данных). Сообщение, безусловно, сбивает с толку, однако, увидев его, вы должны понять, что оно указывает на не- правильную версию драйвера. Чтобы убедиться в несоответствии версий, проверьте наличие информации о версии в DLL — в драйверах для Delphi 6 эта информация отсутствует. Чтобы по- высить надежность приложения, вы можете добавить соответствующую проверку в его код. Для этого можно воспользоваться специальным вызовом Windows APL function GetDriverVersion (strDriverName string) Integer, var nlnfoSize, nDetSize DWord. pVInfo, pDetail Pointer. begin //по умолчанию версия б - в случае отсутствия информации о версии Result = 6. // читаем информацию о версии nlnfoSize = GetFileVersionlnfoSize (pChar(strDriverName) nDetSize) if nlnfoSize > 0 then beoin 0648
Компоненты dbExpress 649 GetMem (pVInfo, nlnfoSize). try GetFileVersionlnfo (pChar(strDriverName) 0. nlnfoSize pVInfo). VerQueryValue (pVInfo. 'V. pDetail nDetSize) Result = HiWorld (TVSFixedFilelnfo(pDetaiK) dwFileVersionMS). finally FreeMem (pVInfo). end. end. end. Этот фрагмент кода позаимствован из примера DbxMutti, речь о котором пойдет далее. Программа использует этот код для того, чтобы сгенерировать исключение в случае, если используется драйвер неправильной версии: if GetDriverVersion ('dbexpint dll') <> 7 then raise Exception Create ( 'Incompatible version of the dbExpfrress driver "dbexpress dll" found') Если вы попытаетесь скопировать драйвер из каталога bin среды Delphi 6 в рабо- чий каталог вашей программы, вы увидите на экране данное сообщение об ошибке. Вы можете модифицировать данный код таким образом, чтобы учитывать обновленные версии драйверов или библиотеки, однако подобный код сможет из- бавить вас от проблем, связанных с установкой dbExpress. Существует, однако, альтернативное решение: вы можете статически скомпо- новать драйвер dbExpress внутрь вашего приложения. Чтобы сделать это, включи- те соответствующий модуль (например, dbexprint.dcu mmdbexpora.dcu) в вашу про- грамму, то есть укажите имя модуля в одном из выражений uses. Компоненты dbExpress Компоненты VCL, используемые для взаимодействия с библиотекой dbExpress, — это группа компонентов наборов данных плюс несколько вспомогательных ком- понентов. Отличительным признаком всех этих компонентов является префикс SQL в начале имени каждого из них. Этот префикс указывает на то, что компоненты используются для доступа к серверам RDBMS. В состав этой группы входит компонент подключения к базе данных, несколь- ко наборов данных (универсальный набор, а также специальные версии для таб- лиц, запросов и сохраненных процедур, а также компонент, инкапсулирующий ClientDataSet), а также утилита мониторинга. Компонент SQLConnection Класс TSQLConnection является наследником класса TCustomConnection. Этот класс, подобно другим аналогичным классам (Database, ADOConnection и IBConnection), обслуживает подключения к базе данных. СОВЕТ------------------------------------------------------------------------ в отличие от других семейств компонентов, в библиотеке dbExpress соединение с базой данных является обязательным. В компонентах наборов данных вы не можете напрямую указать использу- емую базу данных, вместо этого вы ссылаетесь на SQLConnection. 0649
650 Глава 14. Клиент-серверная архитектура с использованием dbExpress Компонент соединения использует информацию из конфигурационных фай- лов drivers.ini и connections.ini. Эти файлы являются конфигурационными файла- ми dbExpress, по умолчанию они располагаются в каталоге Common Files\Borland Shared\DBExpress. В файле drivers.ini перечисляются доступные для использования драйверы dbExpress — по одному для каждой доступной базы данных. Для каждо- го драйвера используется набор параметров подключения, которым присвоены значения по умолчанию. Например, в разделе InterBase присутствуют следующие записи: / [InterBase] GetDri verFunc=getSQLDm verl NTERBASE LibraryName=dbexpint dll VendorLib=GDS32 DLL BlobSize=-l CommtRetain=False Database=database gdb Password=masterkey RoleName=RoleName ServerCharSet=ASCII SQLDialect=l Interbase Translsolation=ReadCommited USer_name=sysdba WaitOnLocks=True В частности, здесь указывается имя DLL-файла драйвера (параметр LibraryName), имя функции, являющейся точкой входа в драйвер (GetDriverFunc), клиентская биб- лиотека производителя базы, а также другие специальные параметры, которые определяются типом используемой базы данных. Если вы просмотрите файл drivers.ini целиком, вы обнаружите, что параметры на самом деле относятся непос- редственно к базе данных. Некоторые из параметров не имеют отношения к уров- ню драйвера (например, база данных, к которой следует подключаться), однако в списке указаны все доступные параметры вне зависимости от того, на каком уров- не они используются. В файле connections.ini содержится описание, относящееся к базе данных. В списке параметры ассоциируются с именем, и для каждого драйвера базы дан- ных можно указать множество параметров соединения. Соединение описывает фи- зическую базу данных, к которой вы намерены подключиться. Вот пример фраг- мента определения IBLocal: [IBLocal] BlobSize=-l CommitRetain=False Databases \Program FilesXCommon FilesXBorland SharedXDataXemployee gdb DriverName=Interbase Password=masterkey Rol eName=RoleName ServerCharSet=ASCII SQLDialect=l Interbase Translation=ReadCommited User_Name=sysdba WaitOnLocks=True Если вы сравните два листинга, вы поймете, что второй является подмноже- ством первого. Когда вы создаете новое соединение, система копирует параметры 0650
Компоненты dbExpress 651 по умолчанию из файла driver.ini. После этого вы сможете отредактировать их для специального соединения — например, указать подходящее имя базы данных. Каж- дое соединение обращается к драйверу для получения значений ключевых атри- бутов, на это указывает значение свойства DriverName. Обратите также внимание на то, что база данных, на которую ссылается данный файл, является результатом моего редактирования — я указал здесь значения параметров, приемлемые для боль- шинства рассматриваемых мною примеров. Важно помнить о том, что данные инициализационные файлы используются только на этапе проектирования. Когда вы выбираете драйвер или соединение на этапе проектирования, значения из этих файлов копируются в соответствующие свойства компонента SQLConnection, как в данном примере: object SQLConnectionl: TSQLConnection Connect!onName = 'IBLocal' DriverName = 'InterBase' GetDriverFunc = 'getSQLDriverINTERBASE' LibraryName = 'dbexpint dll' LoginPrompt = False Params.Strings = ( 'BlobSize=~l' 'CommitRetain=False' 'Databases \Program Files\Cormon Files\Borland Shared\Data\employee.gdb' ' DriverName=Interbase' ‘Passwordmasterkey' 'RoleName=RoleName' 'ServerCharSet=ASCII' 'SQLDia1ect=l' 'Interbase Translation=ReadCannited' 'UserJJame=sysdba ' 'WaitDnLocks=True') VendorLib - 'GDS32.DLL' end На этапе исполнения ваша программа получает всю необходимую информа- цию из свойств компонента, каких-либо обращений к инициализационным фай- лам не происходит, поэтому вы можете даже не включать эти файлы в комплект вашей программы. Теоретически файлы могут потребоваться в случае, если на этапе исполнения вы захотите изменить значения свойств DriverName или ConnectionName. Однако если вы хотите подключить вашу программу к новой базе данных, вы мо- жете напрямую присвоить необходимые значения соответствующим свойствам. При добавлении в программу нового компонента SQLConnection вы можете при- менить один из двух способов. Во-первых, вы можете настроить драйвер, ис- пользуя список значений, доступных для свойства DriverName, а затем выбрать за- ранее предопределенное соединение из списка значений, доступных для свойства ConnectionName. Этот второй список фильтруется в зависимости от того, какой драй- вер вы выбрали. Во-вторых, вы можете сразу же начать с выбора значения свой- ства ConnectionName, в этом случае в списке будут присутствовать абсолютно все возможные значения. Вместо того чтобы подключать существующее соединение, вы можете опреде- лить новое. Для этого необходимо сделать двойной щелчок на компоненте SQL- Connection и запустить редактор dbExpress Connection Editor (рис. 14.4). Этот ре- дактор позволяет не только создать новое соединение, но и просмотреть значения 0651
652 Глава 14. Клиент-серверная архитектура с использованием dbExpress свойств существующих соединений. В левой части рабочего окна редактора пере- числяются все заранее предопределенные соединения (либо для конкретного драй- вера, либо все соединения для всех драйверов). Свойства соединений можно редактировать при помощи таблицы в правой части рабочего окна. Вы можете ис- пользовать кнопки панели инструментов для добавления, переименования и тес- тирования соединений. Кроме того, вы можете открыть окно конфигурации драй- веров dbExpress Drivers Settings (также показанное на рис. 14.4), предназначенное только для чтения. й Лг.1 -1 ±1 ||А1Г DB2Comectron IBConnectron InfotnwConnection MSCormectron MSSQLConoection MySQLConnectron Dracte DtacleCcnnecbon test RoieName CommrtRetam Database DnverName Password I db2cldfl GDS32DLL UBMYSQLdB OCI DLL masterkey RoieName ASCII False C \Program FiesKCommon F Interbase DBEXPDB2DLL dbexprrt dfl dbexpmys dfl dbexpora.dfl I UMwflew» ’ DB2 Interbase MYSQL Oracle Key...... В lobSize sysdba True ServerCharSet SQLDalect Interbase TramlsolatioReadZommited User_Name WaitOnLocks Рис. 14.4. Рабочее окно редактора dbExpress Connection Editor с диалоговым окном dbExpress Drivers Settings Помимо редактирования заранее предопределенных параметров соединения редактор dbExpress Connection Editor позволяет вам выбрать соединение для компо- нента SQLConnection, для этого необходимо щелкнуть на кнопке ОК. Имейте в виду, что если вы измените какие-либо из значений, данные будут немедленно записа- ны в конфигурационные файлы — вы не сможете отменить внесенные изменения, щелкнув на кнопке Cancel (Отмена). Если вы хотите определить доступ к базе данных, лучшим подходом, безуслов- но, является редактирование свойств соединения. В этом случае, если вы хотите обеспечить доступ к той же самой базе данных из другого приложения или из дру- гого соединения, все, что вам потребуется, это выбрать соединение. Однако имей- те в виду, что операция выбора соединения подразумевает копирование данных. Это означает, что если вы измените конфигурацию соединения, значения пара- метров в других компонентах SQLConnection, ссылающихся на это соединение, не изменятся: вы должны будете заново выбрать соединение во всех ссылающихся на него компонентах. Функционирование компонента SQLConnection определяется значениями его свойств. Драйвер и библиотека производителя БД указываются в свойствах, и вы можете изменить эти параметры на этапе проектирования (однако у вас вряд ли возникнет такая необходимость). База данных, а также другие параметры соеди- нения, имеющие отношение к базе данных, указываются в свойствах Params. Это список строк, в котором содержится такая информация, как имя базы данных, имя пользователя и пароль и т. п. На практике, чтобы настроить компонент SQLCon- nection, вы можете выбрать драйвер и настроить имя базы данных напрямую в свои- 0652
Компоненты dbExpress 653 стве Params, не обращая внимания на предопределенное соединение. Однако я не рекомендую использовать этот подход. Предопределенные соединения удобны, однако если данные меняются, вам придется вручную обновлять каждый из ком- понентов SQLConnection. Существует альтернатива. Вы можете настроить свойство LoadParamsOnConnect таким образом, чтобы значения конфигурационных параметров загружались из конфигурационных файлов каждый раз, когда происходит открытие соединения. В этом случае измененные параметры будут автоматически загружены при откры- тии соединения как на этапе проектирования, так и на этапе выполнения. На этапе проектирования этот подход оказывается достаточно удобным (эффект точно та- кой же, как при повторном выборе соединения), однако что касается этапа испол- нения, могут возникнуть сложности, так как к исполняемому файлу приложения в обязательном порядке должен быть приложен файл connections.ini. Удобно это или нет — решать вам. Единственным свойством компонента SQLConnection, не связанным ни с драй- вером, ни с базой данных, является параметр LoginPrompt. Если вы присвоите это- му параметру значение False, вы можете добавить пароль в конфигурацию компо- нента, в результате при подключении не будет отображаться окно ввода пароля. Это удобно в случае, если вы занимаетесь разработкой, однако в производствен- ных условиях такой режим существенно снижает уровень защиты вашей системы. Конечно же, данный режим можно использовать для анонимных подключений, таких, например, как подключения к веб-серверу. Компоненты наборов данных dbExpress В состав семейства компонентов dbExpress входит несколько различных наборов данных: универсальный набор данных (generic dataset), таблица (table), запрос (query) и сохраненная процедура (stored procedure). Последние три компонента обеспечивают совместимость с аналогичными компонентами BDE и обладают од- ноименными свойствами. Если вы не собираетесь адаптировать для dbExpress име- ющийся старый код BDE, для вас будет удобнее использовать универсальный ком- понент SQLDataSet, который позволяет выполнить запрос, обратиться к таблице или запустить сохраненную процедуру. Прежде всего, важно отметить, что все эти наборы данных являются производ- ными от общего специального базового класса TCustomSQLDataSet. Этот класс и про- изводные от него классы представляют собой однонаправленные наборы данных (о свойствах которых я уже рассказывал). На практике это означает, что для пере- мещения между записями вы можете использовать только две операции: First (са- мая первая запись) и Next (следующая запись). Операции Prior (предыдущая за- пись), Last (последняя запись), Locate (переместиться к произвольной записи), закладки и другие навигационные возможности не поддерживаются. ПРИМЕЧАНИЕ-------------------------------------------------------—------ Технически некоторые из операций перемещения по набору данных обращаются к внутренней фун- кции CheckBiDirectional и, в случае если набор данных однонаправленный, генерируется исключе- ние. Функция CheckBiDirectional ссылается на публичное свойство IsUnidirectional класса TDataSet. Вы тоже можете использовать это свойство в своих программах, чтобы блокировать операции, которые не поддерживаются в отношении однонаправленных наборов данных. 0653
654_____________Глава 14. Клиент-серверная архитектура с использованием dbExpress Кроме того, рассматриваемые здесь наборы данных не поддерживают редакти- рование, поэтому множество методов и событий, присутствующих в других ком- понентах, в данном случае недоступно. В частности, здесь нет таких событий, как AfterEdit и BeforeEdit. Как уже было отмечено, из четырех наборов данных, поддерживаемых в рамках dbExpress, фундаментальным является класс TSQLDataSet, который позволяет из- влечь из базы данных набор записей, а также выполнить в отношении базы данных команду. Эти две задачи решаются при помощи метода Open (вместо этого можно присвоить свойству Active значение True) или при помощи метода ExecSQL. Компонент SQLDataSet позволяет извлечь из базы данных всю таблицу цели- ком, кроме того, он позволяет воспользоваться SQL-запросом или сохраненной процедурой для того, чтобы прочитать из БД набор данных или выполнить в отно- шении БД некоторую команду. Один из трех режимов доступа определяется при помощи значения свойства CommandType. Этому свойству можно присвоить одно из трех значений: ctQuery, ctStoredProc и ctTable. Тип доступа влияет на значение свойства CommandText (а также на поведение соответствующего редактора свойств в Object Inspector). Для таблицы или сохраненной процедуры свойство CommandText содержит в себе имя соответствующего элемента базы данных, а в редакторе ото- бражается ниспадающий список, в котором перечисляются все допустимые значе- ния. Для запроса свойство CommandText хранит в себе текст команды SQL, а редак- тор отображает краткую электронную подсказку о том, как формировать запросы SQL (в случае, если это выражение SELECT). Окно редактора показано на рис. 14.5. Рис. 14.5. Редактор свойства CommandText, используемый компонентом SQLDataSet для запросов Если вы используете таблицу, компонент генерирует соответствующий SQL- запрос автоматически, так как библиотека dbExpress ориентирована только на базы данных SQL. Сгенерированный запрос включает в себя все поля таблицы, и если вы настроите значение поля SortFieldNames, в состав запроса будет добавлена ди- ректива sort by. Три других, специальных набора данных dbExpress поддерживают аналогич- ный порядок функционирования, однако запрос SQL указывается в свойстве SQL, 0654
Компонент SQLMonitor 655 сохраненная процедура — в свойстве StoredProcName, а имя таблицы — в свойстве TableName (как и в трех соответствующих компонентах BDE). Входящий в состав Delphi 7 компонент SimpleDataSet В среде разработки Delphi 7 впервые появился компонент SimpleDataSet Этот компо- нент является комбинацией четырех существующих компонентов: SQLConnection, SQLDataSet, DataSetProvider и ClientDataSet Компонент SimpleDataSet задуман как инст- румент, облегчающий разработку, — вместо того чтобы создавать четыре разных ком- понента и соединять их между собой, вы создаете всего один компонент. Компонент SimpleDataSet фактически является клиентским набором данных (то есть аналогом ClientDataSet) с подключенными к нему двумя составными (compound) компонента- ми (из семейства dbExpress) плюс скрытый компонет-провайдер. (В данном случае странно, что провайдер скрыт, так как он создается как составной компонент.) Компонент позволяет вам модифицировать свойства и события составных ком- понентов (помимо провайдера) и заменить внутреннее соединение внешним, бла- годаря этому несколько наборов данных совместно используют одно общее соеди- нение с БД. Компонент SimpleDataSet обладает рядом ограничений, в частности, существуют сложности при манипулировании полями набора данных, осуществ- ляющего доступ к данным (это оказывает влияние на настройку ключевых полей и может повлиять на генерацию обновлений), кроме того, некоторые из событий провайдера недоступны. Исходя из этого, я рекомендую использовать компонент SimpleDataSet только для относительно несложных приложений. ПРИМЕЧАНИЕ-------------------------------------------------------------- В состав Delphi б входит еще более простой и более ограниченный компонент SQLCIientDataSet. Аналогичные компоненты поддерживаются в рамках технологий BDE и IBX. В настоящее время компания Borland объявила, что все эти компоненты следует считать устаревшими. Однако каталог Demos\Db\SQLChentDataset содержит копии оригинальных компонентов, и для обеспечения совмес- тимости вы можете установить их в Delphi 7. Лично я пришел к следующему выводу: если компонент SimpleDataSet выглядит ограниченным в возможностях, то компонент SQLCIientDataSet фактически полностью бесполезен. Компонент SQLMonitor Последним представителем группы компонентов dbExpress является компонент SQLMonitor, который используется для протоколирования запросов, передаваемых от dbExpress серверу базы данных. Благодаря этому вы можете видеть команды, передаваемые базе данных, и низкоуровневые ответы, принимаемые от базы дан- ных. Иными словами, у вас появляется возможность мониторинга трафика между клиентом и сервером на низком уровне. Тип поля Timestamp В среде Delphi 6 впервые появился тип TSQLTimeStampField, который соот- ветствует типу данных отметки времени (time stamp). Этот тип поддержи- вается многими SQL-серверами (включая InterBase). Тип данных TimeStamp представляет время в виде записи, этим он отличается от представления - продолжение . 0655
656 Глава 14. Клиент-серверная архитектура с использованием dbExpress Тип поля TimeStamp (продолжение) в виде числа с плавающей точкой, используемого в классе TDateTime. Тип TimeStamp определяется следующим образом: TSQLTImeStamp = packed record Year : SmallInt; Month : Word; Day : Word; Hour ; Word; Minute : Word; Second : Word: Fractions : LongWord; end; Поле TimeStamp поддерживает автоматическое преобразование стандарт- ных даты и времени при помощи свойства AsDateTime (в отличие от стан- дартного свойства AsSQLTimeStamp). Кроме того, вы можете выполнять специальные преобразования и другие манипуляции отметок времени, используя процедуры модуля SqlTimSt. В частности, вы можете восполь- зоваться следующими полезными функциями: DateTimeToSQLTimeStamp, SQLTimeStampToStr и VarSQLTimeStampCreate. Несколько демонстрационных программ dbExpress Теперь давайте рассмотрим несколько примеров использования dbExpress. В рас- сматриваемых далее программах демонстрируются ключевые возможности упо- мянутых ранее компонентов, а также вы узнаете о том, как использовать компо- нент ClientDataSet для кэширования и редактирования однонаправленных наборов данных. Позже я приведу пример естественного использования однонаправлен- ного запроса, то есть без поддержки кэширования и редактирования. Стандартное визуальное приложение, основанное на dbExpress, использует в своем составе серию компонентов: О SQLCon nection обеспечивает соединение с базой данных через подходящий драй- вер dbExpress. О SQLDataSet ассоциируется с соединением (при помощи свойства SQLConnection) и указывает, какой SQL-запрос должен быть выполнен или какую таблицу сле- дует открыть (для этого используются свойства CommandType и CommandText, о ко- торых мы уже говорили ранее). О DataSetProvider соединяется с набором данных, извлекает данные из SQLDataSet и генерирует необходимые SQL-выражения, связанные с обновлением данных. О ClientDataSet осуществляет чтение данных провайдера и сохраняет все данные (если его свойство PacketRecords равно значению -1) в памяти. Чтобы передать обновления данных в базу данных (через провайдера), необходимо обратиться к методу Apply Updates этого компонента. 0656
Несколько демонстрационных программ dbExpress 657 q DataSource позволяет отобразить данные, содержащиеся в компоненте Client- DataSet на экране при помощи визуальных элементов управления, поддержива- ющих работу с данными. Как я уже отметил ранее, картину можно существенно упростить, если восполь- зоваться компонентом SimpleDataSet, который позволяет заменить два набора дан- ных и провайдер (и, возможно даже, соединение). Компонент SimpleDataSet сочета- ет в себе большинство свойств компонентов, которые он заменяет. Использование одного компонента или нескольких компонентов Чтобы создать первую демонстрационную программу, разместите на форме при- ложения компонент SimpleDataSet и настройте имя соединения внутри подкомпо- нента Connection. В значениях свойств CommandType и CommandText укажите, какие данные вы хотели бы извлечь. При помощи свойства PacketRecords укажите, какое количество записей вы хотите извлекать из базы данных в каждом блоке. Вот ключевые свойства компонента в примере DbxSingle: object SimpleDataSetl: TSimpleDataSet Connection.ConnectlonName = 'IBLocal' Connection.LoginPrompt = False DataSet.CommandText = 'EMPLOYEE' DataSet.CommandType = ctTable end Альтернативный подход реализован в примере DbxMulti — в этой программе со- здается полная последовательность всех необходимых компонентов: object SQLConnectlonl: TSQLConnectlon ConnectlonName = 'IBLocal' LoginPrompt = False end object SQLDataSetl: TSQLDataSet SQLConnection = SQLConnectlonl CommandText = 'select * from EMPLOYEE' end object DataSetProviderl: TDataSetProvider DataSet = SQLDataSetl end object ClientDataSetl: TClientDataSet ProvlderName = 'DataSetProviderl' end object DataSourcel: TDataSource DataSet = ClientDataSetl end В обоих примерах используются несколько визуальных компонентов: сетка и панель инструментов, основанная на архитектуре диспетчера действий (action nianager). ^несение изменений в базу данных В любых приложениях, в которых используется локальный кэш (реализованный 8 Рамках таких компонентов, как ClientDataSet и SimpleDataSet), важно следить За тем, чтобы изменения данных, выполненные пользователем локально, были 0657
658 Глава 14. Клиент-серверная архитектура с использованием dbExpress перенесены в базу данных, расположенную на сервере. Как правило, для этого ис- пользуется метод ApplyUpdates. Вы можете либо в течение некоторого времени со- хранять локальные изменения в локальном кэше, а затем за один прием перемес- тить все эти изменения в базу данных, либо передавать изменения на сервер каждый раз после того, как пользователь модифицирует данные. В этих двух примерах я ис- пользую второй подход, подключая следующий обработчик событий к событиям AfterPost (которое генерируется после операции редактирования или операции до- бавления записи) и AfterDelete компонента ClientDataSet: procedure TForml.DoUpdate(DataSet: TDataSet); begin // немедленно применяем локальные изменения в отношении базы данных SQLC11entDataSetl.ApplyUpdates(0); end; Если вы хотите, чтобы все внесенные пользователем изменения применялись за один раз, вы можете выполнять обновление базы данных либо в момент закры- тия формы, либо в момент завершения работы программы, либо в момент, когда пользователь отдает специальную команду (для этого можно воспользоваться за- ранее предопределенным действием Delphi 7). Мы рассмотрим этот подход по- зднее, когда будем обсуждать поддержку кэширования в компоненте ClientDataSet. Мониторинг соединения В примерах DbxSingle и DbxMulti используется еще одна интересная возможность: мониторинг при помощи компонента SQLMonitor. В рассматриваемых примерах компонент активируется в момент начала работы программы. В примере DbxSingle компонент SimpleDataSet содержит в себе соединение, поэтому монитор не может быть подключен на этапе проектирования, однако это можно сделать в момент за- пуска программы: procedure TFroml.FormCreate(Sender: TObject); begin SQLMonitori.SQLConnection := SimpleDataSetl.Connection; SQLMonitori.Active : = True; SimpleDataSetl.Active : = True; end: Каждый раз, когда становится доступной строка для протоколирования, ком- понент генерирует событие ОпТгасе, благодаря чему вы можете определить, надо ли вносить эту строку в журнал. Если параметр LogTrace этого события равен True (значение по умолчанию), компонент записывает сообщение в список строк TraceList и генерирует событие OnLogTrace, чтобы оповестить приложение о том, что новая строка была добавлена в журнал. Компонент может также автоматически сохранить журнал в файле, имя кото- рого указано в свойстве FileName, однако эта возможность не используется в при- мере. В демонстрационной программе я осуществляю обработку события ОпТгасе. В рамках этого обработчика содержимое журнала копируется в элемент управле- ния Мето (результат показан на рис. 14.6). Для этого используется следующий код. procedure TForml.SQLMomtorlTraceCSender: TObject; CBInfo: pSQLTRACEDesc; var LogTrace: Boolean); begin Memol.Line := SQLMomtori.TraceList: end; 0658
Несколько демонстрационных программ dbExpress 659 yobwSwiqle I 45 ' INTERBASE • «с dsqLprepare INTERBASE • i$c_d$ql_$ql_info INTERBASE • isc-vax.integer INTERBASE * isc dsql.desctibe bind INTERBASE • J Рис. 14.6. Журнал, сформированный при помощи компонента SQLMonitor, отображается в окне программы DbxSingle INTERBASE • i$c_comrrul_ietaong INTERBASE • isc_d$ql_fiee_statemenl INTERBASE • i$c_slart_lian$acbon INTERBASE • isc_dtql_alocate statement update EMPLOYEE set PHONE-EXT - ? where EMP NO-?and FIRST.NAME - ? and LAST NAME-?and PHONE EXT -? and HIRE DATE-?and DEPT NO-?and JOB_CODE-? and JOB_GRADE -? and JOB_COUNTRY-? and SALARY - ? and FULL_NAME • ? Генерация SQL-кода обновления базы данных Если вы запустите программу DbxSingle и измените, скажем, номер телефона одно- го из сотрудников компании, в журнал будет записана следующая SQL-команда обновления БД: update EMPLOYEE set PHONE_EXT = ? where EMP_NO = ? and FIRST_NAME = ? and LAST_NAME = ? and PHONE_EXT = ? and HIRE_DATE = ? and DEPT_NO = ? and JOB_CODE = ? and JOB_GRADE = ? and JOB_COUNTRY = ? and SALARY = ? and FULL_NAME = ? Компонент SimpleDataSet не позволяет видоизменять этот код путем изменения значения какого-либо из свойств (в отличие от него компонент SQLCIientDataSet обладает свойством UpdateMode, используя которое, вы можете видоизменить вы- ражение обновления БД). В примере DbxMulti вы можете воспользоваться свойством UpdateMode компо- нента DataSetProvider. Этому свойству можно присвоить одно из двух значений: uPWhereChanged или upWhereKeyOnly. Первое из значений приводит к генерации сле- дующего SQL-кода: uPdate EMPLOYEE set PHONE EXT - ? where 0659
660 Глава 14. Клиент-серверная архитектура с использованием dbExpress EMP_NO = ? and PHONEJXT = ? При использовании второго значения генерируется следующий SQL-код: update EMPLOYEE set PHONE_EXT = ? where EMP_NO = ? СОВЕТ----------------------------------------------------------------------------- Этот результат значительно лучше, чем в среде Delphi 6 (в изначальной версии без заплаток), в которой подобная операция приводила к возникновению ошибки неправильной настройки ключе- вого поля. Если вы хотите в большей степени контролировать процесс генерации SQL- выражений обновления информации в БД, вы можете использовать поля одного из наборов данных, расположенных на более низком уровне. Например, компо- нент SimpLeDataSet поддерживает два редактора полей: один для базового компо- нента ClientDataSet (наследником которого он является) и второй — для содержа- щегося внутри него компонента SQLDataSet. Я добавил аналогичные изменения в пример DbxMulti. В компонент SQLDataSet были добавлены постоянные поля, кроме того, были модифицированы параметры провайдера для некоторых по- лей, чтобы добавить эти поля в ключ или исключить их из списка обновляе- мых полей. ПРИМЕЧАНИЕ------------------------------------------------------------------ Мы обсудим проблему этого типа вновь, когда будем изучать подробнее компонент ClientDataSet, провайдер, резольвер и другие технические проблемы позже в данной главе, а также в главе 16. Доступ к метаданным базы данных с использованием SetSchemalnfo В любой системе RDBMS существует специальная таблица (как правило, ее назы- вают системной таблицей (system table)), которая используется для хранения ме- таданных, таких как перечень таблиц БД, входящие в состав этих таблиц поля, индексы и ограничения, а также разного рода другая системная информация. По- мимо того, что dbExpress обеспечивает унифицированный API для работы с раз- личными серверами SQL, эта библиотека поддерживает также стандартный спо- соб доступа к метаданным. В составе компонента SQLDataSet присутствует метод SetSchemalnfo, который заполняет набор данных системной информацией. Метод SetSchemalnfo принимает три параметра. О SchemaType указывает тип необходимой информации. Этот параметр может обладать одним из пяти значений: stTables, stSysTables, stProcedures, stColumns и stProcedureParams. О SchemaObject указывает объект, на который вы ссылаетесь, например имя таб лицы, перечень столбцов которой вы хотели бы получить. О SchemaPattern фильтр, который позволяет вам ограничить ваш запрос табли цами, столбцами или процедурами, начинающимися с заданного набора букв- 0660
Несколько демонстрационных программ dbExpress 661 Это удобно в случае, если вы используете префиксы для идентификации груп- пы элементов. В частности, в программе SchemaTest кнопка Tables читает в набор данных все таблицы базы данных: ClientDataSetl.Close. SQLDataSetl SetSchemalnfo (stTables, ". "); ClientDataSetl Open, Программа использует традиционный набор компонентов, включающий в себя провайдера, клиентский набор данных и источник данных. Результирующие дан- ные отображаются в сетке, как показано на рис. 14.7. Получив из базы данных пе- речень таблиц, вы можете щелкнуть на одной из строк сетки, а затем щелкнуть на кнопке Fields; в результате будет отображен список полей выбранной таблицы. Для этого используется следующий код: SQLDataSetl SetSchemalnfo (stColumns, ClientDataSetl! 'TableJJame']. "), ClientDataSetl Close. ClientDataSetl Open. SchemaTest 1(Ж1 fm» | ► 1 <NULL> SYSDBA 2 2 <NULL> SYSDBA 2 3 <NULL> SYSDBA 4 <NULL> SYSDBA 2 5 <NULL> SYSDBA 2 6 <NULL> SYSDBA 2 7 <NULL> SYSDBA 8 <NULL> SYSDBA 2 9 <NULL> SYSDBA 2 Ю <NULL> SYSDBA 2 11 <NULL> SYSDBA 12 <NULL> SYSDBA COUNTRY CUSTOMER DEPARTMENT EMPLOYEE EMPLOYEE.PROJECT ITEMS JOB PHONEJJST PROJECT PROJ_DEPT_BUDGET SALARY_HISTORY SALES Hables Рис. 14.7. Программа SchemaTest отображает перечень таблиц в базе данных, а также перечень столбцов в любой из этих таблиц Помимо доступа к метаданным БД библиотека dbExpress поддерживает доступ к своей собственной конфигурационной информации. В частности, вы можете уз- Нать перечень установленных драйверов и настроенных соединений. Для этой цели в модуле DbConnAdmin определяется класс TConnectionAdmin, однако данная возмож- ность предназначена в основном для разработчиков (обычные конечные пользо- ватели, как правило, не работают одновременно с несколькими базами данных, Динамически переключаясь между ними). СОВЕТ —__________________________________________________________________________ Доступ к конфигурационным файлам dbExpress и схеме БД иллюстрируется в демонстрационной Д522^мме DbxExplorer, входящей в состав комплекта Delphi. 0661
662 Глава 14. Клиент-серверная архитектура с использованием dbExpresc Запрос с параметром Если вы намерены использовать несколько мало отличающихся друг от друга вер- сий одного и того же запроса SQL, вместо того чтобы каждый раз модифицировать текст запроса, вы можете добавить в него параметр. Например, если вы решили предоставить пользователю возможность выбора сотрудников в заданной стране (используя таблицу employee), вы можете написать следующий запрос с парамет- ром: select * from emplоуее ' where job_country = :country В данном SQL-выражении ‘.country — это параметр. Вы можете настроить его тип данных и стартовое значение при помощи редактора свойства Params компо- нента SQLDataSet. В рабочем окне редактора свойства Params (которое показано на рис. 14.8), вы можете видеть список параметров, определенных внутри выражения SQL. В окне Object Inspector вы можете настроить начальное значение и тип дан- ных каждого из параметров. Рис. 14.8. Редактирование набора параметров компонента SQLDataSet Следующий пример — программа ParQuery — использует ниспадающий комби- нированный список, в котором показаны все возможные значения для парамет- ров. Вместо того чтобы настроить содержимое комбинированного списка на этапе проектирования, вы можете извлечь необходимые данные прямо из базы данных в момент запуска программы. Для этого используется еще один компонент-запрос со следующим SQL-выражением: select distinct job_country from employee После активизации этого запроса программа сканирует результирующий на- бор, извлекая значения и добавляя их в список: procedure TQueryForm.FormCreate(Sender: TObject): begin SqlDataSet2.0pen: while not SqlDataSet2.EDF do begin ComboBoxl.Items.Add (SqlDataSet2.Fields [0].AsString): 0662
Когда одного направления достаточно: распечатка данных 663 SqlDataSet2.Next; end; ComboBoxl Text := ComboBoxl.Items[0]; end; Пользователь может выбрать в списке одно из значений параметра и после это- го щелкнуть на кнопке Select (объект Buttonl), чтобы изменить значение парамет- ра и заново активировать запрос: procedure TQueryForm.ButtonlClick(Sender: TObject): begin SqWataSetl.Close; ClientDataSetl.Close; Queryl Params[0J.Value •= ListBoxl.Items [Listboxl.Itemindex]; SqlDataSetl.Open; ClientDataSetl.Open. end: Этот код отображает в сетке компонента DBGrid сотрудников компании из выбранной страны. Результат его работы показан на рис. 14.9. Вместо того чтобы обращаться к элементам массива Params, указывая их порядковый номер, вы може- те воспользоваться методом ParamsByName. Благодаря этому вы можете избежать проблем, возникающих в случае, если в результате модификации запроса порядок параметров меняется. %* Par Query ^England "у] *| н| ' ' ' ' emp.no ImCNAME £ 28 Am Bennet 5 2/1/1991 120 Ad , л 36 Roger Reeves 6 4/25/1991 120 Sa" 4 и 37 W«e Stansbury 7 4/25/1991 120 En*^ Рис. 14.9. Программа ParQuery во время исполнения Используя запросы с параметром, вы существенно снижаете количество дан- ных, передаваемых от сервера клиенту, и при этом вы сможете использовать ком- понент DBGrid и стандартный пользовательский интерфейс, характерный для при- ложений, работающих с локальными базами данных. СОВЕТ-----------------------------------------------------------------------— Запросы с параметрами часто употребляются для формирования архитектур «основное/подробно- сти» (master/detail) с использованием запросов SQL — по крайней мере, именно это пытается де- лать сама среда Delphi. Свойство DataStore компонента SQLDataSet автоматически заменяет значения параметра полями главного набора данных, обладающих тем же именем, что и параметр. Когда одного направления достаточно: Распечатка данных Ранее уже отмечалось, что библиотека dbExpress возвращает только однонаправ- ленные наборы данных. Вы можете использовать компонент ClientDataSet (или одну 0663
664 Глава 14. Клиент-серверная архитектура с использованием dbExpress из его инкарнаций) для сохранения записей в локальном кэше. Давайте теперь рассмотрим пример программы, в которой однонаправленный набор данных — эТо все, что нужно. Подобная ситуация характерна для программ, осуществляющих формирование отчетов (reporting). Любая подобная программа последовательно перебирает за- писи набора данных одну за другой, двигаясь в одном направлении и, в общем слу- чае, не возвращаясь к предыдущим записям. Программы этой категории формиру- ют распечатываемые отчеты (используя набор специальных компонентов или напрямую обращаясь к принтеру), передают данные другим приложениям, таким как Microsoft Excel или Word, сохраняют данные в виде файлов (включая форма- ты HTML и XML) и выполняют многие другие подобные процедуры. Я не собираюсь углубляться в описание сопутствующих особенностей HTML и XML, поэтому в данном разделе будет рассмотрен пример программы, которая просто распечатывает информацию из базы данных на экране или на принтере. В ней нет никаких хитрых приемов и не используется никаких специальных ком- понентов. Я применяю самую простую поддерживаемую в Delphi методику печа- ти, а именно соединяю файл с принтером при помощи RTL-процедуры AssignPrn. Программа называется UniPrint. В ней однонаправленный компонент SQLDataSet ассоциирован с соединением InterBase и основан на следующем выражении SQL (выражение соединяет таблицу сотрудников с таблицей департаментов и для каж- дого сотрудника показывает, в каком отделе он работает): select d DEPARTMENT, е FULL_NAME. е JOB-COUNTRY е HIRE_DATE from EMPLOYEE e inner join DEPARTMENT d on d DEPT_NO = e DEPT_NO Для выполнения печати я написал универсальную подпрограмму, принимаю- щую в качестве параметров данные, которые необходимо распечатать, индикатор завершенности ProgressBar для отображения статистики, шрифт и максимальный форматный размер каждого поля. Подпрограмма использует механизм поддерж- ки печати на принтере или в файл. Каждое поле распечатывается в виде выровнен- ной по левой границе строки фиксированного размера. Весь отчет отображается на экране в виде нескольких столбцов. При обращении к функции Format исполь- зуется строка формата, которая генерируется автоматически с использованием информации о форматном размере каждого из полей. В листинге 14.1 представлен исходный код основного метода PrintOutDataSet, в котором используются три вложенные блока try/fi nally, обеспечивающие коррек- тное освобождение ресурсов. Листинг 14.1. Основной метод примера UniPrint procedure PrintOutDataSet (data TDataSet. progress TPRogressBar, Font TFont toFile Boolean. maxSize Integer = 30). var PrintFile TextFile, I Integer. sizeStr string oldFont TFontRecall. begin // связываем вывод с принтером или с файлом if toFile then begin 0664
Когда одного направления достаточно: распечатка данных 665 Sei ectDirectory ('Choose a folder'. ". strDir), AssignFile (PrintFile. IncludeTrailingPathDelimiter(strDir) + 'output txt"). end else AssignPrn (PrintFile). 11 ассоциируем принтер с файлом AssignPrn (PrintFile). Rewrite (PrintFile). // настраиваем шрифт и сохраняем изначальный шрифт oldFont = TFontRecal1 Create (Printer Canvas Font). try Printer Canvas Font = Font. try data Open try // распечатываем заголовок (имена полей) полужирным шрифтом Printer Canvas Font Style = [fsBold], for I =0 to data FieldCount - 1 do begin sizeStr = IntToStr (min (data Fields[i] DisplayWidth. maxSize)). Write (PrintFile. Format ('^-' + sizeStr + 's'. [data Fields[i] FieldsName])). end. Wnteln (PrintFile). // для каждой записи набора данных Printer Canvas Font Style = []. while not data EOF do begin // распечатываем каждое поле записи for I =0 to data FieldCount - 1 do begin sizeStr = IntToStr (min (data Fields[i] DisplayWidth maxSize)). Write (PrintFile. Format ('^-' + sizeStr + 's'. [data Fields[i] AsString])). end. Writein (PrintFile) // изменяем ProgressBar (индикатор завершенности) progress Position = progress Position + 1. data Next. end. finally /I Закрываем набор данных data Close. end finally // восстанавливаем изначальный шрифт принтера oldFont Free. end finally Il закрываем принтер/файл CloseFile (PrintFile). end end 0665
666 Глава 14. Клиент-серверная архитектура с использованием dbExpress Программа обращается к этой функции в момент, когда вы щелкаете на кнопке Print All (Распечатать все). В результате выполняется отдельный запрос (select count(*) from EMPLOYEE), который позволяет узнать количество записей в таблице сотрудников. Этот запрос необходим для того, чтобы настроить индикатор завер- шенности (однонаправленный набор данных не позволяет узнать, какое количе- ство записей в нем находится, до тех пор пока вы не извлечете последнюю запись) После этого настраивается шрифт, при помощи которого будет выполняться пе- чать. Затем происходит обращение к подпрограмме PrintOutDataSet: procedure TNavigator.PrintAl1ButtonClick(Sender: TObject): var Font: TFont: begin // настраиваем диапазон индикатора завершенности (ProgressBar) EmplCountData.Open: try ProgressBarl.Max := EmplCountOata.F1elds[OJ.AsInteger: finally EmplCountData.Close: end: Font := TFont.Create: try Font.Name := 'Courirer New'; Font.Size := 9; PrintOutDataSet (EmplData, ProgressBarl. Font. cbFile.Checked): finally Font.Free: end: end: Пакеты и кэш Компонент ClientDataSet читает данные пакетами, в каждый из которых включено количество записей, указанное в свойстве PacketRecords. По умолчанию в этом свой- стве содержится значение -1, это означает, что провайдер извлекает все записи за одну операцию (такой подход оправдан только в случае, если набор данных обла- дает небольшим размером). Если вы присвоите этому свойству значение 0, сервер вернет только дескрипторы полей и никаких данных. Любое положительное зна- чение определяет количество записей в пакете. Если вы извлекаете лишь часть набора данных, в локальной памяти сохраняется лишь часть всех записей набора. Как только вы перемещаетесь за границу локаль- ного кэша, действия системы определяются значением свойства FetchOn Deman • Если это свойство равно значению True (это его значение по умолчанию), при вы ходе за границу кэша компонент ClientDataSet попытается автоматически получить из источника данных следующую порцию записей. Значение этого поля определи ет также порядок извлечения полей BLOB и вложенных наборов данных (значе ния могут не входить в состав пакета, полученного из базы данных, — это опреДе ляется одним из параметров Options провайдера набора данных). 0666
Пакеты и кэш 667 Если свойство FetchOnDemand равно значению False, вы самостоятельно должны извлечь из базы данных дополнительные записи, для чего необходимо обратиться к методу GetNextPacket. Вы можете обращаться к этому методу до тех пор, пока он не вернет значение 0. (Для извлечения полей BLOB и вложенных наборов данных следует воспользоваться методами FetchBlobs и FetchDetails.) ВНИМАНИЕ------------------------------------------------------------------- Имейте в виду, что прежде чем настроить индекс для ваших данных, вы должны извлечь из БД весь набор данных целиком. Для этого можно либо переместиться к его последней записи, либо присво- ить свойству PacketRecords значение -1. В противном случае индекс будет сформирован на частич- ных данных, что может привести к неправильным результатам его использования. Манипулирование обновлениями Одно из основных назначений компонента ClientDataSet — это локальное кэширо- вание данных. Компонент сохраняет данные, вводимые пользователем, в локаль- ном кэше, а затем, когда возникает такая необходимость, передает в базу данных сведения о внесенных пользователем изменениях. Компонент хранит в себе, во- первых, список изменений, которые необходимо внести в сервер базы данных (эти данные хранятся в формате, сходном с форматом компонента ClientDataSet, и дос- тупны через свойство Delta этого компонента), а во-вторых, журнал изменений, которым вы можете манипулировать при помощи нескольких специальных мето- дов (включая возможность отмены последнего изменения — Undo). СОВЕТ-------------------------------------------------------- В Delphi 7 операции Undo (отмена последнего действия) и ApplyUpdates (внести изменения в БД) можно инициировать также при помощи заранее предопределенных действий. Состояние записей Компонент позволяет вам следить за тем, что происходит внутри пакетов данных. Каждая запись из пакета, хранящегося в локальном кэше, может находиться в од- ном из состояний: не модифицирована, модифицирована, вставлена, удалена. Ин- формацию о текущем состоянии записи возвращает метод UpdateStatus: type TUpdateStatus - (usUnmodofied. usModlf1ed. uslnserted. usDeleted): Чтобы упростить процедуру определения текущего состояния записи в наборе Данных, вы можете добавить в этот набор дополнительное вычисляемое поле стро- кового типа (я назвал его ClientDataSetlStatus) и вычислять его значение при помо- щи следующего обработчика события OnCalcFields: Procedure TForml.ClientDataSetlCalcFields(DataSet: TDataSet): begin ClientDataSetlStatus.AsString GetEnumName (Typelnfo(TUpdateStatus). Integer (ClientDataSetl.UpdateStatus)): Этот метод (в котором используется функция RTTI под названием GetEnum- ame) преобразует текущее значение перечисления TUpdateStatus в строку. Резуль- тат показан на рис. 14.10. 0667
668 Глава 14. Клиент-серверная архитектура с использованием dbExpresc Client 3 Tier s Update I Show delta 1 Undo Data | Status |p£PU©|ew> NO IF1RST.NAME |tAST_NAME jPHOME_.EXTj SALARY |»| о □«Unmodified 115 118 Takashi Yamamoto 23 X usUnmodtfied 125 121 Roberto Ferrari 1 $ V usUnmodrfied 100 127 Michael Yanowski 492 i 123 134 Jacques Glon 937 й usUnmodified 623 136 Scott Johnson 265 □«Unmodified 621 138 T J Green 218 □«Unmodified 672 144 John Montgomery 820 1 1 □«Modified 622 145 Mark Guckenheimer 931 □«Inserted u 622 146 John Roland 932 *1 лГ Рис. 14.10. Программа CdsDelta отображает состояние каждой записи набора данных ClientDataSet Доступ к Delta Чтобы понять, какие изменения были внесены в набор данных ClientDataSet, но не были переданы на сервер БД, удобнее всего обратиться к области delta — в этой области хранится список изменений, которые ожидают передачи на сервер. Обра- титься к области delta можно при помощи одноименного свойства Delta. Это свой- ство определяется следующим образом: property Delta: 01 eVari ant: Формат этого свойства точно такой же, какой используется для хранения дан- ных компонента ClientDataSet. Это значит, что вы можете добавить в программу еще один компонент ClientDataSet и подключить его к данным в свойстве Delta пер- вого компонента ClientDataSet. Вот соответствующий код: if ClientDataSetl.ChangeCount > 0 then begin ClientDataSet2.Data = ClientDataSetl Delta. ClientDataSet2 Open: В программе CdsDelta я создал модуль данных с двумя компонентами ClientDataSet и источником данных — компонентом SQLDataSet, подключенным к демонстраци- онной таблице EMPLOYEE сервера InterBase. Оба компонента ClientDataSet содержат в себе дополнительное вычисляемое поле состояния записи. Обработку этого поля осуществляет код, который несколько отличается от рассмотренного ранее анало- гичного обработчика — отличия связаны с тем, что один и тот же обработчик ис- пользуется одновременно обоими компонентами. СОВЕТ----------------------------------------------------------------------- Чтобы создать постоянные поля компонента ClientDataSet, подключаемого к delta (на нения), во время проектирования я временно подключил его к основному провайдеру Структура delta совпадает со структурой набора данных, к которому принадлежит эта область. После того как все постоянные поля были созданы, я удалил эту связь. ________ этапе испол- ClientDataSet. На форме приложения располагаются также две вкладки, на каждой из кото рых отображается сетка DBGrid. На первой вкладке показывается сетка с данными, на второй вкладке — содержимое области delta. Код скрывает или отображает вт° 0668
Пакеты и кэш 669 рую вкладку в зависимости от того, содержатся ли какие-либо данные в журнале изменений. Узнать об этом можно при помощи метода ChangeCount. Основной код, выполняющий обработку delta, сходен с приведенным ранее фрагментом. Вы мо- жете более подробно изучить этот код, обратившись к архиву исходного кода, при- лагаемому к книге. На рис. 14.11 показан журнал изменений приложения CdsDelta. Обратите вни- мание, что набор данных delta содержит две строки для каждой модифицирован- ной записи: изначальное значение и значение модифицированного поля. Но для любой вставленной или удаленной записи строка всего одна. Client 3 Tier Nelson 2 Robert 105900 600 ' “ ’ II в I » X X X X X Wm*....... usUnmodtfed usModified «•Inserted «•Unmodified usModrfied usDeieted usUnmodrfied usModrfied 622 622 121 123 146 John 145 Mark 141 Pierre 134 Jacques Roland Guckenherner Osborne Glon 250 251 932 221 931 32000 32000 110000 390500 937 Рис. 14.11. Программа CdsDelta позволяет вам видеть запросы на обновление БД, временно сохраненные в свойстве Delta компонента ClientDataSet СОВЕТ------------------------------------------------------------------------ Свойство StatusFilter набора данных позволяет фильтровать набор данных Delta (или любой другой компонент ClientDataSet) в зависимости от текущего состояния записей. Например, вы можете на Одной вкладке показать только новые записи набора, на второй — только модифицированные, на третьей — только удаленные записи. Обновление данных Теперь, когда вы лучше понимаете, что происходит в ходе локальных обновлений Данных, вы можете попробовать заставить программу передавать локальные об- новления (сохраненные в delta) на сервер базы данных. Для этого используется метод Apply Updates. Если вы хотите, чтобы за один раз на сервер были переданы абсолютно все обновления, передайте этому методу в качестве параметра значе- ние -1. Если в процессе обновления данных у провайдера (или у компонента-резоль- ьера внутри провайдера) возникают проблемы, генерируется событие OnRecon- , ^leError. Это событие может возникнуть, когда два пользователя в одно и то же ! время пытаются выполнить обновление. В приложениях типа «клиент-сервер», 1 как правило, используется оптимистическая блокировка, поэтому попытка одно- .. вРеменного обновления считается нормой. Событие OnReconcileError позволяет вам модифицировать параметр Action (пе- ; ^Даваемый по ссылке), который определяет, каким образом сервер должен вести ьСебя в подобной ситуации: 0669
670 Глава 14. Клиент-серверная архитектура с использованием dbExpress procedure TForml ClientDataSetlReconcileError(DataSet TClientDataSet. E EReconcileError, UpdateKind TUpdateKind. var Action TReconcileAction) Этот метод принимает три параметра: компонент ClientDataSet (на случай, если в программе существует несколько наборов данных); исключение, которое вызва- ло ошибку (с сообщением об ошибке); тип операции, вызвавшей сбой (ukModify, uklnsert или ukDelete). Возвращаемое значение, которое вы должны сохранить в параметре Action, может быть одним из следующих: type TReconcileAction = (raSkip raAbort raMerge raCorrect, raCancel, raRefresh), Эти значения имеют следующий смысл: О raSkip — сервер должен пропустить конфликтующую запись, оставив ее в обла- сти delta (это значение используется по умолчанию); О raAbort — сервер прерывает всю операцию обновления и не пытается приме- нить в отношении базы данных оставшиеся в области delta изменения; О raMerge — сервер смешивает данные, переданные клиентом, с данными, храня- щимися в БД, иными словами, в базу данных заносятся только те поля, значе- ния которых изменены данным клиентом (другие поля могут быть модифици- рованы другим клиентом); О raCorrect — сервер полностью заменяет данные в БД данными, принятыми от клиента, при этом любые изменения, внесенные другими клиентами, теряются; О raCancel — запрос на модификацию полностью отменяется, из области delta уда- ляется соответствующая строка, значения полей восстанавливаются, то есть становятся равными значениям, изначально извлеченным из базы данных (при этом изменения, внесенные другими клиентами, игнорируются); О raRefresh — обновление в области delta клиента отбрасывается, значения заме- няются значениями сервера (иными словами, сохраняются изменения, внесен- ные другими клиентами). Чтобы протестировать подобную коллизию, вы можете запустить две копии клиентской программы, изменить одну и ту же запись в обоих этих приложениях, а после этого попытаться опубликовать обновления в обоих приложениях. Мы с вами попробуем сделать это позднее для того, чтобы сгенерировать ошибку, од- нако вначале давайте рассмотрим обработку события OnReconcileError. Обеспечить обработку этого события совсем не сложно при условии, что вы воспользуетесь специальной возможностью Delphi IDE. Построение специальной формы для обработки события OnReconcileError — это достаточно часто встречаю- щаяся задача, поэтому Delphi позволяет создать такую форму при помощи Object Repository. В главном меню Delphi IDE выберите File ► New ► Other (Файл ► Соз- дать ► Другое). Перейдите на вкладку Dialogs (Диалоговые окна) и выберите Recon- cile Error Dialog (Диалоговое окно согласования коллизии). Этот модуль экспорти- рует функцию, которую вы можете напрямую использовать, чтобы инициализи- ровать и отобразить соответствующее диалоговое окно. Именно это реализовано в примере CdsDelta: procedure TOmCds cdsEmployeeReconcileError (DataSet TCustomCllentDataSet E EReconcileError UpdateKind TUpdateKind. var Action TReconcileAction) begin 0670
Пакеты и кэш 671 Action - HandleReconcileErrorCDataSet. UpdateKind. Е). end: ВНИМАНИЕ ---------------------------------------------------------------------------- Исходный код модуля Reconcile Error Dialog предлагает воспользоваться диалоговым окном Project Options (Параметры проекта), чтобы исключить эту форму из списка автоматически создаваемых форм проекта. Если вы не сделаете этого, во время компиляции проекта возникнет ошибка. Конеч- но же, делать это необходимо только в случае, если вы не настроили Delphi на отказ от автомати- ческого создания форм. Функция HandleReconcileError создает диалоговое окно и отображает его на эк- ране, как можно видеть в коде, разработанном компанией Borland: function HandleREconcileErrorCDataSet. TDataSet. UpdateKind TUpdateKind. Reconci 1 eError EReconcileError). TReconcileAction. var UpdateForm TReconcileErrorForm. begin UpdateForm - TReconcileErrorForm CreateForm(DataSet. UpdateKind. ReconcileError). with UpdateForm do try if ShowModal - mrOK then begin Result - TReconcileActionCActionGroup Items Objects! ActionGroup Itemindex]). if Result = raCorrect then SetFieldVa1ues(DataSet). end else Result - raAbort. finally Free. end. end. Модуль Reconc, в котором содержится диалоговое окно Reconcile Error (чтобы было понятно конечным пользователям, окно наделено заголовком Update Error (Ошибка обновления базы данных)), включающее в себя более 350 строк кода, поэтому я не буду рассматривать его здесь в подробностях. Однако вы должны понимать устрой- ство этого кода, для этого вам придется внимательно изучить его. Конечно же, вы можете попробовать использовать этот код, не вникая в то, как он устроен. Диалоговое окно отображается на экране в случае возникновения ошибки об- новления базы данных. В окне показывается изменение, которое привело к воз- никновению конфликта, при этом пользователь должен выбрать один из вариан- тов разрешения коллизии. Внешний вид формы представлен на рис. 14.12. Вы можете воспользоваться этой формой на этапе выполнения. СОВЕТ----------------------------------------------------------------------------------— Когда вы обращаетесь к методу ApplyUpdates, вы инициируете сложную последовательность обнов- ления, которая более подробно описывается в главе 16, где речь идет о многозвенных архитекту- рах. Если говорить коротко, дельта (delta) передается провайдеру, который генерирует событие r^UpdateData, а затем воспринимает событие BeforeUpdateRecord для каждой обновляемой записи. события являются двумя местами, в которых вы можете взглянуть на вносимые изменения и Ф^Рснровать выполнение каких-либо специфических операций. 0671
672 Глава 14. Клиент-серверная архитектура с использованием dbExpresc Рис. 14.12. Диалоговое окно Reconcile Error, которое можно создать при помощи Object Repository Использование транзакций Если вы хотите повысить надежность клиентского приложения, работающего с SQL-сервером, вы должны использовать транзакции. Можно сказать, что тран- закция — это последовательность операций, которая должна рассматриваться как единое целое, то есть нечто, не разбиваемое на части. Чтобы прояснить концепцию, приведу пару примеров. Представьте, что вы на- мерены увеличить зарплату каждого из сотрудников компании на фиксированное количество процентов (именно такая задача решается в примере Total главы 13). Как правило, для этого необходимо выполнить последовательность SQL-запро- сов — по одному для каждой записи из таблицы сотрудников. Представьте, что во время выполнения этой последовательности возникает сбой. В результате может оказаться, что у части сотрудников зарплата повысилась, в то время как у другой части она осталась неизменной. Подобное положение неприемлемо. Иными сло- вами, нам бы хотелось, чтобы зарплата либо повысилась абсолютно у всех сотруд- ников, либо вообще не повышалась (изменения зарплаты, внесенные в таблицу перед сбоем, хотелось бы отменить). В этом случае операцию повышения зарпла- ты сотрудников, состоящую из последовательности SQL-запросов, можно рассмат- ривать как транзакцию. Ее необходимо либо выполнить полностью, либо, в случае возникновения сбоя, полностью отменить. Вот еще один пример: финансовые тран- закции. Финансовая транзакция предусматривает изъятие денег из одного места и добавление этих денег в другое место. Если во время такой операции возникает сбой, то либо деньги исчезают в обоих местах, либо появляются лишние деньги. Чтобы этого не происходило, подобную операцию рассматривают как транзакцию- Механизм транзакций является для программиста, работающего с базой дан- ных, весьма мощным инструментом. Вы можете начать транзакцию и выполнить несколько операций, которые будут рассматриваться системой как единая более крупная операция. Далее вы можете либо подтвердить выполнение транзакции (внесенные в базу данных изменения считаются окончательными), либо отменить транзакцию (операции, выполненные в рамках транзакции, отменяются, и база данных возвращается в изначальное состояние). Как правило, необходимость от мены транзакции возникает в случае возникновения ошибки. 0672
. пакеты и кэш 673 Важно подчеркнуть также еще один аспект: транзакции обеспечивают коррект- ное чтение данных из БД. До тех пор пока транзакция не будет подтверждена, дру- гие соединения и/или транзакции не должны «видеть» изменений данных, вноси- мых в БД в рамках этой транзакции. Данные становятся доступны для остальных клиентов и приложений только после того, как транзакция считается успешно за- вершившейся. Исключением является случай, когда клиент читает данные в рам- ках другой транзакции — тогда он будет читать из БД раз за разом одни и те же данные, игнорируя вносимые в базу изменения, — это может потребоваться для выполнения сложного анализа или комплексной процедуры формирования отче- та. Различные SQL-серверы поддерживают различные уровни изоляции транзак- ций, позволяя работать с данными в рамках транзакций с использованием некото- рых или всех упомянутых альтернатив. В Delphi транзакциями пользоваться достаточно легко. По умолчанию каждая процедура публикации данных считается отдельной транзакцией, однако вы мо- жете изменить это поведение, выполнив обработку операций явно. Для работы с механизмом транзакций используются следующие три метода компонента SQL- Connection библиотеки dbExpress (компоненты соединений с другими базами дан- ных поддерживают аналогичные методы): О StartTransaction отмечает начало транзакции; О Commit подтверждает все операции, выполненные в рамках транзакции в отно- шении базы данных; О Rollback возвращает базу данных в состояние, в котором она находилась до на- чала транзакции. Свойство InTransaction позволяет определить, является ли транзакция актив- ной в настоящий момент. Часто для обслуживания транзакций можно использо- вать блок try: если возникает исключение, транзакция отменяется (Rollback), если последняя операция блока try выполняется успешно и во время исполнения блока не возникло ошибок, транзакция подтверждается (Commit). Код может выглядеть следующим образом: var TD: TTransactionDesc: begin TD.TransactionlD .= 1: TD.IsolationLevel .= xilREADCOMMITED. SQLConnectionl StartTransactron(TD): try // -- здесь располагаются операции, выполняемые в рамках транзакции -- SQLConnectionl.Сошли t(ТО); except SQLConnectionl RolIback(TD); end; Каждый метод, управляющий транзакцией, принимает параметр, который иден- тифицирует транзакцию, в отношении которой должен быть выполнен этот метод. Данный параметр — это значение типа TTransactionDesc, в нем хранится идентифи- катор (ID) транзакции и уровень изоляции транзакции. Уровень изоляции тран- закции определяет поведение транзакции в случае, если данные изменяются в рам- ках других транзакций. Существует три предопределенных значения: 0673
674 Глава 14. Клиент-серверная архитектура с использованием dbExpress; О tiDirtyRead — изменения, вносимые в БД в рамках транзакции, немедленно ста- новятся видимыми для других транзакций несмотря на то, что транзакция еще не была подтверждена. В некоторых базах данных эта возможность является единственной, она соответствует поведению баз данных без поддержки тран- закций; О tiReadCommited — для других транзакций становятся доступными только те из- менения, которые были подтверждены в рамках данной транзакции. Для боль- шинства баз данных этот режим является предпочтительным; О tiRepeatableRead — скрывает изменения, внесенные в базу в рамках любых тран- закций, инициированных после текущей. Изменения скрываются даже в случае, если они были подтверждены. Иными словами, в рамках транзакции состояние БД воспринимается таким, каким оно было в начале исполнения транзакции, вне зависимости от любых других транзакций. Последовательное обращение к БД в рамках транзакции приведет к одним и тем же результатам вне зависи- мости от выполнения в отношении БД других транзакций. Подобный режим эффективно поддерживается только InterBase и несколькими другими SQL- серверами. СОВЕТ------------------------------------------------------------------------- Чтобы максимизировать производительность, в состав транзакции следует включать наименьшее возможное количество модификаций БД (только те из них, которые действительно являются час- тью одной атомарной операции). Кроме того, транзакция должна выполняться за относительно небольшой период времени. Не рекомендуется включать в состав транзакции процедуры, связан- ные с пользовательским вводом. Пользователь может отлучиться от компьютера, в результате тран- закция будет активной в течение длительного времени. Локальное кэширование изменений, например с использованием ClientDataSet, позволит вам сделать транзакции короткими и быстрыми: вы от- крываете транзакцию для чтения, после этого закрываете ее, а затем открываете транзакцию для записи и вносите в базу данных полный набор всех сделанных пользователем изменений. Помимо уровня изоляции в структуре TTransactionDesc хранится также иденти- фикатор транзакции (ID). Этот идентификатор полезен только в случае, если сер- вер, с которым вы работаете, поддерживает выполнение нескольких параллельных транзакций через одно соединение. Сервер InterBase обладает такой поддержкой. Чтобы узнать, поддерживает ли сервер множественные транзакции и поддержива- ет ли он транзакции вообще, вы можете обратиться к свойствам MultipleTransactions- Supported и TransactionsSupported. Если сервер поддерживает множественные транзакции, вы можете присвоить каждой из транзакций уникальный идентификатор: var ТО: TTransactionDesc begin TO.TransactionlD : = GetTickCount: TD.IsolationLevel : = xilREADCOMMITED: SQLCOnnectionl StartTransaction(TD). SQLDataSetl Transact!onLevel := TO TransactionlD. Вы также можете ассоциировать набор данных с той или иной транзакцией- Для этого свойству Transaction Level каждого набора данных следует присвоить идеи тификатор транзакции, как показано в последнем выражении предыдущего лис тинга. 0674
Использование InterBase Express 675 Для изучения транзакций и экспериментов с уровнями транзакций вы можете использовать приложение TransSample. На рис. 14.13 показано, что переключатель позволяет вам выбирать разнообразные уровни изоляции транзакций, а кнопки позволяют работать с транзакциями и применять обновления или обновлять дан- ные. Чтобы получить представление о функционировании механизма транзакций, вы должны запустить несколько копий программы (подразумевается, что вы обла- даете достаточным количеством лицензий сервера InterBase). Рис. 14.13. Форма приложения TransSample на этапе проектирования. Переключатель слева вверху позволяет устанавливать различные уровни изоляции ПРИМЕЧАНИЕ ---------------------------------------------------------------- Сервер InterBase не поддерживает режим DirtyRead («грязное» чтение), поэтому в программе Trans- Sample вы не сможете воспользоваться этим режимом, если только не подключитесь к другому серверу. Использование InterBase Express Примеры, рассмотренные ранее в данной главе, были построены с использовани- ем библиотеки dbExpress. Эта библиотека обеспечивает соединение с базой дан- ных вне зависимости от типа SQL-сервера. Если ваше клиентское приложение основано на этой библиотеке, вы можете использовать его для подключения к SQL- серверам разных типов (на практике переключение между SQL-серверами выпол- няется не так просто). Таким образом, библиотеку dbExpress удобно использовать в случае, когда вы планируете обеспечить взаимодействие вашего приложения с серверами различных типов или заранее не знаете, с сервером какого типа оно будет соединяться. Однако если вам заранее точно известно, что программа долж- на взаимодействовать с сервером определенного типа, и если использование про- граммы для подключения к серверам других типов не планируется, вы можете ис- пользовать API конкретного SQL-сервера. Возможно, благодаря этому вам удастся в полной мере воспользоваться преимуществами данного API, однако разработанная 0675
676 Глава 14. Клиент-серверная архитектура с использованием dbExpress; вами программа не сможет взаимодействовать с SQL-сервером какого-либо дру. гого сервера. Конечно же, вам не надо будет напрямую работать с функциями API, вместо этого в своем приложении вы используете специальные компоненты Delphi, кото- рые являются оболочкой этого API. Примером подобного семейства компонентов является InterBase Express (IBX). Как нетрудно понять из названия, эти компо- ненты ориентированы на работу с сервером InterBase. Приложения, взаимодей- ствующие с InterBase и основанные на семействе компонентов IBX, должны рабо- тать лучше и быстрее, кроме того, они могут использовать некоторые специальные возможности, поддерживаемые сервером. Например, в состав IBX входит набор административных компонентов, специально разработанных для InterBase 6. ПРИМЕЧАНИЕ ---------------------------------------------------------------- В данной главе я рассматриваю набор компонентов IBX, так как эти компоненты ориентированы на работу с InterBase (SQL-сервер, рассмотрению которого посвящена данная глава), кроме того, IBX — это единственный набор специализированных компонентов, входящий в комплект поставки Delphi. Однако следует учитывать, что существуют другие наборы специализированных компонентов, пред- назначенные для взаимодействия с Oracle, InterBase и другими SQL-серверами. Хорошим примером альтернативы IBX является набор компонентов InterBase Objects (www.ibobjects.com). Наборы данных IBX В состав набора IBX входят специализированные компоненты наборов данных, а также несколько других компонентов. Компоненты наборов данных являются производными от класса TDataSet. Совместно с ними можно использовать все стан- дартные элементы управления Delphi, ориентированные на работу с данными. Кроме того, они поддерживают редактор полей Field Editor и обладают многими другими возможностями, используемыми на этапе разработки. В комплекте при- сутствует несколько компонентов наборов данных. Три из них по своему предна- значению и набору свойств сходны с аналогичными компонентами «таблица- запрос-сохраненная процедура» библиотеки dbExpress. О IBTable напоминает компонент Table и позволяет вам обращаться к таблице или представлению. о IBQuery напоминает компонент Query и позволяет выполнить запрос SQL, воз- вращая результирующий набор данных. Компонент IBQuery может быть исполь- зован совместно с компонентом IBUpdateSQL для получения редактируемого набора данных. О IBStoredProc является аналогом компонента StoredProc и позволяет вам выпол- нить сохраненную процедуру. Как и аналогичные компоненты dbExpress, данные компоненты предназначе- ны для обеспечения совместимости со старыми компонентами BDE, которые использовались в старых приложениях Delphi. Для новых приложений в обше*1 случае вы должны использовать компонент IBDataSet, который позволяет вам ра ботать с редактируемым набором данных, полученным в результате исполнения SQL-запроса select. Фактически, компонент IBDataSet объединяет в себе возмоЖ ности компонентов IBQuery и IBUpdateSQL. В состав InterBase Express входят также другие компоненты, которые не являются наборами данных, однако активно ис пользуются в приложениях, работающих с базами данных. 0676
Использование InterBase Express 677 О IBDatabase выполняет функции компонента SQLConnection из библиотеки dbExpress. В BDE для выполнения некоторых глобальных задач, возлагаемых на IBDatabase, используется аналогичный компонент Session. О IBTransaction обеспечивает полный контроль над транзакциями. Для InterBase очень важно в явной форме использовать транзакции и должным образом изо- лировать каждую транзакцию, используя уровень Snapshot для формирования отчетов и уровень Read Committed для интерактивных форм. Каждый набор дан- ных явно связан с некоторой транзакцией, поэтому вы можете создать несколь- ко параллельных транзакций, выполняемых в отношении одной и той же базы данных, и выбрать, какой набор данных используется в той или иной транзак- ции. О IBSQL позволяет исполнять выражения SQL, которые не возвращают набор дян- ных (например, запросы DDL или выражения update и delete). О IBDatabaselnfo используется для получения информации о структуре и состоя- нии базы данных. О IBSQLMonitor используется для отладки системы (поддерживаемый в Delphi инструмент SQL Monitor ориентирован на работу с BDE). О IBEvents принимает сообщения, публикуемые сервером. Эта группа компонентов обеспечивает более широкие возможности взаимодей- ствия с сервером, чем это возможно с использованием библиотеки dbExpress. На- пример, благодаря наличию специального компонента транзакции вы можете управ- лять несколькими параллельными транзакциями, выполняемыми в отношении одной или нескольких баз данных, кроме того, вы можете создать единственную транзакцию, выполняемую в отношении нескольких баз данных. Компонент IBDatabase позволяет вам создавать базы данных, тестировать соединения и обра- щаться к системным данным. Связанные с этим возможности компонентов Database и Session из состава BDE ограничены. СОВЕТ--------------------------------------------------------------------- Набор данных IBX позволяет вам настраивать автоматическое поведение для генератора таким образом, чтобы он выполнял функции автоматически инкрементируемого поля. Для этого необхо- димо настроить свойство GeneratorField при помощи специального редактора этого свойства. При- мер рассматривается позже в данной главе, в разделе «Генераторы и ID». Административные компоненты IBX На странице InterBase Admin палитры компонентов Delphi располагаются адми- нистративные компоненты InterBase. Эти компоненты позволяют вам управлять конфигурацией и порядком функционирования базы данных. В большинстве слу- чаев обычное клиентское приложение не предназначено для решения сложных административных задач, таких как резервное копирование и слежение за пользо- Вателями, однако некоторые прикладные программы для опытных пользователей м°гут осуществлять поддержку таких возможностей. Большинство административных компонентов обладают именами, объясняю- щими их предназначение: IBConfigService, IBBackupService, IBRestoreService, IBVali- ati°nService, IBStatisticalService, IBLogService, IBSecurityService, IBServerProperties, 0677
678 Глава 14. Клиент-серверная архитектура с использованием dbExpress IBlnstaLL и IBL)ninstall. Я не намерен рассматривать какие-либо примеры использо- вания этих компонентов, так как они предназначены для разработки приложений управления SQL-сервером и в обычных клиентских программах в большинстве случаев не используются. Однако я добавил пару этих компонентов в пример IbxMon, который рассматривается далее в данной главе. Построение примера IBX Чтобы построить программу, использующую IBX, вам потребуется разместить на форме (или в модуле данных) по крайней мере три компонента: IBDatabase, IBTrans- action и компонент набора данных (в данном случае используется IBQuery). Любое приложение IBX требует создания как минимум одного экземпляра двух первых компонентов. Вы не сможете настроить соединение с базой данных внутри набора данных IBX, как это возможно при использовании других наборов данных. Кроме того, как минимум один объект транзакции потребуется для прочтения результи- рующего набора данных. Вот ключевые свойства компонентов в примере IbxEmp: object IBTransactionl TIBTransaction Active = False DefaultDatabase - IBDatabasel end object IBQueryl TIBQuery Database - IBDatabasel Transaction - IBTransactionl CachedUpdates = False SQL Strings - ( SELECT * FROM EMPLOYEE') end object IBDatabasel TIBDatabase DatabaseName - 'C \Program Files\Camon Files\Borland Shared\Data\employee gdb' Params Strings = ( 'user_name=SYSDBA ' 'passwordinasterkey') Loginprompt = False SQLDialect = 1 end Теперь вы можете подключить компонент DataSource к объекту IBQueryl и с лег- костью сформировать интерфейс приложения. В данном случае я указал путь к базе данных Borland, однако не на каждом компьютере существует папка Program Files, кроме того, файлы демонстрационных баз данных Borland могут быть установле- ны в любом месте диска. Эти проблемы решаются в следующем примере. ВНИМАНИЕ----------------------------------------------------------------------—' Обратите внимание на то, что пароль указан непосредственно в коде, — это чрезвычайно опасно с точки зрения защиты данных. Во-первых, программа может подключаться к базе данных только с использованием учетной записи SYSDBA. Во-вторых, если программу запустил пользователь, и имеющий доступа к базе на данном уровне привилегий, он получит возможность несанкциониро ванного доступа к БД. В-третьих, любой пользователь, обладающий возможностью чтения исполн емого файла программы, сможет извлечь из этого файла регистрационное имя и пароль для АосТУ к базе данных. В данном случае я сохранил имя и пароль непосредственно в коде. Это сделано дл того, чтобы упростить тестирование программы — каждый раз, когда я запускаю программу, мне надо вводить имя пользователя и пароль, однако в реальном приложении лучше запрашивать У пол зователя имя и пароль, чтобы обеспечить должный контроль над доступом к данным. __ 0678
Использование InterBase Express 679 Построение редактируемого запроса В примере IbxEmp используется запрос, который не поддерживает редактирова- ние. Чтобы сделать редактирование возможным, необходимо добавить в запрос компонент IBUpdateSQL Это необходимо сделать даже в случае, если запрос триви- ален. Компонент IBQuery отвечает за исполнение SQL-запросов select, в то время как компонент IBUpdateSQL отвечает за исполнение SQL-запросов insert, update и delete. Подобный подход, основанный на использовании двух компонентов, ха- рактерен для приложений BDE. Сходство между упомянутыми компонентами IBX и аналогичными им компонентами BDE позволяет легко адаптировать старые при- ложения BDE для новой архитектуры IBX. Вот код компонентов (в сокращенном виде): object IBQueryl TIBQuery Database = IBDatabasel Transaction = iBTransactionl SQL Strings = ( 'SELECT Employee EMP_NO Department DEPARTMENT Employee FIRST_NAME ’ + ' Employee LAST_NAME Job JOB_TITLE Employee SALARY Employee DEPT_NO. ’+ ' EMployee JOB_CODE Employee JOB_GRADE. Employee JOB_COUNTRY’ ’FROM EMPLOYEE Employee’ ’ INNER JOIN DEPARTMENT Department ’ ON (Department DEPT_NO = Employee DEPT_NO) ’ • INNER JOIN JOB Job' ’ ON (Job JOB_CODE = Employee JOB_CODE) ' ' AND (Job JOB_GRADE = Employee JOB_GRADE) ' ' AND (Job JOB_COUNTRY = Employee JOB_COUNTRY) ’ 'ORDER BY Department DEPARTMENT. Employee LAST_NAME') UpdateObject = IBUpdateSQLl end object IBUpdateSQLl TIBUpdateSQL RefreshSQL Strings = ( SELECT Employee EMP_NO Employee FIRSTNAME Employee LAST_NAME. ' + 'Department DEPARTMENT Job JOB_TITLE Employee SALARY Employee DEPT_NO 'Employee JOB_CODE Employee JOB_GRADE Employee JOB_COUNTRY‘ FROM EMPLOYEE Employee' INNER JOIN DEPARTMENT Department' ON (Department DEPT_NO - Employee DEPT_NO) ’ INNER JOIN JOB Job' ’ON (Job JOB_CODE = Employee JOB_CODE) ’ 'AND (Job JOB_GRADE - Employee JOB_GRADE) ’ 'AND (Job JOB_COUNTRY - Employee JOB_COUNTRY) 'WHERE Employee EMP_NO= EMP_NO') ModifySQL Strings - ( 'update EMPLOYEE set' FIRST_NAME = FIRST_NAME ' LAST_NAME = LASTJAME ' SALARY = SALARY DEPT_NO = DEPT_NO JOB_CODE = JOB_CODE ' JOB_GRADE = JOB_GRADE ' JOB_COUNTRY = JOB-COUNTRY' where' EMP_NO = OLD_EMP_NO') 0679
680 Глава 14. Клиент-серверная архитектура с использованием dbExpress InsertSQL Strings = ( ’insert into EMPLOYEE' ’(FIRST_NAME LAST_NAME SALARY DEPT_NO, JOB_CODE JOB_GRADE JOB_COUNTRY)' 'values' ’( FIRST_NAME LAST_NAME SALARY DEPT_NO JOB_CODE JOB_GRADE. JOB_COUNTRY)') DeleteSQL Strings = ( ’delete from EMPLOYEE ’ 'where EMP_NO = OLD_EMP_NO') end Если вы разрабатываете приложение с нуля, вместо двух компонентов IBQuery и IBUpdateSQL, как правило, удобнее использовать один компонент IBDataSet, кото- рый сочетает в себе возможности обоих компонентов. Различия между использо- ванием двух компонентов и одного компонента минимальны. Подход, основанный на использовании IBQuery и IBUpdateSQL, удобен в ситуации, когда вы переводите старое приложение BDE на использование технологии IBX. В примере IbxUpdSqL я использую обе альтернативы, поэтому вы можете само- стоятельно оценить разницу между этими двумя подходами. Далее приводится костяк DFM-описания единственного компонента IBDataSet: object IBDataSetl TIBDataSet Database = IBDatabasel Transaction = IBTransactionl DeleteSQL Strings = ( ’delete from EMPLOYEE 'where EMP_NO = OLD_EMP_NO’) InsertSQL Strings = ( 'insert into EMPLOYEE' ' (FIRST_NAME LAST_NAME SALARY DEPT_NO JOB_CODE JOB_GRADE ’ + ' JOB_COUNTRY)' 'values' ' ( FIRST_NAME LAST_NAME SALARY DEPT_NO JOB_CODE. ' + ' JOB_GRADE JOB_COUNTRY)') Sei ectSQL Strings = ( ) UpdateRecordTypes = [cusUnmodified cusModified cuslnserted] ModifySQL Strings = ( ) end Если вы подключите IBQueryl или IBDataSetl к источнику данных и запустите программу, то увидите, что поведение идентично. Компоненты не только ведут себя тождественно, но и обладают сходным набором событий и свойств. В программе IbxUpdSqL я сделал ссылку на базу данных немного более гибкой. Вместо того чтобы набирать имя базы данных на этапе проектирования, я извле- каю имя каталога общих данных (Borland Shared) компании Borland из реестра (имя этого каталога записывается в реестр в процессе установки Delphi). Вот код, который исполняется в момент запуска программы: uses Registry procedure TForml FormCreate(Sender TObject) var Reg TRegistry begin Reg = TRegistry Create try 0680
Использование InterBase Express 681 Reg RootKey = HKEY_LOCAL_MACHINE Reg OpenKey('\Software\Borland\Borland Shared\Data‘ False) IBDatabasel DatabaseName = Reg ReadStringl'Rootdir') + '\employee gdb' finail у Reg Free end. EmpDS DataSet Open end. ПРИМЕЧАНИЕ ------------------------------------------------------------- Подробнее о реестре Windows и об INI-файлах рассказывается в главе 8. Еще одна особенность данного примера — присутствие компонента транзак- ции. Ранее я уже говорил, что библиотека InterBase Express делает использование транзакций обязательным условием. Иначе говоря, вы не сможете работать с IBX, не используя транзакций в явной форме. Однако для этого достаточно добавить на форму две кнопки: подтвердить транзакцию и отменить транзакцию. Транзакция инициируется автоматически в момент, когда вы начинаете редактирование лю- бого связанного с ней набора данных. Я также добавил в программу компонент ActionList. Этот компонент включает в себя все стандартные действия БД, кроме того, в компонент добавлены два до- полнительных действия, связанных с под держкой транзакций: подтверждение тран- закции (Commit) и откат транзакции (Rollback). Оба действия доступны в случае, если транзакция активна: procedure Tfroml ActionllpdateTransactionsISender TObject) begin acCommit Enabled = IBTransactionl InTransaction. acRollback Enabled = acCommit Enabled, end При активизации любого из этих действий выполняется основная операция, однако далее необходимо заново открыть набор данных в новой транзакции. Ме- тод CommitRetaining, вместо того чтобы открывать новую транзакцию, позволяет старой транзакции оставаться в открытом состоянии. В этом случае вы сможете продолжать использовать ваши наборы данных, при этом вы будете видеть только свои собственные модификации, но не будете видеть модификаций, которые вно- сятся в набор данных другими пользователями. Вот соответствующий код: procedure TForml acCommitExecute(Sender TObject) begin IBTransactionl CommitRetainting end. procedure TForml acRollbackExecute(Sender TObject) begin IBTransactionl Rollback // заново открываем набор данных в новой транзакции IBTransactionl StartTransaction EmpDS DataSet Open end Последняя операция не относится к какому-то конкретному набору данных, так как я собираюсь добавить к программе дополнительный альтернативный на- &0Р данных. На рис. 14.14 можно видеть, что действия соединены с панелью 0681
682 Глава 14. Клиент-серверная архитектура с использованием dbExpress инструментов. В начале своей работы программа открывает набор данных, а в кон- це работы автоматически закрывает текущую транзакцию. При этом она спраши- вает у пользователя, что делать. Эта процедура выполняется в рамках следующего обработчика события OnCLose. ВНИМАНИЕ-------------------------------------------------------------------- Имейте в виду, что InterBase закрывает любые открытые курсоры, когда транзакция завершается- это означает, что вы должны заново открыть их и обновить данные даже тогда, когда вы не внесли в данные никаких изменений. Однако, подтверждая транзакцию, вы можете попросить InterBase сохранить контекст транзакции, чтобы избежать излишнего выполнения комбинаций закры- тия и открытия наборов данных. Операция сохранения контекста выполняется при помощи коман- ды CommitRetaining, как рассказывалось чуть раньше. InterBase ведет себя таким образом потому, что транзакция соответствует некоторому снимку данных. Когда транзакция завершается, вы долж- ны прочитать данные снова, чтобы узнать о модификациях, которые, возможно, были сделаны другими пользователями. Команда RollbackRetainmg поддерживается в версии 6.0 системы InterBase, однако я решил ее не использовать, так как при откате транзакции программа должна обновить данные в наборе и отобразить на экране изначальные значения, а не обновления, от которых вы решили отказаться. procedure TForml CormCloseCSender TObject. var Action TcloseAction) var nCode Word. begin if IBTransactionl InTransaction then begin nCode = MessageDlg ('Commit Transaction’ (No to rollback)' mtConfirmation mbYesNoCancel 0). case nCode of mrYes IBTransactionl Commit mrNo IBTransactionl Rollback. mrCancel Action - caNone. // не закрываем end. end end. i IbxlIpdSql WWJW |FlftST..HWE llAST.NAME Щ Sue Arne 107 Kevn 105 OhverH 12 Tem 144 John 94 Randy 29 Roger 44 Leslie 114 81 15 Katherre 136 Scott 2 Robert 109 Ke|y 36 Roger 2B Arm 37 W*e O'Brien .Cook Bender Lee Montgomery Wfems De Souza Phong Varker Young Johnson Nelson Brown Reeves Bennet Stansbury joEPAFtTMENT Consumer Electronics Div Consumer Electronics Div Corporate Headquarters Corporate Headquarters Customer Services Customer Services Customer Support Customer Support Customer Support Customer Support Customer Support Engreemg Engreerrg European Headquarters European Headquarters European Headquarters 1Ж.ТШЕ 1 '1^7} Administrative Assutant Died or Chief Executive Officer Admnstrative Assistant Engreer Manager Engreer Engreer Engreer Manager Technical Writer Vice President Admnstratrve Assistant Sales Co-ordnator Admnstrative Assistant Engreer 31275 111262 5 212850 53793 35000 56295 6948263 56034 38 35000 67241 25 60000 105900 27000 33620 63 22935 39224 06 1 6 ' 6 0 о • 6 6 6 p 6 6 6 6 £ Рис. 14.14. Интерфейс программы IbxlIpdSql 0682
Использование InterBase Express 683 Мониторинг функционирования InterBase Express Как и при использовании dbExpress, библиотека IBX позволяет вам осуществлять мониторинг соединений. Вы можете встроить в ваше приложение копию компо- нента IBSQLMonitor, который будет добавлять в журнал сообщения о выполняемых операциях. Кроме того, вы можете написать еще более универсальное приложение мони- торинга. Именно так я поступил в примере IbxMon. Я разместил на форме компо- нент, осуществляющий мониторинг, а также элемент управления Rich Edit, затем я написал для события OnSQL следующий обработчик: procedure TForml IBSQLMomtorlSQL(EventText String), begin if Assigned (RechEditl) then RichEditl Lines Add (TimeToStr (Now) + ' ' + EventText). end Проверка if Assigned может оказаться полезной в случае, если сообщение посту- пает в момент завершения работы. Данная проверка необходима, если вы добавля- ете код внутрь приложения, за работой которого наблюдаете. Чтобы получать сообщения от других приложений (или от текущего приложе- ния), вы должны настроить параметры мониторинга компонента IBDatabase. В примере IbxUpdSqL (о котором рассказывалось в предыдущем разделе «Построе- ние редактируемого запроса») я включил абсолютно все возможные режимы мониторинга: object IBDatabsael TIBDatabase TraceFlags = [tfQPrepare tfQExecute. tfQFetch, tfErrOr tfStmt. tfConnect tfTransact. tfBlob tfService tfMisc] IBX Monitor Statistics | Server РторегШ | the» | ^47 53 PM ' .....................“ [Application Ibxupdsql] IBDatabase! [Connect] Б 47 53 PM ]АррЬсаГюл Ibxupdsql] . IB Transaction! [Start transaction] Б 47 53 PM ^Application Ibxupdsql] IBDataSet! [Prepare] SELECT Employee EMP_NO Employee FIRST_NAME EmployeeLAST_NAME Department DEPARTMENT JobJOB_TITLE Employee SALARY Employee DEPT_NO Employee JOB_CODE Employee JOB_GRADE Employee JOB_COUNTRY FROM EMPLOYEE Employee INNER JOIN DEPARTMENT Department ON (Department DEPT NO Employee DEPT NO) INNER JOIN JOB Job ON (JobJOB_CODE «EmployeeJDB CODE) AND (JobJOB GRADE «EmployeeJOB GRADE) AND (Job JOB COUNTRY « Employee JOB COUNTRY) ORDER BY Department DEPARTMENT Plan PLAN SORT (JOIN (EMPLOYEE NATURALJOB INDEX (RDB$PRIMARY2) DEPARTMENT INDEX (RDB r $PRIMARY5))) Б 47 53 PM (Application Ibxupdsql) IBDataSet! [Prepare] delete horn EMPLOYEE EMP.NO - OLD_EMP_NO ₽ис. 14.15. Программа IbxMon, основанная на компоненте IBMonitor, отображает сведения о взаимодействии с InterBase 0683
684 Глава 14. Клиент-серверная архитектура с использованием dbExpress Если запустить оба примера одновременно, программа IbxMon отобразит на эк- ране журнал взаимодействия между программой IbxUpdSql и InterBase, как показа- но на рис. 14.15. Получение дополнительных системных данных Программа IbxMon позволяет вам не только наблюдать за ходом соединения с InterBase, но и узнавать значения некоторых конфигурационных параметров сер- вера — для этого используются дополнительные вкладки ее пользовательского интерфейса. В состав этой программы входит несколько административных ком- понентов IBX, отображающих статистику сервера, свойства сервера, а также всех подключенных пользователей. На рис. 14.16 демонстрируется отображение свойств сервера. f Mantaj StaiislKs HwwPmSeijUses { Server Properties 6 49 40 PM ' Databases 1 C \PROGRA~1\COMMON~1\BORLAN~1\DATA\EMPLOYEE GDB : Base Location C \ProgramFilesMnterBeseCofpMnterBese6/ Version V/l V6 01 6 Implementation InleiBase/xBBAVindows NT Service Version 2 I Рис. 14.16. Информация о сервере, отображаемая в приложении IbxMon Далее показан код, извлекающий информацию о пользователях: // получаем данные о пользователе IBSecuntyServicel Displayusers // отображаем имя каждого пользователя for i =0 to IBSecurityServicel UserInfoCount - 1 do with IBSecurityServicel Userlnfo[i] do RichEdit4 Lines Add (Format ('User Is Full Name Is Id £d’. [UsenName FinstName + ' + LastName Userid])) Задачи из реального мира До текущего момента мы рассматривали некоторые специальные методики, свя- занные с программированием InterBase, однако мы не затрагивали вопросов, свя- занных с разработкой реальных приложений. Мы не изучали проблем, которые 0684
Задачи из реального мира 685 возникают при практическом программировании InterBase. В следующих несколь- ких подразделах я подробнее рассмотрю несколько практических методик (поря- док рассмотрения выбран случайно). Мы вместе с Нандо Дессеной (Nando Dessena), который знает InterBase значи- тельно лучше, чем я, использовали все эти методики в семинаре, посвященном переходу от Paradox к InterBase. На семинаре мы рассматривали достаточно боль- шое и сложное приложение, в данной главе я сократил его всего лишь до несколь- ких таблиц, чтобы уложить рассмотрение в рамки данной главы. СОВЕТ-------------------------------------------------------------------- Здесь мы будем рассматривать базу данных под названием mastenng.gdb. Ее можно обнаружить в подкаталоге data пакета исходного кода для этой главы. Вы можете изучить структуру этой базы при помощи приложения InterBase Console, возможно, перед этим будет лучше скопировать базу данных в другой каталог жесткого диска, чтобы вы получили возможность свободно модифициро- вать эту БД. Генераторы и ID В главе 13 я уже отмечал, что я очень люблю активно использовать уникальные идентификаторы для обозначения записей в каждой из таблиц базы данных. ПРИМЕЧАНИЕ ----------------------------------------------------- Я предпочитаю использовать единую последовательность идентификаторов для всей системы. Это означает, что каждый идентификатор является уникальным не только в своей таблице, но и во всей базе данных. Такой подход называется Object ID (OID), о нем уже рассказывалось ранее. Благодаря использованию глобально-уникальных идентификаторов OID у вас появляется дополнительная сте- пень свободы, вы можете использовать объекты из разных таблиц как взаимозаменяемые. Недоста- ток состоит в том, что размер таких идентификаторов должен быть достаточно большим. 32 бит может оказаться недостаточно (в этом случае вы сможете хранить в базе данных не более 4 милли- ардов объектов). В InterBase 6 поддерживаются 64-битные генераторы. Каким образом осуществляется генерация уникальных значений, при условии, что в любой момент времени к базе данных могут быть подключены одновременно несколько клиентов? Если очередное значение последовательности идентифика- торов OID хранить в таблице, в нескольких параллельных транзакциях (иниции- рованных разными пользователями) будет доступно одно и то же значение. Если вы не применяете подход, основанный на таблицах, вы можете воспользоваться механизмом, не зависящим от базы данных. Например, можно использовать дос- таточно большие глобально-уникальные идентификаторы Windows GUID или ре- ализовать подход старших-младших (high-low) чисел (каждому клиенту в начале работы выделяется индивидуальное старшее число, к которому тот добавляет определяемое им самим младшее число; каждый клиент поддерживает свою соб- ственную последовательность младших чисел). Другой подход основан на использовании специального внутреннего механиз- ма базы данных. В разных SQL-серверах эти механизмы обозначаются разными терминами, однако суть одна и та же' чтобы получить глобально-уникальный иден- тификатор, клиент обращается за помощью к серверу. В InterBase этот механизм Называется генератором (generator). Последовательность уникальных идентифи- каторов формируется вне каких-либо транзакций, благодаря этому, даже если Несколько клиентов подключены к базе данных в одно и то же время, при необхо- 0685
686 Глава 14. Клиент-серверная архитектура с использованием dbExpress / димости любому из них будет выделен уникальный в рамках базы данных иденти- фикатор (надеюсь, вы помните, что InterBase требует создания транзакции даже для чтения данных). Я уже показывал, как можно создать генератор. Далее приводится код созда- ния генератора для моей демонстрационной базы данных, за которой следует определение представления (view), которое можно использовать для получения из БД нового значения: create generator gjnaster: 1 create view v_next_id ( next_id ) as select gen_id(gjnaster. 1) from rdbtdatabase В приложении RWBlocks я добавил в модуль данных компонент IBQuery (для со- здания генератора я могу обойтись без редактируемого набора данных), в составе которого присутствует следующий SQL-код: select nextjid from vjiextjd; Использовать подобный код удобнее, чем использовать SQL-выражение напря- мую, так как такой код проще писать и сопровождать даже в случае, если генера- тор изменяется (или вы переключаетесь на использование другого подхода). Бо- лее того, в этом же модуле данных я добавил функцию, которая возвращает новое значение генератора: function TdmMain.GetNewId: Integer: begin // возвращает следующее значение генератора QueryId.Open. try Result := QueryId.Fields[O].Aslnteger: finally Queryld.Close: end; end: Этот метод может быть вызван в обработчике события AfterInsert любого набо- ра данных, чтобы заполнить поле идентификатора в записи, добавляемой в БД: mydataset FieldsByName ('ID').Aslnteger •- data.GetNewId: Как я уже говорил, наборы данных IBX могут быть напрямую связаны с генера- тором, вследствие этого общая картина упрощается. Благодаря специальному ре- дактору свойств (диалоговое окно которого показано на рис. 14.17) соединение между полем базы данных и генератором выполняется тривиально. Обратите внимание на то, что оба этих подхода удобнее, чем подход, основан- ный на использовании серверного триггера, о котором рассказывалось ранее в даН" ной главе. В случае использования триггера приложение Delphi не знает, какой именно ID включен в состав записи, поэтому оно не может отобразить его в соста- ве DBGrid. Чтобы сделать это, необходимо вначале загрузить данные с сервера, а Д-пЯ этого необходимо направить серверу запрос на получение этих данных. Описанный в данном разделе подход основан иа коде, который работает на сто- роне клиента, значит, никаких проблем не возникает. Значение уникального иден- 0686
Задачи из реального мира 687 тифйкатора (то есть ключ) становится известно приложению Delphi еще до того, как происходит публикация записи в базе данных, и вы можете отобразить это значение в составе сетки. IBCIastReg Generator йаИ fiewstatfx |g1master " £*W|io & OnNew Refiord Г CtaPost С OnServet Рис. 14.17. Редактор свойства GeneratorField набора данных IBX Поиск текста вне зависимости от регистра символов Проблема поиска текста вне зависимости от регистра символов характерна не толь- ко для InterBase, но и для других SQL-серверов. Представьте, что вы имеете дело с таблицей, в которой содержится информация о различных компаниях. Представь- те, что вам требуется отобразить в сетке записи из этой таблицы. Отображение слишком большого количества записей в сетке — это плохая идея для клиент-сер- верной среды. Вместо этого вы можете предложить пользователю ввести несколь- ко первых символов имени компании, а далее отобразить в составе сетки лишь те записи, в которых имена начинаются с введенной пользователем последователь- ности символов. Поиск компаний по нескольким первым символам имени будет выполняться довольно часто, кроме того, он будет выполняться в отношении таблицы достаточ- но большого размера. Имейте также в виду, что если вы будете использовать опе- раторы starting with или Like, поиск будет чувствителен к регистру символов. Вот пример подобного SQL-выражения: select * from companies where name starting with 'win'-. Чтобы сделать поиск нечувствительным к регистру, вы можете использовать Функцию upper в обеих частях неравенства. В результате строка, введенная пользо- вателем, и строка, хранящаяся в базе, будут преобразованы к верхнему регистру. Однако подобная операция поиска будет выполняться достаточно медленно, так как в процессе поиска нельзя будет воспользоваться индексом. Можно было бы пря- мо в процессе создания новых записей осуществлять преобразование имен компа- ний к верхнему регистру, иначе говоря, хранить в соответствуюшем поле только заг- лавные символы, однако это не самая лучшая идея, так как если вы распечатываете содержимое таблицы, имена компаний, напечатанные большими буквами, выглядят Неестественно (такой метод печати часто использовался в старых базах данных). 0687
688__________ Глава 14. Клиент-серверная архитектура с использованием dbExpress / Существует другой трюк, который потребует дополнительного места на диске и в памяти в обмен на скорость: добавьте в таблицу дополнительное поле, в кото- ром будет храниться имя компании, преобразованное к верхнему регистру, вос- пользуйтесь триггером на стороне сервера, который будет генерировать значение этого поля и обновлять содержащиеся в нем данные. Для этого поля можно сгене- рировать и поддерживать индекс. Иными словами, в БД будут присутствовать два поля, содержащие имя компании: одно — для поиска, другое — для печати. Определение таблицы может выглядеть следующим образом: create domain d_uid as integer; create table companies ( id d_uid not null, name varchar(50). tax_code varchar(16). name_upper varchar(50). constraint compames_pk primary key (id) ): Операцию копирования имени компании, написанного заглавными буквами, в новое поле нельзя поручить коду, работающему на стороне клиента, так как в этом случае могут возникнуть проблемы, связанные с нарушением целостности данных. Для такой цели лучше всего создать триггер, работающий на стороне сервера, ко- торый автоматически обновлял бы содержимое поля каждый раз, когда имя ком- пании изменяется. Еще один триггер должен срабатывать в случае, когда в базу данных добавляется новая запись: create trigger compames_bi for companies active before insert position 0 as begin new name_upper = upper(new.name); end: create trigger compames_bu for companies active before update position 0 as begin if (new.name <> old name) then new.name_upper = upper(new.name); end: Наконец, после всего этого я добавляю в таблицу индекс при помощи специ- ального выражения DDL: create index i_companies_name_upper on compames(name_upper): Сформировав на стороне сервера подобную структуру, вы можете обеспечить выбор компаний по первым нескольким буквам. Для этого необходимо написать на Delphi следующий клиентский код: dm.DataCompa nies.Cl ose; dm DataCompanies SelectSQL Text 'select c id. c name. c.tax_code.' + ' from companies c ' + ' where name_upper starting with '” + Uppercase (edSearch.Text) + '' dm.DataCompanies.Open: 0688
Задачи из реального мира 689 СОВ! Вы можете воспользоваться заранее подготовленным запросом с параметром, благодаря чему код будет выполняться еще быстрее. Существует еще одна альтернатива: вы можете создать в БД дополнительное вычисляемое поле, однако в подобной ситуации вы не сможете создать индекс в отношении этого поля. В отсутствии индекса поиск будет осуществляться суще- ственно медленнее: name_upper varchar(50) computed by (upper(name)) Обработка информации об адресах и людях Возможно, вы обратили внимание на то, что таблица, описывающая компании, содержит очень мало полезных сведений. В ней нет информации об адресе, а так- же какой-либо контактной информации. Причина состоит в том, что я хотел бы обладать возможностью ассоциировать одну компанию с несколькими разными адресами (ведь компания может обладать несколькими разными офисами) и не- сколькими разными контактными лицами. Каждый адрес связан с той или иной компанией. Однако обратите внимание, что идентификатор адреса никак не связан с идентификатором компании (напри- мер, можно было бы нумеровать адреса, принадлежащие некоторой компании, по порядку начиная с единицы). Вместо этого для идентификации адресов использу- ются идентификаторы, уникальные в рамках БД. Благодаря этому я могу ссылать- ся на адреса из других мест БД, не указывая при этом идентификатор компании. Вот описание таблицы, в которой хранятся адреса компаний: create table locations ( id d_uid not null. id_company d_uid not null. address varchar(40), town varchar(30). zip varchar(lO). state varchar(4). phone varchar(15). fax varchar(15). constraint locations_pk primary key (id). constraint 1ocations_uc unique (id_company. id) alter table locations add constraint 1ocations_fk_compames foreign key (id_company) references companies (id) on update no action on delete no action: Последнее определение связывает поле id_company таблицы адресов (Locations) с полем ID таблицы компаний (companies). Еще одна таблица рассматриваемой базы данных содержит в себе имена сотруд- ников и относящуюся к этим сотрудникам контактную информацию. Каждый из с°трудников ассоциируется с тем или иным адресом. Если строго следовать пра- вилам нормализации, я должен был бы добавить в эту таблицу только ссылку на адрес, так как отсюда можно было бы легко узнать компанию, в которой работает этот человек (каждый адрес связан с компанией). Однако я добавил в таблицу people 0689
690 Глава 14. Клиент-серверная архитектура с использованием dbExpress не только ссылку на адрес, но и ссылку на имя компании. Благодаря этому упро- щается перемена местоположения сотрудника внутри компании, кроме того, по- вышается эффективность обработки запросов (мы избавляемся от дополнитель- ного шага сопоставления таблиц адресов и компаний). Таблица people обладает еще одним любопытным свойством: один из сотруд- ников компании может быть объявлен ключевым контактным лицом (key contact). Для этого в таблице присутствует булевское поле key_contact (оно реализовано в виде значения типа domain, так как InterBase не поддерживает булевских значе- ний в чистом виде), которое становится равным значению Т (то есть True — исти- на) в случае, если запись соответствует ключевому контактному лицу. Значение этого поля для всех остальных контактных лиц компании должно быть равно зна- чению F (то есть False — ложь). Чтобы обеспечить выполнение этого условия, мы добавляем в таблицу два триггера (один срабатывает при добавлении, а второй — при модификации записи). Определение таблицы выглядит следующим образом: create domain d_boolean as chard) default 'F' check (value in ('T', T’)) not null create table people ( id d_uid not null. id_company d_uid not null, id_location d_uid not null. name varchar(50) not null, phone varchar(15), fax varchar(15), email varchar(50). key_contact d_boolean. constraint people_pk primary key (id). constraint people_uc unique (id_company. name) ): alter table people add constraint peop1e_fk_compames foreign key (id_company) references companies (id) on update no action on delete cascade. alter table people add constraint people_fk_locations foreign key (id_company. id_location) references locations (id_company. id): create trigger people_ai for people active after insert position 0 as begin /* если сотрудник является ключевым контактным лицом, снимаем флаг со всех остальных записей (той же компании) */ if (new key_contact = 'Г') then update people set key_contact = T’ where id_company = new id_company and id <> new id: end: create trigger people_au for people 0690
Задачи из реального мира 691 \ active after update position О as begin /* если сотрудник является ключевым контактным лицом, снимаем флаг со всех остальных записей (той же компании) */ if (new.key_contact = "Г and old.key_contact = 'F") then update people set key_contact = 'F' where id_company = new.id_company and id <> new.id; end: Построение пользовательского интерфейса Три рассмотренные ранее таблицы состоят в отношении «основное/подробности» (master/detail). По этой причине в примере RWBlocks используется три компонента IBDataSet для доступа к данным, при этом одна основная таблица связана с двумя второстепенными. Код поддержки взаимосвязи «основное/подробности» стандар- тен, поэтому я не буду подробно рассматривать его здесь (предлагаю вам подроб- нее изучить соответствующий исходный код, прилагаемый к книге). Каждый из наборов данных обладает полным набором SQL-выражений, обес- печивающих редактирование данных. Когда вы добавляете новый элемент в таб- лицы подробностей, программа подключает их к записям основной таблицы. Вот код соответствующих методов: procedure TdmCompanies.DataLocationsAfterInsertIDataSet: TDataSet). begin // добавляем в запись таблицы подробностей // ссылку на главную запись DataLocationsID_COMPANY.Aslnteger .= DataCompamesID.Aslnteger: end. procedure TdmCompanies.DataPeopleAfterlnserttDataSet- TDataSet): begin // добавляем в запись таблицы подробностей // ссылку на главную запись DataPeopleID_COMPANY.Aslnteger .= DataCompamesID.Aslnteger. // в качестве адреса выбираем текущий адрес (если такой есть) if not DataLocations IsEmpty then DataPeopleID_LOCATION.Aslnteger .= DataLocationsID.Aslnteger; // первый добавленный сотрудник становится ключевым контактным лицом // (проверяем, является ли фильтрованный набор данных сотрудников пустым) DataPeopleKEY_CONTACT.AsBoolean := DataPeople.IsEmpty; end. Как видно из кода, компоненты наборов данных располагаются внутри модуля Данных. Каждой форме соответствует отдельный модуль данных (модуль подклю- чается к форме динамически, так как вы можете создать несколько экземпляров каждой формы). Каждому модулю данных соответствует индивидуальная тран- закция, поэтому разнообразные операции, выполняемые в отношении различных страниц, совершенно не зависят друг от друга. Однако подключение к базе данных обслуживается централизованно. Соответствующий компонент располагает- ся в главном модуле данных, па этот компонент ссылаются все остальные на- боры данных. Каждый из модулей данных создается динамически формой, 0691
692 Глава 14. Клиент-серверная архитектура с использованием dbExpress которая на него ссылается, соответствующее ему значение сохраняется в закры- том поле dm формы: procedure TformCompanies.FormCreate(Sender: TObject): begin dm .= TdmCompames.Create (Self); dsCompanies Dataset = dm.DataCompames: dsLocations.Dataset .= dm.DataLocations: dsPeople Dataset : = dm.DataPeople: end: Благодаря этому вы можете с легкостью создать несколько экземпляров фор- мы, к каждому из которой подключен экземпляр модуля данных. На форме, под- ключенной к модулю данных, размещается три элемента DBGrid, каждый из кото- рых связан с модулем данных и одним из соответствующих наборов данных. На рис. 14.18 показана эта форма на этапе исполнения. Форма располагается на основной форме, которая основана на элементе управ- ления с переключающимися вкладками. На каждой вкладке размещается отдель- ная форма. Когда программа начинает работу, происходит создание только той формы, которая связана с самой первой вкладкой. Представленный далее метод ShowForm убирает обрамление формы, а затем настраивает родителя формы: procedure TformMain FormCreate(Sender- TObject): begin ShortDateFormat := 'dd/nm/yyyy'; ShowForm (TformCompanies.Create (Self). TabCompames): end: procedure TformMain.ShowFOrm (Form: TForm: Tab. TtabSheet): begin Form.BorderDtyle := bsNone; Form.Align alClient: Form Parent := Tab. Form.Show: end. Две другие вкладки заполняются на этапе исполнения: procedure TformMain.PageControlIChangetSender TObject): begin if PageControl1 ActivePage Control Count = 0 then if PageControll.ActivePage = TabFreeQ then ShowForm (TformFreeQuery.Create (self). TabFreeQ) else if PageControll.ActivePage = TabClasses then ShowForm (TformClasses Create (self). TabClasses). end. Форма компаний (Companies) обеспечивает поиск компаний по первым несколь- ким буквам имени (об этом мы уже говорили ранее), а также поиск по месту распо- ложения офиса. Вы вводите имя города, а затем возвращаетесь к списку компании, которые обладают офисами, размещенными в этом городе: procedure TformCompanies btnTownClick(Sender. TOBject): begin with dm DataCompames do begin Close. Sei ectSQL.Text .= 0692
Задачи из реального мира 693 'select с.id. с.пате, с tax_code' + ' from companies с ' + ' where exists (select loc.id from locations loc ' + ' where loc id_company = c.id and upper(loc.town) - ' " + UpperCase(edTown.Text) + )•_ Open; dm.DataLocations.Open; dm DataPeople Open; end. end. Рис. 14.18. Форма, являющаяся частью примера RWBIocks, показывает имя компании, расположение офисов и перечень контактных лиц Форма включает в себя много другого исходного кода. В частности, код, запре- щающий пользователю закрывать форму в случае, если некоторые из сделанных им модификаций не опубликованы в базе данных. Кроме того, присутствует код, обеспечивающий формирование диалогового окна с сопоставлением таблиц. Об этом будет рассказано далее. Оплата учебных курсов Часть программы и базы данных имеет отношение к оплате учебных курсов (на самом деле рассматриваемый пример является упрощенной версией реальной про- граммы, которую я использую в своем собственном бизнесе). В состав базы дан- ных входит таблица курсов, в которой перечисляются учебные курсы. Для каждого кУрса указывается заголовок и дата начала. Еще одна таблица служит для хране- ния сведений о регистрации. В этой таблице перечисляются компании, изъявив- шие желание послать своих сотрудников на обучение по тому или иному курсу. В каждой строке таблицы содержится идентификатор курса, идентификатор ком- 0693
694 Глава 14. Клиент-серверная архитектура с использованием dbExpress пании, а также некоторые заметки. Наконец, в третьей таблице перечисляются люди, которые собираются прийти на курс. Каждый человек связан с регистраци- онной записью компании, к которой он принадлежит, кроме того, для него указы- вается оплаченная сумма. Обратите внимание, что регистрационная запись связывает запись о курсе с за- писью о компании. Запись о сотруднике связывается с регистрационной записью. В этом случае база данных становится более нормализованной, так как сотрудник напрямую связывается не с учебным курсом, а с записью о регистрации компании для этого курса. Вот определения таблиц (для краткости некоторые из элементов опущены): create table classes ( id d_uid not null. description varchar(50). starts_on timestamp not null. constraint classespk primary key (id) ); create table classes_reg ( id d_uid not null, id_company d_uid not null. id_class d_uid not null. notes varchar(255). constraint classes_reg_pk primary key (id). constraint classes_reg_uc unique (id_company. id_class) ); create domain d_amount as numeric(15. 2); create table people_reg ( id d_uid not null, id_classes_reg d_uid not null. id_person duld not null. amount d_amount. constraint people_reg_pk primary key (id) ): В модуле данных для этой группы таблиц используются отношения типа «ос- новное/подробности/подробности». Модуль содержит в себе код, который настра- ивает соединение с активной записью в основной таблице, когда создается новая запись в таблице подробностей. Каждый из наборов данных обладает генерируе- мым полем (generator field) для ID, которому соответствуют подходящие SQL- выражения update и insert. Эти выражения генерируются соответствующим редак- тором компонента. Каждый из двух вторичных наборов данных извлекает данные из вспомогательной таблицы (либо из списка компаний, либо из списка сотрудни- ков). Мне пришлось вручную отредактировать выражения RefreshSQL для того, что- бы обеспечить подходящее внутреннее соединение таблиц. Вот пример: object IBClassReg: TIBDataSet Database = DmMain.IBDatabasel Transaction = IBTransactionl AfterInsert = IBClassRegAfterlnsert DeleteSQL.Strings’ ( 'delete from classes_reg' 'where id - :old_1d") 0694
Задачи из реального мира 695 InsertSQL.Strings - ( 'insert into classes_reg (id. id_class. idjcompany. notes)' 'values (:id. :id_class. : idjcompany. motes)') RefreshSQL.Strings - ( 'select reg. id. reg.id_class. reg. idjcompany. reg.notes, c.name ' 'from classes_reg reg’ 'Join companies c on reg.id_company = c.id' 'where id - :id') Sei ectSQL.Strings = ( 'select reg.id. reg.idjclass. reg. idjcompany. reg.notes, c.name ' 'from classes_reg reg' 'join companies c on reg.idjcompany = c.id' 'where id_class » :id') ModifySQL.Strings - ( 'update classes_reg' 'set' ' id = ;id. ' ' idjclass - :idjclass. ' ' idjcompany = :idjcompany. ' ’ notes = motes' 'where id - :old_id‘) GeneratorField.Field = 'id' GeneratorField.Generator = 'gjnaster' DataSource = dsClasses end Чтобы завершить обсуждение IBClassReg, приведу исходный код единственного принадлежащего ему обработчика события: procedure TdmClasses.IBC1assRegAfterInsert(DataSet: TDataSet); begin IBClassReg.FieldByName ('idjclass').AsString := IBC1asses.FieldByName ('id').AsString; end; Набор данных IBPeopleReg обладает сходной конфигурацией, однако набор дан- ных IBClasses на этапе проектирования выглядит проще. Во время исполнения SQL- код этого набора данных динамически меняется, отображая курсы, которые еще не начались, курсы, которые проходят в настоящий момент, курсы, которые уже за- вершены, а также курсы прошлых лет. Для этого используются три вкладки (Open, Closed, Past), на которых отображаются соответствующие группы записей. Интер- фейс показан на рис. 14.19. Три альтернативных SQL-выражения создаются в момент запуска программы или тогда, когда создается и отображается форма регистрации классов. Програм- ма сохраняет финальную часть альтернативных инструкций (после ключевого сло- ва where) в списке строк и выбирает одну из этих строк, когда пользователь пере- ходит от одной вкладки к другой: Procedure TformClasses.FormCreate(Sender: TObject): begin dm := TdmClasses.Create (Self): II соединяем наборы данных с источниками данных dsClasses.Dataset := dm.IBClasses; dsClassReg.DataSet :« dm.IBClassReg; dsPeopleReg.DataSet := dm.IBPeopleReg; II открываем наборы данных 0695
696 Глава 14. Клиент-серверная архитектура с использованием dbExpress dm IBClasses Active = True. dm IBClassReg Active = True. dm IBPeopleReg Active « True // подготавливаем SQL-код для трех вкладок SqlCommands = TstringList Create SqlCommands Add ( where Starts_On > ' 'now'’’) SqlCommands Add (' where Starts_On <- ''now'' and ' + ' extract (year from Starts_On ) >= extract(year from current_timestamp)’). SqlCommands Add (' where extract (year from Starts_0n) < ' + ' extract (year from current_timestamp)'} end procedure TformClasses TabChange(Sender TObject). begin dm IBClasses Active = False dm IBClasses SelectSQL [1] = SqlCommands [Tab Tablndex] dm IBClasses Active = True. end ^•RWilKh w * I » I H I + | - j * I | I j u. I ,*?J. L I ££.. I Г j Сосрремм | FreeCwy j Рис. 14.19. Форма программы RWBIocks, связанная с регистрацией учебных курсов Построение диалогового окна сопоставления значений В рассматриваемом примере выполняется сопоставление значений, извлекаемых из разных таблиц. Например, при выборе очередного курса из таблицы учебных курсов вместо того, чтобы отображать идентификатор компании, форма отобра- жает имя этой компании, извлеченное из таблицы companies Эта функциональ- ность реализована при помощи соединения таблиц внутри SQL-выражения, кро- ме того, сетка DBGrid настроена таким образом, чтобы не отображать ID компании- В ситуации, когда используется локальная база данных, или в клиент-сервернои 0696
Задачи из реального мира 697 среде с относительно небольшим количеством записей можно воспользоваться сопоставляемым полем (lookup field). Однако этот подход требует копирования с сервера на сторону клиента всего набора данных, в отношении которого осуще- ствляется сопоставление. Значит, использовать подобный подход можно только в случае, если в таблице присутствует не более 100 записей. Если вы имеете дело с достаточно большой таблицей, такой как таблица компа- ний, вы можете создать дополнительное диалоговое окно, которое позволит вам выбрать интересующие вас компании. Например, вы можете выбрать компанию, используя форму, которую вы уже построили, и реализованные в этой форме воз- можности поиска. Чтобы отобразить эту форму в виде диалогового окна, програм- ма создает новый экземпляр этой формы, отображает некоторые скрытые кнопки, которые размещены на форме на этапе проектирования, а затем позволяет пользо- вателю выбрать компанию, чтобы сослаться на нее из другой таблицы. Чтобы упростить подобное сопоставление, которое в крупной программе мо- жет использоваться не один, а несколько раз, я добавил на форму компаний класс- функцию, которая возвращает в двух параметрах имя и ID выбранной компании В эту функцию можно передать изначальный ID, чтобы определить изначальный выбор. Далее приводится полный код этой функции-класса. Код создает объект этого класса, если это необходимо, выбирает начальную запись, отображает диа- логовое окно и, наконец, извлекает возвращаемые значения: class function TformCompanies SelectedCompany ( var CompanyName string var Companyld Integer) Boolean. var FormComp TFormCompames. begin Result = False FormComp = TformCompanies Create (Application), FormComp Caption = ‘Select Company’ try // активируем кнопки диалогового окна FormComp btnCancel Visible = True. FormComp btnOK Visible = True // выбираем компанию if Companyld > 0 then FormComp dm DataCompames Sei ectSQL text = 'select c id c name c tax_code + ' from companies c ' + ' where c id’ ' + IntToStr (Companyld) el se FormComp dm DataCompames SelectSQL Text = 'select c id c name c tax_code' + form companies c' + where name_upper starting with 'a'". FormComp dm DataCompames Open FormComp dm DataLocations Open FormComp dm DataPeople Open if FormComp ShowModal = mrOK then begin Result = True Companyld = FormComp dm DataCompames FieldByName ('id ) Aslnteger CompanyName = FormComp dm DataCompames FieldByName (.'name') AsString 0697
698 Глава 14. Клиент-серверная архитектура с использованием dbExpress end finally FormComp Free end. end Еще одна несколько более сложная функция-класс (я не привожу здесь ее код — его можно найти в пакете исходного кода, прилагаемого к данной книге) позволя- ет вам выбрать сотрудника заданной компании, чтобы зарегистрировать его для прохождения учебного курса. В обоих случаях сопоставление инициируется при помощи кнопки с многото- чием, которая появляется в поле сетки, например в поле, в котором указываются имена компаний, зарегистрировавшихся для прохождения учебных курсов. Когда пользователь щелкает на этой кнопке, программа обращается к функции-классу, которая отображает на экране диалоговое окно. Результат, выбранный пользова- телем, используется для обновления скрытого поля, в котором содержится ID ком- пании, а также видимого поля с именем компании. procedure TFormClasses DBGridClassRegEditButtonClick(sender TObject) var CompanyName string Companyld Integer begin Companyld = dm IBClassReg FieldByName ('id_Company ) Aslnteger if TformCompanies SelectCompany (CompanyName Companyld) then begin dm IBClassReg Edit dm IBClassReg FieldByName ('Name") AsString - CompanyName dm IBClassReg FieldByName ('id_Company ) Aslnteger = Companyld end end Форма с редактируемым SQL-запросом Третья вкладка рассматриваемой программы позволяет пользователю напрямую ввести с клавиатуры SQL-запрос и выполнить его в отношении базы данных. Для упрощения этой процедуры на форме присутствует раскрывающийся список, в котором перечисляются все существующие в базе данных таблицы. Перечень таб- лиц в БД формируется в процессе создания формы. Для этого программа обраща- ется к специальной функции: DmMain IBDatabasel GetTableNames (ComboTables Items) При выборе одного из пунктов ниспадающего списка автоматически генериру- ется SQL-запрос: MemoSql Lines Text = 'select * from + ComboTables Text Пользователь (если он обладает необходимыми навыками) может отредакти- ровать SQL-запрос, добавить в него дополнительные условия и выполнить его в от- ношении базы данных, procedure TformFreeQuery ButtonRunClick(Sender TObject) begin QueryFree Close QueryFree SQL = MemoSql Lines QueryFree Open end 0698
Что далее? 699 Третья форма программы RWBlocks показана на рис. 14.20. Конечно же, я не пред- полагаю, что подобную форму со свободно редактируемым запросом SQL следует добавлять в каждую клиентскую программу. Подобная возможность предназначе- на только для опытных пользователей и программистов. Фактически, я добавил ее в свою программу для себя самого. jy IS LeammsXML 10/10/2002 Рис. 14.20. Форма программы RWBlocks, позволяющая самостоятельно вводить и редактировать SQL-запросы Что далее? В данной главе мы подробно рассмотрели создание клиент-серверных приложе- ний в среде Delphi. Мы обсудили основные связанные с этим вопросы и некото- рые интересные приемы работы. После общего введения я рассмотрел исполь- зование библиотеки dbExpress, Я также кратко рассмотрел сервер InterBase и компоненты InterBase Express (IBX). В конце главы я представил читателям при- мер программы из реального мира. В главе 15 мы сконцентрируемся на изучении технологии ADO, которая была Разработана компанией Microsoft. После этого, в главе 16, будут рассмотрены ме- ханизмы поддержки многозвенных приложений под общим названием DataSnap. В этой главе также рассматривается использование библиотеки dbExpress и ком- понента ClientDataSet, однако в несколько ином контексте. 0699
15 Технология ADO С середины 1980-х годов программисты RDBMS пытаются найти «волшебный ключик» от двери, которая ведет в страну независимости от конкретной базы дан- ных. Проблема состоит в том, что данные могут поступать из самых разных источ- ников, каждый из которых обладает своей спецификой. Однако разработка прило- жений существенно упростилась бы, если бы удалось создать унифицированный механизм взаимодействия с самыми разными источниками данных. Это мог бы быть универсальный программный интерфейс API, который позволил бы програм- мистам разрабатывать приложения, одинаковым образом взаимодействующие с различными источниками данных. Такие приложения можно было бы использо- вать для взаимодействия с самими разными системами RDBMS, а также с други- ми источниками данных. За истекшее время различными компаниями было пред- ложено множество решений в этой области. Наиболее значительными являются Microsoft ODBC (Open Database Connectivity) и Borland IDAPI (Integrated Data- base Application Programming Interface). Технология Borland IDAPI больше извест- на под именем BDE (Borland Database Engine). В середине 1990-х годов, с развитием и распространением технологии СОМ (Component Object Model), компания Microsoft объявила о постепенном переходе от ODBC к использованию новой технологии OLE DB. Однако OLE DB, по мне- нию самой компании Microsoft, является интерфейсом системного уровня, этот интерфейс должен использоваться системными программистами. Технология OLE DB является тяжеловесной, сложной и очень чувствительной к ошибкам. Она тре- бует от программиста слишком многого. Работать с OLE DB слишком сложно. Чтобы облегчить работу с OLE DB, был создан дополнительный прикладной уро- вень, который получил название ADO (ActiveX Data Objects). Работать c ADO существенно проще, чем с OLE DB. Технология ADO предназначена для приклад- ных программистов. В главе 14 уже говорилось о том, что компания Borland также решила заменить технологию BDE новой технологией под названием dbExpress. Следует отметить, что ADO по своим возможностям и идеологии в большей степени напоминает BDE. Как BDE, так и ADO поддерживают навигацию, манипулирование наборами дан- ных, обработку транзакций, кэшируемые обновления (в ADO они называются batch updates (пакетные обновления)). Иными словами, концептуально и идеологиче- ски ADO и BDE являются похожими технологиями. 0700
MDAC (Microsoft Data Access Components) 701 ПРИМЕЧАНИЕ------------------------------------------------------------- Я хотел бы поблагодарить Гая Смита Ферриера (Guy Smith Ferrier) за то, что он написал данную главу для книги Mastering Delphi б1. Гай — программист, автор книг и статей, кроме того, он высту- пает на конференциях. Он является автором нескольких коммерческих программных продуктов и многочисленных внутренних систем как для небольших, так и для крупных компаний. Он написал множество статей для журнала The Delphi Magazine, а также для других изданий. Кроме того, он неоднократно выступал на различных конференциях в Северной Америке и в Европе. Гай живет в Англии вместе с женой, сыном и кошкой. В данной главе мы рассмотрим работу с ADO. Мы также рассмотрим dbGo — набор компонентов Delphi, который изначально назывался ADO Express, однако в Delphi 6 был переименован, так как компания Microsoft противится использова- нию обозначения ADO в продуктах, разработанных сторонними производителя- ми. В среде Delphi вы можете работать с ADO без помощи dbGo. Вы можете им- портировать библиотеку типов ADO и получить прямой доступ к интерфейсам ADO. Именно так приходилось работать с ADO в Delphi до появления версии Delphi 5. Однако такой подход не позволяет вам воспользоваться преимущества- ми встроенной в Delphi инфраструктуры взаимодействия с базами данных. В час- тности, вы не сможете воспользоваться элементами управления, специально пред- назначенными для работы с данными, кроме того, для вас будет недоступной технология DataSnap. Во всех примерах данной главы для взаимодействия с ADO используется dbGo. Во-первых, dbGo входит в стандартный комплект поставки Delphi, во-вторых, dbGo является очень удобной технологией. Вне зависимости от того, будете ли вы использовать dbGo или откажетесь от использования этой тех- нологии, материал данной главы будет для вас полезным. ПРИМЕЧАНИЕ---------------------------------------------—------------ Помимо dbGo вы можете использовать для взаимодействия с ADO множество других продуктов, разработанных сторонними производителями, например Adonis, AdoSlutio, Diamond ADO и Kamiak. В данной главе рассматриваются следующие вопросы: О Microsoft Data Access Components (MDAC); О Delphi dbGo; О файлы связи с данными (Data link files); О получение информации о схеме; О использование механизма Jet; О обработка транзакций; О отключенные и хранимые на диске наборы записей; О модель портфеля и установка MDAC. MDAC (Microsoft Data Access Components) На самом деле ADO является частью более крупномасштабной технологии под ; названием Microsoft Data Access Components (MDAC). Термин MDAC является 1 Русское издание: Delphi 6. Для профессионалов. — СПб.: Питер, 2002. — Примеч. перев. 0701
702 Глава 15. Технология АРр общим обозначением для всех разработанных компанией Microsoft техноло- гий, связанных с БД. К этому набору относятся ADO, OLE DB, ODBC и RDS (Remote Data Services). Часто приходится слышать, что люди используют тер- мины MDAC и ADO как синонимы, однако это неправильно. На самом деле ADO является лишь одной из частей MDAC. Когда мы говорим о версиях ADO, мы имеем в виду версии MDAC. К основным версиям MDAC относятся вер- сии 1.5, 2.0, 2.1, 2.5 и 2.6. Компания Microsoft распространяет MDAC в виде отдельного продукта. Этот продукт может быть загружен с веб-узла Microsoft бесплатно. Мало того, фактически его можно бесплатно включать в состав ва- ших собственных продуктов (существуют определенные ограничения, однако большинство разработчиков Delphi без каких-либо проблем удовлетворяют всем этим требованиям). Кроме того, MDAC входит в комплект поставки боль- шинства продуктов Microsoft, имеющих отношение к базам данных. В состав Delphi 7 входит версия MDAC 2.6. Необходимо принять во внимание два важных обстоятельства. Во-первых, с большой долей уверенности можно сказать, что технология MDAC уже установ- лена на клиентских компьютерах ваших пользователей. Во-вторых, вне зависимо- сти от версии MDAC, которая была установлена на клиентских компьютерах ва- ших пользователей, можно с уверенностью сказать, что эта версия рано или поздно будет обновлена до самой свежен (текущей) версии MDAC. Обновление может быть выполнено вами, вашими пользователями или одним из устанавливаемых в системе приложений Microsoft. Подобное обновление фактически невозможно предотвратить, так как MDAC устанавливается в составе такого широко распрос- траненного приложения, как Internet Explorer. К этому следует добавить, что ком- пания Microsoft поддерживает лишь самую последнюю версию MDAC, а также версию, предшествующую самой последней. Исходя из всего этого, можно прийти к выводу: ваше приложение должно работать с самым свежим выпуском MDAC или с предшествующей ему версией. Как разработчик ADO, вы должны регулярно просматривать страницы веб-узла Microsoft, посвященные MDAC. Для этого следует обратиться по адресу www. microsoft.com/data. Здесь вы сможете бесплатно загрузить самую свежую версию MDAC. Также рекомендуется загрузить MDAC SDK (13 Мбайт), если у вас еще нет этого пакета. На самом деле MDAC SDK входит в состав Platform SDK, так что, если у вас есть Platform SDK, значит, вы уже обладаете MDAC SDK. Пусть пакет MDAC SDK станет вашей библией. Вы должны загрузить его и регулярно обращаться к нему для получения необходимых сведений и ответов на любые воп- росы, связанные с ADO. Если вы нуждаетесь в информации, связанной с MDAC, прежде всего вы должны обратиться к MDAC SDK. Провайдеры OLE DB Провайдеры OLE D В обеспечивают доступ к источникам данных. В dbExpress для этой цели используются драйверы, а в BDE — связи SQL Links. В процессе уста- новки MDAC в системе автоматически устанавливаются провайдеры OLE DB, перечисленные в табл. 15.1. 0702
MDAC (Microsoft Data Access Components) 703 Таблица 15.1. Провайдеры OLE DB, входящие в состав MDAC драйвер Провайдер Описание MSDASQL ODBC Drivers Драйверы ODBC (по умолчанию) Microsoft.Jet.OLEDB.3.5 Jet 3.5 Только базы данных MS Access 97 Microsoft.Jet.OLEDB.4.0 Jet 4.0 Базы данных MS Access и другие БД SQLOLEDB SQL Server Базы данных MS SQL Server MSDAORA Oracle Базы данных Oracle MSOLAP OLAP Services Online Analytical Processing SampProv Sample provider Пример провайдера OLE DB для файлов CSV MSDAOSP Simple provider Для создания ваших собственных провайдеров для простых текстовых данных Вот перечень этих провайдеров. О ODBC OLE DB используется для обратной совместимости с ODBC. Подроб- нее ознакомившись с работой ADO, вы узнаете об ограничениях, присущих это- му провайдеру. О Jet OLE DB — поддержка MS Access и других локальных баз данных. Мы вер- немся к рассмотрению этих провайдеров далее. О SQL Server обеспечивает взаимодействие с SQL Server 7, SQL Server 2000 и Microsoft Database Engine (MSDE). MSDE — это упрощенная версия SQL Server, в которой отсутствует большинство инструментов, а кроме того, добав- лен специальный код, который намеренно снижает производительность в слу- чае, если к базе данных одновременно подключаются более пяти пользователей. К преимуществам MSDE следует отнести то, что этот механизм распространя- ется бесплатно и полностью совместим с SQL Server. О OLE DB для OLAP может использоваться напрямую, однако чаще обращение к нему осуществляется через ADO Multi-Dimentional (ADOMD). ADOMD — это дополнительная технология ADO, специально разработанная для Online Analytical Processing (OLAP). Если ранее вы работали с Delphi Decision Cube, Excel Pivot Tables или Access Cross Tabs, значит, вы работали с одной из форм OLAP. Помимо уже перечисленных здесь провайдеров, компания Microsoft осуществ- ляет поддержку некоторых других провайдеров OLE DB, которые входят в состав Других продуктов или в состав SDK. О Active Directory Services OLE DB входит в состав ADSI SDK; AS/400 OLE DB и VSAM OLE DB входят в состав SNA Server; Exchange OLE DB входит в сос- тав Microsoft Exchange 2000. О Indexing Service OLE DB входит в состав Microsoft Indexing Service — внутрен- ний механизм Windows, ускоряющий поиск информации в файлах при помо- щи построения каталога с файловой информацией. Служба индексирования Indexing Service интегрирована в IIS и часто используется для индексирования веб-узлов. 0703
704 Глава 15. Технология АРо о Internet Publishing OLE DB позволяет разработчикам манипулировать катало- гами и файлами с использованием HTTP. о Существует также категория провайдеров OLE DB, которые называются про- вайдерами обслуживания (service providers). Как следует из имени, эти провай- деры обеспечивают обслуживание других провайдеров OLE DB и зачастую ак- тивизируются автоматически без участия программиста. Например, служба Cursor Service активизируется в случае, если вы создаете курсор на стороне клиента, а провайдер Persistent Recordset активизируется в случае, если вы со- бираетесь сохранить данные на локальном диске. Помимо перечисленных, существует также огромное количество других про- вайдеров OLE DB для MDAC. Провайдеры OLE DB можно получить как от Micro- soft, так и от независимых производителей. Список провайдеров OLE DB очень большой и постоянно меняется, поэтому его невозможно воспроизвести в данной книге. Кроме независимых производителей поставку и поддержку провайдеров OLE DB осуществляют многие производители систем RDBMS. Например, компания Oracle поддерживает собственный провайдер OLE DB под названием ORAOLEDB. СОВЕТ----------------------------------------------------------------------------_ Вы уже, наверное, обратили внимание на то, что в списке отсутствует провайдер OLE DB для InterBase. Во-первых, вы можете воспользоваться драйвером ODBC, во-вторых, вы можете использовать про- вайдер IBProvider, разработанный Дмитрием Коваленко (www.lipetsk.ru/prog/eng/index.html). Нако- нец, вы можете попробовать разработать провайдер самостоятельно. Для этого удобно использовать комплект OLE DB Provider Development Toolkit, разработанный Бинхом Ли (Binh Ly) и доступный по адресу www.techvanguards.com/products/optk/install.htm. Использование компонентов dbGo Программисты, уже знакомые с BDE, dbExpess или IBExpress, без труда узнают компоненты, входящие в состав dbGo (табл. 15.2). Таблица 15.2. Компоненты dbGo Компонент dbGo ADOConnection ADOCommand ADODataSet ADOTable ADOQuery ADOStoredProc RDSConnection Эквивалент из комплекта BDE Описание Подключение к базе данных База данных Исполняет команду SQL Нет эквивалента Многоцелевой наследник TDataSet Нет эквивалента Инкапсулирует таблицу Table Инкапсулирует SQL SELECT Query Инкапсулирует сохраненную процедуру (stored procedure) Stored Proc Подключение Remote Data Services Нет эквивалента Четыре компонента наборов данных (ADODataSet, ADOTable, ADOQuery и ADOSto- redProc) фактически полностью реализованы общим для них базовым классом TCustomADODataSet. Этот компонент несет ответственность за выполнение болЫПИН' 0704
Использование компонентов dbGo 705 ства функций, присущих набору данных. Производные компоненты являются тон- кими оболочками, которые делают доступными для внешнего мира те или иные возможности базового компонента. Таким образом, компоненты обладают множе- ством общих черт. Компоненты ADOTable, ADOQuery и ADOStoredProc предназначены для упрощения адаптации кода, ориентированного на BDE. Однако следует иметь в виду, что эти компоненты нельзя считать полностью идентичными эквивалента- ми аналогичных компонентов BDE. Различия обязательно проявят себя при раз- работке фактически любого приложения за исключением, может быть, самых три- виальных. В качестве основного компонента при разработке новых программ следует считать компонент ADO DataSet, так как, во-первых, этот компонент являет- ся достаточно удобным, а во-вторых, его интерфейс сходен с интерфейсом ADO Recordset. В данной главе я продемонстрирую использование каждого из упомя- нутых компонентов. Практический пример Хватит теории, давайте перейдем к делу. Разместим на форме компонент ADOTable. Для индикации базы данных, к которой следует подключиться, в рамках ADO ис- пользуются строки подключения (connection strings). Если вы знаете, что делаете, вы можете набрать строку подключения вручную. Однако в общем случае для со- здания строки подключения рекомендуется использовать специальный редактор (редактор свойства Connectionstring), рабочее окно которого показано на рис. 15.1. Formi.MMHablel ~ Souee of Comedian *“ ——- — , I ; & UsefiowdlonSUmg j. ..- “ - — j j _______J _ I Рис. 15.1. Редактор строки подключения Щелкните на Build (Сформировать), чтобы запустить разработанный компани- ей Microsoft редактор строк подключения. Его рабочее окно показано на рис. 15.2. Давайте рассмотрим этот инструмент подробнее, так как он является важным средством при работе с ADO. На первой вкладке показаны провайдеры OLE DB и провайдеры обслуживания, установленные на вашем компьютере. Перечень про- вайдеров может быть разным для разных версий MDAC, кроме того, новые про- вайдеры могут появиться в списке в результате установки на компьютере новых прикладных программ. Вернемся к нашему примеру. Выберите провайдер Jet 4.0 OLE DB — для этого сделайте двойной щелчок на надписи Jet 4.0 OLE DB Provider, На экране появится вкладка Connection (Подключение). Внешний вид этой страни- ЦЬ1 для разных провайдеров может быть разным. Для провайдера Jet редактор пред- ложит вам ввести имя базы данных и аутентификационные данные. Вы можете выбрать MDB-файл базы данных Access, входящий в комплект поставки Delphi (например, C:\Program Files\Common Files\Borland Shared\Data\dbdemos.mdb). Щелк- 0705
706 Глава 15. Технология Арр ните на кнопке Test Connection (Протестировать соединение) для того, чтобы убе- диться в правильности вашего выбора. На вкладке Advanced (Дополнительно) вы можете контролировать режим дос- тупа к базе данных. Здесь вы можете настроить эксклюзивный доступ или доступ только для чтения. На вкладке АН (Все) перечисляются все параметры строки под- ключения. Этот список может быть разным для разных провайдеров OLE DB. Хорошо запомните эту страницу, так как с ее помощью можно решить множество разнообразных проблем. Закрыв редактор Microsoft, вы вернетесь к редуктору строк подключения Borland. В рабочем окне этого редактора будет показана строка, ко- торая будет присвоена Connectionstring (здесь я разделил ее на несколько строчек, чтобы удобнее было читать): Provider=Microsoft.Jet.OLEDB.4.0; Data Source=C:\Program FilesXCommon Files\Borland Shared\Data\dbdemos.mdb: Persist Security Info=Fa1se Data Unk Properties Г: Microsoft Jet 4 0 OLE DB Provider Microsoft OLE DB Provider for Indexing Service Microsoft OLE DB Provider .for Internet Publishing Microsoft OLE DE Provider tor ODBC Drivers Microsoft OLE DB Provider for Oracle Microsoft OLE DB Provider for SQL Server Microsoft OLE DB Simple Provider MSOataShape OLE DB Provider for Microsoft Directory Services Рис. 15.2. Первая страница редактора строки подключения Microsoft Строка подключения — это обычная строка символов, в которой через точку с запятой перечисляются параметры и их значения. Такую строку можно редакти- ровать вручную. Параметры и их значения можно перенастраивать в процессе вЫ полнения программы, для этого вы должны написать собственный набор подпрог рамм для выполнения поиска параметра в списке и внесения изменения в его значение. Существует также более простой способ: вы можете скопировать строку в список строк Delphi и воспользоваться механизмом обработки пар «имя—значе ние». Этот прием будет продемонстрирован в примере JetText, о котором буДеТ рассказано далее в разделе «Доступ к текстовым файлам черезJet». 0706
Использование компонентов dbGo 707 После того как вы сформировали строку подключения, вы можете выбрать таблицу. Раскройте список таблиц при помощи свойства TableName в окне Object Inspector. Выберите таблицу Customer. Добавьте компонент DataSource и элемент управления DBGrid, а затем соедините их вместе. В результате получилась реаль- ная, хотя и примитивная программа, использующая ADO (полный исходный код оформлен в виде примера FirstAdoExample). Чтобы увидеть данные, занесите в свой- ство Active набора данных значение True или откройте набор данных внутри обра- ботчика события FormCreate (как это сделано в примере). Второй способ позволяет избежать проблем, если на этапе проектирования база данных недоступна. СОВЕТ--------------------------------------------------------------------------— Если вы планируете использовать dbGo в качестве основной технологии доступа к БД, вам наверня- ка захочется переместить компонент DataSource на страницу ADO палитры компонентов, чтобы не перескакивать постоянно со страницы на страницу. Если вы используете ADO в комбинации с дру- гой технологией, вы можете имитировать установку DataSource на нескольких страницах. Для этого необходимо создать шаблон (Component Template) компонента DataSource и поместить его на стра- ницу ADO. Компонент ADOConnection Когда вы используете компонент ADOTable, он создает свой собственный компо- нент соединения с БД у вас за спиной. Однако вы вовсе не обязаны использовать именно это соединение. В общем случае вы должны создать свое собственное со- единение при помощи компонента ADOConnection, который по сути является экви- валентом компонента SQLConnection из библиотеки dbExpress и компонента Database из библиотеки BDE. Компонент ADOConnection позволяет вам должным образом настроить процедуру аутентификации, контролировать транзакции, напрямую выполнять команды, адресованные БД, кроме того, он позволяет сократить коли- чество подключений, существующих в рамках приложения. Использовать ADOConnection достаточно просто. Разместите этот компонент на форме и настройте его свойство Connectionstring таким же образом, как вы делали это для компонента ADOTable. Кроме того, вы можете сделать двойной щелчок на компоненте ADOConnection (или выбрать пункт Component Editor в контекстном меню) Для того, чтобы напрямую обратиться к редактору строки подключения. Если стро- ка подключения (Connectionstring) указывает на необходимую вам базу данных, вы Можете отключить диалоговое окно подключения к БД, для этого необходимо при- своить свойству LoginPrompt значение False. Чтобы в предыдущем примере восполь- зоваться новым соединением, присвойте значение ADOConnectionl свойству Con- nection компонента ADOTablel. Вы увидите, что значение свойства Connectionstring станет пустым, так как свойства Connection и Connectionstring исключают друг дру- га. Преимущество использования ADOConnection состоит в том, что строка подклю- чения теперь хранится в одном месте, вместо того чтобы храниться в нескольких Разных компонентах. Еще одно более важное преимущество заключается в том, Что несколько разных компонентов могут использовать одно и то же соединение с сервером базы данных. Если вы не добавите в программу вручную сделанный вами Компонент ADOConnection, каждый компонент ADO будет обладать собственным Уединением с сервером. 0707
708 Глава 15. Технология ADO Файлы связи с данными (Data Link Files) Итак, компонент ADOConnection позволяет вам централизовать определение стро- ки подключения в рамках формы или модуля данных. Однако у описанного под- хода по-прежнему имеется один существенный недостаток: если вы идентифици- руете базу данных при помощи некоторого имени файла, путь к этой базе будет жестко закодирован внутри исполняемого файла приложения. В результате воз- можности приложения будут существенно ограничены. Чтобы решить проблему, в ADO используются так называемые файлы связи с данными (Data Link Files). Файл связи с данными — это строка подключения, оформленная в виде INI- файла. Например, в рамках Delphi устанавливается файл dbdems.udl, в котором содержится следующий текст: [oledb] : Все, что расположено ниже данной строки, является строкой инициализации OLE DB Provider=Microsoft Jet OLEDB 4 0. Data Source=C \Program Files\Common Files\Borland Shared\Data\dbdemos mdb Файл связи с данными может обладать любым расширением, однако рекомен- дуется использовать расширение .UDL. Вы можете создать такой файл при помощи любого текстового редактора. Кроме того, чтобы создать такой файл, вы можете открыть окно проводника Windows, правой кнопкой мыши щелкнуть в одной из папок диска, выбрать New ► Text Document (Создать ► Текстовый документ), сме- нить расширение файла на .UDL (я предполагаю, что в вашей системе проводник отображает расширения файлов), затем сделать двойной щелчок на файле — в ре- зультате будет запущен редактор строки подключения Microsoft. Если в редакторе свойства Connectionstring вы выберете Use Data Link File (Ис- пользовать файл связи с данными), в этом свойстве будет автоматически размеще- на строка 'FILE NAME =', за которой будет указано имя файла связи с данными. Такой прием продемонстрирован в примере DataLinkFile. Файлы связи с данными можно разместить в любом месте диска, однако ADO использует для хранения таких фай- лов некоторый стандартный каталог. Узнать имя этого каталога можно при помощи функции DataLinkDir, которая определяется в модуле ADODB. Если конфигурация — по умолчанию используемая в MDAC, значит, эта функция вернет следующее: С \Program Fi1es\Common Fi1es\System\OLE DB\Data Links Динамические свойства Представьте, что вы занимаетесь разработкой среднего звена, расположенного между клиентами и несколькими базами данных. С одной стороны, вы должны сформировать единый унифицированный программный интерфейс для доступа к нескольким разным базам данных, с другой стороны, этот интерфейс должен обес- печивать доступ к специфическим возможностям каждой из баз данных. Чтобы решить обе эти задачи, вы можете разработать тяжеловесный интерфейс, который будет представлять собой сумму возможностей всех баз данных, для взаимодей- ствия с которыми он предназначен. Каждый класс такого интерфейса должен вклЮ" чать в себя все возможные свойства и методы, однако для работы с конкретной ЬД можно будет использовать лишь подмножество свойств и методов класса. Наде- юсь, не стоит доказывать вам, что это решение не является самым лучшим. ДлЯ 0708
Использование компонентов dbGo 709 решения проблемы в ADO используются динамические свойства (dynamic pro- perties). Фактически все интерфейсы ADO, равно как и соответствующие им компо- ненты dbGo, обладают свойством под названием Properties. Это свойство является коллекцией свойств, специфичных для текущей базы данных. К этим свойствам можно обратиться, указав их порядковый номер, например: ShowMessage(ADOTablel Properties[l] Value): Однако в большинстве случаев удобнее использовать имя: ShowMessagetADOConnectionl Properties[ 'DBMS Name'].Value). Набор динамических свойств определяется типом объекта и провайдером OLE DB. Чтобы вы получили представление о важности динамических свойств, я замечу, что такие компоненты, как ADOConnection или Recordset, поддерживают приблизи- тельно 100 динамических свойств. Как будет показано в данной главе, динамичес- кие свойства активно используются в ADO для решения множества разнообраз- ных задач. СОВЕТ --------------------------------------------------------------------------- Важным событием, имеющим отношение к использованию динамических свойств, является событие OnRecordsetCreate. Впервые это событие появилось в Delphi 6. Событие OnRecordsetCreate генери- руется сразу же после создания Recordset, но перед тем как этот компонент будет открыт. Это событие полезно использовать для настройки тех динамических свойств, которые мог/т быть на- строены только тогда, когда набор записей (Recordset) находится в закрытом состоянии. Получение информации о схеме В ADO для получения информации о схеме используется метод OpenSchema ком- понента ADOConnection. Этот метод принимает четыре параметра: О Тип данных, которые будут возвращаться методом OpenSchema. Это значение THnaTSchemalnfo: набор из 40 значений, включая перечни таблиц, индексов, стол- бцов, представлений и сохраненных процедур. О Фильтр, который необходимо применить в отношении к данным, прежде чем они будут возвращены. Пример этого параметра будет продемонстрирован чуть позже. О GUID для запроса, специфичного для провайдера. Этот параметр использует- ся, только если первый параметр равен значению siProviderSpecific. ° Компонент ADODataSet, в составе которого будут возвращены данные. Этот па- раметр иллюстрирует распространенную в рамках ADO тему: если метод воз- вращает некоторое количество данных, он заносит эти данные в Recordset или, в терминологии Delphi, — в компонент ADODataSet. Чтобы воспользоваться методом OpenSchema, вы должны открыть ADOConnection. Следующий код, который является частью примера OpenSchema, извлекает список первичных ключей для каждой таблицы и заносит их в компонент ADODataSet: ADOConnectionl OpenSchema(siPrimaryKeys. EmptyParam. EmptyParam. ADODataSetl): Каждому полю в составе первичного ключа соответствует одна строка в резуль- тирующем наборе данных. Таким образом, если таблица обладает первичным клю- чом, состоящим из двух полей, в результирующем наборе данных такой таблице 0709
710 Глава 15. Технология ADQ будут соответствовать две строки. Значение EmptyParam указывает на то, что пара- метру присваивается пустое значение, значит, параметр игнорируется. Результат работы кода показан на рис. 15.3. 7?1 ALH)Connect»on4)pen'Schem* Aefrtusfanet Pt hay Keys j [тйоййеIwoheJcolumnjsw T a country Name J customer CustNo j employee EmpNo j dents ItemNo J items OrderNo 1 MSysAccessObfecls ID J orders OrderNo j parts PartNo J vendors VendorNo Рис. 15.3. Программа OpenSchema извлекает из БД информацию о первичных ключах таблиц Если в качестве второго параметра вы передаете значение EmptyParam, в состав результирующего набора данных включается вся информация указанного типа для всей базы данных. Очень часто для удобства вы хотите выполнить фильтрацию информации. Конечно же, для этой цели можно применить к результирующему набору данных традиционный фильтр Delphi (для этого можно использовать свой- ства Filter и Filtered или событие OnFilterRecord). Однако в этом случае фильтрация будет выполняться на стороне клиента. Второй параметр позволяет выполнить фильтрацию более эффективно на стороне источника информации о схеме. Фильтр определяется как массив значений. Каждый элемент массива обладает специаль- ным смыслом, имеющим отношение к типу возвращаемых данных. Например, мас- сив фильтров для первичных ключей включает в себя три элемента: каталог (то есть базу данных), схему и имя таблицы. Этот пример возвращает перечень пер- вичных ключей в таблице Customer: var Filter DLEVAriant. begin Filter .= VarArrayCreate([O, 2]. varVariant). Filter[2] = ‘CUSTOMER’; ADOConnectionl siPrimaryKeys. end; ПРИМЕЧАНИЕ я Ту же информацию можно получить при помощи ADOX. ADOX — это дополнительная технОд°?^ ADO, которая позволяет вам получать и изменять информацию о схеме. В SQL эквивалентом AD является язык DDL (Data Definition Language), то есть выражения CREATE, ALTER, DROP и DCL ( Control Language), то есть выражения GRANT, REVOKE. В рамках dbGo технология ADOX напряму не поддерживается, однако вы можете импортировать библиотеку типов ADOX и использовать приложениях Delphi. В отличие от метода OpenSchema, реализация ADOX в Delphi не универсаль « поэтому использовать ее не всегда удобно. Если вы хотите просто получить информацию о схе но не изменять ее, для этой цели, как правило, удобнее использовать метод OpenSchema. DpenSchema( Filter. EmptyParam. ADODataSetl). 0710
Использование механизма Jet 711 Использование механизма Jet Теперь, когда вы получили базовое представление об MDAC и ADO, мы можем перейти к рассмотрению механизма Jet. Для одних этот механизм представляет интерес, другим он совершенно не нужен. Если вы имеете дело с Access, Paradox, dBase, Excel, Lotus 1-2-3, HTML или данными, хранящимися в текстовых файлах, значит, рассматриваемый здесь материал будет для вас полезным. Если вы не за- интересованы в перечисленных здесь форматах, вы можете пропустить весь этот раздел. Как правило, механизм Jet ассоциируется с базами данных Microsoft Access. Действительно, Access является основной системой, с которой взаимодействует Jet. Однако помимо Access механизм Jet позволяет работать с множеством других локальных источников данных. Многие не подозревают об этом, однако именно в этом заключается одно из основных преимуществ Jet. Взаимодействие с Access через Jet в стандартном режиме работы этого механизма выполняется относитель- но просто, поэтому здесь мы не будем рассматривать этот режим использования Jet. Вместо этого мы подробно рассмотрим взаимодействие Jet с другими форматами. ПРИМЕЧАНИЕ -------------------------------------------------- Механизм Jet входит в состав некоторых (но не всех) версий MDAC. В частности, он отсутствует в версии 2.6. В свое время было много споров относительно того, мог/т ли программисты, использу- ющие средства разработки, не принадлежащие Microsoft, включать в комплект поставки своих про- граммных продуктов механизм Jet. Официально считается, что такое возможно. Механизм Jet можно загрузить бесплатно с веб-узла компании Microsoft, кроме того, этот механизм входит в комплект поставки многих продуктов компании Microsoft. Существует два провайдера OLE DB для механизма Jet: Jet 3.51 OLE DB и Jet 4.0 OLE DB. Провайдер Jet 3.51 OLE DB использует Jet 3.51 и поддерживает рабо- ту только с Access 97. Если вы будете применять только Access 97 и не собираетесь переходить на Access 2000, то Jet 3.51 в большинстве случаев даст более высокую производительность по сравнению с провайдером Jet 4.0 OLE DB. Провайдер Jet 4.0 OLE DB поддерживает работу c Access 97, Access 2000 и с Драйверами IISAM (Installable Indexed Sequential Access Method). Устанавливае- мые драйверы ISAM специально написаны для механизма Jet и обеспечивают до- ступ к таким форматам, как Paradox, dBase и текстовые файлы. Именно возмож- ность использования этих драйверов делает Jet полезным и удобным инструментом. Полный список драйверов ISAM, установленных на вашем компьютере, опреде- ляется набором установленного в системе программного обеспечения. Этот спи- сок располагается в реестре по адресу: BKEV_L0CAL_MACHINE\Software\Microsoft\Jet\4 O\ISAM Formats В состав комплекта поставки Jet входят драйверы для Paradox, dBase, Excel, текстовых файлов и HTML. Доступ к Paradox через Jet Изначально механизм Jet предназначен для работы с базами данных Access. Если вы хотите использовать его для взаимодействия с какой-либо другой базой дан- ных, вы должны сообщить, какой из доайвеоов IISAM следует испояьзовять Атл 0711
712 Глава 15. Технология ADO совершенно безболезненная процедура: вы должны настроить параметр Extended Properties строки подключения. Настроить этот параметр можно при помощи ре- дактора строки подключения. Давайте рассмотрим короткий пример. Добавьте на форму компонент ADOTaЫе и откройте окно редактора строки под- ключения. В списке провайдеров выберите Jet 4.0 OLE DB Provider. Перейдите на вкладку All (Все), перейдите к свойству Extended Properties и сделайте на нем двой- ной щелчок для того, чтобы отредактировать значение этого свойства. В качестве значения этого свойства введите Paradox 7.x (как показано на рис. 15.4) и щелкните на кнопке ОК. Теперь перейдите на вкладку Connection и введите имя каталога, в котором содержатся таблицы Paradox (кнопка Browse вам не поможет, так как с ее помощью вы сможете ввести имя файла, но не сможете указать имя ката- лога). После этого вы сможете выбрать таблицу в свойстве TableName компонента ADOTaЫе и открыть ее на этапе проектирования или во время функционирования про- граммы. Иными словами, теперь вы можете работать с базой данных Paradox при помощи технологии ADO. Эта методика демонстрируется в примере JetParadox. fife beta Link Properties PhHtfet | Connection | Advanced T hese же the irabafeatrcn properties for ibis type erf data To edit a value, select a property, then choose Edit Value below. Name... Data Source Value : E xt ended Properties Paradox 7.x Jet OLEDB Compact With Jet OLEDВ Create System Jet OLEDВ Database Loc Jet OLEDВ Database Pas Jet OLEDВ Don't Copy Lo Jet OLEDВ Encrypt Datab Jet OLEDB Engine Type Jet OLEDB Global Bulk Tr Jet OLEDB Global Partial Jet OLEDB New Database Jet OLEDB Registry Path False False 1 False False .. 0 1 2 OK | Cancel | Help Рис. 15.4. Настройка параметра Extended Properties Мне придется несколько огорчить пользователей Paradox: в определенных ус- ловиях вам придется помимо Jet установить в системе также и BDE. Механизм Jet 4.0 нуждается в BDE для выполнения обновлений таблиц Paradox. Однако читать таблицы Paradox механизм Jet может и без помощи BDE. То же самое можно ска- зать обо всех версиях ODBC-драйвера Paradox. Компанию Microsoft часто крити- ковали за это, в результате были разработаны новые драйверы Paradox IISAM- которые могут полноценно работать без поддержки BDE. Эти драйверы можно получить от службы технической поддержки Microsoft. 0712
Использование механизма Jet 713 ПРИМЕЧАНИЕ--------------------------------------------------------------- По мере того как вы будете изучать ADO, вы поймете, что эта технология в значительной степени зависит от провайдера OLE DB и от конкретной системы RDBMS, с которой вы имеете дело. Несмо- тря на то, что ADO позволяет вам напрямую работать с локальными форматами данных (это демон- стрируется в этом и в следующих примерах), везде, где возможно, рекомендуется устанавливать в системе локальный SQL-сервер. При работе с ADO лучше всего использовать Access или MSDE. Если для вас это не приемлемо, подумайте о возможности использования InterBase или Firebird — об этом рассказывается в главе 14. Доступ к Excel через Jet При помощи провайдера Jet OLE DB вы можете легко обратиться к Excel. Подоб- но тому как это происходило в предыдущем разделе, вы присваиваете параметру Extended Properties значение Excel 8.0. Представим, что у вас есть электронная таб- лица Excel с названием ABCCompany.xls, внутри которой присутствует лист с назва- нием Employees, и вы хотите открыть и прочитать этот файл из приложения, на- писанного на Delphi. Если вы достаточно хорошо знакомы с СОМ, вы можете воспользоваться автоматизацией (automation) компонентов Excel. Однако реше- ние, основанное на ADO, реализуется значительно проще, кроме того, ADO не тре- бует, чтобы на компьютере было установлено приложение Excel. СОВЕТ -------------------------------------------------------------------- Для чтения документов Excel можно воспользоваться также компонентом XLSReadWrite (который можно получить по адресу www.axolot.com). Этот компонент работает и в отсутствие Excel, кроме того, он не требует времени для загрузки Excel (как это происходит в случае использования автома- тизации). Убедитесь в том, что целевая электронная таблица не открыта в Excel. Дело в том, что ADO нуждается в эксклюзивном доступе к файлу. Добавьте на форму компонент ADODataSet. Настройте строку подключения на использование провай- дера Jet 4.0 OLE DB. Присвойте параметру Extended Properties значение Excel 8.0. На вкладке Connection настройте имя базы данных таким образом, чтобы оно соот- ветствовало полному имени каталога и XSL-файла (вы также можете указать от- носительный путь к файлу). Компонент ADODataSet открывает и исполняет значение, хранящееся в свойстве CommandText. Это значение может быть именем таблицы, SQL-выражением, сохра- ненной процедурой или именем файла. Режим интерпретации этого значения оп- ределяется свойством CommandType. Мы хотим указать, что значение свойства CommandText является именем таблицы и что из этой таблицы необходимо получить все столбцы. Для этого мы присваиваем свойству CommandType значение cmdTableDirect. Выберите свойство CommandText в окне Object Inspector, и вы увидите стрелку раскры- вающегося списка. Раскройте список — в нем будет присутствовать единственная псевдотаблица — Exployees$ (рабочие книги Excel отмечаются суффиксом $). Добавьте компонент DataSource и элемент управления DBGrid и соедините их вместе, в результате вы получите вывод примера JetExcel, показанный на рис. 15.5. По умолчанию просмотр данных в таблице несколько затруднен, так как каждый столбец обладает шириной не более 255 символов. Вы можете изменить отобража- емый размер полей, добавив в сетку столбцы и изменив свойства Width или доба- вив постоянные поля и изменив свойства Size или DisplayWidth. 0713
714 Глава 15. Технология ADO К к | La^Namt, firstNaae "I? AccesringAnEMcei Spreadsheet Using Ж i ‘ ! Arthur Dent * 55 41 338-5831 Ford Prefect (41)9957 0293 Marvin Robot (41)232-9198 Tnfcan Тгйап '♦ 55 41 282 2399 Zaphod Beebiebrox 273-3522 Рис. 15.5. Содержимое электронной таблицы ABCCOmpany.xls в Delphi — небольшое посвящение Дугласу Адамсу (Douglas Adams) Обратите внимание, что вы не можете оставить набор данных в открытом со- стоянии и при этом запустить программу, так как драйвер Excel IISAM открывает файлы XLS в режиме эксклюзивного доступа. Закройте набор данных и добавьте в программу код, выполняющий открытие набора в начале работы программы. Ког- да вы запустите программу, вы обнаружите еще одно ограничение драйвера IISAM: вы можете добавлять новые строки, редактировать существующие, но не можете удалять строки. Вместо компонента ADO DataSet можно использовать компоненты ADOTable или ADOQuery, однако при этом необходимо знать, как ADO обрабатывает символы в именах таблиц и полей. Если вы используете ADOTable и раскрываете список таб- лиц, как и ожидается, вы увидите в этом списке таблицу EmployeesS. К сожалению, если вы попытаетесь открыть эту таблицу, система выдаст сообщение об ошибке. То же самое относится и к выражению SELECT * FROM EmployeesS в компоненте TADOQuery. Проблема связана с символом доллара в имени таблицы. Если в именах таблиц и/или полей вы используете такие символы, как доллар, точка или символ пробела, вы должны заключить имя в квадратные скобки (например, [EmployeesS]). Доступ к текстовым файлам через Jet Драйвер IISAM для доступа к текстовым файлам является одним из самых полез- ных драйверов механизма Jet. Этот драйвер позволяет вам читать и изменять тек- стовые файлы фактически любого формата. Сначала мы рассмотрим простой тек- стовый файл, а затем изучим различные варианты. Представьте, что у вас есть текстовый файл с именем NightShift.TXT, в котором содержится следующий текст: CrewPerson Neo Trinity Morpheus .Hometown .Cincinnati .London .Milan Добавьте на форму компонент ADOTable. Настройте строку подключения на ис- пользование провайдера Jet 4.0 OLE DB. Присвойте параметру Extended Properties строки подключения значение Text. Провайдер Text IISAM рассматривает отдель- ный каталог как единую базу данных, поэтому в качестве имени базы данных вЫ должны указать имя каталога, в котором содержится файл NightShift.TXT. В окне Object Inspector откройте список таблиц в свойстве TableName. Обратите внимание, 0714
Использование механизма Jet 715 что текстовый файл NightShift.TXT рассматривается как таблица, а точка в названии файла заменена символом решетки. Итак, имя таблицы NightShiftflTXT. Присвойте свойству Active значение True. Добавьте компонент DataSource и элемент управления DBGrid, соедините их вместе, и вы увидите, как содержимое файла отображается в сетке. ВНИМАНИЕ ------------------------------------------------------------------------- Если в вашей системе в качестве десятичного разделителя используется запятая, а не точка (то есть вместо числа 1,000.00 отображается число 1.000,00), вы можете либо изменить национальные настройки (Start ► Settings ► Control Panel ► Regional Settings ► Numbers (Пуск ► Настройка ► Панель управления ► Язык и региональные стандарты ► Числа)), либо должным образом настроить файл SCHEMA.INI. О файле SCHEMA.INI будет рассказано в самом ближайшем времени. Ширина столбцов в сетке составляет 255 символов. Вы можете изменить это значение, как вы делали это в примере JetExcel. Для этого в сетку необходимо до- бавить постоянные поля или колонки и настроить соответствующие свойства этих колонок. Кроме того, вы можете определить структуру текстового файла в специ- альном конфигурационном файле SCHEMA.INI. В примере JetTeXt папка базы данных определяется на этапе исполнения в зави- симости от папки, в которой располагается программа. Чтобы модифицировать строку подключения во время исполнения программы, загрузите эту строку в спи- сок строк (выполнив предварительно преобразование разделителей) и восполь- зуйтесь свойством Values для того, чтобы изменить один из элементов строки под- ключения. Вот код из примера: procedure TForml.FormCreate(Sender TObject). var si- TStringList; begin si ; = TStnngList.Create; si.Text = StringReplace (ADOTablel Connectionstring. sLineBreak. [rfReplaceAll]), si.Values ['Data Source'] •= ExtractFilePath (Application.ExeName). ADOTablel.Connectionstring := StringReplace (si Text. sLineBreak, [rfReplaceAll]), ADOTablel Open. si Free; end; Для хранения таблиц в текстовых файлах могут использоваться самые разно- образные форматы. Чаще всего вы можете не беспокоиться о формате текстового файла, так как драйвер Text IISAM просматривает первые 25 записей файла, пы- таясь самостоятельно определить формат файла без вашей помощи. Если ему это не удается или он делает это неправильно, вы можете добавить необходимую кон- фигурационную информацию в файл SCHEMA.INI, расположенный в том же ката- логе, что и текстовые файлы, с которыми вы работаете. В этом файле содержится информация о схеме базы данных (также эту информацию называют метаданны- ми). В файл SCHEMA.TXT можно поместить информацию, которая относится к лю- бому из файлов этого же самого каталога или ко всем файлам каталога. Каждому файлу с данными соответствует отдельный раздел файла SCHEMA.TXT. Напри- мер, файлу NightShift.TXT мы можем поставить в соответствие раздел с именем [NightShift.TXT], 0715
716 Глава 15. Технология ADO В этом разделе мы можем определить формат файла NightShift.TXT. Иными сло- вами, мы можем определить имена, типы и размеры столбцов, используемый на- бор символов и любые специальные форматы колонок (например, дата/время или валюта). Представьте, что вы изменили формат файла NightShift.TXT следующим образом: Neo (Cincinnati Trinity I London Morpheus I Milan 4 Обратите внимание, что в данном случае из файла исключены имена колонок, а в качестве символа-разделителя используется вертикальная черта. Соответству- ющий файл SCHEMA.TXT может выглядеть следующим образом: [NightShift TXT] Format=Delimited(|) ColNameHeader=False Coll=CrewPerson Char Width 10 Col2=HomeTown Char Width 30 Вне зависимости от того, используете ли вы файл SCHEMA.TXT или нет, драйвер Text IISAM обладает двумя ограничениями: вы не можете удалять строки и не можете редактировать строки. Импорт и экспорт Механизм Jet удобно использовать для импорта и экспорта данных. Процесс экс- портирования данных одинаков для каждого экспортированного формата и состо- ит из исполнения выражения SELECT в специальном формате. Рассмотрим пример экспортирования данных из базы данных Access в примере DBDemos в таблицу Paradox. Для этого добавим в программу JetlmportExport активное соединение ADOConnection с названием ADOConnectionl. Это соединение использует механизм Jet для того, чтобы открыть базу данных. Следующий код экспортирует таблицу Customer в файл Customer.db формата Paradox: SELECT * INTO Customer IN ”C \tmp" “Paradox 7 x.“ FROM CUSTOMER Рассмотрим составные части этого SQL-выражения. После ключевого слова INTO указывается новая таблица, которая будет создана в результате выполнения опе- ратора SELECT. До выполнения этого кода таблица с этим именем должна отсут- ствовать в базе. После ключевого слова IN указывается база данных, в которую добавляется новая таблица. В Paradox это должен быть каталог, который уже су- ществует на диске. Сразу же после имени базы данных указывается имя драйвера IISAM, который будет использоваться для экспорта данных. В конце имени драй- вера обязательно нужно добавить символ точки с запятой (;). Ключевое слово FROM является стандартным компонентом любого выражения SELECT. В рассматривае- мом примере эта операция выполняется при помощи компонента ADOConnectionl, вместо фиксированного имени каталога используется текущий каталог програм- мы: ADOConnectionl Execute (."SELECT * INTO Customer IN + CurrentFolder + "Paradox 7 x, " FROM CUSTOMER'). Точно такой же формат используется в любых операциях экспорта, однако в зависимости от используемого драйвера I1SAM имя базы данных интерпретирУ' 0716
Работа с курсорами 717 ется по-разному. Вот аналогичное выражение, которое экспортирует данные в таб- лицу Excel: ADOConnectwnl Execute (‘SELECT ★ INTO Customer IN '“ + CurrentFolder + 'Memos xls" "Excel 8 0." FROM CUSTOMER"). Новый файл Excel с именем dbdemos.xls создается в текущем каталоге програм- мы. В этот документ Excel добавляется рабочая книга с именем Customer, в кото- рую заносятся все данные из таблицы Customer базы данных Access с именем dbdemo. mdb. Вот еще одно выражение, которое экспортирует те же самые данные в HTML- файл: ADOConnectwnl Execute (.'SELECT ★ INTO [customer htm] IN '" + CurrentFolder + .HTML Export," FROM CUSTOMER'). В данном случае база данных — это каталог (как и в Paradox). Имя таблицы включает в себя расширение .htm, поэтому имя таблицы необходимо заключить в квадратные скобки. Обратите внимание, что драйвер IISAM называется не про- сто HTML, a HTML Export. Как следует из названия, драйвер позволяет только экспортировать данные, но не позволяет импортировать их. Наконец, давайте рассмотрим входящий в состав Jet драйвер HTML Import, который является полезным дополнением к HTML Export. Добавьте на форму компонент ADOTable. Настройте строку подключения Connectionstring на исполь- зование провайдера Jet 4.0 OLE DB. Присвойте параметру Extended Properties стро- ки подключения значение HTML Import. В качестве имени базы данных укажите имя HTML-файла, который был создан в результате экспорта (чуть ранее), точнее го- воря, Customer.htm. Теперь присвойте свойству TableName значение Customer. От- кройте таблицу — вы только что импортировали данные из HTML-файла. Имейте в виду, что если вы попытаетесь обновить данные, система выдаст ошибку, так как драйвер предназначен только для импорта. Если вы создали собственный HTML- файл, в котором содержатся таблицы, и хотите открыть эти таблицы с использова- нием данного драйвера, вы должны помнить, что имя таблицы — это значение тега caption в HTML-разделе table. Работа с курсорами У каждого из наборов данных ADO есть два свойства, которые неразрывно связа- ны друг с другом и оказывают значительное влияние на ваше приложение. Это свойства CursorLocation и CursorType. Если вы хотите понять принцип функциони- рования набора данных ADO, вы должны изучить эти два свойства. Положение курсора (свойство CursorLocation) Свойство CursorLocation определяет, каким образом осуществляется извлечение и модификация данных. Этому свойству можно присвоить одно из двух значений: dUseClient (курсор на стороне клиента) или clUseServer (курсор на стороне серве- ра). Выбор значения в большой степени влияет на функциональность, производи- тельность и масштабируемость базы данных. 0717
718 Глава 15. Технология ADO Клиентский курсор обслуживается механизмом ADO Cursor Engine. Этот ме- ханизм является превосходным примером провайдера обслуживания OLE DB: он обеспечивает обслуживание для других провайдеров OLDE DB. Механизм ADO Cursor Engine управляет обработкой данных на стороне клиента. При открытии набора данных все данные результирующего набора перекачиваются с сервера на клиентский компьютер. После этого данные хранятся в памяти, их обновление и обработка осуществляется с использованием ADO Cursor Engine. Этот подход напо- минает использование ClientDataSet в приложениях dbExpress. Преимущество состо- ит в том, что после передачи данных на сторону клиента любые манипуляции с этими данными выполняются значительно быстрее. Кроме того, так как манипуляции вы- полняются в памяти, механизм ADO Cursor Engine обладает более широкими воз- можностями, чем любой из курсоров, работающих на стороне сервера. Далее я под- робнее рассмотрю эти преимущества, а также другие технологии, основанные на клиентских курсорах (в частности, отключенные и постоянные наборы записей). Курсор на стороне сервера управляется самой системой RDBMS. В клиент-сер- верной архитектуре, основанной на таких продуктах, как SQL Server, Oracle или InterBase, это означает, что управление курсором осуществляется на удаленном серверном компьютере. Если речь идет о настольной базе данных, такой как Access или Paradox, серверный курсор управляется программным продуктом, обслужи- вающим базу данных. То есть логически курсор расположен на «сервере», однако физически база данных вместе с курсором располагается на клиентском компью- тере. Как правило, серверные курсоры загружаются быстрее, чем клиентские кур- соры, так как при открытии набора данных с серверным курсором нет необходи- мости перемещать все данные на сторону клиента. Благодаря этому серверные курсоры лучше подходят для обслуживания больших наборов данных, то есть тог- да, когда клиентский компьютер не обладает объемом памяти, достаточным для хранения всего набора данных. Чтобы понять возможности курсоров обоих типов, лучше всего посмотреть, как они функционируют в той или иной ситуации. На- пример, можно взять ситуацию блокирования записей. Чуть позднее я более под- робно расскажу о блокировании. (Если вы хотите заблокировать запись, вам по- требуется серверный курсор, так как система RDBMS должна знать о том, что запись заблокирована.) Еще одной характеристикой, на которую следует обратить внимание при выбо- ре местоположения курсора, является масштабируемость. Серверные курсоры рас- полагаются на стороне сервера. Чем больше пользователей подключается к базе, тем больше курсоров создается на сервере. С каждым новым курсором нагрузка на сервер возрастает. Таким образом, при увеличении количества пользователей об- щая производительность системы может существенно понизиться. Используя кур- соры на стороне клиента, вы можете существенно повысить масштабируемость вашего приложения. Открытие клиентского курсора обойдется вам дороже, так как в процессе открытия все данные передаются на сторону клиента, однако об- служивание клиентского курсора менее обременительно для сервера, ведь основ- ная связанная с этим нагрузка возлагается на клиентский компьютер. Тип курсора (свойство CursorType) Тип курсора во многом определяется местом расположения курсора. Существует пять типов курсоров, один из которых не используется. Неиспользуемый тип на- 0718
Работа с курсорами 719 зывается unspecified (неуказанный). В ADO существует много значений, которые соответствуют неуказанному значению. В Delphi эти значения фактически никог- да не используются. Эти значения присутствуют в Delphi только потому, что они присутствуют в ADO. Дело в том, что технология ADO изначально разрабатыва- лась для таких языков, как Visual Basic и С. В этих языках вы работаете с объекта- ми напрямую, без поддержки вспомогательных механизмов, таких как dbGo. В ре- зультате вы можете создать открытый набор записей (в терминологии ADO — recordset), не указывая при этом значения для каждого из свойств. Таким образом, значения некоторых свойств будут не определены. В этом случае свойству присва- ивается значение unspecified (не указано). Однако в рамках dbGo вы имеете дело с компонентами. Компоненты обладают конструкторами. Конструктор — это фун- кция, которая в обязательном порядке инициализирует каждое из свойств компо- нента. Когда вы создаете компонент dbGo, каждое из его свойств обладает опре- деленным значением. В итоге отпадает надобность в использовании значения unspecified (не указано). Тип курсора влияет на то, каким образом происходит чтение и обновление дан- ных. Можно использовать один из четырех типов курсора: Forward-Only (только вперед), Static (статический), Keyset (набор ключей) и Dynamic (динамический). Прежде чем переходить к обсуждению разнообразных комбинаций типов и место- положения курсора, отмечу одно важное обстоятельство: для курсоров, работаю- щих на стороне клиента, можно использовать только один тип: статический кур- сор. Все остальные типы курсоров могут использоваться только на стороне сервера. Давайте подробнее рассмотрим типы курсоров в порядке возрастания затрат, свя- занных с их обслуживанием. о Forward-only (только вперед). Курсоры этого типа обходятся дешевле всего в смысле затрат. Иными словами, такие курсоры обеспечивают самую высо- кую производительность. Как следует из имени, курсор Forward-only (только вперед) позволяет вам перемещаться по набору данных в направлении от нача- ла к концу. Курсор читает с сервера количество записей, указанное в свойстве CacheSize (по умолчанию 1), каждый раз, как только он покидает последнюю запись в локальном кэше, он читает с сервера следующую порцию записей. Любая попытка переместиться по направлению к началу набора записей за пре- делы локального кэша приводит к возникновению ошибки. Это поведение на- поминает поведение набора данных в библиотеке dbExpress. Курсор Forward- only (только вперед) плохо подходит для формирования пользовательского интерфейса, в котором пользователь обладает возможностью контролировать направление перемещения. Вместе с тем, такой курсор вполне подходит для выполнения пакетных операций, формирования отчетов, при построении веб- приложений, не сохраняющих информацию о состоянии, — в любой из этих ситуаций вы начинаете с начала набора данных и перемещаетесь по направлению к концу набора данных. По достижении конца набор данных закрывается. ° Static (статический). При использовании статического курсора набор данных полностью перемещается на сторону клиента, обращение к нему осуществля- ется при помощи окна размером CacheSize. В результате пользователь получает возможность перемещаться по набору данных в обоих направлениях. Недоста- ток заключается в том, что данные являются статическими — обновления, до- 0719
720 Глава 15. Технология Арр бавления и удаления записей, выполняемые другими пользователями, не вид- ны для статического курсора, так как данные курсора уже прочитаны. О Keyset (набор ключей). Чтобы понять принцип функционирования этого кур- сора, разделите слово Keyset на две части: key и set. Key — это ключ, то есть в данном контексте — идентификатор записи. Зачастую имеется в виду пер- вичный ключ. Set — это множество или набор. Получается «набор ключей». При открытии набора данных с сервера читается полный список всех ключей. Например, если набор данных формируется при помощи выражения SELECT * FROM CUSTOMER, значит, список ключей можно сформировать при помощи выра- жения SELECT CUSTID FROM CUSTOMER. Набор ключей хранится на стороне клиен- та до закрытия курсора. Когда приложение нуждается в данных, провайдер OLE DB читает строки таблицы, используя для этой цели имеющийся у него набор ключей. В результате клиент всегда имеет дело с обновленными данными. Од- нако набор ключей является статическим в том смысле, что после открытия курсора в этот набор нельзя добавить новые ключи, также ключи нельзя уда- лить из набора. Иными словами, если другой пользователь добавляет в табли- цу новые записи, эти изменения не будут видны для клиента. Удаленные запи- си становятся недоступными, а любые изменения в первичных ключах (как правило, пользователям запрещается менять первичные ключи) также стано- вятся недоступными. О Dynamic (динамический). Это наиболее дорогостоящий курсор. Динамичес- кий курсор функционирует приблизительно так же, как курсор набора ключей. Разница заключается в том, что набор ключей заново читается с сервера каж- дый раз, когда приложение нуждается в данных, отсутствующих в кэше. Так как значение свойства ADODataSet.CacheSize по умолчанию равно 1, запросы на чтение данных возникают достаточно часто. Можно себе представить дополни- тельную нагрузку, которую данный курсор создает на сервер DBMS и на сеть. Однако при использовании этого курсора клиент знает не только об изменени- ях данных, но и о добавлениях и удалениях, выполняемых другими клиентами. Вы не всегда получаете то, о чем просите Теперь, когда вы знаете о типах и местоположении курсора, я должен предупре- дить вас о том, что допускается использование далеко не всех комбинаций типов и местоположений курсора. Как правило, это ограничение связано с типом RDBMS и/или провайдером OLE DB. Например, если курсор располагается на стороне клиента, тип курсора может быть только статическим. Вы можете понаблюдать подобное поведение самостоятельно. Добавьте на форму компонент ADODataSet, настройте свойство Connectionstring для подключения к любой базе данных, после этого присвойте свойству ClientLocation значение clUseCursor, а свойству CursorType — значение ctDynamic. Теперь измените значение свойства Active на True и понаблю- дайте за свойством CursorType. Значение этого свойства немедленно изменится на ctStatiс. Следует сделать важный вывод: вы далеко не всегда получаете именно то, о чем просите. Открыв набор данных, всегда проверяйте значения свойств — неко- торые из них могут самопроизвольно изменить свои значения. Для различных провайдеров OLE DB характерны разные изменения свойств. Приведу лишь несколько примеров: 0720
Работа с курсорами 721 О провайдер Jet 4.0 OLE DB изменяет большинство типов курсоров на Keyset (набор ключей); О провайдер SQL Server OLE DB часто меняет Keyset (набор ключей) и Static (статический) на Dynamic (динамический); О провайдер Oracle OLE DB меняет все типы курсоров на Forward-only (только вперед); О провайдер ODBC OLE DB может выполнить самые разные изменения типа курсора в зависимости от используемого драйвера ODBC. Отсутствие счетчика Когда вы пытаетесь прочитать свойство RecordCount какого-либо набора данных ADO, иногда вы обнаруживаете, что это свойство равно -1. Курсор типа Forward- only не знает, какое количество записей входит в состав набора данных, пока он не достигнет конца набора. По этой причине свойство RecordCount равно значению -1. Статический курсор всегда знает, какое количество записей входит в набор данных, так как статический курсор читает все данные набора в момент открытия. Курсор типа Keyset (набор ключей) тоже знает количество записей в наборе, так как в момент открытия набора данных он извлекает из базы данных фиксирован- ный набор ключевых значений. Таким образом, для курсоров Static и Keyset вы можете обратиться к свойству RecordCount и получить точное количество записей в наборе. Динамический курсор не может достоверно знать количество записей, так как каждый раз при чтении данных он заново читает набор ключей, поэтому свой- ство RecordCount для этого курсора всегда равно -1. Вы можете вообще отказаться от использования свойства RecordCount и вместо этого использовать выражение SELECT COUNT(*) FROM имя_таблицы. Однако в результате вы получите неточное зна- чение количества записей в таблице базы данных — это значение далеко не всегда совпадает с количеством записей в наборе данных. Клиентские индексы Одним из преимуществ курсоров, работающих на стороне клиента, является воз- можность создания локальных, или клиентских, индексов. Представьте, что у вас есть набор данных ADO с клиентским курсором и что этот набор соединен с таб- лицей Customer из примера DBDemos. Представьте, что к этому набору подключена сетка DBGrid. Присвойте свойству IndexFieldNames значение CompanyName. Сетка немедленно отобразит записи, упорядочив их в соответствии со значением поля CompanyName. Важно отметить, что для формирования индекса ADO не читает за- ново данные из источника. Индекс формируется на основе данных, хранящихся в памяти. Благодаря этому, во-первых, индекс формируется достаточно быстро, во-вторых, не создается никакой дополнительной нагрузки на сеть и DBMS. В про- тивном случае одни и те же данные пришлось бы раз за разом передавать через сеть в различном порядке сортировки. Свойство IndexFieldNames обладает еще кое-какими интересными возможнос- тями. Например, присвойте этому свойству значение Country;CompanyName — вы Увидите, что записи сначала отсортированы в соответствии с именем страны, а за- 0721
722 Глава 15. Технология ADO тем — в соответствии с именем компании. Теперь присвойте свойству IndexField- Names значение CompanyName DESC (ключевое слово DESC должно быть написано заглавными буквами, но не desc или Desc). В результате записи будут отсортирова- ны в порядке убывания значений. Эта простая, но весьма мощная возможность позволяет вам решить одну из наи- более актуальных проблем, связанных с программированием БД. Пользователи любят задавать неизбежный и неприятный для программистов, но совершенно оправданный вопрос: «Могу ли я щелкнуть на заголовке столбца сетки для того, чтобы отсортировать мои данные?» Существует несколько способов решения этой проблемы. Например, вы можете воспользоваться стандартным (не поддержива- ющим работу с данными) элементом управления, таким как ListView, который поддерживает встроенный механизм сортировки. Кроме того, вы можете выполнить обработку события OnTitleClick компонента DBGrid и в рамках обработчика заново ис- полнять SQL-выражение SELECT, добавляя к нему подходящую команду ORDER BY. Однако любое из этих решений нельзя назвать в полной мере удовлетворительным. Если данные кэшируются на стороне клиента (мы уже обсуждали этот подход, когда говорили о компоненте ClientDataSet), вы можете воспользоваться индексом, сформированным в памяти клиентского компьютера. Добавьте следующий обра- ботчик события OnTitleClick для сетки (полный исходный код входит в состав при- мера Clientindexes): procedure Tfroml DBGridlTitleClick(Column Tcolumn) begin if ADODataSetl IndexFieldNames = Column Field FieldName then ADODataSetl IndexFieldNames = Column Field FieldName + ' DESC el se ADODataSetl IndexFieldNames = Column Field FieldName end Этот простой код проверяет, построен ли текущий индекс на основе поля, кото- рое соответствует столбцу сетки, на заголовке которого сделан щелчок Если да, то на основе этого же поля строится новый индекс, но в нисходящем порядке Если нет, то на основе столбца формируется новый индекс. Когда пользователь щелкает на заголовке столбца первый раз, записи сортируются в порядке увеличения зна- чений Когда пользователь щелкает на этом же столбце второй раз, записи сорти- руются в порядке уменьшения. Вы можете усовершенствовать этот обработчик таким образом, чтобы позволить пользователю щелкать на нескольких заголов- ках, удерживая нажатой клавишу Ctrl. При этом можно формировать более слож- ные индексы. ПРИМЕЧАНИЕ-------------------------------------------------------------- Все то же самое можно реализовать с использованием компонента ClientDataSet, однако этот ком- понент не поддерживает ключевого слова DESC, поэтому для сортировки в порядке уменьшения значений вам потребуется написать дополнительный код. Более того, при смене порядка сортиров- ки компонент ClientDataSet будет заново формировать индекс — это ненужная и, возможно, мед- ленная операция. Клонирование Технология ADO поддерживает множество интересных возможностей Вы мо- жете пожаловаться, что обилие возможностей приводит к увеличению размер3 0722
\ Работа с курсорами 723 исполняемого кода, который приходится устанавливать на клиентском компьютере. Однако благодаря обилию возможностей ADO вы можете формировать мощные и надежные приложения. Одной из удобных возможностей ADO является возмож- ность клонирования. Клонированный набор записей — это новый набор записей, который обладает точно таким же набором свойств, как и изначальный. Вначале я объясню, как происходит клонирование, затем расскажу о том, зачем это надо. ПРИМЕЧАНИЕ------------------------------------------------------------------ Компонент ClientDataSet также поддерживает клонирование, однако я не упомянул об этой возмож- ности в главе 13. Для клонирования набора данных (в ADO — набора записей) используется метод Clone. Клонировать можно любой набор данных ADO, однако в данном при- мере мы будет использовать компонент ADOTable. В программе DataClone (рис. 15.6) присутствуют два компонента ADOTable — один из них подключен к данным, а вто- рой пуст. Оба набора данных подключены к источнику данных DataSource и сетке. Когда пользователь щелкает на кнопке Clone Dataset (клонировать набор данных), выполняется всего одна строка кода, которая клонирует набор данных: AD0Table2 Clone(ADOTablel) IT?* DataClone 1221 Kauai Dive Shoppe 1231 Umsco 1351 Sight Diver 1354 Cayman Divers World Unlimited 1356 Tom Sawyer Diving Centre 1380 Blue Jack Aqua Center 1384 VIP Divers Club 1510 Ocean Paradise 1513 Fantastique Aquatica 1551 1560 The Depth Charge 1563 1624 Makai SCUBA Club 1645 Action Club Marmot Drvers Oub Blue Sports м. Kauai Dive Shoppe Umsco Sight Diver PC Cus*»o 1221 1231 1351 1354 Cayman Divers World Unlimited 1356 Tom Sawyer Diving Centre 1380 Blue Jack Aqua Center 1384 VIP Divers Club 1510; 1513 1551 1560 1563 1624 1645 Action Club : Ocean Paradise Fantastique Aquatica Marmot Divers Club The Depth Charge Blue Sports Make SCUBA Club PC 63' 23 32 pc ZC 87 15 20 PC PC > Рис. 15.6. Форма примера DataClone с двумя копиями набора данных (изначальный и клонированный) Эта строка клонирует набор данных ADOTablel и размещает полученный клон в наборе данных AD0Table2. Благодаря этому вы получаете два представления одних и тех же данных. Каждый набор обладает собственным указателем на текущую запись и собственной копией информации о состоянии, благодаря этому клон ни- как не влияет на изначальную копию данных. Подобное поведение делает клоны отличным инструментом работы с набором данных, не влияя при этом на изна- чальные данные. Еще одна интересная возможность: вы можете создать несколько Разных активных записей — у разных клонов активные записи могут быть разны- ми. Подобную функциональность нельзя реализовать в Delphi, используя лишь Один набор данных. 0723
724 Глава 15. Технология ADO СОВЕТ----------------------------------------------------------------------------------— Набор данных можно клонировать только в случае, если он поддерживает закладки (bookmarks). По этой причине курсоры типа «только вперед» и динамические курсоры не могут быть клонированы. Чтобы определить, поддерживает ли набор записей закладки, вы можете воспользоваться методом Supports (например, ADOTablel.Supports([coBookMark])). Побочный эффект клонирования заключает- ся в том, что закладки, созданные одним из клонов, могут использоваться всеми остальными клонами. Обработка транзакций В разделе «Использование транзакций» главы 14 мы с вами говорили о том, что механизм транзакций позволяет разработчикам группировать отдельные опера- ции в отношении БД в единую логически неразрывную процедуру. Обработка транзакций в ADO осуществляется при помощи компонента ADOCon- nection, для этого используются методы BeginTrans, CommitTrans и RoLLbackTrans. Дей- ствие этих методов сходно с аналогичными методами dbExpress и BDE. Для изу- чения механизма транзакций, встроенного в ADO, воспользуемся программой TransProcessing. В состав программы входит компонент ADOConnection, строка под- ключения которого (свойство Connectionstring) настроена на использование про- вайдера Jet 4.0 OLE DB и на обращение к файлу dbdemos.mdb. В программе при- сутствует компонент ADOTable, подключенный к таблице Customer и связанный с компонентами DataSource и DBGrid для отображения данных. Наконец, в программе присутствуют три кнопки, предназначенные для выполнения следующих команд: ADOConnect ionl.BeginTrans; ADOConnect r onl.Comnn tTrans: ADOConnectionl.Ro11 ba ckTra ns; Используя эту программу, вы можете вносить в базу данных изменения, а за- тем выполнять откат транзакции, то есть отмену этих изменений. В результате база данных будет восстановлена в состояние, в котором она находилась до начала тран- закции. Следует отметить, что обработка транзакций выполняется по-разному в зависимости от базы данных и провайдера OLE DB. Например, если вы подклю- читесь к Paradox с использованием провайдера ODBC OLE DB, вы получите со- общение об ошибке, указывающее на то, что база данных или провайдер OLE DB не могут начать транзакцию. Чтобы определить уровень поддержки транзакции, можно воспользоваться динамическим свойством Transaction DDL соединения: If ADOConnectionl Propertiest'Transaction 001'].Value > DBPRDPVAL_TC_NONE then ADOConnectionl.BeginT rans; Если вы попытаетесь обратиться к этой же базе данных Paradox при помощи провайдера Jet 4.0 OLE DB, никакой ошибки не возникнет, однако из-за ограниче- ний провайдера вы не сможете выполнить откат транзакции. Еще одно странное отличие проявляет себя при работе с Access: если вы ис- пользуете провайдер ODBC OLE DB, вы сможете использовать транзакции, однако не сможете использовать вложенные транзакции. Попытка открыть новую тран закцию параллельно с уже существующей активной транзакцией приведет к воз никновению ошибки. Однако при использовании механизма Jet вы сможете ое проблем использовать вложенные транзакции. 0724
Обработка транзакций 725 Вложенные транзакции Используя программу Trans Processing, попробуйте выполнить следующий тест. 1. Активизируйте транзакцию. 2. Измените значение поля ContactName записи Around The Horn: вместо Thomas Hardy поставьте Dick Solomon. 3. Активизируйте еще одну, вложенную транзакцию. 4. Измените значение поля ContactName записи Bottom-Dollar Markets: вместо Eliza- beth Lincoln поставьте Sally Solomon. 5. Выполните откат внутренней транзакции. 6. Подтвердите внешнюю транзакцию. В результате модификации должны быть внесены только в запись Around The Horn. Если же внутренняя транзакция будет подтверждена, а в отношении внеш- ней транзакции вы выполните откат, в результате в базу данных вообще не будет внесено ни одного изменения (даже изменения, сделанные в рамках внутренней транзакции). Именно так работают вложенные транзакции. Существует ограни- чение: Access поддерживает только пять уровней вложения транзакций. ODBC не поддерживает вложенных транзакций, а провайдер Jet OLE DB под- держивает до пяти уровней вложения. Провайдер SQL Server OLE DB вообще не поддерживает вложения транзакций. Вы должны иметь в виду, что вложение тран- закций может обрабатываться по-разному в зависимости от версии SQL-сервера или драйвера. Необходимую информацию можно получить в документации и при помощи экспериментов. Судя по всему, в большинстве случаев внешняя транзак- ция определяет, будут ли внесены в базу данных изменения, сделанные в рамках внутренней транзакции. Атрибуты компонента ADOConnection Если вы планируете использовать вложенные транзакции, существует еще одно обстоятельство, которое вы должны принимать во внимание. Компонент ADOCon- nection обладает свойством Attributes, которое определяет, каким образом ведет себя соединение в момент, когда транзакция подтверждается или выполняется ее от- кат. В свойстве Attributes хранится множество значений TXActAttributes, которое изначально пусто. Перечисление TXActAttributes включает в себя только два значе- ния: xaCommitRetaining и xaAbortRetaining (иногда это значение ошибочно записы- вают как ха Rollback Retai ni n g, так как с логической точки зрения это более правиль- ное название). Если в свойстве Attributes присутствует атрибут xaCommitRetaining, в момент подтверждения транзакции автоматически открывается новая транзак- ция. Если в свойстве Attributes присутствует атрибут xaAbortRetaining, в момент от- ката транзакции автоматически открывается новая транзакция. Таким образом, если вы добавите в свойство Attributes оба этих атрибута, любые действия будут выполняться в рамках транзакции: в момент завершения очередной транзакции будет автоматически активизироваться следующая. В большинстве случаев программисты предпочитают отказаться от работы в таком режиме и самостоятельно контролировать открытие транзакций, поэтому 0725
726 Глава 15. Технология ADQ данные атрибуты используются нечасто. Следует принимать во внимание особен- ности использования этих атрибутов совместно с вложенными транзакциями. Если вы создаете вложенную транзакцию и присваиваете свойству Attributes значение [xaCommitRetaining, xaAbortRetaining], внешняя транзакция никогда не будет завер- шена. Рассмотрим такую последовательность событий. 1. Начинается внешняя транзакция. 2. Начинается внутренняя транзакция. 3. Выполняется подтверждение или откат внутренней транзакции. В соответствии с настройкой свойства Attributes автоматически начинается но- вая внутренняя транзакция. Таким образом, внешняя транзакция может никогда не завершиться, так как внутренние транзакции неразрывно следуют одна за другой. Можно сделать вы- вод, что свойство Attributes и использование вложенных транзакций — это взаимо- исключающие вещи. Типы блокировки Технология ADO поддерживает четыре различных подхода к блокированию дан- ных для обновления: LtReadOnly, ItPessimistic, LtOptimistic и LtBatchOptimistic (суще- ствует также тип LtUnspecified, однако по изложенным ранее причинам этот тип в Delphi не используется). Для настройки режима блокировки используется свой- ство LockType. В данном разделе я коротко расскажу обо всех четырех способах бло- кировки. В последующих разделах о каждом из этих способов будет рассказано подробнее. Значение LtReadOnly указывает на то, что данные предназначены только для чте- ния, — обновление невозможно. Так как клиент не может выполнить модифика- цию данных, никакой блокировки не требуется. Значения ItPessimistic и LtOptimistic обеспечивают пессимистическую и опти- мистическую блокировку соответственно. Эти режимы являются эквивалентами аналогичных режимов BDE. Однако по сравнению с BDE технология ADO обес- печивает большую гибкость: выбор режима блокировки остается за вами. Если вы используете BDE, решение об использовании пессимистической или оптими- стической блокировки выполняет за вас драйвер BDE. Если вы используете настольную базу данных, такую как Paradox или dBase, значит, драйвер BDE использует пессимистическую блокировку. Если вы используете клиент-сервер- ную базу данных, такую как InterBase, SQLServer или Oracle, драйвер BDE ис- пользует оптимистическую блокировку. Пессимистическая блокировка В данном контексте терминами «оптимистическая» и «пессимистическая» харак- теризуется ожидание программиста относительно возможности возникновения конфликтов при обновлении содержащихся в БД данных одновременно несколь- кими пользователями. Пессимистическая блокировка предполагает, что вероят- ность возникновения конфликта велика. Иными словами, пользователи в одно и то же время модифицируют содержащиеся в БД данные, и высока вероятность того, что два пользователя в одно и то же время попытаются модифицировать одну и ту 0726
Обновление данных 727 же запись базы. Чтобы предотвратить подобный конфликт, запись блокируется в момент, когда начинается редактирование. Запись остается заблокированной до тех пор, пока редактирование завершается или отменяется. Если какой-либо дру- гой пользователь попытается отредактировать ту же самую (заблокированную) запись, он не сможет этого сделать: возникнет исключение «Обновление невоз- можно, запись заблокирована». Этот подход хорошо знаком программистам, которые ранее работали с настоль- ными базами данных, такими как dBase и Paradox. Преимущество состоит в том, что пользователь знает, что если он начал редактировать запись, то сможет успеш- но завершить редактирование и внести модификации в базу. Недостаток — в том, что пользователь полностью контролирует блокирование записи. Если пользова- тель хорошо освоил работу с приложением, редактирование одной записи может занять всего пару секунд, однако в клиент-серверной среде с множеством пользо- вателей даже пара секунд может показаться вечностью. С другой стороны, ничего не подозревающий пользователь может начать редактирование записи и уйти на обед. В этом случае запись останется заблокированной до тех пор, пока он не вер- нется на свое рабочее место. Если не предпринять каких-либо специальных мер, все это время никто не сможет отредактировать заблокированную запись. Чтобы избежать подобного, зачастую используют таймер: если клавиатура и мышь дли- тельное время остаются в бездействии, программа автоматически разблокирует запись. Еще одна проблема, связанная с пессимистическим блокированием, заключа- ется в том, что для пессимистического блокирования требуется курсор, работаю- щий на стороне сервера. Ранее мы уже говорили о том, что местоположение курсо- ра влияет на типы доступных курсоров. Сейчас мы видим, что местоположение курсора влияет также на способы блокирования. Позднее в данной главе мы под- робнее обсудим преимущества использования курсоров, работающих на стороне клиента. Если вы примете решение воспользоваться этими преимуществами, зна- чит, вы не сможете воспользоваться пессимистической блокировкой. Обновление данных Поддержка обновляемых соединений (updatable joins) является одной из основ- ных причин, по которым программисты используют компонент ClientDataSet (или кэшируемые обновления в BDE). Запрос, подразумевающий соединение двух таб- лиц, возвращает пользователю единую таблицу, и пользователь хочет обладать возможностью модифицировать записи этой таблицы. Рассмотрим следующее SQL-выражение: SELECT * FROM Products. Suppliers WHERE Suppliers SupNo=Products SupNo Этот запрос возвращает список продуктов с указанием поставщиков, которые выполняют поставку этих продуктов. Механизм BDE рассматривает любое SQL- соединение так таблицу, предназначенную только для чтения. Дело в том, что до- бавление, обновление и удаление записей в объединенной таблице неоднозначно. Например, если пользователь добавляет в объединенную таблицу новую запись, 0727
728 Глава 15. Технология ADQ надо ли добавлять в таблицу нового поставщика и новый продукт или можно огра- ничиться только добавлением продукта? Архитектура ClientDataSet/Provider по- зволяет вам указать первичную обновляемую таблицу (в этой книге об этом не рассказывается) и выполнить дополнительную настройку SQL-обновлений. Об этом частично рассказано в главе 14, а кроме того, я расскажу об этом в главе 16. Технология ADO поддерживает механизм кэширования обновлений, который называется пакетными обновлениями (batch updates) и функционирует приблизи- тельно так же, как аналогичный механизм BDE. В следующем разделе я более под- робно рассмотрю механизм пакетных обновлений ADO. Однако для того, чтобы решить проблему обновления SQL-соединений вы можете обойтись и без помощи этого механизма. Дело в том, что ADO поддерживает обновление SQL-соедине- ний. В программу JoinData добавлен компонент ADODataset, основанный на приве- денном ранее SQL-выражении. Если вы запустите программу, вы сможете отре- дактировать одно из полей и сохранить изменения в базе (для этого достаточно переместиться на другую запись). Никаких ошибок не возникнет, так как ADO успешно выполнит обновление БД. Дело в том, что в отличие от BDE в ADO ис- пользуется более практический подход. В рамках ADO, когда выполняется соеди- нение нескольких таблиц, каждое поле знает, к какой таблице оно принадлежит. Если вы обновляете поле в таблице Products и публикуете изменения в базе, для обновления формируется SQL-выражение UPDATE, которое обновляет значение поля в таблице Products базы данных. Если помимо поля таблицы Products вы изме- няете также поле таблицы Suppliers, значит, генерируются два SQL-выражения UPDATE — по одному для каждой таблицы. При добавлении новой строки в SQL-соединение механизм ADO ведет себя подобным же образом. Если вы вставляете строку и добавляете значения только для полей таблицы Products, значит, генерируется только одно SQL-выражение INSERT, которое добавляет новую запись в таблицу Products. Если вы вводите зна- чения для полей обеих таблиц, генерируется два SQL-выражения INSERT — по одному для каждой таблицы. Важен порядок, в котором выполняются эти выраже- ния, так как новый продукт может ссылаться на нового поставщика, поэтому инфор- мация о поставщике должна добавляться в таблицу Suppliers в первую очередь. Серьезная проблема возникает, если выполняется удаление строки из объеди- ненной таблицы. При попытке выполнить удаление строки объединенной табли- цы вы увидите сообщение об ошибке. Конкретный текст сообщения зависит от версии ADO, а также от используемой базы данных. Это сообщение может сбить вас с толку, так как, скорее всего, оно не будет иметь отношения к истинной при- чине проблемы. Проблема связана с тем, что невозможно удалить запись, на кото- рую ссылаются другие записи. В нашем примере вы, скорее всего, увидите сообще- ние о том, что невозможно удалить запись о продукте, потому что существуют другие записи, ссылающиеся на эту запись. Однако если вы проведете пару экспе- риментов, вы обнаружите, что ошибка возникает вне зависимости от того, суше" ствуют ли в базе другие записи, ссылающиеся на удаляемый продукт, или таких записей нет. Чтобы понять причину проблемы, необходимо воспользоваться та- ким же подходом, какой используется при добавлении новых записей в объедИ" ненную таблицу. В случае удаления строки объединенной таблицы механизм ADU генерирует два SQL-выражения DELETE: одно для таблицы Suppliers, а второе — ДлЯ 0728
Обновление данных 729 таблицы Products. Выражение DELETE для таблицы Products выполняется успешно, а вот выражение DELETE для таблицы Suppliers дает сбой — поставщик продуктов не может быть удален из таблицы, так как с ним, как правило, связано несколько ссы- лающихся на него записей о продуктах. СОВЕТ-------------------------------------------------------------------- Если вы хотите посмотреть, какие именно SQL-выражения генерируются в результате выполнения той или иной команды ADO, имейте в виду, что, используя SQL Server, вы можете просмотреть эти выражения при помощи инструмента SQL Server Profiler. Даже если вы понимаете, как именно работает этот процесс, полезно взглянуть на проблему глазами пользователя. Я могу поспорить, что когда пользователь уда- ляет запись из объединенной таблицы, в 99 процентах случаев он намерен удалить только запись о продукте, оставив запись о поставщике без изменений. К счастью, вы можете добиться такого результата, если воспользуетесь специальным дина- мическим свойством Unique Table. С его помощью вы можете указать, что удаление строки имеет отношение только к таблице Products, но не к таблице Suppliers. Для этого используется следующий код: ADOQueryl Properties[ 'Unique Table'] Value = 'Products' Присвоить значение этому свойству на этапе проектирования нельзя, поэтому данное выражение следует разместить в обработчике события OnCreate. Пакетные обновления (Batch updates) При использовании механизма пакетных обновлений любые изменения, вноси- мые пользователем, накапливаются в локальной памяти. Позже полный пакет этих изменений за одну операцию может быть внесен в базу данных. Очевидно, что по- добный подход обеспечивает выигрыш в производительности, однако существуют и другие преимущества, делающие его удобным. В частности, при использовании механизма пакетных обновлений пользователь может выполнять изменения даже тогда, когда он отключен от базы данных. Эта возможность может употребляться для обеспечения работы пользователя в автономном режиме, при применении тех- нологий наподобие Briefcase (Портфель), а также в веб-приложениях, основанных еще на одном механизме ADO, который называется RDS (Remote Data Services). Вы можете включить пакетные обновления для любого набора данных ADO. Для этого необходимо присвоить свойству LockType значение LtBatchOptimistic пе- ред тем, как набор данных будет открыт. Кроме того, вы должны присвоить свой- ству CursorLocation значение clUseClient, так как пакетные обновления обрабатыва- ются механизмом ADO Cursor Engine. В результате любые изменения, вносимые пользователем в набор данных, будут сохраняться в области delta (в этой области хранится список изменений). Набор данных будет выглядеть так, как будто дан- ные были изменены, однако на самом деле информация об изменениях хранится в памяти — эти изменения не внесены в базу данных. Чтобы опубликовать измене- ния в БД (перенести их из памяти в базу данных), необходимо обратиться к мето- Ду Apply Batch (этот метод эквивалентен методу ApplyUpdates механизма BDE): ADODataSetl UpdateBatch Если вы хотите отменить все накопленное в памяти, воспользуйтесь методом CancelBatch — это аналог метода CancelUpdates. В рамках механизмов пакетных об- 0729
730 Глава 15. Технология ADQ новлений ADO, кэшируемых обновлений BDE и механизма кэширования Client- DataSet используется много других методов и свойств со схожими именами. На- пример, как и в BDE, свойство UpdateStatus набора данных ADO может исполь- зоваться для того, чтобы узнать состояние некоторой записи: была ли запись добавлена, модифицирована, удалена или она не подвергалась каким-либо изме- нениям. Это свойство весьма удобно, если вы хотите выделить модифицирован- ные записи цветом или отобразить их состояние в строке состояния. Существуют некоторые различия в синтаксисе, например вместо вызова RevertRecord в ADO ис- пользуется вызов CancelBatch(arCurrent);. Одна весьма полезная возможность механизма кэшированных обновлений BDE отсутствует в ADO: механизм слежения за тем, существуют ли неопубликованные в БД обновления. В BDE для этой цели используется свойство UpdatesPending. Это свойство содержит в себе значение True в случае, если в набор данных были внесе- ны изменения, которые еще не опубликованы в базе данных. Это свойство удобно использовать в обработчике события OnCloseQuery: procedure TForml FormCloseQueryC Sender TObject. var CanClose Boolean) begin CanClose = True if ADODataSetl UpdatesPending then canClose = (MessageDlgl'Существуют изменения, которые еще не опубликованы в БД #13 + 'Завершить приложение в любом случае7' mtConfirmation [mbYes mbNo] 0) = mrYes) end Однако, обладая необходимыми знаниями и изобретательностью, вы можете написать свою собственную функцию ADOUpdatesPending. Чтобы написать такую функцию, вы должны знать, что наборы данных ADO поддерживают свойство FilterGroup, которое функционирует примерно так же, как фильтр. В отличие от свой- ства Filter стандартного набора данных, которое фильтрует данные, исходя из не- которого условия, свойство FilterGroup может выполнять фильтрацию, исходя из состояния записи. Существует несколько состояний фильтрации, и одно из них соответствует значению fgPendingRecords. Это состояние соответствует записям, которые были модифицированы, но информация об изменении которых еще не была опубликована в базе данных Таким образом, чтобы взглянуть на все измене- ния, которые были сделаны, но не опубликованы, достаточно выполнить две стро- ки кода: ADDDataSetl FilterGroup = fgPendingRecords ADODataSetl Filtered = True. Естественно, после выполнения этих команд в наборе данных будут присут- ствовать записи, которые были удалены, при этом поля этих записей будут пусты- ми, — это не очень удобно, так как вы не сможете понять, какая именно запись была удалена. (Первая версия ADOExpress функционировала иначе: для удален- ных записей отображались все значения полей.) Чтобы решить проблему UpdatesPending, вам потребуется использовать клони- рование наборов данных (об этой возможности рассказывалось ранее). Функция ADOUpdatesPending настраивает свойство FilterGroup таким образом, чтобы в наборе данных присутствовала только информация о сделанных, но не опубликованных изменениях. Теперь надо проверить, присутствует ли в наборе данных хотя бы одна запись. Если хотя бы одна запись присутствует, значит, некоторые изменения еше 0730
Обновление данных 731 не опубликованы в базе данных. Если после фильтрации набор данных оказался пустым, значит, все сделанные ранее изменения уже опубликованы в БД. Однако если вы попытаетесь проверить количество записей в реальном наборе данных, перенастройка свойства FilterGroup приведет к смещению указателя на текущую запись — пользовательский интерфейс немедленно отреагирует на это. Во избежа- нии этого, воспользуйтесь клоном набора данных: function ADOUpdatePending(ADODataSet TCustomADODataSet) boolean var Clone TADOOataSet begin Clone = TADODataSet Create(ml). try Clone Clone(ADOOataSet). Clone FilterGroup = fgPendingRecords Clone Filtered = True Result = not (Clone BOF and Clone EOF). Clone Close finally Clone Free end end В этой функции вы клонируете изначальный набор данных, настраиваете свой- ство FilterGroup, а затем проверяете, находится ли указатель одновременно в начале и в конце набора данных. Если это так, значит, набор данных пуст. Оптимистическая блокировка Ранее мы с вами рассмотрели использование свойства LockType и обсудили прин- цип функционирования пессимистической блокировки. В данном разделе мы рас- смотрим оптимистическую блокировку. Оптимистическая блокировка не только яв- ляется предпочтительной для высокопроизводительных систем, помимо этого именно такой тип блокировки используется при выполнении пакетных обновлений. Оптимистическая блокировка предполагает, что возникновение конфликта между двумя пользователями, пытающимися отредактировать одну и ту же запись одновременно, маловероятно. Отсюда следует, что любому пользователю разре- шается редактировать любую из записей в любое время. Последствия конфликтов обрабатываются в момент сохранения изменений в базе данных. Таким образом, конфликты рассматриваются как исключение из правила. Если два пользователя попытаются сохранить изменения одной и той же записи, выполнить это удастся только первому пользователю, второму пользователю будет отказано. Подобное поведение реализуется в приложениях, работающих в рамках модели Briefcase (Портфель), а также в веб-приложениях, в которых отсутствует постоянное со- единение с базой данных и поэтому невозможно реализовать пессимистическую блокировку. В отличие от пессимистической, оптимистическая блокировка не тре- бует длительного блокирования ресурсов- запись блокируется только в момент обновления. Таким образом, в среднем потребление ресурсов ниже, а база данных более масштабируема Давайте рассмотрим пример Представьте, что у вас есть компонент ADO- DataSet, соединенный с таблицей Customer из примера dbdemos.mdb, при этом свой- 0731
732 Глава 15. Технология Арр ство LockType обладает значением LtBatchOptimistic, а содержимое набора данных отображается в сетке DBGrid. Предположим также, что у вас есть кнопка, при щел- чке на которой происходит обращение к методу UpdateBatch. Запустите две копии этой программы (это программа Batchllpdates). Начните редактирование записи в первой копии программы. Для простоты в процессе демонстрации я использую один компьютер, однако все то же самое произойдет и в случае, если редактирова- ние одной и той же записи будет выполняться на двух компьютерах. 1. Выберите компанию Bottom-Dollar Markets в Канаде и измените ее имя на Bottom-Franc Markets. 2. Сохраните изменения в базе, для этого просто перейдите к другой записи и щел- кните на кнопке пакетного обновления. 3. Во второй копии программы найдите ту же самую запись и измените имя ком- пании на Bottom-Pound Markets. 4. Перейдите к другой записи и щелкните на кнопке пакетного обновления. Об- новление не сработает. Как и в случае с другими сообщениями об ошибках ADO, текст сообщения бу- дет зависеть не только от версии ADO, но также от того, насколько точно вы по- вторили описанную последовательность действий. В ADO 2.6 на экране появится следующее сообщение об ошибке: Row cannot be located for updating. Some values may have been changed since it was last read (He удается обнаружить строку для обновле- ния. Некоторые значения строки могли быть изменены с момента последнего чте- ния). В этом заключается вся суть оптимистической блокировки. Для обновления записи выполняется следующее SQL-выражение: UPDATE CUSTOMER SET CompanyName="Bottom-Pound Markets" WHERE CustomerID="BOTTM" AND CompanyName="Bottom-Dollar Markets" Выражение идентифицирует запись, используя первичный ключ и значение поля CompanyName, какими они были в момент чтения записи из базы в память. Ожидается, что в результате выполнения этого выражения будет модифицирова- на одна запись таблицы. Однако в рассмотренном нами примере в результате вы- полнения выражения ни одна из записей не модифицируется. Такой исход возмо- жен, только если запись была удалена, изменился первичный ключ или изменяемое поле было изменено кем-то до вас. В любом из этих случаев обновление окончится неудачей. Если бы второй пользователь попытался изменить значение поля ContactName (но не CompanyName), тогда выражение обновления выглядело бы следующим об- разом: UPDATE CUSTOMER SET ContactName="Liz Lincoln" WHERE CustomerID="BOTTM" AND ContactName="El izabeth Lincoln" В нашем случае обновление было бы выполнено успешно, так как другой пользо- ватель не изменит ни первичный ключ, ни контактное имя. Это поведение напо- минает режим Update Where Changed механизма BDE. Вместо свойства UpdateMode, используемого в BDE, в рамках ADO используется динамическое свойство Update Criteria набора данных. В следующем списке перечислены допустимые значения этого динамического свойства. 0732
Обновление данных 733 Константа Способ идентификации записей adCriteriaKey Только столбцы, входящие в состав первичного ключа adCriteriaAIICols adCriteriaUpdCols Все столбцы Только столбцы первичного ключа и модифицированные столбцы adCriteriaTimeStamp Только столбцы первичного ключа и столбец отметки времени Не следует думать, что один из этих режимов будет предпочтительным для все- го вашего приложения. На практике выбор режима определяется тем, информа- ция какого характера содержится в таблице. Предположим, в таблице Customer при- сутствуют только три поля: CustomerlD (идентификатор), Name (имя) и City (город). Изменение любого из этих полей никак не влияет на значения других полей таб- лицы, поэтому в подобной ситуации вполне можно использовать режим adCrite- riaUpdCols (этот режим используется по умолчанию). Однако если помимо пере- численных полей в таблицу входит еще поле PostaLCode (почтовый индекс), тогда обновление этого поля должно быть в обязательном порядке согласовано с обнов- лением поля City (город). Иными словами, нельзя допустить, чтобы один пользо- ватель модифицировал поле PostaLCode и в то же самое время другой пользователь модифицировал поле City без всякого согласования с первым пользователем. В этом случае более безопасным будет режим обновления adCriteriaALLCoLs. Следует также рассказать о том, как ADO осуществляет обработку ошибок в процессе обновления нескольких записей. В BDE и ClientDataSet для этой цели вы можете воспользоваться событием OnUpdateError, которое позволит вам отреа- гировать на ошибку, связанную с обновлением записи, и разрешить проблему пе- ред тем, как перейти к следующей записи. В ADO такой возможности нет. Вы мо- жете следить за прогрессом, успехом или неудачей пакетного обновления при помощи событий OnWiLLChangeRecord и OnRecordChangeCompLete, однако вы не може- те изменить содержимое обновляемой записи и попытаться заново внести ее в базу, как это возможно в рамках BDE и ClientDataSet. Проблема состоит также в том, что если ошибка возникает в ходе пакетного обновления, процедура обновления не останавливается, а продолжает выполняться. Пакетное обновление выполняется До самого конца, то есть до тех пор, пока все изменения не будут внесены в базу (при этом могут возникнуть другие ошибки). В результате вы можете получить сбивающее с толку или неправильное сообщение об ошибке. Если в ходе пакетно- го обновления возникло несколько ошибок (не удалось обновить несколько запи- сей), ADO 2.6 отобразит на экране следующее сообщение: Multistep OLE DB operation generated errors. Check each OLE DB status value, if available. No work was done (В ходе выполнения многошаговой операции OLE DB возникли ошибки. Если возможно, проверьте каждое из состояний OLE DB. Никакой работы не было сделано). Про- блема заключается в последнем предложении: No work was done. ADO сообщает нам, что ничего не было сделано, однако это не так. Запись, ставшая причиной ошибки, Действительно не была обновлена, однако все остальные записи успешно опубли- кованы в базе данных. 0733
734 Глава 15. Технология ADO Разрешение конфликтов, связанных с обновлением данных В процессе обновления разумным будет следующий подход: пользователь вносит в набор данных необходимые изменения, затем выполняется пакетное обновле- ние, при обновлении некоторых записей возникают ошибки, когда процесс обнов- ления полностью завершается, пользователю выдается перечень проблемных за- писей и предоставляется возможность разрешить каждый из конфликтов. Чтобы получить перечень всех проблемных записей, можно воспользоваться значением fgConflictingRecords свойства FilterGroup: ADODataSetl.FilterGroup := fgConflictingRecords: ADODataSetl.Filtered .— True: Для каждого поля каждой проблемной записи можно сообщить пользователю три ключевых значения: значение поля, которое содержалось в записи в момент, когда эта запись была впервые прочитана из БД; значение поля, которое было при- своено полю этим пользователем; значение, которое содержится в БД на текущий момент (то есть во время попытки обновления). Для этого служат следующие свой- ства TfieLd. Свойство Описание NewValue Значение, занесенное в поле пользователем CurValue Значение, которое обнаружено в базе в момент обновления OldValue Значение, которое содержалось в базе в момент первого чтения записи Те, кто работает с компонентом ClienDataSet, знают о существовании удобного диалогового окна ReconcileErrorForm. Это диалоговое окно формируется автомати- чески, оно показывает пользователю старые, новые и текущие значения полей про- блемной записи и позволяет пользователю выбрать метод решения конфликта. К сожалению, в ADO аналог этого диалогового окна отсутствует. Класс TRecon- cileErrorForm был разработан специально для компонента ClientDataSet, поэтому его очень сложно адаптировать для использования с наборами данных ADO. Следует также сказать о порядке функционирования упомянутых свойств клас- са TField. Эти свойства основаны на объектах ADO Fields, на которые они ссылаются. Это означает, что порядок функционирования этих свойств целиком и полностью определяется провайдером OLE DB, который вы используете. Остается надеять- ся, что используемый вами провайдер корректно поддерживает возможности, в ко- торых вы нуждаетесь. Большинство провайдеров хорошо справляются с этой за- дачей, однако провайдер Jet OLE DB возвращает одно и то же значение для свойств CurValue и OldValue. Говоря точнее, в качестве текущего значения поля возвращает- ся значение, которое содержалось в поле в момент первого чтения записи из базы данных. Иными словами, Jet не позволяет определить значение, присвоенное полю другим пользователем (если только вы сами не предпримете каких-либо дополни- тельных действий, чтобы осуществить это). Если вы используете провайдер SQr Server OLEDB, вы сможете обратиться к свойству CurValue только после выполне- ния метода Resync набора данных, при этом параметр AffectRecords должен быть 0734
Отключенные наборы записей 735 равен значению adAffectGroup, а параметр ResyncValues — содержать значение adRe- syncUnderlyingValues. Вот соответствующий код: adoCustomers.FilterGroup := fgConf1ictingRecords: adoCustomers.Filtered := true; adoCustomers. Recordset. Resync(adAffectGroup. adResynclJnderly 1 ngVa 1 ues): Отключенные наборы записей Теперь, когда вы знаете о пакетных обновлениях, мы можем приступить к изуче- нию еще одной возможности ADO: отключенных наборах записей. Отключенный набор записей (disconnected recordset) — это набор записей, который отключен от подключения к базе данных. Пользователь может работать с таким набором запи- сей в точности так же, как он работает с обычным, подключенным набором записей. Это весьма впечатляющая возможность: между подключенным и отключенным наборами записей фактически не существует различий. Свойства и возможности фактически идентичны. Чтобы отключить набор записей от подключения, необ- ходимо присвоить свойству CursorLocation значение cLUseCLient, а свойству LockType — значение LtBatchOptimistic. После этого вы присваиваете свойству Connection значе- ние nil, в результате набор записей становится отключенным: ADODataSetl.Connection := nil: После этого набор записей продолжает хранить в себе те же данные и поддер- живать те же возможности навигации. Он позволяет добавлять, редактировать и удалять записи. Единственное отличие состоит в том, что вы не можете выпол- нить пакетное обновление, так как для этого вам потребуется подключиться к сер- веру БД. Чтобы восстановить соединение (и воспользоваться методом UpdateBatch), необходимо вновь настроить свойство Connection: ADODataSetl.Connection := ADOConnectionl: Эта возможность также поддерживается в BDE и в других технологиях работы с данными, однако для этого вы должны переключиться на использование Client- DataSet. Прелесть ADO состоит в том, что вы с самого начала можете разработать приложение так, будто бы вы понятия не имеете о возможности отключения набо- ра записей от БД. Вы используете обычные компоненты dbGo самым обычным образом. После этого вы можете без лишних сложностей добавить в вашу программу возможность отключения от БД так, как будто вы только что узнали о существова- нии этой возможности. При этом вам не потребуется менять основной код вашего приложения. Отключение набора записей может потребоваться для того, чтобы: ° минимизировать общее количество параллельных подключений к БД; ° обеспечить работу приложения в автономном режиме (в соответствии с моде- лью Briefcase). О приложениях, работающих в рамках модели Briefcase (Портфель), мы пого- ворим в одном из следующих разделов. Большинство бизнес-приложений, работающих в рамках концепции «клиент- сервер», открывают таблицы базы данных и поддерживают постоянное подключе- 0735
736 Глава 15. Технология ADp ние к БД в течение всего времени, пока таблица находится в открытом состоянии. Однако на самом деле подключение необходимо только во время выполнения двух операций: извлечение данных из БД и обновление данных в БД. Представьте, что вы модернизируете вашу программу таким образом, чтобы она выполняла отклю- чение от базы данных сразу же после того, как происходит открытие таблицы и из- влечение данных. Как только необходимые данные передаются на клиентский ком- пьютер, соединение разрывается. Вот соответствующий код: ADODataSetl.Connect!on :=nil: ADOConnectionl.Connected := False; После этого соединение может потребоваться только в случае, когда пользова- тель желает выполнить пакетное обновление. Код выполнения обновления выгля- дит следующим образом: ADOConnectionl.Connected : = True; ADODataSetl.Connect!on := ADOConnectionl; try ADODataSetl.UpdateBatch; finail у ADODataSetl.Connection : = nil; ADOConnectionl.Connected : = False; end; Если вы будете использовать подобный подход в вашем приложении, среднее количество соединений с БД, открытых в одно и то же время, будет минималь- ным — соединения будут открываться только на короткое время, когда они дей- ствительно нужны. Очевидно, что подобную клиент-серверную структуру суще- ственно проще масштабировать. Сервер сможет обслуживать значительно большее количество клиентов. Существует также недостаток: формирование соединения с некоторыми (но не со всеми) БД требует значительного времени, в подобной си- туации пакетное обновление будет выполняться медленнее. Накопление соединений (Connection Pooling) Разговоры о разрыве и повторном открытии соединений наводят нас на мысль, нельзя ли использовать соединения повторно? Этот механизм называется Con- nection Pooling. При его использовании, когда приложение завершает работу с под- ключением, подключение не уничтожается, вместо этого оно откладывается в об- ласть накопления (пул) и затем может использоваться повторно. Этот процесс выполняется автоматически, конечно, при условии, что используемый вами про- вайдер OLE DB поддерживает его и этот механизм включен. В этом случае ника- ких дополнительных действий предпринимать не надо. Производительность является основной причиной применения подобного ме- ханизма. Чтобы установить соединение с базой данных, зачастую требуется время. Если вы имеете дело с настольной базой данных, такой как Access, соединение ус- танавливается фактически мгновенно. Однако в клиент-серверной среде, напри- мер при использовании сервера Oracle, доступ к которому осуществляется через сеть, для формирования соединения может потребоваться несколько секунД- Та- ким образом, имеет смысл подумать о повторном использовании такого ценного ресурса, как соединение с БД. 0736
Отключенные наборы записей 737 При использовании механизма накопления ADO каждый раз, когда приложе- ние «уничтожает» объект ADOConnection, этот объект добавляется в специальный пул (место временного хранения). Если в дальнейшем возникает необходимость создать новое соединение, система автоматически выполняет поиск подходящего соединения в пуле. Если обнаруживается существующее соединение, обладающее строкой подключения, совпадающей с той, в которой нуждается пользователь, это соединение используется повторно. Если подходящего соединения не обнаружи- вается, происходит его создание. Соединения остаются в пуле до тех пор, пока либо они не будут востребованы, либо приложение не завершит работу, либо не истечет время тайм-аута. По умолчанию продолжительность тайм-аута составляет 60 секунд, однако начиная с версии MDAC 2.5 это значение можно изменить. Для этого необ- ходимо указать продолжительность тайм-аута в ключе реестра HKEY_CLASSES_ROOT\ CLSID\<CLSID-npoBaiiflepa>\SPTimeout. Процесс накопления и повторного использо- вания подключений выполняется в прозрачном режиме без каких-либо дополни- тельных действий со стороны разработчика. Аналогичный механизм накопления соединений функционирует в BDE при использовании MTS (Microsoft Transaction Server) и СОМ+, однако в отличие от BDE, механизм ADO выполняет накопле- ние соединений самостоятельно, без помощи MTS или СОМ+. По умолчанию механизм накопления соединений активизирован для всех про- вайдеров MDAC OLE DB реляционных баз данных (включая SQL Server и Oracle). Важным исключением является провайдер Jet OLE DB. Если вы используете ODBC, вы можете выбрать между накоплением соединений ODBC и накоплени- ем соединений ADO, однако не рекомендуется использовать оба этих механизма одновременно. Начиная с версии MDAC 2.1 по умолчанию накопление соедине- ний ADO включено, а накопление соединений ODBC отключено. ПРИМЕЧАНИЕ-------------------------------------------------------------—----- Вне зависимости от провайдера OLE DB накопление соединений не осуществляется в среде Windows 95. К сожалению, в составе ADO отсутствуют инструменты, позволяющие следить за содержимым пула соединений. Однако для этой цели вы можете использовать инструмент SQL Server Performance Monitor, который снабдит вас информацией о подключениях к базе данных SQL Server. Включить или отключить накопление соединений можно либо при помощи реестра, либо при помощи строки подключения. В реестре для этой цели исполь- зуется параметр OLEDB_SERVICES, который расположен в разделе HKEY_CLASSES_ROOT\ CLSID\<CLSID-npoBaMflepa>. В этом параметре хранится битовая маска, при помощи которой вы можете отключить некоторые службы OLE DB, в том числе накопле- ние соединений, журналирование транзакций и другие. Чтобы отключить накоп- ление соединений с использованием строки подключения, добавьте в конце стро- ки подключения последовательность символов ;OLE DB Services=-2. Чтобы включить Накопление соединений для провайдера Jet OLE DB, добавьте в конце строки под- ключения последовательность ;OLE DB Services=-1. В результате будут включены все службы OLE DB. 0737
738 Глава 15. Технология ADp Сохранение набора записей в постоянной памяти (Persistent Recordset) Возможность сохранения набора записей на жестком диске является важной со- ставной частью приложений Briefcase (Портфель). Используя эту возможность, вы можете сохранить содержимое набора записей в файл на локальном жестком диске. Позже вы можете загрузить данные из этого файла в набор записей. Помимо прочих преимуществ, эта возможность позволяет разработчикам создавать реаль- ные однозвенные приложения — вы можете установить приложение базы данных, но при этом не устанавливать саму базу данных. Благодаря этому для установки про- граммы требуется очень небольшое пространство на клиентском жестком диске. Чтобы сохранить набор данных на жестком диске, используется метод SaveToFile: ADODataSetl SaveToFile( 'Local ADTG"). В результате обращения к этому методу данные вместе с областью дельта со- храняются на локальном жестком диске. Чтобы загрузить данные из файла, мож- но воспользоваться методом LoadFromFile, который принимает единственный пара- метр — имя загружаемого файла. Формат файла называется ADTG (Advanced Data Table Gram). Этот формат является собственностью компании Microsoft. Это весьма эффективный формат. При желании вы можете сохранить файл в формате XML, для этого необходимо передать методу SaveToFile второй параметр: ADODataSetl.SaveToFi 1е('Local.XML'. pfXML), Следует иметь в виду, что (в отличие от ClientDataSet) ADO не содержит в себе собственной поддержки формата XML — для генерации XML-кода используется механизм MSXML. Следовательно, пользователь вашего приложения должен бу- дет установить Internet Explorer 5.0 или загрузить MSXML с веб-узла компании Microsoft. Если вы планируете сохранять данные локально в формате XML, помните о не- которых недостатках этого подхода: О сохранение и загрузка файлов XML выполняется медленнее, чем сохранение и загрузка файлов ADTG; О XML-файлы (создаваемые механизмом ADO, впрочем, как и любые другие XML-файлы) обладают значительным размером, их размер существенно пре- вышает размер ADTG-файла, содержащего такие же данные (как правило, раз- мер XML-файла в два раза превышает размер аналогичного ADTG-файла); О формат XML, генерируемый ADO, является специфическим форматом Micro- soft (это характерно для многих других компаний, поддерживающих собствен- ный порядок генерации XML), это означает, что XML-код, генерируемый ADO, не может быть прочитан компонентом ClientDataSet и наоборот (к счастью, эту проблему можно решить, воспользовавшись входящим в состав Delphi компо- нентом XML Transform, который позволяет выполнять преобразование между Р33' личными структурами XML). Если вы собираетесь использовать механизм сохранения набора записей толь- ко в рамках однозвенного приложения и ие предполагаете использовать модель Briefcase (Портфель), тогда вы можете воспользоваться компонентом ADODataSet, присвоить его свойству CommandType значение cmdFile, а свойству CommandText — 0738
Отключенные наборы записей 739 имя файла. Благодаря этому вы избавляетесь от необходимости обращаться к ме- тоду LoadFromFile вручную. Однако вам по-прежнему придется обратиться к мето- ду SaveToFile. В приложении Briefcase (Портфель) этот подход будет слишком ограничивающим, так как в таких приложениях набор данных используется в двух режимах. Модель Briefcase (Портфель) Теперь, когда вы знакомы с пакетными обновлениями, отключенными наборами записей и сохранением наборов записей в локальных файлах, вы можете присту- пить к изучению модели Briefcase (Портфель). Основная идея состоит в том, что- бы обеспечить пользователю возможность работать с вашим приложением даже тогда, когда он не имеет возможности подключиться к базе данных, то есть когда пользователь путешествует или находится в офисе у клиента компании. Проблема заключается в том, что в офисе у клиента пользователь не может подключиться к корпоративной сети вашей компании, а значит, не может подключиться к серве- ру базы данных. Отсюда следует, что данные не могут попасть на пользователь- ский портативный компьютер и быть обновлены. Чтобы обеспечить автономную работу пользователя, вам потребуются такие технологии, как отключенные наборы данных и сохранение наборов данных на локальном диске. Представьте себе, что вы разработали клиент-серверное прило- жение, которое обеспечивает доступ к серверу базы данных и полностью удовлет- воряет запросам пользователя. Теперь пользователь требует, чтобы вы обеспечи- ли работу этого приложения в автономном режиме, то есть даже тогда, когда нет возможности подключиться к серверу БД. Для этого вы должны добавить в при- ложение возможность сохранения данных на пользовательском локальном жест- ком диске. Иными словами, прежде чем пускаться в путешествие, пользователь должен отдать команду подготовки приложения к работе в автономном режиме. По этой команде каждая таблица сохраняется в локальном файле при помощи ме- тода SaveToFile. В результате на пользовательском жестком диске возникает кол- лекция файлов ATDG или XML, которые являются отражением содержимого базы Данных. После этого пользователь может отключить свой портативный компью- тер от сети и продолжить работу с привычным для него приложением в автоном- ном режиме. Приложение должно автоматически определять, работает ли оно автономно или существует возможность подключения к сети. Для того чтобы определить это, вы можете попытаться подключиться к базе данных и проверить, открылось ли со- единение. Также вы можете проверить присутствие локального файла портфе- ля или воспользоваться собственным специальным флагом. Если приложение ра- ботает в автономном режиме, вместо того чтобы подключаться к базе данных, оно Должно извлечь данные из локального файла при помощи метода LoadFromFile. Когда требуется опубликовать данные в базе, вместо метода UpdateBatches приложение Должно обращаться к методу SaveToFile для каждой из таблиц. Если возможна связь с базой данных через сеть, необходимо присвоить значение True свойству Connected компонента ADOConnection, а также свойству Active каждого из компонентов ADO- DataSet. Когда пользователь возвращается из путешествия, он должен внести ин- формацию о сделанных им изменениях в базу данных. Для этого необходимо 0739
740 Глава 15. Технология ado загрузить данные из локальных файлов, подключить наборы данных к базе дан- ных и обратиться к методу UpdateBatch. СОВЕТ---------------------------------------------------------------------------- Полная реализация модели Briefcase (Портфель) содержится в примере BatchUpdates, о котором уже рассказывалось ранее. Пара слов об ADO.NET ADO.NET — это часть новой архитектуры .NET, разработанной компанией Micro- soft. Архитектура .NET является попыткой компании Microsoft заново перепроек- тировать средства и инструменты разработки программного обеспечения для того, чтобы сделать их более удобными для создания веб-приложений. ADO.NET явля- ется новым развитием ADO, ориентированным на решение проблем, связанных с разработкой веб-систем и устраняющим многие недостатки устаревшей техно- логии ADO. Проблема ADO состоит в том, что эта технология основана на СОМ. Для одно- и двухзвенных приложений СОМ является вполне приемлемой плат- формой, однако в мире Веб использовать СОМ в качестве транспортного механизма фактически невозможно. Для СОМ характерны три основные проблемы, которые ограничивают использование этой технологии в Веб: во-первых, СОМ функцио- нирует только в среде Windows, во-вторых, передача наборов записей требует мар- шализации СОМ, в-третьих, вызовы СОМ не могут проникать через корпоратив- ные брандмауэры. Технология ADO.NET решает все три проблемы благодаря использованию XML. Еще одной особенностью ADO.NET является разделение традиционного набо- ра записей ADO на несколько отдельных классов. Вместо того чтобы решать мно- жество проблем, каждый из классов предназначается для решения одной конкрет- ной проблемы. Например, в ADO.NET присутствует класс DataSetReader, который обеспечивает доступ к данным только для чтения в режиме Forward-only (только вперед), при этом данные располагаются на стороне сервера. Благодаря всем этим ограничениям данный класс обеспечивает быстрое чтение результирующего на- бора данных. Класс DataTable функционирует как отключенный набор записей на стороне клиента. Класс DataRelation обладает общими чертами с провайдером MSDataShape OLE DB. В любом случае знание традиционной технологии ADO ока- жется чрезвычайно полезным при изучении новой технологии ADO.NET. Что далее? В данной главе мы рассмотрели технологию ADO (ActiveX Data Objects) и dbGo — набор компонентов Delphi, предназначенных для доступа к интерфейсам ADO. Вы узнали о том, как работать с MDAC (Microsoft Data Access Components) и различ- ными механизмами обращения к серверам баз данных. Я рассказал вам о преимУ' ществах и недостатках технологии ADO. 0740
Что далее? 741 В главе 16 речь пойдет о встроенной в Delphi архитектуре DataSnap, которая позволяет разрабатывать трехзвенные клиентские и серверные приложения. Эту задачу можно решить и при помощи ADO, однако данная книга в первую очередь посвящена Delphi, поэтому я расскажу вам о решении проблемы, которое являет- ся естественным для среды Delphi. После того как мы с вами завершим рассмотре- ние DataSnap, мы продолжим изучать встроенную в Delphi архитектуру для работы сданными. В главе 17 мы рассмотрим процесс разработки собственных компонен- тов, предназначенных для работы с данными. 0741
*1Съ Многозвенные Ю приложения DataSnap Мы с вами рассмотрели работу с локальными базами данных, а также затронули вопросы программирования клиент-серверных приложений, осуществляющих вза- имодействие с SQL-серверами. Однако некоторые крупные компании зачастую нуждаются в более сложной и, вместе с тем, еще более эффективной архитектуре, В течение нескольких последних лет компания Borland Software Corporation уде- ляет огромное внимание нуждам крупных предприятий. Чтобы подчеркнуть это, пару лет назад имя компании даже было заменено на Inprise1. Позже наименова- ние Borland было восстановлено, однако нацеленность на рынок крупных компа- ний сохранилась. Среда разработки Delphi поддерживает множество разнообразных технологий: трехзвенные архитектуры, основанные на Windows NT и DCOM, приложения, основанные на TCP/IP и сокетах, а также веб-службы, основанные на XML и SOAP. В данной главе мы подробно рассмотрим многозвенные архитектуры, предназна- ченные для доступа к базам данных. Решения, ориентированные на XML, будут рассматриваться нами в главах 22 и 23, которые посвящены XML, SOAP и веб- службам. Прежде чем продолжить, я должен подчеркнуть два важных обстоятельства. Во-первых, инструменты, обеспечивающие поддержку трехзвенных архитектур, присутствуют только в версии Delphi Enterprise. Во-вторых, в Delphi 7 вы не обя- заны платить за внедрение приложений DataSnap. Вы просто покупаете среду раз- работки и после этого можете устанавливать разработанное вами приложение на любом количестве серверов, не выплачивая компании Borland при этом деньги за каждый сервер. Это весьма серьезное изменение политики распространения при- ложений DataSnap. Ранее разработчики были вынуждены платить за установку приложения DataSnap на каждом из серверов. Изначально плата за каждый сер- вер была достаточно высокой, однако со временем она понижалась. И вот, нако- нец, в Delphi 7 необходимость платить за каждую серверную лицензию полностью отпала. Новая политика распространения DataSnap безусловно повысит интерес к данной технологии со стороны разработчиков. По этой причине я решил подрой‘ но рассмотреть данную технологию в одной из глав книги. В данной главе рассматриваются следующие темы: О логическая трехзвенная архитектура; Производное от английского слова enterprise — предприятие. — Ппимеи прпрп 0742
Одно, два и три звена в истории Delphi 743 О технические основы DataSnap; О протоколы подключения и пакеты данных; О вспомогательные компоненты Delphi (на стороне клиента и на стороне сервера); О брокер подключения и другие расширенные возможности. Одно, два и три звена в истории Delphi Изначально приложения баз данных для PC функционировали только на стороне клиента. Иначе говоря, как сама программа, так и файлы базы данных, с которыми работала эта программа, располагались на одном и том же компьютере — это был компьютер, с которым непосредственно работал пользователь. Затем некоторые пытливые программисты решили переместить файлы базы данных на сетевой фай- ловый сервер. Однако прикладная программа, осуществляющая работу с этими файлами, а также весь исполняемый код, обеспечивающий доступ к данным, как и раньше, располагались на клиентском компьютере, то есть на компьютере пользо- вателя. Вместе с тем, в результате перемещения базы данных на файловый сервер появилась возможность обеспечить доступ к одним и тем же данным для несколь- ких пользователей. Подобную конфигурацию по-прежнему можно реализовать в Delphi с использованием базы данных Paradox, однако в настоящее время такой подход все менее популярен, несмотря на то что буквально несколько лет назад он использовался фактически повсюду. Следующий крупный этап — это формирование клиент-серверной архитекту- ры. Поддержка этой архитектуры присутствует в Delphi с самых первых версий этой среды разработки. В клиент-серверной среде на стороне сервера помещаются не только файлы базы данных, но и программное обеспечение, обеспечивающее доступ к этим файлам. На стороне клиента располагается пользовательский ин- терфейс и средства обращения к серверному механизму доступа к данным. В кли- ент-серверной среде клиент играет менее важную роль, благодаря этому снижает- ся нагрузка на клиентский компьютер. На стороне сервера выполняется большая часть работы, связанная с обработкой данных. В этой ситуации мощный сервер может обеспечить обслуживание для множества менее мощных клиентов. Естественно, существует множество других преимуществ клиент-серверной модели доступа к данным. Например, если данное хранятся и обрабатываются на Центральном сервере, значит, упрощается их резервное копирование, слежение за их целостностью, обеспечение безопасности, централизованное управление огра- ничениями и т. п. Сервер базы данных часто называют SQL-сервером, так как SQL — это наиболее распространенный в настоящее время язык запросов к данным. Сер- вер БД часто называют также RDBMS (Relational Database Management System — система управления реляционной базой данных), подразумевая, что сервер обес- печивает средства для управления данными, такие как утилиты резервного копи- рования и механизмы репликации. Конечно же, далеко не всегда возникает необходимость использования полно- ценной системы RDBMS — во многих случаях достаточно использовать традици- онный однозвенный подход (когда база данных располагается на клиентском ком- 0743
744 Глава 16. Многозвенные приложения DataSnap пьютере). Кроме того, вы можете организовать клиент-серверную среду, исполь- зуя для этой цели всего один изолированный компьютер. Имеется в виду установ- ка системы RDBMS локально на клиентском компьютере. Для этой цели вполне подходит SQL-сервер InterBase. В этом случае ваше приложение будет разделено на два отдельных логических звена: клиентская часть и серверная часть, однако оба этих звена физически будут располагаться на одном компьютере. Вместе с тем, вы сможете работать с серверной частью вашего приложения как с полноценным SQL-сервером. Итак, традиционная клиент-серверная среда подразумевает формирование двух звеньев: пользовательский интерфейс, действующий на стороне клиента, и систе- ма доступа к данным, работающая на стороне сервера. Однако в современных ин- формационных системах все больший акцент делается не на простом доступе к дан- ным, а на специальных процедурах обработки этих данных. Иными словами, пользователь хочет не только получить данные из БД, но и определенным образом обработать их. Порядок обработки данных может быть разным для разных пред- приятий. Алгоритмы обработки данных часто называют бизнес-правилами (business- rules), или бизнес-логикой. Итак, по мере усложнения информационных систем на общей картине появляется третье звено: механизм, осуществляющий обработ- ку данных. Этот механизм функционирует отдельно от пользовательского интер- фейса и от системы, обеспечивающей доступ к данным. Часто говорят о логической трехзвенной архитектуре. Прилагательное «логическая» в данном случае указы- вает на то, что в системе по-прежнему используются только два компьютера (то есть два физических звена), однако приложение теперь делится на три отдельных логических звена: интерфейс, бизнес-логика и система доступа к данным. В Delphi 2 поддержка логической трехзвенной архитектуры появилась в виде модулей данных (data modules). Сейчас вы уже знаете, что модуль данных — это невизуальный контейнер, в котором можно разместить компоненты доступа к дан- ным (а также, при желании, любые другие невизуальные компоненты), кроме того, в модуле данных часто размещают обработчики событий, связанных с базами дан- ных. С одним и тем же модулем данных могут работать несколько разных форм, иными словами, вы можете сформировать несколько разных пользовательских интерфейсов для работы с одними и теми же данными. Вы можете создать одну или несколько форм для ввода данных, для формирования отчетов, для отображе- ния информации в формате «основное/подробности» (master/detail), а также для отображения разнообразных диаграмм и графиков. Логическая трехзвенная архитектура решает множество проблем, однако ей присущи также несколько недостатков. Во-первых, раздел программы, реализую- щий бизнес-логику, должен быть скопирован на каждый из клиентских компью- теров. Этот код создает дополнительную нагрузку на клиентские компьютеры, но хуже всего то, что возрастают расходы, связанные с сопровождением клиентского программного кода. Во-вторых, если несколько клиентов модифицируют одни и те же данные, не существует простого способа обработки возникающих конфликтов обновления БД. Наконец, при использовании логической трехзвенной архитекту- ры Delphi вам придется установить и настроить программное обеспечение обра- щения к серверу БД, а также необходимые библиотеки SQL-сервера на каждом из КТТИРИТГКИУ КПМПКЮТРППВ 0744
Одно, два и три звена в истории Delphi 745 Чтобы решить эти проблемы, можно переместить код, осуществляющий обра- ботку данных, на отдельный компьютер, и спроектировать клиентские приложе- ния таким образом, чтобы они взаимодействовали с этим компьютером. Именно для решения этой задачи в Delphi 3 была добавлена поддержка удаленных моду- лей данных (Remote Data Modules). Удаленные модули данных могут работать на отдельном компьютере, который называют сервером приложений (application server). Сервер приложений в свою очередь связывается с сервером DBMS (систе- ма управления базой данных может быть установлена непосредственно на сервере приложений или на отдельном компьютере). Таким образом, клиентские компью- теры взаимодействуют с базой данных не напрямую, а через сервер приложений. Здесь у нас возникает фундаментальный вопрос: надо ли устанавливать на кли- ентском компьютере программное обеспечение доступа к базе данных? В рамках традиционной клиент-серверной архитектуры (даже с тремя логическими звенья- ми) вы должны установить программное обеспечение доступа к базе данных на каждом из клиентских компьютеров. Это может оказаться непростым делом, осо- бенно если в вашем распоряжении несколько сотен клиентских компьютеров. В рамках физической трехзвенной архитектуры вы должны установить программ- ное обеспечение доступа к базе данных только на сервере приложений — устанав- ливать этот код на клиентских компьютерах не надо. Клиентская часть приложе- ния теперь включает в себя только код пользовательского интерфейса — этот код чрезвычайно просто установить, он фактически не требует администрирования и создает минимальную нагрузку на процессор. Такое программное обеспечения называют тонким клиентом (thin client). Если говорить на языке маркетинга, та- кой клиент называется тонким клиентом с нулевым администрированием (zero- configuration thin-client). Однако забудем о маркетинговых терминах и сосредото- чимся на технических вопросах. Технические основы DataSnap Когда компания Borland впервые добавила в Delphi поддержку физической трех- звенной архитектуры, соответствующая технология получила наименование MIDAS (Middle-tier Distributed Application Services — распределенные прикладные служ- бы среднего звена). В частности, в составе Delphi 5 присутствовала третья версия этой технологии, MIDAS 3. Позднее технология была переименована в DataSnap, ее возможности были расширены. Чтобы воспользоваться DataSnap, на сервере (на компьютере, играющем роль среднего звена) необходимо установить специальные библиотеки, которые обес- печивают извлечение данных из SQL-сервера, базы данных или любых других источников данных. При использовании DataSnap данные вовсе не обязательно хранить на SQL-сервере. DataSnap позволяет извлекать данные из множества раз- нообразных источников, включая SQL, другие серверы DataSnap, а также данные, вычисляемые на лету. Как можно предположить, клиентская часть DataSnap чрезвычайно легковес- на, ее легко установить и настроить. Все, что необходимо на стороне клиента, — это единственный небольшой файл Midas.dll — небольшая динамическая библиотека, в рамках которой реализуются компоненты ClientDataSet и RemoteServer и которая обеспечивает подключение к серверу приложений. Вместо того чтобы включать 0745
746 Глава 16. Многозвенные приложения DataSnap эту библиотеку в комплект поставки вашего приложения, вы можете статически ском- поновать ее, то есть добавить ее код в состав исполняемого файла вашего приложе- ния. Для этого необходимо добавить модуль Midas Lib в выражение uses исходного кода вашей программы. Об этом уже рассказывалось в одном из разделов главы 13. Интерфейс lAppServer Две части приложения DataSnap взаимодействуют друг с другом при помощи ин- терфейса lAppServer. Определение этого интерфейса приведено в листинге 16.1. Вряд ли вам придется обращаться к методам этого интерфейса напрямую, так как в Delphi присутствуют компоненты, реализующие этот интерфейс на стороне сервера, а так- же компоненты, которые обращаются к этому интерфейсу на стороне клиента. Эти компоненты упрощают поддержку интерфейса lAppServer, а иногда позволяют про- граммисту, образно говоря, вообще забыть о его существовании. На практике сер- вер сделает объекты, реализующие этот интерфейс, доступными для клиента на- ряду с другими интерфейсами. Листинг 16.1. Определение интерфейса lAppServer type lAppServer = interfaceCIDispatch) ['{1AEFCC20-7A24-11D2-98B0-C69BEB4B5B6D} ’] function AS_ApplyUpdates(const ProviderName WideString, Delta OleVariant. MaxErrors Integer out ErrorCount Integer. var OwnerData OleVariant) OleVariant. safecall. function AS_GetRecords(const ProviderName WideString Count Integer out RecsOut Integer Options Integer const CommandText WideString. var Params OleVariant. var OwnerData OleVariant) OleVariant. safecall. function AS_DataRequest(const ProviderName WideString. Data OleVariant) OleVariant safecall function AS_GetProviderNames OleVariant safecall function AS_GetParams(const ProviderName WideString var OwnerData OleVariant) OleVariant safecall. function AS_RowRequest(const ProviderName WideString Row OleVariant. RequestType Integer var OwnerData OleVariant) OleVariant. safecall. procedure AS_Execute(const ProviderName WideString. cost CommandText WideString var Params OleVariant. var OwnerData OleVariant) safecall. end ПРИМЕЧАНИЕ----------------------------------------------------------------------------; Сервер DataSnap открывает интерфейс для доступа при помощи библиотеки типов СОМ — об этой технологии было рассказано в главе 12. Протокол соединения Технология DataSnap определяет высокоуровневую архитектуру взаимодействия. Для передачи данных между средним звеном и клиентским компьютером могут использоваться самые разные технологии. DataSnap поддерживает множество раз- личных протоколов, включая: О DCOM (Distributed СОМ) и Stateless COM (MTS или COM+). Механизм DCOM поддерживается в Windows NT/2000/XP, а также в Windows 98/Ме, 0746
Одно, два и три звена в истории Delphi 747 он не требует какого-либо дополнительного обеспечения на сервере. Техноло- гия DCOM является расширением технологии СОМ. Эта технология позволя- ет клиентскому приложению использовать серверные объекты, расположенные на другом компьютере. Инфраструктура DCOM позволяет вам использовать объекты СОМ, не сохраняющие информацию о состоянии (stateless), доступ- ные в рамках СОМ+ и в рамках более старой технологии MTS (Microsoft Trans- action Server). СОМ+ и MTS обеспечивают такие механизмы, как безопасность, управление компонентами и транзакции баз данных. Эти технологии доступ- ны в Windows NT/2000/XP и в Windows 98/Ме. Для технологии DCOM ха- рактерны множество проблем, в частности, сложность конфигурирования, невозможность преодоления брандмауэров. По этим причинам даже компания Microsoft отказалась от дальнейшего развития СОМ в пользу решений, осно- ванных на SOAP; О Сокеты TCP/IP. Эта технология поддерживается абсолютным большинством современных систем. Если вы используете TCP/IP, вы можете обеспечить под- ключение клиентов через Веб — в этом сокеты TCP/IP выгодно отличаются от DCOM. Кроме того, в отличие от DCOM, сокеты TCP/IP требуют существен- но меньших усилий для конфигурирования. Для использования сокетов на компьютере среднего звена должна функционировать программа ScktSrvr.exe, которая поставляется компанией Borland. Эта программа может работать как обычное приложение, а может быть запущена в режиме службы. Эта програм- ма принимает клиентские запросы и передает их удаленному модулю данных (функционирующему на том же сервере) через СОМ. Сокеты не обеспечивают защиту от сбоев на стороне клиента, так как сервер не может узнать о состоя- нии клиента и не может освободить ресурсы даже в случае, если клиент неожи- данно «подвисает»; О HTTP и SOAP. Использование HTTP в качестве транспортного протокола уп- рощает подключение клиентов через брандмауэры или прокси-серверы (кото- рые, как правило, не любят нестандартных сокетов TCP/IP). Для поддержки HTTP в рамках DataSnap вам потребуется установить на сервере специальную библиотеку httpsrvr.dll, которая воспринимает клиентские запросы и создает необходимые удаленные модули данных с использованием СОМ. Соединения, основанные на HTTP, используют SSL в качестве механизма защиты передава- емых через сеть данных. Наконец, веб-соединения, основанные на HTTP, могут использовать встроенную в DataSnap поддержку накопления объектов (object- pooling). ПРИМЕЧАНИЕ--------------------------------------------------------------- Транспорт HTTP DataSnap в качестве формата пакетов может использовать XML, в результате лю- бая платформа и любой инструмент, поддерживающие XML, смогут стать составной частью архи- тектуры DataSnap. Применение XML является расширением оригинального формата пакетов DataSnap, который также является платформенно-независимым. Передача XML через HTTP является основой технологии SOAP, о которой подробно рассказывается в главе 23. Вплоть до версии Delphi 6 в качестве транспортного механизма приложений DataSnap можно было использовать CORBA (Common Object Request Broker Architecture). Однако в результате проблем совместимости с новым механизмом 0747
748 Глава 16. Многозвенные приложения DataSnap VisiBroker CORBA компании Borland в Delphi 7 приложения DataSnap не могут использовать DataSnap. Наконец, следует отметить, что вы можете трансформировать пакеты данных в XML-формат и передавать их браузеру. В этом случае в рамках архитектуры появ- ляется еще одно звено: веб-сервер получает данные от среднего звена (бизнес-ло- гики) и передает их клиентскому веб-браузеру. Эта архитектура называется Internet Express, о ней я расскажу в главе 22, которая посвящена технологиям XML. Пакеты данных Поддерживаемая в рамках Delphi многозвенная архитектура доступа к данным базируется на идее пакетов данных. В этом контексте пакет данных — это блок данных, который перемещается от сервера приложений к клиенту или от клиента обратно к серверу приложений. Технически пакет данных — это некоторое под- множество набора данных. В нем содержится, как правило, несколько записей, а так- же описание этих данных (перечень полей и типов содержащихся в них значений). Помимо этого пакет данных содержит в себе ограничения, то есть правила, кото- рым должна соответствовать информация, содержащаяся в наборе данных. Обыч- но эти ограничения настраиваются на сервере приложений и передаются на сторо- ну клиента вместе с данными. Любой обмен данными между клиентом и сервером — это обмен пакетами дан- ных. Компонент провайдера на стороне сервера управляет передачей нескольких пакетов данных, входящих в состав одного большого набора данных, — он обеспе- чивает максимально высокую скорость ответа на запрос клиента. Когда клиент принимает пакет данных, этот пакет помещается в ClientDataSet, после этого пользо- ватель может редактировать содержащиеся в нем данные. Как уже было отмечено ранее, вместе с данными клиент принимает информацию об ограничениях, накла- дываемых на эти данные. Эти ограничения учитываются в процессе редактирова- ния. Выполнив редактирование, пользователь отдает команду опубликовать изме- нения в базе данных. В результате от клиента к серверу отправляется пакет дан- ных, который называют дельтой (delta). В этом пакете содержится информация о различиях между изначальными и новыми значениями модифицированных запи- сей. Иными словами, клиент отправляет серверу запрос на изменение некоторых записей, указывая в этом запросе изначальные и новые значения. Получив такой запрос, сервер пытается внести изменения в базу данных. Я сказал «пытается», так как если сервер обслуживает одновременно несколько клиентов, возможно, запи- си, которые требуется изменить, уже изменены кем-то чуть ранее. В этом случае запрос на обновление оканчивается неудачей. В составе дельта-пакета присутствуют изначальные значения данных, поэтому сервер может быстро определить, были ли данные изменены другим клиентом. Если это так, сервер генерирует сообщение OnReconcileError, которое является важным элементом архитектуры тонких клиентов. Иными словами, в рамках трехзвеннои архитектуры используется механизм обновления, аналогичный тому, который ис- пользуется в архитектуре кэшируемых обновлений. В главе 14 мы с вами говори- ли о том, что компонент ClientDataSet хранит данные в локальной памяти (то есть в кэше). В память читается лишь небольшое подмножество всех записей, доступ- 0748
Одно, два и три звена в истории Delphi 749 ных на сервере, дополнительные элементы подгружаются в память только тогда, когда в этом есть необходимость. Когда клиент обновляет записи или добавляет в набор данных новые записи, сведения об этих модификациях сохраняются в еще одной области локальной памяти, которая называется дельта-кэшем (delta-cache). Помимо этого клиент может сохранить пакеты с данными на диске и работать в автономном (то есть отключенном от сервера) режиме. Это осуществляется с ис- пользованием поддержки MyBase, о которой мы говорили в главе 13. Любая слу- жебная информация, в том числе сообщения об ошибках и диагностические сооб- щения, передается через сеть также в виде пакетов, таким образом, пакет данных действительно является основополагающим элементом архитектуры DataSnap. ПРИМЕЧАНИЕ ------------------------------------------------------------ Важно понимать, что пакеты данных не зависят от используемого транспортного протокола. Пакет данных — это всего лишь последовательность байтов. Эту последовательность можно передать с использованием любого протокола, который позволяет передавать последовательности байтов. Для этой цели подходят любые сетевые протоколы, включая DCOM, HTTP и TCP/IP. Благодаря этому архитектура DataSnap адаптируема для множества транспортных протоколов и множества плат- форм. Компоненты поддержки DataSnap (на стороне клиента) Теперь, когда мы познакомились с основами встроенной в Delphi поддержки трех- звенной архитектуры, мы можем перейти к изучению компонентов, при помощи которых осуществляется поддержка этой архитектуры. Для разработки клиент- ской части приложения в Delphi используется компонент ClientDataSet, который обеспечивает все стандартные возможности работы с набором данных и реализует клиентскую часть интерфейса lAppServer. В этом случае данные извлекаются при помощи удаленного подключения. Подключение к серверной части приложения осуществляется при помощи еще одного компонента, работающего на стороне клиента. Для этой цели вы можете использовать один из трех специализированных компонентов подключения (все три этих компонента располагаются на странице DataSnap). О DCOMConnection — этот компонент используется на стороне клиента для под- ключения к серверу DCOM или серверу MTS, располагающемуся либо на те- кущем компьютере, либо на другом компьютере, идентифицируемом при помо- щи свойства ComputerName. Соединение осуществляется с зарегистрированным объектом, которому соответствует заданный идентификатор ServeGUID или имя ServerName. ° Socketconnection — этот компонент служит для подключения к серверу через сокет TCP/IP. При этом необходимо указать IP-адрес или доменное имя, а также GUID серверного объекта (в свойстве InterceptGUID). Этот компонент обладает дополнительным свойством Supportcallbacks, которое необходимо отключить, если вы не поддерживаете обратный вызов (callback) и намерены установить программу на клиентском компьютере Windows 95, на котором не установлена библиотека Winsock 2. 0749
750 Глава 16. Многозвенные приложения DataSnap ПРИМЕЧАНИЕ ----------------------------------------------------------------- На странице WebServices вы также можете обнаружить компонент SoapConnection, для которого требуется специальный тип сервера. Об этом будет рассказано в главе 23. О WebConnection — этот компонент используется для подключения по протоколу HTTP. Такое подключение может быть выполнено через брандмауэр. Вы дол- жны указать URL, местоположение библиотеки httpsrvr.dll и имя или GUID удаленного серверного объекта. В состав DataSnap входят также несколько дополнительных клиентских ком- понентов, которые используются в основном для управления соединениями. О ConnectionBroker — этот компонент можно использовать в качестве псевдонима компонента соединения. Это может оказаться полезным, если в рамках вашего приложения используется несколько клиентских наборов данных. Чтобы изме- нить физическое подключение каждого из этих компонентов, вам потребуется всего лишь изменить значение свойства Connection компонента ConnectionBroker. Кроме того, вы можете использовать события этого компонента вместо анало- гичных событий компонентов фактических соединений. Благодаря этому, если в дальнейшем вам придется сменить транспортную технологию, вы сможете сделать это, фактически не меняя никакого кода. Также вы можете обращаться к объекту AppServer компонента ConnectionBroker вместо соответствующего свой- ства физического соединения. О SharedConnection — этот компонент может быть использован для подключения к вторичному (дочернему) модулю данных удаленного приложения, который пользуется физическим подключением основного модуля данных. Иными сло- вами, приложение может подключиться к нескольким серверным модулям дан- ных, используя единое общее подключение. О LocalConnection — этот компонент используется для подключения к локальному набору данных и использования этого набора в качестве источника пакетов данных. Такой же эффект можно получить, если подключить ClientDataSet на- прямую к провайдеру. Однако если для этой цели вы будете использовать LocalConnection, вы можете разработать локальное приложение, в котором бу- дет применяться точно такой же код, как и в многозвенной программе, при этом вы будете обращаться к интерфейсу lAppServer «ложного» подключения. Бла- годаря этому программу легче масштабировать. Несколько других компонентов, расположенных на странице DataSnap, имеют отношение к трансформации пакетов данных в специальные форматы XML. Оо этих компонентах (XMLTransform, XMLTransformProvider и XMLTransformClient) будет рас- сказано в главе 22. Компоненты поддержки DataSnap (на стороне сервера) На стороне сервера (на самом деле на компьютере, играющем роль среднего звена) необходимо создать приложение, или библиотеку, которая включает в себя уда ленный модуль данных (remote data module) — специальную версию класса TData- 0750
Построение простого приложения 751 Module. Второй альтернативой является использование специализированного мо- дуля данных для СОМ с транзакциями (transactional СОМ). На странице Multitier диалогового окна New Items (доступ к которому осуществляется при помощи File ► New ► Other) располагаются ярлыки запуска специальных мастеров, предназначен- ных для создания обоих типов удаленных модулей данных. Единственный специальный компонент, который потребуется вам на стороне сервера, это компонент DataSetProvider. Один такой компонент нужен для каждой таблицы или запроса, которые должны быть доступны для клиентского приложе- ния. Клиентское приложение будет использовать отдельный компонент Client- DataSet для каждого экспортированного набора данных. О компоненте DataSet- Provider рассказывалось в главе 13. Построение простого приложения Теперь можно приступить к созданию простой программы DataSnap. Рассмотрев пример, вы получите представление о практическом использовании только что упомянутых мною компонентов, а также познакомитесь с проблемами, которые заставят вас обратить внимание на некоторые нетривиальные аспекты использо- вания DataSnap. Я буду выполнять построение клиентской и серверной части трех- звенного приложения в два этапа. На первом этапе я просто тестирую технологию, используя минимум элементов. Полученная в результате программа будет весьма примитивной — она всего лишь демонстрирует работоспособность инфраструктуры. На втором этапе я добавляю дополнительные возможности в клиентскую и сер- верную части приложения. В каждом из примеров я отображаю данные, которые извлекаются из локальной базы данных InterBase с использованием dbExpress. Я намеренно размещаю все три звена моего приложения на одном компьютере — это делается для простоты. Полагаю, что далеко не все читатели обладают возмож- ностью прямо во время чтения книги проверить работоспособность DataSnap в се- тевой среде с несколькими компьютерами. Кроме того, для описания процедур, которые необходимы для установки этих приложений на нескольких компьюте- рах с использованием нескольких разных платформ и технологий, мне потребова- лось бы написать по крайней мере еще одну книгу. Самый первый сервер приложений Серверную часть базового примера построить очень легко. Создайте новое прило- жение, а затем добавьте в него удаленный модуль данных. Для этого воспользуй- тесь соответствующим значком на странице Multitier окна Object Repository. Начнет работу мастер создания удаленного модуля, окно этого мастера показано на рис. 16.1. Мастер предложит вам ввести имя класса и стиль создания экземпляров (стиль инстанцирования). Введите имя класса, например AppServerOne, и щелкните на кноп- ке ОК, в результате Delphi добавит модуль к программе. Модуль данных будет об- ладать обычными свойствами и событиями, однако его класс будет обладать сле- дующим определением Delphi: type TappServerOne = class(TremoteDataModule. lappServerOne) 0751
752 Глава 16. Многозвенные приложения DataSnap private { закрытые объявления } protected class procedure UpdateRegistryIRegister: Boolean, const Classic. ProgID string), override. public { открытые объявления } end Remote Data Module Wizard Instancr® TheadinaM'--:ti | Multiple Instance | Apartment Рис. 16.1. Мастер создания удаленного модуля данных Во-первых, этот класс является производным от класса TRemoteDataModule. Во- вторых, он реализует специальный интерфейс lAppServerOne, который является производным от стандартного интерфейса lAppServer (стандартный интерфейс DataSnap). Кроме того, в классе перекрывается метод UpdateRegistry, благодаря чему добавляется поддержка транспортных протоколов Веб и сокетов. В этом можно убедиться, взглянув на код, сгенерированный мастером. В конце модуля вы може- те обнаружить объявление фабрики классов, которое должно быть вам понятно, если вы уже прочитали главу 12: initialization TComponentFactory CreatelComServer. TAppServerOne, Class_AppServerOne. ciMultiInstance, tmApartment). end Теперь вы можете добавить в этот модуль данных компонент набора данных (в данном случае я использую компонент SQLDataSet из библиотеки dbExpress), под- ключить его к базе данных, таблице или запросу, активировать его и, наконец, добавить компонент DataSetProvider и связать этот компонент с набором данных. В результате вы получите примерно следующий DFM-код: object AppServerOne TAppServerOne object SQLConnectionl TSQLConnection ConnectlonName - 'IBLocal' LoginPrompt - False end object SQLDataSetl TSQLDataSet SQLConnection - SQLConnectionl CommandText - 'select * from EMPLOYEE' end object DataSetProviderl TDataSetProvider DataSet = SQLDataSetl Constraints = True end end 0752
Построение простого приложения 753 Основная форма данной программы совершенно бесполезна, поэтому вы мо- жете просто добавить на нее метку, на которой будет написано, что это главная форма серверного приложения. Когда вы скомпилируете этот сервер, вы должны запустить его один раз. В результате этого сервер автоматически зарегистрируется в вашей системе как сервер автоматизации (Automation Server) и станет доступен для клиентских приложений. Конечно же, вы должны зарегистрировать сервер на компьютере, на котором вы намерены его запустить, это должен быть либо клиент- ский компьютер, либо компьютер среднего звена. Самый первый тонкий клиент Теперь, когда у вас есть сервер, вы можете построить клиент, который сможет под- ключиться к этому серверу. Для этого вновь создайте обычное приложение Delphi и добавьте в него компонент DCOMConnection (или другой аналогичный компонент, соответствующий типу соединения, которое вы хотели бы протестировать). В рам- ках этого компонента определяется свойство ComputerName, которое используется для идентификации компьютера, на котором работает сервер приложения. Если вы хотите протестировать взаимодействие клиента и сервера, расположенных на одном и том же компьютере, вы можете оставить это поле пустым. Выбрав компьютер сервера приложений, вы можете отобразить список доступ- ных серверов DataSnap в ниспадающем списке свойства ServerName. В этом списке показываются имена зарегистрированных серверов, по умолчанию это имя испол- няемого файла сервера, за которым следует имя класса удаленного модуля данных, например AppServl.AppServerOne. Есть также альтернативный способ идентифика- ции серверов DataSnap: вы можете указать GUID серверного объекта в свойстве ServerGUID. Если вы указываете имя, Delphi автоматически присваивает свойству ServerGUID идентификатор GUID сервера, этот идентификатор извлекается из рее- стра. Теперь вы можете присвоить свойству Connected компонента DCOMConnection значение True. На экране появится форма сервера, это говорит о том, что клиент активизировал серверную часть приложения. Обычно вы не должны выполнять эту операцию, так как компонент ClientDataSet активирует компонент RemoteServer вместо вас. Я описываю это действие только для того, чтобы вы получили пред- ставление о том, что происходит за кулисами DataSnap. СОВЕТ---------------------------------------------------------------------- На этапе проектирования свойству Connected компонента DCOMConnection обычно присваивают значение False, так как в этом случае вы сможете открыть проект Delphi даже тогда, когда сервер DataSnap еще не зарегистрирован. Как можно предположить, далее на форму клиентского приложения следует Добавить компонент ClientDataSet. Компонент ClientDataSet следует подключить к компоненту DCOMConnectionl (а следовательно, к одному из экспортируемых им провайдеров) при помощи свойства RemoteServer. Список доступных провайдеров отображается в свойстве ProviderName в виде ниспадающего списка. В рассматри- ваемом примере вы сможете выбрать только один провайдер — DataSetProviderl, — так как это единственный провайдер в созданном вами сервере. В результате вы- полнения этой операции набор данных, расположенный в памяти клиентского 0753
754 Глава 16. Многозвенные приложения DataSnap компьютера, связывается с набором данных dbExpress, расположенным на сторо- не сервера. Если вы активируете клиентский набор данных и добавите несколько элементов управления, предназначенных для работы с данными (например, DBGrid), вы немедленно увидите, что в них появятся серверные данные (как показано на рис. 16.2). К ^ThinClientl оерт_мо1ем₽ле OwjaMg." 5 l.±l U 1 1 1 1.-1 600 2 Robert 621ЬС0МСопп^Ж4>В,ис' 130 а ж 5*К<п 180 8 Leslie 622 «QT" 44 9 PhB ззо ЙЙЬЙЙ п к J 000 СН 12 Тип 900 ~£-.1 14 Stewart 623 15 KathHine 671 " 20 Cta 671 24 Pete 120 28 Ann 623 29 Води 110 34 Janet Nelson, Robert Young, Bruce Lambert, Kim Johnson, Leslie Forest, PM Weston, К J Lee, Tern HaB, Stewart Young, Katherine Papadopoulos, Chns Fisher, Pete Bennet, Ann De Souza, Roger Baldwin, Janet 12/28| 12/28 I 2/6/1 1 4/5/1 j 4/17/ 1/17/-4 5/1/1 6/4/1 6/14/ 1/1/1 9/12/ 2/1/1 2/18/ 3/21 /»] лГ/ Рис. 16.2. Когда вы активируете компонент ClientDataSet, подключенный к удаленному модулю данных на этапе проектирования, данные, полученные от сервера, становятся видны на стороне клиента Вот DFM-файл для минимальной конфигурации клиента ThinClil: object Forml TForml Caption = 'ThinClientl' object DBGridl TDBGrid Align = alClient DataSource = DataSourcel end object DCOMConnectionl TDCOMConnection ServerGUID = ' {09E11D63-4A55-11D3-B9F1-00000100A27B} ’ ServerName = 'AppServl AppServerOne' end object ClientDataSetl TCIlentDataSet Aggregates = <> Params = <> ProviderName = 'DataSetProviderl' RemoteServer = DCOMConnectionl end object DataSourcel TDataSource DataSet = ClientDataSetl end end Очевидно, что построенная нами в данном случае трехзвенная программа весьма проста, однако она демонстрирует разделение обязанностей по обработке и ото- бражению данных между двумя исполняемыми файлами. На текущий момент кли- ент выполняет функции простой программы просмотра данных. Если вы редакти- руете данные на стороне клиента, они не будут обновлены на сервере. Чтобы 0754
Добавление в сервер ограничений, накладываемых на данные 755 добавить в программу возможность редактирования данных, вы должны добавить в клиентскую часть дополнительный код. Однако перед этим давайте добавим не- которые дополнительные функции в сервер. Добавление в сервер ограничений, накладываемых на данные Когда вы создаете традиционный модуль данных в Delphi, вы можете с легкостью добавить бизнес-логику путем обработки генерируемых набором данных событий или путем настройки свойств объектов полей и обработки событий, связанных с этими объектами. Однако в этой главе мы обсуждаем трехзвенную архитектуру. Значит, бизнес-логика должна располагаться в среднем звене, а клиент должен быть освобожден от обязанностей по обработке данных. В рамках архитектуры DataSnap можно организовать пересылку информации о некоторых ограничениях с сервера на сторону клиента и заставить клиентское приложение следить за выполнением этих ограничений в процессе ввода данных пользователем. Помимо этого можно организовать передачу на сторону клиента свойств полей (например, минимальное и максимальное допустимые значения, маска отображения и маска ввода), а также обеспечить обновление данных через набор данных, используемый для доступа к данным (или через соответствующий объект UpdateSql). Ограничения, накладываемые на поля и на набор данных Когда интерфейс провайдера создает пакет данных для передачи клиенту, он вклю- чает в него определения полей, ограничения, накладываемые на таблицу и на поля, а также одну или несколько записей (в соответствии с запросом компонента Client- DataSet). Подразумевается, что информация об ограничениях хранится на стороне сервера или на среднем звене. Иначе говоря, вы можете настроить среднее звено и сформировать логику распределенного приложения, используя ограничения, определяемые с использованием SQL. Ограничения, создаваемые с использованием выражений SQL, могут быть на- значены либо в отношении всего набора данных, либо в отношении отдельных полей. Провайдер передает ограничения клиенту вместе с данными, и клиент при- меняет эти ограничения перед тем, как отправить модифицированные данные об- ратно на сервер. Благодаря этому снижается общий объем сетевого трафика: если бы клиент не выполнял проверку ограничений, модифицированные пользовате- лем данные передавались бы среднему звену, а затем SQL-серверу, и только на SQL-сервере обнаруживалось бы, что данные не удовлетворяют ограничениям. Таким образом, хранить ограничения следует на стороне сервера, однако обработ- ка ограничений должна выполняться не только на стороне сервера, но и на стороне клиента. Если информация об ограничениях хранится на сервере, то в случае из- менения бизнес-правил вам потребуется модифицировать код только на сервере. Если же информация об ограничениях будет храниться на стороне клиента, то при 0755
756 Глава 16. Многозвенные приложения DataSnap изменении бизнес-правил вам придется обновлять программу на множестве кли- ентских компьютеров. Каким образом формируются ограничения? Для этой цели можно использо- вать несколько разных свойств. О Наборы данных BDE обладают свойством Constraints, которое является коллек- цией объектов TCheckConstraint. Каждый объект обладает несколькими свойства- ми, включая выражение и сообщение об ошибке. О Каждый объект поля определяет свойства CustomConstraint и ConstraintError- Message. Существует также свойство ImportedConstraint для ограничений, импор- тированных с SQL-сервера. О Каждый объект поля обладает свойством DefaultExpression, которое может быть использовано локально или передано компоненту ClientDataSet. Это свойство не является фактическим ограничением, это всего лишь рекомендация для ко- нечного пользователя. В следующем примере, AppServ2, в удаленный модуль данных, подключенный к базе данных EMPLOYEE сервера InterBase, добавляется несколько ограничений. Пос- ле подключения таблицы к базе данных и создания объекта поля можно настроить следующие специальные свойства (сообщения об ошибках для ясности переведе- ны на русский язык): object SQLDataSetl TSQLDataSet object SQLDataSetlEMP_NO TsmallintField CustomConstraint - 'X > 0 and x < 10000' ConstraintErrorMessage « 'Номер сотрудника должен быть положительным числом, не превышающим 10000' FieldName = 'EMP_NO' end object SQLDataSetlFIRST_NAME TstringField CustomConstraint - 'x <> '#39#39 ConstraintErrorMessage - 'Требуется ввести имя' FieldName - 'FIRST_NAME' Size - 15 end object SQLDataSetlLAST_NAME TstringField CustomConstraint - 'not x is null' ConstraintErrorMessage - 'Требуется ввести фамилию' FieldName = 'LAST_NAME' end end ПРИМЕЧАНИЕ Выражение 'x<>'#39#39 в переводе c DFM соответствует выражению х о ", то есть строка х не должна быть пустой строкой. Последнее ограничение not х is null означает, что вы допускаете присвоение пустой строки, но не допускаете присвоение значения null. Свойства полей Свойства объектов полей, располагающихся на среднем звене, могут передаваться клиентскому набору данных ClientDataSet (и копироваться в соответствующие объекты полей на стороне клиента). Для этого следует воспользоваться значением 0756
Добавление в сервер ограничений, накладываемых на данные 757 poln с Fie Id Props свойства Options компонента DataSetProvider. Этот флаг контролиру- ет передачу на сторону клиента свойств полей Alignment, DisplayLable, DisplayWidth, Visible, DisplayFormat, EditFormat, MaxValue, MinValue, Currency, EditMask и DisplayValues (если эти свойства определены в отношении поля). Вот пример еще одного поля в примере AppServ2 с некоторыми специальными свойствами: object SQLDataSet1SALARY TBCDField DefaultExpression = '10000' FieldName = 'SALARY' DisplayFormat = '#.###' EditFormat = '###' Precision = 15 Size = 2 end Благодаря этому вы можете писать код среднего звена в точности так же, как пишется код настройки полей стандартного клиент-серверного приложения. Этот подход позволяет также быстро перевести обычное клиент-серверное приложение на использование трехзвенной архитектуры. Основным недостатком передачи свойств полей на сторону клиента является снижение производительности: для передачи дополнительной информации между звеньями требуется дополнитель- ное время. При отключении poIncFieldProps сетевая производительность наборов данных с множеством столбцов существенно повышается. Сервер может отправлять на сторону клиента информацию лишь о некоторых полях. Для этого необходимо при помощи редактора полей определить постоян- ные (persistent) объекты лишь для некоторых полей. Если фильтруемое поле необходимо для идентификации записей для последующих обновлений (напри- мер, если поле является частью первичного ключа), вы можете воспользоваться свойством ProviderFlags на стороне сервера для того, чтобы передать значение поля на сторону клиента, однако сделать это значение недоступным для компонента ClientDataSet (это решение обеспечивает несколько более высокий уровень защиты по сравнению с решением, когда вы передаете поле на сторону клиента и уже на клиентском компьютере скрываете это поле). События, связанные с полями и таблицами Обработчики событий, связанных с набором данных и объектами полей, располо- женных в среднем звене, разрабатываются традиционным образом. Набор данных среднего звена обрабатывает обновления, принимаемые от клиента, как и обыч- ный клиентский набор данных. Это означает, что обновления рассматриваются, как обычные операции с набором данных, будто пользователь выполняет редакти- рование, добавление и удаление данных локально. Запрос на выполнение обновления генерируется при помощи свойства Resolve- ToDataSet компонента TDatasetProvider. Это свойство позволяет подключить либо набор данных, используемый для ввода, либо второй набор данных, использу- емый для обновлений. Такой подход можно использовать в отношении набо- ров данных, которые поддерживают выполнение операций редактирования. К этой категории относятся наборы данных BDE, ADO и InterBase Express, однако наборы данных новой библиотеки dbExpress операций редактирования не поддерживают. 0757
758 Глава 16. Многозвенные приложения DataSnap В рамках описанного подхода обновления выполняются набором данных, бла- годаря чему вы получаете широкие возможности контроля над обновлением (ге- нерируются все стандартные связанные с этим события), однако снижается про- изводительность. В данном случае гибкость важнее, так как вы можете кодировать, используя привычные для вас методики. Кроме того, в рамках данной модели вы можете с легкостью преобразовать имеющееся клиент-серверное приложение в трехзвенное приложение. Однако не забывайте о том, что пользователь клиент- ской программы получит сообщения об ошибке только после того, как содержи- мое локального кэша (дельта) будет передано на компьютер среднего звена. В ре- зультате может возникнуть ситуация, когда пользователь получит сообщение об ошибке, имеющее отношение к данным, обновление которых было выполнено пол- часа назад. Согласитесь, что это может сбить его с толку. Если вы используете опи- сываемый подход, возможно, обновление кэша следует выполнять каждый раз, когда на стороне клиента генерируется событие AfterPost. Наконец, если вы решите, что обновление данных должно выполняться не про- вайдером, а набором данных, Delphi поможет вам выполнить обработку возмож- ных исключений. Любые исключения, генерируемые событиями обновления на компьютере среднего звена (например, OnBeforePost), автоматически трансформи- руются в ошибки обновления. В результате на стороне клиента активируется со- бытие OnReconcileError (подробнее об этом событии будет рассказано далее в дан- ной главе). Иными словами, на среднем звене не возникает никакого исключения — сигнал об ошибке передается на сторону клиента. Добавление дополнительных возможностей на стороне клиента Теперь, после того как мы добавили в серверную часть нашего приложения неко- торые ограничения и свойства полей, предлагаю вернуться к рассмотрению кли- ента. Самая первая версия клиента была очень простой, однако сейчас мы можем добавить несколько дополнительных возможностей для того, чтобы улучшить функционирование клиента. В программе ThinCli2 я добавил поддержку проверки состояния записи и доступ к информации, содержащейся в области дельта (об- ласть, где хранятся обновления, которые необходимо отправить обратно на сер- вер). Для этого я использовал некоторые методики работы с ClientDataSet, о кото- рых мы уже говорили в главе 13. Помимо этого программа выполняет обработку возникающих конфликтов обновления и поддерживает модель Briefcase (Порт- фель). Важно отметить, что когда пользователь редактирует данные локально, при нарушении бизнес-правил (которые определяются на стороне сервера в виде огра- ничений) клиентское приложение немедленно оповестит пользователя об этом. Кроме того, сервер передает клиенту значение по умолчанию для поля Salary в слу- чае, если пользователь создает новую запись. Вместе с этим значением на сторону клиента передается также формат отображения этого поля (DisplayFormat). На рис. 16.3 вы можете видеть одно из сообщений об ошибке, которое отображается клиентским приложением. Текст сообщения передается клиенту от сервера. Сооо- 0758
Добавление дополнительных возможностей на стороне клиента 759 щение отображается на экране, когда пользователь редактирует данные локально, а не тогда, когда он отправляет изменения серверу. Update I )>.) И...... Snapshot. Reload.. I ShowDeke Sfez..../.. . usUnmodtfied usUnmodtfied usUnmodtfied ^Unmodified usUnmodtfied usUnmodtfied usUnmodtfied usUnmodtfied X usModrfied 125 121 Roberto 7/12/1993 Thmckz Bruce Undo 12/28/1988 621 99912 Рис. 16.3. Сообщение об ошибке, отображаемое программой ThinCli2 в случае, если пользователь вводит слишком большое значение для ID сотрудника Последовательность обновления В клиентской программе присутствует кнопка, которая позволяет пользователю применить внесенные им изменения к серверу. Кроме того, клиентская программа поддерживает стандартное диалоговое окно разрешения возникших конфликтов обновления. Вот полная последовательность операций, связанных с обработкой запроса на обновление с указанием возможных ошибок. 1. Клиентская программа обращается к методу ApplyUpdates компонента Client- DataSet. 2. Содержимое области дельта передается провайдеру, расположенному в сред- нем звене. Провайдер генерирует событие On Update Data. В рамках обработчика этого события вы можете проанализировать сделанные пользователем измене- ния перед тем, как они будут переданы серверу базы данных. На данном этапе вы можете изменить содержимое дельты, так как эти данные содержатся в фор- мате, совместимом с ClientDataSet. 3. Провайдер (точнее говоря, часть провайдера, которая называется резольвер — resolver) применяет каждую строку дельты в отношении сервера базы данных. Перед тем как применить каждое из обновлений, провайдер принимает собы- тие BeforeUpdateRecord. Если вы установили флаг ResolveToDataSet, будут генери- роваться локальные события набора данных, расположенного в среднем звене. 4. В случае возникновения ошибки на стороне сервера провайдер сгенерирует событие OnUpdateError (в среднем звене), и у программы появится возможность исправить ошибку на уровне среднего звена. б. Если программа среднего звена не исправляет ошибку, соответствующий за- прос на обновление остается в дельте. Ошибка передается клиенту. Это проис- ходит либо немедленно, либо после того как на среднем звене будет накоплено 0759
760 Глава 16. Многозвенные приложения DataSnap определенное количество ошибок. Это количество определяется значением па- раметра MaxErrors вызова ApplyUpdates. 6. Дельта-пакет с оставшимися в нем обновлениями передается обратно клиенту Для каждого оставшегося обновления генерируется событие OnReconcileError компонента ClientDataSet. В рамках обработчика этого события программа мо- жет попытаться решить проблему (возможно, для этого потребуется вмешатель- ство пользователя). Пользователь может исправить сделанные им изменения и попытаться заново передать их серверу. Обновление данных Возможно, пока пользователь работал сданными, эти данные были модифициро- ваны другими пользователями. Чтобы освежить имеющуюся у него информацию, клиент может обратиться к методу Refresh компонента ClientDataSet. Однако эту операцию можно выполнить только в случае, если в кэше нет ни одного обновле- ния. Если в кэше присутствует хотя бы одно обновление, обращение к методу Refresh приводит к генерации исключения: if cds.ChangeCount = 0 then cds.refresh; Если модифицированы были только некоторые из записей, вы можете осве- жить все остальные (то есть не измененные записи) при помощи метода Refresh- Record. Этот метод обновляет только текущую запись и только в случае, если текущая запись не была модифицирована пользователем. Метод RefreshRecord об- новляет текущую запись и сохраняет сделанные пользователем изменения в жур- нале изменений. Например, вы можете освежать запись каждый раз, когда эта запись становится активной, если только запись не была модифицирована и изме- нения не были опубликованы на сервере: procedure TForml.cdsAfterScroll(DataSet: TDataSet): begin if cds.UpdateStatus = uslInModified then cds.RefreshRecord: end; Рис. 16.4. Форма программы ClientRefresh, которая автоматически освежает значения полей активной записи Если данные часто подвергаются изменениям со стороны множества пользова- телей и каждый из пользователей должен немедленно видеть эти изменения, из- менения необходимо вносить в базу данных немедленно при помощи методов 0760
Добавление дополнительных возможностей на стороне клиента 761 AfterPost и AfterDelete, кроме того, необходимо постоянно обращаться к методу Refresh Records для активной записи (как показано ранее) или для каждой из запи- сей, отображаемых в сетке DBGrid. Данный код является частью программы Client- Refresh, которая подключается к серверу AppServ2. Для отладочных целей програм- ма также заносит в журнал значение поля EMP_NO для каждой обновляемой записи, как показано на рис. 16.4. В программе присутствует кнопка, которая позволяет освежить информацию в каждой из видимых на экране строк таблицы. В обработчике, связанном с этой кнопкой, курсор набора данных перемещается с текущей записи на самую первую видимую на экране запись, а затем — на самую последнюю видимую запись. Коли- чество видимых строк равно RowCount -1, так как в самой первой строке отобража- ются имена столбцов — эта строка не изменяется. Каждое перемещение от строки к строке генерирует событие AfterScroll, в обработчике которого выполняется код обращения к Refresh Record, показанный ранее. Далее приводится код функции, ко- торая освежает все видимые на экране строки. Эта функция может выполняться через равные промежутки времени при помощи таймера: // обход защищенного доступа type MyGrid = class (TDBGrid) end: procedure TForml.Button2Click(sender: TObject); var i: Integer: bm: TBookmarkStr: begin // освежаем информацию в видимых на экране строках cds.DisableControls: // начинаем с текущей строки 1 := TmyGrid (DbGridl).Row: bm := cds.Bookmark: try // переходим к первой видимой на экране строке while 1 > 1 do begin cds.Prior: Dec (i): end: // возвращаемся к текущей строке i •= TmyGrid(DbGridl).Row: cds.Bookmark : = bm; // просматриваем все строки, пока сетка не закончится while 1 < TmyGrid(DbGridl).RowCount do begin cds.Next; Inc (i). end: finally // восстанавливаем конфигурацию cds.Bookmark := bm. cds.EnableControls; end: end; 0761
762 Глава 16. Многозвенные приложения DataSnap Обновление строк сетки в подобном стиле приводит к резкому увеличению се- тевого трафика, поэтому я рекомендую вам включать обновления только тогда когда в этом действительно есть необходимость. Кроме того, вы можете реализо- вать более эффективный механизм, в рамках которого сервер оповещает всех кли- ентов об изменении некоторой записи. После этого клиент может самостоятельно определить, заинтересован ли он в получении обновленных данных. Дополнительные возможности DataSnap На текущий момент я рассмотрел лишь некоторые из возможностей технологии DataSnap. В данном разделе я очень коротко расскажу о некоторых других воз- можностях этой технологии. Для демонстрации этих возможностей я разработал две программы: AppSPlus и ThinPlus. К сожалению, если я буду подробно рассматри- вать каждый из этих механизмов, данная глава превратится в отдельную книгу, поэтому я ограничусь кратким описанием каждого из рассматриваемых здесь ме- ханизмов. ВНИМАНИЕ-------------------------------------------------- Для запуска программы ThinPlus потребуется входящий в состав Delphi сервер сокетов (он распола- гается в подкаталоге bin). Если вы не запустите эту программу, при попытке подключения к серверу клиент отобразит на экране ошибку сокета. Однако вы можете с легкостью модифицировать сетевую конфигурацию программы, для этого достаточно изменить IP-адрес сервера на сто- роне клиента. Помимо возможностей, обсуждаемых в следующих разделах, программы AppSPlus и ThinPlus демонстрируют использование сокетного соединения, ограниченное про- токолирование событий и обновлений на стороне сервера, а также прямое извле- чение записи на стороне клиента. Последняя возможность реализуется при помо- щи следующего кода: procedure TclientForm.ButtonFetchClickCSender- TObject): begin ButtonFetch.Caption IntToStr (cds.getNectPacket): end: Эта возможность позволяет вам получить с сервера больше записей, чем требу- ется для отображения клиентского пользовательского интерфейса (элемента управ- ления DBGrid). Другими словами, вы можете извлекать записи напрямую, не ожи- дая, пока пользователь пролистает сетку. Я предлагаю вам подробнее изучить код обоих этих примеров, после того как вы прочитаете все остальные подразделы дан- ного раздела главы. Запросы с параметрами Если вы хотите использовать параметры в запросе или сохраненной процедуре- тогда, вместо построения специального метода со специальным обращением к сер- веру, вы можете использовать специальную возможность Delphi. Для начала опре- делите запрос с параметром в третьем звене: select * from customer where Country = -Country 0762
Дополнительные возможности DataSnap 763 Используйте свойство Params для того, чтобы настроить тип и значение по умол- чанию для параметра. На стороне клиента для этой цели вы можете воспользо- ваться командой Fetch Params (Получить информацию о параметрах) контекстного меню компонента ClientDataSet, после того как вы подключите компонент к подхо- дящему провайдеру. На этапе выполнения вы можете обратиться к методу Fetch- Params компонента ClientDataSet Теперь вы можете установить локальное значение по умолчанию для парамет- ра, для этого воспользуйтесь свойством Params. Значение параметра будет переда- но среднему звену в момент, когда вы пытаетесь получить данные из БД. В про- грамме Thin Plus обновление параметра выполняется с использованием следующего кода: procedure TformQuery btnParamClick(Sender: TObject); begin cdsQuery.Close; cdsQuery Params[0].AsString := EditParam.Text; cdsQuery.Open, end: На рис. 16.5 показана вторичная форма этого примера, на которой отображается результат выполнения запроса с параметром. На рисунке видны некоторые дан- ные, полученные с сервера. О том, как определить набор этих данных, рассказыва- ется в разделе «Настройка пакетов данных». Ik Data sent at 2:58; 16 PM 6215 Underwater SCUBA Company 6582 Norwest’ei SCUBA Lmrted PO Box Sn 91 PO Box 6834 Рис. 16.5. Вторичная форма примера ThinPlus, на которой видны данные, полученные в результате выполнения запроса с параметром 1 emuda . , , i Вызов методов сервера Так как сервер обладает обычным COM-интерфейсом, в его составе могут присут- ствовать любые методы и свойства, к которым можно обращаться со стороны кли- ента. Для этого необходимо открыть редактор библиотеки типов сервера и ис- пользовать его так, как вы используете его в отношении любого другого сервера. В примере AppSPlus я добавил в состав COM-интерфейса метод Login, обладающий следующей реализацией: Procedure TappServerPlus Logintconst Name. Password WideString). begin 0763
764 Глава 16. Многозвенные приложения DataSnap // TODO: добавить реальный код аутентификации . . . if Password <> Name then raise Exception Create ('Неправильная комбинация имени/пароля') el se Query.Active := True: ServerForm.Add ('Login:' + Name + 'V + Password); end; В данном случае, чтобы упростить пример, я добавил в программу весьма при- митивный код. Вместо того чтобы сравнивать имя и пароль с таблицей паролей (как это было бы в реальном приложении), программа выполняет простую про- верку. Если пароль не равен имени, запрос остается в неактивном состоянии. Та- кой метод блокирования доступа нельзя назвать надежным, так как запрос может быть активирован провайдером. Более надежным решением было бы отключение провайдера DataSetProvider. Чтобы обратиться к серверу, клиент может воспользо- ваться свойством AppServer компонента удаленного подключения. Далее приводится пример обращения из клиентской программы ThinPlus. Обращение осуществляет- ся из обработчика события AfterConnect компонента соединения: procedure TclientForm.ConnectionAfterConnect(Sender: TObject); begi n Connection AppServer.Login (Edit2.Text. Edit3.Text); end; Имейте в виду, что вы можете обращаться к методам интерфейса СОМ при помощи DCOM, а также используя сокетное или HTTP-соединение. Так как про- грамма использует соглашение о вызовах safecall, исключение, генерируемое на сервере, автоматически перенаправляется и отображается на стороне клиента. В этом случае, когда пользователь устанавливает флажок Connect, функциониро- вание обработчика события, используемого для активизации клиентских наборов данных, прерывается, и пользователь, указавший неправильный пароль, не смо- жет увидеть данные. ПРИМЕЧАНИЕ------------------------------------------------------------------------------ Вместо прямого обращения от клиента к серверу вы можете реализовать обратный вызов сервера, адресованный клиенту. Такой подход можно использовать, например, для того, чтобы оповестить каждого из клиентов о некотором событии. Реализовать подобное можно при помощи событии СОМ. Существует также другой способ; вы можете добавить новый интерфейс, реализуемый клиен- том, который передает объект реализации серверу. В этом случае сервер может обратиться к мето- ду клиентского компьютера. Следует иметь в виду, что обратный вызов невозможен в случае, если вы используете соединения HTTP. Отношения типа «основное/подробности» Если приложение среднего звена экспортирует несколько наборов данных, вы мо- жете извлекать из них данные, используя для этой цели несколько компонентов ClientDataSet на стороне клиента, которые соединены локально для формирования структуры «основное/подробности». Если при этом вы не намерены перемешать все записи на сторону клиента, у вас возникнут проблемы с набором данных, со- держащим подробности. Данное решение делает сложной процедуру применения обновлений. Вы не можете удалить запись из основной таблицы до тех пор, пока не будут удалены все Г'П ОТО TI tltTZS Z- оаттглт*™ D TnA TTUirnV ПП rrori Атт/-\Г->'Т'ЛТТ тчт г ТТЛ ПГ)О« 0764
Дополнительные возможности DataSnap 765 вить записи в таблицы подробностей до тех пор, пока в основную таблицу не будет добавлена соответствующая им основная запись. (Разные серверы обрабатывают этот случай по-разному, однако описанное поведение является стандартным в лю- бой ситуации, в которой используется ключ из другой таблицы.) Чтобы решить проблему, вы можете написать сложный код на стороне клиента, который будет выполнять обновление записей в обеих таблицах в соответствии с некоторыми правилами. Совершенно другой подход состоит в том, чтобы получить от сервера единствен- ный набор данных, в котором подробности содержатся в виде дополнительного поля, которое имеет тип TDataSetField. Чтобы добиться этого, необходимо устано- вить отношения «основное/подробности» на стороне сервера: object TableCustomer: TTable DatabaseName - 'DBDEMOS' TableName = 'customer.db' end object TableOrders: TTable DatabaseName = 'DBDEMOS' MasterFields = 'CustNo' MasterSource = DataSourceCust TableName = 'ORDERS DB' end object DataSourceCust: TDataSource DataSet = TableCustomer end object Providercustomer: TDataSetProvider DataSet = TableCustomer end На стороне клиента таблица подробностей выглядит как дополнительное поле компонента ClientDataSet, а элемент управления DBGrid отображает ее как дополни- Рис. 16.6. Программа ThinPlus демонстрирует два режима отображения таблицы подробностей: в отдельном окне или в составе той же самой формы. Как правило, в приложении используется только один из этих методов, но не оба эти метода одновременно 0765
766 Глава 16. Многозвенные приложения DataSnap тельный столбец с кнопкой, на которой изображен символ многоточия. При щелч- ке на этой кнопке на экране появляется вторичная форма с сеткой, в которой отображается таблица подробностей (рис. 16.6). Если вы хотите сформировать до- статочно гибкий пользовательский интерфейс на стороне клиента, вы можете добавить на форму еще один компонент ClientDataSet, подключенный к полю набора данных при помощи свойства DataSetField. Просто создайте постоянные (persistent) поля для основного набора данных ClientDataSet и настройте свойство: object cdsDet: TCIientDataSet DataSetField = cdsTableOrders end В подобной конфигурации вы можете отобразить набор данных с подробностя- ми в отдельном элементе DBGrid, расположенном на той же самой форме (нижняя сетка на рис. 16.6) или в любом другом удобном для вас месте. Имейте в виду, что при использовании подобной структуры обновления относятся только к основной таблице, и сервер должен обеспечить корректную последовательность обновления данных даже в сложных ситуациях. Использование брокера соединений Я уже говорил о том, что компонент ConnectionBroker может оказаться полезным в случае, если вы захотите изменить физическое соединение, используемое несколь- кими компонентами ClientDataSet одной и той же программы. Вы можете подклю- чить все компоненты ClientDataSet к единственному компоненту ConnectionBroker, а затем, чтобы изменить физическое соединение, используемое всеми клиентски- ми наборами данных, вы можете изменить соединение, используемое брокером. В программе ThinPlus используется следующая конфигурация: object Connection- TSocketConnection ServerName = 'AppSPlus AppServerPlus' AfterConnect = ConnectionAfterConnect Address = '127 О О Г end object Connect!onBrokerl: TConnectionBroker Connection = Connection end object cds TCIientDataSet ConnectionBroker = Connect!onBrokerl end // на вторичной форме object cdsQuery TCIientDataSet ConnectionBroker = ClientForm.ConnectionBrokerl end Фактически, это все, что вам необходимо сделать. Чтобы изменить физическое соединение, поместите новый компонент подключения DataSnap на основную фор- му и присвойте ссылку на этот компонент свойству Connection. Дополнительные параметры провайдера Я уже упоминал о свойстве Options компонента DataSetProvider, обратив ваше вни- мание на то, что вы можете воспользоваться этим свойством для добавления свойств полей в пакет данных. Существует также несколько других параметров, которые можно использовать для настройки пакета данных и для управления поведением клиентской программы. Вот сокращенный список этих параметров. 0766
дополнительные возможности DataSnap 767 о Значение poFetchBlobsOnDemand позволяет минимизировать время, которое тра- тится на загрузку данных BLOB. При использовании этого режима клиентское приложение может загружать поля BLOB, присвоив сворктгву FetchOnDemand (загрузка по требованию) компонента ClientDataSet значение True или обратив- шись к методу Fetch Blobs для избранных записей. Аналогичным образом вы можете отключить автоматическую загрузку записей подробностей, восполь- зовавшись значением poFetchDetailsOnDemand. И снова клиент может использо- вать свойство FetchOnDemand или обратиться к методу Fetch Details. О При использовании структуры «основное/подробности» вы можете контроли- ровать обработку данных в связанных таблицах при помощи значений poCascade- Deletes и poCascadeUpdates. Значение poCascadeDeletes указывает, должен ли про- вайдер удалять записи подробностей перед тем, как удалить запись из основной таблицы. Этот параметр можно использовать в случае, если сервер базы дан- ных автоматически удаляет связанные записи для обеспечения целостности БД. Аналогичным образом используется значение poCascadeUpdates, которое прика- зывает серверу выполнять автоматическое обновление ключевых значений в связке «основное/подробности». О Вы можете наложить ограничения на операции, выполняемые на стороне кли- ента. Самое строгое ограничение — poReadOnly — запрещает выполнение любых обновлений. Если вы хотите предоставить пользователю ограниченные возмож- ности редактирования данных, вы можете воспользоваться значениями poDis- ablelnserts (запретить добавление новых записей), poDisableEdits (запретить ре- дактирование записей) или poDisableDeletes (запретить удаление записей). о Значение poAutoRefresh указывает переслать клиенту копии модифицирован- ных им записей. Это может оказаться полезным в случае, если эти записи мо- дифицировались параллельно несколькими пользователями и эти несколько пользователей внесли в записи несколько не конфликтующих между собой из- менений. Значение poPropogateChanges предписывает переслать обратно клиен- ту изменения, сделанные в рамках обработчиков событий BeforeUpdateRecord или AfterUpdateRecord. Эта возможность полезна, если вы используете поля с авто- матическим инкрементом, триггеры и другие способы модификации данных на сервере или в среднем звене. О Наконец, свойство poAllowCommandText позволяет клиенту настроить SQL-зап- рос или имя таблицы, используемые в среднем звене. Для этого используются методы GetRecords или Execute. Компонент SimpleObjectBroker Компонент SimpleObjectBroker обеспечивает простой способ обнаружения сервер- ного приложения среди нескольких серверных компьютеров. Вы передаете этому компоненту перечень доступных компьютеров, и клиент пытается установить связь с каждым из них по порядку до тех пор, пока не будет установлено соединение. Более того, если вы включите свойство LoadBalanced, компонент будет случай- ным образом выбирать один из серверов. Если несколько клиентов используют одну и ту же конфигурацию, подключения будут автоматически распределены Между несколькими серверами. Если подобный подход покажется вам слишком примитивным, имейте в виду, что некоторые дорогостоящие системы распределе- ния нагоузки сЬактически выполняют то жр самое. 0767
768 Глава 16. Многозвенные приложения DataSnap Накопление объектов Если несколько клиентов подключается к серверу в одно и то же время, вы можете воспользоваться одним из двух способов. Во-первых, вы можете создать объект удаленного модуля данных для каждого клиента и обработать каждый из запросов по порядку (это поведение является поведением по умолчанию для сервера СОМ, функционирующего в стиле ciMultilnstance). Кроме того, вы можете сделать так, что- бы в системе для каждого нового клиента создавался новый экземпляр вашего сер- верного приложения (стиль ciSinglelnstance). Этот подход требует больше ресурсов и больше подключений к SQL-серверу (а значит, большего количества лицензий). Существует альтернативный способ, основанный на использовании встроен- ного в DataSnap механизма накопления объектов (Object Pooling). Все, что вам нужно для того, чтобы воспользоваться этим механизмом, это добавить в переоп- ределенный метод U pdate Registry обращение к вызову RegisterPooled. Механизм на- копления объектов позволяет вам распределить несколько объектов среднего зве- на между множеством клиентов. При этом количество клиентов может значительно превышать количество объектов. Механизм накопления встроен в СОМ+, однако технология DataSnap делает его доступным для совместного использования с со- единениями HTTP и сокетными соединениями. Пользователи клиентских компьютеров тратят значительную долю времени на прочтение данных и ввод с клавиатуры изменений и обновлений. Лишь незначи- тельная доля времени уходит на обращение к методам сервера. Если клиент не обращается к методу объекта среднего звена, этот же самый удаленный модуль данных может быть использован для обслуживания другого клиента. Инфраструк- тура DataSnap не зависит от текущего состояния соединения, поэтому любой зап- рос расценивается на компьютере среднего звена как совершенно новая не завися- щая от предыстории операция, даже если сервер специально предназначен для обслуживания только одного клиента. Настройка пакетов данных Вы можете включить в состав пакета, формируемого интерфейсом lAppServer, лю- бые удобные для вас данные. Существует несколько способов сделать это. Проще всего выполнить обработку события OnGetDataSetProperties Обработчик этого со- бытия принимает параметр Sender, который указывает на набор данных, из которо- го извлекаются данные, а также параметр Properties, который является массивом значений типа OleVariant, в котором вы можете разместить дополнительную ин- формацию. Вам потребуется определить один массив вариантов для каждого до- полнительного свойства и включить имя дополнительного свойства, его значение, а также информацию о том, хотите ли вы, чтобы данные вернулись на сервер вме- сте с дельта-пакетом (параметр IncludelnDelta). Конечно же, вы можете передать свойства связанного компонента набора дан- ных, однако помимо этого вы можете передать любое другое значение. Например, в программе AppSPlus я передаю клиенту отметку времени, в которое был выпол- нен запрос, а также параметры запроса: procedure TappServerPlus ProviderQueryGetDataSetPropertiest Sender TObject. DataSet TDataSet. out Properties OleVariant) 0768
Что далее? 769 begin Properties = VarArrayCreate([0.1], varVariant), PropertieslO] - VarArrayOf(['Time’. Now. True]). Propertiesll] = VarArrayOfd'Param'. Query ParamstO] AsString. False]), end. На стороне клиента компонент ClientDataSet поддерживает метод GetOptional- Parameter, который позволяет извлечь значение дополнительного свойства с ука- занным именем. Помимо этого компонент ClientDataSet поддерживает метод SetOpti- onalParameter, который позволяет добавить дополнительные свойства к набору данных. Эти значения будут сохранены на диске (при использовании модели Briefcase) и со временем будут переданы среднему звену (когда переменной Inclu- delnDelta будет присвоено значение True). Вот пример извлечения набора данных: Caption - 'Data sent at ' + TimeToStr (TDateTime ( cds Query GetOptionalParam('Time'))) Labell Caption = 'Param ' + cdsQuery GetOptionalParam('Param') Эффект выполнения этого кода можно видеть на рис. 16.5, Альтернативный и более мощный подход к настройке пакета данных, передаваемых клиенту, заклю- чается в использовании события OnGetData компонента провайдера. Обработчик этого события принимает исходящий пакет данных в виде клиентского набора дан- ных. Используя методы этого набора данных, вы можете отредактировать данные перед тем, как они будут переданы на сторону клиента. Например, вы можете зако- дировать некоторые данные или отфильтровать некоторые ненужные записи. Что далее? Компания Borland впервые включила поддержку многозвенной технологии в со- став пакета Delphi 3. После этого от версии к версии встроенные в Delphi механиз- мы поддержки многозвенной архитектуры постоянно совершенствовались, их воз- можности расширялись. В Delphi 6 помимо прочих улучшений и смены названия на DataSnap была также добавлена поддержка SOAP. Об этом протоколе будет подробнее рассказано в главе 23. С другой стороны, в Delphi 7 была прекращена поддержка CORBA. Вместе с тем с вступлением в силу новой лицензионной схемы (в рамках новой схемы фактически разрешается свободное распространение продуктов на основе DataSnap) технология DataSnap в составе пакета Delphi 7 стала еще более привле- кательной для разработчиков. Поддержка SOAP существенно расширяет область применения этой технологии, однако сокетные соединения также обеспечивают хороший баланс между эффективностью передачи данных и простотой внедрения. К обсуждению SOAP и других технологий обмена данными через сеть мы еще вернемся в последующих главах. Мы подробно изучим сокеты, программирова- ние Интернета и SOAP. А сейчас вновь обратимся к изучению вопросов, связанных с разработкой баз данных. В следующей главе мы рассмотрим вопросы разработки собственных компонентов наборов данных и элементов управления, работающих сданными. 0769
*| Разработка I / компонентов, работающих с данными В главе 9 мы подробно рассмотрели вопросы, связанные с разработкой компонен- тов Delphi. Теперь, когда мы обсудили программирование баз данных, мы можем вернуться к этому вопросу и сосредоточить внимание на разработке компонентов, специально предназначенных для работы с данными. Существует две категории таких компонентов: элементы управления для рабо- ты с данными и компоненты наборов данных. Компоненты первой категории пред- назначены для отображения данных, содержащихся в поле, записи или таблице для пользователей программы. Компоненты второй категории предназначены для чтения данных из базы данных или другого источника. В этой главе я расскажу о разработке компонентов обеих категорий. Здесь будут рассмотрены следующие вопросы: О элементы управления для работы с данными: класс TDataLink; О элементы управления для работы с отдельным полем; О компоненты TrackBar и ProgressBar; О элементы управления для работы с отдельной записью; О компонент просмотра записей; О построение собственных наборов данных; О сохранение набора данных в локальном потоке. Связь с данными Когда вы пишете на Delphi программу взаимодействия с базой данных, вы соедп няете несколько элементов управления, предназначенных для работы с данными, с компонентом DataSource, а затем подключаете компонент DataSource к набору ДаН ных. Соединение между элементом управления и компонентом DataSource называ ется data link (связь с данными). Эта связь представлена в виде объекта класса TDataLink или производного класса. Объект этого класса создается и управляет 0770
Связьсданными 771 элементом управления, который использует его в качестве соединения с источни- ком данных. Итак, чтобы приспособить элемент управления для работы с данны- ми, необходимо добавить к нему объект связи с данными и сделать доступными для внешнего мира некоторые из его свойств, такие как DataSource и Data Field. Среда Delphi использует объекты DataSource и DataLink для двусторонней ком- муникации. Набор данных использует соединение для того, чтобы оповестить свя- занные с ним элементы управления о поступлении новых данных (например, ког- да набор данных активируется или когда изменяется текущая запись). Элементы управления используют соединение для того, чтобы получить текущее значение поля или обновить поле и оповестить набор данных об этом событии. Взаимоотношения между всеми этими компонентами усложнены тем фактом, что некоторые из соединений могут быть типа «один ко многим». Например, вы можете соединить несколько источников данных с одним и тем же набором дан- ных. Кроме того, вы можете соединить несколько элементов управления с одним и тем же источником данных. Класс TDataLink В данной главе мы будет иметь дело с классом TDataLink и производными от него классами. Все эти классы определены в модуле DB. Этот класс обладает набором защищенных виртуальных методов, которые используются подобно тому, как ис- пользуются события. Каждый из этих методов фактически ничего не делает. Лю- бой из этих методов можно переопределить в подклассе для того, чтобы пере- хватить пользовательскую операцию или какое-либо другое событие, связанное с источником данных. Вот перечень методов, извлеченный из исходного кода класса; type TDataLink = class(TPersistent) protected procedure ActiveChanged: virtual: procedure CheckBrowseMode: virtual; procedure DataSetChanged; vi rtual; procedure DataSetScrolled(D1stance: Integer): virtual: procedure FocusControl(Field: TfieldRef): virtual: procedure EditlngChanged: virtual: procedure LayoutChanged: virtual: procedure RecordChanged(F1eld: TField); virtual; procedure UpdateData: virtual: Все эти виртуальные методы вызываются закрытым методом DataEvent, кото- рый является чем-то вроде точки входа в класс источника данных. Обращение к этому методу осуществляется в результате генерации нескольких событий, свя- занных с данными (см. перечисление TDataEvent). События исходят из набора дан- ных, полей или источника данных и в основном применяются к набору данных. Метод DataEvent компонента набора данных передает события подключенным к нему источникам данных. Каждый из источников обращается к методу Notify- DataLinks, чтобы передать событие каждому из подключенных объектов DataLink, После этого источник даных генерирует собственное событие OnDataChange или OnUpdateData. 0771
772 Глава 17. Разработка компонентов, работающих сданными Классы, производные от TDataLink Класс TDataLink формально не является абстрактным классом, однако этот класс почти никогда не используется напрямую. Если вы хотите создать собственный элемент управления для работы с данными, вы должны воспользоваться одним из классов, производных от TDataLink, или разработать собственный класс, являющий- ся потомком TDataLink. Наиболее важным классом, производным от TDataLink, яв- ляется класс TFieldDataLink, который используется элементами управления, ассо- циирующимися с единственным полем набора данных. Большинство элементов управления, работающих с данными, входят в состав именно этой категории. Класс TFieldDataLink решает большинство проблем, с которыми имеют дело элементы управления этой категории. Для всех элементов управления, ассоциированных с таблицами или отдельны- ми записями набора данных, определяются специализированные подклассы класса TDataLink. Позднее я продемонстрирую вам процедуру создания такого подкласса. Класс TFieldDataLink обладает списком событий, соответствующих переопределяе- мым им виртуальным методам базового класса. Благодаря этому реконфигурирова- ние класса упрощается, так как вместо того, чтобы создавать новый производный класс, вы можете написать необходимые обработчики событий. Далее приводится пример переопределенного метода, который генерирует подходящее событие: procedure TF1eldDataLInk.ActiveChanged: begin UpdateField: if Asslgned(FonActiveChange) then FonActiveChange(Self): end: В составе класса TFieldDataLink присутствуют также свойства Field и FieldName, которые позволяют вам подключить элемент управления к некоторому полю на- бора данных. В объекте связи хранится также ссылка на текущий визуальный ком- понент — для этого используется свойство Control. Разработка элементов управления, ассоциируемых с полем набора данных Теперь, когда вы знакомы с основными принципами функционирования классов семейства TDataLink, давайте перейдем к практической демонстрации разработки подобного элемента управления. Вначале я рассмотрю примеры разработки вер- сий компонентов ProgressBar и TrackBar, специально предназначенных для работы с данными. Первый компонент — ProgressBar — можно использовать для того, что- бы отобразить числовое значение, например количество процентов, в составе ви- зуального пользовательского интерфейса. Второй компонент — TrackBar — может использоваться для изменения численного значения. ПРИМЕЧАНИЕ---------------------------------------------------------------------"7 Код компонентов, рассматриваемых в данной главе, содержится в папке MdDataPack, в которой можно обнаружить также одноименный пакет, предназначенный для установки этих компонент В других папках можно обнаружить программы, которые используют эти компоненты.___ 0772
Разработка элементов управления, ассоциируемых с полем набора данных 773 Компонент ProgressBar — только для чтения Версия компонента ProgressBar, предназначенная для работы с данными, является весьма простой формой элемента управления данными, так как этот компонент предназначен только для чтения. Этот компонент является производным от обыч- ного стандартного компонента ProgressBar и добавляет к нему несколько свойств объекта DataLink, который в нем инкапсулирован: type TmdDbProgress = class(TProgressBar) private FDataLInk: TFieldDataLink; function GetDataFleld: string; procedure SetDataFleld (Value: string); function GetDataSource: TDataSource; procedure SetDataSource (Value: TDataSource); function GetField: TField; protected // обработчик события DataLink procedure DataChange (Sender: TObject); public constructor Create (Aowner: Tcomponent); override: destructor Destroy; override; property Field; TField read GetField; published property DataField: string read GetDataFleld write SetDataFleld; property DataSource: TDataSource read GetDataSource write SetDataSource; end; Как и любой другой элемент управления, подключающийся к единственному полю, данный элемент управления публикует два свойства: DataSource и DataField. Чтобы обеспечить функционирование этих свойств, вам не потребуется писать слишком сложный код, достаточно просто экспортировать свойства из внутренне- го объекта DataLink следующим образом: function TMdDbProgress.GetDataFleld: string; begin Result := FDataLInk.FieldName; 1 end; t procedure TMdDbProgress.SetDataFleld (Value: string); begin FDataLInk.FieldName := Value; end; function TMdDbProgress.GetDataSource: TDataSource; begin Result ;= FDataLInk.DataSource: end: procedure TMdDbProgress.SetDataSource (Value: TDataSource): begin FDataLInk.DataSource :- Value; end; function TMdDbProgress.GetField: TField: 0773
774 Глава 17. Разработка компонентов, работающих с данными begin Result = FDataLink Field end Конечно же, чтобы этот компонент мог нормально функционировать, вы долж- ны создать объект DataLink в момент создания компонента и уничтожить объект DataLink в момент уничтожения компонента: constructor TMdDbProgress Create (AOwner TComponent) begin inherited Create (AOwner) FDataLink = TFieldOataLink Create. FDataLink Control = Self FDataLink OnDataChange = DataChange: end destructor TMdDbProgress Destroy begin FDataLink Free FDataLink = ml inherited Destroy. end Обратите внимание, что конструктор делает один из своих методов обработчи- ком события для объекта DataLink. Именно в этом методе располагается наиболее важный код компонента. Каждый раз, когда меняются данные, внешний вид ком- понента ProgressBar также меняется, чтобы соответствовать текущему значению ассоциированного поля: procedure TMdDbProgress DataChange (Sender TObject). begin if FDataLink Field is TnumericField then Position = FDataLink Field Aslnteger else Position = Min end У Data-aware ProgrestBar Oerno OrderNo Pat№ Sty 10 11 Discount Рис. 17.1. Версия компонента ProgressBar, предназначенная для работы с данными, в составе примера DbProgr 0774
Разработка элементов управления, ассоциируемых с полем набора данных 775 Стандартное правило функционирования элементов управления VCL указы- вает, что в случае, если значение поля некорректно, никакого сообщения об ошиб- ке не отображается, вместо этого элемент управления просто отключает вывод. Проверку типа поля можно выполнять также внутри метода SetDataField. На рис. 17.1 показан вывод программы DbProgr. Для отображения значения поля Qty (Quantity — количество) используется два элемента управления: метка и Pro- gressBar. Благодаря этой визуальной подсказке вы можете быстро перемещаться по таблице и обращать внимание на записи, соответствующие большому количе- ству товаров. Преимущество использования этого компонента заключается в том, что приложение фактически не содержит кода, так как весь важный код содержит- ся в модуле MdProgr, в котором определяется компонент. Итак, вы убедились, что разработать элемент управления для работы с данными, предназначенный только для чтения, совсем не сложно. Однако использование тако- го компонента внутри контейнера DBCtrIGrid — чрезвычайно непростая задача. ПРИМЕЧАНИЕ ----------------------------------------------------------- Если вы помните обсуждение метода Notification в главе 9, должно быть, вас заинтересует, что произойдет, если будет уничтожен источник данных, на который ссылается элемент управления, предназначенный для работы с данными. Спешу вас обрадовать, что источник данных обладает деструктором, который удаляет этот компонент из всех связанных с ним объектов DataLink. Таким образом, нет необходимости использовать метод Notification совместно с элементами управления, предназначенными для работы с данными. Однако во многих книгах и статьях утверждается обрат- ное, кроме того, в составе VCL присутствует большой объем излишнего связанного с этим кода. Репликация элементов управления работы с данными Расширение элемента управления работы с данными таким образом, чтобы он мог быть использован внутри компонента DBCtrIGrid, — это непростая и плохо документированная проблема В модуле MdRepPr пакета MdDataPack присутствует реплицируемая версия компонента ProgressBar. Пример исполь- зования этого компонента содержится в папке RepProg, а в прилагаемом к нему HTML-файле содержится описание процедуры разработки этого при- мера. Компонент DBCtrIGrid обладает специфическим поведением- он отобра- жает на экране несколько версий одного и того же физического компонента управления. Элемент управления может подключаться к буферу данных, от- личающемуся от буфера текущей записи, в результате операции отрисовки компонента выполняются в других областях экрана. Если говорить кратко, чтобы войти в состав DBCtrIGrid, элемент управле- ния должен обладать стилем csReplicable. Этот флаг указывает на то, что ваш компонент поддерживается управляющей сеткой DBCtrIGrid. Во-первых, ком- понент обязан реагировать на сообщение cm_GetDataLink и в ответ на него возвращать объект связи с данными. В результате сетка DBCtrIGrid сможет воспользоваться этим объектом и изменить его. Во-вторых, ваш элемент управления должен поддерживать метод Paint, в результате он может ото- бразить свой вывод в рамках указанного объекта Canvas. Объект Canvas пере- дается в качестве параметра сообщения wm_Paint, если в свойстве Controlstate установлен флаг csPaintCopy. - продолжение & 0775
776 Глава 17. Разработка компонентов, работающих еда иными Репликация элементов управления работы с данными (продолжение) Код соответствующего примера достаточно сложен, а компонент DBCtrLGrid используется не так уж часто, поэтому я решил не включать в книгу подроб- ного рассмотрения всех связанных с этим деталей. Вы можете самостоятельно изучить код, взглянув в комплект исходного кода, прилагаемый к данной книге. Вот вывод программы, использующей данный компонент: RepProgr 15 16 17 18 8 11 13 7 6 26 20 30 5 7 8 6 3 10 €52 50 €40 00 €45 00 €37 50 €50 00 DINERS DINERS DINERS DINERS DINERS 2563350385642037^ 6146617034656232 4818531612351817 2513715852358158 0521773736155304 w| а>>! aiaaiiiiiaiai ант»» iMUiinia Hl MiMMiMaaaaaaai Компонент TrackBar, поддерживающий чтение-запись Теперь давайте рассмотрим разработку компонента, который позволяет не только читать, но и модифицировать данные, содержащиеся в БД. Общая структура ком- понента данного типа мало чем отличается от только что рассмотренного компо- нента, предназначенного только для чтения. Однако новый компонент включает в себя несколько дополнительных составляющих. Когда пользователь начинает вза- имодействовать с компонентом, код должен перевести набор данных в режим ре- дактирования, а затем оповестить набор данных о том, что данные изменились. После этого набор данных воспользуется обработчиком события FieldDataLink для того, чтобы запросить модифицированное значение. Чтобы создать элемент управления работы с данными, который модифицирует данные, я расширил элемент управления TrackBar. Это не самый простой пример, однако он демонстрирует несколько важных методик. Вот определение класса компонента (изъятое из модуля MdTrack пакета MdDataPack): 0776
Разработка элементов управления, ассоциируемых с полем набора данных 777 type TmdDbTrack = class(TTrackBar) private FDataLink: TFieldDataLink; function GetDataField: string: procedure SetDataField (Value: string): function GetDataSource: TDataSource: procedure SetDataSource (Value: TDataSource); function GetField: TField: procedure CNHScrolKvar Message: TWMHScroll): message CN_HSCROLL: procedure CNVScroll(var Message: TWMVScroll): message CN_VSCROLL; procedure CMExittvar Message. TCMExit): message CMEXIT; protected // обработчики событий объекта DataLink procedure DataChange (Sender: TObject); procedure UpdateData (Sender: TObject); procedure ActiveChange (Sender: TObject); public constructor Create (AOwner TComponent), override; destructor Destroy; override: property Field: TField read GetField; published property DataField: string read GetDataField write SetDataField; property DataSource- TDataSource read GetDataSource write SetDataSource: end; В сравнении с рассмотренным ранее элементом управления, предназначенным только для чтения, этот класс обладает более сложной структурой, так как в его состав входят три обработчика сообщений, включая обработчики оповещений и д- ва новых обработчика событий объекта DataLink. Компонент настраивает три обра- ботчика событий в конструкторе, кроме того, в этом же конструкторе компонент отключает сам себя; constructor TMdDbTrack Create (AOwner: TComponent); begin inherited Create (AOwner); FDataLink = TFieldDataLink.Create: FDataLink.Control .= Self: FDataLink OnDataChange ;= DataChange: FDataLink.OnUpdateData := UpdateData; FDataLink OnActiveChange = ActiveChange: Enabled := False: end; Методы Get и Set, а также событие DataChange аналогичны тем, которые исполь- зуются в компоненте TMdDbProgress. Отличие заключается лишь в том, что когда источник данных или поле с данными меняются, компонент проверяет текущий статус, чтобы узнать, надо ли ему себя активизировать: procedure TMdDbTrack.SetDataSource (Value: TDataSource). begin FDataLink.DataSource .= Value: Enabled .= FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.Readonly. end. Этот код проверяет три условия: объект связи с данными должен быть активен, он должен ссылаться на реально существующее поле и это поле не должно быть полем только для чтения. 0777
1 778 Глава 17. Разработка компонентов, работающих с данными Когда пользователь меняет поле, компонент должен проверить, является ли имя роля корректным. Чтобы протестировать это условие, компонент использует блок try/finally: procedure TMdDbTrack SetDataFleld (Value: string): begin try FDataLInk.FieldName := Value. finally Enabled := FDataLink.Active and (FDataLink.Field <> nil) and not FDataLink.Field.Readonly: end: end: Элемент управления выполняет точно такой же тест, когда активируется или деактивируется набор данных: procedure TMdDbTrack.ActiveChange (Sender: TObject): begin Enabled = FDataLink Active and (FDataLink Field nil) and not FDataLink.Field Readonly: end: Наибольший интерес представляет часть кода, имеющая отношение к пользо- вательскому интерфейсу. Когда пользователь начинает двигать «рукоятку» инди- катора, компонент переводит набор данных в режим редактирования, позволяет базовому классу обновить положение «рукоятки» и оповещает объект связи с данными (а следовательно, и источник данных) о том, что данные измени- лись. Вот код: procedure TMdDbTrack.CNHScroll(var Message- TWMHScrol1): begin // переходим в режим редактирования FDataLink.Edit; // обновление данных inherited. // оповещаем систему FDataLink Modified; end: procedure TMdDbTrack.CNVScroll(var Message: TWMVScroll): begin // переходим в режим редактирования FDataLink Edit: // обновление данных inherited: // оповещаем систему FDataLink Modified: end: Когда набор данных нуждается в получении новых данных (например, чтобы выполнить операцию Post), он запрашивает их из компонента через событие OnUp- dateData компонента TFieldDataLink: procedure TMdDbTrack.UpdateData (Sender. TObject): begin if FDataLink.Field is TNumericField then FDataLink.Field.Aslnteger := Position; end. 0778
Создание собственного объекта DataLink 779 При выполнении условий компонент обновляет данные в соответствующем поле таблицы. Наконец, когда компонент теряет фокус ввода, он должен форсировать обновление данных (если данные были изменены), так как в этом случае другие элементы управления, связанные с этими данными, будут отображать обновлен- ное значение, как только пользователь переместит фокус ввода на новое место. Если данные не менялись, компонент не выполняет обновления данных в таблице. Это стандартный код CMExit для компонентов VCL: procedure TMdDbTrack.CMExit(var Message. TCMExit): begin try FDataLink.UpdateRecord; except SetFocus: raise; end: inherited: end; Я написал демонстрационную программу для тестирования этого компонента. Рабочее окно этой программы показано на рис. 17.2. Программа DbTrack содержит флажок, который активирует и деактивирует таблицу. Кроме того, на форме про- граммы располагается несколько визуальных компонентов и пара кнопок, кото- рые можно использовать для подключения и отключения компонента TrackBar от поля, к которому он относится. Я добавил эти кнопки па форму для того, чтобы можно было протестировать активацию и деактивацию компонента TrackBar. Data-aware TrackBar Dema ItemNa OrderNo // y / D»c:= |“ ; 4 ] 1005 '' ( 7612 Рис. 17.2. Пример DbTrack демонстрирует использование элемента управления TrackBar. Используя этот элемент управления, вы можете изменять данные в таблице. Флажок и кнопки позволяют протестировать активацию и деактивацию компонентов Создание собственного объекта DataLink Элементы управления, разработанные мной в предыдущих разделах данной гла- вы, ассоциируются с некоторым конкретным полем набора данных. Именно по- этому для того, чтобы установить соединение с источником данных, я использую объект TFieldDataLink. Теперь давайте рассмотрим процедуру разработки элемента 0779
780 Глава 17. Разработка компонентов, работающих с данными управления, который работает с набором данных как с единым целым. Это будет компонент, позволяющий просматривать целые записи набора данных. В Delphi присутствует компонент сетки (DBGrid), который отображает на экра- не несколько полей нескольких записей одновременно. Элемент управления, ко- торый я планирую разработать, будет отображать все поля текущей записи, ис- пользуя сетку специального вида. Изучив следующий пример, вы узнаете, как можно сформировать специальный элемент управления сетки и использовать его в комбинации со специальным объектом связи с данными. Компонент просмотра записей В Delphi нет стандартных элементов управления, которые позволяли бы манипу- лировать несколькими полями единственной записи и при этом не отображали бы других записей таблицы. Элементами управления, отображающими несколько полей одной таблицы, являются только компоненты DBGrid и DbCtrlGrid. В общем случае эти компоненты предназначены для отображения нескольких полей не- скольких записей одновременно. Компонент просмотра записей, который я буду рассматривать в данном разде- ле, основан на сетке из двух столбцов. В первом столбце отображаются имена по- лей, а во втором столбце отображаются соответствующие значения. Количество строк в сетке соответствует количеству полей. Если количество полей не помеща- ется в видимую область, к сетке добавляется полоса прокрутки. Для построения этого компонента вам потребуется связь с данными — класс, соединенный с компонентом просмотра записей и объявленный в разделе реали- зации данного модуля. Этот же подход используется в VCL для некоторых специа- лизированных объектов связей с данными. Вот определение нового класса: type TMdRecordLink = class (TDataLink) private Rview TMdRecordView publ i c constructor Create (View TMdRecordView) procedure ActiveChanged override procedure RecordChanged (Field TField) override. end Как можно видеть, класс переопределяет методы, имеющие отношение к ос- новному событию, — в данном случае активации и изменению данных (или запи- си). Есть еще один способ: вы можете экспортировать события, а затем сделать так, чтобы компонент выполнил их обработку, как это делает класс TFieldDataLink. Конструктор принимает в качестве параметра компонент, ассоциированный с объектом связи: constructor TMdRecordLink Create (View TMdRecordView). begin inherited Create. Rview = View end. После того как ссылка на ассоциированный компонент сохранена в одной из внутренних переменных объекта, другие методы класса могут напрямую работать с этим компонентом: 0780
Создание собственного объекта DataLink 781 procedure TMdRecordLink.Act!veChanged: var I Integer, begin // присваиваем количество строк Rview RowCount = DataSet.FieldCount; // перерисовать все . . . Rview Invalidate. end. Код RecordLink весьма прост. Большинство сложностей в этом примере связано с использованием сетки. Чтобы не иметь дела с бесполезными свойствами, я фор- мирую класс сетки как производный от класса TCustomGrid. Этот класс содержит в себе большую часть кода для сетки, однако большинство его свойств, событий и методов определены с модификатором доступа protected. По этой причине класс TMdRecordView обладает достаточно длинным объявлением — он должен опублико- вать многие из свойств, существующих в базовом классе. Вот фрагмент этого объяв- ления (за исключением свойств базового класса): type TMdRecordView = cl ass(TCustomGrid) pri vate // поддержка работы с данными FDataLink TDataLink function GetDataSource TDataSource, procedure SetDataSource (Value TDataSource): protected // переопределенные методы TCustomGrid procedure DrawCell (ACol ARow Longint. ARect- TRect. AState TgridDrawState) override. procedure ColWidthsChanged override procedure RowHeightsChanged, override. publ1c constructor Create (AOwner TComponent). override destructor Destroy override. procedure SetBounds (ALeft. ATop, AWidth AHeight Integer), override: // публичные свойства родительского класса (пропущены . ) published // свойства связанные с работой с данными property DataSource TDataSource read GetDataSource write SetDataSource. // опубликованные свойства родительского класса (пропущены .) end Помимо повторного определения свойств (чтобы сделать их опубликованны- ми) компонент определяет объект связи с данными и свойство DataSource. В этом компоненте нет свойства DataField, так как компонент ассоциируется с целой запи- сью. Конструктор компонента очень важен. В конструкторе настраиваются значе- ния многих неопубликованных свойств, включая параметры сетки: constructor TMdRecordView Create (AOwner TComponent), begin inherited Create (AOwner) FDataLink = TMdRecordLink Create(self). II настраиваем количество ячеек и фиксированных ячеек RowCount =2 //по умолчанию Col Count = 2 FixedCols = 1. 0781
782 Глава 17. Разработка компонентов, работающих с данными FixedRows = 0. Options = EgoFixedVertLine goFixedHorzLine. goVertLine goHorzLine, goRowSizing], DefaultDrawing = False. ScrollBars = ssVertical, FSaveCellExtents = False end. Сетка состоит из двух столбцов (один из которых фиксирован), и в ней нет ни одной фиксированной строки. Фиксированный столбец используется для измене- ния размера каждой из строк. К сожалению, пользователь не может перетаскивать фиксированные строки для того, чтобы изменить размер столбцов, так как фикси- рованные элементы не меняют своего размера, а в состав нашей сетки входит один фиксированный столбец. ПРИМЕЧАНИЕ ------------------------------------------------------------------------- Можно было бы добавить дополнительный пустой столбец, как в элементе управления DBGrid. Все- таки я предпочитаю мой вариант реализации. Для изменения размера столбцов я использую альтернативный подход. Размер первого столбца (в котором содержатся имена полей) может быть изменен либо при помощи программного кода, либо визуально на этапе проектирования, а раз- мер второго столбца (в котором хранятся значения полей) изменяется таким обра- зом, чтобы занять оставшуюся площадь компонента: procedure TMdRecordView SetBound (ALeft. ATop AWidth AHeight Integer), begin inherited. ColWidth [1] = ClientWidth - ColWidths[OJ end Изменение размера выполняется либо когда изменяется одна из колонок, либо когда изменяется размер всего компонента. Благодаря этому коду свойство Default- ColWidth компонента становится равно фиксированному размеру первого столбца. После того как все настроено, переопределяется ключевой метод компонента, DrawCell. Исходный код этого метода показан в листинге 17.1. В этом методе эле- мент управления отображает информацию о полях и их значениях. Необходимо отобразить три вещи. Если объект связи с данными не подключен к источнику данных, сетка отображает знак пустого элемента (//). При отображении первого столбца компонент показывает значение свойства DisplayName поля. Это же значе- ние используется элементом DBGrid для заголовков столбцов сетки. При отображе- нии второго столбца компонент обращается к текстовому представлению значе- ния поля, которое содержится в свойстве DisplayText (для полей Мето используется метод AsString). Листинг 17.1. Метод DrawCell компонента RecordView procedure TMdRecordView DrawCell(ACol. ARow Longint. ARect TRect, AState TGridDrawState). var Text string CurrField TField Bmp TBitmap. begin 0782
Создание собственного объекта DataLink 783 CurrField = nil: Text = // значение по умолчанию И рисуем задний фон if (ACol = 0) then Canvas Brush Color = FixedColor else Canvas Brush Color = Color. Canvas FillRect (ARect) // оставляем небольшую рамку InflateRect (ARect, -2, -2), if (FDataLink DataSource <> ml) and FDataLink Active then begin CurrField = FDataLink DataSet FieldsEARow] if ACol = 0 then Text = CurrField DisplayNAme else if CurrField is TmemoField then Text = TmemoField (CurrField) AsString else Text = CurrField DisplayText. end if (ACol = 1) and (CurrField is TgraphicField) then begin Bmp = Tbitmap Create try Bmp Assign (CurrField), Canvas StretchDraw (ARect. Bmp). finally Bmp Free. end end else if (ACol = 1) and (CurrField is TMEmoField) then begin DrawText (Canvas Handle Pchar (Text). Length (Text) ARect, dt_WordBreak or dt_NoPrefix) end else // отображаем вертикально отцентрованную строку DrawText (Canvas Handle PChar (Text) Length (Text). ARect. dt_vcenter or dt_SingleLine or dt_NoPrefix). if gdFocused in AState then Canvas DrawFocusRect (ARect). end. Во второй части метода выполняется обработка графических полей и полей Мето. Если поле является полем TMemoField, при обращении к функции DrawText флаг dt_SingleLine не используется. Вместо этого добавляется флаг dt_WordBreak, благодаря которому выполняется перетекание текста со строки на строку. Для гра- фических полей используется совершенно иной подход. Изображение, хранящее- ся в поле, переносится во временный объект типа TBitmap, а затем растягивается таким образом, чтобы заполнить пространство ячейки. Обратите внимание, что компонент присваивает свойству DefaultDrawing значе- ние False, благодаря этому он берет на себя ответственность за перерисовку фона и прямоугольника, обозначающего фокус ввода. Кроме того, выполняется обра- щение к функции InflateRect, из-за чего между границей ячейки и ее содержимым сохраняется небольшое пространство. Вывод текста выполняется при помощи еще одной функции Windows API под названием DrawText. Эта функция центрует текст вертикально в ячейке. 0783
784 Глава 17, Разработка компонентов, работающих с данными Код отображения компонента работает как во время функционирования про- граммы (рис. 17.3), так и на этапе проектирования. Возможно, вывод, осуществля- емый этим компонентом, не идеален, однако данный компонент может быть поле- зен во многих случаях. Вы можете воспользоваться им для того, чтобы отобразить данные одной записи таблицы, и при этом вам не надо разрабатывать специаль- ную форму с метками и множеством элементов управления, работающих с данны- ми. Важно помнить о том, что данный компонент позволяет только читать данные. Конечно же, вполне допустимо добавить в него возможность редактирования дан- ных (все необходимое уже присутствует в классе TCustomGrid). И все же, вместо того чтобы добавлять поддержку редактирования, я хочу продемонстрировать вам код, выполняющий отображение полей BLOB. * Record View ComportefA Ottma Species Но Category Angelfish Coiaewnjieihe Blue Angelfish Species Marne Pomacanthus nauarchus Length {cm] 30 Lengthjn 11 8110236220472 Moles Habitat is around boulders, caves, coral ledges and crevices in shallow waters Swims alone or in groups Giephic Рис. 17,3. Программа ViewGnd демонстрирует вывод, отображаемый компонентом Recordview Чтобы сделать данные более читаемыми, я решил сделать строки, относящиеся к полям BLOB, в два раза выше обычных текстовых строк. Эта операция выполня- ется в момент, когда активируется набор данных, подключенный к элементу управ- ления. Метод ActiveChanged также вызывается методами RowHeightsChanged, под- ключенными к свойству DefaultRowHeight базового класса: procedure TMdRecordLink ActiveChanged. var I Integer. begin // настройка количества строк Rview RowCount = DataSet FieldCount // в два раза увеличиваем высоту графических полей и полей Мелю for I = 0 to DataSet FieldCount - 1 do if DataSet Fields [I] is TBlobField then Rview RowHeights [I] = Rview DefaultRowHeight * 2, // заново перерисовываем все Rview Invalidate end 0784
Модернизация компонента DBGrid 785 Возникает незначительная проблема. В методе DefineProperties класс TCustomGrid сохраняет значения RowHeights и ColHeights. Вы можете отказаться от сохранения этих свойств в потоке путем переопределения метода. В переопределенном методе вы можете не обращаться к методу родительского класса с использованием ключе- вого слова inherited. Однако это не самая лучшая идея. Отключить сохранение свойств в потоке позволяет защищенное поле FSaveCeUExtents (именно это свой- ство я использую в коде компонента). Модернизация компонента DBGrid Помимо разработки совершенно новых специализированных элементов управле- ния для работы с данными программисты Delphi часто выполняют модернизацию стандартного компонента DBGrid. В данном разделе мы рассмотрим процесс модер- низации DBGrid. В рассматриваемом далее примере я решил добавить в DBGrid фун- кциональность, которую я использовал при разработке компонента RecordView. Говоря точнее, новый модернизированный компонент DBGrid отображает содержи- мое графических полей и полей Мето прямо в составе сетки. Чтобы достичь этого, необходимо сделать так, чтобы высоту строки в сетке можно было изменить, — благодаря этому появится дополнительное место для графики и большого количе- ства текста. Пример сетки на этапе проектирования показан на рис. 17.4. j MdDbGrid Demo Snapper BaDtstodes соп$р»сйит Wrasse Red Emperor Lutianut sebae Giant Maori Wrasse Cheinus unduletus Blue Angelfish Panacanthus nauarchus This is the largest of all the wrasse It is found m dense reef areas feeding on a wide variety of moSusks fishes sea Habitat is around boulders caves coral ledges and crevices in shdbw waters Swims alone or m groups Also known as the big spotted tnggerfrsh Inhabits outer reef areas and feeds upon crustaceans and mollusks by crushing them Called seaperch in Australia inhabits the areas around lagoon coral reefs and sandy bottoms Рис. 17.4. Пример использования компонента MdDbGnd на этапе проектирования. Обратите внимание на отображение содержимого полей с графикой и полей Мето Чтобы отобразить на экране текст Мето и графику, потребовалось всего лишь адаптировать код компонента RecordView. Основная проблема связана с изменени- ем высоты строк сетки. В результате я написал не так уж и много строк кода, одна- ко это стоило мне нескольких часов работы! В данном случае элемент управления не должен создавать специальный объект связи с данными, так как в его основе лежит компонент, который уже обладает достаточно сложным соединением с данными. В новом классе присутствует новое 0785
786 Глава 17. Разработка компонентов, работающих с данными свойство, в котором указывается количество строк текста для каждой строки. Кро- ме того, новый класс переопределяет несколько виртуальных методов: type TMdDbGnd = class(TdbGrid) private FLinesPerRow Integer procedure SetLinesPerRow (Value Integer). protected procedure DrawColumnCelKconst Rect TRect. DataCol- Integer. Column Tcolumn State TgridDrawState). override. procedure LayoutChanged. override. public constructor Create (AOwner TComponent). override. published property LinesPerRow Integer read FLinesPerRow write SetLinesPerRow default 1. end ПРИМЕЧАНИЕ ------------------------------------------------------------------------------ В отличие от универсального компонента сетки, с которым мы работали ранее, компонент DBGrid — это виртуальное представление набора данных, — не существует никакой связи между количеством строк, отображаемых на экране, и количеством строк в таблице набора данных. Если вы пролисты- ваете записи с данными, это вовсе не значит, что вы пролистываете строки сетки DBGrid, — строки сетки остаются в стационарном состоянии, а данные перемещаются между ними с одной строки на другую, создавая видимость пролистывания. По этой причине программа не пытается настроить высоту отдельной строки сетки таким образом, чтобы эта высота соответствовала отображаемым данным. Вместо этого высота всех строк настраивается при помощи специального многострочного значения. Значение по умолчанию присваивается полю FLinesPerRow в конструкторе. Вот код метода Set для данного свойства: procedure TmdObGrid SetLinesPerRow(Value Integer) begin if Value <> FLinesPerRow then begin FLinesPerRow = Value LayoutChanged end end Побочный эффект изменения количества строк заключается в обращении к виртуальному методу LayoutChanged. Система обращается к этому методу в момент, когда один из многочисленных параметров вывода изменяет свое значение. В коде метода вначале происходит обращение к методу родительского класса (inherited), затем выполняется настройка высоты каждой из строк. За основу принята та же самая формула, которая использовалась в классе TCustomDBGrid: высота текста вы- числяется с использованием простого слова Wg, написанного с использованием текущего шрифта (это слово используется потому, что в его состав входит одно- временно заглавная буква и строчная буква с нижним хвостиком). Вот соответ- ствующий код: procedure TMdDbGrid LayOutChanged. var PixelsPerRow PixelsTitle. I Integer 0786
Модернизация компонента DBGrid 787 begin inherited LayOutChanged. Canvas Font = Font PixelsPerRow = Canvas TextHeight('Wg') + 3. If dgRowLines in Options then Inc (PixelsPerRow GridLinesWidth). Canvas Font = TitleFont PixelsTitle = Canvas TextHeight('Wg') + 4: if dgRowLines in Options then Inc (PixelsTitle GridLineWidth) 11 настраиваем количество строк RowCount = 1 + (Height - PixelsTitle) div (PixelsPerRow * FLinesPerRow): // настраиваем высоту каждого ряда DefaultRowHeight = PixelsPerRow * FLinesPerRow. RowHeights [0] = PixelsTitle for I =1 to RowCount - 1 do RowHeights [I] = PixelsPerRow * FLinesPerRow // посылаем сообщение WM_SIZE. чтобы базовый компонент заново И пересчитал параметры видимых строк внутри метода UpdatesRowCount PostMessage (Handle. WM_SIZE. 0. MakeLong(Width. Height)), end. ВНИМАНИЕ ---------------------------------------------------------------------------------- Свойства Font и TitleFont содержат значения no умолчанию для сетки. Эти значения можно пере- определить при помощи соответствующих свойств отдельных объектов столбцов DBGrid. Данный компонент игнорирует эти параметры. Сложнее всего было написать последние несколько выражений этого метода. Можно настроить свойство DefaultRowHeight, однако в этом случае строка с заго- ловками, скорее всего, будет слишком высокой. Вначале я попытался настроить DefaultRowHeight, а затем — высоту первой строки, однако при использовании этого подхода усложняется код, используемый для вычисления количества видимых строк в сетке (свойство VisibleRowCount, предназначенное только для чтения). Если вы укажете количество строк (чтобы избежать скрытия строк за нижней границей сетки), базовый класс будет продолжать заново вычислять их параметры. Далее приводится код, используемый для отображения данных. Этот код позаимствован из рассмотренного ранее компонента Recordview и слегка адаптирован для исполь- зования в новом компоненте сетки: procedure TMdDbGrid DrawColumnCel1 (const Rect TRect DataCol Integer. Column TColumn State TgridDrawState). var Bmp TBitmap OutRect TRect. begin if FLinesPerRow = 1 then inherited DrawColumnCel1(Rect. DataCol. Column. State) else begin 0787
788 Глава 17. Разработка компонентов, работающих с данными // очищаем пространство Canvas.FillRect (Rect): // копируем прямоугольник OutRect := Rect; // ограничиваем вывод InflateRect (OutRect. -2, -2): // выводим данные, содержащиеся в поле if Column.Field is TgraphlcFleld then begin Bmp := TBitmap.Create: try Bmp.Assign (Column.Field): Canvas.StretchDraw (OutRect. Bmp); finally Bmp.Free: end: end else if Column.Field is TMemoField then begin DrawText (Canvas.Handle. PChar (Column.Field.AsString), Length (Column.Field.AsString). OutRect, dt_WordBreak or dt_NoPrefix) end else // отображаем вертикально отцентрованную строку текста DrawText (Canvas.Handle, PChar (Column.Field.DisplayText). Length (Column.Field.DisplayText). OutRect. dt_vcenter or dt_S1ngleL1ne or dt_NoPref1x): end: end: Изучив код, можно заметить, что когда пользователь отображает единствен- ную строку сетки, сетка использует стандартный механизм отображения, при этом поля Мето и графические поля не отображаются. Как только пользователь увеличи- вает счетчик отображаемых строк, процедура отображения данных усложняется. Чтобы посмотреть этот код в действии, запустите пример GridDemo. Эта про- грамма обладает двумя кнопками, которые можно использовать для увеличения и уменьшения высоты строк сетки. Еще две кнопки позволяют изменить шрифт. Это важный тест, так как высота каждой ячейки в пикселах равна высоте шрифта, умноженной на количество строк. Разработка компонентов наборов данных Когда я рассказывал вам о классе TDataSet и различных семействах компонентов наборов данных (см. главу 13), я упомянул о возможности разработки своего соб- ственного класса набора данных. Пришло время рассмотреть эту возможность под- робнее. Зачем нужно разрабатывать собственный компонент набора данных? Это может потребоваться в ситуации, когда вы намерены обеспечить доступ к некото- рому источнику данных, не устанавливая при этом на стороне клиента лишних библиотек и специального программного обеспечения. Самостоятельная разработка компонента набора данных является одной из наи- более сложных задач для разработчика компонентов Delphi. Для ее решения ис- пользуется кодирование на низком уровне с применением множества указателей. 0788
Разработка компонентов наборов данных 789 Компания Borland не выпустила какой-либо официальной документации, описы- вающей процедуру создания собственного набора данных. Если вы не обладаете достаточным опытом программирования в среде Delphi, возможно, будет лучше, если вы пропустите остаток этой главы и вернетесь к изучению этого материала позднее, когда у вас появится опыт. TDataSet — абстрактный класс, который объявляет несколько виртуальных аб- страктных методов. Каждый подкласс TDataSet должен переопределить все эти ме- тоды. Прежде чем приступать к самостоятельной разработке своего набора данных, давайте рассмотрим некоторые технические элементы класса TDataSet. Прежде все- го, хочу обратить ваше внимание на механизм буферизации записей. Класс под- держивает список буферов, в которых хранятся значения различных записей. В буферах, прежде всего, хранятся данные, однако помимо этого в них содержится также разного рода служебная информация, используемая для управления запи- сями. Буферы не обладают заранее предопределенной структурой. Любой набор данных должен выделять буферы, заполнять их информацией и уничтожать их. Набор данных должен также копировать данные из буферов в различные поля на- бора данных и наоборот. Иными словами, на разрабатываемый вами набор данных возлагается ответственность за обработку этих буферов. Помимо обслуживания буферов компонент набора данных должен обеспечить навигацию между записями, управление закладками, определение структуры на- бора данных, создание подходящих полей данных. Класс TDataSet — это всего лишь скелет, который вы должны заполнить содержимым, то есть подходящим кодом. К счастью, большая часть кода разрабатывается в рамках стандартной структуры, которая используется всеми классами, производными orTDataSet. Ознакомившись с ключевыми идеями, вы сможете разработать множество наборов данных, исполь- зуя фактически один и тот же код. Чтобы упростить повторное использование кода при разработке наборов дан- ных, я собрал общие возможности, присутствующие в любом наборе данных, в класс TMdCustom DataSet. Однако я не намерен вначале рассматривать базовый класс, а за- тем — специализированную реализацию, так как это будет сложно понять. Вместо этого я подробно рассмотрю методы, необходимые как для универсальных, так и для специфических классов. Определение классов Вначале я хочу представить вам объявление двух классов, о которых пойдет речь в этом разделе главы. Первый класс (TMdCustomDataSet) — это универсальный са- мостоятельно разработанный набор данных. Он содержит в себе код, который можно использовать для самостоятельной разработки других наборов данных. Второй класс (TMdDataSetStream) — это специализированный компонент, работаю- щий с данными, которые содержатся в файловом потоке. Объявление обоих этих классов приводится в листинге 17.2. Помимо виртуальных методов они содержат в себе серию защищенных полей, используемых для управления буферами, отсле- живания текущей позиции, подсчета записей, а также поддержки другой функ- циональности. В самом начале листинга присутствует определение структуры (TMdRecInfo), в которой хранятся дополнительные служебные данные для каждой 0789
790 Глава 17. Разработка компонентов, работающих с данными записи, размещаемой в буфере. Компонент набора данных помещает эту информа- цию в каждом буфере записи сразу же за полезными данными. Листинг 17.2. Объявление классов TMdCustomDataSet и TMdDataSetStream //в нодуле MdDsCustom type EmdDataSetError = class (Exception); TmdRecInfo = record Bookmark: Longlnt; BookmarkFlag: TBookmarkFlag; end; PmdRecInfo = 'TMdRecInfo; TMdCustomDataSet = class(TDataSet) protected // состояние FIsTableOpen: Boolean; // данные, связанные с записью FRecordSize. // размер фактических данных FRecordBufferSize // полезные данные + служебные данные (TRecInfo) FCurrentRecord // текущая запись (от 0 до FrecordCount - 1) BofCrack. // перед первой записью EofCrack: Integer; // после последней записи // создание, закрытие и т. п. procedure Internal Open; override; procedure Internal Close; override: function IsCursorOpen: Boolean; override: // специальные функции function InternalRecordCount: Integer: virtual: abstract; procedure InternalPreOpen: virtual: procedure InternalAfterOpen; virtual: procedure InternalLoadCurrentRecord(Buffer: PChar); virtual: abstract; // управление памятью function AllocRecordBuffer: PChar; override; procedure InternalInitRecordtBuffer: PChar): override; procedure FreeRecordBuffer(var Buffer: PChar); override; function GetRecordSIze: Word; override; // перемещение и навигация (используется сетками) function GetRecord(Buffer: PChar: GetMode: TgetMode; DoCheck: Boolean): TGetResult: override; procedure InternalFirst; override; procedure InternalLast: ovberride; function GetRecNo: Longlnt: override; function GetRecordCount: Longlnt; override; procedure SetRecNotValue: Integer); override; // закладки procedure InternalGotoBookmark(Bookmark: Pointer): override; procedure InternalSetTorecord(Buffer: PChar); override: procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override: procedure GetBookmarkData(Buffer: PChar: Data: Pointer): override: procedure SetBookmarkFlag(Buffer: PChar: Value: TbookmarkFlag); override: function GetBookmarkFlag(Buffer: PChar): TBookmarkFlag; override; // редактирование (функции-заглушки) procedure Internal Delete: override: procedure InternalAddRecord(Buffer: Pointer: Append: Boolean): override: 0790
Разработка компонентов наборов данных 791 procedure Internal Post: override: procedure Internal Insert: override; // другое procedure InternalHandleException: override: published // переопределенные свойстве набора данных property Active: property BeforeOpen: property AfterOpen: property BeforeCiose: property AfterClose; property Beforeinsert: property AfterInsert: property BeforeEdit; property AfterEdit; property BeforePost: property AfterPost: property BeforeCancel: property AfterCancel; property BeforeDelete: property AfterDelete: property BeforeScrol1; property AfterScroll; - property OnCalcFields; property OnDeleteError: property OnEditError; property OnFilterRecord: property OnNewRecord; property OnPostError: end; // в модуле MdDsStream type TMdDataFileHeader = record VersionNumber: Integer; RecordSize; Integer: RecordCount: Integer; end; TMdDataSetStream = class(TMdCustomOataSet) private procedure SetTableName(const Value: string); protected FDataFi1 eHeader: TMdDataFileHeader: FDataFileHeaderSize. // необязательный размер заголовка файла FrecordCount: Integer: // текущее количество записей FStream: TStream: // физическая таблица FTableName: string: // путь и имя файла таблицы FFieldDffset: TList; // смещения полей в буфере protected // открытие и закрытие procedure Internal PreOpen: override: procedure InternalAfterOpen: override: procedure Internal Close ; override ; procedure InternalInitFieldOefS: override; // поддержка редактирования procedure lnternalAddRecord(Buffer: Pointer; Append: Boolean): override; _ л продолжение тУ 0791
792 Глава 17. Разработка компонентов, работающих с данными Листинг 17.2 (продолжение) procedure Internal Post; override; procedure Internal Insert, override: 11 поля procedure SetFneldData(Field: TField. Buffer: Pointer), override; // виртуальные методы набора данных function InternalRecordCount- Integer; override; procedure InternalLoadCurrentRecord(Buffer: PChar): override; public procedure CreateTable. function GetFieldData(Field: TField; Buffer- Pointer): Boolean; override; published property TableName- string read FTableName write SetTableName; end. Я разделил все методы на несколько секций (в этом можно убедиться, взглянув в исходные файлы) и пометил каждый из этих разделов римскими цифрами. Вы можете видеть эти цифры в комментариях, описывающих метод, благодаря чему, просматривая длинный листинг, вы немедленно можете узнать, в какой из четы- рех секций (разделов) вы находитесь. Раздел I. Инициализация, открытие и закрытие Прежде всего, давайте рассмотрим методы, которые отвечают за инициализацию набора данных, открытие и закрытие файлового потока, используемого для хране- ния данных. Помимо инициализации внутренних данных компонента эти методы отвечают за инициализацию и подключение соответствующих объектов TFields к компоненту набора данных. Чтобы это сработало, вам необходимо лишь иници- ализировать свойство FieldsDef определениями полей вашего набора данных, а пос- ле этого обратиться к нескольким стандартным методам, чтобы сгенерировать объекты TField и связать их с компонентом. Вот универсальный метод InternalOpen: procedure TMdCustomDataSet.InternalOpen; begin Internal PreOpen; // специфический метод для подклассов // инициализация определений полей Internal ImtFieldDefs; // если не существует постоянных объектов полей, создать поля динамически if DefaultFields then CreateFields: // подключить объекты TField к реальным полям BindFields (True): InternalAfterOpen; // специфический метод для подклассов // настраиваем конец и начало файла, позицию записи и размер BofCrack := -1. EofCrack •= InternalRecordCount. FCurrentRecord •= BofCrack: FRecordBufferSize • = FrecordSize + sizeof (TMdRecInfo): BookmarkSize sizeOf (Integer): 0792
Разработка компонентов наборов данных 793 // все в порядке: теперь таблица открыта FIsTableOpen : = True; end; Обратите внимание на то, что метод настраивает большинство локальных по- лей класса, а также поле BookmarkSize базового класса TDataSet. Внутри этого мето- да я обращаюсь к двум методам, которые могут быть переопределены в подклассе моего класса. Это методы InternalPreOpen и InternalAfterOpen. Первый предназначен для выполнения операций, необходимых в самом начале, например для проверки, может ли набор данных быть открытым, а также чтения заголовочной информа- ции из файла. Код проверяет внутренний номер версии и сравнивает его со значе- нием, которое было сохранено на диске, когда таблица была впервые сохранена. Это делается для обеспечения целостности. Сгенерировав исключение в данном методе, вы можете прервать операцию открытия. Вот код двух этих методов в производном классе набора данных, основанных на потоке (комментарии и сообщения, отображаемые на экране, для ясности пере- ведены на русский язык): const HeaderVerswn = 10; procedure TMdDataSetStream.InternalPreOpen; begin // размер заголовка FDataFileHeaderSize := sizeOf (TMdDataFileHeader); // проверка существования файла if not FileExists (FTableName) then raise EmdDataSetError.Create ('Таблица не найдена’): 11 создание потока для файла FStream := TFi1 eStream.Create (FTableName. fmOpenReadWrite); // инициализация локальных данных (загрузка заголовка) FStream.ReadBuffer (FDataFileHeader. FDataFileHeaderSize): if FDataFileHeader.VersionNumber <> HeaderVersion then raise EMdDataSetError Create (’Неправильная версия файла'): // читаем файл, двойная проверка - после FRecordCount := FDataFileHeader.RecordCount: end. procedure TMdDataSetStream.InternalAfterOpen. begin // проверка размера записи if FDataFileHeader.RecordSize <> FRecordSize then raise EMdDataSetError.Create (’Несоответствие размера записи'): 11 сверяем количество записей с размером файла if (FDataFileHeaderSize + FRecordCount * FREcordSize) <> FStream.Size then raise EMdDataSetError.Create ('InternalOpen: неправильный размер записи'): end; Второй метод — InternalAfterOpen — используется для операций, выполняемых после того, как настроены определения полей. Внутри метода размер записи, про- читанный из заголовка файла, сравнивается со значением, вычисленным при по- мощи метода InternallnitField Defs. Код также проверяет, чтобы количество записей, 0793
794 Глава 17. Разработка компонентов, работающих с данными прочитанных из заголовка, было совместимо с текущим размером файла. Эта про- верка может не сработать в случае, если набор данных не был корректно закрыт. Возможно, вы захотите модифицировать этот код таким образом, чтобы в заголо- вок в любом случае записывалось корректное количество записей. Метод набора данных InternalOpen отвечает за обращение к методу Internal- InitField Defs, который формирует определения полей (либо на этапе проектирова- ния, либо во время исполнения). В данном случае я решил хранить определения полей в отдельном INI-файле. В этом файле каждому полю соответствует отдель- ный раздел. В каждом разделе содержится имя поля и тип данных, хранящихся в поле, а также размер строковых данных. В листинге 17.3 показан файл Contrib.INI, используемый для приложения, демонстрирующего работу этого компонента. Листинг 17.3. Файл Contib.ini для демонстрационного приложения [Fields] Number = 6 [Fieldl] Type = ftString Name = Name Size = 30 [Field2] Type = ftlnteger Name = Level [Field3] Type = ftDate Name = BirthDate [Field4] Type = ftCurrency Name = Stipend [Field5] Type = ftString Name = Email Size = 50 [Field6] Type = ftBoolean Name = Editor Этот файл или аналогичный ему должен обладать точно таким же именем, как и имя файла таблицы, кроме того, он должен располагаться в том же самом катало- ге. Метод InternallnitFieldDefs (текст которого показан в листинге 17.4) читает содер- жимое INI-файла и использует извлеченные из него значения для формирования определений полей и определения размера каждой записи. Метод также инициали- зирует внутренний объект TList, в котором хранится смещение для каждого поля в за- писи. Этот список используется для доступа к данным внутри буфера записи. Листинг 17.4. Метод InternallnitFieldDefs набора данных, основанного на файловом потоке procedure TMdDataSetStream.Internal Im tFieldDefs. var ImFileName. FieldName string. 0794
Разработка компонентов наборов данных 795 IrnFile: TImFile; nFields. I. TmpFieldOffset. nSize: Integer; FieldType; TFieldType; begin FFieldOffset •= TList.Create: FieldDefs Clear: TmpFieldOffset --О: IniFilename •= ChangeFileExt(FTableName. '.ini'); Inifile = TImFile Create (IniFilename); // защищаем INI-файл try nFields := IniFile Readinteger (' Fields', 'Number'. 0): if nFields = 0 then raise EdataSetOneError.Create (’ ImtFieldsDefs: Ноль полей?"); for I := 1 to nFields do begin // создаем попе FieldType .= TFieldType (GetEnumValue (Typeinfo (TFieldType). IniFile Readstring ('Field' + IntToStr (I). 'Type'. "))): FieldName := ImFile ReadStnng ('Field' + IntToStr (I). 'Name'. "); if FieldName = " then raise EdataSetOneError Create ( ‘ImtFieldsDefs. Нет имени для поля ‘ + IntToStr (I)). nSize = Im File. Readinteger (.'Field' + IntToStr (I). 'Size'. 0); FieldDefs Add (FieldName. FieldType. nSize, False): // сохраняем смещение и вычисляем размер FF1eldOffset.Add (Pointer(TmpFieldOffset)): case FieldType of ftString: Inc (TmpFieldOffset, nSize + 1). ftBoolean. ftSmalllnt. ftWord: Inc (TmpFieldOffset. 2): ftlnteger. ftDate. ftTime: Inc (TmpFieldOffset. 4); ftFloat, ftCurrency. ftDateTime: Inc (TmpFieldOffset, 8); else raise EdataSetOneError.Create ( ‘ImtFieldsDefs: Неизвестный тип поля'); end. end. // for fi nally IrnFile Free. end. FRecordSize := TmpFieldOffset. end. Чтобы закрыть таблицу, достаточно отключить поля (при помощи стандарт- ных вызовов). Каждый класс должен сохранить данные на диске, обновить заголо- вок файла и освободить память: procedure TMdCustomDataSet Internalclose: begin // отключить объекты попей BinfFields (False). // уничтожить объект поля (если он не постоянен) if DefaultFields then DestroyFields. // закрыть файл FIsTableDpen False: end. 0795
796 Глава 17. Разработка компонентов, работающих с данными procedure TMdDataSetStream Internalclose: begin // если это необходимо, сохранить обновленный заголовок if (FDataFiТeHeader.RecordCount <> FRecordCount) or (FDataFileHeader RecordSize = 0) then begin FDataF11 eHeader RecordSize •= FRecordSize: FDataFi1 eHeader RecordCount = FRecordCount: if Assigned (FStream) then begin FStream Seek (0. soFromBeginning): FStream WriteBuffer (FDataFileHeader. FDataFileHeaderSize): end. end: // освобождаем внутренний список смещений полей и поток FFIeldOffset Free. FStream Free. inherited Internalclose, end. Еще одна функция используется для того, чтобы проверить, находится ли на- бор данных в открытом состоянии: function TMdCustomDataSet IsCursorOpen. Boolean. begin Result := FIsTableOpen. end. Итак, мы рассмотрели методы открытия и закрытия набора данных. Эти методы должны быть реализованы фактически в любом наборе данных. Однако поми- мо них часто возникает необходимость реализовать метод создания таблицы. В рассматриваемом примере метод CreateTable создает пустой файл и добавляет ин- формацию в заголовок: фиксированный номер версии, нулевой размер записи (мы не можем знать размер записи до того момента, пока не будет выполнена инициа- лизация полей) и счетчик записей (который тоже изначально равен нулю): procedure TMdDataSetStream CreateTable. begin Checkinactive. Internal ImtFieldDefs. // создаем новый файл if FileExists (FTableName) then raise EMdDataSetError Create ('Файл с именем ' + FTableName + ’ уже существует”): FStream • = TFileStream Create (FTableName. fmCreate or fmShareExclusive): try // сохраняем заголовок FDataFileHeader VersionNumber • = Headerversion: FDataFileHeader RecordSize = 0. // используется позднее FDataFileHeader RecordCount =0: // таблица пуста FStream WriteBuffer (FDataFileHeader. FDataFileHeaderSize). finally // закрываем файл FStream Free. end. end. 0796
Разработка компонентов наборов данных 797 Раздел II. Перемещение и управление закладками Как уже отмечалось ранее, любой набор данных должен поддерживать механизм управления закладками. Этот механизм необходим для перемещения по набору данных. Логически, закладка (bookmark) — это ссылка на некоторую запись набо- ра данных, нечто, уникально идентифицирующее запись таким образом, что набор данных может обращаться к этой записи и сравнивать ее с другими записями. Техни- чески, закладка — это указатель. Закладку можно реализовать либо как указатель на специальную структуру данных, в которой хранится информация о записи, либо как порядковый номер записи. Для простоты я воспользуюсь вторым способом. Обладая закладкой, вы должны быть в состоянии обнаружить соответствую- щую запись. Однако если у вас есть буфер с записью, вы также должны обладать возможностью получить соответствующую закладку. Именно по этой причине структура TmdRecInfo добавляется к данным записи в каждом из буферов записей. В этой структуре хранится закладка записи, а также некоторые флаги закладки, которые определяются следующим образом: type TBookmarkFlag = (bfCurrent. bfBOF. bfEOF. bflnserted): Флаги хранятся в каждом из буферов записей, при желании вы можете полу- чить эти флаги из заданного буфера записи. Итак, подведем итог. В буфере записи хранятся данные записи, закладка и фла- ги закладки, как показано на рис. 17.5. Рис. 17.5. Структура буфера записи Чтобы обратиться к закладке и флагам, можно прибавить к указателю на буфер размер данных записи и преобразовать полученное значение к типу PMdRecInfo (указатель на структуру TMdRecInfo). Иными словами, размер данных использует- ся в качестве смещения. Далее приводится код двух методов, демонстрирующих установку и сброс флагов закладки, которые проясняют описанную методику: procedure TMdCustomDataSet SetBookmarkFlag (Buffer PChar. Value TBookmarkFlag). begin PMdRecInfo(Buffer + FRecordSize).BookmarkFlag .= Value: end: 0797
798 Глава 17. Разработка компонентов, работающих с данными function TmdCustomDataSet GetBookmarkFlag (Buffer PChar) TBookmarkFlag begin Result = PMdRecInfoCBuffer + FRecordSize) BookmarkFlag end Похожим образом устроены методы, которые используются для получения и из- менения закладки для записи. Однако в них присутствует дополнительная тон- кость: в параметре Data возвращается указатель на закладку. Значение, о котором говорит этот указатель, необходимо привести к типу Integer, в результате вы полу- чите собственно закладку: procedure TMdCustomDataSet GetBookmarkData (Buffer PChar Data Pointer). begin Integer!DataA) = PMdRecInfoCBuffer + FRecordSize) Bookmark end procedure TMdCustomDataSet SetBookmarkData (Buffer PChar Data Pointer) begin PMdRecInfoCBuffer + FRecordSize) Bookmark = Integer(DataA) end Ключевым методом механизма управления закладками является метод Internal- GotoBookmark, который используется набором данных для того, чтобы сделать ука- занную запись текущей записью. Операция перемещения по закладке не является стандартной навигационной операцией — более распространенными являются операции перемещения к следующей или предыдущей записи (для этого исполь- зуется метод GetRecord, о котором речь пойдет в следующем разделе) или операции перемещения к самой первой или самой последней записи набора данных (для этого используются методы InternalFirst и Interna [Last, о которых я расскажу чуть далее). Как это ни странно, в качестве параметра метод InternalGotoBookmark принимает не закладку, а указатель на закладку. Помимо этого существует метод Internal- SetToRecord, который позволяет переместиться к указанной записи, однако в каче- стве параметра этому методу передается указатель на буфер — вы должны само- стоятельно извлечь закладку из этого буфера. После этого метод InternalSetToRecord обращается к методу InternalGotoBookmark. Вот исходный код обоих методов: procedure TMdCustomDataSet InternalGotoBookmark (Bookmark Pointer) var ReqBookmark Integer begin ReqBookmark = Integer (Bookmark^). if (ReqBookmark >= 0) and (ReqBookmark < InternalRecordCount) then FcurrentRecord = ReqBookmark el se raise EMdDataSetError Create ('Закладка ' + IntToStr (ReqBookmark) + ' не найдена") end procedure TMdCustomOataSet InternalSetToRecord (Buffer PChar). var ReqBookmark Integer begin ReqBookmark - PMdRecInfoCBuffer + FRecordSize) Bookmark Interna1GotoBookmark (PReqBookmark) end 0798
Разработка компонентов наборов данных 799 Помимо рассмотренных методов управления закладками существуют также несколько дополнительных навигационных методов, позволяющих переместить- ся к некоторой специфичной позиции внутри набора данных. Например, зачастую возникает необходимость переместиться к самой последней записи в наборе или к самой первой записи в наборе. Оба связанных с этим метода на самом деле пере- мещают указатель не на первую или последнюю запись, а в специальные позиции перед самой первой записью и после самой последней записи. В результате курсор не указывает на какую-либо запись. Компания Borland использует для обозначе- ния такой позиции специальный термин crack. Первая позиция называется BofCrack (beginning-of-file — начало файла), ей соответствует значение -1 (которое настра- ивается в методе InternalOpen), так как позиция самой первой записи в наборе со- ответствует значению 0 (ноль). Вторая специальная позиция называется EofCrack (end-of-file — конец файла), ей соответствует значение, равное количеству записей в наборе, так как последняя запись в наборе располагается в позиции FRecordCount -1. Чтобы сделать код проще в прочтении, я использую для обозначения двух этих позиций два локальных поля, EofCrack и BofCrack: procedure TMdCustomDataSet InternalFirst. begin FcurrentRecord = BofCrack end procedure TMdCustomDataSet Internal Last. begin EofCrack = InternalRecordCount FcurrentRecord = EofCrack end Метод InternalRecordCount — это виртуальный метод, определенный в разрабо- танном мною классе TMdCustom DataSet. В разных наборах данных количество запи- сей может либо храниться в локальном поле (как в случае с рассматриваемым на- бором данных, основанном на потоке, который использует для этой цели поле FRecordCount), либо вычисляться на лету. Еще одна группа необязательных методов используется для получения номера текущей записи (эта возможность используется компонентом DBGrid для отобра- жения пропорциональной вертикальной полосы прокрутки), изменения номера текущей записи и определения количества записей Код этих методов легко по- нять, если вы вспомните, что значение внутреннего поля FCurrentRecord изменяется в диапазоне от 0 до количества записей минус 1 Однако в отличие от этого номер записи, возвращаемый системе, лежит в диапазоне от 1 до значения, равного коли- честву записей: function TMdCustomDataSet GetRecordCount Longint begin CheckActive Result = InternalRecordCount end function TMdCustomDataSet GetRecNo Longint. begin UpdateCursorPos if FcurrentRecord < 0 then 0799
800 Глава 17. Разработка компонентов, работающих с данными Result .= 1 el se Result .= FcurrentRecord + 1. end. procedure TMdCustomDataSet SetRecNotValue- Integer): begin CheckBrowseMode. if (Value > 1) and (Value <= FRecordCount) then begin FCurrentRecord = Value - 1, Resync([]). end. end. Обратите внимание, что рассматриваемый нами универсальный класс набора данных реализует все методы данной секции. Производный набор данных, осно- ванный на потоке, никак не модифицирует эти методы. Раздел III. Буферы записей и управление полями Теперь, когда мы рассмотрели все вспомогательные методы, давайте рассмотрим код, который является основой компонента набора данных. Ранее мы рассматри- вали методы, предназначенные для создания записей, открытия записей и переме- щения между записями. Теперь я планирую продемонстрировать код, который перемещает данные из потока (файла на диске) в буферы записей и из буферов записей в объекты TField, которые подключены к элементам управления, поддер- живающим работу с данными. Управление буферами записей — непростая задача, так как каждый набор данных должен выделять, очищать и освобождать память, необходимую для хранения данных: function TMdCustomDataSet AllocRecordBuffer PChar. begin GetMem (Result. FRecordBufferSize). end. procedure TMdCustomDataSet FreeRecordBuffer (var Buffer PChar). begin FreeMem (Buffer). end Выделение памяти подобным образом необходимо потому, что набор данных добавляет в буфер не только данные записи, но и некоторую служебную информа- цию, поэтому система не может знать, сколько конкретно памяти требуется выде- лить. Обратите внимание, что в методе AllocRecordBuffer компонент выделяет па- мять для буфера записи, в котором предполагается хранить не только полезные данные, но и информацию о записи. В методе InternalOpen я пишу следующий код: FRecordBufferSize = InternalRecordSize + sizeof (TmdRecInfo). Помимо этого компонент также обязан реализовать функцию для переустановки буфера (InternallnitRecord). В процессе переустановки буфер заполняется нулями или пробелами. Как ни странно, вы должны также реализовать метод, который возвращает раз- мер каждой записи — имеется в виду только размер полезных данных, а не размер 0800
Разработка компонентов наборов данных 801 всего буфера целиком. Этот метод необходим для реализации свойства RecordSize, предназначенного только для чтения. Это свойство используется в нескольких специальных случаях в рамках VCL. В разработанном мною универсальном набо- ре данных метод GetRecordSize возвращает значение поля FRecordSize. Теперь мы вплотную приблизились к основным методам набора данных. В эту группу входят методы GetRecord, InternalPost, InternalAddRecord и InternalDelete. Ме- тод GetRecord читает данные из файла. Метод InternalPost обновляет данные в фай- ле. Метод InternalAddRecord добавляет в файл новую запись. Метод InternalDelete удаляет данные (этот метод в рассматриваемом наборе данных не реализован). Наиболее сложным методом этой группы является GetRecord, который служит для нескольких целей. Система использует этот метод для извлечения данных теку- щей записи и занесения их в буфер, который передается методу в качестве параметра. Кроме того, метод используется для извлечения данных для следующей или преды- дущей записи. Иными словами, метод может работать в одном из трех режимов: type TgetMode = (gmCurrent. gmNext. gmPrior), Конечно же, предыдущая или последующая записи могут отсутствовать. Даже текущая запись может отсутствовать, например, если таблица пуста или если воз- никла внутренняя ошибка. В любом из этих случаев вместо извлечения данных метод возвращает код ошибки. Иными словами, метод может возвратить одно из следующих значений: type TGetResult = (grOK. grBOF. grEOF, grError), Проверка на наличие записи выполняется несколько неординарным способом. Необходимо убедиться в том, что запрашиваемая запись находится в допустимом диапазоне. Запрашиваемая, но не текущая. Именно поэтому в данном методе ис- пользуются такие, с первого взгляда странные, условные выражения. Например, в ветке gmCurrent оператора case используется условное выражение CurrentRecord >= InternalRecordCount. Чтобы полностью понять разнообразные варианты, необходи- мо прочитать код пару раз. Когда я несколько лет назад впервые писал подобный код, мне потребовалось поставить несколько экспериментов (и несколько раз наблюдать, как моя программа Дает фатальный сбой в результате рекурсивных вызовов). Чтобы протестировать код, представьте, что вы используете элемент DBGrid, соединенный с вашим набо- ром данных. В этом случае система станет выполнять серию обращений к методу GetRecord до тех пор, пока либо отображаемая на экране сетка не заполнится дан- ными, либо метод GetRecord не вернет значение grEOF. Вот полный код метода GetRecord: II III Извлечение данных для текущей предыдущей или следующей записи И (с перемещением к этой записи если это необходимо) function TMdCustomDataSet GetRecordfBuffer PChar. GetMode TgetMode, DoCheck Boolean) TGetResult. begin Result = grOK. // возвращаемое значение no умолчанию case GetMode of gmNext // перемещение назад if FCurrentRecord < InternalRecordCount - 1 then Inc (FCurrentRecord) 0801
802 Глава 17. Разработка компонентов, работающих сданными else Result := grEOF: // конец файла gmPrior: // перемещение вперед if FcurrentRecord > 0 then Dec (FcurrentRecord) e] se Result :» grEOF; // начало файла gmCurrent: // проверка, является пи запись пустой if FcurrentRecord >= InternalRecordCount then Result ;= grError; end; // загрузка данных if Result = grOK then InternalLoadCurrentREcord (Buffer) else if (Result = grError) and DoCheck then raise EMdDataSetError.Create ('GetRecord: Неправильная запись'); end: Если возникает ошибка и при этом параметр DoCheck равен значению True, ме- тод GetRecord генерирует исключение. Если выбор записи осуществляется коррек- тно, компонент загружает данные из потока, перемещаясь в позицию текущей за- писи (для этого размер записи умножается на порядковый номер записи). Кроме того, необходимо добавить в буфер флаг закладки и значение закладки (порядко- вый номер записи). Это осуществляется при помощи еще одного созданного мною виртуального метода InternalLoadCurrentRecord. Благодаря наличию этого метода в производном классе можно реализовать только этот метод, в то время как слож- ный код метода GetRecord будет позаимствован из базового класса: procedure TMdDataSetStream.InternalLoadCurrentRecord (Buffer: PChar); begin FStream.Position := FDataFileHeaderSize + FRecordSize * FCurrentRecord: FStream.ReadBuffer (Buffer*. FRecordSize); with PMdRecInfo(Buffer + FRecordSize)* do begin BookmarkFlag := bfCurrent: Bookmark := FCurrentRecord; end: end: Данные перемещаются в файл в двух случаях: во-первых, когда вы модифици- руете текущую запись (то есть публикация данных проходит после редактирова- ния), во-вторых, когда вы добавляете в набор новую запись (то есть публикация проходит после вставки или добавления). В обоих случаях используется метод InternalPost, однако чтобы определить тип публикации, вы можете обратиться к свойству State набора данных. В обоих случаях буфер записи вам не передается, поэтому вы должны воспользоваться свойством ActiveRecord класса TDataSet. Это свойство указывает на буфер текущей записи: procedure TMdDataSetStream.Internal Post: begin CheckActive; if State = dsEdit then begin // замена данных новыми данными FStream.Position := FDataFileHeaderSize + FRecordSize * FCurrentRecord; FStream.WriteBuffer (ActiveBuffer*. FRecordSize); 0802
Разработка компонентов наборов данных 803 end: el se begin // всегда добавляем Internal Last: FStream.Seek (0. soFromEnd): FStream.WriteBuffer (ActiveBuffer*. FRecordSize): Inc (FRecordCount): end: end: Существует еще один связанный с этим метод — InternalAddRecord. Обращение к этому методу осуществляется из метода AddRecord, который, в свою очередь, вы- зывается из методов InsertRecord и AppendRecord. Последние два метода являются публичными методами, к которым может обратиться пользователь. Эти два мето- да принимают значения полей в качестве параметров. Чтобы реализовать метод InternalAddRecord, достаточно реплицировать код, используемый для добавления новой записи в методе InternalPost: procedure TMdDataSetOne.InternalAddRecord(Buffer: Pointer: Append: Boolean): begin // всегда добавляем в конец Internal Last: FStream.Seek (0. soFromEnd): FStream.WriteBuffer (ActiveBufferx. FRecordSize): Inc (FRecordCount); end: Для полного комплекта мне следовало бы реализовать операцию удаления те- кущей записи. Эта операция является общераспространенной, однако ее достаточно сложно реализовать. Простой подход состоит в том, что вместо удаляемой записи в файле остается пустое место. Считается, что данные в такой области отсутству- ют. Однако в этом случае необходимо разработать код, который корректно обслу- живал бы подобные части файла. Альтернативный способ состоит в создании ко- пии файла, в котором отсутствует удаленная запись. После этого изначальный файл заменяется копией. Оба варианта достаточно сложно реализовать, поэтому я ре- шил отказаться от реализации операции удаления записи. Раздел IV. Из буфера в поле В методах, рассмотренных в предыдущем разделе, выполняется перемещение дан- ных из файла в буфер, расположенный в памяти. Однако среда Delphi мало что может сделать с этими данными, так как она не знает, каким образом их интерпре- тировать, следовательно, она не может работать с ними напрямую. Чтобы обеспе- чить взаимодействие между Delphi и содержимым буфера, необходимо реализо- вать в рамках компонента еще два метода: GetData и SetData. Метод GetData копирует Данные из буфера в объекты полей набора данных, а метод SetData перемещает дан- ные обратно из объектов полей в буфер. Потом Delphi сможет выполнить переме- щение данных из объектов данных в элементы управления, работающие с этими Данными, и обратно, из элементов управления в объекты полей. Код этих двух методов не так уж и сложен, ведь смещения полей внутри записи Данных сохранены в списке TList (объект с названием FFieldOffset). Чтобы получить Данные для некоторого поля, вы должны прибавить к указателю на начало буфера 0803
804 Глава 17. Разработка компонентов, работающих с данными смещение, соответствующее объекту текущего поля. Размер данных при этом со- ставит Field.DataSize. Слегка сбивает с толку то обстоятельство, что оба метода принимают парамет- ры Field и Buffer. Вначале можно подумать, что Buffer — это буфер записи. Однако я обнаружил, что Buffer — это указатель на данные, которые хранятся в объекте поля. Если для перемещения данных вы воспользуетесь одним из методов объекта поля, этот метод обратится к одному из методов GetData и SetData набора данных, в результате, скорее всего, возникнет бесконечная рекурсия. Вместо этого вы долж- ны воспользоваться указателем ActiveBuffer для доступа к буферу записи, добавить подходящее смещение для доступа к текущему полю, а затем использовать значе- ние параметра Buffer для доступа к данным поля. Единственное различие между двумя методами — это направление перемещения данных: function TMdDataSetOne.GetFieldData (Field: TField: Buffer: Pointer): Boolean: var FieldOffset: Integer: Ptr: PChar: begin Result := False: if not IsEmpty and (Field.FieldNo > 0) then begin FieldOffset :- Integer (FFieldOffset [Field.FieldNo -1]): Ptr := ActiveBuffer: Inc (Ptr. FieldOffset): if Assigned (Buffer) then Move (Ptr*. Buffer*. Field.DataSize): Result :*= True: if (Field is TdateTimeField) and (Integer(Ptr*) - 0) then Result := False: end: end: procedure TMdDataSet0ne.SetFieldData(F1eld: TField: Buffer: Pointer): var FieldOffset: Integer: Ptr: PChar: begin if Field.FieldNo >= 0 then begin FieldOffset :- Integer (FFieldOffset [Field.FieldNo - 1]): Ptr := ActiveBuffer: Inc (Ptr, FieldOffset): if Assigned (Buffer) then Move (Buffer*. Ptr*. Field.DataSize) else raise Exception.Create ( 'Очень плохая ошибка в данных TMdDataSetStream.SetField'): DataEvent (deFieldChange. Longint(Field)): end: end: Метод GetField должен возвращать либо True, либо False в зависимости от того, содержит ли поле данные или оно пусто (говоря точнее, в поле содержится значе ние null). Однако определить, является ли поле пустым, достаточно сложно (если. 0804
Разработка компонентов наборов данных 805 конечно, не использовать для этой цели специальные маркеры), так как в разных полях хранятся данные разных типов. Например, проверка наподобие PtrA<>#o имеет смысл только в случае, если вы используете строковые представления для всех полей. Если вы будете использовать данный критерий, пустые строки и це- лые значения, равные нулю, будут отображаться, как null-значения (соответству- ющие элементы управления будут пустыми). Возможно, именно это вам и надо. Проблема состоит в том, что булевские значения False также будут воспринимать- ся как пустые значения, то есть не будут отображаться в элементах управления. Хуже того, значения с плавающей точкой, в которых экспонента равна нулю, так- же не будут отображаться. В данном примере я рассматриваю поле даты/времени, начинающееся с нуля, как пустое поле. Без этого кода Delphi пытается преоб- разовать некорректную нулевую дату, и в результате генерируется исключе- ние (тип данных TDateTime не используется для представления даты и времени в составе поля набора данных — для хранения даты и времени используется другое внутреннее представление). Указанный код работал с предыдущими версиями Delphi. ВНИМАНИЕ ---------------------------------------------------------------------- Пытаясь исправить эту проблему, я также обнаружил, что если для поля происходит обращение к IsNull, для обслуживания запроса происходит обращение к методу GetFieldData. При этом данному методу не передается никакого буфера, а система всего лишь проверяет возвращаемое этим мето- дом значение. Именно поэтому я добавил в код метода проверку if Assigned(Buffer). Существует еще один метод, который не принадлежит ни к одной из рассмот- ренных категорий. Этот метод называется InternalHandleException. Этот метод про- сто нейтрализует исключение, так как он активируется только на этапе проекти- рования. Тестирование набора данных, основанного на файловом потоке После выполнения всей этой работы мы можем приступить к тестированию разра- ботанного нами компонента. Демонстрационное приложение устанавливается вме- сте с пакетом компонентов для данной главы. Программа называется StreamDSDemo. Форма, отображаемая этой программой, показана на рис. 17.6. На ней располага- ется панель с двумя кнопками, флажок и компонент навигации, а также сетка DBGrid, заполняющая собой остальное пространство. На рис. 17.6 показана форма на этапе проектирования. Однако я активировал набор данных собственной разработки, поэтому в сетке отображаются данные. Я уже подготовил INI-файл с определением таблицы (листинг этого файла был приведен ранее, в подразделе, посвященном инициализации компонента), кроме того, я предварительно запустил программу, чтобы добавить в файл некоторые Данные. Вы можете модифицировать форму, используя редактор полей Delphi, и с его помощью настроить свойства различных объектов полей. Все работает так, как будто вы имеете дело с одним из стандартных наборов данных, однако чтобы про- грамма заработала, вы должны указать полное имя файла в свойстве TableName. 0805
806 Глава 17. Разработка компонентов, работающих с данными Рис. 17.6. Форма примера StreamDSDemo. Набор данных активирован, поэтому вы можете видеть данные на этапе проектирования ВНИМАНИЕ ---------------------------------------------------------------- В демонстрационной программе определяется абсолютный путь к таблице, поэтому вы должны исправить его в случае, если вы перемещаете примеры на другой диск или в другой каталог. В при- мере свойство TableName используется только на этапе проектирования. Во время выполнения программа ищет таблицу в текущем каталоге. Код демонстрационной программы чрезвычайно прост, особенно если срав- нивать его с кодом разработанного нами набора данных. Если таблица не су- ществует, вы можете создать ее, щелкнув на кнопке Create New Table (Создать новую таблицу): procedure TForml ButtonlClick(Sender: TObject): begin MdDataSetStreaml CreateTable: MdDataSetStreaml Open: CheckBoxI.Checked := MdDataSetStreaml.Active, end: Прежде всего, создается файл. Для этого внутри метода CreateTable происходит открытие и закрытие этого файла. После этого осуществляется открытие таблицы. Подобным же образом ведет себя компонент ТТаЫе (который осуществляет откры- тие таблицы при помощи метода CreateTable). Чтобы открыть или закрыть таблицу можно щелкнуть на флажке: procedure TForml CheckBoxlClick(Sender. TObject): begirr MdDataSetStreaml.Active := CheckBoxI.Checked. end: Наконец, я создал метод, специально предназначенный для тестирования кода управления закладками (все работает нормально). 0806
Листинг каталога в наборе данных 807 Листинг каталога в наборе данных Набор данных в Delphi — это объект, в котором хранятся некоторые данные, при этом абсолютно не важно, откуда изъяты эти данные. В качестве набора данных может использоваться SQL-сервер или локальный файл, однако эту же самую тех- нологию можно использовать для отображения списка системных пользователей, листинга файлов в каталоге на диске, свойств объектов, сведений в формате XML, а также множества других данных. В данном разделе будет рассмотрен набор данных, содержащий в себе список файлов. Я разработал универсальный набор данных, основанный на списке объек- тов в памяти (используя при этом TObjectList), азатем сделал производную версию, в которой объекты соответствуют файлам в каталоге. Задача разработки подобно- го компонента упрощается тем, что он предназначен только для чтения. Благодаря этому код выглядит проще, чем код компонента, рассмотренного в предыдущем разделе. ПРИМЕЧАНИЕ ------------------------------------------------------------ Некоторые представленные здесь идеи обсуждаются в статье, которую я написал для веб-узла Borland Community. Статья была опубликована в июне 2000 года по адресу bdn.borland.com/article/ 0,1410,20587,00.html. Список как набор данных Универсальный основанный на списке набор данных называется TMdListDataSet. Этот класс содержит в себе список объектов. Список создается в момент открытия набора данных и уничтожается, когда набор данных закрывается. Этот набор дан- ных не сохраняет данные записи в буфере, вместо этого он хранит в буфере только позицию в списке. Вот определение класса: type TMdListDataSet = class (TMdCustomDataSet) protected // список, в котором содержатся данные FList: TObjectList: // виртуальные методы набора данных procedure InternalPreOpen: override; procedure Internal Close: override; // виртуальные методы специализированного набора данных function InternalREcordCount: Integer: override; procedure InternalLoadCurrentRecord (Buffer- PChar); override. end; Можно заметить, что благодаря использованию рассмотренного ранее универ- сального разработанного мною набора данных, чтобы получить работающий на- бор данных, достаточно переопределить всего несколько методов класса TDataSet и несколько методов разработанного мною класса. Когда набор данных открыва- ется, вы должны создать список и настроить размер записи, чтобы отметить, что вы сохраняете в буфере только индекс списка: Procedure TMdListDataSet.Internal PreOpen; begin 0807
808 Глава 17. Разработка компонентов, работающих с данными FList = TObjectList Create (True). // владеет объектами FRecordSize =4 // целое число - идентификатор элемента списка end Производные классы на этом этапе должны также заполнить список объек- тами. СОВЕТ--------------------------------------------------------------------------- Подобно компоненту ChenDataSet, разрабатываемый мною набор данных хранит данные в памяти. Однако, используя несколько хитрых трюков, вы можете создать список ложных объектов и после этого подгружать реальные объекты только тогда, когда возникает необходимость обращать- ся к ним. При закрытии набора данных необходимо освободить список. Количество за- писей соответствует размеру списка: function TMdListDataSet InternalRecordCount Integer begin Result = fList Count end Еще один метод сохраняет данные текущей записи в буфере записи, добавляя к ним информацию о закладке. Полезные данные — это позиция текущей записи, которая совпадает с индексом в списке (а также с закладкой): procedure TMdListDataSet InternalLoadCurrentRecord (Buffer PChar) begin PInteger (Buffer)* = fCurrectRecord with PMdRecInfo(Buffer + FRecordSize)* do begin BookmarkFlag = bfCurrent Bookmark = fCurrentRecord end end Данные каталога Производный набор данных, предназначенный для работы с файловым каталогом, должен обеспечить загрузку объектов в память при открытии набора данных, оп- ределение значений свойств, а также чтение и запись значений этих свойств Кро- ме того, в наборе данных должно содержаться свойство, в котором указывается имя каталога, с которым работает этот набор данных. Говоря точнее, в этом свой- стве должно указываться имя каталога плюс шаблон, определяющий имена фай- лов (например, c:\docs\*.txt): type TMdDirDataset = class(TMdListDataSet) private FDirectory string procedure SetDirectory(const NewDirectory string). protected // виртуальные методы класса TDataSet procedure InternallnitFieldDefs override procedure SetFieldData(Field TField Buffer Pointer) override. function GetCanModify Boolean override // виртуальные методы специализированного набора данных procedure InternalAfterOpen override 0808
Листинг каталога в наборе данных 809 public function GetFieldData(Field TField Buffer Pointer) Boolean override. publ i shed property Directory string read FDirectory write SetDirectory end Функция GetCanModify — это еще один виртуальный метод класса TDataSet, ис- пользуемый для того, чтобы определить, предназначен ли набор данных только для чтения В этом случае метод возвращает значение False. В процедуре Set Field Data можно не писать никакого кода, однако вы должны ее определить, так как в базо- вом классе это абстрактный виртуальный метод. Так как вы имеете дело со списком объектов, модуль включает в себя определе- ние класса этих объектов В данном случае сведения, связанные с файлом, извле- каются из буфера TSearchRec при помощи конструктора TFileData: type TFileData = class public ShortFileName string Time TDataTime Size Integer Attr Integer constructor Create (var Filelnfo TsearchRec) end constructor TFileData Create (var Filelnfo TSearchRec). begin ShortFileName = Filelnfo Name Time = FileDateToDateTime (Filelnfo Time) Size = Filelnfo Size Attr = Filelnfo Attr end При открытии набора данных конструктор вызывается для каждой папки: procedure TMdDirDataset InternalAfterDpen. var Attr Integer Filelnfo TSearchRec Fi1 eData TFileData begin // сканируем все файлы Attr = faAnyFile FList Clear if SysUtils FindFirst(fDirectory Attr Filelnfo) - 0 then repeat FileData = TFileData Create (Filelnfo) FList Add (FileData) until SysUtils FmdNext(Filelnfo) <> 0 SysUtils FindClose(Filelnfo) end На следующем шаге необходимо определить поля набора данных, которые в данном случае фиксированы и зависят от доступных данных каталога: Procedure TMdDirDataset Internal ImtFieldDefs begin if fDirectory = then raise EMdDataSetError Create (’Каталог не найден') 0809
810 Глава 17. Разработка компонентов, работающих с данными // определения полей FieldDefs. Clear: Ft eldDefs.Add ('FileName', ftString. 40. True): FieldDefs.Add ('TimeStamp'. ftDateTime): FieldDefs.Add ('Size', ftlnteger): FieldDefs.Add ('Attributes'. ftString. 3); FieldDefs.Add ('Folder'. ftBoolean): end: Наконец, компонент должен обеспечить перемещение данных из объекта спис- ка (на который ссылается текущий буфер записи, то есть значение, ActiveBuffer) в каждое из полей набора данных. Эта процедура выполняется в рамках метода GetFieldData. Для перемещения данных используется либо функция Move, либо фун- кция Str Сору, а зависимости от типа данных. Кроме того, функция выполняет пре- образование файловых атрибутов в специальные символьные метки (Н — для скрытых файлов, R — для файлов, предназначенных только для чтения, и S — для системных файлов). Также функция определяет, является ли файл подкаталогом. Вот код этой функции: function TMdOirDataset.GetFieldData (Field: TField; Buffer: Pointer): Boolean; var FileData: TFileData; Bool 1: WordBool; strAttr: string: t: TdateTimeRec; begin FileData := fList [Integer(ActiveBuffer*)J as TFileData; case Field.Index of 0: //имя файла StrCopy (buffer. pchar(FileData.ShortFileName)); 1: // отметка времени begin t -.= DateTimeToNative (ftdatetime. FileData.Time); Move (t. Buffer*. sizeof (TDateTime)); end: 2: // размер Move (FileData.Size. Buffer*, sizeof (Integer)): 3: // атрибуты begin StrAttr ;= ' if (FileData.Attr and SysUtils.faReadOnly) > 0 then strAttr [1] :- if (FileData.Attr and SysUtils.faSysFile) > 0 then StrAttr [2] 'S': if (FileData.Attr and SyslItils.faHidden) > 0 then strAttr [3] :- '«': StrCopy (Buffer. pchar(strAttr)); end: 4: // папка begin Booll := FileData.Attr and SysUtils.faDirectory >0; Move (Booll. Buffer*, sizeof (WordBool)): end: end: // case Result : = True: end: 0810
Набор данных, содержащий информацию об объектах 811 Сложность заключается в определении внутреннего формата дат, хранящихся в полях типа «дата/время». Этот формат не совпадает со стандартным форматом TDateTime, используемым в Delphi, мало того, он не совпадает с внутренним форма- том TTimeStamp. Формат поля типа «дата/время» называется native date and time format. Для его обработки я написал функцию преобразования, которую позаим- ствовал из кода V CL: function DateTimeToNative(Datatype: TFieldType; Data: TDateTime): TDateTimeRec: var TimeStamp: TTimeStamp: begin TimeStamp := DateTimeToTimeStamp(Data): case Datatype of ftDate: Result.Date := TimeStamp.Date: ftTime: Result.Time : = TimeStamp.Time: el se Result.DateTime : = TimeStampToMSecs(TimeStamp); end: end: Обладая таким компонентом, совсем не сложно написать демонстрационную программу (рабочее окно которой показано на рис. 17.7). Для этого достаточно подключить сетку DBGrid к набору данных и добавить на форму компонент выбора каталога ShellTreeView. В программе этот компонент настроен на работу только с файловой системой (для этого его свойству Root присвоено значение С:\). Когда пользователь выбирает новую папку, обработчик события OnChange элемента управ- ления ShellTreeView выполняет обновление набора данных: procedure TForml.ShellTreeViewChangetSender: TObject: Node: TtreeNode): begin MdDirDataSetl.Close: MdDirDatasetl.Directory := ShellTreeViewl.Path + MdDirDatasetl.Open: end; ВНИМАНИЕ -------------------------------------------------------------------------------- Если ваша версия Windows некорректно работает с элементами управления, доступными в рамках Delphi, вы можете воспользоваться примером DirDemoNoShell, в котором используются старомод- ные файловые элементы управления в стиле Windows 3.1. Набор данных, содержащий информацию об объектах Ознакомившись с предыдущим примером, можно заметить, что список объектов концептуально напоминает строки таблицы в наборе данных. В Delphi вы можете построить набор данных, являющийся оболочкой списка объектов, как в случае с классом TFileData. Было бы интересно расширить этот пример таким образом, что- бы полученный в результате набор данных мог поддерживать работу с перечнем универсальных объектов. Это можно было бы реализовать при помощи встроен- ных в Delphi механизмов RTTI. 0811
812 Глава 17, Разработка компонентов, работающих с данными FjJMai 3. FfeName JJweSteap. * Agent > 9/13/2002 10 49 10 AH True tf О Bat Э/13/2ОО2 10 49 10 AH . ,.l stumel F Program Ftes - dclacn70 bpl 8/9/2002 2 00 00 PH 54,272 Paisa _J 20x20 d*lphi32 «x« 8/9/2002 2 00 00 PH 545.792 Falsa ~~1 Accessories DELPHI32 DCI 8/9/2002 2 00 00 PH 5.846 Falsa i 23 Adobe — delphi32 dat 9/14/2002 6 28 32 PH 3.742 Falsa + i Alarre dclbave70 bpl 8/9/2002 4 00 00 PH 66.580 False dcllntraveb 50 70 bpl 8/9/2002 4 00 00 PH 160.256 False Borland - S 2j Database Desktop CLXdaiphi32 dab 8/9/2002 4 00 00 PH 4.449 False № 2J Ddphfor NET Pt _ Pelphi up« 8/9/2002 4 00 00 PH 3.120 False W 23 Delphi HTHL2-strict dtd 8/9/2002 4 00 00 PH 18,796 False Щ 2J DefchG HTHL2 dtd 8/9/2002 4 00 00 PH 18,140 False E Q Delphi? HTML3 2 dtd 8/9/2002 4 00 00 PH 21 675 False -^38 * 2i Demos - HTHLlatl ant 8/9/2002 4 00 00 PH 11 956 False 21 Doc _ HTHbspecial ant 8/9/2002 4 00 00 PH 4.114 False J I Jjij HTHLsyabol ant 8/9/2002 4 00 00 PH 14.447 False Рис. 17.7. Программа DirDemo отображает необычный набор данных, в котором показан перечень файлов в выбранном каталоге Рассматриваемый в данном разделе компонент является производным от ком- понента TMdListDataSet, рассмотренного ранее. В компоненте присутствует важный конфигурационный параметр: целевой класс, который хранится в свойстве ObjClass (см. полное определение класса TMdObjDataSet в листинге 17.5). Листинг 17.5. Полное определение класса TMdObjDataSet type TMdObjDataSet = classtTMdListDataSet) private PropList PpropList nProps Integer FObjClass TPersistentClass DbjClone TPersistent FChangeToClone Boolean procedure SetObjClass (const Value TpersistentClass), function GetObjects (I Integer) TPersistent procedure SetChangeToClone (const Value Boolean) protected procedure InternalImtFieldOefs override. procedure Internalclose, override procedure Internal Insert override procedure Internal Post, override procedure Internal Cancel override procedure Internal Edit override procedure SetFieldDatatField TField Buffer Pointer): override. function GetCanModify Boolean override. procedure Internal PreOpen override public function GetFieldDatatField TField Buffer Pointer) Boolean, override. property Objects [I Integer] TPersistent read GetObjects function Add TPersistent published property ObjClass TpersistentClass read FobjClass write SetObjClass. property ChangesToClone Boolean read FChangeToClone write SetChangeToClone default False end 0812
Набор данных, содержащий информацию об объектах 813 Класс используется методом Internallnit Field Deft для определения полей набора данных на основе опубликованных свойств целевого класса. Извлечение сведений о свойствах осуществляется при помощи RTTI: procedure TMdObjDataSet InternalFieldDefs. var i Integer. begin if FobjClass - nil then raise Exception Create (.‘TMdObjDataSet Неназначенный класс"). И определения полей FieldDefs Clear, nProps - GetTypeData(fObjClass Classinfo)* PropCount, GetMem!PropList. nProps * SizeOf(Pointer)), GetPropInfos (fObjClass Classinfo. PropList), for i =0 to nProps - 1 do case PropList [i] PropType* Kind of tkInteger. tkEnumeration, tkSet FieldDefs Add (PropList [i] Name, ftlnteger 0) tkChar FieldDefs Add (PropList [i] Name ftFixedChar, 0). tkFloat FieldDefs Add (PropList [i] Name ftFloat. 0). tkString tkLStnng FieldDefs Add (PropList [i] Name. ftString 50). H TODO исправить размер tkWString FieldDefs Add (PropList [i] Name. ftWideString. 50). // TOOO исправить размер end end Похожий, основанный на применении RTTI код используется в методах GetField- Data и SetFieldData. Когда мы используем свойства для доступа к данным набора данных, у нас появляется удобная возможность либо напрямую связать операции чтения-записи с данными, либо употреблять для обращения к данным специально предназначенные для этого методы. В этом случае бизнес-правила приложения можно реализовать в виде методов чтения-записи для соответствующих свойств. Можно было бы подключить код к объектам полей и в рамках этого кода выпол- нять все необходимые проверки, однако подход, основанный на использовании свойств, в большей степени соответствует концепции ООП. Далее приводится несколько упрощенная версия метода GetFieldData (метод SetFieldData выглядит очень похоже): function TobjDataSet GetFieldData ( Field TField Buffer Pointer) Boolean. var Obj TPersistent Typeinfo PTypelnfo, IntValue Integer. Fl Value Double, begin if FList Count = 0 then begin Result = False exit end 0813
814 Глава 17. Разработка компонентов, работающих с даННЙМи Obj flist [Integer(ActiveBufferA)J as TPersistent: Typeinfo := PropList [Field.FieldNo-lJ*.PropType*; case Typeinfo Kind of tklnteger. tkChar. tkWChar. tkClass. tkEnumeration. tkSet- begin IntValue := GetOrdProptObj. PropList {Field.FieldNo-lJ): Move (IntValue. Buffer*. sizeof (Integer)): end: tkFloat: begin FlValue := GetFloatProptObj. PropList [Field.FieldNo-lJ): Move (FlValue. Buffer*. sizeof (Double)): end: tkString. tkLString. tkWString: StrCopy (Buffer. pchar(GetStrProp(Obj. PropList {Field.FieldNo-1]))): end. Result := True. end: Этот код активно использует указатели, поэтому он выглядит ужасно, однако если ранее вы уже ознакомились с процедурой самостоятельной разработки соб- ственного набора данных, вы обнаружите, что код не такой уж и сложный. Он об- ращается к некоторым структурам данных, определенным в модуле Typlnfo, — в слу- чае возникновения вопросов вы должны обратиться к коду этого модуля. При использовании подобного наивного подхода, основанного на прямом ре- дактировании данных объекта, у вас может возникнуть вопрос, что произойдет, если пользователь отменит операцию редактирования. Разработанный мною на- бор данных предлагает два альтернативных подхода. Оба они основаны на клони- ровании объектов путем копирования опубликованных свойств. Выбор метода осуществляется при помощи свойства ChangesToClone. Основная процедура DoClone использует код RTTI, похожий на тот, который мы уже использовали для копиро- вания всех опубликованных данных объекта в другой объект, создавая тем самым эффективную копию объекта (или клон). Клонирование выполняется при каждом из этих двух подходов. В рамках пер- вого подхода операции редактирования выполняются в отношении объекта-кло- на, который копируется поверх оригинального объекта в процессе выполнения операции Post. В рамках второго подхода операции редактирования выполняются в отношении оригинального объекта, а объект-клон используется в случае, если пользователь отменяет редактирование при помощи запроса Cancel. Выбор метода осуществляется при помощи свойства ChangesToClone. Вот код связанных с этим трех методов: procedure TObjDataSet.Internal Edit; begin DoClone (fList [FcurrentRecord] as TdbPers. ObjClone): end: procedure TObjDataSet.Internal Post, begin 1f FChangeToClone and Assigned (ObjClone) then DoClone (ObjClone TdbPers (fList [fCurrentRecord])); end: procedure TMdObjDataSet Internal Cancel: 0814
Что далее? . 815 begin if not FChangeToClone and Assigned (ObjClone) then DoClone (ObjClone. TPersistent(fList [fCurrentRecord])): end. В методе SetFieldData вы должны модифицировать либо клонированный объект, либо оригинальный экземпляр. Эту же разницу следует принять во внимание при разработке метода GetFieldData. В листинге 17.5 видно, что класс также обладает массивом Objects, который об- ращается к данным в рамках подхода ООП, кроме того, в классе поддерживается метод Add, аналогичный методу Add для коллекции. При обращении к Add код со- здает новый пустой объект целевого класса и добавляет его во внутренний список: function TMdObjDataSet Add: TPersistent. begin if not Active then Open: Result := fObjClass Create: fList.Add (Result). end; Чтобы продемонстрировать использование компонента, я разработал пример ObjDataSetDemo. В программе используется демонстрационный целевой класс TDemo с несколькими полями. На форме присутствует кнопка, позволяющая добавлять новые объекты (форма программы показана на рис. 17.8). Чтобы ознакомиться с наиболее интересной особенностью программы, запустите ее и обратите внимание на колонки сетки DBGrid. После этого отредактируйте целевой класс TDemo, добавь- те к нему новые опубликованные свойства. Теперь снова запустите программу — вы увидите, что для каждого из добавленных вами свойств в сетке DBGrid появится дополнительный столбец. Mark 21 3242.43 33 6716.54 28 3722.38 24 4747.94 62 597.24 |Адё.7 John Я Joseph « В111 John Рис. 17.8. Программа ObjDataSetDemo отображает содержимое набора данных, элементами которого являются объекты Что далее? В данной главе мы ознакомились с внутренностями встроенной в Delphi архитекту- ры, обеспечивающей работу с данными. Вначале мы изучили вопросы, связанные с разработкой элементов управления, предназначенных для работы с данными, 0815
816 Глава 17. Разработка компонентов, работающих с данными затем узнали об особенностях класса TDataSet и разработали на его основе пару собственных наборов данных. Используя информацию, представленную в этой части книги, вы сможете обеспечить более гибкий контроль над структурой своих приложений, работающих с базами данных. Программирование приложений для работы с базами данных — это ключевой элемент Delphi, именно поэтому я посвятил рассмотрению связанных с этим воп- росов несколько глав книги. В следующей главе я планирую рассмотреть новый встроенный в Delphi 7 механизм формирования отчетов (reporting engine). Мы еще вернемся к разбору вопросов взаимодействия с базами данных, когда будем ана- лизировать доступ к данным через Веб в главах 20 и 21, а также при обсуждении технологий XML и SOAP в главах 22 и 23. 0816
*|О Формирование IО отчетов с использованием Rave Клиентское приложение, работающее с базой данных, позволяет вам просматри- вать и редактировать данные, однако зачастую возникает необходимость распеча- тать эти данные на бумаге или представить их в каком-либо ином виде, адаптиро- ванном для презентационных целей. Технически Delphi поддерживает распечатку данных множеством разных способов. В частности, ваше приложение может на- прямую выводить обычный текст на экран, в файл или на принтер. Кроме того, для печати вы можете использовать объект Canvas, ассоциированный с принтером, може- те воспользоваться механизмом формирования отчетов, поддерживаемым использу- емой вами базой данных, наконец, Delphi позволяет генерировать документы в раз- личных форматах, начиная от Microsoft Word и заканчивая Sun OpenOffice. В этой главе мы сосредоточимся на изучении технологий формирования отчетов (reporting). Прежде всего, темой нашего рассмотрения будет разработанный сторонним произ- водителем и включенный в состав Delphi 7 механизм формирования отчетов под на- званием Rave. Если вы заинтересованы в изучении других поддерживаемых в Delphi механизмов печати, рекомендую обратиться к моему веб-узлу, где можно найти по- лезный материал по этой теме. О моем веб-узле рассказывается в приложении В. Инструменты формирования отчетов являются важной составляющей инфра- структуры работы с данными, так как подобные инструменты сами по себе могут выполнять сложную обработку данных. Подсистема формирования отчетов мо- жет быть отдельным независимым приложением работы с БД. В данной главе мы будем рассматривать формирование отчетов как составную часть разрабатывае- мой вами программы Delphi, однако вы всегда должны помнить о том, что меха- низмы формирования отчетов обладают автономным характером. Формированию отчетов необходимо уделять самое пристальное внимание, так как отчет — это форма пользовательского интерфейса вашего приложения. Ин- формация, представленная в виде отчета, может обрабатываться вне вашего при- ложения и, возможно, обладает даже большей значимостью, чем само ваше прило- жение. Помимо пользователей, работающих с разработанной вами программой, отчет, скорее всего, будет использоваться множеством других людей. По этой причине очень важно обеспечить высокое качество отчетов, а также высокую степень гибкости архитектуры, которая позволяет пользователям формировать эти отчеты. 0817
818 Глава 18. Формирование отчетов с использованием Rave ПРИМЕЧАНИЕ-------------------------------------------------—---------— Эта глава написана при содействии Джима Гункела (Jim Gunkel) из компании Nevrona Designs, кото- рая является производителем программного продукта Rave. Здесь рассматриваются следующие вопросы: О знакомство с Rave (Report Authoring Visual Environment); О компоненты Rave Delphi; О компоненты Rave Designer; О от базы данных к отчету; О дополнительные возможности Rave. Знакомство с Rave Отчет (report) — это средство представления данных, с которыми работает прило- жение, в удобном для прочтения виде. Чтобы люди смогли работать с данными, хранящимися внутри компьютера, эти данные должны быть представлены в удоб- ной, осмысленной и информативной форме. Традиционные приложения форми- рования визуальных отчетов были ориентированы на создание листингов данных в стиле таблиц, однако в настоящее время появилась необходимость в формирова- нии отчетов более сложного вида. Продукт Rave Reports — это среда разработки визуальных отчетов, обладаю- щая множеством уникальных возможностей, которые позволяют сделать процесс формирования отчетов простым, быстрым и эффективным. Rave поддерживает огромное количество разнообразных форматов и позволяет воспользоваться мно- жеством совершенных технологий, таких как отображение (mirroring) и проч. Бла- годаря этому вы можете создавать новые отчеты, используя в качестве заготовок отчеты, сформированные вами ранее. В данной главе я кратко расскажу о возможностях Rave. Дополнительная ин- формация о Rave может быть обнаружена в файлах электронной помощи, в доку- ментации PDF, записанной на Delphi CD, в нескольких демонстрационных проек- тах, а также на веб-узле производителя по адресу www.nevrona.com. ПРИМЕЧАНИЕ------------------------------------------------------- Ключевой особенностью Rave и одной из причин, по которым компания Borland выбрала именно этот программный продукт, является возможность использования Rave как в среде Windows, так и в среде Linux. Речь идет не только о компонентах Rave, которые превосходно интегрируются вместе с VCL и CLX. Сама программа Rave Designer является кросс-платформенным приложением, которое основано на использовании CLX. Rave, Report Authoring Visual Environment Чтобы запустить среду разработки визуальных отчетов, необходимо сделать двой- ной щелчок на компоненте TrvProject, размещенном на форме приложения, или выбрать Tools ► Rave Designer в главном меню Delphi IDE. Главное рабочее окно Rave показано на рис. 18.1. Как можно видеть, окно Rave Designer разделено на несколько 0818
Знакомство с Rave 819 секций: Page Designer (Дизайнер страниц), Event Editor (Редактор событий), панель Property (Свойства), панель Project Tree (Дерево проекта), панели инструментов, Toolbar Palette (Палитра панелей инструментов) и строка состояния. I Rave Reports S.0 - f:\b&ohs\nirf74X>dg^ TjWI Text co't '&neh₽ Page Designer CvtniEtSter wirBfwrwpiteBBl &S£ Уош ’This is the Title .u^i^wuun^mi«ji!imiiMiiniii»iibi S Ф Report Library S % Report 1 Й0 Pagel Ttmii Memol Global Page Catalog 4^ Data View Dictionary “3 This is some 1*xi l*ve *dd«d to th* mtmo • theTttte And mor* text And mot* text And mar* text text And mor* t*xt And mor* text And moi* t 3.6 " And moi* tod And mor* tod. And mor* text text And mot* tod And mot* tod. And moi* mot* text And mor* t*M AAd mor* t*>d And And moi* t*xL And mar* tod INama property The name of the component The name can on^ cortam the letters AZ. 0-9 and the underscore character LI The name must be And mot* text And mor* tod And mot* text t*>d And mor* tod And mor* text And moi* moi* t*>d And mot* t*Kt And mot* text And And mote text And m«r* text And moi* text text And mor* tod And mor* text And moi* mor* text And mor* text And mote text And And mor* text And mor* tod. And mor* text Рис. 18.1. Окно Rave Designer с простым отчетом внутри ПРИМЕЧАНИЕ-------------------------------------------------------------- Каждый раз, когда вы хотите увидеть результат ваших усилий, работая с окном Rave Designer, нажмите клавишу F9 для того, чтобы увидеть предварительный просмотр текущего отчета. Помните о том, что Rave позволяет вашим конечным пользователям создавать или модифицировать свои собственные отчеты. В связи с этим Rave Designer мож- но настроить для пользователей с разным уровнем опыта. Для этого откройте диа- логовое окно Edit (Правка) ► Preferences (Предпочтения) и в разделе Environment (Окружение) выберите уровень пользователя: Beginner (Новичок), Intermediate (Пользователь со средним уровнем опыта) или Advanced (Опытный пользователь). Благодаря этому ваши конечные пользователи смогут работать на уровне, на кото- ром они чувствуют себя комфортно, и не будут обладать большими возможностя- ми, чем вы собираетесь для них предоставить. Кроме того, вы можете заблокиро- вать некоторые из возможностей, чтобы в некоторых отношениях имеющийся отчет нельзя было модифицировать. Page Designer (Дизайнер страниц) и Event Editor (Редактор событий) В центральной части окна Rave Designer располагается область проектирования стра- ницы (Page Designer). В этой области происходит визуальное проектирование отчета. 0819
820 Глава 18. Формирование отчетов с использованием Rave Кроме того, здесь же вы можете переключиться на использование редактора собы- тий (Event Editor). Этот редактор позволяет вам работать со сценариями, обеспечи- вающими реконфигурирование отчета на этапе исполнения программы. Дизайнер страниц Page Designer является наиболее заметным аспектом Rave. Этот дизайнер отображает основу вашего отчета, именно здесь вы выполняете ос- новные операции, связанные с проектированием отчета. На странице отображает- ся сетка направляющих линий, однако при желании вы можете выполнить допол- нительную настройку страницы при помощи диалогового окна предпочтений. Дизайнер позволяет проектировать несколько разных страниц. Имена этих стра- ниц отображаются над окном дизайнера (в представленном рисунке отображается всего одна страница с именем Pagel). Редактор событий (Event Editor) позволяет в виде сценария определить специа- лизированный код для компонентов формирования доклада. Каждому компонен- ту соответствует несколько различных типов событий, которые могут использо- ваться для вычислений, манипуляций со строками или какой-либо специальной логики формирования отчета. Event Editor необходим для реализации некоторых специальных возможностей Rave. Далеко не каждый отчет требует использования сценариев, поэтому я подробнее расскажу об Event Editor в конце данной главы. Панель свойств Панель свойств располагается в левой части рабочего окна. Эта панель помогает настраивать внешний вид и поведение используемых вами компонентов. Эта па- нель по своему назначению сходна с панелью Object Inspector в среде Delphi. Когда вы выбираете компонент, на панели свойств отображаются разнообразные свой- ства, ассоциированные с этим компонентом. Если ни одного компонента не выбра- но, панель свойств остается пустой. Как и в Delphi IDE, вы можете изменить значение свойства, для этого достаточ- но воспользоваться соответствующей графой редактирования, выбрать пункт из ниспадающего списка или открыть диалоговое окно редактора. Если некоторому свойству соответствует несколько вариантов выбора (напротив свойства распола- гается ниспадающий список), вместо того чтобы раскрывать ниспадающий список и выбирать один из вариантов, вы можете сделать двойной щелчок на свойстве, и значение свойства сменится следующим в списке. Панель иерархии проекта Панель иерархии проекта располагается в правой части рабочего окна Rave. Эта панель очень информативна. Она обеспечивает простой способ перемещаться по структуре разрабатываемого вами проекта. В состав иерархии входят три основ- ных узла: Report Library (Библиотека отчетов), Global Page Catalog (Глобальный ката- лог страниц) и Data View Dictionary (Словарь просмотра данных). Рассмотрим эти разделы подробнее. О Report Library (Библиотека отчетов) — в этом разделе содержатся все отчеты, входящие в проект. Каждому отчету соответствует одна или несколько стра- ниц. На каждой из страниц, как правило, располагается один или несколько компонентов. О Global Page Catalog (Глобальный каталог страниц) — позволяет управлять шаб- лонами отчетов. Шаблон может содержать в себе один или несколько компо- 0820
Знакомство с Rave 821 нентов, на его основе можно создавать разнообразные новые отчеты. Для этого примеряется поддерживаемая в рамках Rave технология отображения. Шаб- лон может содержать такие элементы, как заголовок и завершающая часть от- чета, предпечатные формы, водяные знаки или полное определение страницы для использования в дальнейших отчетах. Глобальный каталог страниц можно рассматривать в качестве репозитория — центрального местоположения эле- ментов отчетов, к которому вы хотите обеспечить доступ из разных отчетов. О Data View Dictionary (Словарь просмотра данных) — здесь определяется пред- ставление данных и другие связанные с данными объекты для отчетов. Панели инструментов и палитра панелей инструментов Как и в Delphi, в Rave присутствуют два типа панелей инструментов: панели с ком- понентами и панели IDE. Однако в отличие от Delphi (где палитра компонентов с множеством вкладок используется только для компонентов), в Rave вы можете поместить панели любого типа в открытой области в верхней части окна или при- стыковать их в состав палитры панелей со множеством вкладок. Поначалу эта про- цедура может оказаться сбивающей с толку, однако после того как вы расположи- те панели так, как вам удобно (используя команды Dock и Undock контекстного меню), вы почувствуете, что это достаточно гибкая возможность. По умолчанию в Rave присутствуют следующие палитры компонентов: Standard, Drawing, Report и Bar Code. Я подробнее расскажу об этих панелях далее в этой же главе. На текущий момент достаточно будет сказать, что в результате установки дополнительных пакетов в состав Rave могут быть добавлены дополнительные панели, кроме того, порядок компонентов на панелях можно изменить. Панели редактирования позволяют изменять или модифицировать компонен- ты проекта или существующие компоненты. Вот перечень команд, доступных на панели редактирования. О Project Toolbar (Панель проекта) позволяет создавать новый отчет, страницу и компонент объекта данных в рамках вашего проекта. Панель Project Toolbar может использоваться также для создания новых проектов, для сохранения или загрузки существующих проектов, а также для предварительного просмотра и печати текущего проекта. О Alignment Toolbar (Панель выравнивания) — на этой панели содержится множе- ство инструментов для выравнивания и позиционирования компонентов на странице. Компонент, выбранный первым, используется в качестве основного ориентира для выравнивания. Порядок вывода компонентов на печать можно изменить, воспользовавшись кнопками упорядочивания: Move Forward (Переме- стить ближе к зрителю), Move Behind (Переместить дальше от зрителя), Bring to Front (Переместить на передний план) и Send to Back (Переместить на задний план). Специальные кнопки позволяют вам перемещать компоненты на очень маленькое расстояние. О Fonts Toolbar (Панель шрифтов) может использоваться для того, чтобы изме- нить атрибуты шрифта, такие как имя, размер, стиль и выравнивание. О Fills Toolbar (Панель заливок) здесь можно выбрать стиль заливки для замкну- тых форм, таких как прямоугольники и окружности. 0821
822 Глава 18. Формирование отчетов с использованием Rave О Lines Toolbar (Панель линий) позволяет изменить толщину и стиль линий и об- рамлений. О Colors Toolbar (Панель цветов) дает возможность определить первичный и вто- ричный цвета (как правило, цвет рисования и фоновый цвет). Левая кнопка мыши выбирает первичный цвет, а правая кнопка мыши выбирает вторичный цвет. На панели присутствуют также восемь наиболее часто используемых цве- тов. В результате двойного щелчка на квадратике первичного или вторичного цвета открывается диалоговое окно редактора цветов, который позволяет вы- брать любой из возможных цветов. О Zoom Toolbar (Панель масштабирования) обеспечивает увеличение или умень- шение масштаба изображения, отображаемого в рабочем окне дизайнера стра- ницы. О Designer Toolbar (Панель дизайнера) позволяет настроить Page Designer и Rave Designer при помощи диалогового окна предпочтений (Preferences). Строка состояния В самом низу рабочего окна Rave вдоль его нижней границы располагается строка состояния. В строке состояния отображается информация о состоянии прямого соединения с данными, а также о позиции указателя мыши и размере компонен- тов. Состояние подключения к данным отображается при помощи цвета специ- ального индикатора: серый цвет означает, что подключение не активно, а зеленый цвет — что соединение активно. Желтый цвет означает ожидание ответа, а крас- ный цвет — тай-маут. Значения X и Y — это координаты указателя мыши в единицах, используемых для измерения страницы. Когда вы размещаете компонент на странице и не отпус- каете кнопку мыши, размер компонента отображается указанием значений dX и dY (d в данном случае означает «дельта», то есть разница координат). Использование компонента RvProject На рис. 18.1 показан простейший отчет, сгенерированный при помощи Rave и со- храненный в файле с расширением .rav. Чтобы подключить этот отчет к вашему приложению Delphi 7, используется страница Rave с несколькими компонентами. Центральным компонентом является компонент RvProject. Поместите этот компо- нент на форму или в модуль данных, присвойте его свойству ProjectFile имя Rave- файла и для кнопки напишите следующий код обработчика события: RvProjectl.Execute: В результате вы получите работающее приложение (в комплекте исходных файлов этот пример называется RavePrint), которое поддерживает предваритель- ный просмотр и распечатку отчета. Окно предварительного просмотра показано на рис. 18.2. Файл, на который ссылается компонент RvProject, является отдель- ным файлом, прилагаемым к программе, его можно модифицировать, при этом отчет будет изменен без необходимости изменения программы Delphi. Существу- ет также альтернативный способ: вы можете встроить содержимое файла .rav внутрь исполняемого файла программы Delphi, для этого необходимо включить RAV-фаил в состав DFM-файла. Для этого воспользуйтесь свойством StoreRAV компонента проекта Rave. 0822
Знакомство с Rave 823 F3e Page Zoom Q И » M page [i This is the Title This is some text I've added to the memo t/*• Report Preview And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And more text And Рис. 18.2. Окно предварительного просмотра отчета Rave ПРИМЕЧАНИЕ --------------------------------------------------------------- Как уже было отмечено ранее, компонент RvReport, равно как и любые другие компоненты Rave Reports, могжет быть использован как в приложениях VCL, так и в приложениях CLX. Весь механизм Rave Report является полностью кросс-платформенным. Для управления наиболее важными параметрами отчета и предварительного просмотра можно подключить компонент RvNDRWriter или RvSystem к свойству Engine компонента Rv Project. О RvNDRWriter — этот компонент при выполнении отчета позволяет генерировать файл в формате NDR. Формат NDR разработан компанией Nevrona Design и яв- ляется собственностью этой компании. Этот бинарный формат подразумевает хранение всей информации, необходимой для воспроизведения отчета во мно- жестве других распространенных форматов. О RvSystem комбинирует компонент RvNDRWriter со стандартным интерфейсом пе- чати, предварительного просмотра и преобразования в другой формат. Компо- нент RvSystem позволяет настроить множество свойств и параметров настройки пользовательского интерфейса, кроме того, некоторые события могут быть оп- ределены для того, чтобы заменить стандартные диалоговые окна видоизме- ненными версиями. Преобразование форматов Механизм Rave генерирует файл или поток данных в формате NDR, который со- здается компонентом RvNDRWriter. Встроенный в Rave механизм преобразования позволяет конвертировать это внутреннее представление во множество других 0823
824 Глава 18. Формирование отчетов с использованием Rave распространенных форматов. Чтобы предоставить пользователю возможность выбора целевого формата, поместите необходимые компоненты преобразования в форме Delphi. При обращении к методу Execute (как в примере RavePrint) вы мо- жете выбрать один из файловых форматов в диалоговом окне, отображаемом Rave и показанном на рис. 18.3. Output Option* Selected Printer----:— .rWsSeeiw1 LaserJet 3iso Adobe Acrobat i'PL>F) feb Page (HTML) Г Destination- Printer Pretdew ipe Snapshot File (NDR; Rave Snapshot File (NOR) I Native Printer Output (PRN)| Рис. 18.3. После исполнения проекта Rave пользователь может выбрать формат вывода или механизм преобразования форматов На странице Rave палитры компонентов Delphi присутствуют следующие ком- поненты преобразования форматов: О RvRenderPreview может использоваться для отображения NDR-файла или NDR- потока на экране. Компонент ScrollBox используется для отображения страниц доклада, кроме того, поддерживается множество методов и свойств, позволяю- щих создавать диалоговые окна предварительного просмотра. Если вы не нуж- даетесь в специализированных средствах предварительного просмотра, для этой цели вместо компонента RvRenderPreview рекомендуется использовать компо- нент RvSystem. о RvRenderPrinter передает NDR-файл или NDR-поток на принтер. Если вы не нуждаетесь в специализированном интерфейсе печати или предварительного просмотра, этот компонент вам не потребуется — стандартная функциональ- ность принтера реализуется в рамках компонента RvSystem. О RvRenderPDF осуществляет преобразование NDR-файла или NDR-потока в формат PDF (Adobe Acrobat). Вы можете воспользоваться свойствами и со- бытиями для дополнительной настройки вывода этого компонента, а также для поддержки сжатия. О RvRenderHTML осуществляет преобразование NDR-файла или NDR-потока в - формат HTML (DHTML). Каждая страница отчета преобразуется в отдельную HTML-страницу. При этом допускается использование шаблонов. О RvRenderRTF осуществляет преобразование NDR-файла или NDR-потока в формат RTF (Rich Text Format). О RvRenderText осуществляет преобразование NDR-файла или NDR-потока в формат обычного текста. В процессе этого преобразования многие графи- 0824
Знакомство с Rave 825 ческие команды и компоненты, такие как линии и прямоугольники, игно- рируются. Вместо того чтобы предлагать пользователю выбрать целевой формат, вы мо- жете сделать это программно. Например, чтобы преобразовать отчет в формат PDF, вы можете воспользоваться следующим кодом (позаимствованным из примера RavePrint): procedure TFormRave btnPdfClick(Sender TObject). begin RvSysteml DefaultDest » rdFile RvSysteml DoNativeOutput = False RvSysteml RenderObject = RvRenderPDFl RvSysteml OutputFileName = 'Simple2 pdf' RvSysteml SystemSetups = RvSysteml SystemSetups - [ssAllowSetup], RvProjectl Engine = RvSysteml. RvProjectl Execute. end Подключение к данным Компоненты подключений к данным обеспечивают связь между данными, содер- жащимися в приложении Delphi, и компонентом DirectDataView, используемым в Rave Designer. Имейте в виду, что значение, содержащееся в свойстве Name каждо- го из подключений, используется для формирования связи с отчетом Rave. По этой причине после создания компонентов DirectDataView в Rave менять имена компо- нентов подключений не рекомендуется. Поддерживаются следующие компоненты подключений: О RvCustomConnection обеспечивает передачу данных в отчет Rave при помощи программируемых событий. Этот компонент может использоваться для пере- дачи в отчет данных, никак не связанных с базой данных. О RvDataSetConnection подключает к компоненту DirectDataView отчета Rave любой класс, являющийся производным от класса TDataSet. При помощи свойства FieldAliasList вы можете модифицировать имена полей набора данных, заменяя их именами, более понятными для разработчиков или конечных пользователей отчета. Если вы хотите выполнить сортировку или фильтрацию для поддерж- ки внутри отчета Rave объединений таблиц или отношений типа «основное/ подробности», вы можете выполнить обработку событий OnSetSort и OnSetFilter. О RvTableConnection и RvQueryConnection — эти компоненты подключают к Direct- DataView компоненты Table и Query из набора компонентов BDE. Для подключе- ний к таблицам Rave обеспечивает собственные механизмы сортировки и филь- трации. Чтобы продемонстрировать создание отчета, связанного с базой данных, я раз- работал демонстрационную программу RaveSingle, которая является развитием программы DbxSingle (пример DbxSingle рассматривался нами в главе 14). В про- грамму добавлен проект Rave и подключение к данным: object RvDataSetConnectionl TRvDataSetConnection RuntimeVisi bi 11ty = rtDeveloper DataSet = SimpleDataSetl 0825
826 Глава 18. Формирование отчетов с использованием Rave end object RvProjectl: TRvProject ProjectFile = 'RaveSingle.rav' end Открыв Rave Designer, я создал новый проект и сохранил его в файле RaveSingle.rav в папке RaveSingle. Чтобы получить возможность обратиться к данным, содержа- щимся в программе Delphi, вам потребуется добавить в проект представление дан- ных (Data View). Для этого щелкните на кнопке New Data Object, расположенной на панели инструментов Project, выберите Direct Data View (позднее я расскажу об аль- тернативах), а затем — одно из доступных соединений. Список, который вы увиди- те, зависит от набора подключений, созданных внутри проекта, который является активным в Delphi IDE. Теперь вы можете создать отчет с использованием мастера. В главном меню Rave Designer выберите Tools ► Report Wizards ► Simple Table (Сервис ► Мастера от- четов ► Простая таблица). Выберите представление данных (Data View) — если вы правильно выполнили все описанные действия, на текущий момент у вас должно быть только одно представление данных. В следующем окне необходимо выбрать поля набора данных, которые вы хотели бы включить в состав отчета. Вы должны увидеть отчет, подобный тому, который показан на рис. 18.4. Рис. 18.4. Отчет RaveSingle (сгенерированный при помощи мастера) на этапе проектирования В иерархии проекта можно видеть, что мастер сгенерировал страницу отчета, на которой располагается регион (Region) с тремя полосами (компонентами Band): для заглавия, для заголовка таблицы и для отображения данных в таблице. Позднее, в разделе «Регионы и полосы», я подробнее рассмотрю использование этих компонентов. На текущий момент я хочу продемонстрировать вам созда- ние простого рабочего примера, с которым вы можете самостоятельно поэкс- периментировать. 0826
Компоненты Rave Designer 827 Компоненты Rave Designer В предыдущем разделе мы обсудили компоненты Rave, доступные в Delphi. Одна- ко при формировании отчета большая часть работы выполняется в дизайнере от- четов. Базовым элементом любого отчета является компонент RaveProject (его мож- но видеть в корне иерархии проекта). Этот компонент также называют диспетчером проекта (Project Manager). Его не надо располагать непосредственно на отчете, так как в каждом отчете присутствует только один объект этого типа (пример- но так же, как в любом приложении Delphi присутствует только один объект Application). Компонент RaveProject является владельцем всех остальных компонентов, вхо- дящих в состав отчета. Как и любой другой компонент в Rave, компонент RaveProject обладает набором свойств. Чтобы увидеть значения этих свойств, в иерархии про- ектов выберите RaveProject и взгляните на панель свойств. Чтобы создать новый компонент RaveProject, щелкните на кнопке New Project (Создать проект), располо- женной на панели инструментов Project. В результате будет создан новый файл, а также новый компонент RaveProject. В состав каждого проекта может входить несколько отчетов, каждому из кото- рых соответствует компонент Report. В компоненте Report содержатся страницы отчета. В одном проекте может быть несколько отчетов, и каждому отчету может соответствовать несколько страниц. Каждой странице соответствует компонент Раде. Компонент Раде — это базовый визуальный компонент, на котором вы може- те располагать визуальные компоненты отчета. Именно здесь выполняется визу- альное проектирование отчета и формирование его внешнего вида. Все отчеты, входящие в состав проекта, перечисляются в разделе Report Library иерархии проекта. Однако помимо раздела Report Library в составе иерархии присут- ствует также глобальный каталог страниц (Global Page Catalog) и словарь представ- лений данных (Data View Dictionary). Словарь представлений данных — это подроб- ный список каналов, через которые приложение Delphi может передавать данные в проект Rave (для передачи данных используются рассмотренные ранее компо- ненты подключений Rave). Помимо этого данные в отчет могут поставляться на- прямую из базы данных. В процессе проектирования отчета вы размещаете визуальные компоненты либо на самой странице, либо внутри какого-либо контейнера, такого как Band (Полоса) или Region (Регион). Некоторые из компонентов могут быть никак не связанными с базой данных, например такие, как Text, Memo и Bitmap, расположенные на панели Standard (Стандартные). Другие компоненты специально предназначены для ра- боты с данными, то есть их можно соединить с полями таблицы базы данных. К ним относятся, например, DataText и DataMemo, расположенные на панели Report. Базовые компоненты На панели инструментов Standard (Стандартные) располагается семь компонен- тов: Text, Memo, Section, Bitmap, Metafile, FontMaster и PageNumlnit. Многие стандарт- ные компоненты часто используются при формировании отчетов. 0827
828 Глава 18. Формирование отчетов с использованием Rave Компоненты Text и Мето Компонент Text используется для отображения в составе отчета единственной стро- ки текста. Этот компонент выполняет функции метки, в которой содержится един- ственная строка текста (но не данные). Когда вы размещаете этот компонент в от- чете, вокруг него автоматически обрисовывается прямоугольник, обозначающий его границу. Компонент Мето похож на компонент Text, однако любой компонент Мето мо- жет содержать в себе несколько строк текста. Если вы указали имя шрифта для компонента Мето, весь текст, отображаемый в этом компоненте, будет отображаться с использованием указанного вами шрифта (как при использовании компонента Мето в Delphi). После того как вы ввели текст, размеры компонента Мето можно изменить, при этом текст внутри этого компонента будет реформатирован подхо- дящим образом. Если часть текста в компоненте Мето не отображается, попро- буйте изменить размер этого компонента таким образом, чтобы весь текст стал видимым. Компонент Section Компонент Section используется для того, чтобы группировать компоненты (ана- логичную функцию выполняет компонент Panel в среде Delphi). Все компоненты, расположенные в рамках секции, можно перемещать с места на место при помощи единственного щелчка мышью. При работе с компонентами Section удобно использовать иерархию проекта (Project Tree). Раскрыв соответствующий узел, легко увидеть, какие компоненты располагаются в той или иной секции. СОВЕТ----------------------------------------------------------- Компонент Section важен при выполнении отображения, то есть когда вы формируете дизайн отче- та на основе уже сформированной заготовки. Графические компоненты Компоненты Bitmap и Metafile позволяют вам добавлять изображения в состав от- чета. Компонент Bitmap поддерживает файлы растровых изображений с расшире- нием .bmp, а компонент Metafile поддерживает файлы векторных изображений с расширениями .wmf и .emf. Метафайлы не поддерживаются в случае, если вы используете Rave в приложениях CLX, так как метафайлы основаны на использо- вании специальной внутренней технологии Windows. Компонент FontMaster Каждый компонент Text, присутствующий в отчете, обладает свойством Font. При- своив значение этому свойству, вы можете назначить шрифт, которым будет ото- бражаться текст. Во многих случаях бывает удобно назначить один и тот же шрифт одновременно нескольким объектам. Конечно же, для этой цели вы можете выде- лить одновременно несколько компонентов, однако при этом вам придется помнить о том, какие именно компоненты должны поддерживать шрифт, обладающий за- данной гарнитурой, заданным размером и заданным начертанием. Задача еще бо- лее усложняется, если эти компоненты размещаются в разных отчетах. 0828
Компоненты Rave Designer 829 Решить проблему можно с использованием компонента FontMaster. Этот ком- понент позволяет определить стандартный шрифт для различных частей отчета, таких как заголовок, тело и завершающая часть (footer). Компонент FontMaster яв- ляется невизуальным компонентом (об этом свидетельствует зеленый цвет кноп- ки), поэтому (в отличие от Delphi IDE) на странице отчета не будет никакой ссылки на этот компонент. Как и в случае с любыми другими невизуальными компонента- ми Rave, обратиться к нему можно только с использованием окна иерархии проек- та (Project Tree). После того как вы настроили свойство Font компонента FontMaster, вы можете легко подключить его к телу текста. Выберите компонент Text/Memo и при помощи панели свойств настройте свойство FontMirror таким образом, чтобы его значение указывало на один из компонентов FontMaster. Все компоненты, связанные с Font- Master, будут использовать шрифт, указанный в свойстве Font этого компонента. Имейте в виду, что когда вы настраиваете свойство FontMirror некоторого ком- понента, свойство Font этого компонента будет переопределено значением свой- ства Font подключенного вами компонента FontMaster. Кроме того, для компонента, связанного с FontMaster, автоматически отключается панель инструментов Font (шрифт). На одной странице может располагаться более чем один компонент FontMaster. Рекомендуется присваивать этим компонентам информативные имена, объясня- ющие их предназначение. Кроме того, вы можете поместить все эти компоненты на глобальной странице (Global Page), благодаря чему к ним можно будет обратить- ся из любого отчета, входящего в состав проекта. Номера страниц PageNumlnit — это невизуальный компонент, который позволяет вам заново начать нумерацию страниц внутри отчета Rave. Он используется примерно так же, как и другие невизуальные компоненты. Необходимость в использовании PageNumlnit возникает в случае, когда требуется обеспечить какое-либо сложное форматиро- вание отчета. Например, представьте, что вы формируете отчет с информацией о заказчике. Каждый месяц каждый из заказчиков вашей компании получает подобный отчет. В разные месяцы отчет может состоять из разного количества страниц. Представь- те, что на первой странице содержится общая информация о заказчике, на вто- рой — информация о кредитах и депозитах, а на третьей — информация об удерж - ках и дебитах. Скорее всего, для первых двух частей отчета потребуется не более чем по одной странице. Но что касается третьей части отчета, если заказчик ведет себя достаточно активно, информация об удержках и дебитах может не уместить- ся на одной странице, и тогда вам потребуется добавить в отчет дополнительные страницы. Пользователь, работающий с этим отчетом, может пожелать, чтобы ну- мерация страниц в каждом разделе отчета начиналась заново. В этом случае в пер- вой и во второй частях отчета страницы будут пронумерованы так: 1 of 1 (1 из 1). Однако в третьей части отчета, соответствующего активному заказчику, может со- держаться не одна, а, например, три страницы. В этом случае страницы будут про- нумерованы следующим образом: 1 of 3, 2 of 3 и 3 of 3. Для реализации подобной нумерации используется компонент PageNumlnit. 0829
830 Глава 18. Формирование отчетов с использованием Rave Компоненты рисования Как и другие стандартные компоненты, компоненты рисования не связаны с дан- ными. Rave поддерживает работу с компонентами линий: универсальные линии могут быть проведены в любом направлении, горизонтальные и вертикальные ли- нии обладают фиксированным направлением. Помимо этого поддерживаются ком- поненты геометрических фигур, включая квадраты, прямоугольники, окружности и эллипсы. Вы можете добавить форму на страницу отчета, а затем разместить по- верх нее какой-либо другой элемент. Например, вы можете разместить прямо- угольник так, чтобы он обрамлял собою компонент DataBand. Компоненты штрих-кода Компоненты штрих-кода (Bar code) используются для добавления в доклад разно- образных штрих-кодов. Если вы хотите добавить в отчет штрих-код, вы должны представлять себе, как формируются и как используются штрих-коды. Чтобы определить значение, которое будет отображаться при помощи штрих-кода, перей- дите на панель свойств и введите это значение в качестве значения свойства Text. Rave поддерживает несколько разновидностей штрих-кодов: О POSTNET (POSTal Numeric Encoding Technique) используется почтовой служ- бой Соединенных Штатов Америки; О I2of5BarCode (interleaved 2 of 5) — только для численной информации; О Code39BarCode позволяет кодировать десятичные цифры, заглавные буквы и несколько специальных символов; О Codel28BarCode — код с высокой плотностью полосок, позволяющий кодиро- вать все 128 символов ASCII; О UPCBarCode (Universal Product Code) поддерживает фиксированную длину 12 разрядов и используется для кодирования продуктов; О EANBarCode (European Article Numbering System) идентичен UPC, однако под- держивает фиксированную длину 13 разрядов: 10 цифровых символов, 2 сим- вола, обозначающих код страны, и проверочный разряд. Объекты доступа к данным В большинстве случаев отчет базируется на данных, которые извлекаются либо напрямую из базы данных, либо из приложения Delphi. Данные могут быть извле- чены из набора данных, подключенных к базе данных, или от некоторого программ- ного кода внутри приложения Delphi. Когда вы щелкаете на кнопке New Data Object (Создать объект данных), расположенной на панели Project, па экране появляется список возможных вариантов выбора (см. рисунок на следующей странице). Рассмотрим эти варианты подробнее: О Компонент RaveDataBase (Database Connection) обеспечивает параметры подключения к базе данных для компонента DriverDataView. Доступны будут только те соединения с базой данных, для которых установлены драйверы DataLink. О Компонент RaveDirectDataView (Direct Data View) позволяет извлекать дан- ные из компонента подключения к данным, расположенного внутри приложе- 0830
Компоненты Rave Designer 831 ния Delphi, как в последнем рассмотренном примере. Соответствующий пункт списка называется Direct Data View (то есть представление данных с прямым под- ключением), однако прямое подключение в данном случае не используется. Вместо этого применяется подключение, основанное на данных, извлекаемых из БД основным Delphi-приложением. Такое подключение нельзя назвать пря- мым. Data Connections ...................... 0 Data Lookup Security Controller © Database Connection © Direct Date View © Driver Data View § Simple Security Controler О Компонент RaveDriverDataView (Driver Data View) позволяет определить запрос на специальное подключение к базе данных с использованием языка зап- росов, такого как SQL. Для формирования запроса открывается специальное окно редактора запросов. О Компонент RaveSimpleSecurity (Simple Security Controller) реализует базо- вый механизм защиты, основанный на простом списке пользовательских имен с соответствующими им паролями. Список содержится в свойстве UserList. В каждой строке списка UserList содержится имя пользователя и пароль в формате User Field = password. Существует специальное булевское свойство CaseMatters, которое определяет, является ли пароль чувствительным к ре- гистру символов. О Компонент RaveLookupSecurity (Data Lookup Security) позволяет сверять пользовательское имя и пароль с записями в таблице базы данных. Свойство Data View содержит ссылку на компонент Data View, который будет использовать- ся для поиска имени и пароля. Свойства UserField и Password Field используются для хранения проверяемых имени и пароля. Регионы и полосы Компонент Region (Регион) является контейнером для компонентов Band (Поло- са). В самой простой форме компонент Region может соответствовать всей страни- це отчета целиком. Подобное происходит в случае, если отчет выглядит подобно списку. Многие отчеты, отображающие информацию, представленную в виде «ос- новное/подробности», могут быть выполнены с использованием единственного региона. Однако не следует думать, что на странице может располагаться только один регион. Свойства компонента Region позволяют менять размер этого компо- 0831
832 Глава 18. Формирование отчетов с использованием Rave нента и его расположение на странице. Регионы помогут обеспечить значитель- ную гибкость при формировании сложных отчетов. На одной странице можно раз- местить несколько регионов. Регионы могут быть расположены друг рядом с дру- гом, они могут налагаться друг на друга или даже выходить за границы страницы. СОВЕТ-------------------------------------------------------------------- Не следует путать компонент Region с компонентом Section. Компонент Region может содержать в себе только компоненты Band. Компонент Section может содержать в себе любую группу компонен- тов, включая компоненты Region, однако компонент Section не может напрямую содержать в себе компоненты Band. Когда вы работаете с компонентами Band, вы должны помнить одно важное пра- вило: любой компонент Band должен располагаться внутри компонента Region. Обратите внимание, что количество регионов на странице не ограничено, кроме того, никак не ограничивается также количество компонентов Band в регионе. Если вы можете представить себе визуальный внешний вид вашего отчета, используя комбинацию регионов и полос, вы сможете реализовать любые ваши мысли в ре- альном дизайне. Существует два типа компонентов полос: О DataBand используется для отображения информации итеративного характера из DataView. Как правило, DataBand содержит в себе несколько компонентов DataText. Свойство DataView компонента DataBand должно ссылаться на компо- нент DataView, данные из которого будут отображаться в рамках DataBand; О Band используется для отображения заголовка (header) и завершения (footer) в рамках региона. Существует несколько типов заголовка и завершения, как то: Body (Тело), Group (Группа) и Row (Строка). Выбор типа осуществляется при помощи свойства BandStyle. Заголовок и завершение страницы не являются ком- понентами Band — их можно поместить вне региона прямо на страницу. Важным свойством компонента Band является ControilerBand. Это свойство оп- ределяет, к какому компоненту DataBand принадлежит компонент Band, то есть каким компонентом DataBand управляется данный компонент Band, После того как вы настроите контролирующий компонент DataBand, обратите внимание, что гра- фический символ на компоненте Band указывает в направлении контролирующе- го компонента DataBand, при этом цвет символов обоих компонентов совпадает. О символьных кодах, отображаемых на компоненте, рассказывается в следующем разделе. Редактор стиля полос (Band Style Editor) Перейдите к свойству BandStyle компонента Band или DataBand и щелкните на кнопке с многоточием для того, чтобы открыть окно редактора стиля Band Style Editor (см. рисунок на следующей странице). Этот редактор обеспечивает простой метод выбора возможностей, которыми должен обладать выбранный вами компонент Band. Для выбора возможностей до- статочно установить соответствующие флажки. Обратите внимание, что для ком- понента Band можно активировать несколько возможностей одновременно. Это означает, что один и тот же компонент Band может выполнять функции одновре- менно заголовка (Body Header) и завершения (Body Footer). В левой части окна Band Style Editor отображается порядок размещения компо- нентов на странице отчета. Компоненты DataBand повторяются три раза, чтобы от- 0832
Компоненты Rave Designer 833 метить тот факт, что они повторяются. Редактируемый компонент Band выделен: в левой части окна может отображаться несколько компонентов Band, однако ре- дактировать можно только один из них (тот, который выделен). [Band Style Editor tWffi fry tW Y DataViewl TitleBand(S) V DataViewIBand IB) ф DataViewl DataBand (Waste ф DataViewl DataBand (Waste , ф DataViewl DataBand (Waste H 'sh ' JSl: DO I f Pnntto«*®n ' " S' &xlr Header® J“ group Header^») ; 1Г gawHeacterfra i Г gem® i Г" RowFaoter (rj j Г erouE footer (й j Г fooler ф) FtW Ooo^fence S' SrstCO S' gew₽«8e(₽5 Г Wgw Column (CJ Для каждого компонента в левой части редактора стиля полос указывается буква или символ, которые информируют пользователя о поведении того или иного компонента. Аналогичные буквы и символы отображаются в области Page Layout дизайнера Rave (пример показан на рис. 18.6). В окне редактора Band Style Editor компоненты Band расставлены в том логическом порядке, в котором они были раз- мещены на странице отчета на этапе проектирования. Последовательность отобра- жения компонентов Band во многом определяется порядком, в котором они распо- ложены в окне редактора Band Style Editor. Вначале на печать выводятся заголовки (им соответствуют заглавные буквы BGR, которые обозначают Body, Group и Row, то есть «тело», «группа» и «строка» соответственно). За заголовками на печать выводится компонент DataBand. После него следуют завершения (маленькие буквы bgr) для каждого уровня. Однако если для некоторого уровня определено несколько заголовков, тогда заголовочные ком- поненты Band обрабатываются в том порядке, в котором они расставлены в компо- ненте Region. Таким образом, можно разместить все заголовки вверху, все компо- ненты DataBand — в середине, а все завершения — в нижней части региона для всех уровней отчета «основное/подробности». Кроме того, вы можете распределить за- головки между уровнями. Для обозначения отношений «предок-потомок» или «основное/подробности» между различными компонентами Band используются два символа: О символ треугольника (в виде стрелочки, направленной вверх или вниз) указы- вает на то, что компонент Band контролируется другим компонентом Band, об- ладающим тем же самым цветом (уровнем). Контролирующий компонент рас- полагается по направлению стрелки; О символ ромбика обозначает контролирующий компонент Band. 0833
834 Глава 18. Формирование отчетов с использованием Rave Имейте в виду, что при желании можно сформировать отношения типа «основ- ное/подробности/подробности», при этом один основной компонент может конт- ролировать два компонента, отображающих подробности, или один из компонен- тов, отображающих подробности, может контролироваться другим компонентом, отображающим подробности. Компоненты, связанные с данными Внутри компонента DataBand можно разместить несколько разных компонентов связи с данными. Чаще всего используется компонент DataText, который позволяет отобразить значение текстового поля из набора данных (как продемонстрировано в примере RaveSingle). Для настройки свойства DataField можно воспользоваться одним из двух вари- антов. Во-первых, вы можете выбрать одно из полей, воспользовавшись ниспада- ющим списком. Этот способ подходит в случае, если в рамках одного компонента DataText требуется отобразить значение только одного текстового поля. Но что если вы хотите отобразить текст, являющийся комбинацией нескольких полей? Для этой цели можно воспользоваться специальным редактором Data Text Editor. Data Text Editor Во многих отчетах требуется отобразить текст, являющийся комбинацией несколь- ких полей набора данных. Наиболее простыми примерами являются страна, город и почтовый индекс или имя и фамилия. В коде Delphi для отображения такого текста используются выражения, подобные следующим: City + + State + ' ' + Zip FirstName & LastName Для редактирования свойства DataField компонента DataText используется спе- циальный редактор Data Text Editor, показанный на рис. 18.5, который помогает вам формировать комбинированные поля. Чтобы открыть окно этого редактора, щелкните на кнопке с многоточием. Редактор позволяет выполнять конкатенацию полей, параметров или переменных и тем самым формировать сложное текстовое поле. Выбирая значения в ниспадающих списках, вы можете формировать слож- ное выражение для формируемого вами текстового поля. Редактор позволяет ис- пользовать множество комбинаций, здесь я рассмотрю их весьма поверхностно, а вы сможете попробовать их на практике. Обратите внимание на то, что диалоговое окно разделено на пять групп: Data Fields (поля данных), Report Variables (переменные отчета), Project Parameters (пара- метры проекта), Post Initialize Variables (переменные постинициализации) и Data Text. В графе Data Text отображается результат ваших действий: при добавлении новых элементов смотрите на содержимое этой графы. Справа от окошка Data Text распо- лагаются две кнопки: «плюс» (+) и «амперсанд» (&). Кнопка «плюс» соединяет два элемента вместе без пробелов, а кнопка «амперсанд» выполняет конкатена- цию с единственным пробелом (если только предыдущее поле не было пустым). Таким образом, вы выбираете «плюс» или «амперсанд», а затем выбираете текст в одной из групп, расположенных выше окошка Data Text. Компонент DataText не только позволяет отображать в составе отчета данные из базы данных, вы также можете использовать этот компонент для отображения 0834
Компоненты Rave Designer 835 параметров проекта и переменных отчета. Перейдите к группе Report Variables (пе- ременные отчета), раскройте ниспадающий список, чтобы узнать, какие перемен- ные доступны для отображения. В списке Project Parameters (параметры проекта) могут содержаться параметры UserName (имя пользователя), ReportTitle (наимено- вание отчета) и UserOptions (параметры пользователя). Эти переменные инициа- лизируются приложением. Чтобы создать список параметров проекта, раскройте раздел Project (самый верхний пункт) в иерархии проекта. После этого на панели свойств щелкните на кнопке с многоточием напротив свойства Parameters (пара- метры), для того чтобы открыть редактор строки. Здесь вы можете указать пара- метры, которые будут переданы из приложения в отчет Rave (такие как UserName). Data Text Editor l~PeteR«ffc---------- > DataVev L РеГеьг etaView'i , ВйаНеИ ЕМР.ЫО Selectee , DataView' ^Report Vartatte? - CurrentPage _»] meert Report vet |--Ргф« Parameter» Post Initiate Vam Insert PI Var й Pew Text FIRSTJ4AME Рис. 18.5. Рабочее окно редактора Data Text Editor Компонент DataMemo Компонент DataMemo отображает содержимое поля Memo из DataView. Основное отличие между DataMemo и DataText заключается в том, что компонент DataMemo позволяет отобразить текст, не помещающийся на одной строке, то есть компо- нент DataMemo переносит текст со строки на строку. Например, компонент DataMemo можно использовать для того, чтобы напечатать комментарий о заказчике в ниж- ней части каждой страницы счета. Компонент DataMemo часто используют для слияния почтовых адресов. Для это- го необходимо настроить свойства DataView и DataField на источник данных для поля Мето. После этого необходимо щелкнуть на кнопке с многоточием рядом со свой- ством MailMergeltems — в результате будет запущен редактор Mail Merge Editor. Этот редактор позволяет выбрать изменяемые элементы поля Мето. 0835
836 Глава 18. Формирование отчетов с использованием Rave Чтобы воспользоваться редактором Mail Merge Editor, щелкните на кнопке Add. В поле Search Token укажите элемент, входящий в Мето, который должен быть из- менен. После этого либо введите заменяющую строку в окне Replacement, либо щел- кните на кнопке Edit, чтобы запустить редактор Data Text Editor, который помо- жет вам выбрать разнообразные компоненты DataView и поля. Вычисление итоговых значений Компонент CalcText позволяет вычислять численное значение на основе некоторо- го набора значений. В отличие от компонента DataText компонент CalcText специ- ально предназначен для выполнения вычислений и отображения результатов. Свойство CalcType определяет тип вычислений. Этому свойству можно присво- ить одно из следующих значений: Average (среднее), Count (счетчик), Maximum (максимальное), Minimum (минимальное) и Sum (сумма). Например, вы можете вос- пользоваться этим компонентом для того, чтобы отобразить итоговую сумму выс- тавленного счета в верхней части каждой из страниц счета. Свойство CountBlanks определяет, должны ли пустые поля участвовать в вы- числении значений Average и Count. Если свойство RunningTotal равно True, значит, зна- чение не будет переустанавливаться в 0 каждый раз, когда оно выводится на печать. Циклический перебор данных внутри страниц Компонент DataCycle фактически является невидимой разновидностью компонен- та DataBand. Этот компонент предоставляет компоненту Раде возможности после- довательного перебора данных. DataCycle полезен для формирования отчетов в стиле формы, когда использование регионов и полос является слишком тяжеловесным подходом. Важно убедиться в том, что компонент DataCycle отображается перед любым расположенным на странице компонентом, осуществляющим обработку информации из того же представления данных DataView. Дополнительные возможности Rave Теперь, когда я познакомил вас с основами Rave, вы, должно быть, понимаете, что эта система является чрезвычайно сложной. Я мог бы написать отдельную книгу, посвященную Rave. На текущий момент я уже продемонстрировал вам пару при- меров использования Rave. Можно было бы продолжить рассматривать примеры построения разнообразных отчетов с комплексной структурой, однако надеюсь, что, используя специальные мастеры и ознакомившись с материалом этой главы, вы сможете сделать это самостоятельно. Теперь я продемонстрирую построение от- чета типа «основное/подробности», а также расскажу о некоторых возможностях Rave, при самостоятельном изучении которых у вас могут возникнуть проблемы. ПРИМЕЧАНИЕ---------------------------------------------------— Я не упомянул о многих интересных возможностях Rave. В частности, Rave позволяет включать в состав разработанного вами приложения дизайнер отчетов, благодаря чему конечные пользовате- ли приложения получают возможность самостоятельно редактировать отчет. Дизайнер отчетов можно либо приложить к основной программе в виде отдельной дополнительной утилиты, либо включить в состав основного приложения Delphi. Существует также серверная версия Rave, которая позволя- ет отображать отчеты, расположенные на веб-сервере. 0836
Дополнительные возможности Rave 837 СОВЕТ---------------------------------------------------------------------------------— Чтобы получить дополнительную информацию об этих и других возможностях Rave, обратитесь к веб-узлу компании Nevrona. Сведения о возможностях Rave можно почерпнуть из коллекции полез- ных советов, расположенной по адресу www.nevrona.com/rave/tips.shtml. Отчеты типа «основное/подробности» Чтобы создать в Rave отчет типа «основное/подробности», в соответствующем приложении Delphi должны присутствовать два набора данных, однако в самой программе между ними вовсе не обязательно устанавливать отношения типа «ос- новное/подробности» — эти отношения могут быть установлены внутри отчета Rave. В демонстрационной программе RaveDetails доступ к каждому из наборов данных осуществляется с использованием подключения Rave: object dsDepartments: TSimpleDataSet Connection = SQLConnectionl DataSet.CommandText = 'select * from DEPARTMENT' end x object dsEmployee: TSimpleDataSet Connection = SQLConnectionl DataSet CommandText = 'select * from EMPLOYEE' end object RvConnectionDepartments: TRvDataSetConnection DataSet = dsDepartments end object RvConnectionEmployee- TRvDataSetConnection DataSet = dsEmployee end В отчете присутствуют два соответствующих представления данных, каждое из которых подключено к компоненту DataBand (оба компонента располагаются внутри региона). Первый компонент DataBand ассоциирован с основным набором данных и не обладает какими-либо специальными настройками. Второй компо- нент DataBand определяет отношения типа «основное/подробности», используя несколько свойств. Свойство MasterDataView ссылается на представление данных основного набора данных, а свойства MasterKey и DetailKey ссылаются на поля, при помощи которых определяется объединение (в данном случае оба этих свойства ссылаются на поле DEPT_NO). Свойство ControLlerBand ссылается на компонент Data- Band, который отображает данные из основного набора данных. Если вы имеете дело с отчетом типа «основное/подробности», наиболее важ- ные настройки выполняются с использованием редактора Band Style Editor, в кото- ром вы должны настроить полосу как полосу, содержащую подробности. Вид ра- бочего окна этого редактора для примера RaveDetails показан на рис. 18.6. Если вы не хотите, чтобы отображаемые подробности переходили на следующую страницу (в этом случае основные данные и часть подробностей будут располагаться на раз- ных страницах), присвойте свойству KeepRowTogether значение True. ВНИМАНИЕ--------------------------------------------------------------------— Для создания отчета типа «основное/подробности» можно воспользоваться специальным масте- ром, запускаемым из Rave. К сожалению, в версии Rave, входящей в состав Delphi 7, этот мастер не работал. На момент написания книги не было опубликовано никакого исправления или обновления, устраняющего эту проблему, однако в будущем, возможно, такое исправление или обновление станет возможным. 0837
838 Глава 18. Формирование отчетов с использованием Rave jusmgtneBanaatyietaxor oemescne | locations and occurrences where the band 5 wi pre* A band can be set to prrt I multiple locations and occurrences The НчмГл*, влсммл» о ‘Х.Л» v-at n I а» | Гап. | - т - к «О» ВВВВ | 0/уRdveProject Prsr* s ’ Г So*He«tor®) । I ! Г i Г Swl*«*(r» | | у j Г Vunraurtr) jF Oduefedi«(i0 IF ®<MX footed) MntOMffim ’ F yewPegeCP) } F NE*Cefc»hte> ’ Simple Table- lYBiHiawnfa»»w»!M......... DEPARTMENT BudgeVSalar iDEPARTMEMT | I But .W В ф Report Ltxary В ^Report HJ Q Marfage ф Globd Page Catalog S Data View Dictionary В 0 DataViewl & DataViewlOEPT.NO & DataViewlDEPARTM & DataVewlHEAD.DE W DataVewlMNGR.NC Й DataViewIBUDGET # DataVewlLOCATIOS. DataViewIPHONE.N' В 0 DateVew2 fcf DataVew2£MP_N0 : & OatsWewStRST.HA/ # DateView2LAST_NA> & DataVew2PHDNE_E & DataVew2HIRE.DAT. SDataView2DEPT.N0 Й DataVewilOB.CODf S DataVewZIOB.GRAt & DataVewilOB.COUr & DataView2SALARY й DataView2FULL_NAK Рис. 18.6. Отчет типа «основное/подробности». На переднем плане рабочее окно редактора Band Style Editor Отчеты со сценариями В начале главы я упомянул об окне Event Editor дизайнера Rave, однако до нынеш- него момента я еще ни разу не воспользовался этим окном. Этот инструмент при- меняется для того, чтобы включать в состав отчета исполняемый код (сценарии), который выполняется в результате генерации разнообразных событий, связанных с различными компонентами (как в Delphi). При помощи сценариев вы можете в еще большей степени контролировать генерацию отчета Rave. Язык, используе- мый для написания сценариев, основан на языке Pascal, его можно считать вари- антом языка Delphi. Rave Details выделяет полужирным шрифтом заработные платы, превышающие определенное значение. Чтобы реализовать это, очевидно, необходимо написать код сценария, который выполнялся бы для каждого элемента полосы подробнос- тей, иными словами, для каждой записи в таблице сотрудников. Вместо того что- бы напрямую изменять значение свойства Font, я решил добавить в отчет два раз- личных компонента FontManager: компонент с именем fmPlainFont соответствует обычному шрифту, а компонент с именем fmBoldFont соответствует полужирному шрифту. Чтобы осуществить выделение некоторых значений полужирным шрифтом, необходимо выполнить обработку события Before Print. Чтобы написать обработ- чик этого события, перейдите на страницу Event Editor, выберите компонент DataText, подключенный к полю Salary, а затем выберите событие. В окне редактирования кода введите следующее: 0838
Дополнительные возможности Rave 839 if DataView2Salary.AsFloat > 100000 then self.FontMirror fmBoldFont: else self.FontMirror fmPlamFont; end if. Сценарий изменяет значение свойства FontMirror текущего объекта (self) таким образом, чтобы это свойство ссылалось на один из двух компонентов FontManager, расположенных на странице. Выбор компонента FontManager определяется значе- нием поля. Обратите внимание на то, что DataView2Salary — это ссылка на одно из полей представления данных, подключенных к текущему компоненту DataText. От- компилируйте сценарий и посмотрите на результирующий отчет, показанный на рис. 18.7. Report Preview Simple Table Report DEPARTMENT Budget/Salary LOCATION Corporate Headquarters t,000,000 00 Monterey Bender, Otver H 53,733 00 212,85PM Sales and Marketing 2,000,000 00 San Francisco MacDonald, MaryS Yanowski, Mehael 111,262.50 44,000 00 Engineering 1,100,000 00 Monterey Nelson. Robed Brown, Kefy 105,900.00 27.00GJX) Рис. 18.7. Полужирный шрифт используется для некоторых значений благодаря выполнению внутреннего сценария ВНИМАНИЕ----------------------------------------------------------------- Каждый раз после редактирования сценария не забывайте щелкать на кнопке Compile, в противном случае внесенные вами изменения не будут иметь эффекта. Отражение (Mirroring) Механизм отражения позволяет вам менять внешний вид отчета в зависимости от значения того или иного поля или полей. Для этой цели используются шаблоны Rave. Каждый шаблон может содержать в себе один или несколько компонентов. Шаблоны можно повторно использовать при помощи встроенной в Rave техноло- гии отражения (mirroring). Компонент DataMirrorSection предназначен для отобра- жения других компонентов Section в зависимости от содержимого DataField. Благо- даря использованию отображенных секций компонент DataMirrorSection может быть чрезвычайно гибким. Следует помнить о том, что компонент Section может содер- жать любые другие компоненты, включая графику, регионы, текст и проч. 0839
840 Глава 18. Формирование отчетов с использованием Rave Например, при помощи компонента DataMirrorSection вы можете сделать так, чтобы один и тот же отчет генерировал разные форматы конвертов для американ- ской почтовой службы и для международной почты. В частности, конверт, пред- назначенный для иностранного заказчика, содержит в центре наименование стра- ны. В отличие от него конверт, предназначенный для американского заказчика, вообще не содержит наименования страны, а целевой почтовый адрес располага- ется в правой части и ближе к нижней кромке конверта. Естественно, один из форматов будет использоваться в качестве формата по умолчанию. Если формат по умолчанию не определен, а значение поля не соответ- ствует ни одному из допустимых вариантов, тогда используется обычное содер- жимое компонента DataMirrotSection. Комплексные вычисления Помимо простого компонента CalcText, о котором рассказывалось ранее, дизайнер Rave поддерживает три дополнительных компонента для выполнения более слож- ных вычислений. Это компоненты CalcTotal, CalcControlter и CalcOp. CalcTotal CalcTotal — это невизуальная версия компонента CalcText. Когда этот компонент выводится на печать, его значение сохраняется в параметре проекта (который оп- ределяется в свойстве DestParam) и форматируется в соответствии со свойством DisplayFormat. Компонент CalcTotal полезен в случае, если результат выполнения вычисления в дальнейшем используется для выполнения других вычислений. Если результат работы CalcTotal используется только в других компонентах CalcOp и боль- ше нигде, вы можете оставить значение свойства DestParam пустым. CalcController CalcController — это невизуальный компонент, который выполняет функции кон- троллера для компонентов CalcText и CalcTotal. Компоненты CalcText и CalcTotal свя- зываются с компонентом CalcController при помощи свойства Controller. В момент печати компонент CalcController информирует все вычислительные компоненты о том, что необходимо выполнить соответствующие им операции вычисления. Бла- годаря этому можно выполнять согласованные вычисления в отношении группо- вых полос (Group Bands), подробных полос (Detail Bands) или всей страницы цели- ком — в зависимости от того, где располагается компонент CalcController. Компонент CalcController также выполняет инициализацию компонентов CalcText и/или CalcTotal специфическим значением (при помощи свойств InitCalcVar, InitDataField и InitValue). Компонент CalcController инициализирует значения только в случае, если на него ссылается свойство Initializer компонента CalcText или CalcTotal. CalcOp CalcOp — это невизуальный компонент, который выполняет некоторую операцию в отношении значений, полученных из различных других источников данных. Опе- рация определяется свойством Operator этого компонента. Результат может быть сохранен в параметре проекта. Имя и формат параметра определяются значения- ми свойств DestParam и DisplayFormat. 0840
Что далее? 841 Например, представьте, что вы намерены сложить два компонента DataText. Иначе говоря, компонент CalcOp будет выполнять операцию А + В = С, где А и В — это значения двух компонентов DataText, а С — это результат сложения, сохранен- ный в параметре проекта. Данные для операции сложения могут извлекаться из источников трех разных типов: О DataField — поле в таблице (то есть в компоненте DataView, если употреблять терминологию Rave). В этом случае для того, чтобы выбрать поле, вы должны вначале выбрать компонент DataView; О Value — вы записываете в свойство численное значение; О CalcVar — соответствует другой вычисляемой переменной. Вы можете выбрать переменную из ниспадающего списка, в котором перечисляются все вычисляе- мые переменные, расположенные на странице. Это значение может быть из другого крмпонента CalcOp или из другого компонента, выполняющего вы- числения. Выбрав два источника данных, вам следует указать операцию, которая должна быть выполнена в отношении этих данных. Свойство Operator содержит раскрыва- ющийся список, в котором можно сделать подходящий выбор. В рассматриваемом нами примере должен быть использован оператор coAdd. Иногда в отношении одного из значений предварительно требуется выполнить некоторую функцию. В подобной ситуации используется свойство Function. Бла- годаря этому свойству вы можете выполнить предварительное преобразование значения (например, преобразовать часы в минуты), вычислить тригонометричес- кую функцию (синус числа) или выполнить какое-либо другое вычисление (ска- жем, вычислить квадратный корень или абсолютное значение). Как правило, все подобные вычисления необходимо выполнять в определен- ном порядке. Порядок выполнения вычислений определяется порядком располо- жения компонентов в иерархии проекта. Компоненты вычисляются в порядке сверху вниз. Таким образом, все компоненты CalcOp и другие вычислительные ком- поненты должны располагаться в иерархии проекта в правильном порядке. Это особенно важно, когда какой-либо вычислительный компонент в качестве исход- ных использует значения, полученные в результате срабатывания других вычис- лительных компонентов. Что далее? В данной главе мы обсуждали использование Rave для формирования отчетов из приложений Delphi. Я рассмотрел компоненты, которые используются в прило- жении Delphi для соединения с отчетом, и компоненты, которые используются в отчете для отображения данных, передаваемых приложением. Я также затронул некоторые дополнительные возможности Rave, такие как использование редакто- ра Band Style Editor, сценарии, отображение, вычислительные компоненты и не- которые другие. 0841
842 Глава 18. Формирование отчетов с использованием Rave Если вы хотите получить дополнительную информацию, обратитесь к PDF- файлам, поставляемым вместе с Delphi, но не устанавливаемым в процессе уста- новки. Эта документация вместе с другими полезными сведениями содержится на прилагаемом к Delphi 7 компакт-диске. Данная глава завершает рассмотрение встроенных в Delphi технологий работы с базами данных. Однако мы еще вернемся к ним в следующей части книги, где будут рассматриваться вопросы обмена данными через Интернет и программиро- вание Веб. Следующая часть начинается с рассмотрения некоторых базовых тех- нологий, таких как использование сокетов. Затем я рассмотрю механизмы генера- ции HTML, традиционное веб-программирование Delphi, а также использование IntraWeb. После этого я перейду к рассмотрению технологий XML и SOAP. 0842
ЧАСТЬ IV Delphi, Интернет и a.NET. Предварительный обзор В этой части: ♦ Глава 19. Интернет-программирование: сокеты и Indy ♦ Глава 20. Веб-программирование с использованием WebBroker и WebSnap ♦ Глава 21. Веб-программирование с использованием IntraWeb ♦ Глава 22. Использование технологии XML ♦ Глава 23. Веб-службы и SOAP ♦ Глава 24. Архитектура Microsoft .NET с точки зрения Delphi ♦ Глава 25.Обзор Delphi for .NET: язык и RTL 0843
*1Q Интернет- 1-7 программирование: сокеты и Indy С началом эры Интернета стало модным писать программы, основанные на интер- нет-протоколах, поэтому мы решили посвятить этой теме целых пять глав. В главе 19 рассказывается о низкоуровневом программировании сокетов и протоколов Интернета. Глава 20 посвящена веб-программированию на стороне сервера, глава 21 опи- сывает программирование в интранет-сетях, а в главах 22 и 23 обсуждаются сетевые службы и XML. В этой главе мы начнем с общего обзора технологии сокетов, затем перейдем к использованию Internet Direct (Indy) компонентов, поддерживающих низкоуров- невую работу как с сокетами, так и с наиболее общими протоколами Интернета. Мы также рассмотрим некоторые элементы протокола HTTP и вопросы создания HTML-файлов на основе баз данных. Хотя вы, вероятно, хотите сразу же начать работу с высокоуровневыми прото- колами, нам все же придется начать с изучения ключевых концепций и работы приложений низкого уровня. Знание основ работы сокетов и TCP/IP облегчит понимание большинства других концепций. В этой главе рассматриваются темы: О использование сокетов; О компоненты прямого доступа (Indy); О низкоуровневое программирование сокетов; о Winlnet API; о почтовые протоколы (SMTP и POP3); О протокол HTTP; О создание HTML; О от базы данных до HTML. Создание сокет-приложений Delphi 7 поставляется с двумя наборами TCP-компонентов — сокет-компонента- ми Indy (IdTCPClient и IdTCPServer) и оригинальными компонентами Borland (TcpClient и TcpServer), которые также доступны и в Kylix. Они размещены на закладке Internet 0844
Создание сокет-приложений 845 палитры компонентов. Компоненты TcpClient и TcpServer были созданы для замены компонентов ClientSocket и ServerSocket, использовавшихся в прежних версиях Delphi. Сейчас компоненты ClientSocket и ServerSocket объявлены устаревшими (хотя все еще доступны), и Borland предлагает использовать вместо них компоненты Indy. В этой главе, обсуждая низкоуровневое программирование сокетов, мы сосре- доточимся на использовании Indy. Прежде чем рассмотреть пример соединения на основе сокета низкого уровня, давайте разберем основные концепции TCP/IP, чтобы получить представление о распространенных сетевых технологиях. Компоненты INTERNET DIRECT (INDY) OPEN SOURCE Как уже говорилось ранее, Delphi поставляется с набором свободно распро- страняемых интернет-компонентов, называемых компонентами прямого доступа (Internet Direct, Indy). Первоначально эти компоненты назывались WinShoes (перифраз имени библиотеки WinSock). Они разработаны груп- пой программистов под руководством Чада Хауэра (Chad Hower) и доступ- ны как в Delphi, так и в Kylix. Более полную информацию о последних вер- сиях этих компонентов можно получить на сайте http://www.nevrona.com/indy. Delphi 7 связан с Indy 9, но необходимо проверять веб-сайт при обновле- нии версий. Все компоненты распространяются бесплатно и дополнены мно- гими примерами и понятными справочными файлами. Новая версия, Indy 9, включает в себя намного больше компонентов, чем предыдущая версия (Indy 8, доступная в Delphi 6). В ней имеются две новых страницы па- литры компонентов (Component palette) — «Indy Intercepts» и «Indy I/O Handlers». Наряду более чем с сотней компонентов, входящих в палитру Delphi, Indy поддерживает огромное количество дополнительных возможностей, на- чиная от развития TCP/IP приложений для клиента и сервера и заканчивая различными протоколами для шифрования и защиты. Компоненты Indy легко уз- нать по приставке Id в начале названия. Рассмотрим некоторые из них. Блокирующие и неблокирующие соединения При работе с сокетами в Windows чтение данных из сокета или запись в не- го могут осуществляться асинхронно, не прекращая выполнение другого кода в вашем сетевом приложении. Такое соединение называется неблокирующим. Поддержка сокетов в Windows посылает сообщение в тот момент, когда дан- ные становятся доступны. Альтернативный подход — использование блоки- рующих соединений. При этом приложение ждет завершения операции чте- ния или записи перед выполнением следующей строки кода. При работе с блокирующими соединениями необходимо создавать поток на сервере и, как правило, работать с потоком на стороне клиента. В компонентах Indy используются только блокирующие соединения. Любая операция сокета клиента может быть выполнена в потоке или (более простой вариант) с использованием специального вспомогательного ком- понента Indy (IdAntiFreeze). Использование блокирующих соединений для реализации протокола упрощает логику программы, так как нет необходи- продолжение 0845
846 Глава 19. Интернет-программирование: сокеты и Indy Компоненты INTERNET DIRECT (INDY) OPEN SOURCE {продолжение) мости проверять состояние каждого компьютера, как при неблокирующих соединениях. Все серверы Indy используют многопоточную архитектуру, которую мож- но контролировать с помощью компонентов IdThreadMgrDefault и IdThread- MgrPool. Первый используется по умолчанию; второй поддерживает дина- мические потоки и рассчитан на быстрые соединения. Основы программирования сокета Чтобы понять описание компонентов сокета, необходимо сначала познакомиться с некоторыми терминами, связанными с Интернетом вообще и с сокетами в част- ности. Сердцем Интернета является Transmission Control Protocol/Internet Protocol (TCP/IP), который, по сути, является комбинацией двух отдельных протоколов. Эти протоколы, работая совместно, обеспечивают соединение через Интернет. Они также могут обеспечить соединение в локальных сетях (intranet). Говоря кратко, IP отвечает за определение и маршрутизацию дейтаграмм (datagrams) (модулей данных для передачи через Интернет) и определение схемы адресации. TCP отве- чает за услуги транспортировки более высокого уровня. Конфигурирование локальной сети: ip-адреса Если в вашем распоряжении есть локальная сеть, то удобно будет протестировать последующие программы в ней. В противном случае можно один и тот же компью- тер использовать и в качестве сервера, и в качестве клиента. В этом случае адрес 127.0.0.1 (адрес локального хоста — localhost) всегда является адресом текущего компьютера. Если ваша сеть имеет сложную конфигурацию, попросите системно- го администратора установить необходимые IP-адреса, а если в сети всего пара связанных компьютеров, можно установить IP-адрес самостоятельно. Это четырех- байтное число, обычно представленное в виде четырех компонентов (называемых октетами), разделенных точками. Первый компонент (октет) определяет класс ад- реса, а само формирование таких адресов подчиняется достаточно сложной логике. Определенные IP-адреса зарезервированы для незарегистрированных внутрен- них сетей. Маршрутизаторы Интернета игнорируют эти диапазоны адресов, так что можно проводить тестирование, не взаимодействуя с настоящей сетью. На- пример, «свободный» диапазон IP-адресов с 192.168.0.0 до 192.168.0.255 может использоваться для экспериментов с сетью менее чем из 255 компьютеров. Местные доменные имена Как найти соответствие между IP-адресом и доменным именем? В Интернете про- грамма-клиент осуществляет поиск на сервере доменных имен (DNS). Но можно также использовать локальный файл HOSTS — текстовый файл, который можно легко отредактировать, чтобы получить удобные соответствия. В качестве примера можно посмотреть файл HOSTS.SAM, установленный в одном из подкаталогов каталога Windows (или в самом каталоге Windows, в зависимости установленной у вас вер- сии системы). Вы можете переименовать его в HOSTS (без расширения), чтобы ак- тивизировать локальные соответствия. 0846
Создание сокет-приложений 847 Что лучше использовать в программах — IP-адрес или имя хоста (доменное имя)? Имена хоста легче запомнить и они не требуют изменений при смене IP- адреса (по любой причине). С другой стороны, IP-адрес не требует какого-либо взаимодействия с DNS, в то время как для имени хоста такая операция необходи- ма (а она в Интернете отнимает много времени). Порты TCP Каждое TCP-соединение работает через порт, который представлен двухбайтным числом. IP-адрес и порт TCP вместе определяют интернет-соединение, или сокет (socket). Различные процессы, выполняющиеся на одном компьютере, не могут использовать один и тот же сокет (порт). Некоторые порты TCP зарезервированы для стандартного использования оп- ределенными протоколами высокого уровня и службами. Другими словами, сле- дует использовать определенные номера портов при реализации этих служб, и не использовать их ни в каких других случаях. Вот короткий список: Протокол Порт HTTP (протокол передачи гипертекста) 80 FTP (протокол передачи файлов) 21 SMTP (простой протокол передачи почты) 25 POP3 (протокол POP, версия 3) 110 Telnet 23 В файле SERVICES (еще одном текстовом файле, аналогичном файлу HOSTS) перечислены стандартные порты, используемые службами. Вы можете сами до- полнять этот список, задавая вашей службе любое имя. Сокеты клиента всегда ука- зывают номер порта или имя службы сокета сервера, с которым требуется уста- навливать соединение. Протоколы высокого уровня Пора определить термин «протокол». Протокол — это набор правил для клиента и сервера, определяющий коммуникационный поток. Низкоуровневые протоко- лы Интернета (типа TCP/IP) обычно реализуются операционной системой. Но термин «протокол» применяется также для стандартных высокоуровневых прото- колов Интернета (типа HTTP, FTP или SMTP). Эти протоколы определены в стан- дартной документации, доступной в Интернете на сайте Internet Engineering Task Force (http://www.ietf.org). При желании можно организовать собственное соединение со своим (возмож- но, очень простым) протоколом, то есть набором правил, определяющим, какой запрос клиент может посылать серверу и как сервер может отвечать на различные запросы. Пример создания собственного протокола мы рассмотрим позже. При- кладные протоколы расположены на более высоком уровне, чем протоколы транс- портировки, потому что они абстрагированы от транспортного механизма, пред- ставляемого TCP/IP. Это делает протоколы независимыми не только от опе- рационной системы и аппаратных средств, но также и от физической структуры сети. 0847
848 Глава 19. Интернет-программирование: сокеты и Indy Подключения через сокеты При передаче через сокет начинает работу программа-сервер, которая просто ждет запроса от программы-клиента. Программа-клиент запрашивает соединение у сер- вера, к которому требуется подключение. Когда клиент посылает запрос, сервер может принять соединение, открывая специальный сокет на стороне сервера, ко- торый образует соединение с сокетом на стороне клиента. Для поддержки этой модели существуют три типа сокетных соединений. о Клиентское соединение инициируется клиентом и связывает локальный со- кет клиента с удаленным сокетом сервера. Сокеты клиента должны описать сервер, с которым требуется установить соединение, указав его имя хоста (иди его IP-адрес) и порт. О Слушающие соединения (listening connections) — пассивные серверные соке- ты, ожидающие запросов клиента. Как только клиент делает новый запрос, сер- вер активизирует новый сокет, выделенный для этого конкретного соединения, и затем возвращается к слушанию. Слушающие серверные сокеты должны ука- зать порт, предоставляющий реализуемую ими службу. (Клиент соединится через этот порт.) о Серверные соединения — это соединения, активизируемые серверами; они принимают запрос от клиента. Разница в типах подключения имеет значение только на этапе установки связи между клиентом и сервером. В дальнейшем (после того, как связь установлена) обе стороны могут обмениваться друг с другом данными и запросами в произволь- ном порядке. Использование TCP-компонентов Indy Для того чтобы две программы могли связаться через сокет (в локальной сети или через Интернет), используются компоненты IdTCPClient и IdTCPServer. Один из них записывается в программе клиента, а другой — в программе сервера. При этом дол- жен использоваться тот же самый порт, позволяющий программе клиента обра- щаться к хосту программы сервера. Затем можно установить соединение между этими двумя приложениями. Например, в программе IndySockl используются эти два компонента с такими параметрами настройки: // Программа сервера object IdTCPServerl: TIdTCPServer Defaultport = 1050 end // Программа клиента object IdTCPClientl: TldTCPClient Host = ’local host' Port = 1050 end ПРИМЕЧАНИЕ------------------------------------------------------------" Серверные сокеты Indy позволяют устанавливать связь с несколькими IP-адресами и/или портами, используя набор связывания Bindings collection. 0848
Создание сокет-приложений 849 Из этого следует, что в программе клиента можно соединится с сервером, вы- полняя IdTCPClientl. Connect. Программа сервера регистрирует всю информацию. Когда клиент устанавли- вает или обрывает связь, программа записывает IP этого клиента наряду с выпол- няемой в данный момент операцией, как в следующем случае OnConnect: procedure TFormServer. IdTCPServerlConnect (AThread: TldPeerThread): begin IbLog.Items.Add. ('Connected from: ' + AThread. Connect!on.Socket.Binding.PeerlP); end Теперь, после установки подключения, необходимо заставить эти две програм- мы связаться. Сокеты клиента и сервера читают и записывают правила, которые можно использовать для передачи данных, но записи многопоточного сервера, хра- нящего и обрабатывающего множество команд (обычно основанных на строковых данных), далеко не тривиальны. Однако Indy упрощает разработку сервера посредством его командной архи- тектуры. На сервере можно определить множество команд, которые хранятся в CommandHandlers IdTCPServer. В примере IndySockl сервер имеет три обработчика, все имплементации которых различны, для того чтобы показать различные вари- анты работы. Первая команда сервера, названная test, наиболее проста. Установим команд- ную строку, числовой код и строку результатов в свойстве ReplyNormal обработчи- ка команды: object IdTCPServerl: ТIdTCPServer CommandHandlers = < item Command = 'test' Name = 'TldCommandHandlerO' ParseParams = False ReplyNormal.NumericCode = 100 ReplyNormal.Text.Strings = ('Hello from your Indy Server') ReplyNormal.TextCode = '100' end Код программы клиента выполняет команду и показывает ответ в соответствии со следующим: procedure TFormCl1 ent.btnTestClick(Sender: TObject); begin IdTCPClientl.SendCmd ('test'): ShowMessage (IdTCPClientl.LastCmdResult.TextCode + ' ; ' + IdTCPClient1.LastCmdResult.Text.Text): end: Для более сложных случаев необходимо выполнить код на сервере, а читать и писать непосредственно через подключенный сокет. Этот подход показан во вто- рой команде протокола, описанного в показанном примере. Вторая команда серве- ра называется execute. У нее не описан специальный набор свойств (только имя команды), но имеется следующий обработчик события OnCommand: procedure TFormServer.IdTCPServerlTIdCommandHandlerlCommand( ASender: TIdCommand); begin 0849
850 Глава 19. Интернет-программирование: сокеты и Indy ASender.Thread.Connect!on.Writeln ('This is a dynamic response'): end; Соответствующий код клиента пишет имя команды в сокет соединения и затем считывает ответ из единственной строчки, используя другие методы: procedure TFormClient.btnExecuteClick(Sender: TObject); begin IdTCPClientl.WriteLn('execute'): ShowMessage (IdTCPClientl.ReadLn): end; Результат работы в этом случае похож на результат из предыдущего примера, но поскольку в этом случае используется низкоуровневый подход, его легче при- способить к потребностям конкретного пользователя. Одно из таких дополнений показано в третьей и последней команде примера. Оно позволяет программе кли- ента запрашивать bitmap-файл от сервера (происходит своего рода совместное ис- пользование файла). Команда сервера имеет параметры (имя файла) и определена следующим образом: object IdTCPServerl: TIdTCPServer CommandHandl ers = < item CmdDelimiter = ' ' Command = 'getfile' Name = 'TIdCommandHandler2' OnCommand = IdTCPServerlTIdCommandHandler2Command ParamDelimiter = ' ' ReplyExceptionCode = 0 ReplyNormal.NumericCode = 0 Tag = 0 end> Программа рассматривает первый параметр как имя файла и возвращает его в потоке (последовательном файле). В случае ошибки управление перехватывает- ся сервером, который завершает соединение. Это не очень красивое решение, но оно обеспечивает безопасность и простоту в разработке: procedure TFormServer.IdTCPServerlTIdCommandHandler2Command( ASender: TIdCommand); var filename: string: fstream. TFileStream: begin if Assigned (ASender.Params) then filename :- HttpDecode (ASender.ParamsCO]). if not FileExists (filename) then begin ASender Response.Text := 'File not found'. IbLog.Items.Add ('File not found: ' + filename): raise EldTCPServerError Create ('File not found- ' + filename). end else begin fstream := TFileStream.Create (filename. fmOpenRead); try ASender.Thread.Connection.WriteStream(fstream. True. True): IbLog.Iterns.Add ('File returned. ' + filename + ' (' + IntToStr (fstream Size) + ')'): 0850
Создание сокет-приложений 851 finail у fstream.Free; end; end; end; Вызов вспомогательной функции HttpDecode с параметром необходим, чтобы закодировать путь к файлу, включая пробелы, как единый параметр, который за- тем возвращается в клиентскую программу, вызвавшую HttpEncode. Как можно видеть, сервер также принимает и регистрирует возвращенные файлы и их разме- ры или сообщение об ошибке. Программа клиента читает последовательный файл и копирует его в компонент Image, который затем и показывает (рис. 19.1). procedure TFormClient btnGetFileClick(Sender TObject): var stream- TStream. begin IdTCPClientl WriteLn( 'getfile ' + HttpEncode (edFileName.Text)). stream = TMemoryStream.Create: try IdTCPClientl.ReadStream(stream); stream Position = 0. Imagel Picture.Bitmap.LoadFromStream (stream): finally stream.Free. end; end; J^IndySocIcl dient Connect Disconnect Рис. 19.1. Пример клиентской программы IndySockl Передача данных базы данных через сокетное соединение Используя уже изученные приемы, мы можем создать приложения для передачи записей баз данных через сокеты. Основная идея состоит в том, чтобы написать 0851
852 Глава 19. Интернет-программирование: сокеты и Indy внешнюю форму для ввода данных и внутреннюю программу для их хранения. Клиентская часть будет состоять из простой формы для ввода записей. В ней ис- пользуются строковые поля базы Company, Address, State, Country, Email, Contact, а так- же числовое поле с плавающей точкой для идентификатора компании (ComplD). ПРИМЕЧАНИЕ ----------------------------------------------------------- Передавать записи базы данных через сокеты можно при помощи DataSnap и сокетных соединений (см. главу 16 «Многозвенные приложения DataSnap») или при поддержке SOAP (более подробно в главе 23 «Веб-службы и SOAP»). Написанная программа-клиент работает с набором данных клиента. Его струк- тура должна быть записана в текущем каталоге. (Соответствующий код можно посмотреть в тексте обработчика событий OnClick.) После заполнения формы и на- жатия на кнопку Send All обработчик событий OnClick отправляет данные на сервер, где и происходит проверка корректности новых записей. Правильность записи определяется по значению поля ComplD, которое не заполняется пользователем, а указывается сервером после того, как данные отправлены. Используя структуру FileName=FileValue, программа-клиент упаковывает все введенные данные в список строк, который затем отправляется на сервер. В этот момент программа ждет, пока сервер вернет значение поля ComplD, которое сохра- няется в текущей записи. Все эти коды занимают место в потоке, блокируя пользо- вательский интерфейс во время длительной операции. Щелчком по кнопке Send пользователь стартует новый поток: procedure TForml.btnSendCllek(Sender: TObject); var SendThread: TSendThread; begin SendThread := TSendThread.Create(cds); SendThread.OnLog := OnLog; SendThread.ServerAddress EditServer.Text: SendThread.Resume; end; Поток имеет несколько параметров: данные для конструктора, адрес сервера, сохраненный в свойстве ServerAddress, а также запись события для передачи в глав- ную форму. Код потока открывает соединение и сохраняет пересланные записи до окончания своей работы: procedure TSendThread.Execute: var I: Integer; Data: TStringList: Buf: String: begin try Data := TStmngList.Create; fldTcpClient :- TldTcpClient.Create (nil); try fldTcpClient.Host := ServerAddress: fldTcpClient.Port := 1051; fldTcpClient.Connect; fDataSet. First: while not fDataSet.Eof do 0852
Создание сокет-приложений 853 begin // если запись еще не зарегистрирована if fDataSet.FieldByName('CompID’).IsNul1 or (fDataSet.FieldByName('CompID').Aslnteger = 0) then begin FLogMsg : = 'Sending ' + fDataSet.F1eldByName('Company').AsString: Synchronize(DoLog); Data.Clear; // создать строку co структурой "FieldName^Value" for I := 0 to fDataSet.FieldCount - 1 do Data.Values [fDataSet.FieldsCI],FieldName] := fDataSet.Fields [I] .AsString: // отправить запись f IdTcpCl1 ent.Writeln ('senddata'); fldTcpCHent.WriteStrings (Data. True); // ожидание ответа Buf := fldTcpClient.ReadLn; fDataSet.Edit; fDataSet.FieldByName('CompID').AsString : = Buf: fDataSet.Post; FLogMsg := fDataSet.FieldByName('Company').AsString + ' logged as ' + fDataSet.F1eldByName('CompID').AsString; Synchronize(DoLog); end; fDataSet.Next; end; finail у fIdTcpCllent.Disconnect; fldTcpClient.Free; Data.Free; end; except // определение ошибки набора данных // (совместное редактирование и т. д.) end; end; Теперь давайте посмотрим на серверную часть. Программа-сервер тоже исполь- зует таблицу базы данных (которая, как и в клиентской части, должна быть сохра- нена в текущем каталоге). Но в таблицу на сервере добавлены еще два поля — тек- стовое LoggedBy и поле данных LoggedOn. Значения этих полей сервер определяет автоматически при получении данных вместе с определением значения поля CompID. Это делается обработчиком событий по команде Senddata. procedure TForml.IdTCPServerlTIdCommandHandlerOCommand( ASender: TIdCommand); var Data: TStrings; I; Integer; begin Data := TStringList.Create; try ASender.Thread.Connection.ReadStri ngs(Data); cds.Insert; // установка поля при помощи строк for 1 := 0 to cds.FieldCount - 1 do cds.Fields [I].AsString 0853
854 Глава 19. Интернет-программирование: сокеты и Indy Data.Values [cds.Fields[I].FieldName]: // заполнить ID. отправителя и дату Inc(ID); cdsCompID.AsInteger := ID: cdsLoggedBy.AsString := ASender.Thread.Connection.Socket.Binding.PeerIP: cdsLoggedDn.AsDateTime := Date: cds.Post: // возврат ID ASender Thread.Connection.WriteLn(cdsCompID.AsString): finally Data.Free: end: end: Рис. 19.2. Пример клиентской и серверной программ для работы с базой данных IndyDbSock Так как данные обрабатываются при помощи структуры FileName=FileValue, нет никаких проблем с тем, что порядок следования полей может не совпадать. (Прав- да, некоторые данные могут оказаться просто утеряны.) После получения данных и записи их в таблицу сервер отправляет клиенту значение поля CompID. Получив это значение, программа-клиент сохраняет его и помечает запись как отправлен- ную. После этого сделанные пользователем изменения серверу не отправляются. 0854
Отправка и получение почты 855 Чтобы выполнить обновление, необходимо добавить измененное поле в клиент- скую базу данных, а на сервере сделать проверку получаемого поля — новое оно или измененное. Если получено измененное поле, то сервер должен не добавлять новую запись, а изменять уже существующую. Как показано на рис. 19.2, в программе сервера есть две страницы: одна с обычным журналом, а вторая — с компонентом DBGrid, отображающая текущее состояние в базе данных сервера. Программа-клиент — это базовая форма для вво- да данных с новыми кнопками для отправки данных и для удаления уже суще- ствующих записей (в том числе тех, чей ID зарегистрирован). Отправка и получение почты Одна из наиболее частых операций в Интернете — это отправка и прием сообще- ний электронной почты. В этой книге мы не будем останавливаться на проблеме написания почтовых программ, поскольку множество хороших универсальных программ уже существует. Примеры можно найти в демонстрационных файлах Indy. Но кроме написания многофункциональных программ можно делать неко- торые интересные вещи с почтовыми компонентами и протоколами. Условно раз- делим эти возможности на две группы: О Автоматическое создание почтовых сообщений. Можно написать приложе- ние, содержащее окно About (о программе), чтобы отправить уведомление о ре- гистрации обратно в службу маркетинга, или специальный пункт меню, чтобы отправить запрос в службу технической поддержки. Можно даже создать при- ложение так, чтобы в случае ошибки автоматически устанавливалось соедине- ние со службой поддержки. Еще можно автоматически отправлять сообщения на листы рассылки или генерировать сообщения на веб-сайте (подробнее об этом — в конце главы). О Использование почтовых протоколов для общения с пользователями, кото- рые редко подключаются к сети. В ситуации, когда необходимо передать ин- формацию от одного пользователя другому (причем оба пользователя не под- ключены к сети постоянно), можно написать серверное приложение для син- хронизации их доступа и снабдить каждого клиентским приложением для взаимодействия с сервером. Другой вариант — использовать имеющийся по- чтовый сервер и написать две программы, основанные на почтовых протоко- лах. При таком способе передачи данные, как правило, представлены в специ- альном формате, поэтому удобнее создать для таких сообщений отдельный почтовый ящик. Можно исправить пример IndyDbSock так, чтобы использовать почтовые сообщения вместо сокетного соединения. Основное преимущество такого подхода в том, что можно работать через брандмауэры, и не страшны отключения сервера, так как информация сохраняется на почтовом сервере. Отправка и получение почты Для работы с протоколами электронной почты при помощи Indy необходимо по- местить в сообщение компонент IdMessage, заполнить его данными, а затем отпра- вить при помощи компонента IdSMTP. Чтобы получить сообщение из почтового 0855
856 Глава 19. Интернет-программирование: сокеты и Indy ящика, используется компонент IdРорЗ, который возвращает объект IdMessage Для того чтобы получить общее представление о работе подобных приложений, рас- смотрим следующую программу. Она создана для отправки сообщений сразу не- скольким людям. Их адреса берутся из файла формата ASCII. Эта программа пер- воначально использовалась для отправки сообщений людям, подписавшимся на рассылку на веб-сайте. Затем в нее были добавлены возможность работы с базой данных и автоматическое чтение журналов подписчиков. Но первая версия еще может послужить хорошим вводным примером использования SMTP-компонен- тов Indy. Программа Send List хранит список имен и адресов в локальном файле, который отображается в окне списка. При помощи ряда кнопок можно добавлять в список записи, удалять их или редактировать При выходе из программы все изменения автоматически сохраняются. Рассмотрим следующую часть программы. В верхней части диалогового окна, изображенного на рис. 19.3, можно ввести тему письма, адрес отправителя, инфор- мацию, используемую для соединения с сервером (имя хоста, имя пользователя, а также пароль, если он необходим). 7 Send list Subject |................................. Host j .... — UsesNams j Pa .wod | s Nan» L«t _____________________________________________ MaMessage fl Ths i$ a lest message Message sent by the Send List program of the book Mastering Delphi AddlaVst gembve Рис. 19.3. Программа SendList во время проектирования Значение полей редактирования можно постоянно хранить в INI-файле. Но лучше этого не делать, так как в этом случае настройки вашего почтового соедине- ния станут общедоступными Значения полей редактирования и список адресов позволяют отправлять несколько сообщений, настроив каждое при помощи сле- дующего кода: 0856
Отправка и получение почты 857 procedure TMainForm BtnSendAllClick(Sender TObject). var nltem Integer. Res Word begin Res = MessageDlg ('Start sending from item ' + IntToStr (ListAddr Itemindex) + (' + ListAddr Items [ListAddr Itemindex] + ')?'#13 + '(No starts form 0)' mtConfirmation [mbYes mbNo mbCancel] 0) if Res = mrCancel then Exit if Res = mrYes then nltem = ListAddr Itemindex el se nltem = 0 // соединение Mail Host = eServer Text Mail Username = eUserName Text if ePassword Text <> ' then begin Mail Password = ePassword Text Mail AuthenticationType = atLogin end; Mail Connect // отправка сообщения так же как обычного сообщения try // установка фиксированной части заголовка MailMessage From Name = eFrom Text Mai(Message Subject = eSubject Text MailMessage Body SetText ( reMessageText Lines GetText) MailMessage Body Insert (0 'Hello') while nltem < ListAddr Items Count do begin // показать текущий выбор Application ProcessMessages ListAddr Itemindex = nltem MailMessage Body [0] = 'Hello ' + ListAddr Items [nltem] MailMessage Recipients EMailAddresses = ListAddr Items [nltem] Mail Send(MailMessage) Inc (nltem) end; finail у И готово Mail Disconnect end end: Электронную почту можно использовать для отправки сообщений разработчи- кам об ошибках в приложениях. Эту возможность следует использовать на стадии разработки приложения. Для этого легко преобразовать пример ErrorLog из второй главы для отправки сообщения об ошибках (всех или только некоторых). 0857
858 Глава 19. Интернет-программирование: сокеты и Indy Работа по протоколу HTTP Работать с почтовыми сообщениями, несомненно, интересно и, вероятно, прото- колы электронной почты — самые распространенные интернет-протоколы. Еще один популярный протокол — HTTP. Он используется веб-серверами и веб-брау- зерами. Следующая часть этой главы и две последующие главы посвящены имен- но этому протоколу и языку HTML. В Интернете на стороне клиента происходит, как правило, только просмотр HTML-файлов. Кроме создания собственного браузера, можно вставить в свою программу элемент ActiveX из библиотеки Internet Explorer. Этот вариант пока- зан в примере WebDemo в главе 12 «Отд СОМ к СОМ+». Можно также активизиро- вать браузер, установленный на компьютере пользователя. Например, можно от- крыть HTML-страницу, вызвав метод ShellExecute (он определен в модуле ShellApi). ShellExecute (Handle, ’open’ FileName, ”, ’ ’. sw_ShowNorma 1): Использование ShellExecute позволяет выполнить документ (например, файл). Windows запустит программу, связанную с расширением НТМ, выполняя действие, которое передано в качестве параметра (в данном случае — open (открыть)). Ана- логично можно просматривать информацию на веб-сайте, используя вместо име- ни файла строку 'http://www.example.com'. По префиксу'http' система понимает, что необходимо запустить веб-браузер. На стороне сервера создаются просматриваемые HTML-странички. Иногда достаточно создать неизменяемую страницу и время от времени ее обновлять, вы- бирая данные из базы данных. В другом случае может потребоваться генерировать страницу динамически, в зависимости от запроса пользователя. Для начала рассмотрим работу с HTTP, создав простые приложения для кли- ента и сервера. Затем рассмотрим компоненты для создания HTML-документов. В главе 20 перейдем от уровня «ключевых технологий» к поддерживаемому Delphi стилю быстрой разработки веб-документов (RAD, Rapid Access Development), включая технологии веб-серверных расширений (CGI, ISAPI и модули Apache), а также обсудим архитектуру WebBroker и WebSnap. Захват НТТР-содержания В качестве примера работы с HTTP рассмотрим специальную поисковую програм- мку. Она соединяется с сайтом Google, производит поиск по ключевому слову и за- поминает первые 100 найденных ссылок. Она не показывает HTML-код страни- цы, а выбирает адреса сайтов, занося их в список. В отдельный список заносятся описания этих сайтов (описание конкретного сайта становится доступным при щелчке по его адресу). Таким образом, программа демонстрирует две технологии, извлечение веб-страницы и синтаксический разбор HTML-кода. Чтобы увидеть возможности работы с блокирующими соединениями (напри- мер, используемыми в Indy), рассмотрим программу, работающую в фоновом ре- жиме. Этот подход удобен тем, что позволяет задавать одновременно несколько запросов на поиск. Используемый в приложении WebFind потоковый класс прини-, мает в качестве входных данных для поиска URL. В методе Synchronize можно вызвать два выходных процесса — AddToList и ShoW- Status. Они заносят результаты или возвращаемые данные в главную форму, 0858
Работа по протоколу HTTP 859 добавляя строку в окно списка или изменяя строку состояния SimpleText. Основ- ную работу в потоке производит метод Execute. Поток активизируется главной формой следующим образом: const strSearch = 'http://www.google com/search?as_q=': procedure TForml.BtnFindClick(Sender: TObject): var FindThread: TFindWebThread: begin // создать замороженный поток, установить начальное значение и запустить FindThread := TFindWebThread.Create (True): FindThread FreeOnTerminate := True; FindThread.strUrl := strSearch + EditSearch.Text +’&num=100': // взять первые 100 найденных элементов FindThread.Resume: end: В строке адреса (strUrl) указывается адрес поисковой системы и параметр as_q, означающий искомые слова. Параметр &num=100 определяет количество выбран- ных сайтов. Это число не может быть больше 100. ВНИМАНИЕ-------------------------------------------------------------------------------- Программа WebFind работала без ошибок с сайтом Google во время написания и тестирования книги. Однако программное обеспечение сайта может со временем измениться, и WebFind может начать работать с ошибками. Так было с программой, описанной в книге Delphi 6 (серия «Для профессио- налов»). В ней не был указан параметр user agent, и после замены программного обеспечения Google этот сервер стал блокировать запросы. Проблема решалась указанием любого значения user agent. Вызов Resume активизирует метод потока Execute, который, в свою очередь, ак- тивизирует методы GrabHtml и HtmlToList (листинг 19.1). Первый метод соединяет- ся с HTTP-сервером и при помощи динамически созданного компонента IdHttp считывает HTML-код с результатами поиска. Второй метод выбирает из этих ре- зультатов адреса и записывает их в строку strRead. Листинг 19.1. Класс TFindWebThread (в программе WebFind) unit FindTh: interface uses Classes. IdComponent. SysUtils, IdHTTP; type TFindWebThread = class(TThread) protected Addr, Text. Status: string: procedure Execute; override: procedure AddToList: procedure ShowStatus: procedure GrabHtml, procedure HtmlToList; procedure HttpWork (Sendee TObject: AWorkMode: TWorkMode; const AWorkCount: Integer): 0859
860 Глава 19. Интернет-программирование: сокеты и Indy Листинг 19.1 (продолжение) publ1 с StrUrl string strRead string end; implementation { TFindWebThread } uses WebFindF wimnet procedure TFindWebThread AddToList. begin if Forml ListBoxl Items IndexOf (Addr) < 0 then begin Forml ListBoxl Items Add (Addr). Forml DetailsList Add (Text). end; end; procedure TFindWebThread Execute begin GrabHtml HtmlToList Status = 'Done with ' + StrUrl. Synchronize (ShowStatus). end; procedure TFindWebThread GrabHtml. var Httpl TIdHTTP begin Status = 'Sending query ' + StrUrl. Synchronize (ShowStatus) Httpl = TIdHTTP Create (ml). try Httpl Request UserAgent = 'User-Agent NULL'. Httpl OnWork = HttpWork strRead = Httpl Get (StrUrl). finally Httpl Free. end; end; procedure TFindWebThread HtmlToList. var strAddr strText string. nText integer nBegin, nEnd Integer begin Status = 'Extracting data for + StrUrl. Synchronize (ShowStatus) strRead = LowerCase (strRead) repeat // поиск первой части ссылки HTTP nBegin = Pos ('href-http' strRead). if nBegin <> 0 then 0860
Работа по протоколу HTTP 861 begin // получение оставшейся части строки, начинающейся с 'http' strRead = Copy (strRead nBegin + 5 1000000). // найти окончание ссылки HTTP nEnd = Pos (’>' strRead). strAddr = Copy (strRead 1 nEnd - 1). // продолжить strRead = Copy (strRead. nEnd + 1. 1000000), // добавить URL. если он не содержит 'google' if Pos ('google' strAddr) = 0 then begin nText = Pos ('</a>' strRead) strText = copy (strRead 1. nText - 1). // удаление кэшированных ссылок и копий if (Pos ('cached' strText) = 0) then begin Addr = strAddr. Text = strText AddToList end. end end. until nBegin = 0 end procedure TFindWebThread HttpWork(Sender TObject AWorkMode TWorkMode const AWorkCount Integer) begin Status = 'Received ' + IntToStr (AWorkCount) + ' for ' + strllrl. Synchronize (ShowStatus). end procedure TFindWebThread ShowStatus. begin Forml StatusBarl SimpleText = Status end. end Программа ищет вхождение подстроки 'href=http' и копирует текст до закрыва- ющего символа «>». Если в строке встречается слово 'google' или в полученном тексте содержится слово 'cached', то этот текст исключается из результата. На рис. 19.4 показано окно программы. Можно задать несколько запросов на поиск одновременно, однако нужно следить, чтобы результаты добавлялись в один и тот же компонент памяти (memo). Winlnet API Для работы с протоколами FTP и HTTP не обязательно использовать VCL-kom- поненты. Можно воспользоваться специальными API, входящими в поставку Micro- soft Winlnet DLL. Эта библиотека встроена в ядро операционной системы и реа- лизует протоколы FTP и HTTP поверх API-сокетов Windows. При помощи трех функций — InternetOpen, InternetOpenURL и In tern etRead Fi Le — мож- но получить файл, находящийся по любому адресу, чтобы сохранить или обработать его. Не менее простые методы существуют для FTP. С ними можно ознакомиться, прочитав исходный код модуля Winlnet.pas, который содержит все эти функции. 0861
862 Глава 19. Интернет-программирование: сокеты и Indy у Web Find JO|2£l Search (use ♦ to separate values) | Borland http //www borland com/ http //www borland com/delphi/ .hnp:/7bdn borlandcom/ http //bdn borland com/cpp/0,1419.2 00 html http /Aww borland co ip/ http //www borland co ip/cppbuildet/fieecompilet/ http //www borland de/ http //www borland de/|buildet/ http //www borland tu/ http //www borland I r/ http //info borland com/newsgroups/ http //info borland com/devsuppott/bde/ http //www borland cz/ http //www borland es/ http //www borland it/ http //www borland it/webservices/ <b>botland</b> developer network home page Done with http //www google com/search?as q=Borland&num=100 Рис. 19.4. Использование приложения WebFind для поиска сайтов на поисковом сервере Google СОВЕТ------------------------------------------------------------------------------------------ Файл помощи к библиотеке Winlnet не входит в SDK Help, поставляемый с Delphi. Ознакомиться с ним можно на сайте MSDN по адресу, msdn.microsoft.com/hbrary/en-us/wininet/wininet reference.asp. Функция InternetOpen создает соединение и возвращает дескриптор, который используется при вызове InternetOpenURL. Эта функция возвращает дескриптор URL и передает его функции InternetReadFile для прочтения данных. В приведен- ном ниже примере данные сохраняются в локальную строку. После получения дан- ных программа прерывает связь с URL и завершает сеанс связи с Интернетом, дваж- ды вызывая функцию InternetCloseHandle: var hHttpSession hReqUrl Hinternet. Buffer array [0 1023] of Char. nRead Cardinal strRead string nBegin nEnd Integer begin strRead = '' hHttpSession = InternetOpen ('FindWeb' INTERNET_OPEN_TYPE_PRECONFIG. ml ml 0). try hReqUrl = InternetOpenURL (hHttpSession Pchar(StrUrl) ml 0 0 0), try // прочитать все данные repeat InternetReadFile (hReqUrl QBuffer sizeof (Buffer) nRead) strRead = strRead + string (Buffer) until nRead = 0. 0862
Работа по протоколу HTTP 863 finally InternetCloseHandle (hReqUrl). end. fi nail у InternetCloseHandle (hHttpSession). end end Ваш собственный браузер Скорее всего, вы не будете создавать новый веб-браузер. В любом случае имеет смысл узнать, как при помощи средств просмотра HTML из библиотеки CLX (эле- мент управления TextBrowser) можно извлечь из Интернета HTML-файл и локаль- но отобразить его. Подключив этот элемент управления к клиенту HTTP Indy, сразу получаем простейший текстовый браузер с ограниченными возможностями для навигации. Код должен выглядеть следующим образом: TextBrowserl Text = IdHttpl Get (Newllrl) где Newllrl — полный адрес необходимого веб-ресурса. В примере BrowseFast URL вводится в комбинированном списке, сохраняющем несколько последних запро- сов. Поскольку для отображения графических изображений требуется более слож- ный код, в результате подобного запроса отображается только текстовая часть веб- страницы (рис. 19.5). Поэтому элемент управления TextBrowser правильнее называть не браузером, а просто средством просмотра файлов. В примере добавлена минимальная поддержка гиперссылок. Когда пользова- тель подводит курсор мыши к ссылке, ее текст копируется в переменную (New- 1В I j?/ BrowseFast |http77www marcocantu com W* j Source ] C5ck here la adverti,jw on marcocartu.com P“Orflf> De ekfmori Menu for Homo News : Octore 23rd* The Delphi 7 version of CanTools Wizards is available for download n ; 1.апТсе^»<*гЧюг<,М1Пе 4e : October 1 st I m fnshmg to write Mastering Delphi 7 the book should be out m : January/February August. 18th Borland has announced the forthcoming release of Delphi 7 and of K0x 3 Delphi 7 is an mportant step towards the future Delphi for NET I m wnbng a Mastering Delphi 7 but it wont be released soon as my plan t$ to cover al of the features and third party products n Delphi 7 and this takes some tone to be done with care August 8th. 2002 The site has been moved to a Lnux server Much causes a few problems here and there with case inconsistencies If you cannot hnd a ink try uppercase or lowercase the first letter or email me with the problem June 13 I ve fnat^ had tme to put together al of the mx gorlar C Confprensg ^МауЛю”- ^Anahesn CA. Marco's^ ^Delphi Tutorial a inque c anto the XML world <5-6 June 2002 3 m hostng r my office to trpm ‘ot 9-11 June 2002 pCCS.2902RJKl where I Salks on Delphi and XML aJDDI andXSLT The De also my Italian events. Рис. 19.5. Вид текстового браузера BrowseFast 0863
864 Глава 19. Интернет-программирование: сокеты и Indy Request), которая затем используется для формирования нового HTTP-запроса при щелчке по элементу управления. Однако объединить текущий адрес с запросом достаточно сложно, даже при помощи класса IdUrl из библиотеки Indy. Приведен- ный ниже код обрабатывает только простейшие случаи: procedure TForml TextBrowserlClick(Sender TObject). var Un TIdUri begin if NewRequest <> '' then begin Un = TIdUri Create (LastUrl) if Pos ( http ' NewRequest) > 0 then GoToUrl (NewRequest) else if NewRequest [1] = '/ then GoToUrl ('http //’ + Uri Host + NewRequest) else GoToUrl ( http //' + Un Host + Uri Path + NewRequest) nPos = 0 end Этот пример достаточно тривиален, но плохо применим на практике. Следует помнить, что от полноценного браузера требуется несколько больше, чем просто возможность соединения по протоколу HTTP и просмотра HTML-файлов. Простой НТТР-сервер Ситуация с разработкой собственного HTTP-сервера несколько другая Создать на основе HTML статичный сервер далеко не просто, хотя среди демонстрацион- ных файлов Indy есть хорошие примеры, которые можно взять за основу. Однако значительно больший интерес представляет создание полностью динамических серверов. Этому вопросу и будет посвящена следующая глава. Как начать разработку обычного HTTP-сервера, покажем на примере програм- мы HttpServ. В этой программе присутствует форма с окном списка для хранения запросов и компонент IdHTTPServer со следующими настройками: Object IdHTTPServerl TIdHTTPServer Active = True DefaultPort = 8080 OnCommandGet ’ IdHTTPServerlCommandGet end Сервер использует порт 8080 вместо стандартного порта 80, поэтому его можно запустить вместе с другим веб-сервером Весь код находится в обработчике собы- тия OnCommandGet, который возвращает фиксированную страницу и информацию о запросе: procedure TForml IdHTTPServerlCommandGet(AThread TIdPeerThread Requestinfo TIdHTTPRequestInfo Responseinfo TIdHTTPResponseInfo) var HtmlResult String, begin // записать Listboxl Items Add (Requestinfo Document) // ответить HtmlResult = '<hl>HttpServ Demo</hl>' + 0864
Генерация файлов HTML 865 '<p>This is the only page you"ll get from this example </p><hr>' + '<p>Request ' + Requestinfo Document + '</p>' + '<p>Host ' + Requestinfo Host + </p>' + '<p>Params ' + Requestinfo UnparsedParams + '</p> + '<p>The headers of the request follow <br>' + Requestinfo RawHeaders Text + </p>' Responseinfo ContentText = HtmlResult, end Если в командной строке браузера задан путь и некоторые параметры, то они будут отображены в преобразованном виде. На рис. 19.6 видно, как отображается следующая строка' http //localhoct 8080/test?user-marco ! MazitU * у* Б* loofe. уДОя» •fr http //localhwt HttpServ Denio Ths и the only page you'll get from this example Request /test Host localhost 8080 Params user=marco The headers of the request follow Host localhost 8080 User Agent MoaDa/5 0 (Wmdows U Windows NT 5 0 en US rv 1 1) Gccko/20020826 Accept text/xml apphcation/xml,apphcation/xhtmHxml text/html q=0 9 text/plam q=0 8 video/x mngpmage/png tmage/jpeg,image/gf q=0 2 text/css “/“ q=0 1 Accept Language en us en q=0 50 Accept Encoding gap deflate compress q=0 9 Accept Charset ISO 8859 1 utf 8 q=0 66 * q=0 66 Keep Abve 300 Connection keep ahve E3 S3 Рис. 19.6. Страница, отображаемая при подключении браузера к программе HttpServ Этот пример может показаться очень простым. Более интересную его версию мы продемонстрируем дальше, обсуждая проблему генерирования файлов HTML при помощи компонентов Delphi. ПРИМЕЧАНИЕ---------------------------------------------------------------- Возможно, вы планируете создавать веб-сервер или другой сервер в Интернете при помощи Delphi. Тогда в качестве альтернативы компонентам Indy имеет смысл познакомиться с компонентами DXSock (разработка фирмы Brain Pathwork DX, www.dxsock.com). Генерация файлов HTML Язык разметки гипертекста (Hypertext Markup Language), название которого более известно в форме акронима HTML, является наиболее широко распрос- траненным форматом данных для Веб Все браузеры, как правило, читают имен- но этот формат. Он принят в качестве стандарта одной из организаций, конт- ролирующих форматы в Интернете, — W3C (World Wide Web Consortium, www.w3.orc). Документ, определяющий стандарт HTML, можно посмотреть на www.w3.org/MarkUp. 0865
866 Глава 19. Интернет-программирование: сокеты и Indy Компоненты Delphi для создания файлов HTML Компоненты Delphi для создания файлов находятся на вкладке Internet палитры компонентов. С их помощью можно не только создавать файлы HTML, но и пре- образовывать таблицы базы данных в формат HTML. Многие разработчики применяют эти компоненты только для создания дополнительных возможностей веб-сервера. Однако три компонента из пяти могут использоваться для создания статических HTML-файлов в любых приложениях, несмотря на то что включены в технологию WebBroker. Перед тем как перейти к рассмотрению примера HtmlProd, кратко опишем ос- новное назначение этих компонентов. О Наиболее простым компонентом является PageProducer, работающий с HTML- файлом, в котором расставлены специальные теги. HTML-файл может быть сохранен как внешний файл или как внутренний список строк. Преимущество такого подхода в том, что вы можете создавать подобный файл, используя свой любимый HTML-редактор. Во время выполнения PageProducer преобразует спе- циальные теги в HTML-код, что позволяет легко изменять содержание разде- лов HTML-документа. Специальные теги имеют базовый формат <#tagname>. Внутри тега можно использовать произвольные параметры. Работа с тегами производится в обработчике событий OnTag компонента PageProducer. О Компонент DataSetPageProducer расширяет возможности PageProducer, автомати- чески заменяя специальные теги соответствующими названиями полей из ис- точника данных. О Основная идея DataSetTableProducer состоит в том, чтобы простым и гибким способом создать из набора данных таблицу в формате HTML. Его обычно ис- пользуют для отображения таблиц, запросов и т. п. Удобный режим предвари- тельного просмотра позволяет прямо на этапе разработки увидеть, как будет выглядеть в браузере HTML-документ. О ОиегуТаЫеРгоЬисегиБОкОиегуТаЫеРгоЬисегпохожина DataSetTableProducer, приэтом они были разработаны специально для создания запросов с параметрами на основе данных, введенных в HTML-форму для поиска (для BDE и dbExpress соответственно). Эти компоненты описаны в главе 20. Создание страниц HTML Наиболее простой пример использования специальных тегов с символом # — со- здание HTML-страницы, показывающей поле с текущей (или какой-либо другой, вычисленной на основе текущей) датой. Ниже приведен HTML-код, генерирую- щий демонстрационный пример для компонента PageProducerl программы HtmlProd. <html><head> <title>Producer Demo</title> </head><body> <hl>Producer Demo</hl> <p>This is a demo of the page produced by the <b><#appname></b> application on <b><#date></b>.</p> <hr> <p>The prices in this catalog are valid until <b> <#expiration days=21></b>.</p> </body></html> 0866
Генерация файлов HTML 867 ВНИМАНИЕ--------------------------------------------------------------------- Лучше создавать этот файл в редакторе HTML. Однако некоторые редакторы могут автоматически вставлять кавычки вокруг параметров тега days=«21», так как этого требуют форматы HTML4 и XML. Эти кавычки можно удалить на этапе синтаксического анализа кода (перед вызовом обработ- чика события OnHtmITag), используя свойство StripParamQuotes компонента PageProducer. Кнопка Demo Page копирует выходные данные компонента PageProducer в свой- ство Text компонента Мето. Функция Content компонента PageProducer считывает входящий HTML-код, проводит его синтаксический разбор и вызывает для каж- дого специального тега обработчик событий OnTag, который поверяет значение тега (игнорируя параметр TagString) и возвращает HTML-текст в зависимости от зна- чения параметра ReplaceText, выводя его, как показано на рис. 19.7. 7' HtmlProd 7 7/ " .__________-I ' Country: \. {Argentina CapitS ':*|Виепо»АЬи I ВЙ Table Codfeertt’.'dSouth America Г'css SweHTML | F7 StatlgtswH •>ммммшМкшмом1М1йннымшм1М*ын1^ <head> <Wle> Producer Demo</Ule> </head> <body> <h1>Producer Demo</'h1> <p>This is a demo of the page produced by the <b>HtmlProd exe</b> application on <b>12/4/2002</b> </p> <hr> <p>The prices in this catalog are valid until <b>12/25/2002</'b> </p> </body> </html> Рис. 19.7. Выходные данные примера HtmlProd. Результат работы компонента PageProducer после нажатия на кнопку Demo Page procedure TFormProd PageProducerlHTMLTag(Sender. TObject. Tag: TTag: const TagString String. TagParams: TStrings: var ReplaceText. String); var nDays. Integer; begin if TagString = 'date' then ReplaceText •= DateToStr (Now) else if TagString = 'appname' then ReplaceText •= ExtractFilename (Forms.Application.Exename) else if TagString = 'expiration' then begin nDays := StrToIntDef (TagParams ValuesE'days']. 0); if nDays <> 0 then ReplaceText .= DateToStr (Now + nDays) 0867
868 Глава 19. Интернет-программирование: сокеты и Indy else ReplaceText = ’<!>{expirati on tag error}</!>' end end. Обратите внимание на код преобразования последнего тега #expiration, так как ему требуется параметр. Компонент PageProducer помещает весь текст параметра тега (в примере это 'days=21') в строку списка параметров (TagParamrs). Для того чтобы извлечь из нее значение (число после знака равенства), можно воспользо- ваться свойством Values списка строк TagParams и одновременно соответствующим поиском. Если значение не найдено или не является целым числом, программа сообщит об ошибке. СОВЕТ -------------------------------------------------------------------------- Компонент PageProducer поддерживает определяемые пользователем теги, которые могут быть любой строкой. Но прежде чем создавать свой тег, нужно посмотреть список специальных тегов, определенных в TTags. Среди возможных значений есть такие, как tgLmk (для тега link), tglmage (для тега img), tgTable (для тега table) и некоторые другие. Если вы создадите собственный тег, как в примере PageProd, то параметр Тад обработчика HTMLTag примет значение tgCustom. Создание страниц данных В примере HtmlProd есть компонент DataSetPageProducer, который обеспечивает со- единение таблицы базы данных и исходного кода HTML: <html><head> <title>Data for <#name></title> </head><body> <hl><center>Data for <#name></center></hl> <p>Capital <#caprtai></p> <p>Contrnent <#continent></p> <p>Area <#area></p> <p>Population <#population></p><hr> <p>Last updated on <#date><br> HTML file produced by the program <#program>.</p> </body></html> В примере используются теги с названиями полей связанного набора данных (обычная таблица базы данных COUNTRY.DB). Программа автоматически заменяет их значениями полей текущей записи. Приложение HtmlProd может работать в ка- честве HTTP-сервера (как это происходит, рассмотрим позже), с которым соеди- няется браузер. Как выглядит его окно, показано на рис 19.8. Заметим, что в исходном коде программы вообще нет ссылок на базу данных: procedure TFormProd DataSetPageProducerlHTMLTag(Sender TObject Tag TTag const TagString String TagParams TStrings var ReplaceText String) begin if TagString = 'program' then ReplaceText = ExtractFilename (Forms Application Exename) else if TagString = 'date' then ReplaceText = DateToStr (Date) end Создание таблиц HTML Третья кнопка в примере HtmlProd называется Print Table. Она связана с компонен- том DataSetTableProducer и вызывает функцию Content, которая копирует результат 0868
Генерация файлов HTML 869 в свойство Text поля Мето. Таким образом, при соединении свойства DataSet этого компонента с ClientDataSetl генерируется стандартная таблица в формате HTML. Количество строк в таблице указывается в свойстве MaxRows. По умолчанию число строк равно 20, но можно указать и другое значение. Если необходимо выве- сти все строки, то указывается значение -1. Ж Date for Argentina - МоеШ« Не Edit View Go Bookmarks Tools Window Help Capital Buenos Aires Continent South America Area 2,777,815 Population 32,300,003 Last updated on 10/21/2002 HTML file produced by the program HtmlProd exe Data for Argentina Рис. 19.8. Вид окна браузера при нажатии на кнопку Print Line в программе HtmlProd СОВЕТ--------------------------------------------------------------------------------- Компонент DataSetTableProducer обрабатывает записи начиная не с первой, а с текущей. Поэтому при повторном нажатии на кнопку Print Table в выходных данных не будет ни одной новой записи. Чтобы избежать этого, следует добавить вызов метода First перед вызовом метода Content. Добиться более полного вывода данных можно двумя путями. Во-первых, можно включить информацию заголовка (Header) и завершения (Footer) для создания эле- ментов заголовка и закрытия HTML-документа и добавить заголовок таблицы (Caption). Во-вторых, можно настроить саму таблицу, используя свойства строк, таблицы и колонок (RowAttributes, TableAttributes и Columns). Редактор свойств ко- лонок, являющийся по умолчанию также и редактором компонента, позволяет ус- тановить большую часть этих свойств и сразу увидеть результат (рис. 19.9) Перед использованием этого редактора можно установить свойства полей таблицы при по- мощи редактора полей (Fields editor). Можно, например, вывести значения полей Area (площадь) и Population (население) с использованием разделителей разрядов. Рассмотрим три варианта настройки HTML-таблицы. о Свойство Columns можно использовать для настройки цвета и шрифта заголов- ков столбца, а также цвета и выравнивания оставшихся ячеек. О Свойство TField используется для вывода данных в нужном формате. В приме- ре свойство DysplayFormat поля ClientDataSetlArea установлено как ###,###,###. Можно даже встроить теги HTML в выходные данные поля. 0869
870 Глава 19. Интернет-программирование: сокеты и Indy Я [haDelauk J/., jl.................. рфйЬжИчг |s Kwts»«v|i“ №»-|wo 3 ЯеИХагее ЯЛПда Name TSttngFekl Capital TSttngFekl 3 Area TFbatFreld Population TFbetFeld DataSetTableProducer Demo i American Countries Capital Buenos An e\ South Aina ica LaPaz Continent Aiea Population > 7,815 32,300,003 South Aina ica 1 098,^5 .300 000 Рис. 19.9. Редактор свойства Columns компонента DataSetTableProducer показывает итоговую таблицу HTML (при подключении базы данных) О Для дальнейшей настройки вывода можно использовать обработчик событий OnFormatCell компонента DataSetTableProducer. Таким способом можно установить атрибуты отдельной ячейки в столбце или настроить вывод целой строки (со- хранив настройки в параметре CellData), в частности, встроить теги HTML. При помощи свойства Columns это сделать невозможно. Последним способом в примере HtmlProd установлены полужирный шрифт и красный фон в ячейках с большими значениями полей «площадь» и «население», procedure TFormProd DataSetTableProducerlFormatCel1(Sender TObject. CellRow, CellColumn Integer, var BgColor THTMLBgColor. var Align THTMLAlign var VAlign THTMLVAlign var CustomAttrs CellData String). begin if (CellRow > 0) and (((CellColumn = 3) and (Length (CellData) > 8)) or ((CellColumn = 4) and (Length (CellData) > 9))) then begin BgColor = 'red', CellData = ’<b>’ + CellData + '</b>', end. end. В оставшемся коде содержатся настройки компонента генерации таблиц, вклю- чая настройку заголовка и завершения. Просмотреть его можно, открыв исходный код примера HtmlProd. Использование стилей Последние версии HTML содержат мощный механизм разделения содержимого страницы и его представления — каскадные стили (Cascading Style Sheets, CSS). С его помощью можно отделить форматирование HTML-документа (цвет, вид и размер шрифта и т. д.) от собственно содержимого страницы и провести четкое 0870
Генерация файлов HTML 871 разделение между работой веб-дизайнера и работой программиста. Кроме того, код становится более гибким, что позволяет облегчить обновление сайта. Каскадные стили являются комплексной технологией, в которой форматирование задается для основных типов разделов HTML и специальных «классов». Более подробно об этом рассказано в руководствах по HTML. Изменить создание таблицы из примера HtmlProd, используя стили, очень лег- ко. Достаточно в свойстве Header второго компонента DataSetTablProduce добавить ссылку на используемый стиль: <link rel="stylesheet" type="text/css" href="test css"> и изменить код обработчика событий OnFormatCell, добавив вместо двух строк, из- меняющих цвет и начертание шрифта, следующую команду: CustomAttrs = 'class-"highthght"' Теперь красный фон ячейки и полужирный шрифт определяются как стиль highlight в таблице стилей (файл test.css можно посмотреть в исходном коде при- мера) в первом компоненте DataSetTablProduce. Для изменения общего вида таблицы достаточно изменить файл CSS, не трогая самого кода таблицы. При создании большого количества элементов форматиро- вания использование таблицы стилей позволяет уменьшить размер документа и, что немаловажно, время его загрузки. Динамические страницы на собственном сервере Компонент HtmlProd можно использовать не только для создания статических фай- лов HTML, но и как веб-сервер, используя подход, подобный рассмотренному в примере HttpServ, но более практичный. Программа обрабатывает запрос от од- ного из возможных генераторов страниц, сохраняя имя компонента в запросе. Это происходит в обработчике событий OnCommandGet компонента IdHTTPServer, кото- рый использует метод FindComponent для размещения соответствующего генерато- ра компонента. var Req. Html String. Comp TComponent begin Req = Requestinfo Document if Req [1] = '/' then Req = Copy (Req 2 1000), // пропустить знак Comp = FindComponent (Req). if (Req <> '') and Assigned (Comp) and (Comp is TCustomContentProducer) then begin ClientDataSetl First. Html = TCustomContentProducer (Comp) Content. end; Responseinfo ContentText = Html. end; Если параметр отсутствует (или имеет недопустимое значение), сервер выво- дит меню допустимых компонентов, основанных на HTML. Html = ’<hl>HtmlProd Menu<hl><p><ul>'. for I * 0 to Componentcount - 1 do if Components [i] is TCustomContentProducer then 0871
872 глава 19. Интернет-программирование: сокеты и Indy Html := Html + '<li><a href="/’ + Components [iJ.Name + + Components [iJ.Name + '</a></li>'; Html := Html + ’</ul></p>'; Если программа возвращает таблицу, использующую CSS, то браузер запросит этот файл у сервера. В программу добавлен код, позволяющий это сделать. Он демонстрирует, как сервер отвечает, возвращая файлы, а также как указывать тип MIME ответа (ContentType). if Pos ('test.css'. Req) > 0 then begin CssTest : = TStringList.Create; try CssTest.LoadFromFile (ExtractFil ePath (Application.ExeName) + 'test.css'); Responseinfo.ContentText ; = CssTest.Text; Responseinfo.ContentType : = 'text/css': finally CssTest.Free; end: Exit; end; Что далее? В этой главе мы рассмотрели ряд ключевых интернет-технологий, в том числе при- меры использования сокетов и основных протоколов Интернета (работу с элект- ронной почтой и HTTP). Много других примеров применения компонентов Indy можно найти в демонстрационных программах, написанных различными разра- ботчиками (эти программы не встроены в Delphi 7). После такого введения в мир Интернета можно углубиться в две его ключевые области: настоящее и будущее. В двух следующих главах речь пойдет о настоящем Интернета — создании веб-приложений (в том числе и динамических). Сначала рассмотрим более старую технологию WebBroker, а затем перейдем к новой архи- тектуре WebSnap. Далее, в главе 21, рассмотрим IntraWeb. Затем перейдем к буду- щему — в главах 22 и 23 рассмотрим технологию XML и другие, с ней связанные. 0872
Oft Веб-программирование с использованием структур WebBroker и WebSnap Роль сети Интернет в современном мире имеет тенденцию к повышению, что определяется успешным развитием глобальных сетей связи, основанных на НТТР- протоколе. В главе 19 мы обсуждали этот протокол и разработали использующие его приложения, которые размещаются на стороне клиента и на стороне сервера. Поскольку в настоящее время существует ряд высокопроизводительных, масштаби- руемых, удобных веб-серверов, вам вряд ли придется создавать свой собственный. Динамические приложения веб-серверов обычно построены на основе интеграции серверных сценариев и исполняемых программ веб-серверов, а не на замене их пользовательским программным обеспечением. Данная глава полностью сконцентрирована на разработке приложений, разме- щаемых на серверной стороне и расширяющих возможности существующих веб- серверов. Сейчас мы изучим, как осуществляется интеграция динамической гене- рации на сервере. Эта глава является логическим продолжением предыдущей, но не закрывает тему интернет-программирования в данной книге. Глава 21 посвя- щена существующей в Delphi 7 технологии IntraWeb. А глава 22 возвращается к вопросу интернет-программирования с точки зрения языка XML. ВНИМАНИЕ ---------------------------------------------------- Для изучения работы некоторых примеров данной главы необходимо иметь доступ к веб-серверу. Простейшее решение этой проблемы заключается в использовании уже установленной на вашем компьютере версии IIS или Personal Web Server компании Microsoft. Однако предпочтительней ис- пользовать Apache Web Server, который можно найти на сайте www.apache.org. Я не буду уделять много времени тонкостям настройки веб-сервера, разрешающей использование приложений. Эти сведения вы можете найти в описании своего сервера. В данной главе рассматриваются следующие вопросы: о динамические веб-страницы; о CGI-, ISAPI- и Apache-модули; о архитектура WebBroker; О отладчик Web Арр Debugger; о архитектура WebSnap; о адаптеры и написание серверных сценариев. 0873
874 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Динамические веб-страницы При «путешествии» по Интернету на клиентский компьютер, как правило, загру- жаются статические страницы (текстовые файлы HTML-формата). Как веб-ди- зайнер вы можете вручную создавать эти страницы, но для большинства случаев коммерческого применения более рационально строить статические страницы на основе сведений из баз данных (SQL-сервер, ряд файлов и т. п.). При использова- нии такого подхода производится «снимок» данных и представление его в HTML- формате, что приемлемо, когда данные меняются не часто. Этот вариант мы рас- сматривали в главе 19. Вместо статических HTML-страниц можно создавать и динамические страни- цы. При этом на основе запроса браузера производится непосредственное извле- чение сведений из базы данных. Таким образом, HTML-страница, отправляемая вашим приложением, представляет свежие текущие данные, а не «снимок» старых данных. Такой подход рационален при частом изменении данных. Как упоминалось ранее, существует пара способов программирования настра- иваемого поведения веб-сервера, и эти технологии идеально подходят для дина- мической генерации HTML-страниц. Помимо весьма популярных технологий, основанных на сценариях, существует два общих протокола для программирова- ния веб-серверов: CGI- (Common Gateway Interface) и API-веб-серверы. СОВЕТ---------------------------------------------------------- Не забывайте, что технология WebBroker среды Delphi (представленная как в редакции Enterprise Studio, так и в Professional) нивелирует различия между CGI и API-серверами, предоставляя общую структуру классов. Таким образом, можно легко переключить CGI- в ISAPI-библиотеку или интегри- ровать ее в Apache. Обзор CGI CGI — это стандартный протокол связи между браузером клиента и веб-сервером. Это не единственный эффективный протокол, но он широко распространен и яв- ляется платформенно-независимым. Этот протокол позволяет браузеру отправ- лять и получать данные. Он основан на стандартном вводе/выводе приложения, работающего с командной строкой (как правило, консольном приложении). Когда сервер обнаруживает запрос страницы, обращенный к CGI-приложению, он за- пускает приложение и отправляет стандартный вывод этого приложения обратно на компьютер клиента. Для написания CGI-приложений можно использовать множество средств и язы- ков программирования, a Delphi является лишь одним из них. Несмотря на оче- видное ограничение, заключающееся в том, что веб-сервер может строиться на Windows или Linux, в Delphi и Kylix имеется возможность построить довольно сложные CGI-программы. CGI является низкоуровневой технологией, поскольку для получения информации от веб-сервера и передачи обратно она использует стан- дартный ввод/вывод командной строки, а также переменные окружения. Для построения CGI-программы без использования поддерживающих ее клас- сов можно создать консольное приложение Delphi, удалить обычный модуль про- екта и заменить его следующими выражениями: 0874
Динамические веб-страницы 875 program CgiDate. {SAPPTYPE CONSOLE} uses SysUtils, begin wnteln ('content-type text/html'), writein, writein C<htmlxhead>"), writein ('<title>Time at this site</title>'}; writein ('<lhead><body>"). writein (‘<hl>Time at this site</hl>"), writein ('</?r>') wnteln C<h3>") wnteln (FormatDateTimef' “Today is “ dddd, mrrrrm d, yyyy. ' + "'<br> and the time is" hh mm ss AM/PM', Now)); wnteln C</h3>‘) wnteln ('<hr>"), writein (‘<i>Page generated by CgiDate exe</;>'). writein ('</body></html>“) end CGI-программы, используя стандартный вывод, создают заголовок, за которым следует HTML-текст. При непосредственном выполнении этой программы вы уви- дите этот текст в окне терминала. Если же эту программу запустить на веб-сервере и вывод отправить браузеру, то в последнем будет представлен отформатирован- ный HTML-текст (рис. 20.1). Time at this site Today is Monday, October 21, 2002, and the time is 12:30 AM Page generated by CgfDate.exe Рис. 20.1. Результат выполнения CgiDate в браузере Построение сложных приложений с помощью обычного CGI потребует боль- шого труда. Например, для извлечения сведений о состоянии HTTP-запроса необ- ходимо обратиться к соответствующим переменным окружения: // get the pathname GetEnvironmentVanable ('PATH_INFO' PathName sizeof (PathName)), 0875
876 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Использование динамических библиотек Полностью иной подход заключается в использовании API-веб-серверов: попу- лярных ISAPI (Internet Server API, представленных Microsoft), менее известных NSAPI (Netscape Server API) или API Apache. Эти API позволяют написать биб- лиотеки, которые сервер загружает в свое адресное пространство и оставляет в па- мяти. После того как сервер загрузил библиотеки, он может выполнять индивиду- альные запросы посредством организации потоков главного процесса, а не запуском EXE-файла для каждого запроса (как это происходит в CGI-приложениях). Когда сервер получает запрос на определенную страницу, он загружает DLL (если это еще не было сделано ранее) и выполняет соответствующий код, который для выполнения запроса может запустить новый поток или использовать существу- ющий. После этого библиотека отправляет соответствующие HTTP-данные обратно клиенту, запросившему страницу. Поскольку связь в основном осуществляется в оперативной памяти, такой тип приложений работает значительно быстрей CGI. Технология WebBroker среды Delphi Представленный фрагмент CGI-кода демонстрирует обычный, прямой подход к этому протоколу. Я мог бы представить аналогичные низкоуровневые примеры для модулей IS API или Apache, но в Delphi значительно интересней использовать технологию WebBroker. Она состоит из иерархии классов VCL и CLX (построен- ных для упрощения разработки серверных программ Интернета), а также модулей данных особого типа, называемых WebModules (веб-модули). Эта структура включе- на в обе редакции (Enterprise Studio и Professional) (в отличие от более новой и рас- ширенной структуры WebSnap, которая доступна только в версии Enterprise Studio). При использовании технологии WebBroker очень просто создать ISAPI- или CGI-приложения, а также Apache-модули. На первой странице (New) диалого- вого окна New Items выберите значок Web Server Application. Следующее диало- говое окно предложит выбрать ISAPI, CGI, модуль Apache 1 или 2, а также Web Арр Debugger: New Web Server Application t You may «elect bom one of the following types of Work) i Wide Web setvei appfcalms Г" ISAR/NSAPI Oynatmc tnk Library ‘ f £Ql Stand-alone execrable ", ’ fipachel it Shared Module (DLL) i Apache?» Shared Module [РЦ.1 I % ***** ***** ************ ***** X I Г* Web Арр Debugger executable* ’ I Class Marne j I Г Cn®r Platform < | Ohj Cancel { Help ....'......................... 0876
Технология WebBroker среды Delphi 877 В любом случае Delphi сгенерирует проект с WebModule, который является не- визуальным контейнером, подобным модулю данных (data module). Все эти моду- ли идентичны; будет изменяться только главный файл проекта. Для CGI он будет выглядеть следующим образом; program Project2 {SAPPTYPE CONSOLE} uses WebBroker CGIApp Umtl in 'Unitl pas' {WebModulel WebModule) {$R * res} begin Application Initialize Application CreateFormCTWebModulel WebModulel), Application Run end. Хотя это консольная CGI-программа, ее код похож на стандартное Delphi-при- ложение. Однако здесь используется небольшая уловка: объект Application, исполь- зуемый этой программой, является не обычным глобальным объектом классаТАррН- cation, а объектом нового класса. В зависимости от типа веб-проекта это объект класса TCGIApplication или другого класса, исходящего от TWebApplication. Самые основные действия производятся в WebModule. Этот компонент исходит от класса TCustomWebDispatcher, обеспечивающего поддержку ввода и вывода ва- ших программ. Класс TCustomWebDispatcher определяет свойства Request и Response, которые содержат запрос клиента и ответ, который будет отправлен обратно кли- енту. Оба этих свойства определены с использованием базового абстрактного класса (TWebRequest и TWebResponse), но приложение инициализирует их с помощью спе- циального объекта (подклассов TISAPIRequest и TISAPIResponse). Эти классы дела- ют доступной всю информацию, передаваемую на сервер, поэтому для доступа к этой информации используется единый подход. То же самое относится и к отве- ту, с которым легче манипулировать. Ключевое преимущество такого подхода за- ключается в том, что код, написанный с использованием WebBroker, не зависит от типа приложения (CGI, ISAPI и Apache); имеется возможность переходить от од- ного к другому, изменяя лишь файл проекта или заменяя его на другой — нет необ- ходимости изменять программный код, написанный в WebModule. Это является структурой Delphi-каркаса Для написания кода приложения в WebModule можно использовать редактор Actions, определяющий последователь- ность действий (хранимую в свойстве-массиве Actions) в зависимости от pathname (составного имени) запроса: ^Editing WebModule 1. Acttons 4 ЙЗ 4 ♦ Name PatHtfo; Enatiedj Defau# | Producer WebAcbonlteml Zone True x |WehAcHonhem2 /iwo True OataSelTablePtoducetl 0877
878 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Составное имя (полное имя пути) — это часть URL CGI- или ISAPI-приложе- ния, которое следует за именем программы, но до ее параметров, например pathl: http://www.example.com/scnpts/cgitest.exe/pathl7paraml-date Посредством определения различных действий ваше приложение может легко отвечать на запросы с различными составными именами. Вы также можете назна- чить различные компоненты-производители или вызвать различные обработчики событий OnAction для любого из возможных составных имен. Конечно же, для об- работки общих запросов составное имя можно опустить. Учтите также, что вме- сто построения приложения на основе WebModule можно использовать обычный модуль данных (data module) и добавить в него компонент WebDispatcher. Это ра- зумно в том случае, если вы хотите преобразовать существующее Delphi-приложе- ние в расширение веб-сервера. ВНИМАНИЕ ------------------------------------------------------------------------ WebModule происходит от базового класса WebDispatcher и не нуждается в нем как в отдельном компоненте. В отличие от WebSnap-приложений, WebBroker-программы не могут иметь несколько диспетчеров или множество веб-модулей. Обратите также внимание на то, что действия WebDispatcher ничего не должны делать с действиями, хранящимися в компонентах ActionList или ActionManager. При определении сопровождающих HTML-страниц, запускающих приложе- ние, ссылки будут выполнять запросы страниц к URL для каждого из этих путей (paths). Наличие единой библиотеки, которая может выполнять различные опера- ции в зависимости от параметров (в данном случае — составного имени), позволяет серверу содержать копию этой библиотеки в памяти и выполнять запросы пользо- вателей значительно быстрей. То же самое частично верно для CGI-приложений: сервер должен запустить несколько экземпляров, но может поместить файл в кэш- память и тем самым обеспечить к нему более быстрый доступ. При использовании события OnAction вы указываете ответ (response) на дан- ный запрос (request) — два основных параметра, передаваемые в обработчик собы- тия. Вот пример: procedure TWebModulel.WebModulelWebActionltemlAction(Sender: TObject; Request. TWebRequest: Response: TWebResponse: var Handled: Boolean), begin Response.Content : = '<htmlxheadxt7tJe>Hello Page</title></head><body>‘ + '<hl>Hello</hl>' + ’<hrxpxi>Page generated by Marco<h></p></body></html>‘; end. В свойстве Content параметра Response вы вводите HTML-текст, который дол- жен увидеть пользователь. Единственный недостаток заключается в том, что код, выводимый в браузере, будет корректно представлен в нескольких строках, но, про- сматривая сходный HTML, вы увидите единственную строку, соответствующую всему строчному значению. Для того чтобы сделать HTML-код удобным для восприятия (разбивкой на несколько строк), можно вставить символ #13 (возврат каретки) или (что даже лучше) использовать кросс-платформенное значение sLineBreak. Для того чтобы и другие действия обрабатывали этот запрос, последний пара- метр Handled устанавливается в False. По умолчанию он имеет значение True; при установке этого значения после обработки запроса вашим действием WebModule предполагает, что обработка завершена. Большая часть программного кода веб- 0878
Технология WebBroker среды Delphi 879 приложений приходится на обработчики событий OnAction действий, определен- ных в контейнере WebModule. Эти действия с помощью параметров Request и Response получают запросы от клиента и возвращают ответ. При использовании компонентов-производителей (продюсеров) событие OnAction часто возвращает в качестве Response.Content значение Content компонента-продю- сера с назначенной операцией. Можно сократить этот код, присвоив компонент- продюсер свойству Producer действия, и вам не придется больше писать эти обра- ботчики событий (но не стоит прибегать к обоим методам, поскольку это может привести к путанице). ПРИМЕЧАНИЕ----------------------------------------------------------------- Вместо свойства Producer можно использовать свойство Producercontent. Это свойство позволяет подключаться к пользовательским классам продюсеров, которые не являются наследниками класса TCustomContentProducer, а реализуют интерфейс I Producecontent. Свойство Producercontent явля- ется почти интерфейсным свойством: оно ведет себя точно так же, но это поведение обусловлено его редактором свойств и не зависит от поддержки Delphi интерфейсных свойств. Отладка с помощью Web Арр Debugger Отладка написанных в Delphi веб-приложений очень сложна. Нет возможности просто запустить программу и установить в ней точки прерывания. Вместо этого необходимо «убедить» веб-сервер запустить вашу CGI-программу или библиоте- ку с Delphi-отладчиком. Это можно сделать, указав основное приложение в диало- говом окне Run Parameters (Параметры запуска) среды Delphi, но этот подход под- разумевает, что Delphi запускает веб-сервер (который, как правило, является службой ОС Windows, а не автономной программой). Для решения этих проблем компания Borland разработала специальную програм- му — отладчик Web Арр Debugger. Эта программа, активизируемая соответствующим пунктом меню Tools (Сервис), является веб-сервером, ожидающим запросы на опре- деленном порту (по умолчанию — 1024). При поступлении запроса программа может перенаправить его автономному исполняемому файлу. В Delphi 6 эта связь была ос- нована на COM-технологиях, в Delphi 7 — на Indy-сокетах. В обоих случаях прило- жение веб-сервера можно запустить из IDE Delphi, установив все необходимые точки прерывания, а затем (после активизации программы через Web Арр Debugger) отла- дить программу точно так же, как это делается для обычных исполняемых файлов. Web Арр Debugger выполняет очень полезную задачу по протоколированию всех полученных запросов, а также ответов, отправляемых браузеру. Помимо этого про- грамма имеет страницу Statistics (Статистика), на которой отслеживается время, требуемое на выполнение каждого запроса, что позволяет проверить эффектив- ность приложения в различных условиях. Еще одной новой особенностью про- граммы Web Арр Debugger в Delphi 7 является то, что она является не VCL-, а CLX- приложением. Изменен пользовательский интерфейс и осуществлен переход от СОМ к сокетам, что сделало Web Арр Debugger доступной в Kylix. ВНИМАНИЕ--------------------------------------------------------------- Ввиду того что Web Арр Debugger использует Indy-сокеты, ваше приложение будет часто получать исключения типа EidConnClosedGracefully. По этой причине это исключение во всех проектах Delphi 7 автоматически отключено. 0879
880 Глава 20. Веб-программирование с использованием WebBroker и WebSnap За счет использования соответствующей функции в диалоговом окне New Web Server Application можно упростить создание нового приложения, совместимого с отладчи- ком. Эта функция определяет стандартный проект, содержащий как главную форму, так и веб-модуль. Эта (бесполезная) форма включает программный код, обеспечива- ющий код инициализации и добавления приложения в реестр Windows: initialization TWebAppSockObjectFactory Create! ’program name') Отладчик Web App Debugger использует эти сведения для получения списка до- ступных программ. Это приходится делать в тех случаях, когда используется стан- дартный URL отладчика, указываемый в форме как ссылка (рис. 20.2). Этот спи- сок включает все зарегистрированные серверы, а не только запущенные, и может использоваться для активизации программ. Однако это не очень удачная идея, поскольку для того, чтобы отладить программу, ее необходимо запускать в IDE Delphi. (Обратите внимание, что список можно расширить, щелкнув на View Details (Подробности); при таком представлении в список включаются исполняемые фай- лы и дополнительные сведения.) Модуль данных для проекта такого типа включает следующий код инициали- зации: uses WebReq initialization if WebRequestHandler <> ml then WebRequestHandler WebModuleClass = TWebModule2. Web App Debugger должен использоваться только для отладки. Для размещения (развертывания) приложения необходимо использовать другие средства. Можно создать файлы проекта другого типа серверных программ и добавить в проект того же веб-модуля в качестве отлаживаемого приложения. Обратный процесс более сложен. Для отладки существующего приложения вы должны создать программу такого же типа, удалить веб-модуль, добавить существу- ющий и скорректировать его добавлением строки, устанавливающей WebModuleClass класса WebRequestHandler, как в предыдущем фрагменте программного кода. Рйе ЕЛ Xfew So Bookmarks Tools Window Неф Registered Servers View List j toewDetnils -^•Search ] CustQueDebug CustQueP Projectl prova2 servennfo Serverinfo WSnap2WSnap2 WSnapMD WSnapMD WSnapSession WSnapSession Рис. 20.2. Список приложений, зарегистрированных отладчиком Web Арр Debugger 0880
Технология WebBroker среды Delphi 881 ВНИМАНИЕ------------------------------------------------------ Хотя зачастую имеется возможность перевести программу с одной веб-технологии на другую, это возможно не всегда. В примере CustQueP, рассматриваемом далее, вместо использования свойства ScnptName запроса (великолепно подходящего для CGI-программ) я использовал IntemalScnptName. С использованием Web Арр Debugger связаны еще два интересных момента. Во- первых, вы можете проверить работу программы без необходимости установки веб- сервера и без его настройки. Другими словам, для проверки работоспособности программы ее не надо устанавливать, можно проверить прямо на месте. Во-вто- рых, вместо предварительной разработки приложения, как в CGI, можно сразу приступать к экспериментированию с многопоточной архитектурой, не прибегая к загрузке и выгрузке библиотек (что зачастую подразумевает выключение веб- сервера и, возможно, даже компьютера). Создание многоцелевого WebModule Для демонстрации того, насколько легко создать многофункциональное сервер- ное приложение на основе Delphi я построил пример BrokDemo. Этот пример со- здан на основе технологии Web Арр Debugger, но он относительно прост для пере- компилирования его в качестве CGI-приложения или библиотеки веб-сервера. Ключевым элементом WеЬВгокег-примера является список действий. Действия могут изменяться с помощью редактора Actions или непосредственно в Object TreeView. Список действий также доступен на странице Designer редактора, так что имеется возможность графически показать их взаимосвязи с объектами базы данных. При рассмотрении можно отметить, что каждое действие имеет специфическое имя. Я также присвоил интуитивно понятные имена обработчикам событий OnAction. Например, TimeAction в качестве имени метода более понятно, чем автоматически генерируемое Delphi имя WebModulelWebActionltemlAction. Каждое действие имеет отличающееся составное имя (pathname) и оно помече- но как значение по умолчанию, которое используется, даже если составное имя не указано. Первая интересная идея, реализованная в программе, заключается в ис- пользовании двух компонентов PageProducer: PageHead и PageTail, которые форми- руют начальную и заключительную части каждой страницы. Централизация этого программного кода делает его проще для изменения, особенно если он основан на внешних HTML-файлах. HTML-код, создаваемый этими компонентами, добавля- ется в обработчике события OnAfterDispatch веб-модуля в начало и в конец конеч- ного HTML: procedure TWebModulel WebModulelAf'terD:spatch(Sender TObject Request TWebRequest Response TWebResponse. var Handled Boolean) begin Response Content = PageHead Content + Response Content + PageTail Content, end Начальная и конечная части добавляются на завершающем этапе генерации страницы потому, что это позволяет компонентам создавать HTML так, как будто они делают его целиком. Начало работы с HTML в событии OnBeforeDispatch сви- детельствует о том, что отсутствует возможность непосредственного присвоения компонентов-продюсеров действиям, или о том, что компонент-продюсер перекроет содержание свойства Content, которое уже предоставлено для ответа. 0881
882 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Компонент PageTail включает настраиваемый тег имени сценария, заменяемый представленным ниже кодом, в котором используется текущий объект-запрос, до- ступный в веб-модуле: procedure TWebModulel.PageTailHTMLTag(Sender: TObject: Tag: TTag: const TagString: String: TagParams: TStrings: var ReplaceText: String): begin if TagString = 'script' then ReplaceText := Request.ScriptName: end: Этот код активизируется для расширения тега <#script> свойства HTMLDoc ком- понента PageTail. Программный код временных действий совершенно ясен. Дей- ствительно интересная часть начинается с составного имени (path) Menu, которое является действием по умолчанию. В его обработчике события OnAction для пост- роения списка доступных действий приложение использует цикл for (используя их имена без первых двух символов, которыми в данном примере являются Wa), обеспечивая связь каждого из них с анкером (тегом <а>): procedure TWebModulel.MenuActlon(Sender: TObject: Request: TWebRequest; Response: TWebResponse: var Handled: Boolean): var I: Integer: begin Response.Content := '<h3>Menu</h3><ul>'#13; for I := 0 to Actions.Count - 1 do Response.Content := Response.Content + '<);> <a href-'" + Request.ScriptName + Act1on[I].Pathinfo + ' ">' + Copy (Action[I].Name, 3. 1000) + ’</a>’#13; Response.Content := Response.Content + '</ul>'; end: Пример BrokDemo также предоставляет пользователям список системных на- строек, относящихся к запросам, которые могут быть полезны при отладке. Они также показательны для изучения того, сколько сведений (а особенно — каких) передает HTTP-протокол от браузера к веб-серверу и обратно. Для производства этого списка программа ищет значение каждого свойства класса TWebRequest: procedure TWebModulel.StatusAction(Sender: TObject; Request: TWebRequest: Response: TWebResponse; var Handled: Boolean); var I: Integer: begin Response.Content := ‘<h3>Status</h3>'#13 + 'Method: ' + Request.Method + '<br>'#13 + 'Protocol Version: ' + Request.Protocol Version + '<br>'#13 + 'URL: ' + Request.URL + '<br>'#13 + 'Query: ' + Request.Query + '<br> '#13 + ... Динамическое создание отчетов базы данных Пример BrokDemo демонстрирует еще два действия, указываемые составными име- нами /table и /record. Для этих действий программа создает список имен и затем выводит полные сведения из каждой записи, используя для форматирования всей таблицы компонент DataSetTableProducer, а для построения представления записи — DataSetPageProducer. Вот свойства этих двух компонентов: 0882
Технология WebBroker среды Delphi 883 object DataSetTableProducerl: TDataSetTableProducer DataSet = dataEmployee DnFormatCel1 = DataSetTableProducerlFormatCell end object DataSetPage: TDataSetPageProducer HTMLDoc.Strings = ( '<h3>Employee: <#LastName></h3>‘ '<ul><h> Employee ID: <#EmpNo>' ‘<11> Name: <#FirstName> <#LastName>' ’<li> Phone: <#PhoneExt>' '<li> Hired On: <#HireOate>' '<li> Salary: <#Salary></u?>') DnHTMLTag = PageTailHTMLTag DataSet = dataEmployee end Для создания всей таблицы вы подключаете DataSetTableProducer к свойству Producer соответствующего действия, не предоставляя специального обработчика события. За счет добавления ссылок к конкретным записям таблица получается более мощной. Представленный ниже программный код выполняется в отноше- нии каждой ячейки таблицы, но ссылка создается только для первого столбца, а не для первой записи (строки с заголовком): procedure TWebModulel.DataSetTableProducerlFormatCel 1 (Sender: TDbject: Cell Row. Cell Column: Integer: var BgColor: THTMLBgColor; var Align: THTMLAUgn: var VAlign: THTMLVAUgn: var CustomAttrs, CellData: String); begin if (CellColumn = 0) and (CellRow <> 0) then CellData := ‘<a href-'" + ScriptName + '/record?LastName-‘ + dataEmployee!’Last_Name'] + '&F1rstName»' + dataEmployee['Flrst_Name'] + ’"> ' + CellData + ' </a>': end: Результат этого действия представлен на рис. 20.3. При выборе пользователем одной из ссылок программа вызывается снова. Она проверяет список строчных значений QueryFields и извлекает параметры из URL. Далее значения, соответству- ющие полям таблицы, используются для поиска записей (с помощью вызова Find- Nearest): procedure TWebModulel.RecordAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse: var Handled: Boolean); begin dataEmployее.Open; // перейти к запрашиваемой записи dataEmployee.Locate ('LAST_NAME:FIRST_NAME'. VarArrayOf([Request.QueryFiel ds. Va 1 ues[ ‘LastName' ]. Request.QueryFields.Values!‘FirstName']]). []); // получить выходное значение Response.Content := Response.Content + DataSetPage.Content; end: Запросы и формы Предыдущий пример использовал ряд компонентов, генерирующих HTML-содер- жание, представленных ранее в этой главе. В эту группу входят и другие компо- 0883
884 Глава 20. Веб-программирование с использованием WebBroker и WebSnap ненты, которые мы еще не использовали: QueryTableProducer (для BDE) и его брат SQL QueryTableProducer (для dbExpress). Как вы сейчас увидите, эти компоненты делают построение даже сложных программ работы с базами данных весьма лег- кой задачей. Не grit View Со goofcmatks Tods Г,- UUB wndow yelp |h /locdhost 1024/BrokDebug BrokD^ z j ЦэЦк JM» РД' WebBroker Oetno | Web Broker Demo A p Number Last Name First Name Phone Ext Hire Date Salary ±i Nelson Robert 1250 12/28/1988 105900 97500 102750 4 Young Bruce 1233 12/28/1988 Lambert Kim [22 2/6/1989 A Johnson Leslie 410 4/5/1989 64635 9 6 mr.c Forest PM [229 4/17/1989 75060 ”*'^6 Рис. 20.3. Результат работы примера BrokDemo, который создает HTML-таблицу с внутренними ссылками Предположим, что нам необходимо найти в базе данных всех клиентов Можно создать следующую HTML-форму (для оптимального форматирования встроен- ную в HTML-таблицу): <h4>Customer QueryProducer Search Form</h4> <form action="<#script>/search' method="POST"> <table> <tr><td>State </td> <td><input type= text name- State ></td></tr> <trxtd>Country </td> <td><input type- text name- Country ></td></tr> <tr><td></td> <td><center><input type- Submit ></center></td></tr> </table></form> СОВЕТ------------------------------------------------------------------------------------ Как и Delphi, HTML-формы могут содержать ряд компонентов. Для облегчения разработки этих форм существуют визуальные средства, хотя их можно определить и вручную с помощью HTML- кода. К допустимым элементам управления относятся кнопки, строки ввода (input text), комбиниро- ванные списки (selections) и переключатели (input buttons). Имеется возможность определить кнопки специальных типов, например Submit и Reset, которые реализуют стандартные действия. Важным элементом форм является метод запроса, в качестве которого может выступать либо POST (данные передаются автоматически и получить их можно в свойстве ContentFields), либо GET (данные пере- даются как часть URL, а извлечь их можно в свойстве QueryFields) Вы должны отметить очень важный элемент данной формы имена компонен- тов ввода (State и Country) должны точно соответствовать параметрам компонента SQLQuery 0884
Технология WebBroker среды Delphi 885 SELECT Customer State_Province. Country FROM CUSTOMER WHERE State_Province = State OR Country = Country Этот код используется в примере CustQueP (сокращение от Customer Query Producer: продюсер запроса клиентов). Для его создания я поместил в WebModule компонент SQLQuery и создал для него объекты-поля. В тот же WebModule я добавил компонент SQLQueryTableProducer, подключенный к свойству Producer действия /search. Программа генерирует верный отклик При активизации компонента SQL- Query-TableProducer вызовом его функции Content он инициализирует компонент SQLQuery за счет получения параметров из HTTP-запроса. Компонент может авто- матически изучить метод запроса и использовать либо свойство Query Fields (если запрос — GET), либо ContentFields (если запрос — POST). Одной из проблем использования статических HTML-форм является невоз- можность указания, по каким странам и штатам можно вести поиск. Для устране- ния этой сложности вместо элемента управления «строка редактирования» мож- но использовать элемент «поле со списком». Однако при добавлении пользователем новой записи в таблицу базы данных вам необходимо автоматически обновить список элементов. И в качестве конечного решения вы можете разработать ISAPI DLL, производящую форму на лету, заполняющую все элементы выбора доступ- ными элементами. HTML-код для данной страницы будет генерироваться действием /form, кото- рое подключается к компоненту PageProducer. PageProducer содержит следующий HTML-текст, встроенный в два специальных тега: <h4>Customer QueryProducer Search Form</h4> <form action= <#script>/search method= POST > <table> <tr><td>State </td> <td><select name='State"><option></option><#State_Province></select></td></tr> <tr><td>Country </td> <td><select name= Country ><option></option><#Country></select></td></tr> <tr><td></td> <td><center><input type= Submit ></center></td></tr> </table></form> Можно отметить, что эти теги имеют те же имена, что и некоторые поля таблиц. Когда PageProducer столкнется с одним из этих тегов, он добавит HTML-тег <option> для каждого неповторяющегося значения соответствующего поля Вот обработ- чик события OnTag, который является общим и может использоваться повторно: procedure TWebModulel PageProducerlHTMLTaglSender TObject Tag TTag const TagString String TagParams TStrings var ReplaceText String) begin ReplaceText = ' SQLQuery2 SQL Clear SQLQuery2 SQL Add ( select distinct + TagString + from customer") try SQLQuery2 Open try SQLQuery2 First while not SQLQuery2 EOF do begin ReplaceText = ReplaceText + 0885
886 Глава 20. Веб-программирование с использованием WebBroker и WebSnap '<option>' + SQLQuery2.Fields[0J.AsString + '</opt?on>'#13; SQLQuery2.Next; end. finail у SQLQuery2.Close; end; except ReplaceText := ’{wrong field: ' + TagString + end. end; Этот метод использует второй компонент SQLQuery, который я вручную помес- тил на форму и подключил к совместно используемому компоненту SQLConnection. Результат его работы представлен на рис. 20.4. Данное серверное расширение, как и многие другие, позволяет пользователю просматривать подробности определенной записи таблицы данных. Как и в пре- дыдущем примере, это достигается с помощью настройки вывода первого столбца, генерируемого компонентом QueryTableProducer: procedure TWebModulel.QueryTableProducerlFormatCel1( Sender: TObject. CellRow. CellColumn. Integer: var BgColor THTMLBgColor: var Align- THTMLAlign; var VAlign THTMLVAlign. var CustomAttrs. CellData: String); begin if (CellColumn = 0) and (CellRow <> 0) then CellData := ’<a href-"' + Request.ScriptName + '/record?Company=' + CellData + + CellData + '</a>'#13: if CellData = '' then CellData := '&nbsp:': end; Customer QueryPioducer Search Form State | Country .............’X Belgium Canada England Fiji France Hong Kong Italy Japan Netherlands Switzerland USA Рис. 20.4. Пример CustQueP создает HTML-форму с компонентом выбора, который динамически отображает изменение содержания базы данных 0886
Технология WebBroker среды Delphi 887 ПРИМЕЧАНИЕ-------------------------------------------------------------- Когда в HTML-таблице имеется пустая ячейка, большинство браузеров воспроизводит ее без грани- цы. По этой причине в каждую пустую ячейку я добавил символ неразрывного пробела (&nbsp;). Это необходимо делать в каждой HTML-таблице, генерируемой продюсерами таблиц Delphi. Действием для этой ссылки является /record и передача определенного элемен- та осуществляется после параметра ? (без имени параметра, что несколько нестан- дартно). Программный код, используемый для генерации HTML-таблиц на осно- ве записей, не использует компонент-продюсер, как мы делали ранее. Вместо этого он выводит данные каждого поля в пользовательскую таблицу: procedure TWebModulel.RecordAction(Sender- TObject. Request TWebRequest; Response. TWebResponse, var Handled: Boolean): var I: Integer: begin if Request QueryFields Count = 0 then Response Content := 'Record not found' else begin Query2 SQL.Cl ear; Query2.SQL Add ('select * from customer ' + 'where Company^"' + Request.QueryFields.Vai ues['Company'] + ""); Query2 Open: Response.Content •= ’<htmi><head><title>Customer Record</title></head><body>'#13 + '<hl>Customer Record: ' + Request QueryFieldsCO] + ’</W>'#13 + '<table border>'#13: for I := 1 to Query2.FieldCount - 1 do Response Content :- Response.Content + '<tr><td>' + Query2 Fields [I].FieldName + '</td>'#13'<td>' + Query2.Fields [I].AsString + '</td></tr>'#13: Response Content := Response.Content + '</tab/e></)r>'#13 + // указатель на форму запроса '<а href-'" + Request.SenptName + '/form"> ' + ' Next Query </a>’#13 + '</body></html>’fL3; end, end: Работа c Apache Если вы планируете использовать Apache вместо IIS или иного веб-сервера, то для развертывания приложений практически на любом сервере вы сможете восполь- зоваться преимуществами CGI-технологии. Однако использование CGI ведет к некоторому снижению скорости и к возникновению ряда проблем обработки све- дений о состоянии (поскольку отсутствует возможность сохранить какие-либо дан- ные в памяти). Это является основательной причиной написать ISAPI-приложе- ние или динамический модуль Apache. С помощью технологии WebBroker среды Delphi можно откомпилировать один и тот же программный код в обоих вариан- тах, поэтому перевод программы на другую веб-платформу стал значительно про- ще. CGI-программу или динамический модуль Apache можно также перекомпи- лировать в среде Kylix и разместить их на Linux-сервере. 0887
888 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Как я уже говорил, Apache может выполнять обычные CGI-приложения, но для осуществления быстрого отклика имеет специальную технологию сохранения про- грамм расширения сервера загруженными в память на все время. Для построения такой программы в Delphi можно воспользоваться значком Apache Shared Module (Совместно используемый модуль Apache) в диалоговом окне New Web Server Appli- cation (Новое веб-серверное приложение), выбрав вариант Apache 1 или Apache 2 в зависимости от версии планируемого к использованию веб-сервера. ВНИМАНИЕ --------------------------------------------------------------------------- Хотя среда Delphi 7 поддерживает Apache версии 2.0.39, она не поддерживает распространенную в настоящее время версию 2.0.40 ввиду изменений в библиотеке интерфейсов. Использование вер- сии 2.0.39 является неразумным, поскольку она имеет проблемы в системе безопасности. Сведения о том, как скорректировать VCL-код и сделать модули совместимыми с Apache 2.0.40 и выше, рассы- лаются членам Borland R&D через группу новостей и доступны на веб-сайте Боба Сворта (Bob Swart) по адресу www.drbob42.com/delphi7/apache2040.htm. Если вы решите создавать Apache-модуль, вы придете к необходимости созда- ния библиотеки, имеющей следующий исходный программный код: library Apachel: uses WebBroker. ApacheApp. ApacheWm in 'ApacheWm.pas ' {WebModulel: TWebModule}: {$R *.res} exports apachejnodule name 'apacheljnodule': begin Application.Initialize: Appl 1 cat 1 on.CreateForm(TWebModulel. WebModulel): Application.Run: end. Обратите внимание на инструкцию exports, которая указывает имя, используе- мое конфигурационным файлом Apache для ссылки на данный динамический мо- дуль. В исходный программный код проекта можно добавить еще два определе- ния: имя модуля и тип содержания: ModuleName := 'Apacheljnodule': ContentType:= 'Apachel-handler': Если не устанавливать эти значения, Delphi назначит их по умолчанию, добав- ляя строчные значения _module и -handler в имя проекта. Apache-модуль обычно размещается не в папке сценариев, а в подкаталоге modules сервера (по умолчанию — c:\Program Files\Apache\modules). Вам необходи- мо скорректировать файл http.conf, добавив в него строку загрузки модуля: LoadModule apacheljnodule modules/apachel.dll И, наконец, вы должны указать, когда вызывается этот модуль. Обработчик, определяемый модулем, может быть связан либо с данным расширением файла (так, чтобы модуль обрабатывал все файлы, имеющие указанное расширение), либо 0888
Практические примеры 889 с физической или виртуальной папкой. В последнем случае папка не существует, но Apache полагает, что она имеется. Вот как можно настроить виртуальную папку для модуля Apachel: «Location /Apachel>SetHandler Apachel-handler«/Location> Поскольку Apache по наследству чувствителен к регистру (основан на Linux), вы, вероятно, захотите добавить и вторую виртуальную папку с написанием име- ни в нижнем регистре: «Location /apachel>SetHand1er Apachel-handler«/Locat1on> Теперь вы можете вызвать приложение-пример с помощью URL: http://locaLhost/ Apachel. Преимущество использования в Apache виртуальных папок заключается в том, что пользователь не различает физические и динамические части сайта, в чем можно убедиться, экспериментируя с примером Apachel. Практические примеры После общего введения в теорию разработки серверных приложений с помощью WebBroker эту часть главы мы завершим практическими примерами. Первый — это классический веб-счетчик. Второй — расширение программы WebFind (пред- ставленной в главе 19), которая вместо заполнения списка создает динамическую страницу. Графический счетчик обращений Серверные приложения, которые вы строили до сих пор, были основаны только на работе с текстом. Конечно же, можно легко добавить ссылку на существующие графические файлы. Однако, что более интересно, можно построить серверные про- граммы, способные генерировать меняющееся во времени изображение (графику). Типичным примером является счетчик обращений к странице. Для написания веб-счетчика текущее значение обращений сохраняется в файл, а затем при каж- дом очередном обращении это значение считывается и увеличивается на единицу программой счетчика. Все, что надо для возвращения этих сведений, — это HTML- текст с количеством обращений. Программный код совершенно очевиден: procedure TWebModulel.WebModulelWebAct1 on ItemlActi on(Sender: TObj ect: Request: TWebRequest: Response: TWebResponse; var Handled: Boolean); var nHit: Integer: LogFIle: Text: LogFileName: string: begin LogFileName := 'WebCont.log'; System.Assign (LogFIle, LogFileName); try // прочитать, если файл существует if FileExIsts (LogFileName) then begin Reset (LogFIle): Readin (LogFIle. nHit); Inc (nHit): 0889
890 Глава 20. Веб-программирование с использованием WebBroker и WebSnap end else nHit = 0. // сохранить новые данные Rewrite (LogFile): Wnteln (LogFile. nHit): finally Close (LogFile): end: Response.Content := IntToStr (nHit), end: ВНИМАНИЕ----------------------------------------------------------------------------------- Данная упрощенная обработка файла не масштабируема. При одновременном обращении к страни- це нескольких посетителей этот программный код может вернуть неверные результаты или приве- сти к ошибке ввода/вывода, поскольку один поток может открыть файл для чтения, а другой поток может попытаться открыть его для записи. Для поддержки подобного сценария необходимо приме- нять мьютексы (или критические секции в многопоточных программах) для того, чтобы каждый последующий поток ожидал, пока текущий поток, использующий этот файл, завершит свою задачу. Более интересно создать графический счетчик, который бы мог легко внедряться в HTML-страницу. Существует два подхода к строительству графического счет- чика: можно подготовить битовое изображение для каждой выводимой цифры, а затем осуществлять их вывод с помощью программы либо позволить программе «рисовать» битовое изображение в памяти с последующим выводом графики. В WebCount я использовал второй подход. Сначала необходимо воспользоваться компонентом Image, размещающим изоб- ражение в памяти. Это изображение можно «нарисовать» с помощью обычных методов класса TCanvas. Затем подключить это изображение к объекту TJpeglmage. Обращение к битовому изображению с помощью компонента TJpeglmage вызыва- ет его преобразование в JPEG-формат. После этого можно сохранить JPEG-дан- ные в поток и вернуть их. Как можно заметить, необходимо пройти довольно мно- го этапов, но программный код получается вполне простой: // создать битовое изображение в памяти Bitmap •= TBitmap Create, try Bitmap.Width := 120: Bitmap.Height :- 25; // нарисовать цифры Bitmap Canvas Font.Name = 'Arial': Bitmap.Canvas Font.Size .= 14: Bitmap.Canvas Font.Color := RGB (255, 127. 0): Bitmap Canvas Font Style = [fsBold]: Bitmap.Canvas TextOut (1. 1. 'Hits. ' + FormatFloat (’###.###.###’, Int (nHit))); // преобразовать в JPEG и вывести Jpegl = TJpeglmage Create: try Jpegl CompressionQuality : = 50. Jpegl Assign(Bitmap): Stream : = TMemoryStream.Create: Jpegl.SaveToStream (Stream), Stream.Position = 0. Response.ContentStream .= Stream, 0890
Практические примеры 891 Response.ContentType := 'image/jpeg'; Response.SendResponse; // response-обьекг освободит поток finally Jpegl Free. end. finail у Bitmap.Free; end: За возвращение JPEG-изображения отвечают три выражения: два, которые ус- танавливают свойства Contentstream и ContentType объекта Response, и заключитель- ный вызов SendResponse. Тип содержания (контента) должен соответствовать типу возможных MIME-типов, понимаемых браузером, кроме того, важен порядок сле- дования выражений. Объект Response также имеет метод SendStream, но он должен вызываться только после отправки определенного типа данных отдельным вызо- вом. Результат выполнения этой программы: I webcount.eMe (3PfG ptxeU) * file Edit Sew Go gooktMris lotfc Window Help Hits: 29 Для внедрения программы в страницу добавьте в HTML следующий код: <img src=”http-//localhost/scripts/webcount.exe" border=0 alt=”hit counter"> Поиск с помощью поисковой машины В главе 19 мы рассматривали использование клиентского компонента Indy HTTP для извлечения результатов поиска сайта Google. Давайте расширим этот пример, преобразовав его в серверное приложение. Программа WebSearcher, доступная как CGI-приложение или исполняемый файл отладчика Web Арр Debugger, имеет два действия: сначала возвращает HTML, полученный от поисковой машины, а затем проверяет HTML-заполнение компонента ClientDataSet (клиентский набор данных), который привязывается к продюсеру таблиц страниц для генерации окончатель- ного вывода. Вот программный код второго действия: const strSearch = 'http://мм google.сот/search?as_q=borland+delphi&num=100’: procedure TWebModulel.WebModulelWebActionltemlAction(Sender: TObject. Request: TWebRequest. Response: TWebResponse. var Handled: Boolean): var I: integer: begin if not cds Active then cds.CreateDataSet else cds.EmptyDataSet; 0891
892 Глава 20. Веб-программирование с использованием WebBroker и WebSnap for 1 := 0 to 5 do // сколько страниц? begin // получить данные с поискового сайта GrabHtml (strSearch + ’&start=' + IntToStr (i*100)): // сканировать их для заполнения набора данных HtmlStringToCds: end; cds.First: // возвратить содержание продюсера Response.Content := DataSetTableProducerl.Content: end: Метод GrabHtml идентичен примеру WebFind. Метод HtlStringToCds похож на со- ответствующий метод примера WebFind (который добавляет пункты в список); он добавляет адреса и их текстовое описание посредством вызова: cds.InsertRecord ([0, strAddr. strText]): Компонент ClientDataSet настраивается тремя полями: два строчных и счетчик строк. Это пустое дополнительное поле требуется для включения дополнительно- го столбца в продюсер таблицы. Программный код заполняет столбец в событии форматирования ячейки, который помимо прочего добавляет гиперссылку: procedure TWebModulel.DataSetTableProducerlFormatCel 1 (Sender: TObject: CellRow. CellColumn: Integer; var BgColor: THTMLBgColor: var Align: THTMLAlign: var VAlign: THTMLVAlign: var CustomAttrs, CellData: String): begin if CellRow <> 0 then case CellColumn of 0: CellData := IntToStr (CellRow): 1: CellData := '<a href="’ + CellData + ' ">' + SplitLong(CellData) + '</a>': 2: CellData := SplitLong (CellData): end: end: Вызов SplitLong используется для добавления дополнительных пробелов в вы- ходной текст, что позволяет избежать наличия очень больших столбцов. (Браузе- detphi resources Рис. 20.5. Программа WebSearch показывает результаты поиска, выполненного поисковой машиной Google Л О v**< go Toofc нМы borlaiid developer netw borne page delphi & amp. kykx c home page boilaiid delphi “ studio 1 eleased1 _____ introducing boilaiid de page 1 4 delplu super page library for b delplu delphi web devel develop bmes Iv -•-& novel devnote-& quot. netware appbcahons _ bniiand dalel 0892
WebSnap 893 ры не умеют разбивать текст на несколько строк, если они не содержат пробелы или другие специальные символы.) Результат этой программы — несколько мед- ленное приложение (поскольку оно должно перенаправить множество НТТР-зап- росов). Результат его работы представлен на рис. 20.5. WebSnap А сейчас, когда я представил основные элементы разработки приложений веб-сер- веров в Delphi, давайте перейдем к более сложной архитектуре, появившейся в Delphi 6: WebSnap. Существует две основные причины, по которым я не пере- шел к этому вопросу в начале главы. Во-первых, WebSnap построена на фунда- менте, предлагаемом технологией WebBroker; нельзя изучать использование но- вых функций, если вы не знаете основ. Например, WebSnap в техническом плане представляет собой CGI-программу либо ISAPI- или Apache-модуль. Во-вторых, поскольку WebSnap входит в состав только редакции Enterprise Studio, возмож- ность его использования имеют не все программисты. WebSnap имеет несколько значительных отличий от WebBroker, таких как воз- можность наличия множества веб-модулей, каждый с соответствующей страни- цей, интеграция с серверными сценариями, XSL и поддержка технологии Internet Express (два последних элемента будут рассмотрены в главе 22). Более того, для обработки общих задач, таких как регистрация в системе, управление сеансом и т. п.? имеется множество готовых к использованию компонентов. Вместо пере- числения всех этих функций в WebSnap я решил рассмотреть их по мере возраста- ния сложности и в применении к конкретным приложениям. Эти приложения в целях проверки построены с использованием Web Арр Debugger, но их можно легко разместить на требуемом сервере с помощью любой из доступных технологий. При разработке WebSnap-приложения точкой отсчета является диалоговое окно, которое можно вызвать либо со страницы WebSnap диалогового окна New Items (File ► New ► Other) (рис. 20.6), либо с помощью панели инструментов Internet среды разработки (которая по умолчанию не видна). Это окно позволяет выбрать тип приложения (как и при создании WebBroker-приложения) и определить ис- ходные компоненты приложения (далее можно добавлять и другие). Нижняя часть диалогового окна определяет поведение первой страницы (домашней страницы или страницы по умолчанию). Аналогичное диалоговое окно выводится и для по- следующих страниц. Если оставить настройки по умолчанию и ввести имя домашней страницы, ди- алоговое окно создаст проект и откроет TWebAppPageModuLe. Этот модуль содержит указанные по умолчанию компоненты: О WebAppComponents — контейнер для всех централизованных служб WebSnap- приложения, таких как список пользователей, базовый диспетчер, службы се- анса и т. п. Не все свойства могут устанавливаться, поскольку приложению могут не понадобиться некоторые из служб. О одна из основных служб, предлагаемых компонентом PageDispatcher, которая (автоматически) содержит список доступных страниц приложения и опреде- ляет страницу по умолчанию. 0893
894 Глава 20. Веб-программирование с использованием WebBroker и WebSnap New WebSnap ДррйсаЬоп You may iU$tK%!roinone ofthe following types ot ж Wide Web swvet «ppSeatkm „ Г jSAR/HSARDynanseLirklJbfary e Spaohetx StwredModifeJDLL} C Apache JvSh&edMedutefDU-l G Web App Oebuggei executable Hess I Рис. 20.6. Варианты, предлагаемые диалоговым окном New WebSnap Application О Еще одна основная служба, предоставляемая компонентом AdapterDispatcher, которая обрабатывает подзадачи HTML-форм и запросы изображений. О ApplicationAdapter — первый компонент из семейства адаптеров, с которыми вы столкнетесь. Эти компоненты предлагают поля и действия для серверных сце- нариев используемых программой. В частности, ApplicationAdapter имеет адап- тер полей, отражающий значение своего собственного свойства ApplicationTitle. Если ввести значение этого свойства, то оно станет доступным в сценариях. О PageProducer, который создает HTML-код страницы, в данном случае — страни- цы по умолчанию. В отличие от WebBroker-приложения, HTML-код этого ком- понента не хранится в свойстве HTMLDoc (список строк) или не указывается свой- ством HTMLFile. HTML-файл — это внешний файл, по умолчанию хранящийся в папке, содержащей исходный программный код проекта. Обращение к нему из приложения происходит с помощью выражения, подобного выражению вклю- чения ресурсов: {*.html}. О Поскольку HTML-файл, включаемый PageProducer, содержится как отдельный файл (в его размещении вам в конечном итоге поможет компонент LocateFile- Service), его можно редактировать, изменяя результирующую страницу програм- мы без необходимости перекомпиляции приложения. Благодаря поддержке серверных сценариев возможные изменения могут быть связаны не только с фиксированной частью HTML-файла, но и с некоторым динамическим со- держанием. По умолчанию HTML-файл основан на стандартном шаблоне, уже содержащем элементы сценария. 0894
WebSnap 895 ВНИМАНИЕ----------------------------------------------------------------- Сходство между включением ресурсов и ссылкой на HTML в основном является лишь синтаксичес- ким. HTML-ссылка используется только для привязки местонахождения файла во время разработки в то время как директива include осуществляет включение данных, на которые она ссылается, не- посредственно в исполняемый файл. Благодаря этой директиве в редакторе Delphi можно просмотреть HTML-файл (с разумным выделением синтаксиса), выбрав соответствующую нижнюю вклад- ку. Редактор также имеет страницы для WebSnap-модуля, включая по умолчанию страницу HTML Result, на которой можно просмотреть HTML, сгенерированный после обработки сценариев, а также страницу Preview, на которой можно увидеть, как страница будет представлена в браузере пользователя. Редактор Delphi 7 для WebSnap-модуля включает более мощный редактор HTML, чем в Delphi 6. В нем более удачно реализовано выделение синтаксиса и функция code completion. Если вы предпочитаете редактировать HTML-код вашего приложения в более совер- шенном редакторе, его можно указать на странице Internet диалогового окна Environ- ment Options (Параметры окружения). Щелкните на кнопке Edit HTML-расшире- ния и выберите внешний редактор из контекстного меню редактора или с помощью специальной кнопки на панели инструментов Internet среды Delphi. Стандартный HTML-шаблон, используемый WebSnap, добавляет в любую стра- ницу программы свой заголовок и название приложения с помощью подобных строк сценария: <hl><%= Application Title Ж></И1> <h2><%= Page Title X></h2> Мы вскоре вернемся к написанию сценариев; в следующем разделе мы присту- пим к разработке примера WSnapl, но сначала завершим обзор, представив исход- ный программный код простого модуля веб-страницы: type Thome = class(TWebAppPageModule) end function home Thome implementation {$R ★ dfm} {* html} uses WebReq WebCntxt. WebFact. Variants. function home Thome. begin Result = Thome(WebContext FindModuleClass(Thome)). end initialization if WebRequestHandler <> ml then WebRequestHandler AddWebModuleFactory(TWebAppPageModuleFactory Create( 0895
896 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Thome, TWebPagelnfo Create([wpPublished {, wpLoginRequired}] ' html'), caCache)). end Для поддержки кэширования страниц этот модуль вместо обычного глобаль- ного объекта формы использует глобальную функцию. Данное приложение в раз- деле инициализации имеет дополнительный программный код (именно в коде ре- гистрации), позволяющий приложению «знать» роль страницы и ее поведение. ПРИМЕЧАНИЕ------------------------------------------------------------------- В отличие от представленных в данной главе примеров использования WebBroker, WebSnap-приме- ры компилируются каждый в свою папку. Это сделано из-за того, что в ходе их выполнения необхо- димы HTML-файлы, а я по возможности стараюсь упрощать размещение. Управление множеством страниц Первая заметная разница между WebSnap и WebBroker заключается в том, что вместо наличия одного модуля данных с множеством действий, подключенных к компонентам-продюсерам, WebSnap имеет множество модулей данных, каждый из которых соответствует действию и имеет компонент-продюсер с присоединен- ным к нему HTML-файлом. В страницу (модуль) можно добавлять множество действий, но основная идея заключается в том, что приложения структурируются относительно страниц, а не относительно действий. При работе с действиями имя страницы указывается в имени запроса. I PageProducerPagel Tjfe | Page Pr oduc er Р age 1 Eubbshect p logh Required Г" Modute Qp&ow Creation. |0nDemand Caehngi | Cache Instance Г Oefayit OK j Cancel | Рис. 20.7. Диалоговое окно New WebSnap Page Module 0896
WebSnap 897 В качестве примера в WebSnap-приложение (создаваемое по умолчанию) я до- бавил две страницы. Для первой страницы в диалоговом окне New WebSnap Page Module выберите стандартный продюсер страницы и укажите имя date (рис. 20.7). Для второй — выберите DataSetPageProducer и назовите его country. После сохране- ния файлов можно приступать к проверке работы приложения. Благодаря сцена- рию, который мы рассмотрим далее, каждая страница содержит список всех дос- тупных страниц (если только вы не сбросили флажок Published в диалоговом окне New WebSnap Page Module). Страницы будут пустыми, но, по крайней мере, структура сохранена. Для за- вершения создания домашней страницы можно непосредственно отредактировать присоединенный к ней HTML-файл. Для страниц данных используйте тот же под- ход, что и в WebBroker-приложении. Добавьте в HTML-текст несколько настраи- ваемых тегов: <p>The time at this site is <#time> </p> Для замещения этого тега конкретным значением времени я также добавил программный код в обработчик события OnTag компонента-продюсера. Для третьей страницы (страница страны) включите в HTML теги для полей таблицы «country»: <h3>Country <#name></h3> После чего подсоедините ClientDataSet к продюсеру страницы: object DataSetPageProducer TDataSetPageProducer DataSet = cdsCountry end object cdsCountry: TCIientDataSet FileName = 'C \Program FTles\Coimon Files\Borland Shared\Data\country cds' end Чтобы открыть этот набор данных при первом создании страницы и сбросе их и переходе к первой записи для последующих обращений, необходимо обработать событие OnBeforeDispatchPage модуля веб-страницы: cdsCountry Open cdsCountry First Если вы планируете преобразовать существующий WebBroker-код в WebSnap- архитектуру, очень важно, что WebSnap-страница может быть очень похожа на часть WebBroker-приложения (особенно в действиях, связанных с продюсером). Кроме того, можно также перевести существующий компонент DataSetTable Producer в новую архитектуру. Технически можно создать новую страницу, удалить ее ком- понент-продюсер, заменить его на DataSetTableProducer и связать этот компонент со свойством PageProducer модуля веб-страницы. На практике этот подход приведет к отрыву HTML-файла от его сценариев. В примере WSnapl была использована более разумная методика. Я добавил в HTML-файл настраиваемый Ter(<#htmltable>) и использовал событие OnTag про- дюсера страницы для добавления в HTML результирующей таблицы набора дан- ных: if TagString = 'htmltable' then ReplaceText = DataSetTableProducerl Content. 0897
898 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Серверные сценарии Наличие множества страниц в серверной программе (каждая из которых связана со своим модулем страниц) изменяет сам способ написания программ. Наличие на стороне сервера сценариев предлагает еще более мощный подход. Например, стандартный сценарий примера WSnapl учитывает приложение и заголовки стра- ниц, а также индекс страниц. Индекс генерируется нумератором (enumerator) — технология, используемая для сканирования списка в коде сценария WebSnap. Давайте рассмотрим его: <table cell spacing»"О" cellpadding="O"><td> <% е = new Enumerator!Pages) s = '' c - 0 for (. ie.atEndO: e moveNextO) { if (e.itemO Published) { if (c > 0) s += '&nbsp;|&nbsp. ' if (Page.Name != e.item().Name) s += ’<a href»’" + e.itemO.HREF + + e.itemO .Title + ’</a>' else s +» e.item().Title C++ if (c>l) Response.Write(s) %> </td></table> СОВЕТ----------------------------------------------------------------------------- Обычно сценарии WebSnap пишутся на JavaScript — объектно-ориентированном языке, известном в качестве языка интернет-программирования ввиду того, что он является единственным языком сценариев, обязательно обрабатываемым браузерами (на клиентской стороне). JavaScript (имею- щий техническое название ECMAScript) позаимствовал стержневой синтаксис языка С и практически не связан с языком Java. WebSnap использует механизм ActiveScripting компании Microsoft, поддер- живаемый как JScript (разновидность JavaScript), так и VBScript. В отдельной ячейке этой таблицы (которая, как это ни странно, не имеет строк) сценарий с помощью команды Response.Write выводит строчное значение. Это зна- чение строится с помощью цикла for, количество повторений которого определя- ется значением нумератора страниц приложения, хранимым в глобальном элементе Pages. Заголовок каждой страницы добавляется к строчному значению только в том случае, если страница опубликована. Каждый заголовок использует гиперссылку без включения ссылки на текущую страницу. Наличие такого кода в сценарии, а не в жестко программируемом коде Delphi-компонента, позволяет передавать его гра- мотному веб-дизайнеру, который может превратить его во что-то более привлека- тельное в визуальном плане. ПРИМЕЧАНИЕ------------------------------------------------------------------" Понятие «опубликованной» или «неопубликованной» страницы не связано с каким-либо свойством в модуле веб-страницы. Это состояние контролируется флагом метода AddWebModuleFactory, вызы- ваемым кодом инициализации модуля веб-страницы. Для достижения желаемого результата можно данный флаг «закрыть» или «открыть» комментарием. 0898
Серверные сценарии 899 В качестве примера возможностей сценариев в пример WSnap2 (расширение примера WSnapl) я добавил страницу демонстрационного сценария (demoscript). Сценарий данной страницы может сгенерировать полную таблицу (результат пред- ставлен на рис. 20.8): <table border»1 cellspacing=O> <tr> <th>&nbsp:</th> <% for (j=l;J<-5:j++) { %> <th>Column <£=j %></th> <% } %> </tr> <£ for (i=l:i<~5,i++) { %> <tr> <td>Line <£=i %></td> <% for (j=1.j<-5:j++) { <td>Value= <%=i*j %></td> <% } Z> </tr> <% } </table> demoscript Column 1 Column 2 Column 31 Column 4 Column 5 Line 1 Value= 1 Value= 2 Value= 3 • Value= 4 Value= 5 Line 2 Vahie=2 Value=4 Vahe= 6 i Value= 8 Vahe= 10 Line 3 Value= 3 Value= 6 Value= 9 :Valut= 12 Value= 15 Valuer 20 Line 4 Value= 4 Vahe= 8 Value= 12;Value= 16 Line 5 Value= 5 Value= 10 Value= 15 Value=20 Value= 25 Рис. 20.8. Пример WSnap2 напоминает обычный сценарий и пользовательское меню, хранимое во включаемом файле В этом сценарии символ <%= заменяет более длинную команду Response.Write. Еще один важный момент использования сценариев на стороне сервера заключа- ется в возможности включении одних страниц в другие. Например, если вы пла- нируете заменить меню, вы можете включить соответствующий HTML и сцена- рий в отдельный файл, а не изменять его и корректировать на всех страницах. Включение файла обычно выполняется подобным выражением: <!-- ^include fi 1 е= "menu .html" --> В листинге 20.1 представлен полный исходный программный код включения файла меню, на который ссылаются все остальные HTML-файлы проекта. На 0899
900 Глава 20. Веб-программирование с использованием WebBroker и WebSnap рис. 20.9 представлен пример этого меню, выводимого в верхней части страницы с помощью рассмотренного ранее сценария генерации таблицы. Листинг 20.1. Файл menu.html, включаемый в каждую страницу примера WSnap2 <html> <head> < fitle><£= Page.Title 3X/title> </head> <body> < h2><£= Application.Title %></h2> «table cellspacing="O" cellpadding="2" border="l" bgcolor="#cOcOcO"> <tr> < % e = new Enumerator(Pages) for (; le.atEndO; e.moveNext()) { if (e. item(). Published) { if (Page.Name .'= e.itemf).Name) Response.Write ('<td><a href=‘" + e.item().HREF + + e.itemf). Title + '</a></td>') else Response.Write ('<td>' + e.itemf). Title + ’</td>) } } X> </tr> </table> <hr> <hl><£= Page.Title %></hl> <p> Этот сценарий формирования меню использует список Pages и глобальные объекты сценариев Раде и Application. WebSnap делает доступными и ряд других глобальных объектов, включая EndUser и Session (если вы добавите в приложение соответствующие адаптеры), Modules и Producer, которые позволяют обращаться к компоненту Producer модуля веб-страницы. Сценарию также доступны объекты Response и Request этого модуля. Адаптеры Помимо перечисленных выше глобальных объектов в сценарии можно обращать- ся ко всем адаптерам, доступным в соответствующем модуле веб-страницы. (При обращении к адаптерам других модулей, включая модули совместно используе- мых веб-данных, перед их именем необходимо использовать префикс, состоящий из объекта Modules и имени соответствующего модуля.) Адаптеры позволяют пе- редавать сведения от откомпилированного кода Delphi в интерпретируемый сце- нарий, обеспечивая для вашего приложения интерфейс работы со сценариями. Адаптеры содержат поля, представляющие данные, и действия, представляющие команды. Серверные сценарии могут обращаться к этим значениям и выполнять эти команды, передавая им специальные параметры. Поля адаптеров Для осуществления подстройки в адаптеры можно добавить новые поля. Напри- мер, в примере WSnap2 я добавил настраиваемое поле. После выделения этого ком- 0900
Серверные сценарии 901 понента его можно либо открыть в редакторе полей (доступен с помощью контек- стного меню), либо с помощью события OnGetValue назначить ему значение. По- скольку хотелось бы сосчитать количество ответов (или обращений (hit)) на лю- бой странице веб-приложения, для увеличения значения локального счетчика (HitCount) также можно обрабатывать событие OnBeforePageDispatch глобального компонента PageDispatcher. Вот программный код этих двух методов: procedure Thome.PageD1spatcherBeforeDispatchPage(Sender: TObject; const pageName: String: var Handled: Boolean); begin Inc (HitCount): end: procedure Thome.AppHjtCountGetValue(Sender: TObject: var Value: Variant); begin Value := HitCount: end: Конечно же, для подсчета обращений к каждой странице можно использовать имя страницы (также можно было бы обеспечить непрерывность, поскольку счет- чик сбрасывается каждый раз при запуске нового экземпляра приложения). Пос- ле того как пользовательское поле добавлено в существующий адаптер (соответ- ствующий объекту сценариев Application), к нему можно обращаться из любого сценария, например: <p>Application hits since last activation: <%= Application.AppHitCount. Value %></p> Компоненты-адаптеры Точно таким же образом на определенные страницы можно добавлять пользова- тельские адаптеры. Если необходимо обеспечить передачу между несколькими полями, воспользуйтесь общим компонентом Adapter. К другим настраиваемым адаптерам (помимо уже знакомого глобального Application Adapter) относятся: О компонент PagedAdapter, имеющий встроенную поддержку вывода его содержа- ния на множество страниц; О компонент DataSetAdapter, используемый для обращения из сценария к набору данных Delphi (dataset) (см. раздел « WebSnap и базы данных»); О компонент StringValueslist, который содержит список пар «имя-значение» и мо- жет использоваться как непосредственно, так и для предоставления значений полю адаптера. Производный адаптер DataSetValuesList играет ту же роль, но по- лучает список пар непосредственно из набора данных, обеспечивая этим под- держку подстановок и проч.; О ориентированные на пользователей адаптеры, такие как EndUser, EndUserSession и LoginForm, используются для доступа к сведениям о пользователе и сеансе, а также для создания в приложении формы регистрации, которая автоматиче- ски соединена со списком пользователей. Эти адаптеры будут рассмотрены да- лее в этой главе. Использование AdapterPageProducer Большинство из представленных адаптеров используются совместно с компонен- том AdapterPageProducer. Этот компонент может генерировать разделы сценария 0901
902 Глава 20. Веб-программирование с использованием WebBroker и WebSnap после того, как вы визуально определили требуемый результат. В качестве приме- ра я добавил в приложение WSnap2 страницу ввода/вывода (inout), имеющую адап- тер с двумя полями (одно стандартное, другое — логического типа): object Adapted: TAdapter OnBeforeExecuteAction = AdapterlBeforeExecuteAction object TAdapterActions object AddPlus. TAdapterAction OnExecute = AddPlusExecute end object Post: TAdapterAction OnExecute = PostExecute end end object TAdapterFields object Text: TAdapterField OnGetValue = TextGetValue end object Auto: TAdapterBooleanField OnGetValue = AutoGetValue end end end Этот адаптер также содержит пару действий, осуществляющих отправку ввода текущего пользователя и добавление в текст знака «+». Такой же знак «+» добав- ляется, если установлен флажок Auto. Разработка пользовательского интерфейса для этой формы и связанного с ним сценария требует потратить некоторое время на работу с «открытым» HTML. Но используемый в этой странице компонент AdapterPageProducer имеет встроенный конструктор HTML, который компания Borland назвала Web Surface Designer. При использовании этого инструмента мож- но визуально добавить форму в HTML-страницу и в нее добавить AdapterFieldGroup. Для автоматического вывода редакторов двух полей подключите эту группу по- лей к адаптеру. Далее, для предоставления кнопок всех действий адаптера, можно добавить AdapterCommandGroup и связать его с AdapterFieldGroup (рис. 20.9). Если быть более точным, поля и кнопки выводятся автоматически, если уста- новлены свойства AddDefaultFields и AddDefaultCommands группы полей и группы команд. Эффект визуальных операций по построению этой формы можно увидеть в следующем фрагменте DFM-файла: object AdapterPageProducer- TAdapterPageProducer object AdapterForml TAdapterForm object AdapterFieldGroupl. TAdapterFieldGroup Adapter = Adapter! object FldText: TAdapterDi spl ay Field FieldName = 'Text' end object FldAuto TAdapterDisplayField FieldName = 'Auto' end end object AdapterCommandGroup!. TAdapterCommandGroup Displaycomponent = AdapterFieldGroupl object CmdPost TAdapterActionButton ActionName = 'Post' 0902
Серверные сценарии 903 end object CmdAddPlus: TAdapterActionButton ActionName = 'AddPlus' end end end end Рис. 20.9. Web Surface Designer при разработке страницы inout примера WSnap2 А теперь, когда имеется HTML-страница со сценариями по перемещению дан- ных в обоих направлениях, издающая команды, давайте рассмотрим исходный программный код, обеспечивающий работу этого примера. Сначала необходимо добавить в класс два локальных поля для хранения и манипулирования полями адаптера, кроме того, для них необходимо реализовать событие OnGetValue, возвра- щающее значения полей. При щелчке на каждой из кнопок необходимо извлечь текст, передаваемый пользователем (автоматического копирования в поле соот- ветствующего адаптера не происходит). Этого можно достичь просмотром значе- ния свойства ActionValue этих полей, которое устанавливается только в том случае, если что-либо введено (по этой причине при пустом поле ввода поле логического типа устанавливается в False). Чтобы не повторять этот программный код для обо- их действий, поместите его в событие OnBeforeExecuteAction данного модуля веб- страницы: procedure Tinout.AdapterlBeforeExecuteAction(Sender. Action TObject. Params: TStrings; var Handled- Boolean); begin if Assigned (Text.ActionValue) then fText ;= Text.ActionValue.Values [0]: fAuto := Assigned (Auto.ActionValue). end; Обратите внимание, что хотя каждое действие может иметь несколько значе- ний (в том случае, если компонент допускает множество выделений), в данном 0903
904 Глава 20. Веб-программирование с использованием WebBroker и WebSnap случае это не так: можно извлечь первый элемент. И наконец, вот программный код событий OnExecute этих двух действий: procedure Tinout.AddPlusExecutetSender: TObject: Params: TStrings): begin fText := fText + end: procedure Tinout.PostExecute(Sender: TObject: Params: TStrings): begin if fAuto then AddPlusExecute (Self, nil): end; В качестве альтернативы поля адаптера имеют public-свойство EchoActionField- Value, которое можно установить для получения значения, введенного пользовате- лем, и помещения в результирующую форму. Эта технология обычно использует- ся в случае возникновения ошибок, что позволяет пользователю изменить ввод, начиная с уже введенных значений. СОВЕТ----------------------------------------------------------------------------------- Компонент AdapterPageProducer имеет специальную поддержку каскадных таблиц стиля (Cascading Style Sheets, CSS). CSS для страницы можно определить либо свойством StylesFile, либо с помощью списка строчных значений Styles. Любой элемент редактора пунктов продюсера может определить особый стиль или использовать какой либо стиль из присоединенной CSS. Выполнение последней операции (что рекомендуется) осуществляется с помощью свойства StyleRule. Сценарии предпочтительней программного кода? Даже этот пример комбинированного использования адаптера, адаптера продюсе- ра страниц с их визуальными конструкторами показывает мощь этой архитекту- ры. Однако этот подход также имеет и свой недостаток: разрешая генерировать сценарий компоненту (в HTML имеется только тег <#SERVЕRSCRIРТ>), вы экономи- те много времени, но в конечном счете приходите к смешению сценария и про- граммного кода, поэтому изменения в пользовательском интерфейсе потребуют обновления программы. Разделение ответственности между разработчиком Delphi- приложений и разработчиком HTML-сценариев утрачено. И, в конечном счете, вам придется запустить сценарий для выполнения чего-либо, с чем Delphi-npo- грамма справится сразу же. По-моему, WebSnap — это мощная архитектура и огромный шаг вперед в ис- пользовании технологии WebBroker, но она должна применяться с большой осто- рожностью, чтобы избежать злоупотребления некоторыми из имеющихся техно- логий лишь потому, что они простые и мощные. Например, для генерации первой версии страницы рационально использовать конструктор AdapterPageProducer, но потом необходимо скопировать сгенерированный сценарий в «открытый» HTML- текст продюсера страниц, для того чтобы веб-дизайнер смог модифицировать сце- нарий специальными средствами. Для нестандартных приложений я предпочитаю использовать возможности, предлагаемые языками XML и XSL, которые доступны в этой архитектуре, даже если они и не играют главной роли. Этот вопрос более подробно рассмотрен в гла- ве 22. 0904
WebSnap и базы данных 905 Размещение файлов После того как вы написали приложение, подобное только что рассмотренному, его можно развернуть в качестве CGI или динамической библиотеки. Вместо по- мещения шаблонов и включаемых файлов в одной папке с исполняемым файлом для них можно создать подкаталог или пользовательскую папку. С этой задачей справляется компонент LocateFileService. Этот компонент с первого взгляда не понятен. Вместо указания целевого ката- лога в качестве свойства система запускает одно из событий этого компонента в тот момент, когда требуется найти этот файл. (Такой подход является более мощным.) Вот эти три события: OnFindlncludeFile, OnFindStream и OnFindTemplateFile. Первое и последнее возвращают имя файла для использования в параметре var. Событие OnFindStream позволяет непосредственно создавать поток, используя поток, уже существующий в памяти или создаваемый на лету, извлекаемый из базы данных, получаемый по HTTP-соединению или полученный любым возможным способом. В простейшем случае использования OnFindStream можно написать следующий программный код: procedure TPageProducerPage2.LocateFI1eServIcelFlndlncludeF11 e ( ASender: TObject: AComponent: TComponent: const AFIleName: String: var AFoundFile: String; var AHandled: Boolean); begin AFoundFIle := DefaultFolder + AFileName; AHandled := True: end: WebSnap и базы данных Среда Delphi всегда блистала в сфере программирования баз данных. Поэтому не удивительно в структуре WebSnap встретить фундаментальную поддержку обра- ботки наборов данных. В частности, с помощью компонента DataSetAdapter можно подключиться к набору данных и вывести его значения в форме или с помощью визуального редактора компонента AdapterPageProducer — в таблице. WebSnap Data Module В качестве примера я создал новое WebSnap-приложение (WSnapTable) с Adapter- PageProducer на основной странице, выводящим таблицу, и другим компонентом AdapterPageProducer на второй странице, выводящим форму с одной записью. В это приложение в качестве контейнера, компонентов работы с наборами данных я так- же добавил WebSnap Data Module (модуль данных WebSnap). Этот модуль дан- ных имеет ClientDataSet, жестко связанный через провайдер с набором данных dbExpress, и основан на использовании InterBase-подключения: object ClientDataSetl: TCIientDataSet Active = True ProviderName = ’ DataSetProviderl' end object SQLConnectionl: TSQLConnectlon 0905
906 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Connected = True ConnectionName» 'IBLocal' LoginPrompt = False end object SQLDataSetl: TSQLDataSet SQLConnection = SQLConnectionl CommandText = select CUSTJO. CUSTOMER. ADDRESSJ.INE1, CITY. STATE_PROVINCE, ' + COUNTRY from CUSTOMER' end object DataSetProviderl TDataSetProvider DataSet = SQLDataSetl end DataSetAdapter А теперь, когда имеется доступный набор данных, на первую страницу можно до- бавить DataSetAdapter и подключить его к ClientDataSet веб-модуля. Адаптер авто- матически сделает доступными все поля набора данных и ряд предопределенных действий обработки этого набора данных (таких как Delete, Edit и Apply). Для ис- ключения некоторых из них и настройки их поведения действия и поля можно явно добавить в коллекции Actions и Fields, но зачастую в этом нет необходимости. WSnapTable table Previous Page 1 2 ? Neat Pag* CUST_NO CUSTOMER ! address_linei \ CITY r STATE-PROVINCE j COUNTRY COMMANDS 1001 Signature Design ; 15500 Pacific Heights [Blvd. ’•San Diego CA [USA i [Edit Delete 1002 Dallas Technologies ;P 0 Box 47000 [Dallas TX |USA fUSA Eritt Delete 1003 Buttle, GnSth and Co i 2300 Newbury Street iBoston 1 MA E-it Delete 1004 Central Bank •66 Lloyd Street [Manchester : England EdrtDKrte 1005 DT Systems LTD [400 Connaugfit Road Central Hong jKong tHong Kong Edit Delete 1006 DataServe International [2000 Caring Avenue : Ottawa ON [Canada Edu Delete a sa a «a Рис. 20.10. Страница, выводимая примером WSnapTable при запуске, содержит начальную часть таблицы Подобно PagedAdapter, компонент DataSetAdapter имеет свойство PageSize, исполь- туемое для указания количества элементов, выводимых на каждой странице. Ком- тонент также имеет команды, используемые для перехода между страницами. Этот тодход особенно удобен в тех случаях, когда вы хотите вывести в визуальную таб- тицу большой набор данных. Вот главные настройки адаптера основной страницы тримера WSnapTable: 0906
WebSnap и базы данных 907 object DataSetAdapterl: TDataSetAdapter DataSet = WebDataModulel.ClientDataSetl PageSize = 6 end Соответствующий продюсер страницы имеет форму, содержащую две группы команд и визуальную таблицу. Первая группа команд (выводимая над сеткой таб- лицы) имеет предопределенные команды обработки страниц: CmdPrevPage (преды- дущая), CmdNextPage (следующая) и CmdGotoPage (переход к странице). Последняя команда генерирует список номеров страниц, что позволяет пользователю перехо- дить на каждую из них непосредственно. Компонент AdapterGrid имеет стандарт- ные столбцы плюс дополнительный столбец, содержащий команды Edit и Delete. Нижняя группа команд обеспечивает кнопку, используемую для создания новой записи (рис. 20.10). Целиком настройки AdapterPageProducer можно найти в лис- тинге 20.2. Листинг 20.2. Настройки AdapterPageProducer для главной страницы WSnapTable object AdapterPageProducer: TAdapterPageProducer object AdapterForml. TAdapterForm object AdapterCommandGroupl: TAdapterCommandGroup Displaycomponent = AdapterGridl object CmdPrevPage. TAdapterActionButton ActionName = 'PrevPage' Caption = 'Previous Page' end object CmdGotoPage: TAdapterActionButton... object CmdNextPage: TAdapterActionButton ActionName = 'NextPage' Caption = 'Next Page end end object AdapterGridl: TAdapterGrid TableAttributes.CellSpacing = 0 TableAttributes Cellpadding = 3 Adapter = DataSetAdapterl AdapterMode = 'Browse' object ColCUST_NO TAdapterOisplayColumn object AdapterCommandColumnl. TAdapterCommandColumn Caption = 'COMMANDS’ object CmdEditRow TAdapterActionButton ActionName = 'EditRow' Caption = 'Edit' PageName = 'formview' DisplayType = ctAnchor end object CmdDeleteRow: TAdapterActionButton ActionName = 'DeleteRow' Caption - 'Delete' DisplayType = ctAnchor end end end object AdapterCommandGroup2. TAdapterCommandGroup Displaycomponent = AdapterGridl _ л пподолжение & 0907
908 Глава 20. Веб-программирование с использованием WebBroker и WebSnap Листинг 20.2 (продолжение) object CmdNewRow TAdapterActionButton ActionName = 'NewRow' Caption = 'New' PageNatne = 'formview' end end end end В этом листинге необходимо обратить внимание на два момента. Во-первых, свойство AdapterMode сетки установлено в Browse (можно также установить значе- ния Edit, Insert и Query). Этот режим вывода набора данных для адаптеров опреде- ляет тип пользовательского интерфейса (строки редактирования и другие элемен- ты управления), а также видимость других кнопок (например, кнопки Apply и Cancel присутствуют только в режиме редактирования). СОВЕТ-------------------------------------------------------------------------------- Режим адаптера также можно изменить с использованием серверного сценария и обращения к Adapter.Mode. Во-вторых, я модифицировал вывод команд в сетке с помощью значения ctAnchor свойства DisplayType. Подобные свойства имеются в большинстве компонентов этой архитектуры, что позволяет подстраивать создаваемый ими HTML-код Редактирование данных в форме Ряд команд подключен к другой странице, которая будет выведена только после того, как будут вызваны эти команды. Например, свойство PageName команды edit установлено в formview. Вторая страница приложения имеет AdapterPageProducer с компонентами, связанными с тем же DataSetAdapter, поэтому все запросы будут синхронизированы. Если щелкнуть на команде Edit, то программа откроет вторую страницу, которая выводит данные одной записи. В листинге 20.3 целиком представлен продюсер второй страницы программы. Визуальное построение HTML-формы с использованием специального конструк- тора Delphi (рис. 20.11) было очень простой операцией. Листинг 20.3. Настройки AdapterPageProducer для страницы formview object AdapterPageProducer TAdapterPageProducer object AdapterForml TAdapterForm object AdapterErrorListl TAdapterErrorList Adapter = table DataSetAdapterl end object AdapterCommandGroupl TAdapterCommandGroup Displaycomponent = AdapterFieldGroupl object CmdApply TAdapterActionButton ActionName = 'Apply' PageNatne = 'table' end object CmdCancel TAdapterActionButton ActionName = 'Cancel' PageName = 'table' 0908
WebSnap и базы данных 909 end object CmdDeleteRow TAdapterActionButton ActionName - 'DeleteRow' Caption = 'Delete' PageName = 'table' end end object AdapterFieldGroupl TAdapterFieldGroup Adapter = table DataSetAdapterl AdapterMode = 'Edit' object FldCUST_NO TAdapterDisplayField DisplayWidth = 10 FieldName = 'CUST_N0' end object FldCUSTOMER TAdapterDisplayField end end end AdapterForml 1 AdaplerCommar ' AdapleiFieWGro CUST_NO (iooi CUSTOMER (Signature Design ADDRESS_LINE1 (l 5500 PacificHeightsBlvd CITY (san Diego STATE_PROXTNCE[ca b COUNTRY |usa Рис. 20.11. Страница formview, выводимая примером WSnapTable в режиме разработки в конструкторе Web Surface Designer (или редакторе AdapterPageProducer) В этом листинге можно отметить, что все операции отправляют пользователя назад к главной странице, а затем AdapterMode устанавливается в Edit, если только не возникнут конфликты или ошибки обновления. В этом случае эта же страница будет выведена повторно с указанием ошибок, которые уточняются добавлением компонента AdapterErrorList в верхней части формы. Вторая страница не публикуется ввиду того, что выбор ее без указания опреде- ленной записи не будет иметь никакого значения. Для того чтобы сделать страни- 0909
910 Глава 20. Веб-программирование с использованием WebBroker и WebSnap цу непубликуемой, используется «комментирование» соответствующего флага в про- граммном коде инициализации. И наконец, для того чтобы зафиксировать измене- ния в базе данных, необходимо вызвать метод ApplyUdpates в событиях OnAfterPost и OnAfterDelete компонента ClientDataSet, размещенного в этом модуле данных. Еще одна проблема (которую я не смог обойти) связана с тем, что SQL-сервер назначает идентификатор каждому клиенту, так что при вводе новой записи дан- ные в ClientDataSet и в базе данных больше не выравниваются. Это может привести к возникновению ошибок Record Not Found (запись не найдена). Отношение Master/Detail в WebSnap Компонент DataSetAdapter имеет специальную поддержку отношения «главный- подчиненный» (Master/Detail) между наборами данных. После того как определе- ны отношения между наборами данных, определите адаптер для каждого набора данных, а затем подключите свойство MasterAdapter адаптера подчиненного набо- ра. Установление отношения master/detail между адаптерами делает их работу сла- женной. Например, при изменении рабочего режима главного набора или вводе новых записей подчиненный набор автоматически перейдет в режим Edit или вы- полнит обновление. Пример WSnapMD использует модуль данных для определения такого отноше- ния. Он содержит два компонента ClientDataSet, каждый из которых через провай- дера подсоединен к SQLDataSet. Компоненты обращения к данным ссылаются на таблицу, а компоненты ClientDataSet определяют отношение. Этот же модуль дан- ных содержит два адаптера, которые обращаются к этим наборам, и повторно определяют отношение master/detail: object dsaDepartment: TDataSetAdapter DataSet = cdsDepartment end object dsaEmployee- TDataSetAdapter , DataSet = cdsEmployee MasterAdapter = dsaDepartment end ВНИМАНИЕ --------------------------------------------------------------------- Сначала для того, чтобы избежать беспорядка в модуле данных, я попытался использовать компо- нент SimpleDataSet, но такой подход не работает. Часть программы, определяющая отношение master/detail, работала корректно, но при переходе с помощью кнопок с одной страницы к следую- щей или предыдущей все заканчивалось неудачей. Причина в том, что в случае использования SimpleDataSet программная ошибка («жучок») закрывает набор данных при каждом взаимодей- ствии, теряя сведения о состоянии. Единственная страница этого WebSnap-приложения имеет компонент Adapter- PageProducer, связанный с обоими адаптерами наборов данных. Форма этой стра- ницы имеет группу полей, связанную с главным набором данных, и визуальную таблицу, связанную с уточняющим набором. В отличие от других примеров, я по- пытался улучшить пользовательский интерфейс добавлением настраиваемых атрибутов различных элементов. Я использовал серый фон, вывел некоторые гра- ницы сетки (HTML-таблицы зачастую используются Web Surface Designer), от- центрировал большинство элементов и добавил интервалы. Обратите внимание, 0910
WebSnap и базы данных 911 что я добавил дополнительные пробелы в заголовки кнопок для того, чтобы они не стали очень маленькими. Программный код будет представлен следующим по- дробным фрагментом, а результат работы — на рис. 20.12: WSnap Master/Detail Рис. 20.12. Пример WSnapMD представляет master/detail-структуру object AdapterPageProducer: TAdapterPageProducer object AdapterForml: TAdapterForm Custom = 'Border~"l" CellSpacing="O" CellPadding="10" ' + 'BgColor=’’Silver" align="center” ’ object AdapterCommandGroupl. TAdapterCommandGroup Displaycomponent = AdapterFieldGroupl Custom = 'Align-’’Center’’’ object CmdFirstRow: TAdapterActionButton. . object CmdPrevRow: TAdapterActionButton .. object CmdNextRow: TAdapterActionButton.. object CmdLastRow TAdapterActionButton... end object AdapterFieldGroupl TAdapterFieldGroup Custom - 'BgColor="Si Iver" ' Adapter = WDataMod.dsaDepartment AdapterMode = 'Browse' end object AdapterGridl: TAdapterGrid TableAttributes BgColor = 'Silver' TableAttributes CellSpacing = 0 TableAttributes CellPadding = 3 HeadingAttnbutes BgColor = 'Gray' Adapter = WDataMod dsaEmployee AdapterMode = 'Browse' object ColEMP__NO TAdapterOisplayColumn... object ColFIRST_NAME TAdapterOisplayColumn... object ColLAST_NAME: TAdapterOisplayColumn... object ColOEPT-NO: TAdapterOisplayColumn... 0911
912 Глава 20. Веб-программирование с использованием WebBroker и WebSnap object ColJOB_CODE TAdapterDisplayColumn object ColJOB_COUNTRY TAdapterDisplayColumn object Col SALARY TAdapterDisplayColumn end end end Сеансы, пользователи и разрешения Еще одной интересной особенностью архитектуры WebSnap является поддержка сеансов и пользователей. Поддержка сеансов осуществляется классическим под- ходом: временными cookies. Cookies посылаются браузеру, поэтому последующие запросы от того же пользователя опознаются системой. Добавление данных в се- анс вместо адаптера приложения дает возможность иметь данные, которые зави- сят от определенного сеанса или пользователя (хотя пользователь может запус- гить множество сеансов, открыв несколько окон браузера на одном компьютере). Для поддержки сеансов приложение оставляет данные в памяти, вот почему эта особенность не доступна в CGI-программах. Использование сеансов Для того чтобы подчеркнуть важность такой поддержки, я построил WebSnap-при- зожение с единственной страницей, представляющей как общее количество обра- цений, так и количество обращений за каждый сеанс. Эта программа содержит компонент SessionService со стандартными значениями свойств MaxSessions и Default- ’imeout. Для каждого нового запроса программа увеличивает на единицу как част- юе поле nHits модуля страницы, так и значение SessionHits текущего сеанса: procedure TSessionDemo WebAppPageModuleBeforeDispatchPage(Sender TObject const PageName String var Handled Boolean) ieg1n // increase application and session hits Inc (nHits) WebContext Session Values {.'SessionHits'] Integer (WebContext Session Values [ ‘SessionHits ]) + 1. nd :obet------------------------------------------------------------------------- )бъект WebContext (типа TWebContext) является переменной-потоком, создаваемой WebSnap для аждого запроса. Он обеспечивает многопоточный доступ к другим глобальным переменным, ис- ользуемым программой. HTML-код выводит сведения о состоянии путем использования настраивае- 1ых тегов, значения в которые подставляются событием OnTag продюсера страниц i сценария, вычисляемого механизмом (engine). Вот ключевая часть HTML-файла: h3>Plain Tags</h3> p>Session id <#SessionID> br>Session hits <#SessionHits></p> h3»Script</h3> o>Session hits (via application) ^Application SessionHits Value%> эг>Арр11cation hits ^-Application Hits Value%></p> 0912
Сеансы, пользователи и разрешения 913 Параметры вывода предоставляются обработчиком события OnTag и события- ми OnGetValue поля- procedure TSessionDemo PageProducerHTMLTag(Sender TObject Tag TTag const TagString String TagParams TStrings var ReplaceText String), begin if TagString = 'SessionlD then ReplaceText = WebContext Session SessionlD else if TagString = 'SessionHits' then ReplaceText = WebContext Session Values ['SessionHits ] end procedure TSessionDemo HitsGetValue(Sender TObject var Value Variant), begin Value = nHits, end procedure TSessionDemo SessionHitsGetValuetSender TObject var Value Variant). begin Value - Integer (WebContext Session Values [ SessionHits"!). end Эффект работы этой программы можно видеть на рис. 20.13. Я активизировал два сеанса в двух различных браузерах. ПРИМЕЧАНИЕ ---------------------------------------------------------------------------- В этом примере я использовал как традиционную подстановку тегов WebBroker, так и новый адап- тер полей WebSnap совместно со сценарием, предоставив вам возможность сравнить два подхода. Не забывайте, что оба подхода доступны только в WebSnap-приложении. . Е«« С* НИ* Й> 1М»' №> SessionDemo SessionDemo Plain Tags Session id RztkbSFkgRYq0nU4 Session hits 6 Script Session hits (via application) 6 Application hits 10 Phi» Tags Session id FB8hoPLrL2Ty8Um Session hits 4 Script Session hits (via application) 4 Application hits 9 Рис. 20.13. Два экземпляра браузера оперируют двумя различными сеансами в одном и том же WebSnap-приложении Запрос входа в систему Помимо общих сеансов WebSnap имеет специальную поддержку для пользовате- лей и аотгтпчнппваииыу срянгон основанных на ВХОДе В CHCTeMV floeinT В ППИЛО- 0913
914 Глава 20. Веб-программирование с использованием WebBroker и WebSnap жение можно добавить список пользователей (с помощью компонента WebllserList) с регистрационным именем и паролем. Этот компонент является элементарным по отношению к данным, которые он хранит. Однако вместо заполнения списка пользователей этот список можно поместить в таблицу базы данных (или в любой иной собственный формат) и для извлечения сведений о пользователях и провер- ки их паролей использовать события компонента WebllserList. В общем случае в приложение также добавляются компоненты SessionService и EncHJserSessionAdapter. При этом можно попросить пользователя войти в систему, указав на каждой странице, может ли ее просмотреть любой или только пользова- тель, подтвердивший свои полномочия. Это достигается установкой флага wpLogin- Required в конструкторе классов TWebPageModuleFactory и TwebAppPageModuleFactory в программном коде инициализации веб-модуля страницы. СОВЕТ------------------------------------------------------------------------ Права и сведения о публикации включены в factory, а не в WebPageModule, поскольку программа может проверить права доступа и список страниц даже без загрузки модуля. Когда пользователь пытается просмотреть страницу, которая требует пароля, выводится страница входа в систему, указанная в компоненте EndUserSessionAdapter. Такую страницу можно построить, создав новый веб-модуль страницы на основе AdapterPageProducer с добавлением LoginFormAdapter. В редакторе страницы добавь- те группу полей с формой, подключенной к группе полей LoginFormAdapter, а также добавьте группу команд с кнопкой Login по умолчанию. Конечная форма входа в систему будет иметь поля для ввода имени пользователя и его пароля, а также поля запрашиваемой страницы. Последнее значение автоматически заполняется запрашиваемой страницей, если эта таблица требует аутентификации и пользова- тель ранее не посещал ее. Таким образом, пользователь может немедленно обра- титься к требуемой странице, не возвращаясь к основному меню. Форма входа обычно не публикуется, поскольку соответствующая команда Login доступна, если пользователь не входил в систему; если пользователь в систему во- шел, то она будет заменена командой Logout. Доступ к этой команде достигается посредством стандартного сценария модуля веб-страницы, например, так: < % if (EndUser Logout null) { %> < % if (EndUser DisplayName !="){%> <hl>Welcome <X=EndUser DisplayName < % } %> < % if (EndUser Logout Enabled) { %> <a href="<l=EndUser Logout AsHREFX>”>Logout</a> <% } %> <% if (EndUser LoginForm Enabled) { %> <a href=<l=EndUser LoginForm AsHREFX»Login</a> <% } %> <X } %> Больше нечего сказать о приложении WSsnapUsers, поскольку оно практически не имеет настраиваемого кода. Данный сценарий на основе стандартного шаблона демонстрирует доступ к сведениям о пользователе. Права доступа к отдельной странице Помимо запроса пароля страницей можно предоставить право определенным поль- зователям просматривать больше страниц, чем это смогут другие. Любой пользо- 0914
Что далее? 915 ватель имеет набор прав, разделенных точками с запятой или запятыми. Пользова- тель должен обладать всеми правами, определенными для запрашиваемой страницы. Эти права (обычно перечисленные в свойствах ViewAccess и ModifyAccess адаптера) указывают, может ли пользователь соответственно просматривать или редактиро- вать данные элементы. Эти настройки являются точечными и могут быть приме- нены как ко всему адаптеру целиком, так и только к определенным полям (обрати- те внимание, я говорю о полях адаптера, а не о компонентах пользовательского интерфейса в конструкторе). Например, для некоторых пользователей можно за- крыть просмотр ряда столбцов таблицы, спрятав соответствующие поля (а также и другие элементы, определенные в свойстве HideOptions). Глобальный компонент PageDispatcher также имеет события OnCanViewPage и OnPageAccessDenied, которые можно использовать для управления доступом к раз- личным страницам программы с помощью программного кода, обеспечивая пол- ный контроль. Что далее? В этой главе мы рассмотрели веб-приложения, использование множества сервер- ных технологий и две различные структуры библиотеки классов Delphi: WebBroker и WebSnap. Это было не очень подробное представление, поскольку по одной этой теме можно написать целую книгу. Данная глава является отправной точкой, и (как обычно) я старался сконцентрироваться на прояснении основных концепции, а не на построении сложных примеров. Если вы захотите поглубже изучить структуру WebSnap и увидеть примеры в действии, загляните в каталог расширенных примеров Delphi \Demos\WebSnap. Некоторые особенности, связанные с XML, XSL и клиентскими сценариями, бу- дут рассмотрены в главе 22. Глава 21 посвящена еще одной альтернативной структуре разработки веб-сер- верных приложений в Delphi: IntraWeb. Это средство стороннего производителя существует уже несколько лет, но сейчас оно стало более значимым, поскольку компания Borland включила его в состав Delphi 7. Как вы увидите, IntraWeb мо- жет быть интегрирована в WebBroker или WebSnap либо использоваться как от- дельная архитектура. 0915
04 Веб-программирование I с использованием IntraWeb Начиная с появления Delphi 2 Чед Зет Хауэр (Chad Z. Hower) занимался создани- ем Delphi-архитектуры, упрощающей разработку веб-приложений, основной идеей которой было сделать веб-программирование таким же простым, как и стандарт- ное визуальное Delphi-пограммирование. Одни программисты и так хорошо зна- комы с HTML, JavaScript, Cascading Style Sheets и новейшими интернет-техноло- гиями. Другие просто хотят иметь возможность построить веб-приложение в Delphi так же, как VCL- или CLX-приложения. Архитектура IntraWeb предназначенадля второй категории разработчиков, хотя она настолько мощна, что ею выгодно пользоваться даже опытным веб-програм- мистам. По словам Чеда, IntraWeb предназначенадля постройки веб-приложений, а не веб-сайтов. Кроме того, компоненты IntraWeb могут использоваться в специ- альных приложениях или в программах, основанных на архитектурах WebBroker и WebSnap. В этой главе я не смогу охватить все тонкости IntraWeb: в палитру компонен- тов устанавливается 50 компонентов и несколько конструкторов модулей — это очень большая библиотека. Я планирую рассмотреть основы, что позволит вам сделать выбор, как эту технологию лучше использовать в предстоящих проектах или в их частях. ПРИМЕЧАНИЕ -------------------------------------------------------------- Документацию на IntraWeb в формате PDF можно найти на компакт-диске Delphi 7. Если его не удалось там найти, руководство по эксплуатации можно найти и загрузить с веб-сайта компании Atozed Software. Поддержка IntraWeb имеется и в группах новостей компании Borland. СОВЕТ---------------------------------------------------------------------------- Эта глава имела специальную рецензию Чеда Зет Хауэра, также известного под псевдонимом «Kudzu». Он является первым автором и координатором проекта Internet Direct (Indy; см. главу 19), а также автором IntraWeb. В сферу интересов Чеда также попадают программирование и обеспечение свя- зи с использованием TCP/IP, межпроцессное взаимодействие, распределенные вычисления, интер- нет-протоколы и объектно-ориентированное программирование. В свободное время он увлекается пешими прогулками, ездой на велосипеде, плаванием на байдарке, горнолыжным спортом, а также почти всеми видами отдыха на открытом воздухе. Чед также помещает на сайте Kudzu World (http: //www.Hower.org/Kudzu/) бесплатные статьи, программы и утилиты и прочие диковинки. Чед эмиг- рировал из Америки и в настоящее время летом живет в Санкт-Петербурге (Россия), а зимой пере- бирается в Лимасол (Кипр). Его можно найти по адресу cpub@Hower.org. 0916
Введение в IntraWeb 917 В этой главе рассматриваются следующие вопросы: О IntraWeb, веб-приложения и веб-сайты; О использование компонентов IntraWeb; О интеграция с WebBroker и WebSnap; О веб-приложения баз данных; О использование компонентов для клиентской стороны. Введение в IntraWeb IntraWeb — это библиотека компонентов, производимых в настоящее время ком- панией Atozed Software (www.atozedsoftware.com). В Professional- и Enterprise-вы- пусках Delphi 7 можно найти соответствующую версию IntraWeb. Как вы увидите позже, в версии Professional IntraWeb может использоваться только в режиме стра- ниц (Page mode). Хотя Delphi 7 является первой версией IDE компании Borland, включающей набор этих компонентов, IntraWeb существует уже несколько лет. Теперь она по достоинству оценена и получила поддержку, о чем говорит наличие компонентов сторонних производителей. ПРИМЕЧАНИЕ------------------------------------------------------------ К компонентам сторонних производителей в IntraWeb относятся IWChart от Steema (создатель TeeChart), IWBold от Centillex (для интеграции с Bold), IWOpenSource, IWTranslator, IWDialogs, IWDataModulePool от Arcana, IW Component Pack от TMS и IWGranPrimo от GranPrimo. Обновленный список сотрудни- чающих компаний можно найти на www.atozedsoftware.com. Хотя исходный программный код основной библиотеки не предоставляется (его можно получить за плату), архитектура IntraWeb очень открыта; доступен пол- ный исходный программный код компонентов. Сейчас IntraWeb является частью стандартной поставки Delphi и доступна в Kylix. Аккуратно разработанные Intra- Web-приложения могут работать на любой платформе. СОВЕТ------------------------------------------------------------------------- Помимо Delphi- и Linux-версий существуют C++ Builder- и Java-версии IntraWeb. Ведется работа над .NET-версией и она, скорее всего, появится в следующей .NET-версии Delphi. Как владелец Delphi 7 вы имеете возможность получить первый значимый вы- пуск обновления (версия 5.1) и сможете обновить свою лицензию до полного вы- пуска IntraWeb Enterprise, включая обновления и поддержку от компании Atozed Software (см. ее сайт). В этом обновлении (5.1) будет более серьезная документа- ция (справочные и PDF-файлы). От веб-сайтов к веб-приложениям Как упоминалось ранее, основная идея IntraWeb заключается в построении веб- приложений вместо создания веб-сайтов. При работе с WebBroker или WebSnap (см. главу 20) вы мыслите понятиями веб-страниц и продюсеров страниц, работа 0917
918 Глава 21. Веб-программирование с использованием IntraWeb происходит на уровне генерации HTML. При работе с IntraWeb вы мыслите поня- тиями компонентов, их свойств и их обработчиков событий так же, как и при визу- альной разработке. В качестве примера создайте новое IntraWeb-приложение, выбрав File ► New ► Other (Файл ► Новый ► Другие). На странице IntraWeb диалогового окна New Items выберите Stand Alone Application (Автономное приложение). В следующем диало- говом окне (которое является частью Delphi, а не мастером IntraWeb) можно вы- брать существующую папку или создать новую (я говорю так подробно, поскольку диалоговое окно не предоставляет эту информацию). Окончательная программа имеет файл проекта и два разных модуля (их структуру рассмотрим позднее). А теперь давайте создадим пример (IWSimpleApp). Для этого выполните следу- ющие действия. 1. Откройте главную форму программы и со страницы IW Standard палитры ком- понентов добавьте на нее кнопку, строку редактирования и список. Не надо добавлять VCL-компоненты со страницы Standard — лишь IntraWeb-компонен- ты IWButton, IWEdit и IWListbox! 2. Немного измените их свойства: object IWButtonl- TIWButton Caption = 'Add Item' end object IWEditl TIWEdit Text = 'four' end object IWListboxl. TIWListbox Items.Strings = ( 'one' 'two' ' three') end 3. Дважды щелкнув на кнопке, создайте обработчик события OnClick, написав зна- комый код: procedure TformMain.IWButtonlCTтck(Sender TObject). begin IWListBoxl Items Add (IWEditl Text), end. Вот и все, что необходимо сделать для создания веб-приложения, способного добавлять текст в список (рис. 21.1 — здесь представлена окончательная версия программы с дополнительными кнопками). Важно отметить, что после запуска этой программы при каждом щелчке на кнопке браузер посылает приложению новый запрос, который запускает обработчик события и производит новую HTML-стра- ницу, основанную на новом состоянии компонентов формы. Результат работы программы можно просматривать в браузере, но предпочти- тельно это делать в контроллере форм IntraWeb (рис. 21.2). Автономное IntraWeb- приложение представляет собой полномасштабный HTTP (в чем вы убедитесь после прочтения следующего раздела). Выводимая форма управляется вызовом IWRun из файла проекта, создаваемого по умолчанию для каждого автономного IntraWeb-приложения. Форма отладки позволяет выбрать браузер и запустить в нем приложение либо скопировать URL приложения в буфер обмена и вставить 0918
Введение в IntraWeb 919 его в браузер. Необходимо помнить, что приложение по умолчанию использует случайный номер порта, изменяющийся при каждом запуске приложения, поэто- му вам придется каждый раз использовать различный URL. Это поведение можно изменить, выбрав конструктор контроллера сервера (подобного модулю данных) и установив его свойство port. В данном примере я использовал порт 8080 (один из известных HTTP-портов), хотя можно выбрать любой. Add item j — No Selection - one two three four four 4Й1 ЙМЗ <8 ~ ....; ;..J........... Рис. 21.1. Программа IWSImpleApp в браузере £•••• Му Intraweb Application Server flte Run Help Active Sessions: 1 Intraweb Version 5 0 43 HTTP Port 8080 Packaged Enterprise License Number 0 Рис. 21.2. Форма контроллера автономного IntraWeb-приложения IntraWeb-код в основном является серверным, но для управления некоторыми функциями приложения IntraWeb генерирует и JavaScript. Имеется возможность выполнить часть кода на клиентской стороне. Это осуществляется за счет исполь- 0919
920 Глава 21. Веб-программирование с использованием IntraWeb зования специальных клиентских компонентов или с помощью написания соб- ственного JavaScript-кода. Для сравнения: две кнопки, находящиеся в нижней ча- сти формы примера IWSimpleApp, выводят окно сообщения двумя разными спосо- бами. Первая из этих кнопок (IWButton2) выводит сообщение с помощью Delphi, ис- пользуя серверное событие: procedure TfоrmMain IWButton2Click(Sender TObject), var nltem Integer begin nltem = IWListboxl Itemindex if nltem >= 0 then WebApplication ShowMessage (IWListBoxl Items [nltem]) else WebApplication ShowMessage ('No item selected") end Вторая кнопка (IWButton3) использует}ауа5спр1, который вставляется в Delphi- программу за счет установки соответствующего обработчика события JavaScript в специальном редакторе свойств для свойства ScriptEvents: |onCI»ck window alertfASender valued 7 IntraWeb Event Script* £& Заглянем за кулисы Как можно заметить, создание IntraWeb-приложения такое же простое, как и со- здание обычного Delphi-приожения, основанного на формах: компоненты поме- щаются на форму и им назначаются обработчики событий. Конечно же, эффект у программ совершенно разный, поскольку веб-приложение запускается в браузере. Для того чтобы понять, как это происходит, давайте рассмотрим порядок выпол- нения этой простой программы. Это поможет разобраться в установке свойств ком- понентов и в общей работе с ними. Поскольку эта программа основана на браузере, нет лучшего способа понять ее работу, чем просмотреть HTML-код, посылаемый браузеру. Просмотрите страни- цу, созданную примером IWSimpleApp в виде HTML (здесь она не представлена, поскольку займет слишком много места), и обратите внимание, что HTML-код по- делен на три основных раздела Первый — это список стилей (основан на НТТР- теге style) с подобными строками: IWEDIT1CSS {position absolute left 40.top 40 z-index 100 font-style normal font-size lOpt.text-decoration none } IntraWeb использует стили не только для определения визуального представ- ления каждого компонента (шрифт и цвет), но и для указания положения компо- нента, используя по умолчанию абсолютное позипиониоование. Каждый стиль 0920
Введение в IntraWeb 921 определяется рядом свойств компонента, поэтому, имея опыт работы с таблицами стилей, вы можете поэкспериментировать. Если вы не знакомы с таблицами сти- лей, просто положитесь на эти свойства, a IntraWeb оптимально разместит компо- ненты на веб-странице. Второй раздел состоит из сценария JavaScript. Основной блок сценария содер- жит код инициализации и код клиентского обработчика событий компонентов, например: function IWBUTTONl_OnClick(ASender) { return SubmitClickConfirm('IUBUTT0N1', true, '') } Этот обработчик вызывает соответствующий серверный код. Если вы предос- тавили JavaScript-сценарий прямо в IntraWeb-приложении, как мы говорили ра- нее, то вы увидите: function IWBUTT0N3_onClick(ASender) { window alert(ASender value) } Раздел сценария страницы также обращается и к другим файлам, необходи- мым браузеру и предоставляемым IntraWeb. Некоторые из этих файлов являются общими, а другие «привязаны» к определенному браузеру: IntraWeb определяет используемый браузер и возвращает различный JavaScript-код и базовые JavaScript- файлы. СОВЕТ------------------------------------------------------------------------------- Поскольку JavaScript не одинаков для всех браузеров, IntraWeb поддерживает только некоторые из них, включая все последние версии Microsoft Internet Explorer, Netscape Navigator и Mozilla с откры- тым исходным кодом (который я использовал при написании данной главы). Браузер Opera имеет очень ограниченную поддержку JavaScript, поэтому если IntraWeb опознает именно его, то по умол- чанию будет выдано сообщение об ошибке (определяется свойством SupportedBrowsers контролле- ра). Opera может использоваться с компонентами Arcana и официально будет поддерживаться Intra- Web 5.1. Не забывайте, что браузер может искажать свою идентификационную информацию, на- пример, Opera зачастую «представляется» как Internet Explorer, предотвращая корректное опознавание, которое используется для ограничения использования веб-сайтов другими браузера- ми, но это может привести к ошибкам времени выполнения или несовместимости. Третья часть сгенерированного HTML — это определение структуры страни- цы. Внутри тега body имеется тег form (в той же строке) со следующим действием: «form onsubmit=''return FormDefaultSubmit() " name="SubmitForm" action="/EXEC/3/DC323E01B09C83224E57E240' method="POST"> Ter form содержит специальные компоненты пользовательского интерфейса, такие как кнопки и строки редактирования: «input type="TEXT” name="IWEDITl" size="17" value="four" id="IWEDIT1" class="IWEDIT1CSS"> «input value="Add Item" name="IWBUTTONl" type="button” onclick=”return IWBUTTONl_OnClick(this) " id-"IWBUTTONl" class=' IWBUTTON1CSS"> Форма также имеет несколько скрытых компонентов, используемых IntraWeb для передачи информации в обоих направлениях. Однако в IntraWeb самый ос- новной способ передачи информации — это URL. В данной программе он выгля- дит так: 0921
922 Глава 21. Веб-программирование с использованием IntraWeb http://127.О.О.1:8080/EXEC/2/DC323E01В09С83224Е57Е240 Первая часть — это IP-адрес и порт, используемые автономным IntraWeb-при- ложением (он изменяется при использовании другой аппаратной архитектуры при размещении программы), за которым следует команда ЕХЕС, последовательно уве- личивающийся номер запроса и идентификатор сеанса. К сеансам мы вернемся позже, а здесь достаточно отметить, что вместо cookies IntraWeb использует URL для того, чтобы сделать приложение независимым от настроек браузера. Если хо- тите, вместо URL можно использовать cookies, изменив значение свойства TrackMode контроллера сервера. ВНИМАНИЕ---------------------------------------------------------------------------- Версия IntraWeb, поставляемая совместно с Delphi 7, имеет ошибку, связанную с cookies и настрой- ками некоторых часовых поясов. Она уже исправлена, и программа-«заплатка» доступна на веб- сайте компании Atozed software. Архитектуры IntraWeb Перед представлением дополнительных примеров, демонстрирующих использо- вание прочих компонентов IntraWeb, имеющихся в Delphi 7, давайте рассмотрим еще один ключевой элемент IntraWeb — различные архитектуры, которые могут использоваться для создания и размещения приложений, основанных на этой биб- лиотеке. IntraWeb-проекты можно создавать в режиме Application (который учи- тывает в учет все возможности IntraWeb) или в режиме Раде (упрощенная версия, которую можно внедрить в существующие WebBroker- и WebSnap-приложения Delphi). Приложения режима Application (приложение) могут быть развернуты как ISAPI-библиотеки, Apache-модули или с помощью IntraWeb-режима Standalone (видоизмененная архитектура режима Application). Программа режима Раде (стра- ница) может быть развернута, как любое WebBroker-приложение (ISAPI, Apache- модуль, CGI и т. д.). IntraWeb предоставляет три различные, но частично пере- крывающиеся архитектуры. Режим Standalone Обеспечивает пользовательский веб-сервер, как в первом построенном нами при- мере. Это удобно для отладки приложения (поскольку вы сможете запустить его непосредственно из IDE Delphi и установить точки останова в любом месте исход- ного программного кода). Режим Standalone также можно использовать для разме- щения приложений в корпоративных сетях, предоставляя пользователям воз- можность работать в автономном режиме на собственном компьютере через веб-интерфейс. При запуске автономной IntraWeb-программы с флагом -install она будет запущена как служба, и диалоговое окно не появится. Режим Standalone обес- печивает вариант размещения программ, разработанных в режиме Application, ис- пользуя в качестве веб-сервера саму IntraWeb. Режим Application Позволяет размещать IntraWeb-приложение на коммерческом сервере, создавая Apache-модуль или IIS-библиотеку. Режим Application позволяет управлять сеан- сами и IntraWeb-возможностями, поэтому является предпочтительным способом для размещения масштабируемого приложения, используемого по Сети. Если быть 0922
Построение IntraWeb-приложений 923 точным, IntraWeb-программы режима Application могут развертываться как авто- номные приложения, ISAPI-библиотеки и Apache-модули. Режим Раде Открывает возможность интеграции IntraWeb-страниц в WebBroker- и WebSnap- приложения. Можно добавить новые возможности в существующие программы или положиться на другие технологии в разделах динамического сайта, основан- ного на HTML-страницах, предоставляя интерактивные части с помощью IntraWeb. Режим Раде является единственной возможностью использования IntraWeb в CGI- приложениях, но ему недостает возможностей управления сеансами. Автономные IntraWeb-серверы не поддерживают режим Раде. В последующих примерах этой главы для простоты и облегчения отладки ис- пользовался режим Standalone, но мы также рассмотрим и режим Раде. Построение IntraWeb-приложений Для создания IntraWeb-приложения имеется множество компонентов. Например, если взглянуть на страницу IW Standard палитры компонентов Delphi, то можно найти впечатляющий список основных компонентов, от знакомых кнопок, списка, строки редактирования переключателей и т. п. до интересных компонентов tree view, menu, timer, grid и link. Я не буду перечислять все компоненты и описывать их использование на примерах — лучше мы рассмотрим несколько компонентов для того, чтобы понять архитектуру, не вникая в специфичные детали. Я разработал пример (IWTree), использующий IntraWeb-компоненты menu и tree view, который умеет создавать компонент в ходе выполнения. Этот удобный ком- понент становится доступным в динамическом меню — являющимся, по сути, стан- дартным меню Delphi — за счет ссылки его свойства AttachedMenu на компонент TMenu: object MainMenul: ТМаinMenu object Treel: TMenuItem object ExpandAlll: TMenuItem object CollapseAlll: TMenuItem object Nl: TMenuItem object EnlargeFontl: TMenuItem object ReduceFontl: TMenuItem end object Aboutl: TMenuItem object Application!- TMenuItem object TreeContents1: TMenuItem end end object IWMenul: TIWMenu AttachedMenu = MainMenul Orientation = iwOHorizontal end Если пункты меню обрабатывают событие OnClick в программном коде, в ходе выполнения они становятся ссылками. Пример меню в браузере можно посмот- 0923
924 Глава 21. Веб-программирование с использованием IntraWeb реть на рис. 21.3. Второй компонент примера — это tree view с набором предопреде- ленных узлов. Этот компонент имеет большой объем JavaScript-кода, позволяю- щего раскрывать и сворачивать узлы непосредственно в браузере (не обращаясь к серверу). В то же время, пункты меню позволяют программе оперировать меню, раскрывая и закрывая узлы и изменяя шрифт. Вот программный код пары обра- ботчиков событий: -iqlxl А Fife Edit yew Go Bookmarks Took Window htefe 0 0 1 B08C j ^Search j About > ....... Tree - towTreeViewliteml R towTreeViewlItem2 QI WTr e e Vie wl Item3 QlWTreeViewlItemS SlWTreeViewlItem? В1 WTre eVie wl I teml 0 - CSlWTreeViewlItem4 H CSlWTreeViewlItemi EllWTreeViewlItemfi ВI WTre eVle wl Item? "! CUllWTreeViewlItemll BlWTreeViewlIteml3 ВIWTre eVlewl Iteml 2 lUTceeViewlItemO (3) IWTreeViewlItem4 (4) Я .WiS Рис. 21.3. Пример IWTree представляет меню, дерево просмотра и динамическое создание компонента menu procedure TformTree ExpandAlllClick(Sender TObject). var i Integer begin for i = 0 to IWTreeViewl Items Count - 1 do IWTreeViewl Items Ст J Expanded = True end. procedure TformTree EnlargeFontlClick(Sender TObject) 0924
Построение IntraWeb-приложений 925 begin IWTreeViewl Font Size = IWTreeViewl Font Size + 2. end Благодаря сходству IntraWeb-компонентов c VCL-компонентами Delphi про- граммный код прост для понимания. Меню имеет два подменю, которые несколько сложней. Первое выводит иден- тификатор приложения, являющийся идентификатором выполнения/сеанса при- ложения. Этот идентификатор берется из свойства AppID глобального объекта WebApplication. Второе подменю (Tree Contents) показывает первые узлы дерева глав- ного уровня совместно с числом непосредственных подузлов. Однако что интерес- но — эти сведения выводятся в компоненте Мето, созданном в ходе выполнения (см. рис. 21.3), точно так же, как это делается в VCL-приложении: procedure TformTree TreeContents1С1ick(Sender TObject) var i Integer begin with TIWMemo Create(Self) do begin Parent = Self Align = al Bottom for i =0 to IWTreeViewl Items Count - 1 do Lines Add (IWTreeViewl Items [i] Caption + ' (' + IntToStr (IWTreeViewl Items [i] SubItems Count) + ')'). end end ПРИМЕЧАНИЕ -------------------------------------------------------------------------- Обратите внимание, что выравнивание в IntraWeb работает так же, как в VCL. Например, меню этой программы имеет выравнивание alTop, tree view имеет выравнивание alClient, а динамическое поле Мето создается с выравниванием alBottom. В качестве альтернативы можно воспользоваться сред- ством привязки (anchor) (так же как в VCL): имеется возможность поместить кнопки или компонен- ты в середине страницы с полным набором из четырех анкеров. Использование этой технологии можно встретить в последующих примерах. Написание многостраничных приложений Все программы, которые мы строили до сих пор, были одностраничными. Как мож- но заметить, даже в данном случае процесс IntraWeb-разработки похож на про- цесс стандартной Delphi (или Kylix) разработки и отличается от большинства других библиотек интернет-разработки. В этом примере мы глубоко изучим про- граммный код, автоматически генерируемый мастером IntraWeb-приложений. Давайте приступим к рассмотрению с самого начала. Главная форма примера IWTwoForms снабжена IntraWeb-компонентом grid. Этот мощный компонент позво- ляет поместить в HTML сетку таблицы как с текстом, так и с другими компонента- ми. В данном случае содержание сетки определяется при запуске (в обработчике события OnCreate главной формы): procedure TFormMain IWAppFormCreate(Sender TObject) var i Integer link TIWURL, begin 0925
926 Глава 21. Веб-программирование с использованием IntraWeb // установить заголовки сетки IWGridl.Cell[0. OJ.Text := 'Row'; IWGridl-CellСО. l].Text 'Owner'; IWGridl CellCO. 2].Text := 'Web Site'; // установить содержание for i := 1 to IWGridl.RowCount - 1 do begin IWGridl.Cell [1,0].Text := 'Row' + IntToStr (i+1): IWGridl.Cell [1,1].Text := 'IWTwoForms by Marco Cantu'; link := TIWURL.Create(Self); link.Text := 'Click here'; link.URL := 'http://www.marcocantu.com'; IWGridl.Cell [1.2].Control link; end; end: Эффект выполнения этого кода представлен на рис. 21.4. Помимо вывода здесь имеется еще ряд интересных моментов. Во-первых, для генерации кода, который будет центрировать компонент grid посередине страницы, даже если пользователь изменит размер окна браузера, используются анкеры Delphi (все установлены в False). Во-вторых, в третий столбец я добавил компонент IWURL, но в сетку табли- цы можно добавить любой другой компонент (включая кнопки и строки редакти- рования). Рис. 21.4. Пример IWTwoForms использует компонент IWGrid, внедренный текст и компоненты IWURL Третий и наиболее важный момент заключается в том, что IWGrid преобразует- ся в HTML-сетку, с рамкой или без нее. Вот фрагмент HTML-кода, сгенерирован- ного для одной из строк сетки: <tr> <td valign“"middle’’ align="left" NOWRAP> <font style="font-size:lOpt:”>Row 2</font> 0926
Построение IntraWeb-приложений 927 </td> <td valign="middle" align-'left" NOWRAP> <font style="font-size lOpt,">IWTwoForms by Marco Cant </font> </td> <td valign=''middle" align="left" NOWRAP> <font style="font-size lOpt;"></font> <a href="#" oncl1ck="parent.LoadURL('http.//www.marcocantu com')" 1d="TIWURLl" name="TIWURLl" style="z-index-100:font-style:normal.font-size.lOpt:text-decoration:none;"> Click here</a> </td> </tr> ПРИМЕЧАНИЕ------------------------------------------------------------------------------— В этом листинге обратите внимание на то, что связанный URL активизируется посредством JavaScript, а не непосредственной ссылкой. Так делается ввиду того, что все действия в IntraWeb предусматри- вают дополнительные клиентские операции, такие как проверка достоверности, орфографическая проверка, отправка. Например, если установить свойство Required компонента и если поле будет пустым, то данные не будут отправляться и вы увидите сообщение о JavaScript-ошибке (настраива- емое установкой описания ошибки в свойстве FriendlyName). Ключевая функция программы — в возможности выводить вторую страницу. Для этого сначала необходимо с помощью значка Application Form (форма прило- жения) на странице IntraWeb диалогового окна New Items (новый элемент) Delphi (File ► New ► Other) добавить в приложение новую IntraWeb-страницу. Поместите на эту страницу обычным образом какие-нибудь компоненты, а затем добавьте в главное окно кнопку или другой элемент управления, который будет использо- ваться для открытия второй страницы (со ссылкой anotherform, хранимой в поле основной формы): procedure TformMain btnShowGraphicClick(Sender: TObject): begin anotherform = TAnotherForm Create(WebApplication). anotherform Show: end. Несмотря на то что программа использует метод Show, он будет воспринимать- ся как ShowModal, поскольку IntraWeb рассматривает видимые страницы как стоп- ку бумаг. Последняя выведенная страница находится наверху стопки и выводится в браузере. Закрытием (сокрытием или уничтожением) этой страницы вы откры- ваете предыдущую страницу. В нашей программе вторая страница закрывает себя самостоятельно вызовом метода Release, который, как в VCL, является корректным способом распределения исполняемой в текущий момент формы. Можно просто спрятать вторую страницу и затем вывести ее повторно, не осуществляя каждый раз повторного создания (особенно если это влечет потерю результатов пользова- тельского редактирования). ВНИМАНИЕ ------------------------------------------------------------------------------- В данной программе в главную форму я добавил кнопку Close. Она не будет вызывать метод Release, но вместо него вызовет метод Terminate объекта WebApplication, передав в него сообщение, напри- мер: WebApplication.Terminate('Goodbyel'). В демонстрационной программе используется другой вызов: TerminateAndRedirect. 0927
928 Глава 21. Веб-программирование с использованием IntraWeb Теперь, после того как вы увидели создание IntraWeb-приложения с двумя формами, мы перейдем к краткому изучению того, как IntraWeb создает главную форму. Вот наиболее важный программный код файла проекта, сгенерированный мастером IntraWeb при создании новой программы: begin IWRun(TFormMain. TlWServerController); Он отличается от стандартного файла проекта Delphi, поскольку вместо при- менения метода к глобальному объекту, представляющему приложение, здесь вы- зывается глобальная функция, хотя эффект почти такой же. Параметрами явля- ются класс главной формы и класс IntraWeb-контроллера, который обрабатывает сеансы и другие возможности. Вторая форма примера IWTwoForms представляет другую интересную особен- ность IntraWeb: расширенную графическую поддержку. Форма содержит графи- ческий компонент с классическим изображением богини Афины. Это достигается загрузкой битового изображения в компонент IWImage: IntraWeb преобразует би- товое изображение в формат JPEG, сохраняет его в кэш-папке, созданной в папке приложения, и возвращает ссылку на него: <img src="/cache/JPGl tmp" name=''IWIMAGEl" border="0" width="153" height="139"> Дополнительная особенность, предоставляемая IntraWeb и эксплуатируемая программой, заключается в том, что для изменения изображения пользователь может щелкнуть на изображении мышью и запустить серверный код. В данной программе эффект заключается в выводе маленьких зеленых колец: Mozilla х| Be Edit Sew fio gookmarla look ’ Close j И Я .□ Этот эффект достигается следующим кодом: procedure Tanotherform IWImagelMouseDown(ASender TObject; const AX. AY: Integer): var aCanvas TCanvas; begin aCanvas .= IWImagel Picture Bitmap.Canvas. aCanvas Pen.Width .= 8. 0928
Построение IntraWeb-приложений 929 aCanvas.Pen.Color : = cl Green; aCanvas.Ellipse(Ax - 10, Ay - 10. Ax + 10. Ay + 10). end, ВНИМАНИЕ --------------------------------------------------------------------------- Операция рисования выполняется на битовом «полотне». Не пытайтесь использовать полотно (canvas) компонента Image (как в компоненте TImage VCL) или в качестве оригинала использовать изобра- жения JPEG-формата, поскольку вы не получите никакого эффекта либо получите сообщение об ошибке. Управление сеансами Если вы занимались веб-программированием, то представляете, что управление сеансами является довольно сложным вопросом. IntraWeb предоставляет предо- пределенное управление сеансами и упрощает работу с ними. Если для специаль- ной формы потребуются сведения о сеансе, все, что нужно сделать, — это добавить в форму поле. IntraWeb-форма и ее компоненты имеют отдельный экземпляр для каждого пользовательского сеанса. Так, в примере IWSession в форму добавлено поле с именем FormCount. Для сравнения я также объявил глобальную переменную модуля с именем Glo baLCount, совместно используемую всеми экземплярами (сеан- сами) приложения. Для повышения полномочий над данными сеанса и для обеспечения совмест- ного доступа к ним можно настроить класс TUserSession, помещаемый мастером IntraWeb в модуль ServerController. В примере IWSession этот класс определен сле- дующим образом: type TUserSession = class publ1 с UserCount: Integer; end; IntraWeb создает экземпляр этого объекта для каждого нового сеанса. Это можно увидеть при изучении метода IWServerControllerBaseNewSession класса TlWServer- Controller в стандартном модуле ServerController: procedure TlWServerController.IWServerControllerBaseNewSessionl ASession TIWApplication; var VMainForm: TIWAppForm): begin ASession Data .= TUserSession Create; end, В программном коде приложения к объекту «сеанс» можно обращаться посред- ством доступа к полю Data глобальной переменой RWebApplication, используемой для доступа к сеансу текущего пользователя. СОВЕТ-----------------------------------------------------------------------------— RWebApplication — это threadvar-переменная, определенная в модуле IWInit. Она предоставляет доступ к данным сеанса многопоточным образом: при обращении к ним требуется особое внимание даже в многопоточной среде. Эта переменная может использоваться за пределами формы или эле- мента управления (которые по своей природе основаны на сеансе). Вот почему она в основном используется в модулях данных, глобальных процедурах и He-IntraWeb-классах. 0929
930 Глава 21. Веб-программирование с использованием IntraWeb Стандартный модуль Servercontroller предлагает еще одну вспомогательную функцию: function UserSession- TUserSession. begin Result = TUserSessiontRWebApplication Data); end; Ввиду того что большая часть программного кода генерируется автоматически, после добавления данных в класс TUserSession вы можете использовать их с помощью функции UserSession, как в представленном ниже фрагменте, позаимствованном из примера IWSession. При щелчке на кнопке программа произведет увеличе- ние некоторых счетчиков (один глобальный и два связанных с сеансом) и пред- ставит их значения: procedure TformMain IWButtonlC)ick(Sender• TObject): begin Interlockedlncrement (GlobalCount). Inc (FormCount). Inc (UserSession.UserCount); IWLabel 1 Text = 'Global. ’ + IntToStr (GlobalCount). IWLabelZ Text = 'Form- ' + IntToStr (FormCount). IWLabel3.Text := 'User: ' + IntToStr (UserSession UserCount): end. Обратите внимание, что во избежание параллельного доступа к глобальной, совместно используемой переменной множеством потоков программа использует Windows-вызов Interlockedlncrement. Альтернативный подход включает использо- вание критического раздела или TidThreadSafelnteger Indy (см. модуль IdThreadsafe). Рис. 21.5. IWSession-приложение имеет сеансовые и глобальные счетчики На рис/ 21.5 представлен результат работы программы (два сеанса, запущен- ные в двух браузерах). Программа также имеет флажок, активизирующий таймер. В IntraWeb-приложениях таймеры работают практически так же, как и в Windows. По истечении временного интервала таймера выполняется программный код. 0930
Построение IntraWeb-приложений 931 В Сети это означает обновление страницы посредством срабатывания обновления в JavaScript-коде: IWTIMERl=setTimeout('SubmitClickC'IWTIMERr',false)’ .5000). Интеграция с WebBroker (и WebSnap) До сих пор мы рассматривали автономные IntraWeb-приложения. При создании IntraWeb-приложения в форме библиотеки, устанавливаемой на IIS или Apache, ситуация сохраняется. Однако все полностью меняется, если вы решите использо- вать в IntraWeb режим Page, интегрированный с WebBroker- (или WebSnap-) при- ложением Delphi. Связующим звеном между двумя «мирами» является компонент IWPageProducer. Этот компонент связывается с действием WebBroker, как и другие компоненты- продюсеры страниц, и имеет специальное событие, которое можно использовать для создания и возвращение IntraWeb-формы: procedure TUebModulel. IWPageProducerlGetForcn/ASender • TlUPageProducer, AWebApplication- TIWApplication. var VForm. TIWPageForm): begin VForm = TformMain.Create(AWebApplication); end. С помощью одной строчки программного кода (плюс добавление компонента IWModuleControher в веб-модуль) WebBroker-приложение может внедрить страни- цу IntraWeb, как в программе Cgilntra. Компонент IWModuIeController предоставля- ет основные службы поддержки IntraWeb. Для правильной работы компонент это- го типа должен присутствовать в каждом IntraWeb-проекте. ВНИМАНИЕ ------------------------------------------------------------------------ Выпуск, поставляемый совместно с Delphi 7, имеет проблему, связанную с Web Арр Debugger и IWModuleController. Она уже устранена, и обновление распространяется бесплатно. Вот окончательный DFM веб-модуля программы-примера: object WebModulel TWebModulel Actions = < item Default = True Name = 'UebActionlteml ’ Pathinfo = '/show' OnAction = WebModulelWebActionltemlAction end item Name = 'UebActwnltemP' Pathinfo = 'hwdemo' Producer = IWPageProducerl end> object IWModuleControllerl TIWModuleController object IWPageProducerl: TlWPageProducer OnGetForm = IWPageProducerlGetForm end end Поскольку это CGI-приложение в режиме Page, оно не имеет возможности управления сеансом. Более того, состояние компонентов страницы при написании 0931
932 Глава 21. Веб-программирование с использованием IntraWeb обработчиков событий не будет обновляться автоматически, как в стандартной IntraWeb-программе Чтобы достичь того же эффекта, необходимо написать спе- циальный программный код, обрабатывающий последующие параметры НТТР- запроса Даже на основе этого примера становится ясно, что режим Раде выполняет за вас меньше работы, чем режим Application, но является более гибким В частно- сти, режим Раде позволяет добавлять в WebBroker- и WebSnap-приложения визу- альные возможности конструирования RAD Управление размещением Программа Cgilntra содержит еще одну интересную технологию, предоставляемую IntraWeb определение пользовательского расположения, основанного на HTML (Это не очень связано с темой, поскольку HTML-расположение работает только в режиме Application, но мне пришлось использовать обе технологии в одном при- мере ) В рассмотренных до сих пор программах результирующая страница являет- ся отображением последовательности компонентов, помещенных в ходе разработ- ки на форму, у которой для изменения конечной HTML-страницы можно изменять свойства А что будет, если внедрить форму ввода данных в сложную HTML-стра- ницу? Построение всего содержания страницы с помощью компонентов IntraWeb является очень неудобным процессом, даже если для внедрения пользовательских фрагментов HTML на странице IntraWeb использовать компонент IWText iC HTML Editar * e* '*** *>”t Tatfe ***______________________ l» j*ж1** og* „ _____ |;|нопм1 Ятimes New Roman Btek В 1 g Html Example code IWlabell More text goes here and you can type it directly And finally we have some text and a combo box withm a gnd Рис. 21.6. HTML Layout Editor является полнофункциональным HTML-редактором Альтернативный подход заключается в использовании диспетчеров располо- жения (layout managers) IntraWeb В IntraWeb повсеместно используются диспет- черы расположения, по умолчанию это компонент IWLayoutMgrForm Еще два до- полнительных варианта — это IWTemplateProcessorHTML для работы с внешним файлом HTML-шаблона и IWLayoutMgrHTML для работы с внутренним HTML Второй компонент включает мощный HTML-редактор, который может исполь- зоваться для подготовки общего HTML, а также встроенный редактор, необходи- 0932
Приложения сетевых баз данных 933 мне IntraWeb-компоненты (кое-что приходится делать вручную с помощью внеш- него редактора) Более того, при выборе IntraWeb-компонента из этого редактора (активизируемого двойным щелчком на компоненте IWLayoutMgrHTML) для настрой- ки свойств компонента вы сможете использовать инспектор объектов Delphi Су- ществующий в IntraWeb редактор HTML Layout Editor (рис 216) является мощным визуальным HTML-редактором, генерируемый им HTML-текст представляется на отдельной странице (HTML-редактор при ближайшем обновлении будет усо- вершенствован, а некоторые существующие упущения устранены ) В сгенерированном HTML-коде язык HTML определяет структуру страницы Компоненты отмечаются только с помощью специального тега, основанного на использовании фигурных скобок <Р> {HWLabelH} {HWButtonH}</P> ПРИМЕЧАНИЕ--------------------------------------------------------------- Обратите внимание, что при использовании HTML компоненты не используют абсолютное позици- онирование, а расположены в общем потоке HTML Следовательно, форма становится лишь «дер- жателем» компонента, поскольку размер и местоположение компонентов формы игнорируются Можно и не говорить, что HTML, который вы видите в визуальном конструк- торе HTML Layout Editor, практически полностью соответствует HTML, который вы увидите при запуске программы в браузере Приложения сетевых баз данных Как и в библиотеках Delphi, значительная часть имеющихся элементов управле- ния IntraWeb относится к разработке приложении баз данных Мастер IntraWeb Application Wizard имеет версию, позволяющую создавать приложение с модулем данных — хорошая точка отсчета для разработки приложений, работающих с база- ми данных В этом случае предопределенный код приложения создает экземпляр модуля данных для каждого сеанса, сохраняя его в данных сеанса Вот предопределенный класс TUserSession (и его конструктор) для IntraWeb- приложения с модулем данных type TUserSession = class(TComponent) publiс DataModulel TDataModulel constructor Create(AOwner TComponent) override end constructor TUserSession Create(AOwner TComponent) begin inherited Datamodulel = TDatamodulel Create(AOwner) end Модуль данных не имеет для него глобальной переменной, если бы таковая имелась, все данные совместно использовались бы сеансами с большим риском возникновения проблем в случае возникновения параллельных запросов во мно- жестве потоков Однако модуль данных «открывает» глобальную функцию, име- 0933
934 Глава 21. Веб-программирование с использованием IntraWeb юшую то же имя, что и глобальная переменная Delphi, которая будет применяться для обращения к модулю данных текущего сеанса: function DataModulel TDataModulel begin Result = TUserSessiontRWebApplication Data) Datamodulel, end. Это позволяет написать: DataModulel SimpleDataSetl Но вместо обращения к глобальному модулю данных будет использоваться модуль данных текущего сеанса. В первой программе-примере, работающей с базами данных (IWScrollData), я до- бавил в модуль данных компонент SimpleDataSet, а в главную форму приложения — компонент IWDBGrid со следующей настройкой: object IWDBGridl TIWDBGrid Anchors = [akLeft, akTop akRight akBottom] BorderSize = 1 Cell Padding = 0 CellSpacing = 0 Lines = tlRows UseFrame = False DataSource = DataSourcel FromStart = False Options = [dgShowTitles] RowAlternateColor = cl Si Tver RowLimit = 10 RowCurrentColor = clTeal end Рис. 21.7. Сетка таблицы примера IWScrollData Наиболее важные настройки относятся к удалению фрейма, содержащего эле- мент управления с собственными линейками прокрутки (свойство UseFrame), вы- воду данных с текущей позиции набора данных (свойство FromStart) и числу строк, выводимых в браузере (свойство RowLimit). В пользовательском интерфейсе я уда- лил вертикальные линии и раскрасил строки в чередующиеся цвета. Кромг того, 0934
Приложения сетевых баз данных 935 я настроил цвет текущей строки (свойство RowCurrentColor); в противном случае поочередные цвета работали бы некорректно, поскольку текущая строка имела бы такой же цвет, как и неактивные строки, независимо от ее положения (установите свойство RowCurrentColor в clNone и увидите, о чем я говорю). Эти настройки созда- ют эффект, который можно видеть, запустив пример IWScrollData, или на рис. 21.7. Программа открывает набор данных при создании формы, используя набор дан- ных, связанный с текущим источником данных: procedure ТformMain IWAppFormCreatetSender TObject) begin OataSourcel DataSet Open. end Особенно важной частью кода примера является программный код кнопок, ко- торые могут использоваться для перехода к следующей странице или для возврата к предыдущей Вот программный код одного из двух методов (второй опущен вви- ду его схожести): procedure TformMain btnffextClicktSender TObject), var i Integer begin nPos = nPos + 10 if nPos > OataSourcel DataSet RecordCount - 10 then nPos = OataSourcel DataSet RecordCount - 10 OataSourcel DataSet First for i =0 to nPos do OataSourcel DataSet Next end. Подключение к уточнениям (подчиненным данным) Сетка таблицы примера IWScrollData выводит отдельную страницу таблицы дан- ных; кнопки позволяют прокручивать страницы вверх и вниз. Альтернативный вариант стиля сетки в IntraWeb предлагает сетку с рамками, которая может пере- мещать огромный объем данных в веб-браузер с экранной областью фиксирован- ного размера, используя фреймы и внутренние линейки прокрутки, как это делает элемент управления ScrollBox в Delphi Все вышеперечисленное отражено в приме- ре IWGridDemo. Эют пример настраивает сетку таблицы вторым, более мощным способом, он устанавливает свойство-коллекцию Columns сетки Эта настройка позволяет под- строить вывод и поведение определенных столбцов, например, выводя гиперссыл- ки или обрабатывая щелчки на пунктах или заголовках ячеек. В примере IWGridDemo один из столбцов (last_name — фамилия) превращен в гиперссылку; номер сотруд- ника передается в качестве параметра в последующую команду (рис. 21 8). В листинге 21.1 представлена сводка ключевых свойств сетки. Обратите вни- мание, в частности, на столбец с фамилиями, который, как я уже говорил, имеет связанное поле (превращающее текст ячейки в гиперссылку) и обработчик собы- тия, отвечающий за действие при его выделении. В этом методе программа создает вторую форму, в которой допускается редактирование данных пользователем. 0935
936 Глава 21. Веб-программирование с использованием IntraWeb : «hS МогЛа * fife В* 8«* & loots Mndow FIRST.NAME LAST_NAME HIRE_DATE JOB.CODE JOB_COUNTRY JOB-GRADE PHONE.EXT —id Robert Ne.scn 12/28/1988 VP USA 2 332 Bruce ycung 12/22/1988 Eng USA 2 233 Kim Lambert 2/6/1989 Eng USA 2 22 Leshe Jchnson 4/5/1989 Mktg USA 3 410 Phil Forest 4/17/1989 Mngr USA 3 229 к J on 1/17/1990 SRep USA 4 34 Tern Lee 5/1/1990 Admin USA 4 256 Stewart Hafl 6/4/1990 Finan USA 3 227 Kathenns Yeung 6/14/1990 Mngr USA 3 231 Chns Papadopoulos 1/1/1990 Mngr USA 3 887 JPete Fisher 9/12/1990 Eng USA 3 888 Ann Benre* 2/1/1991 Admin England 5 5 Roger D* Souxa 2/18/1991 Eng USA 3 288 Janet вдолп 3/21/1991 Sales USA 3 2 Roger Reeves 4/25/1991 Sales England 3 6 MlfchA «ТЛ БпгтЦг'И A 7 <r UC-isf1. Рис. 21.8. Главная форма примера IWGridDemo использует сетку с рамками с гиперссылками на вторую форму procedure TGmdForm IWDBGmdlColumnslClick(ASender TObject, const AValue String) begin with TRecordForm Create (WebApplication) do begin StartID = AValue, Show end end Листинг 21.1. Свойства IWDBGnd в примере IWGridDemo object IWDBGridl TlWDBGrid Anchors = [akLeft, akTop, akRight, akBottom] UseFrame = True UseWidth = True Columns = < item Alignment = taLeftJustify BGColor = clNone DoSubmitValidation = True Font Color = clNone Font Enabled = True Font Size = 10 Font Style = [] Header = False Height = 'O' VAlign = vaMiddle 0936
Приложения сетевых баз данных 937 Visible = True Width = 'О' Wrap = False BlobCharLimit = 0 CompareHighl ight = hcNone DataField - 'FIRSTJJAME' Title Alignment = taCenter Title BGColor = cl None Title DoSubmitValidation = True Title Font Color = cl None Title Font Enabled = True Title Font Size = 10 Title Font Style = [] Title Header = False Title Height = 'O' Title Text = 'FIRST_NAME‘ Title VAlign = vaMiddle Title Visible = True Title Width = 'O' Title Wrap = False end item DataField = 'LASTJAME' LinkField = 'EMP_N0' OnClick = IWDBGridlColumnslCl ick end item DataField = 'HIRE_DATE‘ end item DataField = M8_C0DE end item DataField = 'JOB_COUNTRY' end item DataField = 'JOB_GRAOE' end item DataField = ' PHONEJXT' end> DataSource = DataSourcel Options = [dgShowTitles] end За счет установки свойства StartID второй формы можно указать соответствую- щую запись: procedure TRecordForm SetStart ID(const Value string), begin FStartID - Value DataSourcel DataSet Locatet EMPJJO' Value [J) end ПРИМЕЧАНИЕ------------------------------------------------------------------------------— Столбцы IWDBGnd также имеют событие OnTitleClick, которое может использоваться для сортиров- ки или выполнения других операций с данным столбцом. 0937
938 Глава 21. Веб-программирование с использованием IntraWeb Вторая форма связана с тем же модулем данных, что и главная форма. Поэтому после обновления данных в базе данных вы увидите это в сетке (но обновление содержится только в памяти, поскольку программа не имеет вызова ApplyUpdates). Вторая форма использует несколько элементов редактирования и навигатор, предоставляемые IntraWeb. В ходе выполнения эта форма имеет следующий вид (рис. 21.9). Рис. 21.9. Вторая форма примера IWGridDemo позволяет редактировать данные и перемещаться по записям Передача данных клиентской стороне Независимо от использования, компонент IWDBGrid создает HTML с данными из базы данных, внедренными в ячейки, но он не может работать с данными на клиентской стороне. Такая работа поддерживается иными компонентами IntraWeb. Данные посылаются браузеру в настраиваемом формате, а JavaScript-код в браузе- ре заполняет сетку таблицы и оперирует с данными, переходя от записи к записи 5ез запроса дополнительных данных с сервера. ЗОВЕТ------------------------------------------------------------ Эта архитектура подобна собственной архитектуре Internet Express среды Delphi, которую мы рас- :мотрим в главе 22. В клиентских приложениях можно использовать многие IntraWeb-компонен- гы, но самые важные из них следующие: э IWClientSideDataSet. Размещенный в памяти набор данных, определяемый ус- тановкой в программном коде свойств ColumnNames и Data. В случае последующих обновлений допускается редактирование данных клиентской стороны, их сорти- ровка, фильтрация, определение структур «главный-подчинепный» и т. п. 0938
Приложения сетевых баз данных 939 О IWClientSideDataSetDBLink. Провайдер данных, который можно подключить к набору данных Delphi с помощью свойства DataSource. О IWDynGrid. Компонент «динамическая сетка», подключенный к одному из двух предыдущих компонентов посредством свойства Data, Этот компонент переме- щает все данные в браузер и позволяет оперировать с ними на клиентской сто- роне при помощи JavaScript. О В IntraWeb имеются и другие клиентские компоненты, такие как IWCSLabel, IWCSNavigator и IWDynamicChart (которые работают только с Internet Explorer). В качестве примера их использования я разработал программу IWClientGrid. Про- грамма содержит немного программного кода, поскольку все реализовано в ис- пользуемых компонентах. Вот ключевые элементы его главной формы: object formMain TformMain SupportedBrowsers = [brIE, brNetscape6] OnCreate = IWAppFormCreate object IWDynGridl: TIWDynGrid Align = alCllent Data = IWClientSideDatasetDBLinkl end object DataSourcel- TDataSource Left = 72 Top = 88 end object IWClientSideDatasetDBLinkr. TIWClientSideDatasetDBLink DataSource = DataSourcel end end Набор данных модуля данных связывается с DataSource при создании формы. Результирующая сетка (рис. 21.10) позволяет сортировать данные по любой ячей- ке (используя маленькую стрелку, стоящую после заголовка столбца) и фильтро- вать выводимые данные по одному из возможных значений поля. На рисунке пред- ставлен результат сортировки данных сотрудников по фамилии и фильтрации по стране и степени занятости. Lift MQZ1IU • А Fte £Лг «ем go ewkwte loot Jgndow Htip ЦвЙ | htt- /,М27 0 0 1 ВОвО/ЕХЕС/аЛ>4аСЛ101Р10С309С6057Е240 | j j EMP_NO»IRST_NAMEe&i.AST_NAME <»J'HONE_EXT&:TiEPT_NOi*zJOB_CODE^SOB_GRADE;*JOB_COUNTRY'»' 1Л" *1 R jxl ’All *jfA« il ’А» J .’All P Э ’USA 138 TJ Green 218 621 Eng 4 USA 12 Tem Lee 256 000 Admin 4 USA 61 Luke Leung 3 110 SRep 4 USA 'f 52 Carol Nordstrom 420 180 PRel 4 USA 113 Mary Page 845 671 Eng 4 USA < 44 Leslie Phong 216 623 Eng 4 USA 11 KJ Weston 34 130 SRep 4 USA 94 Randy Williams 892 672 Mngr 4 USA 127 Michael Yanowski 492 100 SRep 4 USA ц Рис. 21.10. Сетка примера IWClientGrid поддерживает различные сортировки и фильтрацию без повторной выборки данных с сервера 0939
940 Глава 21. Веб-программирование с использованием IntraWeb Такая возможность обеспечивается благодаря тому, что данные перемещаются в браузер с помощью JavaScript-кода. Вот фрагмент одного из сценариев, внедрен- ного в HTML-код страницы: «script 1anguage="Javasenptl.2"> var IWDYNGRIDl_TitleCaptions - ["EMPJVO". "FIRSTJIAME". "LASTJIAME”. "PHONEJXT". "OEPT_NO". "JOB_CODE". ”JOB_GRADE". "JOB-COUNTRY"]; var IWDYNGRIDl_CellValues = new ArrayO; IWDYNGRIDl_CellValues[O] = [2. 'Robert'. 'Nelson'. '332', '600'. 'VP' .2, 'USA'}; IWDYNGRIDl_Cel1 Vaiues[l] - [4.'Bruce','Young','233','621’, 'Eng'.2,'USA']; IWDYNGRIDl_CellValues[2] - [5. 'Kim'. ‘Lambert’. '22'. '130'. 'Eng' .2, 'USA']-. IWDYNGRIDl_CellValues[3] = [8. 'Leslie'. 'Johnson'. '410', '1B0'. 'Mktg' .3. 'USA']; IWDYNGRIDl_CellValues[4] = [9, 'Phil'. 'Forest', '229','622‘. 'Hngr'.3.'USA']; СОВЕТ ------------------------------------------------------------------------------------- Причина использования JavaScript вместо языка XML, применяемого в других подобных технологи- ях, заключается в том, что изолированные данные XML может поддерживать только Internet Explorer. Mozilla и Netscape не имеют такой возможности и, в общем-то, имеют ограниченную поддержку XML. Его имитация на языке JavaScript (как это делается в Internet Explorer) — очень дорогостоящая задача во время выполнения. Что далее? Описание особенностей IntraWeb в этой главе далеко не полное, но моей целью было предоставить вам возможность изучить эту технологию для того, чтобы определиться, как ее использовать в предстоящих проектах. IntraWeb настолько мощная, что теперь вместо прочих средств разработки можно использовать среду Delphi. Дополнительную информацию можно найти в документации IntraWeb, разме- щенной на вспомогательном компакт-диске Delphi (а не на основном CD). Стан- дартная установка Delphi содержит демонстрационные примеры IntraWeb, вклю- чая всесторонний пример Features, который одновременно представляет почти все возможности этой библиотеки компонентов. Обновления, дополнительная доку- ментация и примеры можно найти на веб-сайте IntraWeb (www.atozedsoftware.coni). В этой книге описаны и другие альтернативные подходы разработки веб-при- ложений, основанных на XML и XSLT. Глава 22 полностью посвящена обобщенной сводке XML-технологий с точки зрения Delphi. Таким образом, мы имеем другую возможность охвата веб-разработки в Delphi на основе одной из самых любимых мной технологий, являющейся также одной из самых сложных. 0940
QO Использование технологий XML Под построением приложений для Интернета обычно подразумевают использова- ние протоколов и создание пользовательских интерфейсов, основанных на приме- нении браузера. Именно этим мы занимались в предыдущих двух главах. Однако в последнее время Интернет все чаще расценивается как средство обмена бизнес- документами в электронной форме. В последнее время для этой цели был сформи- рован набор стандартов, основанных на использовании формата XML. В этот набор вошли такие стандарты, как транспортный протокол SOAP, схемы XML, предна- значенные для проверки корректности документов, а также технология XSL, позволяющая преобразовывать документы XML в формат HTML. В данной главе я планирую рассмотреть основные технологии XML и поддер- жку этих технологий в среде Delphi. Я полагаю, что далеко не все читатели хорошо знакомы с XML, поэтому в начале главы я коротко расскажу о каждой из связан- ных с XML технологий. Чтобы узнать об XML подробнее, вы должны обратиться к специализированным книгам. В главе 23 я сконцентрируюсь на изучении веб- служб и протокола SOAP. В этой главе будут рассмотрены следующие вопросы: О знакомство с XML (Extensible Markup Language); О работа с XML DOM; О Delphi и XML: интерфейсы и отображение; О грамматический разбор документов XML с использованием SAX; О Internet Express; О использование XSLT; О XSL в рамках WebSnap. Знакомство с XML XML (Extensible Markup Language) — это упрощенная версия языка SGML. В по- следнее время XML находится в центре внимания IT-общественности. XML — язык разметки (Markup language), это означает, что XML использует символы для описания информации, содержащейся в документе. В качестве разметки исполь- зуются маркеры, называемые тегами (tags). Тег — это специальный текст, заклю- 0941
942 Глава 22. Использование технологий XML ченный в угловые скобки (<...>). Язык является расширяемым (extensible), так как (в отличие от HTML, в котором все теги предопределены) XML допускает использование маркеров, определяемых пользователем. Язык XML — стандарт, ко- торый поддерживается Консорциумом World Wide Web (W3C). Основные правила XML изложены в документе XML Recomendation по адресу www.w3.org/TR/REC-xml. Иногда говорят, что в XXI веке язык XML будет тем же, чем был стандарт ASCII в XX столетии. Подразумевается простота и широта распространения XML, кроме того, подчеркивается, что документы XML — это файлы в обычном текстовом фор- мате (возможно, с применением символов Unicode вместо обычных символов ASCII). Важной характеристикой XML является его информативность: каждый тег — это в большинстве случаев осмысленное слово, понятное для человека. Вот пример простого XML-документа: <book> <title>Mastering Delphi 7</title> <author>Cantu</author> <publisher>Sybex</publisher> </book> С самого начала я хотел бы подчеркнуть, что XML обладает рядом недостатков. Самый большой недостаток заключается в том, что без формального описания структуры XML-документ невозможно расшифровать. Так, если вы хотите обме- ниваться документами с другой компанией, вы должны договориться с ней о том, что будет означать каждый из тегов, а также договориться о семантике представле- ния содержимого. (Например, если в документе содержится некоторое численное значение, вы должны договориться о системе измерения или добавить эту инфор- мацию в документ.) Еще один недостаток заключается в том, чго размер XML- документов, как правило, превышает размер аналогичных документов в других форматах. В частности, в XML численные значения представляются в виде сим- вольных строк, а это далеко не самый эффективный способ. Кроме того, повторя- ющиеся открывающие и закрывающие теги требуют значительного места. Хоро- шая новость состоит в том, что XML-документы хорошо сжимаются (по той же причине). Основной синтаксис XML Вот краткий список ключевых элементов синтаксиса XML. О Символы пропуска (пробел, возврат каретки, новая строка, табуляция) игно- рируются (как в документах HTML). Все подобные символы можно использо- вать для того, человеку удобнее было читать XML-документ. Однако програм- мы, осуществляющие обработку XML в автоматическом режиме, не обращают на эти символы никакого внимания. О Комментарии добавляются между маркерами <!— и —>. Любой текст, содержа- щийся между этими маркерами, игнорируется средствами обработки XML. Служебные директивы и инструкции, связанные с обработкой XML-докумен- тов, заключаются между маркерами <? и ?>. О Существует несколько специальных или зарезервированных символов, кото- рые нельзя использовать в документе XML. В частности, нельзя использовать символ «меньше», то есть открывающую угловую скобку (<). Вместо нее следует 0942
Знакомство с XML 943 использовать сочетание символов &lt. Также нельзя использовать символ «ам- персанд» (&). Вместо него следует использовать последовательность символов &атр. Вместо символа «больше», то есть закрывающей угловой скобки (>), сле- дует использовать последовательность символов &gt. Вместо апострофа, то есть одинарной кавычки ('), — последовательность символов &apos, а вместо двой- ной кавычки (") — &quot, О В состав XML-документа могут быть добавлены данные, не являющиеся тек- стом в формате XML (например, это могут быть бинарные данные или сцена- рий на некотором языке программирования). Для обозначения этой информации используется раздел CD АТА, который отмечается маркерами <![CDATA[ и ] ]>. О Все маркеры заключаются в угловые скобки, < и >. Маркеры являются чувстви- тельными к регистру символов (в отличие от HTML). О Каждому открывающему маркеру должен соответствовать замыкающий мар- кер, имя которого начинается с символа косой черты, например: <node>value</node> О Маркеры должны быть корректно вложены друг в друга, то есть они не должны перекрываться. Так, первая строка написана корректно, в то время как вторая строка считается некорректной: <node>xx <nested> yy</nested> </node> // правильно <node>xx <nested> yy</node> </nested> // неправильно О Если маркер не’содержит данных, однако его присутствие важно, вы можете заменить открывающий и замыкающий маркеры единственным маркером, на конце которого стоит символ косой черты <node/>. О Маркеры могут обладать атрибутами. В этом случае внутри угловых скобок помимо имени маркера могут быть указаны имена одного или нескольких ат- рибутов. За каждым именем атрибута указывается знак равенства и значение этого атрибута. Например: <node attmbl="aaa"> О Любой раздел документа XML может включать в себя несколько атрибутов, несколько вложенных тегов и только один блок текста. Блок текста — это зна- чение раздела. Как правило, в раздел XML включают либо текстовое значение, либо вложенные теги, но не то и другое одновременно. Вот пример полного формата раздела XML: <node attribl="aaa" attnb2="bbb"> valuel <childl> value2 </cfn 1 dl> </node> О В каждом разделе может присутствовать несколько подразделов (теги не обя- зательно должны быть уникальными). Имена атрибутов должны быть уникаль- ными для каждого раздела. Хорошо оформленный XML В предыдущем разделе определяется синтаксис XML-документов, однако этого не достаточно. XML-документ считается синтаксически корректным, или хорошо 0943
944 Глава 22. Использование технологий XML оформленным (well-formed), если он удовлетворяет нескольким дополнительным правилам. Следует иметь в виду, что проверка формата на корректность не гаран- тирует, что в документе содержится осмысленная информация. Корректность фор- мата означает только, что теги расположены правильно. Каждый документ обладает прологом, указывающим на то, что документ дей- ствительно является XML-документом. В прологе указывается версия XML, а так- же, возможно, тип кодировки символов. Вот пример пролога: <?xml version=”1.0" encoding="UTF-8''?> Допускается использование кодировок Unicode (UTF-8, UTF-16 и UTF-32), а также некоторых кодировок ISO (таких как ISO-10646-ххх или ISO-8859-ххх). Пролог может содержать также внешние объявления, указание схемы, используе- мой для проверки документа, объявления пространств имен и прилагаемый XSL- файл. Дополнительную информацию об этом можно получить из книг и докумен- тации XML. Документ XML считается хорошо оформленным, если у него есть пролог, если документ обладает корректным синтаксисом (см. набор правил в предыдущем раз- деле) и если он представляет собой дерево разделов (часто их называют узлами — lodes) с единым корнем. Большинство инструментов для работы с XML (включая эраузер Internet Explorer) перед загрузкой XML-документа проверяют, является ди он хорошо оформленным. ПРИМЕЧАНИЕ -------------------------------------------------------------------- 1зык XML является более формализованным и более аккуратным, чем HTML. В настоящее время юнсорциум W3C работает над формированием стандарта XHTML, который позволяет делать доку- генты HTML удовлетворяющими всем правилам синтаксиса XML. Благодаря этому в отношении щавильно оформленных документов HTML можно использовать инструменты, предназначенные 1ля обработки XML. Чтобы сделать документ HTML совместимым с требованиями XML, необходимо !ыполнить некоторые важные преобразования, в частности, необходимо избавиться от атрибутов, вторым не соответствуют значения, добавить все отсутствующие замыкающие маркеры (такие как :/р> и </li>), добавить к отдельно стоящим маркерам завершающий символ косой черты (напри- iep, <hr/> и <br/>), обеспечить корректную вложенность разделов и т. п. Существует специаль- |ый инструмент, выполняющий преобразование HTML в XHTML. Этот конвертер называется HTML idy, его можно найти на веб-узле консорциума W3C по адресу www.w3c.org/People/Raggett/tidy/. ’абота с XML 1тобы поближе познакомиться с форматом XML, вы можете воспользоваться од- шм из существующих XML-редакторов (в частности, редактировать XML позво- :яет сама среда Delphi, кроме того, существует редактор Context, написанный на Delphi). Помимо этого, вы можете загрузить XML-документ в браузере Internet Explorer, чтобы убедиться, что этот документ обладает корректной структурой, nternet Explorer также демонстрирует XML-код в виде древовидной структуры. На момент написания данной книги другие браузеры обладали более ограничен- ой поддержкой XML.) Чтобы упростить все эти операции, я разработал самый простой из всех возмож- ых редактор XML. В его основе лежит компонент Мето с проверкой синтаксиса IML и отображением XML-кода в окне браузера. На форме программы XmLEditOne рисутствует компонент PageControl с тремя страницами. На первой странице — 0944
Знакомство с XML 945 Settings — вы можете ввести путь и имя файла, с которым вы хотите работать. По- чему я не использовал стандартное диалоговое окно? Причина этого станет понят- на, когда я покажу вам расширение программы. Графа редактирования, в которой указывается полное имя файла, автоматически обновляется при условии, что фла- жок AutoUpdate установлен. На второй странице располагается элемент Мето, в котором отображается текст XML-файла. Загрузить и сохранить файл можно при помощи двух кнопок, распо- ложенных на панели инструментов. При загрузке файла, а также каждый раз, ког- да вы модифицируете его текст, содержимое файла загружается в DOM для того, чтобы модуль грамматического разбора выполнил проверку его корректности (эту задачу сложно выполнить при помощи своего собственного кода). Чтобы выпол- нить грамматический анализ кода, я использую присутствующий в Delphi компо- нент ХМ LDocument, который фактически является объектом-оболочкой установлен- ного на компьютере программного модуля DOM, узнать о котором можно при помощи свойства DOMVendor. Более подробно об использовании этого компонента рассказывается в следующем разделе. На текущий момент достаточно сказать, что для грамматического разбора XML-кода необходимо присвоить свойству ХМL это- го компонента строковый список, а затем активизировать компонент. В результа- те активации компонент выполняет грамматический разбор XML-кода. Об ошиб- ке сообщается при помощи исключения. В данном случае подобное поведение нежелательно, ведь в процессе редакти- рования исходников XML этот код очень часто бывает некорректным. Конечно, можно сделать так, чтобы для проверки корректности окончательно отредактиро- ванного XML-кода пользователь должен был щелкнуть на специальной кнопке, однако такой подход мне не нравится. Я предпочитаю, чтобы проверка корректно- сти осуществлялась непрерывно прямо в процессе редактирования кода. Отклю- чить возникновение исключения, которое генерируется компонентом ХМ LDocument, невозможно, поэтому я вынужден работать на более низком уровне. Для этого после получения интерфейса IXMLDocumentAccess из компонента XMLDocument (в рассмат- риваемом коде он называется XmLDoc) я извлекаю свойство DOM Persist (обращаясь к интерфейсу DOM, отвечающему за постоянное сохранение информации). В этот момент я могу также получить интерфейс IDOMParseError из компонента докумен- та, чтобы с его помощью отобразить какое-либо сообщение об ошибке в строке состояния: procedure TFormXmlEdit.MemoXmlChange(Sender: TObject): var eParse: IDOMParseError: begin Xml Doc.Active := True; xmlBar.Panels[l],Text := ’OK’: xmlBar.Panels[2],Text := (XmlDoc as IXMLDocumentAccess).DOMPersist.loadxml(MemoXml.Text): eParse : = (XmlDoc.DOMDocument as IDOMParseError); If eParse.errorCode <> 0 then with eParse do begin xmlBar.Panel s[l].Text = 'Error in: ' + IntToStr (Line) + + IntToStr (LinePos). xmlBar.Panels[2].Text SrcText + '; 1 + Reason: 0945
946 Глава 22. Использование технологий XML end end Пример вывода программы показан па рис. 22.1. На этом рисунке также пока- зано отображение древовидной структуры корректного XML-документа на тре- тьей странице программы. Третья страница построена с использованием компо- нента WebBrowser, включающего в себя элемент ActiveX браузера Internet Explorer. К сожалению, данный компонент не позволяет напрямую обрабатывать строку, содержащую код XML, поэтому, чтобы отобразить этот код при помощи данного компонента, требуется сначала записать XML-код в файл на диске, а затем перей- ти на третью страницу, чтобы инициировать загрузку XML в окне браузера. Перед этим необходимо также как минимум один раз щелкнуть на кнопке Refresh (Обно- вить). ЙИВДИ—О» toad Refresh ................................................ >хж! version-’*! 0” Hnr'«‘»ding-"UTF-8,'> book publisher-’bybex*-»- 1 title Mastering Delphi 7 /title» j Author zMarco Cantu /author /Ьплкг i lead _ £** .Rafreth Satn»! яа, KHLVe» I «.?/•*• vfersnn-"i j' ₽'icr dnj~ t vi v •> <book pjbl I »>- Sybex" » <Мз «Mastering Delphi 7c-'*itte > «'uUtl'Ct «Marco Cantu».? sutler» «/book.» J ’ Рис. 22.1. Пример XmlEditOne позволяет ввести XML-код в окне редактирования Мето. В процессе ввода производится автоматическая проверка ошибок. Результат отображается во встроенном браузере ПРИМЕЧАНИЕ--------------------------------------------------------------- Я использовал рассмотренный код в качестве основы при разработке полнофункционального XML- редактора под названием XmlTypist для компании, с которой я работаю. Разработанный мною ре- дактор поддерживает подсветку синтаксиса, поддержку XSLT, а также некоторые дополнительные возможности. О свободно распространяемом редакторе XML рассказывается в приложении А. Обработка ХМL-документов Теперь, когда вы знакомы с ключевыми элементами XML, мы можем приступить к обсуждению методов обработки XML-документов в программах, разрабатывае- мых в среде Delphi. Многие обсуждаемые здесь методики можно использовать так- же и при разработке программ с использованием других языков и сред, поэтому часть обсуждаемого материала окажется полезной не только пр i работе с Delphi. Существует две стандартных технологии манипулирования документами XML: первая подразумевает использование DOM (Document Object Model), вторая ос- нована на использовании SAX (Simple API for XML). Два эти подхода существен- но отличаются друг от друга. О DOM (Document Object Model) загружает весь документ в память в виде иерар- хического дерева узлов. Вы получаете возможность прочитать любой из них и выполнить в отношении него некоторые процедуры, включая операции ре- дактирования. Таким образом, DOM удобно применять в случае, если вы на- 0946
Знакомство с XML 947 мерены перемещаться по структуре XML-документа и редактировать ее. Кро- ме того, при помощи DOM вы можете создать новый XML-документ с нуля. О SAX (Simple API for XML) не предусматривает загрузку документа в память. Вместо этого данный механизм самостоятельно осуществляет грамматический разбор XML-кода и генерирует событие для каждого его логического элемента. Когда грамматический разбор документа при помощи SAX завершается, доку- мент бесследно исчезает из системы, и дальнейший доступ к нему без повтор- ной загрузки невозможен. Однако операция грамматического разбора с исполь- зованием SAX выполняется значительно быстрее, чем построение в памяти древовидной структуры DOM. Таким образом, использовать SAX удобнее в ситуации, когда требуется прочитать XML-документ однократно, например, если вы осуществляете в нем поиск некоторого фрагмента данных. Существует также третий способ обработки и создания XML-документов, ос- нованный на использовании механизмов работы со строками. Создание докумен- та при помощи добавления к нему новых строк — это самый быстрый способ гене- рации ХМ L-кода, особенно в случае, если требуется выполнить только один проход по документу и нет надобности в дальнейшей модификации уже сгенерированных узлов. Даже чтение XML-документов с использованием строковых функций вы- полняется очень быстро, однако если речь идет о достаточно сложных структурах, при разработке соответствующего кода вы можете столкнуться с трудностями. Наконец, следует отметить, что помимо упомянутых стандартных механизмов, доступных также и в других языках программирования, среда разработки Delphi поддерживает еще две дополнительные технологии работы с документами XML. Обе эти методики могут использоваться только в Delphi. Первая технология пре- дусматривает формирование специальных интерфейсов в соответствии с внутрен- ней структурой XML-документа. Эти интерфейсы формируются исходя из струк- туры конкретного документа и предназначаются для работы с данным конкретным документом, на основе которого они сформированы. Другими словами, в отличие от универсального интерфейса DOM, который может использоваться для работы с любыми документами XML, интерфейсы, специально сформированные средой Delphi, предназначены для обработки одного конкретно взятого документа и не могут использоваться совместно с другими документами XML Как мы увидим далее, такой подход позволяет за короткое время разрабатывать более понятный исходный код, снижается вероятность ошибок программирования, а полученное в результате приложение обладает большей надежностью. Второй подход, предла- гаемый в рамках Delphi, предусматривает использование трансформаций, при помощи которых содержимое документа XML можно разместить в компоненте ClientDataSet или, наоборот, сохранить содержимое компонента ClientDataSet в фай- ле XML с заданной структурой. Эта структура может отличаться от специальной XML-структуры, поддерживаемой ClientDataSet или MyBase. Каждая из упомянутых методик будет рассмотрена в последующих разделах главы Я не буду пытаться ответить на вопрос, какая методика лучшим образом подходит для выполнения тех или иных процедур в отношении документов того или иного типа. Вместо этого я постараюсь подчеркнуть преимущества и недо- статки каждого из подходов. 0947
948 Глава 22. Использование технологий XML Программирование с использованием DOM XML-документ — это древовидная структура. Если мы хотим выполнить обработ- ку этого документа, первое, что приходит в голову, — сформировать в памяти точ- но такую же древовидную структуру и заполнить ее данными, извлеченными из XML-документа. Именно эту задачу решает технология DOM (Document Object Model). DOM — это стандартный интерфейс, поэтому, если вы написали код с ис- пользованием DOM, вы можете менять используемую вами реализацию DOM, не меняя исходного кода (естественно, такое возможно только в случае, если вы не используете нестандартных расширений). Работая в Delphi, вы можете установить несколько реализаций DOM, доступ к которым будет происходить, как к серверам СОМ. Таким образом, работа с эти- ми реализациями DOM станет осуществляться через COM-интерфейсы. В среде Windows наиболее популярной реализацией DOM является разработанная ком- панией Microsoft библиотека MSXML, которая входит в состав MSXML SDK, а так- же устанавливается в составе Internet Explorer (и поэтому присутствует во всех последних версиях Windows) и многих других приложений Microsoft. (Следует иметь в виду, что в составе MSXML SDK помимо самой библиотеки присутству- ют документация и примеры использования.) В состав Delphi 7 входят также дру- гие реализации DOM, в частности, продукт Xerces, разработанный группой Apache Foundations, а также продукт с открытым кодом OpenXML. СОВЕТ------------------------------------------------------ OpenXML — это реализация Object Pascal DOM, написанная на Object Pascal. Ее можно загрузить по адресу www.philo.de/xml. Еще одна реализация DOM, написанная на Delphi, разработана компанией TurboPower. Оба этих решения обладают двумя значительными преимуществами: во-первых, для выполнения программы, написанной с их использованием, не требуется прилагать к ней внешней библиотеки, так как компонент DOM компонуется внутри вашего приложения. Кроме того, написан- ный таким образом код будет кросс-платформенным. Среда Delphi включает реализацию DOM в состав компонента-оболочки под названием ХМLDocument. В рассмотренном ранее примере XmLEditOne я уже исполь- зовал этот компонент, однако сейчас я планирую более подробно описать его фун- кционирование. Основная идея состоит в том, что вместо обращения к интерфей- су DOM вы работаете с набором упрощенных вспомогательных методов и, таким образом, остаетесь еще более независимыми от конкретной реализации DOM. На практике пользоваться интерфейсом DOM не так уж и просто. Документ XML является набором узлов (разделов), каждый из которых обладает именем, текстовым элементом, набором атрибутов и набором дочерних узлов. Набор атри- бутов и набор дочерних узлов хранятся в соответствующих коллекциях. Доступ к элементу набора узлов осуществляется либо по имени, либо в соответствии с по- зицией этого элемента. Имейте в виду, что если узел обладает текстовым элемен- том, этот текст рассматривается как один из дочерних узлов данного узла и, таким образом, перечисляется среди прочих дочерних узлов данного узла в составе соот- ветствующей коллекции. Корневой узел поддерживает дополнительные методы, позволяющие создавать новые узлы, значения или атрибуты. С компонентом ХМLDocument вы можете работать на одном из двух уровней: О Более низкий уровень предусматривает использование свойства DOMDocument. Это свойство принадлежит к типу IDOMDocument и обеспечивает доступ к стан- 0948
Программирование с использованием РОМ 949 дартному интерфейсу W3C Document Object Model. Официальный интерфейс DOM определяется в модуле xmldom и включает в себя такие интерфейсы, как IDOMNode, IDOMNodeList, IDOMAttr, IDOM Element и IDOMText. Благодаря поддержке стандартных интерфейсов DOM среда Delphi позволяет работать с низкоуров- невой, но стандартной программной моделью DOM. Обратите внимание на то, что идентифицировать конкретную реализацию DOM можно при помощи свой- ства DOMVendor компонента XMLDocument. О На более высоком уровне вы можете воспользоваться интерфейсом IXMLDo- cument компонента XMLDocument. Этот интерфейс подобен стандартному ин- терфейсу DOM, однако разработан компанией Borland и определен в модуле XMLIntf. В его состав входят такие интерфейсы, как IXMLNode, IXMLNodeList и IXMLNodeCollection. Данный интерфейс, разработанный компанией Borland, упрощает выполнение некоторых операций DOM, так как заменяет множествен- ные последовательно выполняемые обращения к стандартным вызовам DOM единственным методом или свойством. В рассматриваемых далее примерах (особенно в примере DomCreate) я исполь- зую оба подхода, благодаря чему читатели смогут познакомиться с различиями этих двух методик. Отображение документа XML в элементе управления TreeView Как правило, работа с XML начинается с загрузки документа из файла или созда- ния его на основе символьной строки. Вы также можете начать с создания совер- шенно нового XML-документа. Первым примером, который я планирую рассмот- реть, является программа загрузки XML-документа в DOM и отображения его структуры при помощи элемента управления TreeView. Программа называется Xml- DomTree. Чтобы продемонстрировать читателям код доступа к данным DOM, я до- бавил в программу несколько кнопок, при использовании которых осуществляет- ся доступ к элементам прилагаемого к программе тестового XML-файла. Загрузка документа выполняется относительно несложно, однако чтобы отобразить его в ви- де дерева, требуется написать рекурсивную функцию, при помощи которой осу- ществляется последовательный перебор узлов и подузлов. Приведу исходный код двух методов: procedure TFormXmTTree.btnLoadClick(Sender: TObject); begin OpenOialogl ImtialOir := ExtractFilePath (Applicatlon.ExeName); if OpenOialogl.Execute then begin XMLOocumentl.LoadF romF11e(OpenOia1ogl.Fi1 eName); T reeviewl.Iterns.Clea r; DomToTree (XMLOocumentl.DocumentElement. nil); T reeViewl.Full Expand: end: end; procedure TFormXmlTree.DomToTree (XmlNode: IXMLNode; TreeNode: TTreeNode): var I: Integer: 0949
950 Глава 22. Использование технологий XML NewTreeNode TTreeNode. NodeText string AttrNode IXMLNode begin // пропускаем текстовые узлы и другие специальные случаи if not (XmlNode NodeType = ntElement) then Exit. // добавляем собственно узел NodeText = XmlNode NodeNare if XmlNode IsTextElement the NodeText = NodeText + ' = + XmlNode Nodevalue NewTreeNode = TreeViewl Items AddChildtTreeNode NodeText). // добавляем атрибуты for I = 0 to xmlNode AttributeNodes Count - 1 do begin AttrNode = xmlNode AttributeNodes Nodes[I] TreeViewl Items AddChild(NewTreeNode, ' [‘ + AttrNode NodeName + ' = «' + AttrNode Text + ’«]’), end // добавляем каждый дочерний узел if XmlNode HasChildNodes then for I =0 to xmlNode ChildNodes Count - 1 do DomToTree (xmlNode ChildNodes Nodes [I]. NewTreeNode). end Данный код представляет определенный интерес, так как он демонстрирует выполнение некоторых операций с использованием механизмов DOM Прежде всего, следует отметить, что каждый узел обладает свойством NodeType, при помо- щи которого можно определить, является ли узел элементом, атрибутом, тексто- вым узлом или специальной записью, такой как CDATA или другие ей подобные. Также следует учесть, что вы не можете получить доступ к текстовому представле- нию узла, то есть его свойству NodeValue, если этот узел не обладает текстовым элементом (обратите внимание на то, что текстовый элемент пропускается). Пос- ле того как программа отображает имя узла, а затем его текстовое значение (если такое есть), она показывает содержимое каждого атрибута, а затем для каждого подузла рекурсивно обращается к методу DomToTree (рис. 22.2). Загрузив XML-документ в компонент XMLDocument, вы можете использовать разнообразные методы для получения доступа к различным узлам этого докумен- та. При этом вы можете ссылаться на узлы документа как по имени, так и по по- рядковому номеру. Например, если вы загрузите в программу XmlDomTree тесто- вый документ, содержимое которого показано в листинге 22.1, вы можете получить значение атрибута text корневого узла при помощи выражения: XMLDocumentl DocumentElement Attributes['text'] Обратите внимание на то, что если атрибут text в корневом узле документа от- сутствует, обращение к вызову закончится неудачно, при этом на экране появится весьма туманное сообщение об ошибке Invalid variant type conversion (преобразова- ние типа с использованием неправильного варианта). Согласитесь, что подобное сообщение не может помочь ни вам, ни конечному пользователю Если же вы хо- тите получить доступ к первому атрибуту корневого узла документа, не зная име- ни этого атрибута, вы можете использовать следующий, более универсальный код: XMLDocumentl DocumentElement AttributeNodes Nodes[Q] NodeValue 0950
Программирование с использованием РОМ 951 books [text = Books") Ч book tilie » Mastering Delphi 7) author = Cantu -1 book title = Delphi Developei’s Handbook author = Cantu author = Gooch Я book title = Mastering Delphi 6 author = Cantu 4 book Я book 4 ebook title • Essential Pascal url = http //www marcocantu com author = Cantu ebook title = Thinking in Java url = http //www mindview com author = Eckel Рис. 22.2. Программа XmlDomTree может загрузить любой XML-документ и отобразить его с использованием компонента TreeView Чтобы работать собственно с узлами, необходимо использовать похожую ме- тодику. При этом удобно использовать массив ChildValues. Это поддерживае- мое в Delphi расширение стандарта DOM, которое позволяет обращаться к эле- менту исходя из его имени либо из его позиции: XMLOocumentl DocumentElement ChildNodes Nodesll] ChildValues['author ] Данное выражение возвращает (первого) автора второй книги. Выражение Child Va lues ['book'] использовать нельзя, так как в корневом узле существует несколь- ко подузлов с именем book. Листинг 22.1. Тестовый XML-документ, используемый программами, рассматриваемыми в данной главе <7xml version=“l 0’ encoding=''UTF-8 7> «books text= Books''> <book> <title>Mastering Delphi 7</title> <author>Cantu</author> </book> <book> <title>Delphi Developer's Handbook</title> <author>Cantu</author> <author>Gooch</author> </book> <book> <title>Delphi COM Programming</title> <author>Harmon</author> </book> . л продолжение 6' 0951
952 Глава 22. Использование технологий XML Листинг 22.1 (продолжение} <Ьоок> <title>Thinking in C++</title> <author>Eckel</author> </book> <ebook> <title>Essential Pascal</title> <url>http //www marcocantu com</url> <author>Cantu</author> </ebook> <ebook> <title>Thinking in Java</title> <url>http //www mindview com</url> <author>Eckel</author> </ebook> </books> Создание документов с использованием DOM Ранее я уже упоминал о том, что документ XML можно создать, связав воедино несколько строк, однако такую технологию нельзя назвать надежной. Напротив, если для создания XML-документа вы будете использовать DOM, вы можете быть уверены в том, что этот документ будет корректно отформатированным. Кроме того, если механизм DOM снабдить определением схемы, в процессе добавления данных к документу вы сможете осуществлять проверку корректности структуры этого документа. Разработанный мною пример DomCreate иллюстрирует создание документов XML. Программа может создавать документы XML в рамках DOM, а затем пока- зывать их текст в элементе Мето и, при желании, в TreeView. СОВЕТ --------------------------------------------------------- Чтобы улучшить вывод текста XML-документа при помощи Мето, вы можете использовать пара- метр doAutoIndent компонента XMLDocument. При этом в процессе вывода будут использоваться отступы, и, таким образом, документ XML будет оформлен в несколько более удобном виде. Тип отступа определяется при помощи свойства NodelndentStr. Чтобы отформатировать текст XML, можно использовать также глобальную функцию FormatXMLData. При этом будет использоваться стандарт- ный отступ в два пробела. К сожалению, передать в функцию какое-либо другое значение отступа, по всей видимости, нельзя. Чтобы создать какой-либо простой текст XML при помощи официальных (низ- коуровневых) интерфейсов DOM, необходимо щелкнуть на кнопке Simple (про- стой текст) программы. Для каждого узла программа обращается к методу createElement документа. При этом новые узлы добавляются в качестве дочерних: procedure TForml btnSimpleClick(Sender TObject) var iXml IDOMDocument. iRoot iNode, ihlode2 iChi Id, iAttribute IDOMNode, begin // очищаем документ XMLDoc Active = False. XMLDoc XML Text = ''. XMLDoc Active = True. 0952
Программирование с использованием РОМ 953 // корень 1 Xml = XmlDoc DOMDocument, iRoot = iXml appendChild (iXml createElement ('xml')), // узел “test" iNode = iRoot appendChild (iXml createElement ('test')). iNode appendChild (iXml createElement ('test2')), iChild = iNode appendChild (iXml createElement ('test3')), iChi Id appendChild (iXml createTextNode('simple value')). iNode insertBefore (iXml createElement ('test4') iChild), // репликация узла iNode2 = iNode cloneNode (True), iRoot appendChild (iNode2) Il добавляем атрибут lAttribute = iXml createAttribute ('color ). lAttribute nodevalue = 'red', iNode2 attributes setNamedltem (lAttribute) /) показываем XML в Memo Memol Lines Text = FormatXMLData (XMLDoc XML Text), end Обратите внимание, что узлы, являющиеся текстом, добавляются явно, атри- буты создаются при помощи специального вызова, а для репликации некоторой ветви дерева используется вызов cloneNode. В целом данный код может показаться громоздким, однако со временем вы привыкнете к подобному стилю. Результат работы программы (текст XML, отображенный при помощи Мето и TreeView) пока- зан на рис. 22.3. DomCreate Bottom <xml> <test> <test2/> <test4/> < test 3> simple value</te$t3> <Aest> <test color="fed '> <te$t2/> <test4/> <te$t3> simple value</test3> </test> </xml> P, xml Ь test test2 test4 test3 « simple value IS lest [color» "red"] test2 test4 te$t3 « simple value Рис. 22.3. Программа DomCreate может создавать разнообразные типы документов XML при помощи DOM 0953
954 Глава 22. Использование технологий XML Еще один пример создания XML-документов при помощи DOM основан на применении набора данных. Я добавил в форму набор данных dbExpress (на самом деле в данном случае можно использовать любой другой набор данных), а также снабдил программу кнопкой, которая вызывает написанную мной процедуру Data- SetToDOM, например, следующим образом: DataSetToDOM('customers'. 'customer'. XMLDoc SQLDataSetl). Процедура DataSetToDOM создает корневой узел с текстом, указанным в первом параметре, затем извлекает из набора данных последовательно запись за записью, создает узел, соответствующий второму параметру, а затем создает подузел для каждого поля записи. При этом используется чрезвычайно универсальный код: procedure DataSetToDOM (RootName. RecordName string, XMLDoc TXMLDocument. DataSet TDataSet). var iNode iChi Id IXMLNode. i Integer. begin DataSet Open. DataSet First, // корень XMLDoc DocumentElement = XMLDoc.CreateNode (RootName). // добавляем табличные данные while not DataSet EOF do begin // для каждой записи добавляем узел iNode = XMLDoc DocumentElement AddChild (RecordName). for I =0 to DataSet FieldCount - 1 do begin // для каждого поля добавляем элемент iChild - iNode AddChild (DataSet Fieldsli] FieldName). - iChild Text = DataSet FieldsEi] AsString. end. DataSet Next. end; end. Приведенный здесь код основан на использовании упрощенных интерфейсов DOM, разработанных компанией Borland. Эти интерфейсы поддерживают метод AddChild, позволяющий создать подузел, а также прямой доступ к свойству Text, благодаря которому можно определить дочерний узел с текстовым содержимым. Такая, на первый взгляд простая, процедура формирует XML-представление дан- ных, хранящихся в компоненте DataSet. Позже, в разделе, посвященном XSL, я про- демонстрирую, что эта возможность открывает перед разработчиками широкие перспективы, связанные с публикацией данных в Веб. Еще одна интересная возможность — это генерация XML-документов, описы- вающих объекты Delphi. Программа DomCreate обладает кнопкой, с помощью кото- рой можно сформировать XML-описание нескольких избранных свойств объекта. При этом снова используется низкоуровневый метод работы с DOM: procedure AddAttг (iNode IDOMNode. Name. Value string). var lAttr IDOMNode 0954
Программирование с использованием РОМ 955 begin lAttr = iNode ownerDocument createAttribute (name). lAttr nodeValue = Value iNode attributes setNamedltem (lAttr). end procedure TForml btnObjectClick(Sender TObject): var iXml IDOMDocument. iRoot IDOMNode, begin // очищаем документ XMLDoc Active = False, XMLDoc XML Text = '' XMLDoc Active = True // корень iXml = Xml Doc DOMDocument. iRoot = iXmI appendChild (iXml createElement ('Buttonl')), // несколько свойств в качестве атрибутов // (вместо атрибутов можно добавить также узлы) AddAttr (iRoot, 'Name'. Buttonl Name). AddAttr (iRoot, 'Caption'. Buttonl Caption) AddAttr (iRoot, ‘Font Name'. Buttonl Font Name). AddAttr (iRoot, 'Left'. IntToStr (Buttonl Left)); AddAttr (iRoot, 'Hint' Buttonl Hint), // показываем XML в Memo Memol Lines = Xml Doc XML. end Конечно же, было бы удобно использовать универсальную технологию, позволя- ющую сохранять свойства любого компонента Delphi (говоря точнее, любого объек- та, поддерживающего постоянное сохранение в памяти), рекурсивно обращаясь к каждому постоянному подобъекту и отображая имена компонентов, к которым про- исходит обращение. Именно это осуществляется в процедуре ComponentToDOM, которая использует низкоуровневую информацию RTTI, предоставляемую модулем Typlnfo. В составе данных, получаемых от Typlnfo, содержится перечень свойств компонента. В данном случае программа использует упрощенные интерфейсы Delphi XML: procedure ComponentToDOM (iNode IXmlNode. Comp TPersistent). var nProps i Integer. PropList PPropList, Value Variant. newNode IXmlNode begin // получить список свойств nProps - GetTypeData (Comp Classinfo)* PropCount, GetMem (PropList. nProps * SizeOf(Pointer)), try GetPropInfos (Comp Classinfo PropList). for i =0 to nProps - 1 do begin Value = GetPropValue (Comp PropList [i] Name). NewNode = iNode AddChild(PropList [i] Name). NewNode Text = Value. 0955
956 Глава 22. Использование технологий XML if (PropList [i.PropTypeA.Кт nd = tkClass) and (Value <> 0) then if TObject (Integer(Value)) is TComponent then NewNode.Text : = TComponent (Integer(Value)) Name else // TPersistent но не TComponent: рекурсия ComponentToDOM (newNode. TObject (Integer(Value)) as TPerSTStent); end; finail у FreeMem (PropList); end: end: В данном случае создание XML-документа (отображенного на рис. 22.4) ини- циируется следующими двумя строками кода: XMLDoc DocumentElement XMLDoc CreateNode(Self ClassName). ComponentToDOM(XMLDoc.DocumentElement, Seif). DomCreate <TFoim1> <Name)Form1</Name> <Left>192</Left> <Top>107</Top> (Width) 571 </Width) < Height) 412< /Height) <Но(г5сгоНВаг>2026С644 (Range) 97(/Range> (/HorzScrollBar) <VertScrollBar>20260720<A/ertScroUBar> (ActiveControl) btnR T Tl < /ActiveControl) <B<DiMode)bdLeftToRight</BiDiMode> < Caption) DomCreate< /Caption) < ChentH eight) 385</ClientH eight) TForml Name = Forml Left = 192 Top = 107 Width = 571 Height = 412 H HorzScrollBar Range = 97 VertScroHBar = 20260720 ActiveControl = btnRTTI BiDiMode = bdLeftToRight Caption = DomCreate ClientH eight = 385 ClientWidth = 563 Color = -16777201 Constraints = 20255360 ЕЭ Foot Charset = 1 Color = -16777208 Height = -11 Рис. 22.4. Документ XML, сгенерированный программой DomCreate. Обратите внимание на то, что свойства классов можно развернуть Интерфейсы, сформированные на основе XML-кода Работа с XML при помощи DOM несколько утомительна, так как приходится об- ращаться к данным, исходя из их расположения в файле, а не основываясь на их логическом смысле. Мало того, обработка многочисленных узлов, которые могут 0956
Программирование с использованием РОМ 957 принадлежать к разным типам, как правило, выполняется непросто. Наконец, если вы используете DOM для создания документов, вы можете быть уверены только в том, что создаваемый вами документ хорошо оформлен, однако (если только вы не используете специальных механизмов DOM, осуществляющих проверку коррек- тности схемы) при этом вы можете добавлять любые подузлы в состав любых узлов, что может привести к формированию фактически бесполезных XML-документов, которыми не сможет пользоваться никто, кроме вас. Чтобы решить все эти проблемы, компания Borland добавила в Delphi мастер связывания данных XML (XML Data Binding Wizard). Этот мастер анализирует струк- туру XML-документа или определение документа (схему, DTD или какое-либо другое определение) и генерирует набор интерфейсов для работы с данным доку- ментом. Сгенерированные мастером интерфейсы являются специфическими для данного документа и основаны на его внутренней структуре. Благодаря их исполь- зованию удается получить более понятный, более читаемый код, однако они менее универсальны и позволяют работать только с XML-документами определенного типа. Вместе с тем, в данном случае отсутствие универсальности — это во многом преимущество, а не недостаток. Чтобы активизировать мастер связывания данных XML, можно либо восполь- зоваться соответствующим значком на первой странице диалогового окна New Items (Новые элементы), либо сделать двойной щелчок на компоненте XMLDocument (странно, однако соответствующая команда отсутствует в локальном меню этого компонента). XML Data Binding Wizard Complex Types •4 SB booksType ® text H book [Ж1 title ^author S (S3 title url author i+ bookType ЕЁЙ ebookType Simple Types ® string Soutca Namet Source Datatype ЁосмтепЛп: |ebook |ebookfype 0phons < Previous i Next > 1 Cancel I Рис. 22.5. Мастер связывания данных XML (XML Data Binding Wizard) исходя из структуры документа или его схемы (или другого определения) создает набор специальных интерфейсов для упрощенного и прямого доступа к данным DOM Первая страница мастера позволяет выбрать входной файл, после чего мастер показывает вам графическое представление структуры содержащегося в этом файле XML-документа. Отображение структуры тестового документа из листинга 22.1 показано на рис. 22.5. При помощи данной страницы вы можете присвоить имя 0957
958 Глава 22. Использование технологий XML каждому элементу формируемых интерфейсов. Это надлежит сделать в случае, если вас не удовлетворяют имена, предлагаемые мастером по умолчанию Помимо это- го вы можете также изменить используемые мастером правила генерации имен — чрезвычайно удобная возможность, которая, к сожалению, отсутствует в других частях Delphi IDE. Назавершающей странице мастера предлагается предваритель- ный просмотр структуры интерфейсов, формируемых мастером, а также дополни- тельные параметры генерируемых схем и файлов определений. Для тестового XML-документа с описанием книг и авторов мастер XML Data Binding Wizard генерирует интерфейс для корневого узла, два интерфейса для спис- ков элементов двух различных типов узлов (книги и электронные книги), а также два интерфейса для элементов двух типов. Приведу фрагменты сгенерированного кода из модуля XmllntfDefinition примера Xmlinterface: type IXMLBooksType = interface(IXMLNode) [ ‘ {C9A9FB63-47ED-4F27-BABA-E71F30BA7F11} 'J { Property Accessors } function Get_Text WideString function Get_Book IXMLBookTypeList. function Get_Ebook IXMLEbookTypeList procedure Set_Text(Value WideString) { Methods & Properties } property Text WideString read Get_Text write Set_Text. property Book IXMLBookTypeList read Get_Book property Ebook IXMLEbookTypeList read Get-Ebook end IXMLBookTypeList = interfaceCIXMLNodeCollection) [ {3449EBC4-3222-47B8-B2B2-3BEE504790B6}'J { Methods & Properties } function Add IXMLBookType function Insert(const Index Integer) IXMLBookType function Get_Item(Index Integer) IXMLBookType property Items[Index Integer] IXMLBookType read Get_Item default. end ' IXMLBookType = interface(IXMLNode) ['{26BF5C51-9247-4D1A-8584-24AE68969935)‘J { Property Accessors } function Get_Title WideString function Get_Author IXMLString_List procedure Set_Title(Value WideString) { Methods & Properties } property Title WideString read Get_Title write Set_Title property Author IXMLString_List read Get_Author end Для каждого интерфейса мастер генерирует также класс реализации, в котором содержится код входящих в состав интерфейса методов. В основе этих методов лежат обращения к стандартному интерфейсу DOM. Модуль включает в себя три функции инициализации. Первая из них возвращает интерфейс корневого узла документа, загруженного в компонент XMLDocument (или в любой другой компо- нент, обладающий интерфейсом IXMLDocument), вторая возвращает интерфейс XML-документа, содержащегося в файле, а третья создает новый DOM: function Getbooks(Doc IXMLDocument) IXMLBooksType 0958
Программирование с использованием РОМ 959 function Loadbooks(const FileName WideString) IXMLBooksType, function Newbooks IXMLBooksType, После того как мастер сгенерировал интерфейсы, специально предназначен- ные для работы с конкретным ХМ L-документом, работать с этим документом ста- новится значительно легче. В частности, код доступа к тестовому XML-докумен- ту, аналогичный коду из примера XmLDomTree, разрабатывается существенно проще и выглядит более понятным. Например, чтобы получить атрибут корневого узла, необходимо воспользоваться следующим фрагментом кода: procedure TForml btnAttrClick(Sender TObject). var Books IXMLBooksType begin Books = Getbooks (XmlDocument1). ShowMessage(Books Text) end Просто, не правда ли? Набирая подобный код, вы можете с успехом воспользо- ваться механизмом Code Insight, при помощи которого сможете получить пере- чень свойств каждого узла. Эта возможность доступна благодаря тому, что модуль грамматического разбора может читать определения интерфейсов, однако при этом его нельзя использовать для работы с абсолютно любыми документами XML. До- ступ к узлу, входящему в состав одного из подсписков, осуществляется при помо- щи одного из следующих выражений (скорее всего второго, так как в нем исполь- зуется свойство-массив по умолчанию): Books Book Items[l] Title //полная форма Books Book[l] Title //сокращенная форма Аналогичный упрощенный код может использоваться для генерации новых документов или добавления в них новых элементов. Добавление новых элементов выполняется при помощи специального метода Add, входящего в состав каждого основанного на списке интерфейса. Хочу вновь заострить ваше внимание на том, что если документы, с которыми вы работаете, не обладают заранее заданной струк- турой (как в рассмотренных ранее примерах, основанных на DataSet и RTTI), вы не сможете использовать данный подход. Проверка корректности и схемы Мастер XML Data Binding Wizard может работать с существующими схемами, а может сгенерировать схему на основе некоторого XML-документа, а затем, в случае необ- ходимости, даже записать зту схему в файл с расширением .XDB. Но что такое схе- ма и зачем она нужна? Дело в том, что документ XML описывает некоторые дан- ные, однако для того, чтобы две компании смогли обмениваться этими данными, требуется, чтобы они использовали в своих документах общую, известную обеим этим компаниям структуру. Информация о правилах, в соответствии с которыми составлен некоторый документ XML, содержится в схеме (schema). Схема (sche- ma) — это определение документа, используя которое, можно выполнить провер- ку корректности документа. Процедура проверки корректности обозначается ан- глийским термином validation, который можно перевести на русский язык как «ратификация». 0959
960 Глава 22. Использование технологий XML До последнего времени для проверки корректности документов XML часто использовались методы, основанные на применении формата DTD (Document Туре Definition). Этот формат и в настоящее время широко распространен, однако он обладает рядом недостатков. Документы DTD описывают структуру XML, но не содержат сведений о возможном содержимом каждого узла. Кроме того, докумен- ты DTD сами по себе не являются документами XML, при их составлении исполь- зуется иной, весьма неуклюжий синтаксис. В конце 2000 года консорциумом W3C был одобрен первый официальный на- бросок стандарта схем XML. Поддержка этого стандарта уже добавлена в состав Microsoft DOM под именем XML-Data, однако она плохо совмещается с офици- альной версией стандарта. Схема XML является XML-документом специального вида. При помощи схемы можно убедиться как в корректности внутренней струк- туры ХМ L, так и в правильности содержимого входящих в состав XML узлов. Стан- дарт схем XML базируется на определении и использовании простых и сложных типов данных. С определением типов часто приходится иметь дело в объектно- ориентированном программировании. В схеме определяются сложные типы данных, указывается, какие узлы могут принадлежать тому или иному типу, при желании можно указать допустимую по- следовательность узлов (sequence, all), количество вхождений в состав документа для каждого из подузлов (minOccurs, maxOccurs), а также тип данных каждого конк- ретного элемента. Приведу пример схемы, составленной мастером XML Data Binding Wizard для рассмотренного ранее тестового ХМL-документа, содержащего инфор- мацию о книгах: <?xml version="1.0"?> <xs:schema xml ns:xs="http://www.w3.org/2001/XMLSchema” xml ns:xdb="http://www.borland.com/schemas/delphi/7.0/XMLDataBInding"> <xs:element name="books" type="booksType"/> <xs:complexType name-"booksType"> <xs:annotation> <xs:appi nfo xdb:docElement="books"/> </xs:annotation> <xs.sequence> <xs:element name="book" type="bookType" maxOccurs="unbounded"/> <xs:element name="ebook" type=''ebookType" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="text" type="xs:string"/> </xs:complexType> <xs:complexType name="bookType"> <xs:annotation> <xs:appinfo xdb:repeated="True"/> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element name="author" type-"xs:string" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="ebookType"> <xs.-annotation> <xs:appinfo xdb:repeated="True"/> </xs:annotation> <xs:sequence> <xs:element name="title" type="xs:String"/> 0960
Программирование с использованием РОМ 961 <xs:element name="url" type=”xs:string'7> <xs.element name="author” type=”xs:string'7> </xs:sequence> </xs:complexType> </xs:schema> Хорошая поддержка схем XML встроена в состав Apache Xerces DOM. Помимо этого солидной поддержкой схем обладает также реализация DOM компании Microsoft. Еще одним инструментом, который можно использовать для проверки корректности XML, является XSV (XML Schema Validator). Это программное сред- ство с открытым кодом является неплохим механизмом обработки XML-кода с поддержкой схем. Его можно использовать как напрямую через Веб, так и после загрузки исполняемого файла, запускаемого из командной строки (ссылку на до- машний веб-узел этого инструмента можно найти на веб-страницах W3C XML Schema). ПРИМЕЧАНИЕ -------------------------------------------------------------------------- Редактор Delphi поддерживает завершение кода для файлов XML через DTD. Однако официально эта возможность не поддерживается компанией Borland. Если вы хотите воспользоваться этим ме- ханизмом, скопируйте DTD-файл в подкаталог bin среды Delphi и добавьте ссылку на этот файл при помощи маркера DOCTYPE. Использование SAX API Механизм SAX (Simple API for XML) не предназначен для генерации дерева уз- лов XML. Вместо этого SAX последовательно просматривает передаваемый ему код XML и генерирует события для каждого узла, атрибута, значения и т. п. При использовании SAX загрузка всего XML-документа в память не требуется, благо- даря этому при помощи SAX можно осуществлять обработку очень больших по размеру документов XML. Методика, основанная на использовании SAX, чрезвы- чайно удобна, если документ XML требуется просмотреть только один раз. Кроме того, SAX удобно использовать в случае, если из документа XML требуется из- влечь некоторую специфическую информацию. Вот список наиболее важных собы- тий, которые генерируются механизмом SAX в ходе просмотра XML-документа: О StartDocument и EndDocument для всего документа; О StartELement и End Element для каждого из узлов; О Charasters для содержащегося в узлах текста. При просмотре документа с использованием SAX зачастую удобно сформиро- вать стек, в котором хранится текущий путь в дереве узлов XML. При обработке события StartELement в стек добавляется очередной элемент, а при возникновении события EndELement элемент из стека удаляется. В состав Delphi специальная поддержка технологии SAX не включается, одна- ко работать с SAX можно при помощи библиотеки MSXML, предлагаемой компа- нией Microsoft. При разработке примера SaxDemol я использовал MSXML v2. Я сгенерировал Pascal-версию импортируемого модуля библиотеки типов на ос- нове библиотеки типов. Этот модуль содержится в исходном коде программы, од- нако для того, чтобы программа смогла нормально работать на компьютере, вы должны зарегистрировать в системе соответствующую библиотеку СОМ. 0961
962 Глава 22. Использование технологий XML ПРИМЕЧАНИЕ--------------------------------------------------------------- Использование SAX API демонстрируется также в другом примере, который рассматривается в кон- це данной главы (LargeXml). Помимо прочего этот пример демонстрирует использование механизма OpenXml. Чтобы воспользоваться SAX, необходимо установить в составе читающего мо- дуля SAX обработчик событий SAX, а затем загрузить файл и выполнить его грам- матический анализ. Я предпочитаю использовать интерфейс читающего модуля SAX библиотеки MSXML, предназначенный для программистов VB. Это связано с тем, что официальный интерфейс C++ содержит несколько ошибок, и из-за этого его нельзя корректно импортировать в Delphi. В главной форме примера SaxDemol я объявляю: sax IVBSAXXMLReader В методе FromCreate эта переменная инициализируется реальным СОМ-объек- том: sax = CreateComObject(CLASS_SAXXMLReader) as IVBSAXXMLReader sax ErrorHandler = TMySaxErrorHandler Create Так же осуществляется настройка обработчика ошибок, являющегося классом, поддерживающим специальный интерфейс IVBSAXErrorHandLer, в состав которого входят три метода, соответствующих уровню серьезности возникшей ошибки: error, fatalError и ignorableWarning. Чтобы несколько упростить код, я активирую модуль грамматического разбора при помощи метода parseURL. Перед этим я сообщаю модулю SAX о том, какой объект должен выполнять обработку содержимого: sax ContentHandler = TMySaxHandler Create sax parseURL(filename) Таким образом, весь код обработки содержимого XML полностью располагает- ся в классе TMySaxHandler. Именно этот класс выполняет обработку событий SAX. В данном примере я использую несколько разных обработчиков содержимого, по- этому мною разработан базовый класс, в котором содержится основной код, а за- тем на основе этого класса формируются несколько специализированных версий для выполнения специальной обработки данных. Вот исходный код базового класса, поддерживающего одновременно интерфейсы IDispatch и IVBSAXContentHandler. type TMySaxHandler = class (TInterfacedObject IVBSAXContentHandler) protected stack TStrmgList public constructor Create destructor Destroy override // IDispatch function GetTypeInfoCount(out Count Integer) HResult stdcall function GetTypeInfo(Index LocalelD Integer out Typeinfo) HResult stdcall function GetIDsOfNames(const IID TGUID Names Pointer NameCount LocalelD Integer DispIDs Pointer) HResult stdcall. function Invoke(DispID Integer const IID TGUID LocalelD Integer. Flags Word var Params VarResult Exceplnfo ArgErr Pointer) HResult stdcall 0962
Программирование с использованием РОМ 963 // IVBSAXContentHandler procedure Set_documentLocator(const Paraml IVBSAXLocator). virtual safecall procedure startDocument virtual safecall procedure endDocument virtual safecall procedure startPrefixMapping(var strPrefix WideString var strURI WideString) virtual safecall procedure endPrefixMapping(var strPrefix WideString) virtual, safecall, procedure startElement(var strNamespacellRI WideString var strLocalName WideString var strQName WideString const oAttributes IVBSAXAttributes) virtual; safecall: procedure endElement(var strNamespaceURI WideString var strLocalName WideString var strQName WideString) virtual safecall procedure characters(var strChars WideString) virtual safecall. procedure ignorableWhitespace(var strChars WideString) virtual safecall procedure processinglnstruction(var strTarget WideString var strData WideString) virtual safecall procedure skippedEntity(var strName WideString) virtual safecall end Наиболее интересной является, конечно же, завершающая часть кода, в кото- рой содержится список событий SAX Рассматриваемый базовый класс выполня- ет совсем не сложный набор операций, он заносит в журнал сообщения о начале грамматического разбора (startDocument), его завершении (endDocument), а кроме того, следит за текущим местоположением на иерархическом дереве узлов при помощи стека И TMySaxHandler startElement stack Add (strLocalName) // TMySaxHandler endElement stack Delete (stack Count - 1) Реализация этих процедур содержится в классе TMySimpt.eSaxHandt.er, где пере- определяется обработчик события startEvent. Этот обработчик выполняется для каждого очередного узла и заносит в журнал текущую позицию узла на дереве Эта операция выполняется при помощи выражения Log Add(strLocalName + '( + stack CommaText + ) ) Еще один метод класса — это обработчик события charasters. Событие charasters возникает в момент, когда модуль грамматического разбора SAX встречает значе- ние узла (или тестовый узел). При этом в журнал выводится содержимое узла: procedure TMySimpleSaxHandler characters(var strChars WideString) var str WideString begin inherited str = Removewhites (strChars) if (str <> ") then Log Add ('Text + str) end Результат обработки XML-документа с использованием двух рассмотренных методов показан на рис 22.6. 0963
964 Глава 22. Использование технологий XML SaHDenw 1 Raise В«е | Raise Tates I • staitDocument - books(books) book(books book) title(book$ book title) Text Mastering Delphi 7 authorfbooks bcokzauthor) Text Cantu book(book$zbook) trtlefbooks book title) Text Delphi Developer's Handbook authorfbooks book author) Text Cantu author(book$,book author) Text Gooch bookfbooks book) title(books book title) Text Mastering Delphi 6 authorfbooks book author) Text Cantu book(books book) title(book$ book title) Text Delphi COM Programing authorfbooks book author) Text Harmon book(bookszbook) titlefbooks book title) Text Thinking n C++ authoifbooks book author) Text Eckel Рис. 22.6. Содержимое журнала, сформированное в результате обработки XML-документа с использованием механизма SAX. Пример SaxDemol Рассмотренная операция грамматического разбора XML является универсаль- ной. Ее можно выполнить в отношении любого XML-документа, а обработке под- вергается абсолютно вся информация, содержащаяся в этом документе. Второй рассматриваемый мною обработчик XML, основанный на SAX, выполняет обра- ботку XML-документов, обладающих определенной структурой. Из таких XML- документов извлекаются только узлы определенного типа. Данная программа осу- ществляет поиск узлов типа title. Если узел, анализируемый в рамках обработчика startElement, принадлежит к данному типу, класс присваивает логической перемен- ной isbook значение «истина». Текстовое значение узла анализируется только в случае, если обнаружен узел данного типа: procedure TMyBooksListSaxHandler startElement(var strNamespaceURI strLocalName. strQName WideString const oAttributes IVBSAXAttributes). begin inherited. isbook = (strLocalName = ‘title") end. procedure TMyBooksListSaxHandler characters(var strChars WideString). var str string. begin inherited if isbook then begin str = Removewhites (strChars), if (str <> '') then 0964
Программирование с использованием РОМ 965 Log.Add (stack.Commatext + ': ' + str); end; end. Трансформация XML-документов В Delphi поддерживается еще одна методика обработки XML-документов, кото- рую можно с успехом использовать в некоторых ситуациях. Эта методика подра- зумевает создание трансформации (transformation), при помощи которой содер- жимое XML-документа преобразуется в формат, обрабатываемый компонентом ClientDataSet. При этом данные заносятся в файл MyBase XML. Другая трансфор- мация позволяет преобразовать данные, хранящиеся в ClientDataSet, в XML-файл желаемого формата (или с заданной схемой). При этом используется компонент DataSetProvider. Для генерации таких трансформаций в Delphi используется специальный мас- тер, который называется XML Mapping Tool, или сокращенно XML Mapper. Запустить данный мастер можно из меню Tools (Инструменты), кроме того, этот мастер за- пускается также в виде отдельного приложения. Внешний вид мастера показан на рис. 22.7. Мастер XML Mapper является вспомогательным средством, при помощи которого на этапе проектирования приложения вы можете определить набор пра- вил, согласно которым устанавливается соответствие между узлами некоторого XML-документа и полями пакета данных компонента ClientDataSet. ; SROWDATA\flCM’|\bookSRO SRO'*t>AtA\flO'*1,]\book'fiQ \ROWDATA\ROW1 )@texl SROUttATASRCM hebookkR SROWDAtASBOWl ]\ebook\fl SROWDATALROWl JLebook\R }\books[ hbockLJLauthor !\book»[ )@text Sbooksl Lebock[ N»ke ‘shook sTJLebookfM >\book*[ j\ebook[‘|SaiJthot ' I ... ..... май Datatype* "Sting Mariength* IT Datatype* "Stang1 MarLength- K Datatype* Stmg MaxLength- ?5 Datatype* "Sting MaxLertft- 5 Щ book 4? author — И ebock(') T V urt author "«^оеийвп» «доимСеф нВьосМ S@terf ЬооЩ Datatype* "Stang* Mariength* 27 ** author ftatatype* Stang / Mariength* *6 — Q ebook ДОарасш «gamMfafe Рис. 22.7. Мастер XML Mapper показывает две стороны трансформации, позволяя определить соответствие между ними. Правила соответствия показываются в центральном разделе Окно мастера XML Mapper разделено на три части: О Слева располагается раздел, в котором отображаются сведения о структуре XML-документа. На вкладке Document View (Документ) отображается собствен- но структура XML-документа и в случае, если установлен соответствующий флажок, — содержащиеся в нем данные. На вкладке Schema View (Схема) ото- бражается схема XML-документа. 0965
966 Глава 22. Использование технологий XML О Справа располагается раздел пакета данных, в котором отображается инфор- мация о метаданных, содержащихся в пакете данных. Вкладка Field View (Поля) показывает структуру набора данных, а вкладка Datapacket View (Пакет данных) отображает структуру XML. Обратите внимание на то, что XML Mapper позволя- ет открывать файлы в формате ClientDataSet. О Наконец, центральный раздел содержит информацию о соответствии между элементами XML и ClientDataSet. Этот раздел также состоит из двух вкладок. Вкладка Mapping (Соответствие) показывает соответствие между выбранными справа и слева элементами. Вкладка Node Properties (Свойства узла) позволяет модифицировать типы данных и другие характеристики каждого из соответ- ствий. Вкладка Mapping (Соответствие) центрального раздела рабочего окна мастера содержит локальное меню, позволяющее сгенерировать трансформацию. Каждая из вкладок в других разделах рабочего окна мастера также обладает локальным меню, позволяющим выполнять разнообразные действия. Кроме того, существует общее меню мастера, в котором содержатся несколько команд. При помощи мастера XML Mapper вы можете выполнить одну из следующих за- дач: определить отображение существующей схемы (которую при желании можно извлечь из некоторого документа) на совершенно новый пакет данных; опреде- лить отображение существующего пакета данных на новую схему или документ; определить отображение существующего пакета данных на существующий XML- документ (если в таком соответствии есть необходимость). Помимо преобразова- ния данных из XML-файла в пакет данных вы можете выполнить преобразование этих данных в дельта-пакет компонента ClientDataSet. Такая возможность может понадобиться в случае, если требуется добавить данные из XML-документа в су- ществующую таблицу (например, по мере ввода этих данных пользователем). При этом вы можете трансформировать XML-документ в дельта-пакет для записей, которые модифицируются, удаляются или добавляются. В результате работы мастера XML Mapper создается один или несколько файлов трансформации, каждый из которых соответствует одностороннему преобразова- нию (либо из XML в таблицу, либо из таблицы в XML). Таким образом, чтобы получить возможность преобразовывать данные как в одном, так и в другом на- правлении, вам потребуются минимум два файла трансформации. В дальнейшем данные файлы трансформации используются на этапе проектирования, а также на этапе выполнения компонентами XMLTransform, XMLTransformProvider и XMLTransform- Client. Я решил, что в качестве примера не стоит рассматривать простые задачи, в ко- торых XML обладает удобной для преобразования «прямоугольной» структурой. Вместо этого я попытался сгенерировать отображение рассматривавшегося ранее тестового XML-документа, содержащего информацию о книгах. Структура этого документа плохо вписывается в структуру таблицы, так как в нем существует два списка значений различных типов. Используя раздел XML Document (Документ XML) мастера XML Mapper, я открыл файл Sample.XML, при помощи команды ло- кального меню Select All (Выбрать все) выбрал все элементы XML-кода, а затем сформировал пакет данных при помощи команды Create Datapacket From XML (Со- здать пакет данных на основе XML). При этом правая панель рабочего окна масте- 0966
Программирование с использованием РОМ 967 ра автоматически заполнилась данными, а центральная панель — описанием пред- лагаемой трансформации. Чтобы опробовать эффект трансформации, вы можете воспользоваться кнопкой Create and Test Transformation (Создать и протестировать трансформацию). В результате щелчка на этой кнопке происходит запуск прило- жения, которое осуществляет загрузку документа в набор данных с использовани- ем сформированной вами трансформации. В рассматриваемом нами случае мастер XML Mapper генерирует таблицу с двумя полями наборов данных. Каждому соответствует один из возможных списков по- дэлементов. Предложенная мастером трансформация является единственным воз- можным стандартным решением в данной ситуации, так как каждый из подспис- ков обладает своей собственной структурой. Кроме того, данное решение позволяет редактировать данные при помощи компонента DBGrid, присоединенного к компо- ненту ClientDataSet, а затем записывать эти данные в полноценный XML-файл. Эта процедура демонстрируется в примере XmlMapping. Программа XmlMapping факти- чески является Windows-редактором сложных XML-документов. Чтобы прочитать XML-документ и сделать его доступным для ClientDataSet, программа использует компонент TransformProvider с двумя присоединенными к нему файлами трансформации. Как видно из имени, этот компонент является провайдером доступа к набору данных (dataset). Я не стал напрямую соединять ClientDataSet с компонентом DBGrid и вместо этого добавил в программу два допол- нительных компонента ClientDataSet, соединенных с полями набора данных и взаи- модействующих с двумя элементами управления DBGrid. Чтобы понять структуру программы, рекомендую изучить исходный код DFM (фрагмент которого приве- ден ниже) и обратить внимание на рис. 22.8. XmlMapping title......................... Delphi Developer's Handbook Delphi COM Programming Thinking in C++ Mastering Delphi 7 Mastering Delphi 6 Delphi Power Book} |aih$r| Cantu Harmon 8ruce Cantu Cantu Thinking in Java Essential Pascal http //www mindview com Eckel http //www marcocantu com Cantu Рис. 22.8. При помощи компонента TransformProvider программа XmlMapping делает сложные документы XML доступными для редактирования в нескольких компонентах ClientDataSet 0967
968 Глава 22. Использование технологий XML object XMLTransformProviderl. TXMLTransformProvider TransformRead.TransfomiationFile = 'BooksDefault.xtr' TransformWrite.TransformationFile = 'BooksDefaultToXml .xtr' XMLDataFile = 'Sample.xml' end object ClientDataSetl: TCIlentDataSet ProviderName = 'XMLTransformProviderl' object ClientDataSetltext TStringField object ClientDataSetlbook TDataSetField object ClientDataSetlebook- TDataSetField end object ClientDataSet2. TCIlentDataSet DataSetField = ClientDataSetlbook end object ClientDataSet3: TCIlentDataSet DataSetField = ClientDataSetlebook end Программа позволяет вам редактировать данные различных списков подузлов XML-документа с использованием элементов DBGrid. Вы можете не только изме- нять и удалять существующие записи, но и добавлять в документ новые. Щелкнув на кнопке Save (Сохранить), вы применяете внесенные вами изменения к набору данных. При этом происходит обращение к методу ApplyU pdates, и провайдер транс- формации сохраняет обновленную версию файла на диске. Существует также альтернативный метод обработки XML-документов с исполь- зованием ClientDataSet. Этот подход предусматривает создание трансформаций, которые отображают в набор данных только часть документа XML. Для примера обратите внимание на файл BooksOnly.xtr, расположенный в каталоге XmlMapping. Такой подход может быть полезен при просмотре данных, однако если вы исполь- зуете его для модификации XML-документов, структура и содержимое получен- ных в результате этого документов будут отличаться от изначальных. В составе модифицированных документов будут присутствовать только избранные вами фрагменты XML-кода. Таким образом, данная методика хороша при просмотре данных, но плохо подходит для редактирования этих данных. ПРИМЕЧАНИЕ------------------------------------------------------------------------------ Нет ничего удивительного в том, что файлы трансформаций сами по себе являются XML-документа- ми. В этом можно убедиться, открыв любой из них в редакторе. Обратите внимание на то, что такие XML-документы обладают специальным внутренним форматом. Теперь рассмотрим, как таблицу базы данных или результат запроса при помо- щи трансформации можно преобразовать в XML-файл. В отличие от файлов в ес- тественном формате ClientDataSet, файлы XML можно прочитать в любом текстовом редакторе, они обладают понятным для человека форматом. Чтобы создать при- мер МарТаЫе, я добавил в форму компонент SimpleDataSet из библиотеки dbExpress и подключил к нему компонент DataSetProvider. Затем к компоненту провайдера был подключен компонент ClientDataSet. После открытия таблицы и клиентского набора данных я сохранил содержимое этого набора в XML-файл. Теперь следует запустить XML Mapper, загрузить в него файл пакета данных, при помощи команды Select All (Выбрать все) локального меню выбрать все узлы этого файла и отдать команду Create XML From Datapacket (Создать XML на основе пакета данных). В появившемся диалоговом окне я рекомендую принять соответствие 0968
XML и Internet Express 969 имен, предлагаемое мастером по умолчанию, однако предлагаемое имя для узлов- записей ( ROW) лучше заменить на что-либо более осмысленное (например, Customer). Если теперь вы протестируете сформированную трансформацию, мастер XML Mapper отобразит в виде дерева содержимое результирующего XML-документа. Получив файл трансформации, можно продолжить разработку программы. Из программы удаляется ClientDataSet, после чего в нее добавляется DataSource и DBGrid (так как перед трансформацией пользователь мог модифицировать данные с ис- пользованием соответствующего элемента DBGrid), а также компонент XMLTrans- formClient. Данный компонент обладает подключенным к нему файлом трансфор- мации, однако этот файл не является XML-документом. Вместо этого он указывает на данные через провайдер. После щелчка на кнопке XML-документ не записыва- ется в файл, а отображается в элементе управления Мето (перед этим осуществляет- ся предварительное форматирование). Эта процедура выполняется с использова- нием метода GetDataAsXml, хотя об этом и не рассказывается в электронной справке: procedure TForml btnMapClick(Sender TObject). begin Memol.Lines Text .= FormatXmlDataCXMLTransformClientl.GetDataAsXml( ")): end: Фактически, это весь исходный код, который требуется написать для програм- мы МарТаЫе, рабочее окно которой показано на рис. 22.9. В рабочем окне этой программы отображается изначальный набор данных в сетке DBGrid и результиру- ющий XML-документ в элементе управления Мето под сеткой. Исходный код при- ложения МарТаЫе существенно проще, чем код, который использовался в примере DomCreate для создания аналогичного документа, однако при использовании рас- смотренной здесь методики в процессе разработки приложения вы должны не толь- ко написать саму программу, но также спроектировать трансформацию. В отличие от МарТаЫе, программа DomCreate может работать с любым набором данных прямо в процессе своего функционирования. Никакой связи с той или иной таблицей не требуется, так как в DomCreate для создания XML-документов используется более универсальный код. Теоретически можно сформировать аналогичное динамиче- ское отображение при помощи событий универсального компонента XMLTransform, однако я считаю, что для создания XML-документов с не известной заранее струк- турой удобнее использовать методику, основанную на DOM, о которой речь шла ранее. Обратите внимание на то, что вызов FormatXmlData отображает данные в бо- лее удобном виде, однако существенно замедляет программу, так как внутри вызо- ва осуществляется загрузка XML в DOM. XML и Internet Express Теперь, когда мы научились создавать XML-документы, я хочу рассказать о том, как происходит редактирование XML-данных в Windows-приложениях и через Веб. Задача редактирования XML-данных через Веб особенно интересна, так как в Delphi включены специальные механизмы, позволяющие выполнять связанные с этим процедуры. В состав Delphi 5 была включена специальная архитектура Internet Express, которая теперь является частью платформы с названием WebSnap. ГТ пятгЪппмя WebSnan также включает в себя поддержку XSL. о чем я расскажу позже. 0969
970 Глава 22. Использование технологий XML ^^MapTdble xi L g)STSB~~ - 1221 Kauai Dive Shoppe 1231 Unisco 1351 Sight Diver 1354 Cayman Divers World Unfcnrted 1356 Tom Sawyer Drvng Centre 1380 Blue Jack Aqua Center 1384 VIP Divers Club lAODBt Л 4 976 Sugarloaf Hwy P0BoxZ547 1 Neptune Lane PG Box 541 6321 Third Frydenhoi 23 738 Paddington Lane 32 Man St |aDOR2 Suite 1 Cl J Suite 310 K^xml veision®"1 0”Ъ < Document < Customer < CustN o>1221< /CustN o> < Company >Kauai Dive Shoppe</Company» <Add1>4 976 Sugarloaf Hwy</Addr1> <Addr2>Suite 103</Addr2> <Crty>Kapaa Kauai</Crty> <State>HK/State> <Zip> 94766 1 234</Zip> <CourAiy> Amenca< /Country> <Phone>808 555 0269</Phone> <FfiX>808 555 0278</FAX> <TaxRate>8 5</TaxRate> &Й0 Рис. 22.9. Программа MapTable генерирует XML-документ исходя из таблицы базы данных. При этом используется файл трансформации. Изначальный набор данных отображается в элементе DBGrid вверху, а полученный в результате XML-документ — в элементе Мето внизу В главе 16 я уже рассказывал о разработке приложений DataSnap Для того чтобы эту архитектуру можно было использовать совместно с XML, в состав Internet Express входит клиентский компонент под названием XMLBroker, который можно использовать вместо клиентского набора данных. При этом XMLBroker получает данные от приложения DataSnap, работающего в среднем звене, и делает их дос- тупными для модуля генерации страниц специального типа под названием InetX- PageProducer. Эти компоненты можно использовать в стандартном приложении WebBroker или в программе WebSnap. В основе Internet Express лежит идея о том, что вы разрабатываете расширение веб-сервера (о чем рассказывалось в главе 20), которое, в свою очередь, генерирует веб-страницы, взаимодействуя при этом с сер- вером DataSnap. Ваше приложение действует как клиент DataSnap, при этом вы- полняется генерация веб-страниц для клиентского браузера. Платформа Internet Express предоставляет все необходимое для того, чтобы упростить разработку по- добных приложений. Я знаю, что это звучит запутанно, однако следует отметить, что Internet Express является четырехзвенной архитектурой. Четырьмя звеньями этой архитектуры являются: сервер SQL, сервер приложений (сервер DataSnap), веб-сервер с прила- гаемым к нему специальным приложением и, наконец, веб-браузер. Конечно же, вместо этого вы можете поместить компоненты доступа к базе данных прямо в при- ложение, которое обрабатывает запросы HTTP и занимается генерацией HTML-кода. Это будет соответствовать классической схеме клиент-сервер. Мало того, вы мо- жете осуществлять доступ к локальной базе данных или XML-файлу при помощи простой однозвенной структуры (серверная программа и браузер). Другими словами, Internet Express — это технология разработки клиентов на основе браузера, позволяющая передавать весь набор данных на клиентский ком- 0970
XML и Internet Express 971 пьютер в виде страницы HTML, снабженной встроенным кодом JavaScript, при помощи которого осуществляется манипулирование данными XML и отображе- ние этих данных в пользовательском интерфейсе, определенном при помощи HTML Код JavaScript позволяет браузеру отображать данные и манипулировать ими. Компонент XMLBroker Чтобы решить описанную задачу, механизм Internet Express использует несколь- ко технологий. Пакеты DataSnap преобразуются в формат XML для того, чтобы программа могла разместить эти данные на странице HTML, благодаря чему веб- клиент получает возможность работы с этими данными. На самом деле дельта- пакет тоже представляется в виде XML. Эти процедуры выполняются компонен- том XMLBroker, который обрабатывает XML, передает данные новым компонентам JavaScript. Как и ClientDataSet, компонент XMLBroker обладает: О свойством MaxRecords, которое определяет количество записей, размещаемых на одной странице; О свойством Params, определяющим параметры, которые передаются через про- вайдера в ответ на удаленный запрос; О свойством WebDispatch, определяющим запрос обновления, на который отвеча- ет брокер. Компонент InetXPageProducer позволяет генерировать формы HTML на основе наборов данных визуальным способом подобно тому, как осуществляется разра- ботка пользовательского интерфейса AdapterPageProducer. Фактически архитекту- ра Internet Express, используемые ею внутренние интерфейсы и ее IDE-редактор в совокупности могут рассматриваться как родитель архитектуры WebSnap. Обе эти архитектуры обладают редактором для размещения визуальных компонентов и возможностью генерации сценариев, разница между ними состоит в том, что в одном случае эти сценарии исполняются на стороне сервера, а в другом — на сто- роне клиента. Еще одно существенное отличие состоит в том, что в сравнении с новой платформой WebSnap старая архитектура Internet Express в большей сте- пени ориентирована на использование XML. Лично мне кажется, что это является недостатком WebSnap. СОВЕТ--------------------------------------------------------------------- Общей особенностью InetXPageProducer и AdapterPageProducer является поддержка CSS (Cascading Style Sheets). Оба компонента обладают альтернативными свойствами Style и StylesFile, позволяю- щими определять CSS, а каждый визуальный элемент обладает свойством StyleRule, при помощи которого можно выбрать имя стиля. Поддержка JavaScript Чтобы расширить возможности редактирования данных на стороне клиента, ком- понент InetXPageProducer использует специальные компоненты JavaScript и специ- альный код JavaScript. В состав Delphi входит достаточно обширная библиотека JavaScript, которую браузер должен загрузить для того, чтобы получить возмож- ность взаимодействовать с приложениями Delphi. Конечно, это не очень удобно, олнако это единственный способ пасшипить интерфейс браузера (основанный на 0971
972 Глава 22. Использование технологий XML динамическом HTML) таким образом, чтобы он включал в себя поддержку огра- ничений на значения полей, а также другие возможности, часто применяемые в биз- нес-приложениях. При использовании обычного HTML поддержка всех этих воз- можностей немыслима. Компания Borland предлагает следующие файлы JavaScript, которые следует расположить на веб-узле, на котором располагается ваше прило- жение. Файл Описание Xmldom.js Совместимый c DOM модуль грамматического разбора XML (для браузеров, в которых отсутствует встроенная поддержка XML DOM) Xmldb.js Xmldisp.js Xmlerrdisp.js XmlShow.js Классы JavaScript для элементов управления HTML Классы JavaScript для связывания XML-данных с элементами управления HTML Классы для обработки ошибок Функции JavaScript для отображения данных и дельта-пакетов (для отладочных целей) HTML-страницы, генерируемые с использованием Internet Express, как прави- ло, включают в себя ссылки на эти JavaScript-файлы, например: «script language=Javascript type="text/javascript" s rc=” I ncl udePa thURL/xml db. j s "x/sc ri pt> Данный код JavaScript можно изменить в соответствии с собственными поже- ланиями. Для этого необходимо либо напрямую добавить код прямо в содержимое страницы HTML, либо разработать новый компонент Delphi, совместимый с ар- хитектурой Internet Express и осуществляющий вывод кода JavaScript, возможно, в составе HTML. Например, простой класс TPromptQueryButton в примере INetXCustom генерирует следующий фрагмент HTML, включающий в себя код JavaScript: «script language=javascript type="text/javascript"> function PromptSetFielddnput, msg) { var v = prompt(msg): if (v == null || v == "") return false: input.value = v return true: } var QueryForm3 = document.forms[ 'QueryForm3']; «/script> «input type=button value="Prompt..." onclick="if (PromptSetFieldCPromptResult. 'Enter sane textin')) QueryForm3.submi t():”> СОВЕТ--------------------------------------------------------------------------------------- Если вы планируете использовать Internet Express, обратите внимание на дополнительные демо- компоненты, содержащиеся в каталоге \Demo\Midas\InternetExpress\INetXCustom. Подробные ин- струкции о том, как установить эти компоненты, содержатся в файле readme.txt. Компоненты предлагаются компанией Borland без какой-либо поддержки, однако, используя их, вы можете с минимальными усилиями добавлять в ваши приложения Internet Epress множество удобных допол- нительных возможностей. Конечно же, при использовании данной архитектуры на стороне клиента не требуется устанавливать ничего экстраординарного. Достаточно использовать любой браузер, поддерживающий стандарт HTML 4 и работающий в любой опе- 0972
XML и Internet Express 973 рационной системе. Напротив, к веб-серверу предъявляются определенные допол- нительные требования. Прежде всего, сервер должен быть платформой Win32 (в среде Kylix эта технология недоступна). Также на нем должны быть установле- ны библиотеки DataSnap. Построение примера Чтобы лучше понять то, о чем я рассказываю, а также подробнее рассмотреть не- которые технические детали, разберем простую программу под названием leFirst. Чтобы избежать проблем с конфигурацией, эта программа реализована в виде при- ложения CGI, обращающегося к набору данных напрямую — в данном случае речь идет о локальной таблице, обращение к которой осуществляется с использовани- ем компонента ClientDataSet. Позже я покажу вам, как превратить существующий Windows-клиент DataSnap в интерфейс, основанный на использовании браузера. Чтобы построить leFirst, я создал новое приложение CGI и добавил в его модуль данных компоненты ClientDataSet (подключенный к локальному CDS-файлу) и DataSetProvider. Компонент DataSetProvider подключен к компоненту ClientDataSet. Следующий шаг: создание компонента XMLBroker и подключение его к провайдеру: object ClientDataSetl: TCIientDataSet FileName = ‘C:\Program Files\Coimon Files\Borland Shared\Data\employee.cds' end object DataSetProviderl: TDataSetProvider DataSet = ClientDataSetl end object XMLBrokerl: TXMLBroker ProviderName = 'DataSetProviderl' WebDispatch.MethodType = mtAny WebDispatch.Pathinfo = 'XMLBrokerl' Reconci 1 eProducer = PageProducerl OnGetResponse = XMLBrokerlGetResponse end Свойство ReconcileProducer требуется для того, чтобы в случае конфликта при обновлении отобразить соответствующее сообщение об ошибке. Как мы увидим позже, в состав одной из демо-программ Delphi включен некоторый специальный связанный с этим код, однако в данном простом примере я просто соединил тради- ционный компонент PageProducer с некоторым универсальным HTML-сообщени- ем об ошибке. После настройки брокера XML вы можете добавить InetXPageProducer к модулю веб-данных. Компонент обладает стандартным скелетом HTML, к кото- рому я добавил заголовок, не затрагивая при этом специальных тегов: <HTML><HEAD> <title>IeFirst</title> </HEAD><BODY> <hl>Internet Express First Demo (leFirst.exe)</hl> <#INCLUDES><#STYLES><#WARNINGS><#FORMS><#SCRIPT> </BODY> Специальные теги автоматически обрабатываются при помощи сценариев Java- Script, расположенных в каталоге, имя которого хранится в свойстве Include Path U RL. Вы обязаны разместить в этом свойстве имя каталога веб-сервера, в котором рас- полагаются эти файлы. В рабочей среде Delphi эти файлы располагаются в катало- ге \neinhi7\Source\WebMidas. Специальные теги расширяются следующим образом. 0973
974 Глава 22. Использование технологий XML Тег Эффект <#INCLUDES> <#STYLES> <# WARNINGS» Генерирует инструкции для включения библиотек JavaScript Добавляет определение страницы стилей Используется на этапе проектирования для отображения ошибок в редакторе InetXPageProducer <#FORMS» <#SCRIPT» Генерирует HTML-код, формируемый компонентами веб-страницы Добавляет блок JavaScript, используемый для запуска сценария на стороне сервера ПРИМЕЧАНИЕ----------------------------------------------------------- Компонент InetXPageProducer поддерживает также некоторые дополнительные внутренние теги. В частности, тег <#BODYELEMENTS> соответствует пяти ранее описанным тегам HTML-шаблона. Тег <#COMPONENT Name=WebComponentName> является частью генерируемого HTML-кода, ис- пользуемого для объявления компонентов, генерируемых визуально. Тег <#DATAPACKET XMLBroker= BrokerName> заменяется кодом XML пакета данных. Вы можете видоизменить HTML-код, генерируемый компонентом InetXPage- Producer, при помощи редактора этого компонента. Этот редактор напоминает ре- дактор исполняемых на стороне сервера сценариев WebSnap. Сделайте двойной щелчок на компоненте InetXPageProducer, и Delphi откроет окно, подобное изобра- женному на рис. 22.10. При помощи этого редактора вы можете создавать сложные структуры, основанные на форме запроса, форме данных или любой группе LayoutGroup. В форму данных рассматриваемого мной примера я добавил компо- ненты DataGrid и DataNavigator, не выполняя при этом какой-либо дополнительной настройки (добавление дочерних кнопок, колонок и других объектов, полностью заменяющих элементы, присутствующие на форме по умолчанию). WebModaleLlnetXP«aePr*4ucetl Л1Й ♦ ♦ IneO-TagePioducetl j ErrpNo DataFormt | LastName Dd»aNavtga»« p»s<Name ИЯИЯМ I Salary I StatusCoiumnl “2 Рис. 22.10. Редактор InetXPageProducer позволяет создавать сложные HTML-формы визуально, подобно тому как это выполняется в редакторе AdapterPageProducer 0974
XML и Internet Express 975 Рассмотрим DFM-код моего примера для InetXPageProducer и его внутренних компонентов: object InetXPageProducerl TInetXPageProducer IncludePathURL = '/jssource/' HTMLDoc Strings = ( ) object DataForml TDataForm object DataNavigatorl TDataNavigator XMLComponent = DataGridl Custom = 'a 1ign="center"' end object DataGridl TDataGrid XMLBroker = XMLBrokerl DisplayRows = 5 TableAttributes BgColor - 'Silver' TableAttributes CellSpacing = 0 TableAttributes CellPadding = 2 HeadmgAttributes BgColor = 'Aqua' object EmpNo TTextColumn object LastName TTextColumn object FirstName TTextColumn object PhoneExt TTextColumn object Hi reDate TTextColumn object Salary TTextColumn object StatusColumnl TStatusColumn end end end Здесь можно видеть базовую настройку плюс некоторые дополнительные видоизменения графической конфигурации. Однако истинный смысл этих ком- понентов содержится в формируемом HTML- и JavaScript-коде, просмотреть ко- торый можно на вкладке HTML редактора InetXPageProducer. Приведу несколько фраг- ментов HTML-кода, в которых определяются кнопки, заголовок сетки данных и одна из ячеек этой сетки. // кнопки <table align="center"> <tr><td colspan="2"> <input type="button" value="|<" onclick='if (xml_ready) DataGridl_Disp first!) > <mput type="button" value="«" onclick='if (xml_ready) DataGndl_Disp pgupO. > 11 заголовок сетки данных (data grid heading) <table cellspacing="O" cellpaddmg="2" border="l" bgcolor-"silver"> <tr bgcolor="aqua"> <th>EmpNo</th> <th>LastName</th> </tr> <tr> // ячейка данных <td><div> <input type="text” name="EmpNo" size="10” onfocus='if(xml_ready)DataGridl_Disp xfocus(this) ' onkeydown=‘if(xml_ready)DataGridl_Disp keys<this) '> </div></td> 0975
976 Глава 22. Использование технологий XML После того как HTML-редактор настроен, вы можете вернуться к модулю веб- данных, добавить в него новое действие и при помощи свойства Producer соеди- нить это действие с компонентом InetXPageProducer. Этого должно быть достаточ- но для того, чтобы программа заработала через браузер (рис. 22.11). ?#• leFirst ~ Mo?ilU Уе &Л £ew gocfanerb Joofc idndow Internet Express First Demo (IeFirst.exe) Й ill i ill 4 *1 I Apply Updates ( IEmpN» ; LastNarae Nelson " |4 /oung Lambert |в i Johnson b . coreSf . у у- •st.dlmziiMsa&b. а У- FhstNaint iPhoneExt Hirelble jRoberto5Q |Jv^esd8yDecer jBruce 33 JWednesday Decer ------rr—^йй swwwr——раююй 2 iMonday February C 10 JvYednesday Apnl 0 23 ^onda^T^nl ™iV J™ Salary prow fcSSOO psora 25050 25050 13 iS^E^S^L0***8”***1*1 Вя**^-?|*г**л> Рис. 22.11. Приложение leFirst посылает браузеру компоненты HTML, весь документ XML и код JavaScript для того, чтобы отобразить данные при помощи визуальных компонентов Если взглянуть на HTML-файл, принятый браузером, можно обнаружить таб- лицу, упомянутую ранее, некоторый код JavaScript, а также данные из базы дан- ных в формате пакета данных XML Эти данные собраны брокером XML и переда- ны компоненту InetXPageProducer для того, чтобы этот компонент вставил их в HTML-файл. Обратите внимание на то, что количество записей, передаваемых клиенту, зависит от XMLBroker, но не от количества линий в элементе Grid интер- фейса. Когда XML-данные переданы браузеру, пользователь может использовать кнопки навигационного компонента для просмотра этих данных, при этом допол- нительного обращения к серверу не потребуется. Такое поведение отличается от поведения WebSnap, осуществляющего постраничный обмен данными. Сложно сказать, какой из этих методов лучше, — все зависит от специфики разрабатывае- мого приложения. Существующие в системе классы JavaScript позволяют пользователю вводить новые данные, используя при этом правила, определяемые JavaScript-кодом, вы- полняющим обработку событий динамического HTML. Обратите внимание, что по умолчанию в элементе Grid присутствует дополнительная колонка «звездочка», в которой отмечаются записи, подвергавшиеся модификации. Модифицирован- ные данные собираются браузером в пакет XML и передаются обратно на сервер в момент, когда пользователь щелкает на кнопке Apply Updates (Применить обнов- ление). В этот момент браузер выполняет действие, определяемое свойством Web- Dispatch.Pathinfo брокера XMLBroker Нет надобности экспортировать это действие из модуля веб-данных, так как операция выполняется автоматически (при этом вы можете отключить ее, присвоив свойству WebDispatch.Enable значение False). 0976
Использование XSLT 977 Компонент XMLBroker применяет изменения в отношении сервера и возвращает содержимое провайдера, определяемого при помощи свойства ReconcileProvider. Если это свойство не определено, возникает исключение. Если все работает хоро- шо, компонент XMLBroker передает управление главной странице, на которой со- держатся данные. Однако, используя настоящую методику, я столкнулся с неко- торыми проблемами, поэтому программа leFirst выполняет обработку события OnGetResponse, сообщая об этом при помощи обновления вида: procedure TWebModulel XMLBrokerlGetResponse(Sender TObject Request TWebRequest Response TWebResponse var Handled Boolean). begin Response Content = ’<hl>Updated</hlxp>' + InetXPageProducerl Content. Handled = True end. Использование XSLT Еще одна методика генерации HTML-кода на основе XML-документа основана на использовании языка XSL (extensible Stylesheet Language — расширяемый язык страниц стилей), а точнее его подмножества под названием XSLT (XSL Transfor- mation). Основная цель технологии XSLT — это преобразование XML-документа в другой документ (как правило, тоже XML-документ, но обладающий другой структурой). Например, очень часто технология XSLT используется для преобра- зования документа XML в документ XHTML, который пересылается с веб-серве- ра клиентскому браузеру. Еще одной связанной с этим технологией является XSL- FO (XSL Formatting Objects), которая может использоваться для преобразования XML-документов в PDF или любой другой формат. Документ XSLT — это хорошо оформленный XML-документ. Корневой узел XSLT-файла должен выглядеть следующим образом: <xsl stylesheet version=’l 0" xmlns xsl=" "> Внутри XSLT-файла содержится один или несколько шаблонов (которые на- зывают также правилами, или функциями). Обработку шаблонов выполняет спе- циальный механизм обработки XSLT. Шаблон определяется внутри узла с именем xslrtemplate, который, как правило, снабжается атрибутом match. В самом простом случае шаблон применяется к узлам с заданным именем. Чтобы активировать шаб- лон, вы передаете ему один или несколько узлов при помощи указанного выраже- ния XPath: xsl apply-templates 5е1есГ=''имя_узла" Стартовой точкой этой операции является шаблон, который выполняет обра- ботку корневого узла. Этот шаблон может быть единственным шаблоном XSLT- файла. Внутри шаблонов могут располагаться самые разнообразные команды, на- пример извлечение значения из XML-документа (xsbvalue-of select), выражения цикла (xsl:for-each), условные выражения (xsl:if,xsl:choose), команды сортировки (xsbsort) и команды подсчета (xslmumber). Это лишь несколько типов общих ко- манд XSLT. 0977
978 Глава 22. Использование технологий XML Использование XPath XSLT использует другие XML-технологии, в частности, для идентификации раз- делов XML-документа используется технология XPath. В рамках XPath задается набор правил, позволяющих идентифицировать один или несколько узлов в XML- документе. Правила основаны на определении пути к узлу в рамках древовидной иерархии элементов XML. Например, имя/books/book идентифицирует любой узел book, входящий в состав узла books в XML-документе. Для идентификации узлов в рамках XPath используются некоторые специальные символы. О Символ звездочки (*) обозначает любой узел. Например, имя book/* иденти- фицирует любой подузел узла book. О Символом точки (.) обозначается текущий узел. О Символ вертикальной черты (|) указывает на возможные альтернативы, напри- мер book|ebook. О Двойная косая черта (//) обозначает любой путь. Например, имя //title обозна- чает любой узел title вне зависимости от того, в состав какого родительского узла он входит, а имя books//author идентифицирует все подузлы author в соста- ве узла books вне зависимости от того, какие подузлы располагаются между ними в иерархии узлов XML. О Знаком «коммерческое at» (@>) обозначаются атрибуты, например author/ @lastname. Аналогичный синтаксис можно использовать в случае, если требу- ется выбрать только узлы, обладающие заданным именем и заданным значени- ем заданного атрибута, например, чтобы выбрать всех авторов с заданным име- нем, можно использовать следующую запись: author[@name="marco"]. Существует также множество других правил, однако данное краткое введение в XPath поможет вам получить первичное представление об XPath и облегчит ос- воение рассматриваемых далее примеров. Документ XSTL — это XML-документ, при помощи которого осуществляется обработка структуры исходного ХМ L-до- кумента, в результате чего формируется еще один, результирующий XML-доку- мент, например документ XHTML, который можно просмотреть при помощи веб- браузера. ПРИМЕЧАНИЕ---------------------------------------------------------— К наиболее часто используемым процессорам XSLT относятся MS-XML, Xalan (в составе проекта Apache XML — xml.apache.org) и основанный на Java процессор Xt, разработанный Джеймсом Клар- ком (James Clarke). Помимо этого для обработки XSLT вы можете воспользоваться инструментом TurboPower XML Partner Pro (www.turbopower.com). Практическое использование XSLT После краткого введения в XSLT я позволю себе приступить к рассмотрению не- скольких примеров. Для начала я планирую чуть ближе познакомить читателей с самим языком XSL, а затем подробнее расскажу о том, как осуществляется рабо- та с XSL в среде Delphi. Для ознакомительных экспериментов можно включить ссылку на XSL-файл прямо в код XML-документа. При этом если вы загрузите этот XML-файл в Internet 0978
Использование XSLT 979 Explorer, браузер покажет вам результирующую трансформацию, то есть, как пра- вило, документ XHTML. Взаимосвязь между XML и XSL определяется в заголов- ке XML-документа при помощи команды, подобной следующей: <?xml-stylesheet type="text/xsl" href="samp!elembedded.xsl"?> Именно эта команда присутствует в заголовке документа samplelembedded.xml который входит в состав проекта XslEmbed. Упомянутый в заголовке XSL-файл со- держит в себе множество XSL-фрагментов, которые я не могу обсуждать слишком подробно на страницах этой книги. Например, для формирования списка авторов и создания специальной группы используется следующий код: <h2>Al1 Authors</h2> <ul> <xsl:for-each select="books//author"> <14><xsl -value-of select=''.”/></!i> </xsl-for-each> </ul> <h3>E-Authors</h3> <ul> <xsl:for-each select="books/ebook/author"> <14><xsl:value-of select="."/></11> </xsl:for-each> </ul> Более сложный код используется для извлечения значений узлов при выпол- нении определенных условий, например, только если в подузле анализируемого узла присутствует определенное значение или узел обладает определенным атрибу- том. При этом структура родительских узлов извлекаемого узла не имеет значения. В данном небольшом фрагменте XSL демонстрируется использование оператора if и генерация атрибута в результирующем узле (таким образом, в результирую- щем коде HTML формируется гиперссылка href): <h3>Marco's works (books + ebooks)</h3> <ul> <xsl:for-each select="books/*[author = ’Cantu’]"> <li> <xsl-value-of select="title"/> <xsl:if test«"url"> (<a><xsl attribute name="href''><xsl:value-of select="url"/> </xsl:attribute>Jump to document</a>) </xsl :if> </l T> </xsl:for-each> </ul> Обработка XSLT с использованием WebSnap Из кода разрабатываемой вами программы вы можете обратиться к методу Trans- formNode узла DOM и передать этому методу еще один компонент DOM, в кото- ром содержится XSL-документ. Однако вместо использования данного низкоуров- невого подхода для создания программы обработки XSL можно использовать WebSnap. Чтобы среда Delphi сформировала для вас изначальную заготовку кода, вы можете создать новое приложение WebSnap (для примера я создал програм- му CGI под названием XslCust) и на главной странице поместить компонент XSLPageProducer. В состав Delphi входит скелет XSL-файла для манипулирования 0979
980 Глава 22. Использование технологий XML пакетом данных ClientDataSet, а в редактор добавляются несколько новых видов: вид HTML file заменяется на XSL text, вид XML Tree содержит данные, вид XSL Tree ото- бражает XSL-код при помощи ActiveX-компонента Internet Explorer, вид HTML result показывает код, формируемый в результате выполнения трансформации, наконец, на странице Preview отображается то, что пользователь увидит в рабочем окне бра- узера. СОВЕТ---------------------------------------------------------------------- В Delphi 7 редактор поддерживает полнофункциональное завершение кода для XSLT, благодаря чему редактировать код XSLT так же удобно, как и в других специальных редакторах XML. Чтобы все это начало работать, необходимо передать компоненту XSLPageProducer кое-какие данные. Для этого используется свойство XMLData. Это свойство можно связать как с компонентом XMLDocument, так и напрямую с компонентом XMLBroker, как я и поступил в данном примере. Брокер принимает данные от провайдера, под- ключенного к локальной таблице, соединенной с таблицей Customers из классичес- кой программы DBDEMOS. В результате при помощи сгенерированного Delphi кода XSL вы получаете (даже на этапе проектирования) вывод, показанный на рис. 22.12. № T customers Хьюит XsKuaBm j * Ж 21 Procedures W ?1 Uses CustNo Company Addrl ’Kauai Dive 4-976 1221 Shoppe Sugarloaf sdfsdfsd Hwy TT 20 Box Z- 1231 Umsco 547 Addr2' City- Suite Kapaa Kauai Freeport State Zap Country 12зГ US 1254 02t 80( Bahamas 55 39 1351 S^tDwer JNe₽tun' Lane Kato Paphos Gyp™ Cayman Divers 20 Box World 541 Unlimited 632-1 1356 |.awyer Third .«I _~z. _ Grand Cayman Chnshansted Bnbsb 69 Indies 50 St Croix 00820 79У Рис. 22.12. Результат XSLT-трансформации, сформированной (даже на этапе проектирования) компонентом XSLPageProducer в примере XsICust <?xml version="l О’"?> <xsl stylesheet xmlns xsl="http //www w3 org/1999/XSL/Transform"> <xsl template match-"/"> <html><body> <xsl applу-tempiates/> </bodyx/html> </xsl tempiate> <xsl template match="DATAPACKET”> <table border-"l"> <xsl apply-templates select- METADATA/FIELDS"/> 0980
Использование XSLT 981 <xsl apply-templates select-"R0WDATA/R0W'7> </table> </xsl template» <xsl template match="FIELDS"> <tr> <xsl apply-templates/» </tr> </xsl template» <xsl template match=''FIELD"> <th> <xsl value-of select»"@attrname'7> </th> </xsl template» <xsl template match="ROWDATA/ROW"> <xsl variable name="fieldDefs" select='7/METADATA/FIELDS"/> <xsl variable rame="currentRow" select»"current()"/> <tr> <xsl for-each select»'$fieldDefs/FIELD"> <td> <xsl value-of select="$currentRow/@*[name( )=current()/@attrname]'7xbr/> </td> </xsl for-each» </tr» </xsl template» </xsl stylesheet» ПРИМЕЧАНИЕ -------------------------------------------------------------------------------- По сравнению c Delphi 6 в версии Delphi 7 стандартный шаблон XSL был существенно расширен. Изначальная версия не принимала во внимание поля null, которые не входили в состав пакета XML. На конференции Borland Conference 2002 я представил несколько расширений изначального XSL- кода, и некоторые из моих предложений были добавлены в шаблон. Данный код генерирует HTML-таблицу, расширяя метаданные полей (field metadata) и данные рядов (row data). Поля используются для генерации заголовка таблицы: для каждой записи в каждом ряду добавляется тег <th>. Данные из стро- ки таблицы (row data) используютсядлязаполнениядругих строк таблицы значе- ниями каждого из атрибутов. Извлечения значения каждого из атрибутов (select= "@*") не достаточно, так как атрибут может отсутствовать. По этой причине спи- сок полей и текущая строка сохраняются в двух переменных. Потом для каждого поля код XSL извлекает значение элемента строки, обладающего именем атрибута (@*[name()=...), соответствующего имени текущего поля, сохраненного в атрибу- те attrname (@attrname). Этот код нельзя назвать простым, однако он является ком- пактным и адаптируемым способом анализа различных разделов XML-документа. Выполнение XSL-преобразования напрямую с использованием DOM Без сомнения, выполнение XSL-преобразования с использованием компонента XSLPageProducer — чрезвычайно удобная методика, однако если на основе одного 0981
982 Глава 22. Использование технологий XML и того же набора данных требуется сформировать несколько разных веб-страниц с использованием разных XSL-стилей, применение для этой цели WebSnap не бу- дет оптимальным решением. Вместо этого я предпочел создать CGI-приложение под названием CdsXstl, которое может выполнить преобразование пакета данных ClientDataSet в разные HTML-файлы в зависимости от имени XSL-файла, передан- ного этому приложению в качестве параметра. Используя данный подход, я могу не только модифицировать существующие, но и добавлять в систему новые XSL- файлы, и при этом нет необходимости в перекомпиляции приложения. Чтобы выполнить преобразование, программа загружает в память оба файла: XML и XSL. Для этого используются два компонента XMLDocument под именами xmlDom и XslDom. После этого происходит обращение к методу transformNode доку- мента XML. В качестве параметра этому методу передается XSL-документ. В ре- зультате выполнения метода происходит заполнение третьего компонента XMLDocu- ment с названием HtmlDom: procedure TWebModulel.WebModulelWebActionItemlAction(Sender: TObject: Request: TWebRequest: Response: TWebResponse: var Handled: Boolean): var xslfile, xslfolder: string; attr: IDOMAttr; begin // открываем клиентский набор данных // и загружаем соответствующий ему XML в ООН Cl1entDataSetl.Open; XmlDom.Xml.Text := ClientDataSetl.XMLData: Xml Dorn.Active := True; // загружаем необходимый XSL-файл xslfile := Request.QueryFields.Values ['style']: if xslfile = " then xslfile := 'customer.xsl'; xslfolder := ExtractFilePath (ParamStr (0)) + 'xsl\': if FileExists (xslfolder + xslfile) then xslDom.LoadFormFile (xslfolder + xslfile) el se raise Except!on.Create('Файл не найден: ' + xslfolder + xslfile): XSLDom.Active := True; if xslfile = 'single.xsl' then begin attr .= xslDom.DOMDocument.createAttributeCselect'); attr.value := '//ROU[QCustNo-'" + Request.QueryFields.Values [ 'id'] + '"] ': xslDom.DOMDocument.getElementsByTagName ('xsl:apply-templates’). item[0].attributes.setNamedltem(attr); end: // выполняем трансформацию HTMLDom.Active := True; xmlDom.DocumentElement.transformNode (xslDorn.DocumentElement, HTMLDom); Response.Content :- HTMLDom.XML.Text; end: При помощи DOM осуществляется модификация XSL-документа таким обра- зом, чтобы выполнялось отображение только одной записи. Для этого добавляет- ся выражение XPath, при помощи которого выбирается запись, идентифицируе- мая полем id запроса. Значение id при помощи XSL включается в гиперссылку вместе со списком записей. Здесь я не буду рассматривать содержимое XSL-фай- лов, при желании вы можете самостоятельно ознакомиться с их содержимым, так 0982
Обработка крупных документов XML 983 как все они расположены в подкаталоге XSL каталога, в котором хранятся рабочие файлы данного примера. ВНИМАНИЕ---------------------------------------------------------------- Чтобы запустить эту программу, вы должны разместить XSL-файлы в подкаталоге XSL каталога, в ко- тором расположено ваше CGI-приложение. Если вы планируете хранить XSL в каком-либо другом каталоге, вам придется соответствующим образом изменить исполняемый код. Измените код, который извлекает имя папки XSL из имени программы, хранящегося в первом параметре командной строки (глобальный объект Application, определяемый в модуле Forms, недоступен из приложения CGI). Обработка крупных документов XML Как вы, наверное, уже убедились, одну и ту же задачу в области XML можно вы- полнить несколькими разными способами. В большинстве случаев удобнее вы- брать методику, которая обеспечивает создание более понятного и более удобного в сопровождении кода. Однако если перед вами стоит задача обработки большого количества XML-документов или XML-документов крупного размера, зачастую вам приходится жертвовать понятностью кода в пользу его эффективности. Думаю, что теоретические рассуждения будут менее полезными, чем рассмот- рение конкретного примера. Вы можете воспользоваться рассматриваемым далее кодом, а также модифицировать его для того, чтобы протестировать различные варианты решения. Пример называется LargeXml. Программа решает конкретную задачу: перемещение данных из базы данных в XML-файл и обратно. Используя dbExpress, программа открывает набор данных (расположенный на стороне БД) и несколько раз реплицирует данные в компоненте ClientDataSet, то есть в локаль- ной памяти. Структура локального компонента ClientDataSet заимствуется из ком- понента, который используется для доступа к данным: SimpleDataSetl.Open: ClientDataSetl.FieldDefs := SimpleDataSetl. FieldDefs; Cli entDataSetl.CreateDataSet; Специальный переключатель используется для того, чтобы определить коли- чество данных, которые требуется обработать (на медленных компьютерах в неко- торых вариантах для обработки данных требуется несколько минут). Когда раз- мер данных определен, данные клонируются при помощи следующего кода: while ClientDataSetl.RecordCount < nCOunt do begin SimpleDataSetl.RecNo : = Random (SimpleDataSetl.RecordCount) + 1; ClientDataSetl.Insert: ClientDataSetl.Fields [0].Aslnteger := Random (10000): for I := 1 to SimpleDataSetl.FieldCount - 1 do ClientDataSetl.Fields [1].AsString := SimpleDataSetl.Fields [1].AsString; ClientDataSetl.Post: end; Из ClientDataSet в XML-документ Теперь, когда программа загрузила в оперативную память набор данных (возмож- но, достаточно большого размера), она позволяет сохранить этот набор в файле 0983
984 Глава 22. Использование технологий XML тремя разными способами. Во-первых, можно напрямую сохранить в файле содер- жимое свойства XMLData компонента ClientDataSet, получив в результате документ, основанный на атрибутах. Скорее всего, такой формат вас не устроит. Второе решение — применить трансформацию XmlMapper совместно с компонентом XMLTrans- formClient. Третье решение — выполнить обработку набора данных напрямую и за- писать каждую из хранящихся в нем записей в файл: procedure TForml btnSaveCustomClick(Sender TObject), var str TFileStream, s string. i Integer, begin str = TFileStream Create Cdata3 xml', fmCreate). try ClientDataSetl First s = ’<?xml version="l 0" standalone="yes" ?><employee>': str Write(s[l], Length (s)), while not ClientDataSetl EOF do begin s = ". for i =0 to ClientDataSetl FieldCount - 1 do s = s + MakeXmlstr (ClientDataSetl Fields[i] FieldName, ClientDataSetl Fields[i] AsString). s = MakeXmlStr ('employeeData' s). str Write(s[l], length (s)). ClientDataSetl Next end. s = '</employee>'. str Write(s[l], length (s)). finally str Free. end end. В коде используется простая (но эффективная) функция создания XML- узлов: function MakeXmlstr (node, value string) string. begin Result = '< + node + '>' + value + '</' + node + ’>’. end. Если вы запустите программу, вы увидите время, которое потребовалось для выполнения каждой из операций (рис. 22.13). Самым быстрым способом является сохранение данных из ClientDataSet, однако в этом случае результат, скорее всего, будет для вас неприемлемым. Специальная поточная обработка выполняется лишь немного медленнее, однако вы должны иметь в виду, что этот подход не требует перемещения данных в локальный клиентский набор — вы можете применить его напрямую к однонаправленному набору данных dbExpress. Использование XmlMapper в отношении крупных наборов данных приводит к существенным затра- там времени — этот подход в сотни раз медленнее даже для небольших наборов данных. (Я не рискнул применять его в отношении достаточно крупного набора данных, так как процесс обработки потребовал бы слишком много времени.) 0984
Обработка крупных документов XML 985 Например, для прямой поточной обработки небольшого набора данных потребо- валось 50 миллисекунд, в то время как при использовании XmlMapper для обработ- ки этого же набора потребовалось 10 секунд. И в том и в другом случае был полу- чен фактически один и тот же результирующий документ. •Г LatgeXml Create Qataset <* Medium 00 020 12 578 00 050 00 862 300 SaveXnjgataPackw j Save Transformed Xml | 313 Robert 2025 Jacques 6716 Arm 1617 Janet 4256 WfSie 4747 Leslie 8408 Kim Nelson Glon Bennet Baldwri Stansbury Johnson Lambert Рис. 22.13. Программа LargeXml в действии Из документа XML в набор данных ClientDataSet После того как программа получает доступ к крупному XML-документу (это мо- жет быть файл или XML-код, полученный из внешнего источника), требуется вы- полнить обработку этого документа. Как мы убедились, подход, основанный на использовании XmlMapper, не обеспечивает достаточной производительности, в ре- зультате у вас остается три альтернативы: транформация XSL, SAX или DOM. Мне кажется, что подход, основанный на XSL-трансформации, обеспечивал бы доста- точную эффективность, однако в данном примере я открываю документ с исполь- зованием SAX — это наиболее быстрый подход, не требующий к тому же слишком много кода. Программа позволяет также загрузить документ в DOM, однако я не добавил в нее никакого кода, осуществляющего навигацию по иерархической струк- туре DOM и перенос данных в ClientDataSet. В обоих случаях я тестировал программу совместно с OpenXml и с MSXML. Благодаря этому вы можете сравнить д£а решения, основанные на SAX (к сожале- нию, код несколько различается). Могу сообщить о результатах выполненного мною тестирования: MSXML SAX работает несколько быстрее, чем OpenXml SAX (разница составляет приблизительно 20 процентов), в то же время при загрузке документа в DOM реализация MSXML работает существенно быстрее. Код MSXML SAX использует точно такую же архитектуру, как и в примере SaxDemol, поэтому здесь я привожу листинги только необходимых обработчиков. Как можно видеть, в начале элемента employeeData вы добавляете новую запись, 0985
986 Глава 22. Использование технологий XML которая публикуется, когда этот узел закрывается. Более низкоуровневые узлы добавляются в качестве полей текущей записи. Вот код: procedure TmyDataSaxHandler.startElement(var strNamespaceURI. strLocalName. strQName; WideString; const oAttributes: IVBSAXAttributes); begin inherited: if strLocalName = 'employeeData' then Forml.clientdataset2.Insert; strCurrent : = ''; . end: procedure TmyDataSaxHandler.characters(var strChars: WideString): begin inherited: strCurrent := strCurrent + RemoveWhites(strChars): end; procedure TmyDataSaxHandler.endElement(var strNamespacellRI. strLocalName, strQName: WideString): begin if strLocalName - 'employeeData' then Forml.clientdataset2.Post: if stack.Count > 2 then Forml.ClientDataSetl2.FieldByName (strLocalName).AsString := strCurrent: inherited; end: Код обработчиков в версии OpenXml выглядит похоже. Разница в интерфейсе методов и именах параметров: type TDataSaxHandler = class (TxmlStandardHandler) protected stack: TStringList: strCurrent: string; public constructor Create(aowner: TComponent): override: function endElement(const sender: TxmlCustomProcessorAgent: const locator: TdomStandardLocator: namespaceURI. tagName: WideString): TxmlParserError: override: function PCDATA(const sender: TxmlCustomProcessorAgent: const locator- TdomStandardLocator: data: WideString): TxmlParserError; override; function startElement(const sender: TxmlCustomProcessorAgent: const locator: TdomStandardLocator: namespaceURI. tagName: WideString: attributes: TdomNameValueList): TxmlParserError: override: destructor Destroy: override; end; Кроме того, при использовании OpenXml активировать механизм граммати- ческого анализа SAX сложнее. Эта процедура показана в следующем фрагменте кода (я убрал оттуда код создания набора данных, измерения времени и протоко- лирования): procedure TForml,btnReadSaxOpenClick(Sender: TObject): var agent: TXmlSandardProcessorAgent: 0986
Что далее? 987 reader: TXmlStandardDocReader; filename: string: begin Log := memoLog.Lines; filename := ExtractFilePath (Application.Exename) + 'data3.xml'; agent := TxmlStandardProcessorAgent.Create(nil); readers TXmlStandardDocReader.Create (nil): try reader.NextHandler := TDataSaxHandler.Create (nil): // наш специализированный класс agent.reader := reader: agent.processFi1e(f11 ename. filename): finally agent.free: reader.free: end; end; Что далее? В данной главе я рассмотрел вопросы применения технологии XML и других свя- занных с ней технологий, таких как DOM, SAX, XSLT, схемы XML, XPath и неко- торые другие. Вы увидели, каким образом среда Delphi упрощает работу с DOM благодаря доступу к XML через специальные интерфейсы, а также благодаря ис- пользованию трансформаций XML. Я также рассказал о том, как XSL использует- ся для веб-программирования. Мы обсудили поддержку XSLT в рамках WebSnap и архитектуру Internet Express. В главе 23 обсуждение XML будет продолжено. В ней мы будем обсуждать наиболее интересные и многообещающие технологии, появившиеся в области се- тевых технологий за последние несколько лет. Имеются в виду веб-службы. Я рас- скажу о SOAP и WSDL, кроме того, мы обсудим UDDI и некоторые другие свя- занные с этим технологии. Если вы заинтересованы в изучении обсуждаемых здесь вопросов с точки зрения работы в среде Delphi, вы должны обратиться к специаль- ным книгам, посвященным XML, схемам XML и XSLT. 0987
23 Веб-службы и SOAP Одной из наиболее выдающихся новых возможностей Delphi безусловно является встроенная поддержка веб-служб. Тот факт, что я приступаю к обсуждению свя- занных с этим вопросов в самом конце книги, вовсе не означает, что эта область технологии Delphi наименее значима. Просто я не хотел бы нарушать логический порядок изложения материала. Кроме того, на мой взгляд, не стоит начинать изу- чение Delphi прямо с рассмотрения вопросов разработки веб-служб. Веб-службы — это весьма обширная тема для обсуждения. С ними связано мно- жество технологий и стандартов. Как и ранее в данной книге, я не собираюсь под- робно рассказывать обо всем, что связано с веб-службами. Вместо этого я расска- жу о том, каким образом поддержка веб-служб реализована в Delphi, а также о связанных с этим технических сложностях. В данной главе я расскажу о связанных с веб-службами нововведениях, кото- рые появились в Delphi 7, включая вложения (attachements), настраиваемые заго- ловки (custom headers) и многие другие возможности, которые не поддерживались в Delphi 6. Вы узнаете о том, как создать клиент веб-службы и сервер веб-службы. Кроме того, я покажу, как осуществляется передача данных из базы данных через SOAP с использованием DataSnap. В этой главе будут рассмотрены следующие вопросы: О веб-службы; О SOAPhWSDL; О DataSnap через SOAP; о обработка вложений; о UDDI. Веб-службы Быстро развивающаяся технология веб-служб позволяет изменить порядок взаи- модействия предприятий и организаций через Интернет. Просмотр веб-страниц с использованием браузера и ввод информации о заказе вручную неплохо подхо- дит для обслуживания отдельных клиентов (так называемые приложения типа В2С «бизнес-клиент»), однако такой подход не эффективен, если речь заходит о взаи- модействии целых компаний (так называемые приложения типа В2В — «бизнес- 0988
Веб-службы 989 бизнес»). Если вы хотите купить несколько книг, вы можете просто посетить веб- сайт компании, осуществляющей розничную продажу литературы, сделать заказ и ждать поступления приобретенных вами товаров. Однако если вы управляете работой книжного склада, вам приходится иметь дело с сотнями заказов в день. Обработка всех этих заказов вручную — далеко не самая эффективная методика, особенно если на вашем предприятии уже используется программное обеспече- ние, осуществляющее контроль за содержимым склада, сбор статистических дан- ных и учет товарных потоков. Ручной сбор данных, выводимых этой программой, и ввод их в другое приложение — это чрезвычайно непродуктивный подход. Проблему можно решить при помощи веб-службы: программа, осуществляю- щая слежение за продажами, может автоматически создать запрос и передать его веб-службе, которая немедленно возвращает информацию о заказе. Следующий шаг — организация слежения за доставкой заказанных товаров. На данном этапе ваше приложение может использовать другую веб-службу для каждой поставки до тех пор, пока заказанные товары не достигнут места назначения. Таким обра- зом, вы можете сообщить своим заказчикам о том, сколько им придется ждать поступления заказанных товаров. В момент, когда поставка достигает места на- значения, ваша программа может послать уведомление об этом через SMS или на пейджер, автоматически отослать электронный счет банковской веб-службе и... Я могу продолжать и дальше, однако надеюсь, что основная идея использования веб- служб стала понятной. Веб-службы для взаимодействующих компьютеров оказыва- ются тем, чем для взаимодействующих людей являются электронная почта и Веб. SOAP и WSDL Надеюсь, что теперь читатели понимают основную идею, лежащую в основе кон- цепции веб-служб. Но как два совершенно разных компьютера могут взаимодей- ствовать между собой с использованием технологии веб-служб? Взаимодействие через Веб различных систем становится возможным благодаря протоколу SOAP (Simple Object Access Protocol — простой протокол доступа к объектам). Протокол SOAP основан на стандарте HTTP, благодаря чему обслуживанием запросов SOAP может заниматься веб-сервер, а соответствующие пакеты данных могут переда- ваться через брандмауэры. В рамках стандарта SOAP определяется основанный на XML формат запроса на исполнение метода объекта, расположенного на серве- ре. Стандарт SOAP определяет, каким образом этому методу передаются аргумен- ты, а также каким образом осуществляется возврат результирующих данных. ПРИМЕЧАНИЕ---------------------------------------------------------- Изначально протокол SOAP был разработан компаниями DevelopMentor (компания, принадлежащая Дону Боксу (Don Box) — эксперту в области СОМ) и Microsoft. Этот протокол позволял избавиться от недостатков, с которыми приходилось иметь дело при использовании DCOM внутри веб-серверов. Позже SOAP был предложен консорциуму W3C в качестве открытого стандарта, а затем был под- держан многими компаниями и, в первую очередь, IBM. Сейчас еще рано делать умозаключения о том, смогут ли основанные на SOAP программы таких компаний, как Microsoft, IBM, Sun, Oracle и многих других, реально взаимодействовать друг с другом или каждая из этих компаний попытается создать свою собственную версию стандарта. Однако на текущий момент нелишним будет отме- тить, что протокол SOAP является краеугольным камнем архитектуры dotNet, продвигаемой Microsoft. Кроме того, стандарт SOAP активно поддерживается компаниями Sun и Oracle. 0989
990 Глава 23. Веб-службы и SOAP Очевидно, что в будущем SOAP заменит собой обращение к объектам с исполь- зованием СОМ, по крайней мере, в ситуациях, когда приходится иметь дело с раз- ными типами компьютеров. Параллельно с этим язык описания веб-служб WSDL (Web Services Description Language), используемый для описания служб SOAP, придет на смену языку описания интерфейсов IDL и библиотекам типов, исполь- зуемым в СОМ и СОМ+. Документы WSDL — это еще один тип документов XML. WSDL используется для определения метаданных запроса SOAP. Получив файл в формате WSDL (такие файлы публикуются для того, чтобы клиенты смогли ознакомиться с описанием службы), вы получаете возможность создать програм- му, которая сможет обратиться к соответствующей веб-службе. Среда Delphi обеспечивает двустороннее отображение между WSDL и интер- фейсами. Это означает, что вы можете сгенерировать интерфейс на основе имею- щегося у вас файла WSDL. В результате вы получаете возможность создать кли- ентскую программу, которая может отправлять запросы SOAP при помощи данного интерфейса. Вы можете использовать специальный компонент Delphi, преобразую- щий запросы к локальному интерфейсу в вызовы SOAP (я сомневаюсь, что вам захо- чется вручную формировать XML-код, необходимый для генерации запроса SOAP). Также вы можете определить интерфейс (или использовать один из имеющих- ся) и при помощи специального компонента Delphi сформировать описание этого интерфейса на языке WSDL. Еще один компонент позволит вам выполнить ото- бражение из SOAP в Pascal. Таким образом, добавив этот компонент и объект, об- ладающий определенным вами интерфейсом, в программу, работающую на сторо- не сервера, вы можете создать полноценную, функционирующую веб-службу за считанные минуты. Лингвистический перевод при помощи BabelFish В качестве первого примера программы, обращающейся к веб-службе, я намерен рассмотреть небольшое клиентское приложение, взаимодействующее со службой лингвистического перевода BabelFish, поддерживаемой компанией AltaVista. Дан- ную службу, равно как и другие службы, отлично подходящие для эксперименти- рования, можно обнаружить на веб-узле XMethods (www.xmethods.com). Чтобы создать программу, я загрузил с веб-узла XMethods WSDL-описание службы BabelFish (это описание входит в комплект поставки исходного кода, при- лагаемого к данной книге). После этого при помощи вкладки Web Services (Веб- службы) диалогового окна New Items (Новые элементы) запустил модуль импорта Web Services Importer и выбрал файл описания службы. Мастер позволяет вам пред- варительно просмотреть структуру службы (рис. 23.1) и сгенерировать для дан- ной службы следующий интерфейс Object Pascal (многочисленные комментарии опущены): unit BabelFishService. interface uses InvokeRegistry. SOAPHTTPCIient. Types. XSBuiltlns: type 0990
Веб-службы 991 BabelFishPortType = interface!I Invokable) [ ’ {D2DB6712-EBE0-1DA6-BDEC-8A445595AE0C} 'J function BabelFish(const translationmode: WideString; const sourcedata: WideString): WideString; stdcall; end: function GetBabelFishPortTypedJseWSDL: Boolean=System.False; Addr: string»''; HTTPRIO: THTTPRIO = nil): BabelFiShPortType; implementation // опущено initialization InvRegistry.RegisterInterface(TypeInfo(BabelFi shPortType). 'urn-.xmethodsBabelFish'. "); InvRegistry.RegisterDefaultSDAPAction(TypeInfo(BabelFishPortType). 'urn;xmethodsBabelFish#BabelFish'); end. gw^Pit«w)aariiutw| О BabeFish WSDL Import Wizard & BabeFishSeivice fe. Interfaces Рис. 23.1. Мастер WSDL Import Wizard в действии BabelFishPortTyp* interface(Ilnvokabl*) f *(D2DB6712-IBS0-1DA6-8DZC-8A445S95AZ0C)11 function Bebeiyishfconst translationnode: VideS end; function G*tBab«lTxs№ortTyp« (UssWSDL: Boole*n=Syste Прежде всего, обратите внимание на то, что интерфейс является дочерним по отношению к интерфейсу Ilnvokable. Если сравнивать с базовым интерфейсом Delphi под именем Ilnterface, то в состав интерфейса Ilnvokable не входит никаких дополнительных методов, однако в отличие от Ilnterface, интерфейс Ilnvokable ком- пилируется с использованием флага, указывающего на генерацию RTTI ({$М+}), как и класс TPersistent. Изучив приведенный фрагмент кода, можно также обнару- жить, что в разделе initialization интерфейс зарегистрирован в глобальном реестре вызовов (global invokation registry, или InvRegistry). В процессе регистрации в ре- естр передана информация о типе интерфейса. 0991
992 Глава 23. Веб-службы и SOAP ПРИМЕЧАНИЕ---------------------------------------------------------—- Наличие RTTI-информации для интерфейсов является наиболее важным технологическим преиму- ществом вызовов с использованием SOAP. Конечно же, отображение SOAP-Pascal играет важную роль в деле упрощения разработки программ SOAP, однако наличие RTTI для интерфейса также явля- ется чрезвычайно важным фактором, так как делает всю архитектуру более мощной и надежной. Третьим элементом модуля, сгенерированного мастером WSDL Import Wizard, яв- ляется глобальная функция, названная так же, как и сервис. Это нововведение, появившееся в Delphi 7. Эта функция помогает упростить код, используемый для обращения к веб-службе. Функция GetBabelFishPortTyре возвращает интерфейс под- ходящего типа, который можно использовать для обращения к методу напрямую. Например, следующий код переводит короткое предложение с английского на итальянский (два языка идентифицируются в первом параметре en_it) и отобра- жает результат перевода на экране: ShowMessage (GetBabelFishPortType.BabelFish( 'enjt'. ’Hello, world! ’)); Если вы взглянете на код функции GetBabelFishPortType, вы увидите, что для об- работки вызова функция создает внутренний компонент THTTPRIO. Вы можете так- же вручную поместить этот компонент на клиентскую форму (именно так я посту- пил в рассматриваемом примере). В результате вы получаете больший контроль над разнообразными параметрами этого компонента и можете обрабатывать свя- занные с ним события. Компонент можно настроить двумя основными способами. Во-первых, вы мо- жете сослаться на WSDL-файл (или URL этого файла), импортировать его и из- влечь из него URL для вызова SOAP. Во-вторых, вы можете напрямую передать URL для вызова. В примере существует два компонента, которые используют два описанных альтернативных подхода, однако в обоих случаях эффект получается один и тот же: object HTTPRI01: THTTPRIO WSDLLocation = 'BabelFishService.xml' Service = 'BabelFish' Port = 'BabelFishPort' end object HTTPRI02: THTTPRIO URL = ’http://services.xmethods.net:80/perl/soaplite.cgi' end Осталось совсем немного. Мы обладаем информацией, необходимой для обра- щения к веб-службе, кроме того, мы знаем об аргументах, которые принимаются единственным методом. Теперь необходимо получить интерфейс, к которому тре- буется обратиться из компонента HTTPRIO. Для этого используется выражение, подобное HTTPRIO1 as BabelFish PortType. Поначалу это может показаться удивитель- ным, однако на самом деле все происходит невероятно просто. В рассматриваемом примере обращение к веб-службе выполняется в следующих двух строках: EditTarget.Text .-= (HTTPRIO1 as BabelFishPortType). BabelF1sh(ComboBoxType.Text. Ed 11Source.Text): Информация, выводимая программой на экран, показана на рис. 23.2. Теперь вы можете изучать иностранные языки (правда, учитель далеко не совершенен). Здесь я не буду разрабатывать другие аналогичные программы, позволяющие по- лучать информацию о валютах, прогнозах погоды, ценах на акции и использую- 0992
Построение веб-служб 993 щие многие другие службы, доступные в настоящее время через Веб, так как все эти программы выглядели бы во многом одинаково. ВНИМАНИЕ------------------------------------------------------------------ Несмотря на то что интерфейс обеспечивает вас сведениями о типе параметров, во многих случаях для того, чтобы узнать истинный смысл параметров и то, как они интерпретируются службой, вам потребуется изучить документацию службы. Например, прежде чем обращаться к веб-службе BabelFish, мне потребовалось изучить документацию этой службы, чтобы узнать, с какими языками она работает и каким может быть значение первого параметра главного метода этой службы. BabelFish I* Translate 11 |Th$isa sample message foiari। automatic translation [en_de |diese$ ist eme Beispielanzeige fur eine automatische Ubersetzung Ditect j Рис. 23.2. Пример перевода с английского на немецкий, полученный с использованием веб-службы BabelFish, разработанной компанией AltaVista Построение веб-служб Как мы увидели в предыдущем разделе, обращение к веб-службе с использовани- ем Delphi выполняется чрезвычайно просто. То же самое можно сказать и о созда- нии собственной веб-службы. На вкладке Web Services (Веб-службы) диалогового окна New items (Новые элементы) вы можете выбрать SOAP Server Application (При- ложение SOAP-сервер). Если вы выберете этот вариант, Delphi предложит вам список, сильно напоминающий список, появляющийся на экране при выборе при- ложения WebBroker. Как правило, веб-служба функционирует на веб-сервере. Для этого используется одна из технологий расширения возможностей веб-сервера (CGI, ISAPI, модули Apache и т. п.). Для тестирования можно использовать Web Арр Debugger: После завершения этого этапа Delphi добавит в результирующий веб-модуль (который является обычным веб-модулем без каких-либо специальных добавле- ний) три компонента. О HTTPSoapDispatcher занимается приемом веб-запроса и функционирует в точно- сти как любой другой диспетчер HTTP. О HTTPSoapPascallnvoker выполняет операцию, обратную той, которая выполняет- ся компонентом HTTPRIO. Другими словами, он может транслировать запросы SOAP в обращения к интерфейсам Pascal. О WSDLHTMLPublish используется для формирования WSDL-определения служ- бы, исходя из поддерживаемых ею интерфейсов. Этот компонент выполняет операцию, обратную той, которая выполняется мастером импорта служб Web Services Importer Wizard. Технически этот компонент является еще одним дис- петчером HTTP. 0993
994 Глава 23. Веб-службы и SOAP Веб-служба конвертации валют Когда структура из трех компонентов добавлена в веб-модуль, можно приступать к написанию службы. В качестве примера я возьму программу конвертации евро из главы 3 и преобразую эту программу в веб-службу под названием ConvertService. Для этого, прежде всего, я добавил в программу модуль, определяющий интер- фейс службы: type IConvert = interface(IInvokable) [ • {FF1EAA45-0B94-4630-9A18-E768A91A78E2} '] function ConvertCurrency (Source. Dest: string: Amount: Double): Double: stdcal1: function ToEuro (Source: string. Amount: Double): Double: stdcall: function FromEuro (Dest: string: Amount- Double)- Double; stdcall: function TypesList: string; stdcall; end: Интерфейс определяется непосредственно в коде, без использования таких инструментов, как Type Library Editor, благодаря этому вы можете быстро построить интерфейс для существующего класса, и вам не требуется изучать какие-либо до- полнительные инструменты для этой цели. Обратите внимание, что я, как и обыч- но, назначил интерфейсу уникальный идентификатор GUID. Кроме того, при об- ращении к функциям интерфейса используется соглашение о передаче параметров stdcall. Это происходит из-за того, что конвертер SOAP не поддерживает соглаше- ние register, используемое по умолчанию. Определяя интерфейс как службу, необходимо зарегистрировать его в том же модуле, где содержится его определение. Связанная с этим операция пригодится и на стороне клиента, и на стороне сервера, так как ожидается, что модуль, содер- жащий определение интерфейса, будет использоваться как при разработке серве- ра, так и при разработке клиента. uses InvokeRegistry: Initialization InvRegistry.Register!nterface(TypeInfo(IConvert)); Теперь, когда у нас есть интерфейс, который мы можем предоставить для ис- пользования широкой общественности, осталось разработать его реализацию. Как и всегда, для этого используется код, написанный на стандартном языке Delphi с применением заранее определенного класса TInvokableClass: type TConvert = class (TInvokableClass. IConvert) protected function ConvertCurrency (Source. Dest- string: Amount: Double) Double; stdcal1; function ToEuro (Source: string. Amount: Double) Double, stdcall; function FromEuro (Dest: string: Amount: Double). Double: stdcall; function TypesList: string, stdcall; end. Здесь я не буду подробно рассматривать реализацию этих функций, так как речь о них шла в главе 3, где рассматривалась система конвертации евровалют. Дело в том, что реализация данных функций почти не имеет отношения к основной 0994
Построение веб-служб 995 теме дискуссии — разработке веб-службы в среде Delphi. Необходимо отметить, что в разделе инициализации модуля, содержащего в себе реализацию этих функ- ций, также содержится обращение к функции регистрации: InvRegistry.RegisterlnvokableClass(TConvert): Публикация WSDL Зарегистрировав интерфейс, мы предоставляем программе возможность сгенериро- вать WSDL-описание. Приложение веб-службы (начиная с обновления Delphi 6.02) поддерживает отображение первой страницы, описывающей ее интерфейсы, и под- робное описание каждого из интерфейсов, а также возвращает клиенту WSDL- файл. Иными словами, подключившись к веб-службе при помощи браузера, вы увидите нечто похожее на рис. 23.3. РЙе goofcnwks Tools Window fcfelp > convertservice - PortTypes: $ converter vice - Mozilla Рис. 23.3. Описание веб-службы ConvertService, представленное в виде веб-страницы ПРИМЕЧАНИЕ-------------------------------------------------------------- Многие другие архитектуры веб-служб обеспечивают вас возможностью обратиться к веб-службе через браузер, однако, по сути, эта возможность является бессмысленной, так как основное пред- назначение веб-службы — это обеспечение взаимодействия между несколькими сетевыми прило- жениями. Если вы хотите отобразить какие-либо данные в рабочем окне веб-браузера, для этого вовсе не обязательно разрабатывать веб-службу — достаточно построить веб-узел. icon vert rWSDLl о ConvertCurrency о ToEuro о FromEuro о Types List IWSDLPublish fWSDU u.sts ill thb Por~¥pe< published by this Service ' о GetPortTypeUst . о GetWSDLForPortType $ о GetTypeSystemsList 5 о GetXSDForTypeSystem WSIL: Lirx to WS Inspection document of Services here Генерация описания веб-службы не поддерживалась в Delphi 6 (вместо этого Delphi 6 обеспечивала лишь низкоуровневое описание WSDL), однако в Delphi 7 его очень просто добавить и настроить. Если вы взглянете на веб-модуль Delphi 7 SOAP, обратите внимание на обработчик события OnAction (действие по умолча- нию), в котором выполняется следующее обращение: WSDLHTMLPublishl.ServiceInfo(Sender. Request, Response, Handled): 0995
996 Глава 23. Веб-службы и SOAP Это все, что необходимо сделать, чтобы добавить эту возможность в существу- ющую веб-службу Delphi. Чтобы обеспечить подобную функциональность вруч- ную, вы должны обратиться к реестру вызовов (глобальный объект InvRegistry) при помощи таких вызовов, как GetlnterfaceExternalName и GetMethExternalName. Хочу еще раз отметить важную особенность веб-службы: она может докумен- тировать сама себя при помощи WSDL. Создание специального клиента Теперь можно приступить к рассмотрению методов разработки клиентского при- ложения, обращающегося к службе. На этот раз нет надобности начинать этот про- цесс с обработки WSDL-файла, так как у нас уже есть готовое описание интерфей- са на языке Pascal. Теперь в форме нет даже компонента HTTPRIO, так как этот компонент создается прямо в коде: private Invoker: THTTPRio; procedure TForml.FormCreate(Sender- TObject): begin Invoker := THTTPRio.Createtnil): Invoker.URL := 'http:I/local host/scripts/ConvertServ1ce.exe/soap/1convert'; Convlntf := Invoker as IConvert. end: Вместо использования WSDL-файла компонент Invoker, относящийся к SOAP, связывается с URL. После формирования этой связи и после того, как требуемый интерфейс извлекается из компонента, можно приступить к разработке обычного Pascal-кода для обращения к службе (об этом уже рассказывалось ранее). Когда пользователь выбирает два значения в ниспадающих списках, происхо- дит обращение к методу ТуpesList, который возвращает перечень валют в виде стро- ки наименований, разделенных символами точки с запятой. Символы точки с за- пятой заменяются на символы новой строки, а затем несколько строк напрямую присваиваются элементам ниспадающего списка: procedure TForml Button2Click(Sender: TObject): var TypeNames: string: begin TypeNames := Convlntf TypesList: ComboBoxFrom,Items Text .= StringReplace (TypeNames. sLineBreak. ErfRepIaceAIl]); ComboBoxTo Items •= ComboBoxFrom.Items: end: Как только пользователь выбрал две валюты, можно выполнить преобразова- ние. Для этого используется следующий код: procedure TForml.ButtonlClick(Sender TObject). begin LabelResult Caption : = Format ('£n'. [(Convlntf ConvertCurrency( ComboBoxFrom.Text. ComboBoxTo Text. StrToFloat(EditAmount Text)))]): end. Результат работы программы показан на рис. 23.4. 0996
Построение веб-служб 997 Convert CoIler ЕШ list [ 938.999.1? Рис. 23.4. Клиент ConvertCaller обращается к веб-службе ConvertService, которая подсказывает, сколько итальянских лир можно было получить за указанное количество немецких марок до того, как в мире появилась новая валюта евро Запрос на получение данных из базы данных В данном примере я построил веб-службу (основанную на Web Арр Debugger), кото- рая позволяет внешнему клиенту получить информацию о сотрудниках компании. Данные содержатся в таблице EMPLOYEE базы данных InterBase, с которой мы уже неоднократно имели дело в этой книге. Delphi-интерфейс веб-службы определя- ется в модуле SoapEmployeelntf следующим образом: type ISoapEmployee = Interface (Ilnvokable) ['{77D0D940-23EC-49A5-9630-ADE0751E3DB3} ’] function GetEmployeeNames; string, stdcall; function GetEmployeeData (EmpID string)- string, stdcall; end. Первый метод возвращает перечень имен всех сотрудников компании, а второй возвращает подробные сведения об указанном сотруднике. Реализация данного интерфейса содержится в модуле SoapEmployeelmpl, интерфейс реализован следу- ющим классом: type TSoapEmployee = class(TinvokableClass. IsoapEmployee) publ1c function GetEmployeeNames string: stdcall, function GetEmployeeData (EmpID. string) string, stdcall. end: Вся реализация веб-службы заключена в двух этих методах, а также в несколь- ких вспомогательных функциях, которые осуществляют обработку возвращаемых XML-данных. Прежде чем мы перейдем к коду, работающему с XML, позвольте мне кратко рассмотреть код, имеющий дело с базой данных. Доступ к данным Весь код, связанный с подключением к базе данных и формированием запросов SQL, располагается в отдельном модуле данных. Конечно же, я мог бы создать со- единение и компоненты наборов данных непосредственно внутри методов, однако 0997
998 Глава 23. Веб-службы и SOAP это противоречило бы общей идеологии визуального инструмента программиро- вания Delphi. Вот структура модуля данных: object DataModule3: TDataModule3 object SQLConnection: TSQLConnection ConnectlonName = 'IBConnection’ DriverName = 'Interbase' LoginPrompt = False Params.Strings = // пропущено end object dsEmpLIst: TSQLDataSet CommandText = 'select EMP_NO, LAST_NAME. FIRST_NAME from EMPLOYEE' SQLConnection = SQLConnection object dsEmplListEMP_NO: TStringField object dsEmplListLAST_NAME: TStringField object dsEmplListFIRST_NAME: TStringField end object dsEmpData: TSQLDataSet CommandText = 'select * from EMPLOYEE where Emp_No - :id' Params = < item Datatype - ftFixedChar Name = 'id' ParamTYpe = ptinput end> SQLConnection = SQLConnection end end Можно видеть, что модуль данных обладает двумя SQL-запросами, которые содержатся в компонентах SQLDataSet. Первый запрос используется для того, что- бы получить из базы данных имя и ID каждого сотрудника. Второй запрос извле- кает из базы данных всю информацию о сотруднике с указанным ID. Обработка ХМ L-доку ментов Проблема состоит в том, как вернуть эти данные удаленной клиентской програм- ме. В данном примере я использую подход, который нравится мне больше всего: вместо того чтобы работать со сложными структурами данных SOAP, я возвра- щаю клиенту XML-документы. (Я не могу понять, почему XML используется в рамках SOAP в качестве механизма передачи сообщений и при этом не исполь- зуется для форматирования передаваемых данных. До сих пор лишь немногие веб- службы в качестве результатов своей работы возвращают XML-документы, и я не понимаю, почему другие программисты не используют XML в полной мере.) В данном примере метод GetEmployeeNames создает XML-документ, содержа- щий перечень сотрудников, при этом имя и фамилия каждого сотрудника указы- ваются в качестве значений узлов, a ID сотрудника — в качестве атрибута. Для этой цели используются две вспомогательные функции: MakeXmlstr (ее я уже рас- сматривал в предыдущей главе) и MakeXmlAttribute (ее текст приводится далее): function TSoapEmployeeNames: string; var dm: TDataModule3; begin dm := TDataModule3.Create (ml); try 0998
Построение веб-служб 999 dm.dsEmpL1st.0pen; Result := ’<employeeList>' + sLineBreak: while not dm.dsEmplList.EDF do begin Result := Result + ' ' + MakeXmlStr ('employee'. dm.dsEmplListLASTNAME.AsString + ’ ' + dm.dsEmplList FIRSTNAME.AsStrlng. MakeXmlAttribute ('id', dm.dsEmplListEMPND.AsString)) + sLineBreak; dm.dsEmplList.Next; end; Result := Result + '</employeeList>‘ . finally dm.Free; end: end; function MakeXmlAttribute (attrName. attrValue: string): string; begin Result ;= attrName + + attrValue + ""; end: Вместо того чтобы генерировать XML-код вручную, я мог бы использовать для этой цели XML Mapper или какую-либо другую специальную технологию, одна- ко, как объяснялось в главе 22, создание XML напрямую при помощи строк — это наиболее быстрый подход. Я планирую использовать XML Mapper на стороне кли- ента для обработки данных, принимаемых от сервера. ПРИМЕЧАНИЕ--------------------------------------------------------------------------------- Вы можете удивиться, зачем в программе каждый раз создается новый экземпляр модуля данных. Ведь при создании модуля данных заново создается новое подключение к базе данных (это доста- точно медленная операция). Однако существует также преимущество: такой подход хорошо рабо- тает в многопоточной среде. Если два запроса к веб-службе обрабатываются в одно и то же время, вы можете использовать общее подключение к базе данных, однако при этом для доступа к данным должны использоваться разные наборы данных. Вы можете переместить наборы данных в код фун- кций и сохранить только подключение к модулю данных или, напротив, использовать глобальный общий модуль данных для подключения (который используется одновременно несколькими потока- ми) и специальный экземпляр второго модуля данных, в котором содержатся наборы данных для каждого обращения к методу. Теперь давайте взглянем на второй метод GetEmployeeData. Этот метод исполь- зует запрос с параметрами и форматирует значения полей в виде отдельных узлов XML (для этого используется еще одна вспомогательная функция, FieldsToXml): function TsoapEmployee.GetEmployeeData(EmpID: string): string; var dm: TDataModule3: begin dm := TDataModule3.Create (nil); try dm.dsEmpData.ParamByName('ID').AsString := Empld: dm.ds EmpData.Open; Result := FieldsToXml ('employee', dm.dsEmpData): finally dm.Free; end; end: 0999
1000 Глава 23. Веб-службы и SOAP function FieldsToXml (rootName string, data TDataSet): string. var i Integer. begin Result = '<’ + rootName + '>' + sLineBreak.: for i =0 to data FieldCount - 1 do Result = Result + ' ' + MakeXmlStr ( LowerCase (data FieldsCi] FieldName), data Fields[iJ AsString) + sLineBreak Result = Result + ’</' + rootName + '>’ + sLineBreak . end. Клиентская программа (на основе XML Mapping) Теперь приступим к разработке клиентской программы. Для этого, как обычно, требуется импортировать WSDL-файл, определяющий веб-службу. В данном слу- чае также нужно преобразовать принятые клиентом XML-данные (а именно спи- сок сотрудников, возвращаемый методом GetEmployeeNames) в нечто более удоб- ное для обработки. Для этой цели я использую механизм XML Mapper, который преобразует список сотрудников, полученный от веб-службы, в набор данных, ко- торый можно отобразить на экране при помощи элемента управления DBGrid. Чтобы осуществить это, я прежде всего добавил в программу код, который при- нимает XML со списком сотрудников и копирует его в компонент Мето, а затем — в файл. После этого я открываю XML Mapper, загружаю файл и на основе его со- держимого генерирую структуру пакета данных и файл трансформации. (Файл трансформации можно найти среди файлов исходного кода примера SoapEmployee.) Чтобы отобразить XML-данные в сетке DBGrid, программа использует компонент XMLTrasformProvider, ссылающийся на файл трансформации: object XMLTransformProviderl TXMLTRansformProvider TransformRead TransformationFile = 'EmplListToDataPacket xtr' end Компонент ClientDataSet не соединен с провайдером, так как в этом случае он попытался бы открыть файл XML-данных, указанный в трансформации. В нашем случае XML-данные располагаются не в файле — они передаются компоненту после обращения к веб-службе. По этой причине программа перемещает данные в ком- понент ClientDataSet напрямую в коде: procedure TForml btnGetListClick(Sender TObject) var strXml string begin strXml - GetlSoapEmployee GetEmployeeNames strXML = XMLTransformProviderl TransformRead TransformXML(strXml). ClientDataSetl Xml Data = StrXml ClientDataSetl Open end. Используя этот код, программа отображает список сотрудников в сетке DBGrid (как показано на рис. 23.5). Когда вы извлекаете данные для некоторого конкрет- ного сотрудника, программа извлекает ID активной записи из ClientDataSet и ото- бражает результирующий XML-код в элементе управления Мето' procedure TForml btnGetDetailsClick(Sender TObject) begin 1000
Построение веб-служб 1001 Memo2 Lines Text = GetSoapEmployee GetEmployeeData( ClientDataSetl FieldByName (’id’) AsString). end ^Soap Employee Client empname....... , [id __ Nelson Robert 2 Young Bruce 4 ► Lambert Kim 5 JohnsonLeslie 8 Forest Phil 9 Weston KJ 11 Lee 1 ern 12 Hall Stewart 14 Young Katherine 15 Papadopoulos Chris 20 Fisher Pete 24 Sennet Ann 28 De Souza Roger 29 < employ ee> <emp_no> 5< /emp_no> < hrst_name> Kim</fir$t_name> <lasl_name> LamberK /last_name> <phone_ext> 22< /phone_exl> < hire_date> 2/6/1989< /hire_dale> <dept_no> 190</dept_no> <iob_code> E ng< /|ob_code> < pb_grade> 2< /(ob_grade> <|Ob_country>U5A</[ob_country> <salary> 102750< /salary> <MI_name>Lamberl Kim</fult_name> </employee> Рис. 23.5. Клиентская программа для обращения к веб-службе из примера SoapEmployee Отладка заголовков SOAP Завершая рассмотрение данного примера, я хочу рассказать об использовании Web Арр Debugger для тестирования приложений SOAP. Конечно же, вы можете запус- тить серверную программу в Delphi IDE и с легкостью отладить ее, однако вы мо- жете также следить за заголовками SOAP, передаваемыми через соединение HTTP. Изучение информации SOAP на этом достаточно низком уровне может оказаться непростым делом, однако это весьма надежный способ проверить корректность работы серверного и клиентского приложений. На рис. 23.6 показана последова- тельность сообщений HTTP для выполнения SOAP-запроса из последнего при- мера. Использовать Web Арр Debugger удается далеко не всегда, поэтому для отладки можно воспользоваться возможностью обработки событий компонента HTTPRIO, как это сделано в примере BabelFishDebug. На форме программы располагаются два компонента Мето, в которых вы можете видеть текст SOAP-запроса и текст SOAP- ответа: procedure TForml HTTPRI01BeforeExecute(const MethodName String. var SOAPRequest WideString) begin MemoRequest Text = SoapRequest end procedure TForml HTTPRIOlAfterExecutelconst MethodName String. SOAPREsponse TStream) begin SOAPREsponse Position = 0. MemoResponse Line LoadFromStream(SOAPResponse). end 1001
1002 Глава 23. Веб-службы и SOAP X I ogbetdil POST /SoapEmplQ^Server.soapempl/soap/ISo4«£rnplQMee НТТР/11 SOAPActwn. "urn So««£mpkyeelntf-ISoapEmptojiee#GetEmployed)ata’’ Я Content-Type text/xml User-Agent Borland SOAP 1 2 Host locahost 1024 И--------------“» Content-Length 508 Connection Keep-Alive Cache-Control no-cache <?xml ver$»on«'T 0"Ъ <S0AP-ENV Envelope xmlns SOAP-ENV="http //schemas xmisoap org/soap/envelope/** xmlns x$d«"http //www w3 org/2001/XMLSchema'1 xmlns x$r»"http //www w3 org/2001/XMLSchema-nstance" xmlns SOAP-ENC-'Yiltp //schemas xmisoap org/soap/encoding/“><SOAP-ENV Body SOAP-ENV encodngSlyle="http //schemas xmisoap org/soap/encodrig/,*><NS1.GatEmpiQyeeData xmlns NS 1 «"wn SoapEmployeeintf ISoap£mployee"><EmplD хя.type«”xsd string'> 11 </£mp/D></NSI GetEmptoyeeData></SOAP-ENV Body><ZSOAP-ENV Envelope> ‘Г 1Г«^Ыв₽ЯЙЙч Рис. 23.6. Журнал сообщений HTTP, отображаемый Web Арр Debugger, показывает текст низкоуровневого запроса SOAP Доступ к существующему классу как к веб-службе Мы с вами рассмотрели, как выполняется разработка веб-службы с нуля. Но можно ли разработать веб-службу на основе уже готового кода? Это не так уж и сложно благодаря открытой архитектуре Delphi. Для решения задачи выполните следую- щие действия. 1. Создайте приложение веб-службы или добавьте необходимые компоненты к существующему проекту WebBroker. 2. Определите интерфейс, производный от Ilnvokable, и добавьте к нему методы, которые должны быть доступны в рамках веб-службы (эти методы должны ис- пользовать соглашение о вызове stdcall). Методы должны быть такими же, как методы класса, на базе которого вы хотели бы построить веб-службу. 3. Определите новый класс, являющийся производным от класса, который вы хотели бы использовать в качестве основы веб-службы. Этот класс должен реа- лизовывать ваш интерфейс. В рамках реализации каждого из методов должно осуществляться обращение к методам базового класса. 4. Напишите фабричный метод, который должен создавать объект класса реали- зации каждый раз, когда возникает необходимость обработки SOAP-запроса. Последний шаг является наиболее сложным. Можно было бы определить фаб- рику и зарегистрировать ее следующим образом: procedure MyObjFactory (out Obj: TObject): begin Dbj .= TmylmplClass.Create; end. 1002
DataSnap через SOAP 1003 initialization InvRegistry.RegisterlnvokableClass(TmylmplCl ass. MyObjFactory); Однако данный код создает новый объект для каждого обращения к веб-служ- бе. Использовать единственный глобальный объект тоже плохо: с этим объектом могут работать одновременно несколько пользователей, поэтому если объект хра- нит в себе информацию о состоянии или его методы по тем или иным причинам не могут выполняться одновременно, возникнут серьезные проблемы. Остается един- ственный выход: реализовать механизм управления рабочими сеансами (пример- но такую же проблему мы решали ранее при разработке веб-службы, обращаю- щейся к базе данных). DataSnap через SOAP Теперь, когда вы получили представление о том, как в среде Delphi осуществляет- ся разработка SOAP-сервера и SOAP-клиента, мы можем применить технологию SOAP для построения многозвенного приложения DataSnap. Для создания новой веб-службы мы будем использовать модуль Soap Server Data Module, а для подклю- чения клиентского приложения к этой службе применим компонент SoapConnection. Построение сервера DataSnap SOAP Прежде всего, займемся сервером. Для создания сервера необходимо перейти на вкладку Web Services (Веб-службы) диалогового окна New Items (Новые элементы) и при помощи значка Soap Server Application (Приложение сервера Soap) создать новую веб-службу. Далее при помощи значка Soap Server Data Module (Модуль дан- ных сервера Soap) следует добавить в сервер SOAP серверный модуль данных DataSnap. Именно это я сделал при разработке примера SoapDataServer (для тести- рования этого примера использовалась архитектура Web Арр Debugger). После это- го необходимо разработать обычный сервер DataSnap (или, точнее, приложение DataSnap, выполняющее функции среднего звена в трехзвенной архитектуре), ис- пользуя методы, описанные в главе 16. В данном конкретном случае я использо- вал dbExpress для добавления в программу возможности доступа к InterBase. В ре- зультате получилась следующая структура: object SoapTestDm: TSoapTestDm object SQLConnectionl: TSQLConnection ConnectionName = 'IBLoca1' end object SQLDataSetl: TSQLDataSet SQLConnection = SQLConnectionl CommandText = 'select * from EMPLOYEE' end object DataSetProviderl TDataSetProvider DataSet = SQLDataSetl end end Модуль данных, построенный для сервера DataSnap, основанного на SOAP, обладает специальным интерфейсом (к которому вы можете добавлять методы), 1003
1004 Глава 23. Веб-службы и SOAP являющимся потомком интерфейса lAppServerSOAP. Этот интерфейс определен как опубликованный (published) несмотря на то, что он не является потомком интер- фейса Ilnvokable СОВЕТ ---------------------------------------------------------------------------- В Delphi 6 для публикации данных через SOAP использовался стандартный интерфейс lAppServer. В Delphi 7 вместо этого используется производный интерфейс lAppServerSOAP. Его функциональ- ность точно такая же, однако система получает возможность определить тип обращения в зависи- мости от имени интерфейса. В скором времени вы увидите, каким образом осуществляется обращение к старому приложению при помощи клиента, разработанного в среде Delphi 7. К сожалению, эта процедура не автоматизирована. Класс реализации TSoapTestDm сам по себе является модулем данных, как и дру- гие типы серверов DataSnap. Среда Delphi сформировала для меня следующий код: type ISampl eDataModulе = interface!lAppServerSOAP) ['{D47A293F-4024 4690-9915 8A68CB273D39} ] function GetRecordCount Integer stdcall end TSampleDataModule = class(TsoapDataModule ISampleDataModule. lAppServerSOAP lAppServer) DataSetProviderl TDataSetProvider SQLConnectionl TSQLConnection SQLDataSetl TSQLDataSet public function GetRecordCount Integer stdcall end Базовый класс TSoapDataModule не является потомком класса TInvokableClass, однако это не станет для вас проблемой, если вы разработаете дополнительную фабричную процедуру создания объекта (именно эту задачу решает класс TInvo- kableClass) и добавите эту процедуру в код регистрации (как рассказывалось ранее, в разделе «Доступ к существующему классу как к веб-службе»)'. procedure TSampleDataModuleCreateInstance(out obj TObject) begin obj = TSampleDataModule Create(ml) end initialization InvRegistry RegisterInvokableClass(TSampleDataModule TSampleDataModuleCreatelnstance). InvRegistry RegisterInterface(TypeInfo(ISampleDataModule)) Благодаря небольшому кусочку кода в модуле SOAPMidas серверное приложе- ние также публикует интерфейсы lAppServerSOAP и lAppServer Вы можете сравнить рассмотренный код с сервером SOAP DataSnap, разработанным в среде Delphi 6. Соответствующий код располагается в папке SoapDataServer Этот код по-прежне- му можно откомпилировать в Delphi 7, и он будет отлично работать, однако при разработке новых приложений следует использовать новый, описанный здесь под- ход Соответствующий код располагается в папке SoapDataServer7 СОВЕТ-----------------------------------------------------------------------------------— Приложения веб-служб в Delphi 7 могут включать в себя более одного модуля данных SOAP. Для идентификации модулей данных можно использовать свойство SOAPServerllD компонента Soap- Connection или добавить имя интерфейса модуля данных в конец URL-адреса. 1004
DataSnap через SOAP 1005 Сервер обладает специальным методом (версия программы, разработанная в среде Delphi 6, тоже обладает таким методом, однако он не срабатывает), который использует SQL-запрос с выражением: function TsampleDataModule GetRecordCount Integer. begin // читаем счетчик записей выполнив запрос SQLDataSet2 Open Result = SQLDataSet2 FieldsEO] Aslnteger SQLDataSet2 Close end Построение клиента DataSnap SOAP Чтобы создать клиентское приложение под названием SoapDataClient7, я добавил в программу компонент SoapConnection (со страницы Web Services палитры) и связал его с URL веб-службы DataSnap, ссылаясь при этом на специальный интерфейс: object SoapConnectionl TSoapConnection URL = 'http //localhost 1024/SoapDataServer7 soapdataserver/soap/Isampledatamodule' SOAPServerllD = 'lAppServerSOAP - {C99F4735-D6D2-495C 8CA2-E53E5A439E61}' UseSOAPAdapter = False end Обратите внимание на последнее свойство, UseSOAPAdapter, которое указывает на то, что вы работаете с сервером, разработанным в среде Delphi 7. Сравните этот код с кодом клиента SoapDataCLient (без семерки на конце). Эта версия использует сервер, разработанный в среде Delphi 6 и откомпилированный в среде Delphi 7. Значение свойства UseSOAPAdapter в этом случае должно быть равно True. Это зна- чение принуждает программу использовать обычный интерфейс lAppServer вместо нового интерфейса lAppServerSOAP. Затем я продолжил разработку клиентского приложения так, как если бы это была традиционная программа DataSnap: я добавил в нее компонент ClientDataSet, далее ввел DataSource и DBGrid, после чего выбрал для клиентского набора данных единственный доступный провайдер и связал остальные компоненты обычным образом. Не удивительно, что данное приложение обладает некоторым специаль- ным дополнительным кодом: для того чтобы избежать ошибок в начале работы, соединение устанавливается в результате щелчка на кнопке, а для пересылки из- менений обратно в базу данных используется вызов ApplyUpdates. Преимущество SOAP по сравнению с другими соединениями DataSnap Несмотря на то что данное приложение сильно напоминает другие клиентские и серверные приложения DataSnap, разработанные в главе 16, все же у рассматри- ваемой здесь программы есть одно очень существенное отличие: для взаимодей- ствия с использованием интерфейса lAppServerSOAP программы SoapDataServer и SoapDataCLient, основанные на SOAP, не используют СОМ. Напротив, соедине- ния DataSnap, основанные на сокетах и HTTP, функционируют с использованием локальных COM-объектов и регистрации сервера в реестре Windows. Поддержка SOAP, встроенная в Delphi, позволяет реализовать решение, совершенно не зави- симое от СОМ. Такое решение существенно проще адаптировать для использова- 1005
1006 Глава 23. Веб-службы и SOAP ния в других операционных системах (в отличие от программ, рассматривавшихся в главе 16, приложения, основанные на SOAP, могут быть перекомпилированы в среде Kylix). Клиентская программа может также обратиться к специальному методу, кото- рый я добавил в состав сервера (он возвращает значение счетчика записей). Этот метод может использоваться в реальных приложениях, чтобы загрузить на сторо- ну клиента лишь ограниченный набор записей и при этом оповестить пользовате- ля о том, какое количество записей остались не загруженными с сервера. Клиент- ский код, осуществляющий обращение к этому методу, основан на использовании дополнительного компонента HTTPRIO: procedure TFormSDC.Button3C11ck(Sender: TObject): var SoapData: ISampleDataModule: begin SoapData : = HttpRiol as ISampleDataModul e: ShowMessage (IntToStr (SoapData.GetRecordCount)); end; Обработка вложений Важной возможностью, впервые появившейся в составе Delphi 7, является под- держка вложений SOAP. Вложения (attachements) SOAP — это механизм, пред- назначенный для передачи по каналу SOAP помимо XML-текста каких-либо других данных, например бинарных файлов или изображений. В Delphi обработка вложе- ний основана на использовании потоков. Обязанность по трансформации потока байтов в поток с заданным методом кодирования возлагается на разрабатываемый вами код. Этот процесс не сложен, особенно если вспомнить, что в составе Indy присутствует некоторое количество кодирующих компонентов. Чтобы продемонстрировать порядок использования вложений, я написал про- грамму, которая передает через SOAP бинарное содержимое компонента Client- DataSet (помимо данных содержащего изображения) или отдельного графического изображения. Сервер обладает следующим интерфейсом: type ISoapFish = interface!Ilnvokable) ['{4E4C57BF-4AC9-41C2-BB2A-64BCE470D450}'] function GetCds; TsoapAttachement; stdcall; function GetlmagetfishName: string): TsoapAttachement; stdcall; end; Реализация метода GetCds использует компонент ClientDataSet, который ссыла- ется на классическую таблицу BIOLIFE, создает поток в памяти, копирует в него 1анные, азатем подключает поток к результату выполнения функции TSoapAttache- nent: "unction TSoapFish.GetCds: TsoapAttachement: stdcall; 'ar memStr: TMemoryStream; iegin Result TSOapAttachement.Create: memStr := TMEmoryStream.Create; 1006
Обработка вложений 1007 WebModule2.cdsFish.SaveToStream(MemStr): // бинарные данные Result.SetSourceStream (memStr. soReference); end: На стороне клиента я подготовил форму с компонентом ClientDataSet, подклю- ченным к DBGrid и DBImage. Все, что остается сделать: извлечь вложение SOAP временно сохранить его в поток, расположенный в памяти, затем копировать дан- ные из этого потока в локальный компонент ClientDataSet: procedure TForml.btnGetCdsClieck(Sender: TObject); var sAtt: TSoapAttachement: memSt r: TMemorySt ream: begin nRead := 0; sAtt := (HttpRiol as ISoapFish).GetCds; try memStr : = TMemoryStream.Create; try sAtt.SaveToStream(memStr); memStr.Position := 0: ClientDataSetl.LoadFromStream(MemStr): finally memStr.Free: end; finally DeleteFile (sAtt.CacheFile): sAtt.Free: end: end; ВНИМАНИЕ ----------------------------------------------------------------------------------- По умолчанию вложения SOAP, принятые клиентом, сохраняются во временном файле, на который ссылается свойство CacheFile объекта TSOAPAttachement. Если вы не удалите этот файл, он так и будет храниться в папке временных файлов. Графический вывод приложения (рис. 23.7) выглядит в точности так же, как если бы в клиентский набор данных было загружено содержимое обычного ло- кального файла. В этом клиенте SOAP я явно использую компонент HTTPRIO для того, чтобы следить за поступающими данными (скорее всего, данные обладают большим объемом, и их передача выполняется медленно). Прежде чем обращать- ся к удаленному методу, я присваиваю глобальной переменной nRead значение «ноль». В обработчике события On Receiving Data, соответствующего свойству НТТР- WebNode объекта HTTPRIO, принимаемые данные добавляются в переменную nRead. Параметры Read и Total, передаваемые обработчику, ссылаются на некоторый блок данных, переданный через сокет, поэтому для мониторинга прогресса передачи данных сами по себе они фактически бесполезны: procedure TForml.HTTPRI01HTTPWebNodelReceivingData( Read. Total: Integer): begin Inc (nRead. Read): StatusBarl.SimpleText IntToStr (nRead); Appiication.ProcessMessages; end: 1007
1008 Глава 23. Веб-службы и SOAP Рис. 23.7. Программа FishClient отображает бинарное содержимое ClientDataSet внутри вложения SOAP Поддержка UDDI Растущая популярность XML и SOAP открывает новые возможности по обеспе- чению взаимодействия бизнес-приложений различных предприятий (в рамках концепции В2В — «бизнес-бизнес»), XML и SOAP лежат в основе подобного вза- имодействия, однако этого не достаточно — ключевыми элементами подобной ар- хитектуры являются также стандартизация форматов XML и доступность инфор- мации о том или ином предприятии. Без этих ключевых элементов невозможно сформировать какое-либо реально работающее решение в области веб-служб. Для решения проблемы было предложено несколько стандартов, одним из ко- торых является UDDI (Universal Description, Discovery and Integration). Инфор- мацию об этом стандарте можно получить по адресу www.uddi.org. Для решения упомянутых задач помимо UDDI предлагается использовать также стандарт ebXML (Electronic Business using extensible Markup Language), информация о котором содержится на веб-узле www.ebxmL.org. Эти стандарты частично налагаются друг на друга и частично противоречат друг другу. В настоящее время консорциум OASIS (Organization for the Advancement of Structured Information Standards, www.oasis-open.org) продолжает работу над согласованием и дальнейшим развити- ем этих стандартов. Я не намерен углубляться в проблемы, связанные с бизнес- процессами, вместо этого я планирую рассказать вам о некоторых технических элементах UDDI, так как этот стандарт поддерживается в рамках Delphi 7. Что такое UDDI? Спецификация UDDI (Universal Description, Discovery and Integration — универ- сальное описание, исследование и интеграция) — это попытка создать каталог веб- служб, предлагаемых компаниями, расположенными в разных частях земного шара. 1008
Поддержка UDDI 1009 Целью этой инициативы является построение открытой глобальной платформен- но-независимой инфраструктуры, благодаря которой бизнес-субъекты могут ис- кать друг друга, определять порядок взаимодействия между собой и совместно использовать глобальный бизнес-реестр. Конечно же, все это делается для того, чтобы ускорить развитие электронной коммерции в виде приложений В2В. UDDI — это фактически глобальный бизнес-реестр. Любая компания может зарегистрировать себя в этом реестре. В результате регистрации в реестр попадает описание организации и веб-служб, которые она предоставляет внешнему миру (в рамках UDDI термином «веб-служба» обозначается множество разнообразных механизмов, включая адреса электронной почты и веб-узлы). О каждой компании в реестре UDDI хранится информация трех категорий: О Белые страницы (White Pages) — сюда относится контактная информация, ад- реса и т. п. О Желтые страницы (Yellow Pages) — сюда относится информация об индустри- альной категории компании, продуктах, поставляемых компанией, географи- ческая информация и проч. Говорят, что компания регистрируется в рамках определенной таксономии (taxonomy). Одна компания может быть зарегист- рирована в нескольких таксономиях. Допускается определение дополнитель- ных категорий сведений о компании. О Зеленые страницы (Green Pages) — здесь перечисляются веб-службы, предла- гаемые компанией. Для каждой службы указывается тип службы (тип службы обозначается tModel). Существуют предопределенные типы служб, однако при желании компания может зарегистрировать собственный специальный тип (на- пример, в терминах WSDL). Реестр UDDI можно воспринимать как некий аналог современной службы DNS — он обладает аналогичной распределенной структурой: информация хра- нится на множестве серверов, серверами поддерживается отражение и кэширова- ние данных. Клиенты тоже могут кэшировать данные при условии соблюдения специальных правил. UDDI определяет специальные модели данных для бизнес-субъекта (business entity), бизнес-службы (business service) и шаблона связывания (binding template). К типу BusinessEntity относится базовая информация о предприятии, в частности имя, категория, к которой оно принадлежит, а также контактная информация. Тип BusinessEntity поддерживает таксономии желтых страниц, из которых извлекается информация об индустрии, типах продуктов, географических сведениях и т. п. Тип BusinessService включает описания веб-служб (используемых для зеленых страниц). Основной тип — это только контейнер для связанных с ним служб. Служ- бы могут быть связаны с таксономией (географическое положение, поставляемые продукты и т. п.). Каждая структура BusinessService содержит один или несколько шаблонов связывания BindingTemplate (ссылка на службу). Тип BindingTemplate включает в себя tModel. tModel содержит информацию о форматах, протоколах и безопасности, а также ссылки на технические специфика- ции (возможно, с использованием формата WSDL). Если несколько компаний используют один и тот же тип tModel, программа может взаимодействовать с каждой из них, используя один код. Например, некоторая бизнес-программа может предло- жить tModel для других программ, чтобы они могли взаимодействовать с ней вне за- висимости от того, какая организация использует это программное обеспечение. 1009
1010 Глава 23. Веб-службы и SOAP Интерфейс UDDI API основан на SOAP. Используя SOAP, вы можете как за- регистрировать новые данные, так и направить запрос на получение информации из реестра. Помимо этого компания Microsoft предлагает для работы с UDDI ком- плект SDK, основанный на СОМ, а компания IBM предлагает комплект Java Open Source Toolkit. В состав UDDI API входят методы получения информации (find_xx и get_xx), а также методы публикации сведений в реестре (save xx и delete xx). Эти методы действуют в отношении каждой из четырех основных структур дан- ных (businessEntity, businessService, bindingTemplate и tModel). UDDI в Delphi 7 В состав Delphi 7 входит браузер UDDI, которым вы можете воспользоваться для поиска веб-службы в процессе импорта WSDL-файла. Рабочее окно браузера UDDI показано на рис. 23.8. Этот браузер активизируется при помощи мастера WSDL Import Wizard. Браузер поддерживает работу только с серверами UDDI версии 1 (суще- ствует более свежая версия интерфейса, однако она не поддерживается в рамках Delphi 7). Браузер позволяет установить связь с несколькими заранее предопреде- ленными реестрами UDDI. Параметры работы браузера настраиваются с исполь- зованием файла UDDIBrow.ini, расположенного в папке bin каталога Delphi. 3 6. Л 120ЁПШМ) & + McroFocu* •4 ♦ М«зо Focus Mera (cfomabca LLC Ф + MICRO MACHINES 4; + Mnoform Awdrfl Room ф McraM«r> Corporator) Мт5 j^s****' ' 'Л-*5 ч •ЛЛА- 'flrcrinfttfr. scoftw SOeO/raapoH^BQuayMatierAerviet/rpaourar ^52CSC>U?3d9a49t&®7S366»a05»6b |ье7^7^Ж^396»2ЬЛЭЗ»еИ.............. Рис. 23.8. Браузер UDDI, встроенный в среду Delphi IDE Это весьма удобный способ доступа к информации о веб-службах, но не един- ственный способ, поддерживаемый в рамках Delphi. Браузер UDDI не доступен в виде обычного приложения, однако существуют модули, в которых определяет- ся интерфейс доступа к UDDI. Используя эти модули, вы можете разработать свой собственный браузер UDDI. Далее я набросаю очень упрощенное решение, которое вы можете использо- вать в качестве основы вашего браузера UDDI. Рабочее окно программы Uddilnquiry показано на рис. 23.9. Эта программа обладает набором возможностей, однако не все ее возможности работают гладко и без проблем (в особенности возможность Поиска категории). Причина в том, что использование UDDI подразумевает работу 1010
Поддержка UDDI 1011 с комплексными структурами данных. Модуль импорта WSDL не всегда обеспе- чивает очевидный способ отображения этих структур. Из-за этого код программы становится достаточно сложным. Я планирую продемонстрировать вам только код обычного поиска, мало того, я не собираюсь рассматривать весь этот код целиком (еще одна причина состоит в том, что далеко не все читатели заинтересованы в использовании UDDI). UOOI Inquiry Dhw Search for; |micro ~~ UftfW f~-------——--------------------------- rugUuy ‘ **₽ nticroeoft.com/mquire by Nan е Searct Name _______________ Micro С. Inc Micro Focus Micro Focus Micro Informatics LLC MICRO MACHINES Micro Motion Inc MicroApphcetions, Inc rmcrobizl I PMOriplron . ~ We provide systems inte Welcome to the future of Welcome to the future of This is a UDDI Business Plant and Machinery for Micro Motion manufactur information systems dev desc [fiuainassKfly , j-v 5f6ab96a-50f5-48h4-9f84-. . 7 e 7 6 3 7 8c-fa28~47a2*bBa 9566c53B 7d59-11 d6-8c3 . dce959cf-2B0d-4d9e*bee ca2551 cc-0BBf-46b7-9c1 d4e4bB3(H19e-4edf-9f44 a23c9B1 e-B34c-4bBa-bf3 B7f5faBB-5BBe~4B65-b379 . <.>o<2p tnvfctope xt-' ’ ье. ..js'nnp orq/$o»p/u:iv«rtnpp4 fu«n h««D%Ui 'je-ier-«T=,i.ou fpqf?»n:-*Mlcrosoft Corporation” *ri rK-auj^’fetse' x. '.,“4n..udA «. Du^i-ie-rcf г t xy pufnf rJ^.f•y'-“59593094•dfld-4f5Э-9a2c*вbffc8c9Э51Э'' op₽г^t••^'“''Mlcrosoft Corporation’ autbn vedK^s-r'Scott Wltkln’^ u-eT,/<“-,busJnessEntltY‘'.> http://uddl.mlcrosoft.com/dfscovery? Рис. 23.9. Программа Uddilnquiry является упрощенным браузером UDDI Когда программа начинает работу, она связывает содержащийся в ней компо- нент HTTPRIO с интерфейсом InquireSoap UDDI, этот интерфейс определяется в модуле inquire_vl, который поставляется вместе с Delphi 7: procedure TForml.FormCreate(Sender: TObject); begin httpriol.Uri ;= comboRegistry Text; inft Inquire : = httpriol as InquireSoap; end. В результате щелчка на кнопке Search программа обращается к вызову find_ business интерфейса UDDI API. Многие функции UDDI принимают различные параметры, все эти параметры оформляются в виде структуры типа FindBusiness. Результат выполнения вызова размещается в объекте типа businessList2: procedure TFOrml btnSearchClick(Sender. TObject); var findBusinessData: Findbusiness; businessListData: businessList2; begin httpriol.Uri := comboRegistry.Text; findBusinessData := FindBusiness.Create; findBusinessData.name .= edSerach.Text. 1011
1012 Глава 23. Веб-службы и SOAP flndBusinessData.generic := '1.0'; findBusinessData.maxRows := 20: businessListData : = inftlnquire.find_business(flndBusinessData); BusinessListToListView (businessListData): flndBusinessData.Free; end: Объект businessList2 — это список, который обрабатывается методом business- ListToListView главной формы программы. Наиболее важные сведения отображают- ся в компоненте ListView: procedure TForml.businessListToListView(businessLIst: businessList2); var i: Integer; begin ListViewl.Clear: for i := 0 to businessList.businessinfos.Len do begin with ListViewl.Items.Add do begin Caption := businessList.businessinfos [i].name; SubItems.Add (businessList.businessinfos [1].description); SubItems.Add (businessList.businessinfos [i].businessKey); end; end: end: Щелкнув на одном из пунктов списка ListView, вы можете получить более подроб- ную информацию об этом элементе, однако программа отображает результирующую XML-информацию в виде обычного текста (или отображения, основанного на TWebBrowser) и никак не обрабатывает ее. Как уже говорилось, я не собираюсь под- робнее вдаваться в изучение технических деталей. Если есть желание, вы можете подробнее изучить исходный код. Что далее? В данной главе я рассказал вам о веб-службах, рассмотрел использование SOAP, WSDL и UDDI. Для получения более подробных сведений я предложил вам обра- титься к узлу консорциума W3C, а также к веб-узлам UDDI (www.uddi.org) и ebXML (www.ebxml.org). Я не стал слишком подробно останавливаться на изучении всех этих технологий, однако посчитал, что не упомянуть о них в данной книге нельзя. Прочитав данную главу, вы должны понять, что Delphi — это достаточно мощ- ный инструмент программирования приложений, связанных с веб-службами. Вы можете использовать веб-службы для взаимодействия с программами, написан- ными для платформы Microsoft .NET. Компания Borland планирует обеспечить поддержку .NET в рамках Delphi. В состав Delphi входит демонстрационная вер- сия инструментов, предназначенных для разработки приложений .NET в среде Delphi. Связанные с этим возможности рассматриваются в следующих двух гла- зах данной книги. В главе 24 рассматриваются основы архитектуры Microsoft .NET, а в главе 25 эассказывается о компиляторе Delphi для среды .NET. 1012
О Л Архитектура Microsoft .NET с точки зрения Delphi Каждые несколько лет на свет появляется новая технология, которая переворачи- вает компьютерную индустрию с ног на голову. Некоторые из этих технологий продолжают процветать и развиваться, некоторые мутируют, превращаясь в нечто новое, а другие спустя короткое время оказываются ничем иным, как маркетинго- вой шумихой. Почти как во время войны, появление новой технологии обычно предваряется артподготовкой — организуется мощная рекламная кампания, под- нимается шумиха в компьютерных изданиях и средствах массовой информации. Опытные программисты знают, что во время этой артподготовки лучше пригнуть головы и некоторое время не высовываться, пока шумиха не уляжется и ситуация не прояснится. Время должно показать, стоит ли уделять внимание новой техно- логии, достойна ли она изучения и оправданно ли ее появление на свет. Ваша реакция на появление архитектуры Microsoft .NET зависит от того, чем вы занимались в прошлом. Если ранее вы программировали на Java или на Delphi, скорее всего вы удивитесь, чем именно вызван весь этот необычайный ажиотаж вокруг .NET? EIo если ранее вы изнемогали в тенетах Windows API и C++ (а мо- жет быть, даже обычного С), с появлением .NET вы наверняка почувствуете ра- дость и облегчение. В данной главе будут рассмотрены некоторые технологии, входящие в состав .NET, также я расскажу, каким образом все эти технологии связаны с программи- рованием в среде Windows и в Delphi в частности. Мы начнем с установки и на- стройки компилятора Delphi for .NET Preview. Затем мы кратко рассмотрим некоторые из технологий .NET. Потом мы подробнее обсудим каждую из этих тех- нологий. При этом в качестве иллюстрации я продемонстрирую вам некоторый код Delphi. ПРИМЕЧАНИЕ------------------------------------------------------------ Данная, а также следующая главы написаны Марко Кэнту в сотрудничестве с Джоном Бушакрой (John Bushakra), который работает в подразделении документации Borland RAD. В этой главе будут рассмотрены следующие вопросы: О установка компилятора Delphi for .NET Preview; О платформа Microsoft .NET; О промежуточный язык; О управляемый код и CTS; 1013
1014 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi О сбор мусора; О управление версиями. Установка Delphi for .NET Preview f Компилятор Delphi for .NET Preview не может работать без .NET Framework Run- time, однако этот продукт не устанавливается в составе .NET Preview. Чтобы ус- тановить его, вы должны загрузить свободно распространяемую копию .NET Framework Runtime с веб-узла Microsoft MSDN. Если вы планируете серьезно заниматься разработкой приложений для .NET, я рекомендую вам установить .NET Framework SDK. Этот пакет включает в себя инструменты и документацию для раз- работчиков, однако он занимает существенно больше места. .NET Framework Runtime или SDK следует устанавливать до того, как вы установите Delphi for .NET Preview. Компилятор Delphi for .NET Preview совместим c .NET Framework SDK и ком- плектом Service Pack 1. Если вы установили Service Pack второй или более позд- ней версии, вам потребуется выполнить дополнительный шаг: вы должны будете заново компилировать заранее откомпилированные модули (файлы dcuil), уста- навливаемые в рамках Preview. В последующих версиях Delphi for .NET Preview необходимость в перекомпиляции может отпасть. ПРИМЕЧАНИЕ------------------------------------------------------- Спустя несколько месяцев после выхода в свет Delphi 7 (в ноябре 2002 года) компания Borland выпустила существенное обновление компилятора Delphi for .NET Preview. Если вы являетесь заре- гистрированным пользователем Delphi, вы можете загрузить обновления компилятора .NET Preview с веб-узла компании Borland. Это следует сделать еще до установки версии, которая изначально входит в комплект поставки Delphi, так как чтобы выполнить обновление, вам придется деинстал- лировать изначальную версию .NET Preview. ВНИМАНИЕ ---------------------------------------------------------------- Для тестирования примеров, рассматриваемых в этой и следующей главах, я использую версию Delphi for .NET Preview, которая была выпущена в ноябре 2002 года. Однако большинство примеров с успехом можно откомпилировать и при помощи изначальной версии, входящей в комплект по- ставки Delphi 7. Когда вы читаете эту книгу, возможно, появилась еще более свежая версия компи- лятора Delphi for .NET Preview. Обновленные версии примеров можно найти на веб-узле автора. Установив .NET Framework SDK, вставьте компакт-диск Delphi for .NET Preview и запустите программу установки. Компилятор Preview будет установлен в ката- логе, отдельном от основного каталога Delphi 7. В результате установки Preview никакие конфигурационные параметры Delphi 7 не будут затронуты или модифи- цированы. Ранее я уже отметил, что если вы установили Service Pack 2 для .NET Framework, вам потребуется заново откомпилировать модули, поставляемые вместе с компи- лятором. В этом случае откройте окно командной строки и перейдите в подкаталог source\rtl установочного каталога компилятора .NET Preview. В этом подкаталоге располагается файл rebuild.bat, который необходимо запустить для того, чтобы за- ново откомпилировать модули. Возможно, вы увидите сообщения об ошибках и предупреждения. Имейте в виду, что для данной версии компилятора эти ошиб- 1014
Установка Delphi for .NET Preview 1015 ки в порядке вещей, возможно, они будут устранены в последующих версиях. Пос- ле завершения работы файла rebuild.bat вы можете приступать к использованию компилятора. Компилятор dccil располагается в подкаталоге bin. Помимо компи- лятора в этом подкаталоге располагается файл dccil.cfg, в котором указывается путь поиска модулей по умолчанию (ключ -U компилятора). По умолчанию модули ищутся в подкаталоге units установочного каталога компилятора. В том виде, в котором он поставляется, компилятор Delphi for .NET Preview предназначен для использования из командной строки. Однако компания Borland предлагает желающим воспользоваться неофициаль- ным добавлением к Delphi 7 IDE, которое делает компилятор .NET доступным для использования из графической среды. Это добавление можно загрузить с веб-узла Borland Developers Network. Добавление называется Delphi for .NET Common Line Compiler IDE Integration. В результате установки добавления в главном меню Delphi IDE появляется новый пункт. iNfw.NET|!jj<None> jy topft- project £ Debug IO Pevenfy в Automatically run pevenfy ILDasm Reflector Ц I Hijack IDE ' Options .NET Framework SDK help Alt+Fl About. Однако имейте в виду, что добавление не позволяет вам полноценно проекти- ровать формы и делать другие удобные вещи, привычные для программистов Delphi. Компилятор Delphi for .NET Preview предназначен прежде всего для того, чтобы познакомить вас с возможностями языка и узнать, каким образом библио- тека Delphi Run-Time может выглядеть в контексте .NET. Добавление Delphi мож- но загрузить из области Code Central веб-узла Borland Developer Network по адре- су bdn.borland.com (если вы не можете его найти, посмотрите под номером 18889). Возможно, вам потребуется загрузить еще один полезный инструмент под на- званием Reflector. Эта программа разработана Лутцем Редером (Lutz Roeder), ко- торый, кстати, работает в компании Microsoft. Утилита располагается на веб-узле автора по адресу www.aisto.com/roeder/dotnet. ПРИМЕЧАНИЕ ------------------------------------------------------------------- Reflector — это фактически дизассемблер промежуточного языка Microsoft Intermediate Language (ILDASM). Этот инструмент позволяет вам инспектировать ассемблерные файлы (исполняемые файлы и DLL) платформы .NET, а также связанные с ними типы и члены. 1015
1016 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi Тестирование и установка Настало время протестировать компилятор Delphi for .NET Preview. Для этого да- вайте напишем и откомпилируем простое консольное приложение, выводящее на экран текстовое сообщение. Вы можете набрать этот текст в среде Delphi 7, а можете использовать любой текстовый редактор: program HelloWorld. {SAPPTYPE CONSOLE} uses Borland Delphi SysUtils. begin WriteLn (‘Hello. Delphi1 - Today is' + DateToStr (Now)) end Обратите внимание, что код этого приложения .NET почти ничем не отличает- ся от кода обычных приложений Delphi. Единственная разница состоит в наличии выражения uses. Компания Borland организует модули библиотеки Delphi в виде пространств имен, сходных с Common Language Runtime (CLR). Я расскажу об этом подробнее в следующей главе. Сохраните набранный исходный код в файле с именем HelloWorld.dpr и открой- те окно командной строки. Перейдите в каталог, в котором располагается файл, и наберите dccil HelloWorld dpr В результате работы компилятора в каталоге должен появиться исполняемый файл с именем HelloWorld.exe. Если вы запустите этот файл, на экране отобразится строка, являющаяся выводом программы. Если вы используете Delphi IDE с уста- новленным добавлением .NET, для компиляции программы вы должны нажать комбинацию клавиш Ctrl+F9 или просто F9 в случае, если вы включили IDE hijacking (см. пункт меню Hijack IDE на предыдущем рисунке). Наша первая программа слишком проста. Давайте попробуем написать програм- му .NET, которая будет отображать текст в графическом окне. Программа с назва- нием HelloWin создает и отображает на экране окно и заносит текст в заголовок этого окна. Кроме того, программа обращается к методу Application.Run, который используется для активации цикла обработки сообщений: program HelloWin. uses System Windows Forms. Borland Delphi SysUtils, var aForm Form. begin aForm = Form Create. aForm Text 'Hello Delphi'. Application Run (aForm) end 1016
Платформа Microsoft .NET 1017 На презентациях .NET приведенный код вызывает бурю энтузиазма у програм- мистов, привыкших к использованию средств разработки компании Microsoft. Если вы — программист Delphi, значит, вы использовали примерно такой же код начи- ная с 1995 года, поэтому вам, должно быть, не понять, чем вызван энтузиазм. Вы- вод этой программы не представляет никакого интереса, однако, воспользовавшись ILDASM (его можно запустить напрямую из меню), вы можете убедиться, что по- лученное приложение не является классическим приложением Win32. Вывод ILDASM для программы HelloWorld показан на рис. 24.1. Обратите внимание, что глобальный код модуля располагается внутри псевдокласса uUnit. В отличие от Delphi, среда .NET не поддерживает глобальных процедур и функций — любой код оформляется в виде метода того или иного класса. Однако я забегаю вперед. Давайте начнем с начала и взглянем на устройство платформы .NET. Я» -ttew tW iEdbooks\md7tcdei24'H'?lloWorld'HellC'W<?rid\exe| f MANIFEST + W Borland Delphi Math « W Borland Delphi SysUtils + W Borland Delphi System « W Borland Delphi Types + W Borland Win32 Windows e * HelloWorld - Ж Unit > class public auto ansi U $WakeUp void() a cctor void() ctor void() a Finalization void() U HelloWorld void() assembly HelloWorld Рис. 24.1. Информация о программе HelloWorld отображается в окне ILDASM Платформа Microsoft .NET Платформа .NET основана на множестве разнообразных спецификаций и инициа- тив. Контроль над основной функциональностью платформы .NET передан ассо- циации ЕСМА (European Computer Manufacturer’s Association — ассоциация европейских производителей компьютеров), и в настоящее время эта функцио- нальность стандартизируется. На момент написания данной книги разработка спе- цификации языка C# уже завершена, идет процесс стандартизации этого языка в рамках ISO. ПРИМЕЧАНИЕ --------------------------------------------------- Документы, имеющие отношение к стандарту языка программирования C# и инфраструктуре Common Language Infrastructure (CLI), можно найтй на веб-узле по адресу www.ecma.ch. Интересным элементом спецификации CLI является стандартное соглашение об именовании переменных и методов. 1017
1018 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi С точки зрения программиста основным элементом платформы .NET является управляемая среда исполнения, обеспечивающая исполнение промежуточного кода, который может быть получен в результате компиляции разных языков про- граммирования (с условием, что эти языки удовлетворяют определениям базовых типов данных). Среда исполнения поддерживает множество полезных возможно- стей, в частности, помимо прочего — комплексное управление памятью и встроен- ные механизмы безопасности. На базе этой среды компания Microsoft сформиро- вала объемную библиотеку классов. Эта библиотека обеспечивает разработку кода в самых разных областях (формы Windows, разработка Веб и веб-служб, обработ- ка XML, доступ к базам данных и многое другое). Это лишь краткий обзор. Чтобы перейти к более подробному ознакомлению с .NET, мы с вами должны познакомиться с несколькими важными понятиями, связанными с этой платформой. Каждое из этих понятий обозначается трехбук- венным сокращением. О них я расскажу в последующих разделах. CLI (Common Language Infrastructure) CLI — это главная основа платформы .NET. Стандарт CLI был представлен на рас- смотрение ЕСМА в декабре 2001 года. CLI состоит из нескольких спецификаций: О Common Type System (CTS) — система общих типов. Этот элемент платформы является одним из средств обеспечения интеграции разнообразных языков про- граммирования. CTS определяет, каким образом типы объявляются и исполь- зуются. Компиляторы языков должны удовлетворять данной спецификации, в противном случае интеграция множества языков в рамках .NET была бы не- возможной. О Extensible Metadata — расширяемые метаданные. CLI подразумевает, что каж- дый устанавливаемый модуль (assembly — агрегат) должен быть самоописыва- емым объектом. По этой причине каждый модуль должен нести в себе данные, которые полностью идентифицируют собой этот модуль (имя, версию, сведе- ния о культуре и открытый ключ), данные о типах, определенных внутри агре- гата (модуля), список файлов, на которые ссылается модуль, а также любые специальные разрешения безопасности. Система метаданных является расши- ряемой, благодаря чему агрегат может содержать определенные пользователем описательные документы, которые называют специальными атрибутами. ПРИМЕЧАНИЕ--------------------------------------------------------- Термин «культура» (culture) используется компанией Microsoft в качестве расширения терминов «язык» (то есть язык, на котором написаны сообщения, отображаемые на экране для пользователя) и «локаль» (формат даты, формат чисел и прочие параметры, имеющие отношение к конкретной стране). Иными словами, культура — это набор параметров, имеющих отношение к конкретной стране или региону. О Common Intermediate Language (CIL) — общий промежуточный язык програм- мирования — это язык исполняемый инструкций для абстрактного процессора. Компиляторы, выполняющие генерацию кода для платформы .NET, вместо ес- тественных машинных команд центрального процессора генерируют инструк- ции промежуточного языка. В этом отношении CLI — это аналог байт-кода Java. 1018
Платформа Microsoft .NET 1019 О Р/Invoke — программы, исполняемые в виртуальной среде .NET, изолированы друг от друга. В отличие от традиционной программной модели Win32, в среде .NET между прикладной программой и операционной системой располагается большой и сложный уровень программного обеспечения. Однако .NET Runtime не может полностью заменить Win32 API, поэтому должно существовать сред- ство взаимодействия между этими средами. Это средство называется Platform- Invocation Service (сокращенно P/Invoke или PInvoke). О Framework Class Library (FCL) — сокращенно .NET Framework или просто .NET Fx — это иерархия классов, аналогичная библиотеке классов Borland VCL. Биб- лиотека VCL предлагает программисту аналогичный набор функций, однако в состав .NET Fx входит значительно большее количество классов, благодаря чему с использованием .NET Fx можно решить значительно большее количе- ство разнообразных программистских задач. Архитектурное различие между VCL и .NET Fx состоит в том, что, во-первых, к классам .NET Fx можно обра- титься из других языков, а во-вторых, эти классы могут быть расширены сред- ствами других языков. Это означает, что, программируя на Delphi, вы можете создавать классы-потомки классов .NET Framework, точно так же, как вы мо- жете создавать классы-потомки любого другого класса Delphi. Более того, CLI позволяет вам расширять класс, написанный на любом языке, обладающий ком- пилятором, поддерживающим работу с .NET Runtime. Существуют части FCL, о которых говорят, что они перерабатываемы (factorable). Это означает, что такую часть можно переработать, например для создания сокращенной версии того или иного класса для использования в микрокомпьютере. О Extended Portable Executable (РЕ) File Format. Для поддержки метаданных CLI компания Microsoft использует стандартный формат исполняемых файлов РЕ (Portable Executable). Этот формат является стандартным форматом исполня- емых файлов Win32. Преимущество такого подхода состоит в том, что опера- ционная система может загружать приложения .NET точно так же, как она загружает обычные приложения Win32. Однако на этом сходство заканчивает- ся. Весь реальный исполняемый код .NET содержится в специальном разделе файла РЕ. Когда загрузчик обнаруживает, что он имеет дело с элементом .NET, он передает управление CLR. CLR выполняет обработку элемента .NET. Common Language Runtime (CLR) CLI — это спецификация, в то время как CLR является реализацией этой специ- фикации. Нет ничего удивительного в том, что CLR является супермножеством спецификации CLI. Для программиста CLR — это библиотека времени исполне- ния, включающая в себя абсолютно все необходимое. Библиотека, которая унифици- рует огромное количество служб, предоставляемых операционной системой Windows, и обеспечивает доступ к этим службам в объектно-ориентированном стиле. Если говорить более общими словами, CLR отвечает за каждый из аспектов вселенной .NET: загрузку исполняемых файлов, проверку идентификационных данных и безопасности типов, компиляцию кода CIL в естественные машинные инструкции центрального процессора, а также за управление приложением в тече- ние всего времени жизни этого приложения. Код CIL, предназначенный для за- 1019
1020 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi пуска в среде CLR, называется управляемым кодом (managed code), в то время как весь остальной код (например, исполняемый код процессоров Intel, генерируемый компилятором Delphi 7) называется неуправляемым (unmanaged). Common Language Specification (CLS) Спецификация CLS тесно связана с системой типов CTS. CLS является подмно- жеством той спецификации, которая определяет набор правил для создания типов в различных языках программирования, которые взаимодействуют в рамках .NET. В настоящее время не все языки программирования одинаковы. Некоторые из них обладают возможностями, которые отсутствуют в других языках программирова- ния. CLS — это попытка обнаружить общий средний набор правил, который под- держивается большинством языков. Компания Microsoft постаралась сделать CLS как можно меньше по размеру и при этом обеспечить, чтобы спецификации CLS удовлетворяло как можно большее количество языков. Некоторые из возможностей Delphi не соответствуют CLS. Это вовсе не озна- чает, что код не сможет быть выполнен в рамках CLR. Это означает, что если вы используете возможность языка, не поддерживаемую в других языках, то некото- рая часть вашего кода не может использоваться другими приложениями .NET, если они написаны на языках, отличных от Delphi. ПРИМЕЧАНИЕ ------------------------------------------------------------ Если ранее вы имели дело с Java, возможно, вы обнаружите определенное сходство. Многие кон- цепции платформы .NET позаимствованы от Java, в частности, промежуточный язык и система вир- туального исполнения (разница в том, что байт-код Java может интерпретироваться, в то время как код .NET всегда компилируется методом JIT — Just in Time). Существуют также аналогии в библио- теках классов. Однако в большинстве случаев не стоит безоглядно переносить концепции Java в среду .NET, так как различия в деталях реализации могут существенно повлиять на порядок исполь- зования той или иной возможности. Различные входящие в состав CLI спецификации, очевидно, нацелены на со- здание кросс-платформенных приложений. Однако компания Microsoft вовсе не спешит радовать нас лозунгом write once, run anywhere (один раз написал — работа- ет где угодно). Это связано с фактом, что пользовательский интерфейс рассматри- вается как ключевая часть приложения, однако обычный экран PC и экран мо- бильного телефона обладают совершенно разными возможностями. Существует два основных проекта реализации CLI на нескольких платформах. Проект Rotor (официально называемый Microsoft Shared Source CLI, или MS SSCLI) разрабо- тан специальным исследовательским подразделением Microsoft. Rotor включает в себя огромное количество бесплатного программного обеспечения, распростра- няемого в соответствии с лицензией Microsoft Shared Source License. В составе Rotor присутствуют инструменты, необходимые для построения CLI в среде таких опе- рационных систем, как FreeBSD, Win32 и Mac OS X 10.2. Второй хорошо известной реализацией CLI является проект Mono Project. Этот проект поддерживает Win32 и Linux. Проект Mono распространяется в соответ- ствии со значительно более открытой лицензией и является реализацией «чистой комнаты» CLI в среде Linux. Компания Borland занимает в отношении проекта Mono позицию «подождем - увидим», то есть пока еще не сказано ни единого сло- ва относительно того, будет ли среда Delphi тем или иным образом связана с этим проектом. 1020
Платформа Microsoft .NET 1021 Проект Rotor, в первую очередь, нацелен на студентов и учителей и в основном предназначен для целей обучения и академической среды. Такой вывод можно сделать, изучив лицензию, которая является достаточно открытой для академи- ческих кругов, однако запрещает использование этой реализации CLI для коммер- ческих целей. Проект Mono может составить серьезную конкуренцию проекту Rotor. Наиболее серьезным камнем преткновения по-прежнему является графи- ческий пользовательский интерфейс. Проект Rotor не включает в себя каких-либо графических элементов, в то время как в рамках Mono начата разработка WinForms с использованием библиотеки WINE. Кроме того, с Mono связан проект GTK#, который нацелен на создание библиотеки GTK+, написанной на языке С#. В настоящее время компьютерная среда — это туманная масса с множеством разнообразных возможностей. Наладонные компьютеры обладают собственными достаточно сложными операционными системами, и компания Microsoft имеет весьма острый интерес в этой области. В настоящее время Microsoft работает над созданием версии платформы .NET под названием .NET Compact Framework, пред- назначенной для наладонных устройств. Платформа .NET может стать также ба- зисом, на котором будут строиться многие технологии будущего. Сейчас начина- ют появляться 64-битные процессоры, и, без сомнения, платформа .NET сможет работать в 64-битной среде. Значит ли это, что вы сможете запустить свое приложение .NET на Linux, 64-бит- ной Windows и на PDA? Нет. Сложно себе представить, что интерфейс, разрабо- танный для экрана с диагональю 21 дюйм и разрешением 1600 х 1200, будет авто- матически адаптирован для малюсенького экранчика наладонного компьютера. Таким образом, .NET обеспечивает некоторые преимущества для разработки кросс- платформенных приложений, однако не следует ожидать, что перенос такого при- ложения из одной ОС в другую можно будет выполнить без каких-либо дополни- тельных модификаций. В следующих разделах мы более подробно рассмотрим некоторые компоненты CLI. Агрегаты Компания Microsoft использует термин assembly (агрегат) для обозначения едини- цы распространения (unit of deployment) в среде .NET. В производстве агрегат — это группа отдельных, но взаимосвязанных частей, которые собраны вместе для того, чтобы сформировать единый функциональный модуль. Иногда агрегат со- стоит только из одной части, однако в большинстве случаев он состоит из несколь- ких частей. Это же относится и к агрегатам в среде .NET. Обычно агрегат в среде .NET состоит только из одного файла — это может быть либо исполняемый файл, либо динамическая библиотека (DLL). Однако агрегат может состоять из группы взаимосвязанных библиотек или библиотеки и связан- ных с ней ресурсов, таких как изображения или строки. К агрегату прилагается манифест (manifest). Манифест описывает содержимое агрегата, говоря точнее, манифест агрегата — это описание метаданных агрегата. Манифест — это реализа- ция расширяемых метаданных (Extensible Metadata), о которых мы говорили при обсуждении CLI. Если вы взглянете на установочный каталог Delphi for .NET Preview, вы обна- ружите папку units. В этой папке хранится несколько файлов с расширениями .dcua 1021
1022 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi и .dcuiL Эти файлы не являются файлами РЕ, поэтому их нельзя просмотреть при помощи ILDASM. В файле .dcua содержится каталог пространств имен и типов агрегата .NET (ко- торый, как мы помним, может состоять из нескольких исполняемых файлов). Файл .dcuiL соответствует пространству имен и содержит в себе символы компилятора для пространства имен, а также ссылки на файлы .dcua, которые относятся к про- странству имен (агрегат может предоставлять типы для более чем одного простран- ства имен). ПРИМЕЧАНИЕ--------------------------------------------------------------- Пространство имен (namespace) — это иерархический механизм, предназначенный для организа- ции классов и других типов. Мы подробнее поговорим о пространствах имен в главе 25. Приложение .NET формируется на базе набора агрегатов. Говорят, что прило- жение ссылается на эти агрегаты. Когда набор агрегатов меняется, компилятор должен заново скомпоновать все файлы .dcuiL, содержащие в себе ссылки на фай- лы .dcua, которые больше не принадлежат множеству агрегатов. Аналогичным об- разом, если изменение вносится в агрегат, компилятор должен перекомпоновать соответствующие файлы .dcua и .dcuiL для агрегата. Файл .dcuiL является прибли- зительным эквивалентом файла .dcu, используемого в Delphi. Однако файл .dcua не имеет аналога в среде Delphi для Win32. Различные типы проектов генерируют различные типы агрегатов .NET. Если в исходном коде вы используете слово program, в результате получается исполняе- мый агрегат. При использовании ключевого слова Library генерируется агрегат DLL. Промежуточный язык Common Language Runtime (CLR) — это реализация виртуальной системы испол- нения или виртуальной машины. Как и другие виртуальные машины, CLR обла- дает своим собственным абстрактным процессором. Ранее уже говорилось, что язык ассемблера для виртуального процессора CLR называется Common Intermediate Language (CIL), однако до того, как этот язык стал общепринятым стандартом, он назывался Microsoft Intermediate Language (MSIL). Сокращение MSIL до сих пор часто встречается в документации. Компиляторы для среды CLR не генерируют машинных команд для какого-либо реального микропроцессора, вместо этого они генерируют исполняемый код на языке CIL. Таким образом, получается исполня- емый код, который может быть исполнен в среде CLR. В определенных отношени- ях такой код можно считать платформенно-независимым. Следует помнить, что Microsoft CLR — это лишь одна из реализаций CLI. Инфраструктура CLI может быть реализована на любой аппаратной или программной платформе. Если на плат- форме реализована CLI, значит, исполняемый код CIL может быть выполнен на этой платформе. Конечно же, не существует реального микропроцессора, который в состоянии напрямую выполнить код CIL, поэтому, прежде чем он может быть выполнен, код CIL следует преобразовать в машинные инструкции процессора. Эту задачу реша- ет компилятор JIT (Just in Time — компиляция в момент запуска). В этом отноше- 1022
Промежуточный язык 1023 нии CLR отличается от других виртуальных машин (таких как Java). CLR не явля- ется интерпретатором и не исполняет байт-код. На платформе .NET код CIL все- гда компилируется в машинные инструкции CPU и кэшируется в памяти, поэто- му при повторной загрузке исполняемого файла в большинстве случаев его можно не перекомпилировать. ПРИМЕЧАНИЕ-------------------------------------------------------------- В некоторых средах с ограниченным объемом памяти (например, PDA) компилированный код может быть уничтожен сразу же после выполнения программы — в целях экономии памяти. В этом случае при повторной загрузке его требуется перекомпилировать повторно. Компиляция IL не является слишком дорогостоящей операцией (исследова- тельская команда Microsoft потратила несколько лет на разработку технологии, которая сделала компиляцию JIT почти незаметной). Однако такая компиляция все-таки приводит к расходам времени, к тому же ее необходимо выполнять каж- дый раз при загрузке программы. При запуске большинства приложений в начале работы будет ощущаться небольшая задержка. Особенно это заметно при запуске самого первого приложения .NET в среде Windows — в этом случае значительное время уходит на загрузку в память всей инфраструктуры .NET. Однако следует также учитывать, что в самом начале работы компилируется далеко не весь код. Компилятор JIT согласует свою работу с загрузчиком, таким образом, код IL не компилируется до тех пор, пока не происходит обращение к этому коду (компиля- ция осуществляется отдельно для каждого метода). Запуск большинства приложений в среде .NET осуществляется с использова- нием компиляции JIT, и это считается нормальным поведением. Однако при необ- ходимости вы можете преобразовать файл с управляемым кодом в файл с есте- ственными машинными инструкциями CPU. В результате получится обычный исполняемый код, который можно запустить без использования JIT. В составе .NET Framework присутствует утилита Native Image Generator (Ngen.exe), которая ре- шает эту задачу. Исполняемый код, созданный при помощи Ngen, сохраняется в специальном кэше. В следующий раз, когда CLR пытается загрузить такой агрегат, прежде всего происходит обращение к кэшу. Если в кэше обнаруживается соответствующий естественный исполняемый код, он исполняется вместо версии IL. Имейте в виду, что версия IL также необходима, так как естественный код не содержит в себе ин- формацию о метаданных. Кроме того, конечный пользователь или администратор системы могут удалить естественный код из кэша. В этом случае CLR потребуется заново откомпилировать версию IL. Возможность генерации естественного кода может оказаться весьма полезной, однако прежде чем делать окончательный выбор, вы должны на практике оценить производительность обоих подходов (JIT и естественного кода), чтобы прийти к выводу о том, что компиляция JIT работает неприемлемо медленнее, и существует действительно насущная необходимость преобразования управляемого кода в ес- тественный. В конце концов, компилятор JIT — это реальный компилятор для кон- кретного микропроцессора, поэтому он может выполнять некоторую подчас весь- ма значительную оптимизацию производительности. Компилятор JIT использует хорошие алгоритмы для снижения издержек, связанных с компиляцией, и доста- 1023
1024 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi точно хорошо оптимизирует результирующий код (примерно так же, как это дела- ет компилятор Delphi). Взглянув на IL-код, генерируемый вашим компилятором, вы можете узнать много интересного. Компания Microsoft поставляет утилиту под названием IL Disassembler (ildasm.exe), которую можно использовать для изучения исполняемо- го кода IL на самом низком уровне. Эта утилита располагается в подкаталоге bin установочного каталога .NET Framework SDK. Она позволяет загрузить любой аг- регат и соответствующие ему метаданные: манифест, классы, методы и свойства и, конечно же, IL-код, сгенерированный компилятором. В этой и следующей главах мы подробнее рассмотрим ILDASM. Инструмент Reflector также полезен для изу- чения кода IL. Управляемый и безопасный код Говоря простым языком, управляемый код (managed code) — это любой код, кото- рый загружен, откомпилирован с использованием JIT и запущен под надзором CLR. Как и все исполняемые файлы и библиотеки на платформе Windows, управляе- мый код хранится с использованием формата Microsoft Portable Executable (РЕ). Управляемый файл РЕ содержит дополнительную информацию в заголовке. Ког- да такой файл загружается, управление передается механизму исполнения управ- ляемого кода (это функция в библиотеке MSCorEE.dll). После инициализации механизм исполнения управляемого кода разыскивает точку входа в модуль. Код IL в точке входа компилируется в естественные инст- рукции CPU. После этого начинается исполнение этого кода. Точно такая же си- туация возникает и при исполнении кода в библиотечном модуле. Файл РЕ на- правляет загрузчик в другую функцию библиотеки MSCorEE.dll. В отличие от управляемого, неуправляемый код (unmanaged code) состоит из традиционных, естественных инструкций CPU. Неуправляемый код исполняется вне среды исполнения, поэтому он не может воспользоваться службами CLR (если, конечно, не предпримет специальных мер). Неуправляемый код может создавать классы .NET Framework с использовани- ем служб COM Interop. Класс .NET Framework инкапсулируется при помощи COM-прокси, для неуправляемого кода он выглядит как объект СОМ. Существу- ет также обратная возможность: при помощи специального механизма управляе- мый код может обратиться к COM-объектам в составе COM-сервера. Наконец, служба Platform Invoke, встроенная в CLR, позволяет управляемому коду напря- мую обращаться к вызовам Win32 API. ПРИМЕЧАНИЕ-------------------------------------------------------- Компилятор Delphi for .NET Preview производит полностью управляемый код. В настоящее время этот компилятор не поддерживает возможности смешивания управляемого и неуправляемого кода в рамках одного модуля, однако такая возможность поддерживается компилятором Microsoft Visual C++ .NET (этот компилятор использует механизм с названием IJW: It Just Works). Модуль полностью описывает сам себя, так как в нем содержится как IL-код, так и метаданные, то есть описание элементов данных, используемых кодом. Ис- пользуя комбинацию IL-кода и метаданных, CLR может выполнять верификацию на более высоком уровне (если сравнивать со статической проверкой типов, вы- 1024
Промежуточный язык 1025 полняемой компилятором). Процесс подобной проверки выполняется всегда, если только администратор не отключает его. В результате этой проверки среда испол- нения проверяет, является ли код безопасным с точки зрения типов данных. Код, безопасность типов которого можно проверить, называется безопасным кодом (safe code). В отношении безопасного кода выполняются следующие проверки, связан- ные с типами: О В отношении объектов выполняются только корректные операции. Сюда отно- сятся проверки параметров, проверка возвращаемого типа и проверки видимости. О Объекты всегда присваиваются переменным совместимых типов. О Код не использует указателей в явном виде, так как указатели могут ссылаться на неправильное место в памяти. Как можно предположить, небезопасным кодом (unsafe code) называется код, который не прошел этих проверок. Однако если в отношении кода нельзя выпол- нить проверок безопасности типов, это не значит, что код небезопасен, а всего лишь означает, что в безопасности кода нельзя убедиться. Это может быть вызвано огра- ничениями процесса верификации либо ограничениями компилятора. Когда ком- пилятор Delphi for .NET будет выпущен в виде завершенного продукта, ожидается, что он сможет генерировать код, в безопасности которого с точки зрения приведе- ния типов можно будет убедиться. Некоторые конструкции языка Delphi не удовлетворяют требованиям специ- фикации CLS, однако это не значит, что безопасность кода нельзя проверить. Кон- струкции, не удовлетворяющие требованиям CLS, будут подробно рассмотрены в главе 25. В состав комплекта .NET Framework SDK входит утилита PEVerify, которая тщательно анализирует PE-файл с управляемым кодом на безопасность типов. Она называется peverify.exe. Добавление .NET для Delphi 7 IDE, о котором уже говори- лось ранее, позволяет вам автоматически запускать утилиту PEVerify в отноше- нии вашего кода сразу же после выполнения сборки. Common Type System (CTS) Система общих типов — это бульдозер, который ровняет игровое поле для языков программирования, ориентированных на инфраструктуру .NET. Система CTS пол- ностью специфицирует типы примитивов и объектов, известных CLR. Эти типы используются для определения объектной модели, которая является общей у всех языков, генерирующих код для CLR. На платформе Windows обычным инструментом обеспечения бинарной совме- стимости между различными языками программирования является технология COM (Component Object Model). CTS идет еще дальше, обеспечивая взаимодей- ствие между такими языками, как Eiffel, C# и Delphi. Компоненты, написанные на этих весьма отличающихся языках, могут обмениваться объектами и расширять возможности друг друга при помощи наследования. Подобный уровень интегра- ции языков программирования является беспрецедентным. Все типы, определенные в CTS, делятся на две категории: типы значений (value type) и типы ссылок (reference type). Как следует из имени, типы значений облада- ют семантикой типа «передача по значению». Так, представьте, что у вас есть пере- 1025
1026 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi менная типа «значение». Если вы передаете эту переменную функции в качестве параметра и модифицируете этот параметр внутри функции, значение изначаль- ной переменной останется неизменным. Примерами переменных типа «значение» являются скалярные типы, перечисления и записи. Агрегатные типы, такие как записи (records) в Delphi или структуры (structures) в С#, в рамках CTS называ- ются классами типа «значение». Типы ссылок обладают семантикой «передача по ссылке». Если у вас есть пере- менная типа «ссылка» (например, экземпляр класса) и вы передаете значение этой переменной функции в качестве параметра, любые изменения этого параметра, выполненные внутри функции, повлияют на саму переменную. Примерами ссы- лочных типов являются типы классов и интерфейсов. Типы указателей также яв- ляются ссылочными типами. К ним относятся, например, делегаты, о которых бу- дет рассказано далее. Объекты и свойства Как и в Delphi, в CTS реализована иерархия классов с единственным предком. Любой класс может обладать только одним предком, однако он может реализовы- вать либо ноль, либо несколько интерфейсов. Классы и члены классов могут быть объявлены с использованием модификаторов области видимости: private, public, protected. Эти модификаторы хорошо знакомы программистам ООП, помимо них в рамках CTS поддерживаются также другие модификаторы видимости, о кото- рых будет рассказано в следующей главе. Модификаторы private, public, protected обладают приблизительно тем же значением, что и в Delphi, однако накладывае- мые ими ограничения являются более жесткими — в соответствии с семантикой C++. (В главе 25 о соответствии между модификаторами видимости Delphi и CTS рассказывается подробнее. Мы также рассмотрим изменения, внесенные в язык Delphi для обеспечения совместимости с CTS.) Если вы внимательно изучите литературу, связанную с .NET, вы наверняка обра- тите внимание на сходство между возможностями классовых типов CTS и классов Delphi. Конечно же, в CTS поддерживаются традиционные объектно-ори- ентированные элементы, такие как поля и методы классов. Помимо этого CTS под- держивает свойства, которые реализованы приблизительно так же, как и в Delphi. Каждому свойству в CTS можно поставить методы, предназначенные для чтения и для записи этого свойства. При помощи этих методов доступ к свойству может быть заблокирован или значение свойства может быть вычислено на лету. Кроме того, свойство может использоваться в качестве маскировки закрытого поля клас- са. Однако существуют также различия. В частности, методы get/set, соответству- ющие свойствам, должны обладать той же видимостью, что и само свойство. Это сделано для того, чтобы языки, не поддерживающие работу со свойствами, смогли обращаться к свойствам. Несмотря на то что Delphi не требует делать этого в ис- ходном коде, откомпилированный код в случае необходимости модифицируется без вашего ведома. События и делегаты Одна из причин, по которым интерфейс Win32 API просуществовал так долго, состоит в том, что этот интерфейс основан на фундаментальных концепциях, та- ких, например, как использование адреса функции в механизме обратного вызова 1026
Промежуточный язык 1027 (callback mechanism) этой функции. Вся система событий пользовательского ин- терфейса Windows целиком и полностью основана на функциях обратного вызова (callback functions). Кстати говоря, некоторые события в инфраструктуре VCL ба- зируются на системе событий Windows. Механизм обратного вызова является на- столько мощным средством, что его поддержка добавлена также и в CTS. Чтобы реализовать этот механизм в безопасной с точки зрения типов, нейтральной по отношению к языку среде, в CTS добавлен тип, называемый делегатом (delegate). Делегаты CTS отличаются от обычных указателей на функции. Они могут ссы- латься как на статические, так и на экземплярные методы класса. Объявление де- легата должно соответствовать сигнатуре методов, на которые он будет ссылаться. В среде Delphi for .NET делегаты используются так же, как и процедурные типы: type TmyClass = class public procedure myMethod: end; var threadDelegate: System.Threadlng.Threadstart: tmc: TMyClass: aThread: System.Threading.Thread; begin tmc ; = TMyClass.Create; threadDelegate := @tmc.myMethod; aThread := Thread.Create (threadDelegate); aThread.Start; Переменная th read Deleg ate является переменной типа System .Th reading .Th readStart — это делегатный класс в CLR. Методы, которые можно присвоить этой делегатной переменной, обладают сигнатурой, которая соответствует сигнатуре делегата. В данном случае это процедура, которая не принимает ни одного параметра. (Вы можете обнаружить этот фрагмент кода в подкаталоге Delegate комплекта приме- ров.) В данном случае компилятор скрывает от нас достаточно сложную процедуру обра- ботки кода. За кулисами компилятор должен создать экземпляр класса System.Multi- castDeleg ate. Обращение к делегату — инкапсулируемой функции (в данном случае — myMethod) — осуществляется с использованием класса MulticastDelegate. Этот класс поддерживает одновременную инкапсуляцию нескольких функций в рамках един- ственного делегата. В рамках модели событий пользовательского интерфейса гово- рят, что одному событию ставится в соответствие несколько слушателей (listeners). ПРИМЕЧАНИЕ------------------------------------------------------------------------------- Тот факт, что делегаты являются классами, должен объяснить вам, почему вы можете объявить делегат вне объявления класса. Делегаты являются экземплярами класса System.MulticastDelegate (или другого сгенерированного компилятором класса, являющегося производным от класса System. MulticastDelegate). По этой причине вы можете объявить делегат в любом месте, где вы можете объявить класс. В каждом из языков .NET поддерживается некоторая семантическая конструк- ция, облегчающая создание событий, а также добавление и удаление прослушива- 1027
1028 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi телей событий. Например, в C# для добавления функций в делегат и удаления функций из делегата используются операторы += и -=. В Delphi for .NET Preview для этой цели используется семантика работы с множествами. В главе 25 мы под- робнее рассмотрим эту тему. Я продемонстрирую, каким образом функции Include и Exclude используются для назначения и удаления обработчиков событий. Мы также рассмотрим, каким образом работает используемый в Delphi оператор := в отношении обработчиков событий. Сборка мусора Сборщик мусора — это часть CLR, которая выполняет автоматическое управление выделением и освобождением памяти. В этой области Delphi 7 предлагает совсем немного: для переменных, основанных на интерфейсах, выполняется подсчет ссы- лок (см. главу 2). Когда не остается ни одной ссылки на объект, занимаемая им память освобождается. Эта же базовая идея лежит и в основе сборщика мусора CLR, однако сборщик мусора CLR является более совершенным механизмом. В частности, он может обнаружить ситуацию, когда два объекта ссылаются друг на друга, однако других ссылок на эти объекты не существует. В этом случае па- мять, занимаемую объектами, можно освободить. В Delphi 7 подобные ситуации не отслеживаются. При использовании сборщика мусора (Garbage Collector, GC) вы можете со- здавать объекты, ссылающиеся друг на друга, а затем не беспокоиться об уничто- жении этих объектов. Система сама позаботится об этом. Из этого следует, что вы не обязаны тщательно следить за балансом между созданием и уничтожением объектов. Кроме того, вам не обязательно использовать блоки try/finally для того, чтобы гарантировать уничтожение объектов. Это всего лишь два преимущества, обусловленных использованием GC. Вспоминать о необходимости освобождения неуправляемых ресурсов (таких как дескрипторы окон и файлов) приходится только в процессе разработки низко- уровневых классов. Поддерживаемый в рамках CLR класс System.Object содержит защищенный метод Finalize, который вы можете переопределить для освобожде- ния неуправляемых ресурсов. Прежде чем переопределять метод Finalize, вы долж- ны ознакомиться с некоторыми важными обстоятельствами. (Подробная инфор- мация по этому вопросу содержится во врезке далее. Проблемы, связанные с переопределением метода Finalize Переопределение Finalize нельзя считать эффективным, так как в результате ваш объект будет обрабатываться сборщиком мусора дважды. Сборщик му- сора должен освободить память, однако он не может сделать этого до тех пор, пока не будет выполнен метод Finalize вашего объекта. Таким образом, сборщик мусора должен выполнить специальную обработку всех объектов с переопределенным методом Finalize. Сборщик мусора размещает эти объек- ты в специальном списке. Иными словами, на этом этапе сборки мусора все подобные объекты пока сохраняются в памяти (так как появляется еще одна 1028
Сборка мусора 1029 ссылка на каждый из этих объектов). Затем в рамках отдельного программ- ного потока сборщик мусора выполняет метод Finalize каждого из объектов, после чего объект удаляется из списка. Когда объект удаляется из списка, последняя ссылка на него исчезает, в результате при последующем запуске сборщика мусора память, занимаемая объектом, освобождается. Однако эффективность — это не единственная проблема, связанная с ме- тодом Finalize. Еще одна неясность состоит в том, что вы не можете знать, когда именно будет выполнен метод Finalize. Если для освобождения не- управляемых ресурсов вы используете метод Finalize, ресурсы могут оказаться занятыми дольше, чем вы думаете. Метод Finalize выполняется в программном потоке, который не является текущим программным потоком, в котором вы используете объект. Это означает, что использование механизмов синхронизации и блокирования за- труднено, так как программный поток, в котором исполняется метод Finalize, принадлежит CLR. Мало того, если в процессе исполнения метода Finalize возникает исключение, это исключение перехватывается не вами, a CLR, в результате оно может быть просто проигнорировано. Рекомендованный способ освобождения неуправляемых ресурсов основан на использовании интерфейса IDisposable. В рамках этой стратегии поддерживается как автоматический метод освобождения внешних ресурсов, так и освобождение ресурсов в результате прямого обращения к специальному методу. Чтобы реали- зовать этот подход, необходимо сделать так, чтобы объект реализовывал интер- фейс под названием IDisposable. Этот интерфейс состоит всего из одного метода: Dispose. В отличие от метода Finalize метод Dispose является публичным, то есть открытым. Иными словами, к нему можете обратиться вы или пользователи ваше- го класса. В документации .NET рассказывается о том, как в языке C# реализована се- мантика деструктора, — компилятор автоматически генерирует метод Finalize для вашего класса у вас за спиной. Чтобы реализовать интерфейс IDisposable в С#, вы должны сделать это напрямую. При этом необходимо сделать так, чтобы метод Dispose можно было вызвать из автоматически сгенерированного метода Finalize. Однако в Delphi for .NET этот механизм работает по-другому. Если вы создаете деструктор класса, это вовсе не приводит к тому, что компи- лятор автоматически генерирует метод Finalize. Вместо этого компилятор добавля- ет в класс реализацию интерфейса IDisposable. Однако никто не запрещает вам ре- ализовать IDisposable самостоятельно. Если же вы хотите в этом вопросе положиться на услуги компилятора, объявите деструктор класса следующим образом: destructor Destroy override. Когда компилятор видит это объявление, он генерирует код, чтобы отметить ваш класс как класс, реализующий интерфейс IDisposable. После этого при помощи специальной возможности CIL объявленный вами деструктор помечается как ре- ализация метода Dispose (это происходит несмотря на то, что метод обладает име- нем Destroy, но не Dispose). 1029
1030 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi Обычным способом освобождения ресурсов в Delphi является обращение к ме- тоду Free уничтожаемого объекта. В Delphi for .NET метод Free реализован таким образом, что он тестирует, реализован ли объектом интерфейс IDisposable. Если вы добавили в объект указанную ранее сигнатуру деструктора, компилятор автома- тически генерирует для вашего класса реализацию интерфейса IDisposable. В ре- зультате метод Free обращается к методу Dispose, что приводит к выполнению кода метода Destroy. Давайте создадим проект и объявим класс с деструктором в соответствии с опи- санным подходом. После этого воспользуйтесь ILDASM для инспекции сгенери- рованного кода. Код представлен в листингах 24.1 и 24.2. Листинг 24.1. Код проекта DestructorTest program DestructorTest. {SAPPTYPE CONSOLE} uses MyClass in 'MyClass pas'. var test TMyClass begin WriteLn ('DestructorTest запуск"). test = TMyClass Create test Free WriteLn (.‘DestructorTest завершение'). end Листинг 24.2. Модуль примера DestructorTest unit MyClass interface type TMyClass - class public destructor Destroy, override. end implementation destructor TMyClass Destroy. begin WriteLn (‘Внутри деструктора (на санок деле это метод IDisposable Dispose) '). end end После компиляции этой программы вы можете запустить ее, однако для нас интересно не столько запустить программу, сколько изучить ее с использованием ILDASM. Запустите ILDASM и выберите File ► Open (Файл ► Открыть) Перейди- те в каталог, в котором расположен файл DestructorTest.exe, и откройте его Вы уви- дите примерно такое же окно, как и на рис. 24.2. 1030
Сборка мусора 1031 Z E;\boaks\md7code\24-Oe»tructorTttst4E>esHu( hw ) м» w Нф £ % $ b Е \books\md7code\24\DestructorTest\DestructorTest ехе k MANIFEST • Boriand Delphi Math • Borland Delphi SysUtils • Borland Delphi System • Borland Delphi Types • Borland Win32 Windows • DestructorTest • MyClass iTMyClass- ► class public auto ansi beforefi el dinit ► implements [mscorlib]System Disposable » № @MetaTMyClass ctor voidf) a Destroy voidf) fe К Unit ► class public auto ansi □ $WakeUp voidf) □ cctor voidf) a ctor voidf) □ Finalization voidf) □ MyClass voidf) assembly DestructorTest И Рис. 24.2. Утилита ILDASM отображает вывод для примера DestructorTest Раздел иерархии MyClass — это пространство имен, созданное для хранения всех символов в модуле. Раскройте этот раздел и обратите внимание на записи TMyCLass и Unit. Компилятор Delphi for .NET создает класс CLR для каждого модуля, чтобы реализовать инициализацию и финализацию и при этом обеспечить для вас воз- можность создания глобальных подпрограмм. Все эти подпрограммы становя гея методами класса Unit. Раскройте раздел TMyCLass и обратите внимание на раздел Destroy, для которого отображается розовый блок. Этот узел дерева соответствует методу Destroy класса TMyCLass Сделайте двойной щелчок на узле Destroy, в результате на экране откроется окно, отображающее полный IL-код для этого метода (рис 24 3). f ТМуПаяпОе stray: v«M(> method public newslot virtual instance void DestroyQ al managed ( override [mscorlib]System Disposable Dispose // Code size 27 (0x1 b) maxstack 2 IL 0000 Idsfld class Boriand Delphi System Text Borland Delphi System Unit Output IL_0005 Idstr - - • IL_000a call IL.OOOf call IL 0014 pop IL_OO15 call ret 1//end ot method TMyClass Destroy 3' In destructor class Boriand Delphi System Text Boriand Delphi System Unit <a>WrteOWStnng(dass Borland Delphi System Tert string) class Boriand Delphi System Text Boriand Delphi System Unit (a>WrrteLn(dass Boriand Delphi System Tert) void Borland Delphi System Unit (aJJOTestQ Рис. 24.3. IL-код для деструктора Destroy 1031
1032 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi Найдите строку с директивой .override: .override [mscorlib]System.IDisposable::Dispose Это явное переопределение, которое сообщает, что метод Destroy является реа- лизацией метода Dispose в интерфейсе IDisposable. Именно благодаря директиве .override в момент, когда из метода Free происходит обращение к методу Dispose, управление передается в ваш деструктор. ПРИМЕЧАНИЕ----------------------------------------------------------------------- Способ работы с интерфейсом IDisposable иллюстрирует обычный паттерн, используемый для ком- пилятора Delphi for .NET. В отличие от C# язык Delphi является достаточно старым языком, к кото- рому привыкли многие программисты и на котором на текущий момент написан огромный объем кода. Borland не может отказаться от старых способов и заставить программистов переучиваться на новые методы работы — это привело бы к множеству ошибок и нарушению функционирования существующего кода. Реализация IDisposable выполнена таким образом, чтобы позволить програм- мистам продолжать использовать привычные для них методики и парадигмы и минимизировать затраты, связанные с переносом существующего кода на платформу .NET. Сборка мусора и эффективность Среди программистов использование механизма сборки мусора (Garbage Collector, GC) является темой многочисленных споров. Большинство программистов доволь- ны тем, что GC помогает им существенно снизить вероятность возникновения ошибок управления памятью. Однако некоторые опасаются, что GC работает не достаточно эффективно. Этот страх зачастую мешает широкому распространению данной технологии. Опасения вызваны, в частности, низкой эффективностью GC ранних версий виртуальных машин Java. Даже компания Microsoft вынуждена при- лагать огромные усилия для того, чтобы убедить программистов в высокой эффек- тивности GC. Зачастую это выливается в представление намеренно завышенных оце- нок эффективности GC. Даже в технической документации Microsoft, связанной с GC, вы можете обнаружить слишком много преувеличений и слишком мало фактов. ПРИМЕЧАНИЕ-------------------------------------------------— Я вовсе не хочу сказать, что Microsoft GC плохо справляется со своей работой, напротив, он делает ее достаточно хорошо. Однако этот механизм работает иначе, чем рассказывается в документации Microsoft. В настоящее время для того, чтобы узнать, как именно работает GC, вы должны написать тестовые программы и изучить эффект их работы. В качестве стартовой точки исследования вы можете использовать пример GarbageTest. Воспользуйтесь инструментом анализа памяти Window, чтобы посмот- реть, каким образом запуск программы влияет на распределение памяти. В приме- ре GarbageTest определяется следующий класс крупных объектов (около 10 Кбайт): type TMyClass = class private data: Integer: list: array [1..10240] of Char: S: string; publ1c constructor Create: end: 1032
Установка агрегатов и обработка версий 1033 Самый простой тестовый код выглядит следующим образом: for 1 := 1 to 10000 do begin me := TMyClass.Create; WriteLn (mc.s): end; Если вы напишете эту простую программу в обычном Delphi, затем откомпили- руете и запустите ее, это приведет к перерасходу памяти, так как в программе от- сутствует обращение к методу Free. Однако в среде .NET благодаря GC для хране- ния всех этих объектов используется один и тот же блок памяти, поэтому память фактически не расходуется. Теперь попробуйте сохранить каждый из создаваемых объектов в массиве, объявленном следующим образом: objlist: array [1..10000] of TmyClass; В результате на каждом проходе цикла свободная память будет уменьшаться, что приведет к проблемам. Попробуйте хранить ссылки на объекты в памяти, а за- тем освобождать эти ссылки регулярно или в произвольном порядке. Примеры подобного тестирования содержатся в программном коде, однако вы должны при- менить ваши знания и воображение для разработки более сложных тестов. Установка агрегатов и обработка версий Большинство разработчиков программного обеспечения для Windows сталкива- лись с проблемами, связанными с распространением библиотек общего пользова- ния. Если библиотека используется более чем одним приложением, велика веро- ятность возникновения проблем. Инфраструктура .NET поддерживает несколько сценариев распространения программных модулей. В самом простом сценарии конечный пользователь может просто скопировать файлы в каталог и запустить программу. Когда приходит время удалить приложение с компьютера, конечный пользователь может либо удалить файлы, либо удалить весь каталог целиком (если в каталоге не содержится каких-либо полезных данных). В этом случае нет необхо- димости в использовании специального средства установки, нет необходимости в спе- циальной процедуре деинсталляции и нет необходимости в обращении к реестру. СОВЕТ---------------------------------------------------------------------- При желании вы можете запаковать приложение в пакет Microsoft Installer и при этом все равно выполнять установку в единственный каталог без какого-либо использования реестра. Однако помимо этого простого варианта все остальные сценарии установки выглядят сложно и несколько запутанно, так как проблема сама по себе является достаточно сложной. Каким образом разработчики могут устанавливать в системе новые версии компонентов общего пользования и при этом не нарушать работу приложений, использующих старые версии этих компонентов? Единственное воз- можное решение состоит в том, чтобы обеспечить параллельную установку в сис- теме различных версий одного компонента. Иными словами, в системе будут при- сутствовать и сосуществовать одновременно несколько версий одного и того же компонента. 1033
1034 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi Прежде всего, необходимо уяснить, что времена установки компонентов обще- го пользования в каталоге C:\Windows\System32 безвозвратно ушли в прошлое. Вме- сто этого в рамках .NET Framework поддерживается два типа агрегатов, хорошо определенные правила поиска этих агрегатов и инфраструктура, поддерживающая установку нескольких версий одного и того же агрегата в одной и той же системе. Прежде чем переходить к обсуждению двух типов агрегатов, я расскажу о двух типах установки: О Публичная (Public), или глобальная (Global), установка — публично уста- новленный агрегат предназначен для использования одновременно несколькими приложениями. Это могут быть приложения, разработанные вами, или прило- жения, разработанные разными людьми. Инфраструктура .NET Framework устанавливает хорошо известное местоположение для таких агрегатов. О Закрытая (Private) установка — агрегат, установленный закрыто, не предназ- начен для общего использования. Как правило, такой агрегат устанавливается в каталоге, в котором установлено ваше приложение. Тип создаваемого вами агрегата зависит от того, каким образом вы планируете установить его. Вот два типа агрегатов: О Агрегаты со строго заданным именем (Strong Name Assemblies) — такие аг- регаты обладают цифровой подписью. Сигнатура состоит из идентификатора агрегата (имя, версия, необязательная культура плюс ключевая пара с откры- той и закрытой составляющими). Все глобальные агрегаты должны быть агре- гатами со строго заданным именем. Имя, версия и культура — это атрибуты агрегата, сохраненные в его метаданных. Создание и обновление этих атрибу- тов зависит от используемых вами средств разработки. О Все остальные агрегаты — не существует официального наименования для аг- регатов, не обладающих строго заданным именем. Агрегат без строго заданного имени может быть установлен только при помощи закрытой (Private) установки. В системе нельзя установить одновременно несколько версий такого агрегата. Ключевая пара, при помощи которой осуществляется подпись агрегата со стро- го заданным именем, генерируется специальной утилитой, входящей в состав .NET Framework SDK. Эта утилита называется SN (sn.exe). Утилита SN создает ключе- вой файл, ссылка на который содержится в одном из атрибутов агрегата. Подпи- сать агрегат можно только в момент его создания или компоновки. Однако под- держивается также форма подписи, которая называется отложенной подписью (delayed signing). При этом разработчики работают только с открытой частью ключа. Позже, когда генерируется финальная сборка агрегата, в сигнатуру добавляется закрытый ключ. В настоящее время компилятор Delphi for .NET плохо работает со специальными атрибутами. Однако именно эти атрибуты используются для иден- тификации и ссылок на файл с ключами. По этой причине в Delphi for .NET под- держка агрегатов со строго заданным именем на момент написания данной книги реализована не полностью. Агрегаты со строго заданным именем, как правило, предназначены для общего использования, поэтому они устанавливаются в глобальном кэше агрегатов (Global Assembly Cache, GAC), однако их можно установить также и с использованием закрытой установки. GAC — это системный каталог со специальной внутренней 1034
Установка агрегатов и обработка версий 103! структурой, поэтому вы не можете просто скопировать сюда агрегат. Вместо этогс для установки агрегата вы должны использовать специальную утилиту под назва нием GACUTIL (gacutil.exe), входящую в состав .NET Framework Runtime. Ути лита GACUTIL устанавливает файлы в GAC и удаляет файлы из GAC. GAC — это специальный системный каталог с иерархией, которая поддержива ет параллельную установку нескольких версий агрегатов, как показано на рис. 24.4 Цель этой утилиты — скрыть от пользователя сложную структуру GAC, чтобы oi не беспокоился о выполнении служебных процедур, связанных с установкой и де инсталляцией агрегатов в GAC. Утилита GACUTIL читает идентификационнуи информацию из метаданных агрегата и использует эту информацию для создан и; нового подкаталога в рамках GAC, в этом подкаталоге будет размещаться устанав ливаемый агрегат. lift C:\WIND0WSVasseniMy F fife Edft JiW . 7oQls |2j С \WINOOWS\assembly 3 •.A Accessibility ?3hcscomprngd Cust omMarshalers iSSlCustomMarshaters l^IEExecRemote 3<|IEHost s&IIEHost ^ISymWrapper ^Microsoft JScnpt ^Microsoft VisualBask ^Microsoft VtsualBask vsa Microsoft VisualC ^Microsoft Vsa ^Microsoft Vsa Vb CodeOOMProcessor *^&Microsoft_VsaVb s^mscorctg l^mscorlib £&Regcode is£15oap5ud$Code S&System Native Images Native Images Native Images 1,0.3300,0 7 0 3300 0 1 0 3300 0 1 0 3300 0 1 0 3300 0 1 0 3300 0 1 0 3300 0 1 0 3300 0 7 0 3300 0 7 0 3300 0 7 0 3300 0 7 0 3300 0 7 0 3300 0 7 0 3300 0 7 0 3300 0 I 0 3300 0 I 0 3300 0 I 0.3300 0 I 0 3300 0 I 0 3300 0 b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7fttd50a3a bO3f5f7flld5O&3a b03f5f7fUd50a3a b03f5f7flld50a3a b03f5f7flld50a3a b03f5f7fiid50a3a b03f5f7flld50a3a b03f5f7fiid50a3a b03f5f7fiid50a3a b03f5f7fiid50a3a b03f5f7flid50a3a b03f5f7fl ld50a3a b03f5f7flid50a3a Ь77а5с5б1934е089 b03f5f7flid50a3a b03f5f7flid50a3a Ь77а5с561934е089 b03f5f7fHd50a?a Рис. 24.4. Содержимое глобального кэша .NET отображается в окне проводника Windows ПРИМЕЧАНИЕ-------------------------------------------------—----------- Если вы попытаетесь использовать проводник Windows для просмотра внутренней иерархии GAC вы не сможете этого сделать, так как система скрывает от вас внутреннее строение этого каталоге Иерархию каталогов можно изучить, открыв окно командной строки и перейдя в каталог C:\Wmdows assembly. Когда вы создаете ссылку на агрегат со строго заданным именем в GAC, вы ссы лаетесь на конкретную версию этого агрегата. Если позднее в системе устанавли ваегся тот же агрегат, он будет размещен в отдельном мессе GAC, при этом изна чальная версия будет сохранена. Таким образом, если приложение нуждаетс в конкретной версии агрегата, оно всегда может получить необходимую версию it GAC. 1035
1036 Глава 24. Архитектура Microsoft .NET с точки зрения Delphi Что далее? Ваша реакция на инициативу .NET будет зависеть от totq, в какой среде вы про- граммировали ранее. Если ранее вы имели дело со средой Java или с Delphi, мно- гие концепции .NET покажутся вам знакомыми. Если же ранее вы работали с ме- нее совершенными средствами разработки приложений для Windows (пусть даже объектно-ориентированными), вы наверняка обнаружите в .NET много нового и не- обычного. Платформа .NET является техническим достижением, обладающим ог- ромным количеством вариантов и возможностей. Если сравнивать с более ранни- ми технологиями системного взаимодействия, разработанными Microsoft (Windows API и СОМ), прогресс очевиден. Преимущество также состоит в том, что для ра- боты с .NET вы можете использовать инструменты и языки, к которым вы уже привыкли, и в скором времени к этому набору добавится среда разработки Delphi. В главе 25 мы рассмотрим специфические изменения, которые коснулись язы- ка Delphi. Мы проанализируем возможности и типы, которые считаются устарев- шими в среде .NET, кроме того, мы изучим новые возможности, добавленные в язык. Наконец, мы разберем примеры использования классов .NET Framework в среде Delphi for .NET. 1036
Г Обзор Delphi for .NET: язык и RTL В прошлой главе я рассказал об архитектуре Microsoft .NET. Я кратко проанали- зировал основные элементы этой архитектуры. Теперь настало время подробнее рассмотреть компилятор Delphi for .NET Preview, который поставляется в комп- лекте с Delphi 7. В данной главе я представлю специальные изменения, которые были внесены в язык Delphi, чтобы сделать его совместимым с Common Language Runtime. Для этой цели в язык были сделаны некоторые немаловажные добавле- ния, например пространства имен и новые спецификаторы видимости. Некоторые давно знакомые программистам Delphi возможности, напротив, были отменены, так как они не поддерживаются в среде, безопасной в отношении преобразования типов. Здесь обсуждаются некоторые аспекты нового компилятора, включая исполь- зование некоторых собственных библиотек Microsoft для поддержки .NET и ASP. Следует иметь в виду, что на момент написания данной книги компилятор Delphi for .NET все еще находится на стадии разработки. Компания Borland решила вы- пустить предварительную демонстрационную версию компилятора вместе с Delphi 7, а затем, на протяжении 2002 года вплоть до начала 2003 года (когда будет завер- шен проект Galileo), выпускать периодические обновления этой версии для заре- гистрированных пользователей Delphi. Некоторые из возможностей, о которых рассказывается в данной главе, возможно, не реализованы, а может быть, реализо- ваны не полностью, — все зависит от того, с какой версией компилятора вы работа- ете. В конце данной главы я приведу список веб-ресурсов, за которыми вы можете следить, чтобы своевременно узнать о новостях компилятора Borland Delphi for .NET Preview. ПРИМЕЧАНИЕ------------------------------------------------------------ Как уже отмечалось в главе 24, большую часть материала предоставил Джон Бушакра (John Bushakra). В данной главе рассматриваются следующие вопросы: О изменения языка Delphi; О устаревшие и новые особенности языка; О библиотека времени исполнения и VCL; О использование библиотек Microsoft; О ASP.NET и язык Delphi. 1037
1038 Глава 25. Обзор Delphi for .NET: язык и RTL Устаревшие возможности языка Delphi Для того чтобы сделать язык Delphi совместимым с CLR (Common Language Runtime), потребовалось убрать из него некоторые возможности. В данном разде- ле я рассмотрю возможности, которые удалены из языка Delphi for .NET и счита- ются устаревшими. Позднее я перейду к рассмотрению возможностей, которые добавлены в язык или будут добавлены в него в ближайшем будущем. Устаревшие типы Некоторые из типов, поддерживаемых в Delphi, не будут перенесены в среду Delphi for .NET. Вот перечень типов, которые уже считаются устаревшими либо будущее которых считается неопределенным: о Указатели. Указатели считаются небезопасными типами в среде CLR. Любые формы арифметики указателей считаются запрещенными. В данном случае характеристика «небезопасный» означает, что код, содержащий указатели, нельзя проверить на безопасность типов. Возможно, в финальной версии Delphi for .NET будут поддерживаться небезопасные неуправляемые указатели, одна- ко в настоящее время для реализации части функциональности, реализуемой с использованием указателей, вы можете использовать динамические массивы. Функции GetMem, FreeMem и ReallocMem также считаются устаревшими. о Типы, основанные на file of <тип>. Файловые типы, основанные на старом синтаксисе языка Pascal, то есть file of <тип>, не могут поддерживаться, так как компилятор не может определить размер заданного типа на целевой платформе. О Синтаксис Object, использовавшийся до появления Delphi. Этот синтаксис использовался в эпоху Turbo Pascal. Он позволял вам объявить новый класс в форме type MyClass = object;. Переменные этого типа основаны на стеке, в то время как обычные объекты классовых типов основаны на куче (heap). Таким образом, этот синтаксис не будет поддерживаться в финальной версии компи- лятора. О Real48 и Comp. Эти типы не будут поддерживаться. Real48 — это 6-байтовый тип числа с плавающей точкой. Вместо типа Comp в будущем будет использо- ваться тип Int64, как отмечено в справочнике по языку Delphi 7 (Delphi 7 Lan- guage Reference). Строки и другие типы Далее перечисляются типы, которые не являются кандидатами на исключение из чзыка, однако реализация которых, скорее всего, претерпит изменения. Измене- тия будут фактически прозрачными, однако при определенных условиях вы мо- кете ожидать, что поведение этих типов будет несколько иным. Э Строки (strings). В Delphi for .NET строки соответствуют поддерживаемому в рамках CLR типу System.String, при этом по умолчанию они состоят из двух- байтовых символов (каждый символ кодируется 16-битным числом). То есть они напоминают тип WideString языка Delphi 7. О таких строках говорят, что 1038
Устаревшие возможности языка Delphi 1039 они «широкие» (wide). Соответственно, все символы по умолчанию тоже явля- ются «широкими». О Записи (records). Записи соответствуют типам значений (value types). В главе 24 мы говорили о двух основных категориях типов, поддерживаемых в рамках CTS (Common Type System): типы ссылок (reference types) и типы значений (value types). На платформе .NET запись будет типом значения. CLR требует, чтобы в отношении типов значений невозможно было использовать наследование, од- нако вы можете включать в их состав методы (это совершенно новая для Delphi возможность). Методы, определенные для типов значений, должны быть объяв- лены с ключевым словом final. (Новое ключевое слово final обсуждается далее, в разделе «Новые возможности языка Delphi».) О Тип TDateTime. В Delphi этот тип основан на той же реализации, что и тип DATE компании Microsoft (см. главу 2). На платформе .NET используется иная реализация. Структура System.DateTime (потомок типа System.ValueType) изме- ряет время от полуночи 1 января 0001 С.Е. (Common Era — общая эра) до 11:59:59 Р.М. (23:59:59) 31 декабря 9999 С.Е. У этих часов один «тик» равен 100 наносе- кундам. В дальнейшем среда Delphi for .NET будет использовать для измере- ния времени стандарт платформы .NET. Таким образом, вычисления даты, ос- нованные на числах с плавающей точкой, потребуют вашего внимания при переносе на платформу .NET. Фактически, вы используете поддерживаемые в Delphi функции Trunc и Frac для выделения времени и даты из значения с пла- вающей точкой. о Валюта (Currency). Тип Currency соответствует типу System.0еа’та1среды CLR. Устаревшие возможности кода Помимо перечисленных ранее типов, некоторые возможности языка Delphi также не могут быть перенесены на платформу .NET: О Вариантные записи (Variant Records). Вариантные записи с перекрывающи- мися полями не поддерживаются в среде CLR. В общем случае вы не можете заранее знать порядок расположения полей, так как компилятор JIT (Just in Time) оставляет за собой право оптимизировать расположение данных, чтобы удовлетворять требованиям платформы, на которой выполняется компиляция. О ExitProc. События не всегда происходят так, как вы хотели бы, и в том поряд- ке, в котором вы хотели бы. Проблемы с инициализацией и финализацией мо- дулей решены (однако по-прежнему существуют некоторые требующие вни- мания аспекты, о которых я расскажу чуть позднее), однако процедура ExitProc больше не поддерживается. О Динамическая агрегация интерфейсов. CLR не поддерживает динамическую агрегацию интерфейсов с использованием ключевого слова implements, так как такую агрегацию невозможно проверить на безопасность типов. Класс обязан объявить все интерфейсы, которые он реализует. О Выражения ASM. Компилятор Delphi for .NET Preview не поддерживает встро- енного ассемблера и выражений ASM. Будущее ключевого слова asm в финаль- 1039
1040 Глава 25. Обзор Delphi for .NET: язык и RTL ной версии компилятора сомнительно. Следует иметь в виду, что компилятор будет обладать возможностью смешивать управляемый IL-код и неуправляе- мый код, естественный для процессора, на котором выполняется программа. О Ключевое слово automated. Ключевое слово automated добавлено в язык для поддержки OLE Automation. В среде .NET оно не нужно. То же самое относит- ся к ключевому слову dispid, которое используется для обращения к методам COM Automation по номеру, а не по имени. Однако обратите внимание, что на платформе .NET поддерживаются уникальные идентификаторы GUID, несмот- ря на то, что необходимости в их использовании нет. Эти идентификаторы выг- лядят, как специальные атрибуты типа. О Функции прямого доступа к памяти. Функции прямого доступа к памяти, та- кие как BlockRead, BlockWrite, GetMem, FreeMem и ReaiiocMem, а также функции Absolute и Addr имеют дело с неуправляемыми указателями, поэтому их нельзя использовать в управляемом безопасном коде. Оператор @ поддерживается в текущей версии компилятора Delphi for .NET Preview (скорее всего, поддер- жка этого оператора исчезнет в финальной версии компилятора), однако вы не можете выполнять преобразование указателей или применять арифметику ука- зателей. ПРИМЕЧАНИЕ----------------------------------------------------------- Как уже отмечалось в главе 2, среда Delphi 7 поддерживает новый набор предупреждающих сооб- щений, предназначенных для того, чтобы облегчить вам перевод кода на новый стандарт. Эти предупреждающие сообщения указывают вам на возможности и конструкции языка, которые счита- ются небезопасными на платформе .NET и которых следует избегать. По умолчанию для нового проекта Delphi 7 предупреждающие сообщения отключены, однако если вы заново компилируете в Delphi 7 старый проект, вывод предупреждающих сообщений активируется. Вы можете включить вывод предупреждений при помощи директивы компилятора {$WARN L)NSAFE_CODE ON} и других директив из этой категории. Подробнее об этом рассказывается в главе 2. Новые возможности языка Delphi В первую версию компилятора dccil добавлены новые возможности, необходимые для CLR, кроме того, новые возможности добавлены также в последующие версии этого компилятора. Пространства имен Пространства имен (namespaces) играют важную роль в .NET Framework. Они по- зволяют нескольким независимым сторонним производителям расширить иерар- хию классов без опасений, что может возникнуть конфликт символьных имен. В Windows и СОМ для уникальной идентификации компонентов используется 16-байтный глобально-уникальный идентификатор GLIID, это магическое число дол- жно быть записано в системном реестре. На платформе .NET для решения задачи идентификации компонентов используется концепция пространств имен, метадан- ные, а также строгие и быстрые правила поиска агрегатов. Благодаря всему этому идентификаторы GUID становятся ненужными. 1040
Новые возможности языка Delphi 1041 Как это ни странно, идея модуля в среде Delphi аналогична идее пространства имен CLR. Можно сказать, что модуль — это контейнер для символов (символь- ных идентификаторов), в то время как пространство имен — это контейнер для модулей. В Delphi for .NET пространство имен, к которому принадлежит модуль, определяется в рамках выражения unit: unit NamespaceA.NamespaceB.UmtA; Символы точек указывают на то, что пространство имен NamespaceB является частью пространства имен NamespaceA, а модуль UnitA является частью простран- ства имен NamespaceB. Символы точек разделяют это объявление на несколько ком- понентов. Каждый из компонентов за исключением расположенного в самой пра- вой позиции является именем пространства имен. Все объявление, включая все компоненты и точки, является именем модуля. Символ точки является разделите- лем. Это объявление не добавляет в программу никаких новых символов. В дан- ном примере NamespaceA.NamespaceB — это имя пространства имен, a NamespaceA. NamespaceB.UnitA — это имя модуля. Имя файла с исходным модулем может вы- глядеть следующим образом: NamespaceA.NamespaceB.UnitA.pas, компилятор может сгенерировать выходной файл с именем NamespaceA.NamespaceB.UnitA.dcuil. В случае необходимости при помощи выражения program (а также выражений package и library) можно определить пространство имен по умолчанию для всего проекта. В противном случае проект называется универсальным проектом (generic project), а пространство имен для него указывается при помощи ключа -ns компи- лятора. Если для компилятора не указано пространство имен по умолчанию, тогда пространства имен вообще не используются (как это было в Delphi 7 и предыду- щих версиях). В выражении unit не обязательно указывать членства в каком-либо простран- стве имен. Это выражение может выглядеть, как традиционное выражение Delphi: unit UmtA: Модуль, который не объявляет членства в пространстве имен, называется уни- версальным модулем (generic unit). Универсальные модули автоматически стано- вятся членами пространства имен проекта. Однако имейте в виду, что это никак не влияет на имя файла с исходным кодом. ВНИМАНИЕ--------------------------------------------------------------------- На момент написания данной книги поддержка пространств имен реализована лишь отчасти. В дан- ном разделе скорее описывается, каким образом этот механизм должен работать в будущем, а не как он работает сейчас. В файл проекта вы можете добавить выражение namespaces для того, чтобы пе- речислить список пространств имен, в рамках которых компилятор будет произ- водить поиск универсальных модулей. Выражение namespaces должно располагать- ся сразу же за выражением program (или package, или library) и перед любым другим выражением или блоком типа. Имена пространств имен разделяются запятыми, в конце списка ставится символ точки с запятой. Например: program NamespaceA.MyProgram namespaces FooBar, Foo.Frob. Foo Nitz: В этом примере к пространству поиска универсальных модулей добавляются имена пространств имен Foo.Bar, Foo.Frob и Foo.Nitz. 1041
1042 Глава 25. Обзор Delphi for .NET: язык и RTL Теперь имеет смысл рассмотреть, каким образом компилятор выполняет поиск универсальных модулей при построении программы. Если вы используете модуль и полностью указываете его имя с указанием пространства имен, никаких проблем не возникает: uses Foo.Frob.Gizmos: В этом случае компилятор знает имя файла dcuiL (или имя файла .pas). Однако представьте, что вы указали только следующее: uses Gizmos: Это называется ссылкой на универсальный модуль (generic unit reference). В этом случае компилятор должен обладать методом поиска файла dcuiL. Компилятор производит поиск пространств имен в следующем порядке. 1. Пространство имен текущего модуля (если такое есть). 2. Пространство имен по умолчанию для проекта (если такое есть). 3. Пространства имен, перечисленные в выражении namespaces для проекта (если такие есть). 4. Пространства имен, указанные при помощи ключа компилятора. 5. Что касается первого пункта, если для текущего модуля указывается простран- ство имен, последующие ссылки на универсальные модули, указанные в выра- жении uses, в первую очередь будут разыскиваться в пространстве текущего модуля. Рассмотрим следующий пример: unit Foo.Frob.Gizmos; uses doodads: Прежде всего модуль doodads будет разыскиваться в пространстве имен Foo.Frob. Иными словами, компилятор попытается открыть файл с именем Foo. Frob. Doodads. dcuiL. Если поиск окончится неудачей, компилятор переходит к следующему пунк- ту и в качестве префикса добавляет к имени doodads пространство имен по умолча- нию для проекта. И так далее по списку. Один и тот же символьный идентификатор может присутствовать в несколь- ких разных пространствах имен. Если возникает подобная неоднозначность, вы должны ссылаться на символ по его полному имени, то есть с указанием полного имени пространства имен и модуля, в котором определен этот символ. Если в мо- дуле Foo.Frob.Gizmos определен символ Hoozitz, вы можете сослаться на этот символ одним из следующих выражений: Hoozitz: // если нет конфликта имен Foo.Frob.Gizmos.Hoozitz: Однако следующие выражения будут ошибочными: Gizmos.Hoozitz: 11 ошибка Frob.Gizmos.Hoozitz: l! ошибка Имена модулей и пространств имен могут оказаться достаточно длинными и труднозапоминаемыми. Чтобы упростить работу с ними, вы можете создать псев- доним такого имени. Для этого используется ключевое слово as в выражении uses: uses Foo.Frob.DepartmentOfRedundancyDepartment.UIToys as Toyllmt; Псевдоним имени модуля — это еще один новый идентификатор, поэтому он не может конфликтовать с идентификаторами того же самого модуля (псевдони- 1042
Новые возможности языка Delphi 1043 мы являются локальными для модулей, в которых они определены). Даже если вы определили псевдоним, вы по-прежнему можете использовать изначальное, более длинное имя, указывающее на модуль. ПРИМЕЧАНИЕ-------------------------------------------------------------— Регистр символов, использовавшийся в объявлении пространства имен, сохраняется и записывает- ся в метаданных агрегата. Однако если вы работаете в среде Delphi, два пространства имен, имена которых отличаются только регистром символов, считаются эквивалентными. Расширенные идентификаторы CTS и CLR позволяют интегрировать в единое целое код, написанный на множе- стве разных языков, однако в результате этого у разработчиков компиляторов возникают весьма интересные проблемы. Прежде всего, в разных языках исполь- зуются разные ключевые слова. Как правило, ключевое слово является зарезерви- рованным, его нельзя использовать в качестве символьного идентификатора. Но что делать, если в CLR определен класс, имя которого совпадает с именем одного из ключевых слов вашего языка? Например, в CLR определен класс с именем Туре, однако слово type является ключевым словом языка Delphi. Чтобы избежать кон- фликта, в среде Delphi for .NET можно воспользоваться одним из двух способов (в Delphi 7 и в предыдущих версиях эти способы не реализованы). Во-первых, вы можете использовать полное имя идентификатора, например: var Т: System.Туре: Во-вторых, добавьте к имени класса в качестве префикса символ «амперсанд» (&). Вот выражение, которое эквивалентно предыдущему: var Т: &Туре; В этом выражении амперсанд сообщает компилятору, что следует использо- вать символ с именем Туре и не рассматривать его в качестве ключевого слова. В ре- зультате компилятор будет разыскивать символ Туре в доступных модулях и обна- ружит его в модуле System (механизм работает вне зависимости от того, в каком модуле был определен символ). Ключевые слова final и sealed Для поддержки CLI (Common Language Infrastructure) в среду Delphi for .NET были добавлены еще две концепции: атрибут класса sealed и атрибут метода final. Класс, для которого указан атрибут sealed, не может быть использован в качестве базово- го класса. Вот пример кода: type TDerivl = class (TBase) procedure A; override-. end sealed: На базе такого класса нельзя построить производный класс. Аналогично, вир- туальный метод, помеченный с использованием атрибута final, не может быть пе- реопределен в производном классе. Вот соответствующий пример кода: 1043
1044 Глава 25. Обзор Delphi for .NET: язык и RTL type TDerivl = class (TBase) procedure A; override: final; end; TDeriv2 = class (TDerivl) procedure A; override; // ошибка: нельзя переопределить метод final end Компания Borland добавила в язык Delphi ключевые слова sealed и final для того, чтобы обеспечить поддержку соответствующих возможностей платформы .NET, но зачем компании Microsoft потребовалось изобретать эти атрибуты? Ат- рибуты final и sealed предоставляют пользователям вашего кода важные подсказки относительно того, как вы представляете себе дальнейшее использование разрабо- танных вами классов. Более того, благодаря этим атрибутам компилятор получает возможность генерировать более эффективный код CIL (Common Intermediate Language). Новые спецификаторы видимости и доступа Используемые в Delphi спецификаторы видимости — public, protected и private — отличаются от представлений о видимости, используемых в рамках CLI. В языках наподобие C++ и Java, когда для члена класса вы указываете видимость private или protected, этот член класса становится видимым только для потомков класса, в ко- тором он определен. Однако, как рассказывалось в главе 2, в Delphi видимость private и protected реализована в терминах модулей: все элементы, определенные в рамках одного и того же модуля, считаются видимыми друг для друга. Чтобы стать совместимым с требованиями CTS, язык нуждается в новых спецификаторах видимости. О class private — в отношении члена класса, для которого указана видимость class private, действуют такие же правила видимости, как и в языках программирова- ния C++ и Java. Говоря точнее, к членам, обладающим видимостью class private, можно обратиться только из методов и свойств класса, в которых они объявле- ны. Процедуры и функции, объявленные в этом же модуле, а также методы любых других классов не обладают возможностью доступа к этим членам. О class protected — аналогично, члены со спецификатором видимости class protected видимы только внутри класса, в котором они объявлены, и только внутри клас- сов, являющихся производными от этого класса. Другие классы в этом же мо- дуле обладают правом доступа только в случае, если они являются производ- ными от класса, в котором объявлены эти члены. Тривиальный код, демонстрирующий эти спецификаторы, содержится в при- мере Protected Private, расположенном в папке LanguageTest исходного кода для дан- ной главы. Члены class static В Delphi уже давно поддерживаются методы класса — это методы, которые вы мо- жете применить к классу как к единому целому. При этом класс рассматривается как один из его экземпляров. Параметр Self такого метода ссылается не на текущий 1044
Новые возможности языка Delphi 1045 объект, а на текущий класс. В Delphi for .NET эта идея расширена за счет использо- вания спецификатора class static, при этом появляется возможность использовать свойства class static, поля class static и конструкторы класса. О Методы class static. Подобно методам класса в Delphi 7, к методам class static можно обратиться, не имея объекта класса, при этом не существует параметра Self, который ссылался бы на экземпляр объекта. В отличие от Delphi 7, вы не можете ссылаться на сам класс. Например, обращение к методу ClassName не сработает. Кроме того, в отличие от Delphi 7, в отношении методов class static вы не можете использовать ключевое слово virtual. О Свойства class static. Как и к методам класса, к свойствам class static можно обращаться, не обладая экземпляром объекта. Методы доступа к таким свой- ствам тоже должны быть объявлены как class static. Свойства class static не мо- гут быть опубликованы, кроме того, им нельзя ставить в соответствие значение по умолчанию или значение stored. О Поля class static. К полю class static можно обратиться, не имея экземпляра объекта. Поля и свойства class static, как правило, используются в качестве ин- струментов проектирования. Они позволяют вам объявлять переменные и кон- станты внутри осмысленного контекста объявления класса. О Конструктор класса. Конструктор класса — это закрытый (private) конструк- тор (он должен быть определен со спецификатором class private), который вы- полняется перед самым первым использованием объявляемого класса. CLR не предоставляет никаких гарантий относительно того, когда это происходит. Из- вестно только, что это происходит до самого первого использования класса. В терминах CLR это может оказаться несколько запутанно, так как код счита- ется неиспользованным до тех пор, пока он не выполнен. В рамках класса мож- но объявить только один конструктор класса. Производные классы могут объ- явить свои собственные конструкторы класса, однако для каждого класса может быть объявлен только один такой конструктор. Вы не можете обратиться к кон- структору класса из разрабатываемого вами исходного кода — обращение к конструктору класса осуществляется автоматически. Конструктор класса яв- ляется средством инициализации полей и свойств class static. Запрещено даже использовать ключевое слово inherited, так как компилятор берет на себя все заботы, связанные с вызовом таких конструкторов. Далее приводится фрагмент кода, который иллюстрирует синтаксис новых спе- цификаторов: TMyClass = class class private // можно обращаться только внутри класса TMyClass II конструтор класса должен обладать видимостью class private class constructor Create; class protected // можно обратиться из TMyClass и из его потомков И Функции доступа к свойству Р1 со спецификатором class static class static function getPl : Integer; class static procedure setPKval : Integer): public // к функции fx можно обратиться без экземпляра объекта class static function fx(p : Integer) ; Integer; // свойство Pl co спецификатором class static должно обладать функциями доступа 1045
1046 Глава 25. Обзор Delphi for .NET: язык и RTL // со спецификаторами class static class static property Pl : Integer read getPl write setPl: end: Вложенные типы Вложенный тип (nested type) определяется внутри определения класса, благодаря чему содержащий его класс используется в качестве пространства имен для этого типа. Доступ к вложенному типу осуществляется так же, как и к полю класса, то есть в качестве префикса через символ точки указывается имя класса. Это можно сделать, не обладая экземпляром класса. События с несколькими слушателями В Delphi всегда можно было настроить слушателя для события (event listener), то есть функцию, обращение к которой выполняется в момент, когда генерируется событие. В рамках CLR поддерживается назначение одному событию нескольких слушателей. Иными словами, в момент генерации события может быть выполне- но обращение не к одной, а к нескольким функциям. Такие события называются событиями с несколькими слушателями (multicast events). В Delphi for .NET под- держиваются два новых метода доступа к свойствам: add и remove. Эти методы спе- циально предназначены для поддержки событий с несколькими слушателями. Методы add и remove могут использоваться только в отношении свойств, которые являются событиями. Чтобы обеспечить поддержку событий с несколькими слушателями, должен существовать способ сохранить все функции, которые зарегистрированы в каче- стве слушателей. Как было отмечено в главе 24, события с несколькими слушате- лями реализованы с использованием определенного в рамках CLR класса Multi- castDelegate. Компилятор выполняет самую сложную часть работы за вашей спиной. Ключевые слова add и remove обеспечивают сохранение и удаление слуша- телей событий, однако механизм хранения этих слушателей считается закрытой частью реализации, предполагается, что вы не будете иметь с ним дело. Компиля- тор генерирует методы add и remove автоматически. Эти методы реализуют сохра- нение слушателей событий наиболее эффективным образом. В финальной версии Delphi for .NET методы add и remove должны работать в со- трудничестве с перегруженными версиями стандартных функций Include и Exclude. Если в разрабатываемом вами коде вы хотите зарегистрировать некоторый метод как слушатель события, вы должны обратиться к Include. Чтобы удалить метод из числа слушателей события, обратитесь к методу Exclude: Include(EventProp. eventHandler); Exclude(EventProp. eventHandler): У вас за спиной функции Include и Exclude будут обращаться к методам, соот- ветствующим функциям доступа add и remove соответственно. На момент написа- ния данной книги эта технология еще не работает, поэтому в примерах подобный код не используется. Для поддержки старого кода в Delphi for .NET в отношении обработчика собы- тия можно применить оператор :=. Этот оператор работает так же, как он работал 1046
Новые возможности языка Delphi 1047 и ранее. При помощи этого оператора вы по-прежнему можете назначить един- ственный обработчик некоторого события. В результате компилятор генерирует код, заменяющий обработчик события, который был назначен последним при по- мощи оператора :=. Замене подвергается только этот обработчик события. Опера- тор присваивания работает отдельно и независимо от механизма add/remove (или Include/Exclude). Иначе говоря, используя оператор присвоения, вы никоим обра- зом не влияете на список обработчиков, которые были добавлены в MulticastDelegate. Пример кода, работающего с событиями, содержится в программе XmlDemo. Следующий фрагмент кода является полностью работоспособным на момент на- писания данной книги. Код создает кнопку и устанавливает два обработчика для события Click: MyButton = Button.Create; MyButton.Location := Point.Create ( Width div 2 - MyButton.Width div 2. 2); MyButton.Text := 'Load'-, MyButton.add_C1ick (OnButtonCl1ck); MyButton.add_click (OnButtonClick2); Controls.Add (MyButton); Специальные атрибуты Если вы помните, в главе 24 было сказано, что одним из важных требований CLI является расширяемая система метаданных. Все компиляторы языков .NET долж- ны генерировать метаданные для типов, определенных внутри агрегата. Расширя- емая часть расширяемых метаданных означает, что программисты могут опреде- лять свои собственные атрибуты и применять их к чему угодно: к агрегатам, классам, методам и проч. Компилятор добавляет эти атрибуты в метаданные агрегата. На этапе исполнения вы можете запросить значение некоторого атрибута, применен- ного в отношении того или иного элемента языка (агрегата, класса, метода и т. п.), для этого используются методы CLR-класса System.Ту ре. Специальные атрибуты — это ссылочные типы, производные от CLR-класса System.Attribute. Объявление класса специального атрибута выполняется так же, как объявление любого другого класса (следующий фрагмент кода извлечен из тривиального проекта NetAttributes, являющегося частью папки LanguageTest): type TMyCustomAttribute = class(TCustomAttributes) private FAttr : Integer; public constructor Create(val : Integer); property customAttribute : Integer read FAttr write FAttr: end. constructor TMyCustomAttribute.Create(val: Integer) begin inherited Create; customAttribute := val. end. Синтаксис для применения специального атрибута аналогичен синтаксису, используемому для этой цели в языке С#: 1047
1048 Глава 25. Обзор Delphi for .NET: язык и RTL type [TMyCustomAtt ribute(17)] TFoo = class public function P1(X : Integer) : Integer; end; Специальный атрибут применяется к конструкции, которая следует сразу же за ним. В данном примере специальный атрибут применяется к классу TFoo. Не- сомненно, вы обратили внимание, что синтаксис применения специального атри- бута фактически идентичен синтаксису идентификаторов GUID в Delphi. Возни- кает проблема: идентификаторы GUID применяются к интерфейсам, при этом GUID должен указываться сразу же после объявления интерфейса. С другой сто- роны, специальный атрибут указывается непосредственно перед объявлением, в от- ношении которого он применяется. Каким образом компилятор может определить, является ли строка, заключенная в квадратные скобки, идентификатором GUID в стиле Delphi (и тогда эту строку необходимо применить к интерфейсу, объяв- ленному перед ней), или это специальный атрибут в стиле .NET (и тогда его необ- ходимо применить к самому первому члену, объявленному в рамках интерфейса)? Не существует способа отличить одно от другого, поэтому требуется опреде- лить специальный случай для специальных атрибутов и интерфейсов. Если вы применяете GUID в отношении интерфейса, этот идентификатор должен распо- лагаться немедленно сразу же после объявления интерфейса. Тогда соблюдается привычный синтаксис Delphi: type interface IMylnterface ['(12345678-1234-1234-1234-1234567890db)'] Для применения идентификаторов GUID в рамках CLR используется поддер- живаемый в CLR специальный атрибут GuidAttribute. Этот атрибут является час- тью пространства имен System.Runtime.InteropServices. Если вы используете этот специальный атрибут, вы должны придерживаться стандарта CLR и поместить объявление атрибута перед объявлением интерфейса. Помощник класса Помощники классов (class helpers) — это интригующая новая возможность языка Delphi for .NET. Компания Borland решила добавить эту возможность в язык для того, чтобы обеспечить отображение собственных классов RTL на базовые классы .NET. Позже мы подробнее рассмотрим эту проблему в разделе «Помощники клас- сов для RTL». А сейчас я представлю общее описание этой возможности. Помощник класса позволяет вам расширить класс, не создавая при этом произ- водного класса. Используя этот механизм, вы получаете возможность добавлять в класс новые методы (но не новые данные). В отличие от наследования, вы може- те создавать объекты расширенного класса, используя изначальное имя класса. Ина- че говоря, вы можете добавлять методы к существующему объекту существующе- го класса. Чтобы прояснить эту идею, приведу простой пример. Представьте, что у вас есть класс (возможно, этот класс написан не вами, иначе вы могли бы добавить в него новый метод обычным способом). Объявление клас- са выглядит следующим образом: 1048
Библиотека времени исполнения и VCL 1049 type TMyObject » class private Value: Integer; Text: string, publi c procedure Increase; end: Теперь вы можете добавить в этот класс новый метод. Для этого необходимо написать помощник класса: type TMyObjectHelper = class helper for TMyObject public procedure Show; end. procedure TMyObjectHelper.Show; begin WriteLn (Text + ' ' + IntToStr (Vlaue) Self.ClassType ClassName ToString): end; Обратите внимание, что ключевое слово Self в методе помощника класса ссыла- ется на объект класса, для которого написан этот помощник. Теперь вы можете пользоваться этим объектом следующим образом: Obj := TMyObject Create: Obj.Show; В результате выполнения кода на экране появится имя класса TMyObject. Если вы создадите класс, производный от TMyObject, метод-помощник класса будет до- ступен также в производном классе (иными словами, вы добавляете метод-помощ- ник ко всей иерархии). Все будет работать так, как будто этот метод является обыч- ным членом класса TMyObject. Вы можете поэкспериментировать с этой новой возможностью при помощи примера ClassHelperDemo, расположенного в каталоге LanguageTest. Библиотека времени исполнения и VCL Исходные файлы библиотеки времени исполнения (RTL, Run-Time Library) рас- полагаются в подкаталоге source\rtl каталога, в котором установлен компилятор Delphi for .NET Preview. Наверняка вы обратите внимание на миграцию модулей в пространства имен CLR, что отражается в именах исходных файлов. Компания Borland в большинстве случаев старается сохранять изначальное имя модуля и добавляет к нему префикс Borland.Delphi. Элементы языка, имеющие отно- шение к операционной системе Windows (например, средства работы с реестром и с INI-файлами), размещаются в пространстве имен Borland.Win32, так как это классы, процедуры и функции, которые являются специфичными для Borland адаптерами для специфичных для Windows механизмов. Следует ждать, что подобный подход к именованию будет под держиваться и в будущем, однако не все модули будут пере- 1049
1050 Глава 25. Обзор Delphi for .NET: язык и RTL несены в новую среду подобным образом. Содержимое некоторых модулей, воз- можно, будет добавлено в то или иное существующее пространство имен. Я рекомендую вам внимательно просмотреть исходные файлы RTL, так как это полезно с образовательной точки зрения, однако помните, что вы имеете дело с предварительной версией продукта. Содержимое исходных файлов RTL может меняться от версии к версии — вы не должны делать каких-либо заблаговремен- ных предположений на этот счет. И конечно же, не стоит разрабатывать код, зави- сящий от того или иного аспекта внутреннего строения RTL. Помощники классов для RTL Давайте подробнее рассмотрим, что произошло с RTL в версии Delphi for .NET. Наиболее интересным нововведением, наверное, является появление помощни- ков классов. В Borland.Delphi.System.pas содержится следующее объявление: type TObject - System.Object: Это объявление сообщает вам, что поддерживаемый в Delphi класс TObject яв- ляется псевдонимом класса System.Object. Это чрезвычайно важно: TObject не явля- ется. производным от класса System.Object, он является его семантическим экви- валентом. Но что произошло с методами, которые в предыдущих версиях Delphi были определены в рамках класса TObject? Например, поддерживаются ли та- кие методы, как ClassName и ClassParent? Именно здесь на сцене появляются помощники классов. Методы, которые ранее были напрямую определены и реализованы в классе TObject, теперь объявлены и определены в классе с названием TObjectHelper. Класс TObjectHelper является помощником классаTObject. В файле Borland.Delphi.System.pas присутствует следующее объявление: type TObjectHelper = class helper for TObject procedure Free: function ClassType: TClass: class function ClassName: string: class function ClassNameIs(const Name: string): Boolean: class function ClassParent: TCI ass; class function Classinfo: TObject: class function InheritsFrom(AClass: TClass): Boolean: class function MethodAddress(const Name: string): TObject: class function SystemType: System.Type: function FleldAddress(const Name: string): TObject: procedure Dispatch(var Message); end: Помощник класса позволяет вам расширить класс, не создавая при этом произ- водный класс. Чтобы воспользоваться CLR-классом в рамках существующего Delphi-кода, вы должны расширить класс, но при этом избежать создания нового производного класса. Без сомнения, вы обратили внимание на то, что функциональность инфра- структуры классов Delphi во многом совпадает с функциональностью инфраструк- туры классов .NET Framework. В некоторых случаях возникают наложения, на- 1050
Библиотека времени исполнения и VCL 1051 пример класс Exception по своему назначению и функциям аналогичен классу System.Exception инфраструктуры CLR. С одной стороны, необходимо обеспечить переход на использование нового класса. С другой стороны, поддерживаемый в рамках Delphi класс Exception используется огромным количеством написанных на текущий момент программ Delphi. Единственное подходящее решение состоит в том, чтобы создать механизм, ко- торый позволил бы разработчикам (включая разработчиков Borland) использо- вать функциональность CLR-классов и при этом добавить в классы CLR поддерж- ку поведения, характерного для традиционных классов Delphi. VCL Классы .NET Framework в пространстве имен System.Windows.Forms отнюдь не яв- ляются заменой механизмов GUI, поддерживаемых в рамках Win32 APL То же самое относится и к другим разделам .NET Framework: их роль заключается в об- легчении использования возможностей APL Инфраструктура классов .NET Frame- work обеспечивает более удобный объектно-ориентированный интерфейс, сов- местимый с базовыми службами среды .NET. Подмножество функций Win32, связанное с графическим интерфейсом, по-прежнему доступно в рамках инфра- структуры .NET. Механизмы, связанные с графическим интерфейсом, объедине- ны в пространстве имен System.Windows.Forms, оформлены в виде методов классов и дополнены моделью событий. Однако следует иметь в виду, что классы из про- странства имен System.Windows.Forms обращаются к неуправляемому коду в Win32. Иными словами, когда вы используете System.Windows.Forms, вы по-прежнему об- ращаетесь к вызовам Win32 API, однако между вашей программой и Win32 API располагается толстый слой программного обеспечения под общим названием CLR. Важно понимать, что поддерживаемая в рамках Delphi библиотека VCL исполь- зует точно такой же подход. Однако VCL и System.Windows.Forms поддерживаются в рамках Delphi for .NET параллельно. Это связано со строением иерархий клас- сов. Класс TObject получается из класса System.Object при помощи помощника клас- са. Классы TPersistent и TComponent по-прежнему являются производными от клас- са TObject. Следовательно, VCL-класс TForm, к примеру, не является наследником класса System.Windows.Forms.Form. Вместо этого иерархия классов VCL почти пол- ностью сохранена в том виде, в котором она присутствовала в предыдущих верси- ях Delphi. Класс TForm является прямым наследником класса TWinControl, который, в свою очередь, является потомком TControl, а класс TControl, в свою очередь, явля- ется наследником класса TComponent. Если иерархию классов System.Windows.Forms рассматривать как единую ветвь, то иерархия VCL пролегает как бы параллельно иерархии System.Windows.Forms, но не является производной частью этой иерархии. Обе инфраструктуры для реали- зации графических элементов пользовательского интерфейса обращаются к вызо- вам Win32 API. Исходный код VCL.NET Обновление компилятора Delphi for .NET, выпущенное компанией Borland в но- ябре 2002 года, все еще является предварительной версией, однако, изучив содержа- 1051
1052 Глава 25. Обзор Delphi for .NET: язык и RTL щийся в ней исходный код, можно получить достаточно подробное представление о том, как будет выглядеть архитектура, которую компания планирует реализо- вать в финальном продукте. Откройте модуль Borland.VcLControls, и вы будете удив- лены схожестью содержащегося в нем кода с кодом версии Win32. Исходный код фактически идентичен. Различия существуют на уровне классов TObject и TComponent. Я уже немного рассказал об особенностях реализации класса TObject, теперь давайте рассмотрим базовый класс для всех компонентов VCL. Класс TCompo- nent определяется в три этапа: type TComponent = System ComponentModel.Component; TComponentHelper = class helper (TPersistentHelper) for TComponent TComponentSite = cl ass(TObject. Isite. IServiceProvider) Класс TComponent соответствует поддерживаемому в .NET Framework классу System.ComponentModeLComponent. При этом помощник класса TComponentHelper добавляет в этот класс дополнительные методы и свойства, а следующий класс добавляет дополнительные данные, необходимые для работы помощника класса. Ситуация достаточно сложная, и я не хочу вдаваться в детали, так как класс TComponent помечен как экспериментальный, это означает, что в будущих версиях компилятора его устройство может измениться. Вернемся к VCL. Большая часть компонентов уже доступны для использова- ния, поэтому вы уже можете приступать к переносу старого кода на новую плат- форму. Если вы используете версию, выпущенную в ноябре 2002, имейте в виду, что в этой версии все еще не поддерживается работа с потоками данных. Поэтому вы должны добавить код создания компонентов в конструктор формы (в финаль- ной версии Delphi for .NET можно будет обойтись без этого). Чтобы помочь вам разобраться в архитектуре классов VCL, я хочу продемонст- рировать адаптацию для платформы .NET примера Classinfo из главы 3. В примере NetClassInfo используется слегка модифицированный код (в будущих версиях Delphi for .NET такая модификация не потребуется): Application.Initialize: Forml := TForml.Create (Application); Application.Ma inForm := Forml; Код формы, как я уже говорил, обладает дополнительным методом, обращение к которому осуществляется из конструктора. Этот метод используется для ини- циализации элементов управления. Код этого метода достаточно длинен, поэтому здесь я приведу лишь несколько фрагментов: procedure TForml.ImtializeControls; begin // создание всех элементов управления . . . Label3 := TLabel Create(Self); Panel 1 = Tpanel Create(Self); Labell TLabel.Create(Self); Label2 .= TLabel.Create(Self); // настройка свойств и событий формы Left .= 217; Top ;= 109; Caption 'Class Info'; OnCreate : = FormCreate: 1052
Библиотека времени исполнения и VCL 1053 // инициализация элементов управления (здесь указан код только для одного из них) with Labels do begin Parent := Self; Left := 8; Top ; = 8; Width = 56; Height := 13; Caption := 'Class flame'-. end; Остальной код приложения сохраняется фактически без изменений, что может показаться удивительным, так как это достаточно низкоуровневый пример. Я вы- нужден был удалить обращение к функции InstanceSize, так как в рамках архитек- туры .NET компилятор не может определить размер объекта. Кроме того, когда я тестирую базовый класс, я сравниваю его с классом Object, а не с классом TObject. Далее приводится фрагмент кода, в результате выполнения которого на экране появляется окно, представленное на рис. 25.1; procedure TFOrml.ListClassesClick(Sender. TObject); var MyClass; TClass; begin MyClass := ClassArray [Listclasses.Itemindex]: Editinfo.Text := Format ('Name: £s - Size: Kd bytes', [MyClass.ClassName. 0 (MyClass.InstanceSize}]); with ListParent.Items do begin Clear. while MyClass.ClassName <> 'Object' do begin MyClass ;= MyClass.ClassParent: Add (MyClass.ClassName); end: end; end; ^Reflection in Delphi for .MT aCCRReflection. exe:Text: Mode (Field) |CLRReflection.exe:Texf. Flags (Field) *•! 3CLRReflection.exe:Text: Factory (Field) UcLRReflection.exeiText: Reader (Field) ?jCLRRefTectfon.exe:Texc.’ writer (Field) |CLRReflection.exe:Text: Filename (Field) |CLRReflection.exe:Text: GetHashCode (Method) aciRReflection.exe;Text: Equals (Method) iCLRRef1ection.exe:Text: Tostring (Method) aCLRReflection.exe:Text: GetType (Method) iCLRReflection.exeiText: .ctor (constructor) I aCLRRei’lection.exe.'Texc.* «месатехг (NestedType) aCLRReflection.exe:«TC1ass: ClassParent (Method) |CLRReflection.exe:«TClass: GetHashcode (Method) aCLRReflection.exesOTClass: Equals (Method) > ICLRReflect-ion.exeiOTClass: Tostring (Method) 5 aCLRReflection.exesBTCiass: GetType (Method) HCLRReflection.exe:«TClass: .ctor (Constructor) SCLRReTlectfon.exe:<TClass; .ctor (constructor) aCLRReflection.exe:«TClass: .ctor (constructor) IjCLRRefiection.exe:iTextDeviceFactory: close (Method) »CLRReflection.exe:iTextoeviceFactory: Open (Method) aCLRReflection.exe:ReflectionForm: set_AutoscaleBaseSize (Method) aCLRRefiection.exe:ReTlectionForm: get_AutoscaleBas eSize (Method) IjCLRRefl ecti on. exetReTl ectionForm*. $et_ActiveControl (Method) HCLRRe fl ecti on. exe :Re fl ecti onForrn: get_actf decontrol (Method) . » ICLRReflecti on.exe:ReTlecti onForm: set_>utoscroi i (Method) Jsf -----------.-------.-------...............____________________________________________all Рис. 25.1. Программа NetClassInfo отображает базовые классы для заданного компонента 1053
1054 Глава 25. Обзор Delphi for .NET: язык и RTL Дополнительные примеры использования VCL В качестве стартовой точки для ваших собственных экспериментов с VCL в среде .NET вы можете использовать еще два разработанных мною примера. Программа NetEuroConv является адаптацией программы EuroConv из главы 3. Эта программа основана на использовании механизма конвертации, встроенного в RTL. Программа NetLibSpeed является адаптацией примера LibSpeed из главы 5, который сравнивает скорость создания визуальных компонентов VCL и VisualCLX. Используемая вами версия библиотеки VCL.NET является предварительной, поэтому не следует рас- сматривать результаты измерений как окончательную оценку производительнос- ти, и все же тот факт, что задача, связанная с созданием компонентов библиотеки VCL.NET, выполняется в четыре-пять раз медленнее, чем при использовании обыч- ной библиотеки VCL, может показаться настораживающим. Как я уже сказал, эти примеры следует рассматривать лишь в качестве отправ- ной точки для ваших собственных экспериментов. Следует учитывать, что в сле- дующих версиях Delphi for .NET Preview обе эти программы могут оказаться нера- ботоспособными. ПРИМЕЧАНИЕ--------------------------------------------------- На моем веб-узле вы можете обнаружить обновления этого раздела книги, а также связанных с ним примеров. Использование библиотек Microsoft Разработка библиотеки VCL.NET все еще не завершена, однако в качестве базиса для экспериментирования с компилятором Delphi for .NET вы можете использо- вать библиотеку классов .NET Framework. Например, для освоения работы Delphi for .NET было бы полезно компилировать программы с применением этого компи- лятора, а затем изучать их внутреннее строение с помощью ILDASM (Intermediate Language Disassembler). Именно этим мы займемся в данном разделе книги. Если вы хотите изучить менее сложный пример с использованием поддержки XML, об- ратитесь к программе XmlDemo, которая упоминалась ранее в данной главе. Программа CLRReflection открывает агрегат и затем использует механизм Re- flection (отражение) для изучения модулей и типов, определенных внутри этого агрегата. В программе демонстрируется использование стандартного диалогового окна (OpenFileDialog), конструирование меню, обработка событий, использование динамических массивов Delphi и, конечно же, механизм Reflection. Вначале взгля- нем на код проекта: program CLRReflection: uses System.Windows.Forms. ReflectionUnit: var reflectFora : ReflectionForm; begin 1054
Использование библиотек Microsoft 1055 reflectForm ReflectionForm.Create: System.Wi ndows.Forms.Applicati on.Run(refl ectForm): end. Код выглядит фактически так же, как старое доброе приложение VCL. Вы определяете переменную для главной формы, а затем создаете форму. После этого вы используете метод Run поддерживаемого в .NET Framework класса System. Windows.Forms.Application. В этом код аналогичен (по крайней мере, концептуаль- но) коду VCL. Обратите внимание, что в данном примере я указываю полное имя каждого из классов .NET Framework. Я делаю это специально для того, чтобы показать вам, где именно расположен тот или иной класс. Однако в выражении uses упоминает- ся пространство имен System.Windows.Forms, благодаря чему обращение к методу Run можно сократить. То есть вместо System.Windows.Forms.Appl1cat1 on.Run(refl ectForm): вы можете написать Appl1cat1on.Run(refl ectForm); Теперь посмотрите на листинг 25.1, где показан код модуля, в котором опреде- ляется главная форма. Имейте в виду, что этот код компилируется с использова- нием обновления Delphi for .NET, выпущенного в ноябре 2002 года, но не компи- лируется с использованием версии, изначально поставляемой вместе с Delphi 7. Листинг 25.1. Модуль Reflectionunit примера CLRReflection unit Reflectionllnit; interface uses System.Wi ndows.Forms. System.Reflection. System.Drawl ng. Borland.Del pip.SysUtl1s; type ReflectlonForm = class(System.Windows.Form.Form) private ma1nMenu: System.Wi ndows.Forms.Ma 1 nMenu; fi1 eMenu: System.Windows.Forms.MenuItern: separatoritem: System.Windows.Forms.Menuitem; openItem: System.Windows.Forms.MenuItem; exitItem: System.Windows.Forms.Menuitem: showFileLabel: System.Windows.Forms.Label; typesL1stBox: System.Windows.Forms.ListBox: openF11eDIa1og: Systern.Windows.Forms.OpenFI1eDIa1og; protected procedure InitializeMenu: procedure InitializeControls; procedure PopulateTypes(fl 1 eName: String): { Обработчики Событий } procedure exitltemClick(sender: TObject; Args: System.EventArgs); procedure openltemClick(sender: TObject; Args: System.EventArgs); продолжение 1055
1056 Глава 25. Обзор Delphi for .NET: язык и RTL Листинг 25.1 (продолжение) public constructor Create; end; implementation constructor Reflect1onForm.Create: begin Inherited Create; SuspendLayout; InitializeMenu; InitializeControls: { Инициализация формы и других переменных-членов } OpenFileDialog System.Windows.Forms.OpenFileDialog.Create; OpenFileDialog.Filter 'Assemblies (*.dll;*exe)|*.dll;*,exe': OpenFileDialog.Title 'Open an assembly’: AutoSealeBaseSize := System.Drawing.Size.Createi5. 13): CUentSize System.Drawing.Size.Create(631. 357); Menu := mainMenu: Name ;- 'reflect)onForm'; Text 'Reflection in Delphi for .NET'; { Добавляем элементы управления в коллекцию формы. } Controls.Add(showFi1 eLabel); Controls.Add(typesListBox); ResumeLayout; and: ( Формируем главное меню } procedure Reflect)onForm.InitializeMenu; /ar menuItemArray ; array of System.Windows.Forms.MenuItem: jegin mainMenu System.Windows.Forms.MainMenu.Create; fileMenu : = System.Windows.Forms.Menuitem.Create; openitem System.Windows.Forms.Menuitem.Create; separatoritem : = System.Windows.Forms.Menuitem.Create: exititem := System.Windows.Forms.Menuitem.Create: { инициализация mainMenu } ma i nMenu.Menu Iterns.Add(fi1 eMenu); { инициализация fileMenu ) fileMenu.Index := 0; SetLength(menuItemArray. 3); menuItemArray[O] := openitem: menuItemArraytl] := separatoritem; menuItemArray[2] := exititem; fi1 eMenu.MenuItems.AddRange(menuItemArray); fileMenu.Text ’&File': // openitem openitem.Item.Index := 0: 1056
Использование библиотек Microsoft 1057 openitem.Text '&Open...': openItern.add_Cl1ck(openItemClick): // separatoritem separatoritem.Index 1: separatoritem.Text // exititem exititem.Index :- 2: exititem.Text :- ’E&xit'; exitItem.add_Click(exitItemClick); end; { Создание элементов управления и заполнение формы } procedure Ref1ecti on.Ini ti ali zeControl s: begin { инициализация showFileLabel } showFi1 eLabel :- System.Windows.Forms.Label.Create; showFileLabel.Location System.Drawing.Point.Create(5. 6); showFileLabel.Name :- 'showFileLabel': showFileLabel.Size :- System.Drawing.Size.Create(616. 37): showFileLabel.Tabindex :- 0; showFileLabel.Anchor :- System.Windows.Forms.AnchorStyles.Top or System.Wi ndows.Forms.AnchorSty1es.Left or System.Wi ndows.Forms.AnchorStyles.Ri ght showFileLabel.Text :- 'Showing types in: '; { инициализация typesListBox } typesListBox :- System.Windows.Forms.ListBox.Create: typesListBox.Anchor :- System.Windows.Forms.AnchorStyles.Top or System.Wi ndows.Forms.AnchorStyl es.Bottom or System.Wi ndows.Forms.AnchorStyles.Left or System.Wi ndows.Forms.AnchorStyles.Ri ght: typesListBox.Location :- System.Drawing.Point.Create(8. 46); typesListBox.Name :- ’typesListBox'; typesListBox.Size :- System.Drawing.Size.Create(610. 303): typesListBox.Font :- System.Drawing.Font.Create!’Lucida Console'. 8.25. System. Drawing. FontSty 1 e. Regul ar, System. Drawing.Graphicsllnit. Poi nt. 0): typesListBox.Tabindex :- 1: end: { Обработчик события для пункта Exit меню } procedure ReflectionForm.exitItemClick(sender: TObject: Args: System.EventArgs); begin System.Wi ndows.Forms.Appii cati on.Exi t; end: { Обработчик события для пункта меню Open ) procedure ReflectionForm.openItemClick(sender; TObject; Args: System,EventArgs); begin if OpenFileDialog.ShowDialog = DialogResult.OK then begin showFileLabel.Text :- 'Showing types in: ' + OpenFileDialog.FileName; Popul ateTypes(openFi1eDi a1og.Fi1 eName); end: end; л продолжение & 1057
1058 Глава 25. Обзор Delphi for .NET: язык и RTL Листинг 25.1 (продолжение) { открываем заданный агрегат и отображаем входящие в него модули и типы } procedure ReflectionForm PopulateTypesffileName String) var assy System Reflection Assembly. modules array of System Reflection Module. module System Reflection Module types array of System Type t System Type members array of System Reflection Memberinfo; m System Reflection Memberinfo. i. j. k Integer, s String begin try { Очищаем ListBox } typesListBox BeginUpdate. typesListBox Items Clear. { Загружаем агрегат и извлекаем его модули } assy = System Reflection Assembly LoadForm(fileName). modules = assy GetModules { Для каждого модуля получаем все типы } for 1 =0 to High(modules) do begin module = modules[i], types = module GetTypes { Для каждого типа получаем все его члены } for j =0 to High(types) do begin t = types[j] members - t GetMembers { Для каждого члена получаем информацию о типах и добавляем ее в ListBox } for к =0 to High(members) do begin m = members[k] s - module Name + ' ' + t Name + ’ ' + m Name + ’(’ + m MemberType ToString + ’) ’, typesListBox Items Add(s). end, end end typesListBox EndUpdate. except System Windows Forms MessageBox.Show('Could not load the assembly '). end. end end. В начале модуля объявляется, что он зависит от файлов dcuil инфраструктуры .NET Framework, а также от модуля Borland.Delphi.SysUtils. После этого сразу же следует определение класса для главной формы. Класс главной формы является 1058
Использование библиотек Microsoft 1059 наследником поддерживаемого в рамках .NET Framework класса System.Windows. Forms.Form. Внутреннее строение класса формы должно показаться вам знакомым: каждому элементу управления соответствует переменная-член. В качестве типов этих переменных используются типы из библиотеки классов .NET Framework. Функции exitltemClick и openltemClick — обработчики событий. Сигнатура этих методов определяется инфраструктурой CLR. Любой обработчик события явля- ется процедурой с двумя параметрами: первый параметр — это объект, ставший источником события (потомок класса System .Object), второй параметр — это аргу- менты события, которые размещаются в объекте класса System.EventArgs (или про- изводного класса). О подключении этих обработчиков событий будет рассказано в самом ближайшем будущем. Теперь перейдем к рассмотрению конструктора класса. Я хочу обратить ваше внимание на самое первое выражение в конструкторе, это выражение выглядит следующим образом: inherited Create. ВНИМАНИЕ--------------------------------------:----------------------------- Здесь проявляется важное отличие .NET Framework от Delphi VCL. В Delphi конструктор инициализи- рует переменные-члены, переводя тем самым экземпляр объекта в заведомо корректное состояние. Однако конструктор не выполняет каких-либо выделений памяти. Поэтому часто приходится ви- деть, что конструктор выполняет все необходимые присвоения, а затем обращается к конструктору базового класса. В некоторых случаях вы можете вообще не обращаться к конструктору базового класса, и эта ситуация является вполне допустимой. Однако в Delphi for .NET такой подход является неприемлемым. В вашем собственном конструкторе вы обязаны обратиться к конструктору базово- го класса (inherited Create), причем обращение к этому конструктору должно быть самым первым исполняемым выражением вашего конструктора. В настоящее время, если вы не обеспечиваете подобного обращения, компилятор выдает сообщение об ошибке, в котором указывается, что ссыл- ка Self не инициализирована и что перед обращением к любым полям предка необходимо обратить- ся к конструктору базового класса. После обращения к конструктору класса-предка вы попадаете в знакомую для вас обстановку. Несмотря на то что приведенный код использует другую иерар- хию классов, он должен быть понятен любому программисту Delphi. Чтобы со- здать экземпляр объекта System. Windows.Forms.OpenFileDialog, необходимо обратиться к конструктору Create — именно так осуществляется создание экземпляра любого класса .NET Framework. Следующие несколько строк демонстрируют настройку свойств объекта Ореп- FileDialog и самой формы. Наконец, в коллекцию Controls формы выполняется добавление двух элементов управления (метка для имени файла и ListBox, в кото- ром будет содержаться агрегат). Эта коллекция является свойством типа Control. Controlcollection. Процедура InitializeMenu демонстрирует выделение и настройку экземпляра объекта. В момент инициализации меню File в динамический массив заносится каждый из пунктов меню. После этого динамический массив передается методу AddRange. Этот же результат можно получить, если обратиться к методу Add от- дельно для каждого из пунктов меню. Еще одним интересным аспектом процедуры InitializeMenu является подключе- ние обработчиков событий для пунктов меню. В главе 24 и ранее в данной главе я уже рассказывал о различных сложностях, связанных с делегатами и события- ми, для которых назначены несколько обработчиков. В объявлении события ука- 1059
1060 Глава 25. Обзор Delphi for .NET: язык и RTL >ывается делегат, который используется в качестве механизма обратного вызова. Событие является производным от System.MulticastDelegate (в данном случае деле- •ат System.EventHandler), поэтому другие объекты могут добавлять и удалять обра- ботчики события, и эти обработчики будут выполняться в момент генерации со- бытий. В языке C# для этой цели существуют удобные синтаксические конструкции, щлающие код более понятным. Говоря точнее, в C# для добавления и удаления гбработчиков событий используются операторы += и -= соответственно. Со вре- менем в Delphi также появится удобный синтаксис, основанный на использова- ши механизма Include/Exclude, о котором уже говорилось ранее. Система типов 3TS предписывает, чтобы все компиляторы .NET, использующие эту модель со- бытий, генерировали методы с именами add_<Co6biTMe> и гетоуе_<Событие>. Эти методы add_ и remove_ являются оболочками для методов Combine и Remove, объяв- генных в System.Delegate. На текущий момент для того, чтобы назначить обработчик события, вы долж- 1Ы воспользоваться методами add_ и remove_. Однако в будущем вы сможете на- щачать обработчики без использования этих методов — компилятор возьмет эту >бязанность на себя. В существующем объявлении класса присутствуют объявле- шя двух методов, сигнатуры которых совпадают с делегатом System.EventHandler: то методы openltemClick и exitltemClick. После этого вы обращаетесь к методу dd_Click для соответствующего пункта меню, передавая ваш обработчик события । качестве метода обратного вызова. Теперь, когда мы рассмотрели код начальной инициализации, давайте перей- (ем к рассмотрению кода, отображающего типы, определенные внутри агрегата. 5ы можете загрузить любой агрегат (создав тем самым экземпляр объекта), обла- гая именем файла этого агрегата. Для этого применяется метод LoadFrom. Получив । свое распоряжение объект агрегата, вы можете воспользоваться механизмом Reflection для анализа внутренностей этого агрегата. ASP.NET with Delphi Delphi for ЛЕТ Preview mede thu Delphi for NET Preview made ths Delphi for .NET Preview made this Delplu for NET Preview made tins Delphi for .NET Preview made this Delphi for NET Preview made this Рис. 25.2. Программа CLRReflection, в которой загружен один из агрегатов Коллекция модулей, содержащихся внутри агрегата, доступна при помощи ме- ода GetModules. Чтобы получить типы, определенные внутри модуля, воспольэуй- есь методом GetTypes. Как вы уже видели в процедуре InitializeMenu, вы можете 1060
Использование ASP.NET в языке Delphi 1061 воспользоваться динамическими массивами для свойств, которые обеспечивают доступ к коллекциям. Для этого используется тип System.Array. Наконец, каждый отдельный элемент массивов модулей и типов обладает свой- ством Name, при помощи которого можно построить строку для отображения в со- ставе ListBox. Результат выполнения кода показан на рис. 25.2. Использование ASP.NET в языке Delphi Нравится вам или нет (лично я от этого не в восторге), но технология Microsoft ASP играет важную роль в разработке веб-приложений, по крайней мере, на плат- форме Windows. С появлением новой инкарнации ASP.NET эта технология стала полностью совместимой с инфраструктурой .NET Framework. Теперь, когда по- явился компилятор Delphi for .NET, язык Delphi может стать языком, на котором вы сможете разрабатывать приложения ASP. Чтобы создать тестовое приложение, настройте IIS для поддержки ASP.NET (я не буду описывать здесь эту процедуру, так как она не соответствует тематике данной книги, более подробную информацию можно обнаружить по адресу www.asp. net), после этого скопируйте в целевой каталог файл web.config, входящий в комп- лект поставки компилятора Delphi for .NET Preview (этот файл содержится в под- каталоге aspx). Этот конфигурационный файл определяет соответствие языка и специальной библиотеки, которая также поставляется компанией Borland. Этот файл обладает форматом XML. Вот его основные записи: «compilation debug="true"> <assemblies> «add assembly="DelphiProvider" /> </assemblies> <compilers> compiler language="Delphi" extent!on=".pas" type="Borland.Delphi.DelphiCodeProvider.DelphiProvider" /> </compilers> </compilation> Чтобы протестировать корректность конфигурации, лучше всего написать те- стовую программу. Создайте новый файл (я назвал свой файл aspbase.aspx, он располагается в папке Delphi Asp исходного кода главы) и введите в него следу- ющий код: <html> «body> «hl>ASP.NET with Delphi«/hl> «script language“"Delphi" runat="server"> procedure HelloMEssage(msg: string); var i: Integer; begin for i := 2 to 7 do Response.Write (’«font size=’ + inttostr (i) + + msg + ’«/font> «br>’) 1061
1 1062 Глава 25. Обзор Delphi for .NET: язык и RTL end, </script> <% HelloMessageC'Delphi for .NET Preview made this'): %> </body> </html> В процессе обработки этот файл преобразуется в исходный код .NET, затем ком- пилируется с использованием компилятора Delphi for .NET Preview, а после этого полученный код IL компилируется в ассемблерный код (следует помнить, что в среде .NET даже сценарии перед выполнением компилируются). Если все прошло хорошо, браузер должен отобразить страницу, показанную на рис. 25.3. ASP.NET with Delphi test Рис. 25.3. Программа aspbase.aspx отображает динамическую страничку в окне браузера Теперь вы можете воспользоваться другими возможностями приложения ASP . NET. Напоследок я хотел бы продемонстрировать совместное использование эле- ментов управления и обработчиков событий. Ранее я уже рассказывал вам, каким образом осуществляется настройка обработчиков событий в приложениях .NET, основанных на формах Windows. Теперь давайте посмотрим, как это осуществля- ется в сценариях ASP. Рассматриваемый далее пример сохранен в файле aspui.aspx в папке AspDelphi. Этот пример использует HTML, чтобы определить форму с графой текстового ввода (элемент управления Edit) и кнопкой, а также меткой, в которой выводится вве- денный текст. Для кнопки определяется обработчик события Delphi, который пе- ремещает текст, введенный пользователем, в текст метки (вывод программы пока- зан на рис. 25.4): <html> <body> <hl>ASP.NET with Delphi</hl> «script language="Delphi" runat=’'server"> procedure ButtonClick(Sender: System.Object; E: EventArgs): begin 1062
Что далее? 1063 Message.Text :« Editl.Text: end; </script> <form runat="server"> <asp-textbox id-’Editl” runat="server"/> <asp-.bu.tton tex.t="Click Me1.” QnClick="ButtonCllck" runat="server"/> </form> <pxb><asp;label id="Message" runat=”server" text="message"/></bx/p> </body> </html> *?.'й NetCUssInfa ClmiNzme TButton TBitBtn TEdit TPopupMenu T RadioButton TRadoGroup ТРзпе! fCheckbox TForm T ComboBox TGroupBox TSpeedButton TLabel TCustomPanel TCustomControl TWinControl TControl Component MarshdByRefObject Object |Name' TPainel -Size 0 bytes BateCtatm Рис. 25.4. Вывод примера aspui.aspx после того, как пользователь ввел в окно редактирования текст и щелкнул на кнопке На этом я завершаю весьма краткое знакомство читателей с технологией ASP .NET на базе языка Delphi. Надеюсь, что, ознакомившись с данным разделом, вы получили представление о возможностях, которые открываются перед програм- мистами Delphi в новом мире .NET. Что далее? В ожидании выхода в свет окончательной версии продукта Delphi for .NET (в на- стоящее время этот продукт носит кодовое название Galileo) вы можете присту- пить к экспериментам с демонстрационной версией этого компилятора под назва- нием Delphi for .NET Preview, которая поставляется в комплекте Delphi 7 (а также в виде последующих обновлений, которые можно получить от компании Borland). Конечно же, вы должны внимательно следить за новостями, публикуемыми на веб- узле разработчиков Borland по адресу bdn.borland.com. Кроме того, я рекомендую вам следить за группами новостей, а также за содержимым веб-узла автора данной книги. Компания Borland всегда старалась обеспечить разработчиков лучшими сред- ствами разработки программного обеспечения, и я надеюсь, что данная книга по- может вам в совершенстве овладеть средой разработки Delphi — наиболее успеш- ным инструментом, выпущенным на рынок компанией Borland за последние 1063
1064 Глава 25. Обзор Delphi for .NET: язык и RTL несколько лет. Не забывайте время от времени просматривать мой собственный веб-узел по адресу www.marcocantu.com. На веб-узле можно обнаружить большой объем материала, который я не смог включить в состав данной книги из-за ее огра- ниченных размеров. Более подробные сведения содержатся в приложении В. В приложениях А и Б описываются некоторые добавления к Delphi, которые можно загрузить с моего веб-узла, кроме того, там рассматриваются некоторые другие инструменты, связанные с Delphi и распространяемые бесплатно. Также на моем веб-узле можно найти обновленный и дополненный материал для данной книги. Кроме того, не стесняйтесь использовать размещенные на нем группы но- востей для того, чтобы задавать вопросы, связанные с данной книгой и со средой разработки Delphi. 1064
Приложение А Дополнительные инструменты для Delphi, разработанные автором За последние несколько лет я разработал несколько небольших компонентов и дополнений для Delphi. Некоторые из них были написаны для книг или стали результатом расширений примеров, рассматриваемых в книгах. Другие были раз- работаны для упрощения выполнения повторяющихся операций. Все эти инстру- менты можно получить бесплатно, а к некоторым из них прилагается исходный код. В данном приложении содержится перечень всех этих дополнений. Поддерж- ка этих программных средств осуществляется через группы новостей на моем веб- узле (www.marcocantu.com). Мастеры CanTools Это набор мастеров, которые можно установить в Delphi. Каждый из этих масте- ров появляется либо в виде дополнительного пункта в главном меню, либо в виде подменю в разделе Tools. Эти мастеры никак не связаны друг с другом (их можно бесплатно получить по адресу www.marcocantu.com/cantoolsw). Вот их перечень: О List Template Wizard (мастер создания списков) упрощает разработку схо- жих между собой классов, основанных на списках, в каждом списке хранятся объекты определенного типа. Об этом мастере рассказывалось в главе 4. Мас- тер выполняет операцию поиска и замены в отношении базового файла с ис- ходным кодом, поэтому его можно использовать каждый раз, когда вы хотите заменить повторяющийся код и имя (или другой элемент) класса. О OOP Form Wizard (мастер форм ООП) — о нем тоже упоминалось в главе 4. Этот мастер позволяет вам скрыть опубликованные компоненты формы, делая тем самым форму более объектно-ориентированной и обеспечивая более каче- ственный механизм инкапсуляции. Сделайте форму активной и запустите этот мастер, в результате будет автоматически создан обработчик события OnCreate. После этого вы должны вручную переместить часть кода в раздел инициализа- ции модуля. 1065
Юбб Приложение А О Object Inspector Font Wizard (мастер шрифтов для Object Inspector) позво- ляет вам изменить шрифт, используемый в окне Object Inspector (это может оказаться полезным для презентационных целей, так как шрифт инспектора объектов слишком мелкий, поэтому его плохо видно на проекционном экране). Кроме того, вы можете воспользоваться внутренней возможностью Object Inspector для того, чтобы отобразить имена шрифтов (ниспадающий список для соответствующего свойства) с использованием соответствующего шрифта для каждой из позиций этого списка. О Rebuild Wizard (мастер повторной компоновки) позволяет вам заново ском- поновать все проекты Delphi, расположенные в указанном подкаталоге, при этом каждый из них последовательно загружается в IDE. Используя этот мастер, вы можете выбрать группу проектов (например, прилагаемых к данной книге) и от- крыть один из них, выбрав его имя в списке. Вы можете автоматически отком- пилировать заданный проект или инициировать сборку сразу нескольких про- ектов (эта операция выполняется достаточно медленно). В диалоговом окне результатов компиляции вы можете щелкнуть на кнопке, чтобы продолжить компиляцию проекта только в случае, если соответствующий параметр среды установлен. Если выбранный вами параметр среды не установлен, вы не увиди- те ошибок компилятора, так как сообщения компилятора подавляются в про- цессе каждой компиляции. Вот как выглядит список проектов этого мастера: е VxMkslml7codett4Ch8ngeOwnerChangeC е tMX»k$lrnd7code)D4'Cortain'Cortain dpr е t»oksVnd7codelD4K)ateCo(npt>atecoiTip dpt е tx)oksVnd7code\04K)ateEvt,iDeieEvt dpr e 'booksVnd7code\041DateList'iDeieList dpr e toooksVnd7code\04£ncDemo£ncDemo dpr e 4x)oksind7code\041formToText'FormToTex± e t»oks)md7code^41HideCompWdeComp dpr e Vx»ks)md7code\04M_istDemoM_istdemo dpr e tx)oks)md7code\04V?unPropV?unProp dpr e t»oks)md7code\04\ZCompress\ZCompress O Clip History Viewer (мастер просмотра истории буфера обмена) — этот мас- тер хранит список текстовых фрагментов, которые вы в недавнем прошлом ко- пировали в буфер обмена. Элемент Мето в окне этого мастера показывает пос- ледние 100 строк, скопированных в буфер обмена. Вы можете редактировать любую из этих строк. Во время, пока работает Delphi, мастер принимает текст, скопированный в буфер обмена из других программ (естественно, копируется только текст). Когда я использовал этот мастер, я несколько раз сталкивался с ошибками функционирования буфера обмена. О VCL Hierarchy Wizard (мастер иерархии VCL) отображает (почти) полную иерархию классов VCL, включая установленные вами компоненты сторонних производителей. Позволяет вам выполнить поиск некоторого класса и полу- чить детальную информацию об этом классе (базовый класс, подклассы, опуб- ликованные свойства и т. п.). При щелчке на кнопке генерируется список и древовидная иерархия (сначала одно, потом другое, поэтому индикатор за- вершенности пробегает слева направо дважды). Список классов генерируется 1066
Программа преобразования VcIToClx 1067 при помощи заранее предопределенных основных классов (некоторые из них до сих пор отсутствуют, высылайте ваши предложения), после чего в список добавляются компоненты из установленных пакетов (пакеты Delphi, ваши па- кеты, пакеты сторонних производителей и проч.), а также классы всех опуб- ликованных свойств, обладающих классовым типом. Вместе с тем классы, ис- пользуемые только как публичные свойства, не включены в список. Вот как выглядит окно этого мастера: 7- VCt Hierarchy Овсяяк сиачш | H TControl TVrfoControl TTooNMndow TAnmate TButtonControl TCustomControl TCteControl TCustomLlstControl TCommooCalendar TCustomEdt 41 th P TCustomLabeledEdt TCustomMaskEdt TCustomMemo TCustomRichEdit TDBRichEdt TRchEdt TDBMemo IIIIIIIIIIIIII1I TMemo Size 560 bytes «»» Parent classes TCustomMemo TCustomEdt TVMnControl TControl TComponent TPersistent TObject ==» Typeinfo «» Type Name TMemo Type Kind- tkClass Defined n. StdCtrlspas Properties (67) Align. TARgn Alignment T Alignment Anchors TAnchors BevelEdges TBeveEdges BeveRnner TBevelCut BeveKnd. TBeveKind О Extended Database Forms Wizard (расширенный мастер форм баз данных) обладает более широким набором возможностей по сравнению с мастером Data- base Forms Wizard, поддерживаемым в рамках Delphi IDE. Позволяет вам вы- бирать поля, которые вы хотели бы разместить на форме, а также использовать набор данных, не основанный на BDE. О Multiline Pallette Manager (диспетчер многострочной палитры) позволяет превратить палитру компонентов Delphi в элемент управления TabControl с несколькими рядами вкладок: / Delphi f - Ptoferlt j н» ek twxh «и iw в» «ири«* ииы» I * ЁЙ{»| 4» : Мимбини! Иипй! АййсГш» I .. . ~ i cat» I I IWM* f wa*w> | I s»»» I |ГУ«Г1ПШ i > • fl *'’i { {unoKttfci at»»». I ми»»! юс isos Программа преобразования VcIToClx Этот инструмент можно использовать для преобразования проектов Delphi из основанных на VCL в основанные на CLX (и наоборот, если вы выполните соответствующую конфигурацию). Программа позволяет выполнять преобра- зование всех файлов в заданном каталоге и его подкаталогах. Вот пример вы- вода программы: 1067
1068 Приложение А .<•1 Е \book»Vrid7code\08\VfiWfi.dpr |Це \book$Vnd7code\08\PoiFonn\PoiForm.ctx Же \boofcs'jrid7code^08\FrarT>es2\Ftames2ct)f |||E:\bookt^nxf7code\08\FrarneTab\F(ameTab.dcM OE:\booksVnd7codeM)8\FtamePafl\FfamePagcix WE:\book$W7code\08\Sy^rfenu2\Systnenu2.4x Щ E:\bookt\md7code\08\S creen\S creen фг ^E'^booksVrid7code\06\Fo(fnlntf\Forrnlntf dpt Ж E:\booksVnd7code\08\M diH iA\MdW uh. dpt SE-\book$^nxi7code\08\MdCemo4Mcfclemo фг ?|e \Ьоок$^7соФ\08\5ЬомАрр\5ЬсичАс)рф( Ойл QFomrt QConbok Qt QGraphcs QGnds QlmgLet QMeck QMenut QDuiogs QStdCtri* QExtCtrl* Form» Control* Wndows Giaphc* Gnds ImgUst Mask Menu* Dialog* StdCtrh ExtCbb Исходный код программы располагается в каталоге Tools комплекта исходного юда, прилагаемого к данной книге. Программа VclToClx выполняет преобразование гмен модулей (на основании содержимого конфигурационного файла) и решает гроблему DFM путем переименования файлов DFM в файлы XFM и исправляя сылки, содержащиеся в исходном коде. Программа не предусматривает какой- шбо сложной синтаксической обработки, вместо этого она пытается обнаружить гмена модулей, за которыми следует запятая или точка с запятой, как это происхо- ,ит в выражениях uses. Требуется также, чтобы перед именем модуля располагал- я символ пробела, однако вы можете модифицировать код программы, чтобы на- яду с пробелом она воспринимала и символ запятой. Не следует пренебрегать той дополнительной проверкой, иначе имя модуля Forms будет преобразовано QForms, однако имя QForms в свою очередь будет преобразовано в имя QQForms! Object Debugger (отладчик объектов) la этапе проектирования вы можете использовать Object Inspector для настройки войств компонентов и форм. В Delphi 4 компания Borland добавила инструмент >ebug Inspector, который обладает аналогичным интерфейсом и отображает ана- огичную информацию, однако делает это на этапе исполнения программы. Еще о того, как компания Borland добавила в среду этот механизм, я реализовал клон нспектора объектов, предназначенный для использования на этапе исполнения, э есть для целей отладки программ. Вот окно этого инструмента (см. рисунок на тедующей странице). Инструмент Object Debugger поддерживает чтение-запись любых опублико- гнных свойств компонента и обладает двумя ниспадающими списками, которые эзволяют вам выбрать форму и компонент, расположенный на этой форме. Для гкоторых свойств поддерживаются специальные редакторы. 1068
Memory Snap (снимок памяти) 1069 ' porm! (TForml j > |lislBox1 TListBox Paid , Daetfafe t EttubM ' IMnMBM: ' .«fwl ' • > Ch««i - :" <№»''' '. :’?W* , ' ; ‘ нал* '' Pfch” ' : Si» ' ’ ' Sfste _— •fcX'X М*Н<М'Ж-К*К'М TtmesNewRonmn JpOrifc* > . * ’ i- d Вы можете разместить компонент Object Debugger на главной форме програм- мы (или вы можете создать этот компонент динамически в процессе исполнения программы): он появится в своем собственном окне. Конечно же, существует про- странство для улучшений, однако даже в его теперешней форме данный инстру- мент удобен, полезен и имеет множество поклонников. Исходный код этого компонента располагается в каталоге Tools комплекта ис- ходного кода данной книги. Memory Snap (снимок памяти) Существует множество средств, предназначенных для наблюдения за состоянием памяти приложения Delphi. В процессе разработки одного из проектов я был вы- нужден самостоятельно написать подобный инструмент. Позднее я сделал его до- ступным для широкой общественности. Я написал свой собственный диспетчер памяти, который подключается к дис- петчеру памяти Delphi, используемому по умолчанию. Написанный мною диспет- чер следит за выделением и освобождением памяти. Помимо того что он отобра- жает общее значение (сейчас Delphi также делает это), он может также сохранить детальное описание состояния памяти в файл. Утилита Memory Snap хранит в памяти список выделенных блоков (вплоть до заданного общего числа, которое можно изменить), благодаря этому она может записать содержимое кучи (heap) в файл с низкоуровневым описанием каждого из использованных блоков. Этот список генерируется путем анализа каждого блока памяти с использованием эмпирических методик, изучить которые можно, взгля- нув в исходный код (к сожалению, их не так-то просто понять). Вывод сохраняется в файле, так как при этом дополнительного выделения памяти не требуется, благо- даря чему состояние памяти не меняется. Вот фрагмент одного из таких файлов: 157) 00С035СС: object: [TList - 16] 158) 00С035Е0: buffer with heap pointer [00C032B0] 159) 00C03730. string: [5-1]: Editl 1069
L070 Приложение А 60) 00С03744: object: [TEdit - 544] 61) 00С03968: object: [TFont - 36] 62) 00С03990: object: [TSIzeConstralnts - 32] 63) 00С039В4: object: [TBrush - 24] 64) 00C039F4: buffer with heap pointer [00C01FE4] 65) 00С03В34: buffer with heap pointer [00C01F18] .66) 00С03В48: string: [0-0]: dD .67) 00С03В58: string: [11-2]: c:\mman.log Программу можно расширить, добавив в нее исследование использования па- мяти с распределением по типам (строки, объекты, другие блоки), возможность :лежения за неосвобожденными блоками, а также возможность управления рас- 1ределением памяти. Исходный код этого компонента содержится в каталоге Tools комплекта исход- юго кода для данной книги. Лицензирование и модификация <ак вы можете убедиться, исходный код некоторых из этих инструментов досту- 1ен для широкой общественности. Распространение этих инструментов осуществ- тяется в соответствии с лицензией LGPL (Lesser General Public License, www.gnu. )rg/copyleft/lesser.html), это означает, что вы можете бесплатно использовать и рас- гространять эти инструменты, а также вносить в них любые модификации при условии, что изначальные права на копирование сохраняются за автором. Лицен- 1ия LGPL запрещает вам закрывать исходный код ваших расширений, однако вы можете использовать код этих библиотек при разработке собственных программ, )аспространяемых на коммерческой основе и, возможно, без открытия исходного сода этих программ. Если вы решили исправить какой-либо из этих продуктов 1ли добавить в него новую возможность, я прошу вас сообщить об этом мне и пе- >едать исправленный вами код. Благодаря этому я смогу внести необходимые из- менения в оригинальную версию. Имейте, однако, в виду, что в рамках лицензии -GPL вы не обязаны сообщать мне о каких-либо изменениях, вносимых вами в мои фодукты. 1070
Приложение Б Дополнительные инструменты для Delphi из других источников В настоящее время на рынке представлено огромное количество дополнительных компонентов и инструментов для Delphi. Это могут быть несложные свободно рас- пространяемые утилиты, разработанные одним человеком, или достаточно круп- ные проекты с открытым исходным кодом, уловно-бесплатные программы или высокопрофессиональные компоненты. В данном приложении содержится спи- сок некоторых проектов с открытым исходным кодом, о которых я упоминал в дан- ной книге. Предустановленные компоненты Delphi с открытым исходным кодом В комплект Delphi 7 входит исходный код для двух значительных проектов с от- крытым исходным кодом: О Internet Direct (Indy) — об этом проекте подробно рассказывалось в главе 19. Официальный веб-узел этого проекта располагается по адресу www.nevrona.com/ indy, однако вы можете обратиться также к порталу Indy по адресу www. atozedsoftware.com/indy. Поддержку можно получить в группах новостей ком- пании Borland. О Open XML — это основанная на Delphi реализация механизмов DOM и SAX. Я рассказывал об этом проекте в главе 22. Дополнительную информацию и бо- лее свежие версии можно обнаружить по адресу www.philo.de/xml. Поддержка Open XML осуществляется через списки рассылки, на которые вы можете под- писаться по указанному адресу. В состав Delphi входит также третий проект с открытым исходным кодом: это библиотека сжатия Zlib, о которой рассказывается в главе 4. Я не включил ее в об- щий список, так как она не основана на Delphi. 1071
1072 Приложение Б Другие проекты с открытым исходным кодом Сообщество программистов Delphi с самого начала было чрезвычайно активным. В результате деятельности Delphi-программистов на свет появилось множество инструментов, которые свободно распространялись среди программистов. К неко- торым из этих программных продуктов прилагался открытый исходный код, одна- ко открытый исходный код зачастую доступен также и для коммерческих компо- нентов Delphi. Проект JEDI Проект JEDI (Joint Endeavor of Delphi Innovators), домашняя страница которого располагается по адресу www.delphi-jedi.org, по сути, не является единым проек- том. Скорее это крупное сообщество программистов Delphi, работающих в рамках концепции открытого исходного кода. На веб-узле JEDI можно найти многие про- екты, разрабатываемые этим сообществом, а также ссылки на другие проекты, рас- положенные на других веб-узлах. Проект JEDI начался как попытка транслировать вызовы API для некоторых специфических для Windows библиотек, распространяемых Microsoft и другими компаниями. В результате модули Delphi с объявлениями этих вызовов стали до- ступными для широкого круга разработчиков Delphi. Позднее проект JEDI был существенно расширен. В его рамках появилось множество подпроектов и групп. Помимо постоянно расширяющейся библиотеки API вы можете обнаружить та- кие проекты, KaKjEDI Visual Component Library (JVCL), JEDI Code Library (JCL — набор полезных функций и невизуальных классов, включая удобное средство сле- жения за необработанными исключениями), а также многочисленные проекты в области графики, мультимедиа и компьютерных игр. Здесь же можно обнару- жить многочисленные инструменты, начиная от клиента JEDI Version Control System и заканчивая комплектом преобразования заголовков DARTH, начиная с редактора для программистов и закачивая электронными обучающими програм- мами. К другим проектам, связанным с JEDI, относятся Indy (о котором уже упоми- налось ранее), Gexperts и Delphree (о них рассказывается далее). G Experts GExperts (www.gexperts.org) — это, наверное, самая богатая коллекция добавлений к Delphi IDE. В эту коллекцию входят многочисленные добавления к Delphi IDE, начиная от многострочной палитры компонентов и заканчивая средствами упро- щения навигации по исходному коду. Коллекция GExperts описывается как «на- бор инструментов, построенных для увеличения производительности программи- стов Delphi и C++ Builder». В эту коллекцию входит огромное количество мастеров, включая Procedure List (список процедур), Clipboard History (история буфера об- мена), Expert Manager, Grep Search (поиск Grep), Grep Regular Expression (регу- лярные выражения Grep), Grep Results (результаты Grep), Message Dialog (диало- 1072
Другие проекты с открытым исходным кодом 1073 говое окно сообщений), Backup Project (резервное копирование проектов), Set Tab Order (настройка порядка вкладок), Clean Directories (очистка каталогов), Favorite Files (часто используемые файлы), Class Browser (браузер классов), Source Export (экспорт исходного кода), Code Librarian (библиотекарь кода), ASCII Chart, РЕ Information, Replace Components (замена компонентов), Component Grid (сетка компонентов), IDE Menu Shortcuts (быстрое обращение к пунктам меню), Project Dependencies (зависимости проектов), Perfect Layout (размещение компонентов), То Do List (список TODO), Code Proofreader (проверка корректности кода), Project Option Sets (наборы параметров проекта) и Components to Code. Del ph гее Существует множество других проектов, имеющих отношение к Delphi, которые разрабатываются в рамках концепции открытого исходного кода. Конечно же, я не могу перечислить здесь абсолютно все такие проекты, однако я могу направить вас на веб-узел, который пытается следить за подобными проектами. Узел Delphree (Delphi Free) располагается по адресу delphree.clexpert.com. На этом узле можно найти огромный список проектов Delphi с открытым ис- ходным кодом, в котором можно обнаружить самые разные продукты, включая библиотеки, инструменты для программистов и прикладные программы для ко- нечных пользователей. DUnit Экстремальное программирование (Extreme Programming)1 — это методика орга- низации труда программистов, которая позволяет существенно повысить произво- дительность и сделать программирование более приятным занятием. Экстремальное программирование состоит из множества практик, одна из которых — тестирова- ние модулей (Unit Testing), или, по-другому, предварительное тестирование (Test First)1 2. В рамках этой практики программист постоянно разрабатывает тестовый код, тестирующий корректность работы разрабатываемой им программы. При этом тестирующий код разрабатывается еще до того, как будет написан функциональ- ный, то есть тестируемый код. Благодаря такому подходу разработка существенно упрощается, а результирующий код получается надежным, элегантным и легко модифицируемым. Однако для разработки тестирующего кода вам потребуется специальная инф- раструктура. Группа программистов Delphi разработала ее для вас. Она называет- ся DUnit и доступна на веб-узле SourceForge по адресу dunit.sourceforge.net. 1 Подробнее об этом в серии книг «Экстремальное программирование», вышедшей в издательстве «Питер» в 2002 - 2003 годах. — Примеч. перев. 2 Подробнее об этом в книге К. Бека «Экстремальное программирование: Разработка через тестирова- ние» («Test-Driven Developement»). — Примеч, перев. 1073
Приложение В Бесплатные сопутствующие книги о Delphi Данная книга является уже седьмым изданием Mastering Delphi, однако помимо нее я написал множество других книг и материалов для обучающих курсов, кон- ференций и лекций. Таким образом, вдобавок к тысяче (или около того) страниц данной книги я могу представить читателям некоторый дополнительный матери- ал, связанный с Delphi. На протяжении последних нескольких лет я преобразовал этот материал в форму электронных книг (в формате HTML или PDF), все они доступны бесплатно на моем веб-узле. В данном приложении я перечислю заго- ловки и содержимое этих книг, а также упомяну ссылки на места, где можно загру- зить эти книги (все они расположены на веб-узле по адресу www.marcocantu.com). Essential Pascal В книге Essential Pascal содержится введение в язык программирования Pascal и описание основных принципов этого языка, который был изобретен профессо- ром Никлаусом Виртом (Nicklaus Wirth) и получил распространение благодаря компании Borland, в 1980-х годах выпустившей в свет всемирноизвестный компи- лятор Turbo Pascal. В 100-страничной книге не содержится описание ООП-рас- ширений этого языка, однако там вы найдете подробное описание многих возможностей, которые были добавлены к основному языку компанией Borland на протяжении десятилетий развития серии компиляторов, выпускаемых этой ком- панией. Далее я привожу перечень глав этой книги, саму книгу можно найти по адресу www. m a rcocantu. com/e pascal. о Глава 1. История языка Pascal. о Глава 2. Кодирование на языке Pascal. О Глава 3. Типы, переменные и константы. О Глава 4. Определенные пользователем типы данных. О Глава 5. Выражения. О Глава 6. Процедуры и функции. 1074
Essential Delphi 1075 О Глава 7. Обработка строк. О Глава 8. Память (и динамические массивы). О Глава 9. Программирование Windows. О Глава 10. Вариантные типы (Variants). О Глава 11. Программы и модули. О Глава 12. Файлы в языке Pascal. О Приложение А. Словарь терминов. о Приложение Б. Примеры. Как вы можете обнаружить, обратившись к моему веб-узлу, эта книга была пе- реведена добровольцами на несколько языков помимо английского. Essential Delphi Вначале книга Mastering Delphi была в большей степени ориентирована на читате- лей, которые никогда ранее не использовали Delphi в качестве визуального инст- румента программирования. Однако с течением времени, от издания к изданию, я старался добавить в книгу описание как можно большего количества новых воз- можностей, добавляемых компанией Borland в эту среду. В результате материал, ориентированный на новичков, постепенно изымался из книги (к сожалению, бу- мажная книга может вместить в себя лишь ограниченное количество текста). Кни- га все в большей степени становилась ориентированной на более-менее опытных программистов Delphi. Однако материал, предназначенный для новичков, не ис- чез бесследно. Я оформил его в виде отдельной электронной книги в формате PDF, которую можно бесплатно загрузить с моего веб-узла по адресу www.marcocantu.com/ edelphi. Книга называется Essential Delphi, она до сих пор находится в стадии разра- ботки, вот ее текущее содержание. О Глава 1. Форма — это окно. О Глава 2. Основные аспекты среды разработки Delphi. О Глава 3. Репозиторий объектов Object Repository и мастеры Delphi. О Глава 4. Обзор базовых компонентов. О Глава 5. Создание и обработка меню. О Глава 6. Мультимедиа. О Глава 7. Сохранение конфигурации: от INI-файлов к реестру. О Глава 8. Подробнее о формах. О Глава 9. Delphi Database 101 (вместе с Paradox). О Глава 10. Печать. О Приложение А. Основы SQL. О Приложение Б. Стандартные свойства VCL. 1075
1076 Приложение В Delphi Power Book Наконец, я собрал некоторый дополнительный материал о некоторых специаль- ных аспектах программирования в среде Delphi в виде дополнительной электрон- ной книги под названием Delphi Power Book. На текущий момент в книгу входит всего четыре главы: «Графика в Delphi*, «Примеры использования интерфейсов*, «COM-расширения графической оболочки* и «Отладка*. Я полагаю, что в будущем эта книга будет существенно расширена. Следите за адресом www.marcocantu.com/delphipowerbook. 1076
Веб-узел автора книги Автор книги, Марко Кэнту (Marco Cantu), создал веб-узел специально для разработ- чиков Delphi, который располагается по адресу www.marcocantu.com. Этот узел явля- ется превосходным источником информации о программировании в среде Delphi. На веб-узле автора книги вы обнаружите: О исходный код рассматриваемых в книге примеров; О дополнительные примеры и советы; О компоненты, мастеры и инструменты, разработанные автором; О электронные книги Essential Pascal, Essential Delphi и др.; О написанные автором статьи о Delphi, C++ njava; О множество ссылок на связанные с Delphi веб-ресурсы и документы; О множество разнообразного материала, связанного с книгами автора, с конфе- ренциями, на которых он выступает, а также с проводимыми им учебными се- минарами. На веб-узле автора можно обнаружить группу новостей, в которой существует специальный раздел, посвященный книгам автора, благодаря этому читатели мо- гут обсудить содержимое книг с автором и между собой. Другие разделы этой груп- пы новостей связаны с программированием в среде Delphi и другими общими воп- росами. Доступ к группе новостей может быть выполнен через веб-интерфейс. 1077
Исходный код примеров книги Где его найти В данной книге обсуждается множество примеров и демонстрационных программ, исходный код которых можно получить бесплатно как на веб-узле автора, так и на веб-узле издателя. На веб-узле издательства Sybex (http://www.sybex.com) в графе Search наберите 4201. Выберите ссылку Mastering Delphi 7. Попав на страницу Mastering Delphi 7, щелкните на ссылке Source Code. Если вы хотите получить исходный код с веб-узла автора, используйте URL- адрес www.marcocantu.com/md7 и изучите ссылки на загружаемые файлы. Имейте в виду, что по этому адресу вы можете читать код примеров, не загружая его на свой компьютер. Код представлен в HTML-формате с синтаксисом, выделенным при помощи цвета. Что в комплекте В файле md7code.zip содержатся все файлы, необходимые для перекомпиляции всех примеров книги. Весь набор файлов разделен на каталоги в соответствии с главами книги, в каждом из таких каталогов содержатся подкаталоги, каждый из которых соответствует конкретному примеру. Для работы некоторых примеров требуются файлы базы данных, веб-сервер с подходящей конфигурацией, сервер InterBase или зарегистрированные в системе специальные СОМ-объекты. Помимо этого в файле md7code.zip содержатся копии всех исходных файлов в формате HTML с синтаксисом, выделенным при помощи цвета. Вы можете начать просмотр этих HTML-файлов с файла index.htm или сразу же перейти к файлу конкретного примера (например, Framel.htm) или к файлу конкретной главы (на- пример, 04.htm). В файле crossref.htm собран алфавитный указатель ссылок на иден- тификаторы, классы и ключевые слова, использованные в файлах примеров. Под- держивается возможность поиска. В комплект входят также готовые к запуску откомпилированные исполняемые файлы. Для запуска этих файлов требуются пакеты времени исполнения. Если у 1078
Исходный код примеров книги 1079 вас на компьютере не установлен программный пакет Delphi 7, для запуска де- монстрационных программ вы должны загрузить файл с пакетами времени ис- полнения. Наконец, автор планирует сделать файлы исходного кода доступными через систему CVS на его собственном веб-узле, благодаря чему читатели смогут обра- щаться к самым последним, самым свежим версиям этих файлов. Обратитесь к веб- узлу автора для получения подробной информации об этой возможности. 1079